commit 07a0553e037796d363fd79d495b086d81482a894 Author: LI-CCONG\李聪聪 <1441652193@qq.com> Date: Wed Aug 9 12:56:17 2023 +0800 project init diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3c200cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sql linguist-language=java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..69aadd0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,51 @@ +###################################################################### +# Build Tools + +.gradle +/build/ +!gradle/wrapper/gradle-wrapper.jar + +target/ +!.mvn/wrapper/maven-wrapper.jar + +###################################################################### +# IDE + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +nbproject/private/ +build/* +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +###################################################################### +# Others +*.log +*.xml.versionsBackup +*.swp + +!*/build/*.java +!*/build/*.html +!*/build/*.xml + +### JRebel ### +rebel.xml + +application-my.yaml + +/yunxi-ui-app/unpackage/ diff --git a/Docker-HOWTO.md b/Docker-HOWTO.md new file mode 100644 index 0000000..aa3ae5f --- /dev/null +++ b/Docker-HOWTO.md @@ -0,0 +1,49 @@ +# Docker Build & Up + +目标: 快速部署体验系统,帮助了解系统之间的依赖关系。 +依赖:docker compose v2,删除`name: yunxi-system`,降低`version`版本为`3.3`以下,支持`docker-compose`。 + +## 功能文件列表 + +```text +. +├── Docker-HOWTO.md +├── docker-compose.yml +├── docker.env <-- 提供docker-compose环境变量配置 +├── yunxi-server +│ └── Dockerfile +└── yunxi-ui-admin + ├── .dockerignore + ├── Dockerfile + └── nginx.conf <-- 提供基础配置,gzip压缩、api转发 +``` + +## 构建 jar 包 + +```shell +# 创建maven缓存volume +docker volume create --name yunxi-maven-repo + +docker run -it --rm --name yunxi-maven \ + -v yunxi-maven-repo:/root/.m2 \ + -v $PWD:/usr/src/mymaven \ + -w /usr/src/mymaven \ + maven mvn clean install package '-Dmaven.test.skip=true' +``` + +## 构建启动服务 + +```shell +docker compose --env-file docker.env up -d +``` + +首次运行会自动构建容器。可以通过`docker compose build [service]`来手动构建所有或某个docker镜像 + +`--env-file docker.env`为可选参数,只是展示了通过`.env`文件配置容器启动的环境变量,`docker-compose.yml`本身已经提供足够的默认参数来正常运行系统。 + +## 服务器的宿主机端口映射 + +- admin ui: http://localhost:8080 +- api server: http://localhost:48080 +- mysql: root/123456, port: 3306 +- redis: port: 6379 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..e7d901f --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,60 @@ +#!groovy +pipeline { + + agent any + + parameters { + string(name: 'TAG_NAME', defaultValue: '', description: '') + } + + environment { + // DockerHub 凭证 ID(登录您的 DockerHub) + DOCKER_CREDENTIAL_ID = 'dockerhub-id' + // GitHub 凭证 ID (推送 tag 到 GitHub 仓库) + GITHUB_CREDENTIAL_ID = 'github-id' + // kubeconfig 凭证 ID (访问接入正在运行的 Kubernetes 集群) + KUBECONFIG_CREDENTIAL_ID = 'demo-kubeconfig' + // 镜像的推送 + REGISTRY = 'docker.io' + // DockerHub 账号名 + DOCKERHUB_NAMESPACE = 'docker_username' + // GitHub 账号名 + GITHUB_ACCOUNT = 'https://gitee.com/zhijiantianya/ruoyi-vue-pro' + // 应用名称 + APP_NAME = 'yunxi-server' + // 应用部署路径 + APP_DEPLOY_BASE_DIR = '/media/pi/KINGTON/data/work/projects/' + } + + stages { + stage('检出') { + steps { + git url: "https://gitee.com/will-we/ruoyi-vue-pro.git", + branch: "devops" + } + } + + stage('构建') { + steps { + // TODO 解决多环境链接、密码不同配置临时方案 + sh 'if [ ! -d "' + "${env.HOME}" + '/resources" ];then\n' + + ' echo "配置文件不存在无需修改"\n' + + 'else\n' + + ' cp -rf ' + "${env.HOME}" + '/resources/*.yaml ' + "${env.APP_NAME}" + '/src/main/resources\n' + + ' echo "配置文件替换"\n' + + 'fi' + sh 'mvn clean package -Dmaven.test.skip=true' + } + } + + stage('部署') { + steps { + sh 'cp -f ' + ' bin/deploy.sh ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + sh 'cp -f ' + "${env.APP_NAME}" + '/target/*.jar ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" +'/build/' + archiveArtifacts "${env.APP_NAME}" + '/target/*.jar' + sh 'chmod +x ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh' + sh 'bash ' + "${env.APP_DEPLOY_BASE_DIR}" + "${env.APP_NAME}" + '/deploy.sh' + } + } + } +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd9da62 --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2021 ruoyi-vue-pro + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d86cd4 --- /dev/null +++ b/README.md @@ -0,0 +1,333 @@ +**严肃声明:现在、未来都不会有商业版本,所有代码全部开源!!** + +**「我喜欢写代码,乐此不疲」** +**「我喜欢做开源,以此为乐」** + +我 🐶 在上海艰苦奋斗,早中晚在 top3 大厂认真搬砖,夜里为开源做贡献。 + +如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。 + +## 🐶 新手必读 + +* 演示地址【Vue3 + element-plus】: +* 演示地址【Vue3 + vben(ant-design-vue)】: +* 演示地址【Vue2 + element-ui】: +* 启动文档: +* 视频教程: + +## 🐯 平台简介 + +**芋道**,以开发者为中心,打造中国第一流的快速开发平台,全部开源,个人与企业可 100% 免费使用。 + +> 有任何问题,或者想要的功能,可以在 _Issues_ 中提给艿艿。 +> +> 😜 给项目点点 Star 吧,这对我们真的很重要! + +![架构图](https://static.iocoder.cn/ruoyi-vue-pro-architecture.png?imageView2/2/format/webp) + +* 管理后台的电脑端:Vue3 提供 [element-plus](https://gitee.com/yunxicode/yunxi-ui-admin-vue3)、[vben(ant-design-vue)](https://gitee.com/yunxicode/yunxi-ui-admin-vben) 两个版本,Vue2 提供 [element-ui](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/yunxi-ui-admin) 版本 +* 管理后台的移动端:采用 [uni-app](https://github.com/dcloudio/uni-app) 方案,一份代码多终端适配,同时支持 APP、小程序、H5! +* 后端采用 Spring Boot 多模块架构、MySQL + MyBatis Plus、Redis + Redisson +* 数据库可使用 MySQL、Oracle、PostgreSQL、SQL Server、MariaDB、国产达梦 DM、TiDB 等 +* 权限认证使用 Spring Security & Token & Redis,支持多终端、多种用户的认证系统,支持 SSO 单点登录 +* 支持加载动态权限菜单,按钮级别权限控制,本地缓存提升性能 +* 支持 SaaS 多租户,可自定义每个租户的权限,提供透明化的多租户底层封装 +* 工作流使用 Flowable,支持动态表单、在线设计流程、会签 / 或签、多种任务分配方式 +* 高效率开发,使用代码生成器可以一键生成前后端代码 + 单元测试 + Swagger 接口文档 + Validator 参数校验 +* 集成微信小程序、微信公众号、企业微信、钉钉等三方登陆,集成支付宝、微信等支付与退款 +* 集成阿里云、腾讯云等短信渠道,集成 MinIO、阿里云、腾讯云、七牛云等云存储服务 +* 集成报表设计器、大屏设计器,通过拖拽即可生成酷炫的报表与大屏 + +## 🐳 项目关系 + +![架构演进](https://static.iocoder.cn/yunxi-roadmap.png?imageView2/2/format/webp) + +三个项目的功能对比,可见社区共同整理的 [国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn) 表格。 + +### 后端项目 + + +| 项目 | Star | 简介 | +|-----------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------| +| [ruoyi-vue-pro](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro) | 基于 Spring Boot 多模块架构 | +| [yunxi-cloud](https://gitee.com/zhijiantianya/yunxi-cloud) | [![Gitee star](https://gitee.com/zhijiantianya/yunxi-cloud/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/yunxi-cloud) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/yunxi-cloud.svg?style=social&label=Stars)](https://github.com/YunaiV/yunxi-cloud) | 基于 Spring Cloud 微服务架构 | +| [Spring-Boot-Labs](https://gitee.com/yunxicode/SpringBoot-Labs) | [![Gitee star](https://gitee.com/yunxicode/SpringBoot-Labs/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/yunxi-cloud) [![GitHub stars](https://img.shields.io/github/stars/yunxicode/SpringBoot-Labs.svg?style=social&label=Stars)](https://github.com/yunxicode/SpringBoot-Labs) | 系统学习 Spring Boot & Cloud 专栏 | + +### 前端项目 + +| 项目 | Star | 简介 | +|----------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------| +| [yunxi-ui-admin-vue3](https://gitee.com/yunxicode/yunxi-ui-admin-vue3) | [![Gitee star](https://gitee.com/yunxicode/yunxi-ui-admin-vue3/badge/star.svg?theme=white)](https://gitee.com/yunxicode/yunxi-ui-admin-vue3) [![GitHub stars](https://img.shields.io/github/stars/yunxicode/yunxi-ui-admin-vue3.svg?style=social&label=Stars)](https://github.com/yunxicode/yunxi-ui-admin-vue3) | 基于 Vue3 + element-plus 实现的管理后台 | +| [yunxi-ui-admin-vben](https://gitee.com/yunxicode/yunxi-ui-admin-vben) | [![Gitee star](https://gitee.com/yunxicode/yunxi-ui-admin-vben/badge/star.svg?theme=white)](https://gitee.com/yunxicode/yunxi-ui-admin-vben) [![GitHub stars](https://img.shields.io/github/stars/yunxicode/yunxi-ui-admin-vben.svg?style=social&label=Stars)](https://github.com/yunxicode/yunxi-ui-admin-vben) | 基于 Vue3 + vben(ant-design-vue) 实现的管理后台 | +| [yunxi-ui-admin](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/yunxi-ui-admin) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/yunxi-ui-admin) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro/tree/master/yunxi-ui-admin) | 基于 Vue2 + element-ui 实现的管理后台 | +| [yunxi-ui-admin-uniapp](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/yunxi-ui-admin-uniapp) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/yunxi-ui-admin-uniapp) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro/tree/master/yunxi-ui-admin-uniapp) | 基于 uni-app + uni-ui 实现的管理后台的小程序 | +| [yunxi-ui-go-view](https://gitee.com/yunxicode/yunxi-ui-go-view) | [![Gitee star](https://gitee.com/yunxicode/yunxi-ui-go-view/badge/star.svg?theme=white)](https://gitee.com/yunxicode/yunxi-ui-go-view) [![GitHub stars](https://img.shields.io/github/stars/yunxicode/yunxi-ui-go-view.svg?style=social&label=Stars)](https://github.com/yunxicode/yunxi-ui-go-view) | 基于 Vue3 + naive-ui 实现的大屏报表 | +| [yunxi-ui-app](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/yunxi-ui-app) | [![Gitee star](https://gitee.com/zhijiantianya/ruoyi-vue-pro/badge/star.svg?theme=white)](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/master/yunxi-ui-app) [![GitHub stars](https://img.shields.io/github/stars/YunaiV/ruoyi-vue-pro.svg?style=social&label=Stars)](https://github.com/YunaiV/ruoyi-vue-pro/tree/master/yunxi-ui-app) | 基于 uni-app + uview 实现的用户 App | + +## 🐰 分支说明 + +| | JDK 8 完整版 | JDK 8 精简版 | JDK 17 完整版 | +|-------|-----------------------------------------------------------|--------------------------------------------------------------------|-----------------------------------------------------------------------------| +| 分支 | [`master`](https://gitee.com/zhijiantianya/ruoyi-vue-pro) | [`mini`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/mini/) | [`boot-dev`](https://gitee.com/zhijiantianya/ruoyi-vue-pro/tree/boot3-dev/) | +| 说明 | 包括所有功能 | 只保留核心功能 | 适配 Spring Boot 3.X | +| 系统功能 | √ | √ | √ | +| 基础设施 | √ | √ | √ | +| 会员中心 | √ | √ | √ | +| 工作流程 | √ | x | 适配中 | +| 数据报表 | √ | x | 适配中 | +| 商城系统 | √ | x | √ | +| 微信公众号 | √ | x | √ | + +## 😎 开源协议 + +**为什么推荐使用本项目?** + +① 本项目采用比 Apache 2.0 更宽松的 [MIT License](https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE) 开源协议,个人与企业可 100% 免费使用,不用保留类作者、Copyright 信息。 + +② 代码全部开源,不会像其他项目一样,只开源部分代码,让你无法了解整个项目的架构设计。[国产开源项目对比](https://www.yuque.com/xiatian-bsgny/lm0ec1/wqf8mn) + +![开源项目对比](https://static.iocoder.cn/project-vs.png?imageView2/2/format/webp/w/1280) + +③ 代码整洁、架构整洁,遵循《阿里巴巴 Java 开发手册》规范,代码注释详细,57000 行 Java 代码,22000 行代码注释。 + +## 🤝 项目外包 + +我们也是接外包滴,如果你有项目想要外包,可以微信联系【**Aix9975**】。 + +团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。 + +项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。 + +## 🐼 内置功能 + +系统内置多种多种业务功能,可以用于快速你的业务系统: + +![功能分层](https://static.iocoder.cn/ruoyi-vue-pro-biz.png?imageView2/2/format/webp) + +* 系统功能 +* 基础设施 +* 工作流程 +* 支付系统 +* 会员中心 +* 数据报表 +* 商城系统 +* 微信公众号 + +> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 +> +> * 额外新增的功能,我们使用 🚀 标记。 +> * 重新实现的功能,我们使用 ⭐️ 标记。 + +🙂 所有功能,都通过 **单元测试** 保证高质量。 + +### 系统功能 + +| | 功能 | 描述 | +|-----|-------|---------------------------------| +| | 用户管理 | 用户是系统操作者,该功能主要完成系统用户配置 | +| ⭐️ | 在线用户 | 当前系统中活跃用户状态监控,支持手动踢下线 | +| | 角色管理 | 角色菜单权限分配、设置角色按机构进行数据范围权限划分 | +| | 菜单管理 | 配置系统菜单、操作权限、按钮权限标识等,本地缓存提供性能 | +| | 部门管理 | 配置系统组织机构(公司、部门、小组),树结构展现支持数据权限 | +| | 岗位管理 | 配置系统用户所属担任职务 | +| 🚀 | 租户管理 | 配置系统租户,支持 SaaS 场景下的多租户功能 | +| 🚀 | 租户套餐 | 配置租户套餐,自定每个租户的菜单、操作、按钮的权限 | +| | 字典管理 | 对系统中经常使用的一些较为固定的数据进行维护 | +| 🚀 | 短信管理 | 短信渠道、短息模板、短信日志,对接阿里云、腾讯云等主流短信平台 | +| 🚀 | 邮件管理 | 邮箱账号、邮件模版、邮件发送日志,支持所有邮件平台 | +| 🚀 | 站内信 | 系统内的消息通知,提供站内信模版、站内信消息 | +| 🚀 | 操作日志 | 系统正常操作日志记录和查询,集成 Swagger 生成日志内容 | +| ⭐️ | 登录日志 | 系统登录日志记录查询,包含登录异常 | +| 🚀 | 错误码管理 | 系统所有错误码的管理,可在线修改错误提示,无需重启服务 | +| | 通知公告 | 系统通知公告信息发布维护 | +| 🚀 | 敏感词 | 配置系统敏感词,支持标签分组 | +| 🚀 | 应用管理 | 管理 SSO 单点登录的应用,支持多种 OAuth2 授权方式 | +| 🚀 | 地区管理 | 展示省份、城市、区镇等城市信息,支持 IP 对应城市 | + +### 工作流程 + +| | 功能 | 描述 | +|-----|-------|----------------------------------------| +| 🚀 | 流程模型 | 配置工作流的流程模型,支持文件导入与在线设计流程图,提供 7 种任务分配规则 | +| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 | +| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 | +| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 | +| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转发、委派、退回等操作 | +| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,未来会支持回退操作 | +| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 | + +### 支付系统 + +| | 功能 | 描述 | +|-----|------|---------------------------| +| 🚀 | 应用信息 | 配置商户的应用信息,对接支付宝、微信等多个支付渠道 | +| 🚀 | 支付订单 | 查看用户发起的支付宝、微信等的【支付】订单 | +| 🚀 | 退款订单 | 查看用户发起的支付宝、微信等的【退款】订单 | +| 🚀 | 回调通知 | 查看支付回调业务的【支付】【退款】的通知结果 | +| 🚀 | 接入示例 | 提供接入支付系统的【支付】【退款】的功能实战 | + +### 基础设施 + +| | 功能 | 描述 | +|-----|----------|----------------------------------------------| +| 🚀 | 代码生成 | 前后端代码的生成(Java、Vue、SQL、单元测试),支持 CRUD 下载 | +| 🚀 | 系统接口 | 基于 Swagger 自动生成相关的 RESTful API 接口文档 | +| 🚀 | 数据库文档 | 基于 Screw 自动生成数据库文档,支持导出 Word、HTML、MD 格式 | +| | 表单构建 | 拖动表单元素生成相应的 HTML 代码,支持导出 JSON、Vue 文件 | +| 🚀 | 配置管理 | 对系统动态配置常用参数,支持 SpringBoot 加载 | +| ⭐️ | 定时任务 | 在线(添加、修改、删除)任务调度包含执行结果日志 | +| 🚀 | 文件服务 | 支持将文件存储到 S3(MinIO、阿里云、腾讯云、七牛云)、本地、FTP、数据库等 | +| 🚀 | API 日志 | 包括 RESTful API 访问日志、异常日志两部分,方便排查 API 相关的问题 | +| | MySQL 监控 | 监视当前系统数据库连接池状态,可进行分析SQL找出系统性能瓶颈 | +| | Redis 监控 | 监控 Redis 数据库的使用情况,使用的 Redis Key 管理 | +| 🚀 | 消息队列 | 基于 Redis 实现消息队列,Stream 提供集群消费,Pub/Sub 提供广播消费 | +| 🚀 | Java 监控 | 基于 Spring Boot Admin 实现 Java 应用的监控 | +| 🚀 | 链路追踪 | 接入 SkyWalking 组件,实现链路追踪 | +| 🚀 | 日志中心 | 接入 SkyWalking 组件,实现日志中心 | +| 🚀 | 分布式锁 | 基于 Redis 实现分布式锁,满足并发场景 | +| 🚀 | 幂等组件 | 基于 Redis 实现幂等组件,解决重复请求问题 | +| 🚀 | 服务保障 | 基于 Resilience4j 实现服务的稳定性,包括限流、熔断等功能 | +| 🚀 | 日志服务 | 轻量级日志中心,查看远程服务器的日志 | +| 🚀 | 单元测试 | 基于 JUnit + Mockito 实现单元测试,保证功能的正确性、代码的质量等 | + +### 数据报表 + +| | 功能 | 描述 | +|-----|-------|--------------------| +| 🚀 | 报表设计器 | 支持数据报表、图形报表、打印设计等 | +| 🚀 | 大屏设计器 | 拖拽生成数据大屏,内置几十种图表组件 | + +### 微信公众号 + +| | 功能 | 描述 | +|-----|--------|-------------------------------| +| 🚀 | 账号管理 | 配置接入的微信公众号,可支持多个公众号 | +| 🚀 | 数据统计 | 统计公众号的用户增减、累计用户、消息概况、接口分析等数据 | +| 🚀 | 粉丝管理 | 查看已关注、取关的粉丝列表,可对粉丝进行同步、打标签等操作 | +| 🚀 | 消息管理 | 查看粉丝发送的消息列表,可主动回复粉丝消息 | +| 🚀 | 自动回复 | 自动回复粉丝发送的消息,支持关注回复、消息回复、关键字回复 | +| 🚀 | 标签管理 | 对公众号的标签进行创建、查询、修改、删除等操作 | +| 🚀 | 菜单管理 | 自定义公众号的菜单,也可以从公众号同步菜单 | +| 🚀 | 素材管理 | 管理公众号的图片、语音、视频等素材,支持在线播放语音、视频 | +| 🚀 | 图文草稿箱 | 新增常用的图文素材到草稿箱,可发布到公众号 | +| 🚀 | 图文发表记录 | 查看已发布成功的图文素材,支持删除操作 | + +### 商城系统 + +建设中... + +![功能图](http://static.iocoder.cn/mall%20%E5%8A%9F%E8%83%BD%E5%9B%BE-min.png) + +![GIF 图-耐心等待](https://raw.githubusercontent.com/YunaiV/Blog/master/Mall/onemall-admin-min.gif) + +![GIF 图-耐心等待](https://raw.githubusercontent.com/YunaiV/Blog/master/Mall/onemall-h5-min.gif) + +### 会员中心 + +和「商城系统」一起开发 + +## 🐨 技术栈 + +### 模块 + +| 项目 | 说明 | +|--------------------------------------------------------------------------|--------------------| +| `yunxi-dependencies` | Maven 依赖版本管理 | +| `yunxi-framework` | Java 框架拓展 | +| `yunxi-server` | 管理后台 + 用户 APP 的服务端 | +| `yunxi-module-system` | 系统功能的 Module 模块 | +| `yunxi-module-member` | 会员中心的 Module 模块 | +| `yunxi-module-infra` | 基础设施的 Module 模块 | +| `yunxi-module-bpm` | 工作流程的 Module 模块 | +| `yunxi-module-pay` | 支付系统的 Module 模块 | +| `yunxi-module-mall` | 商城系统的 Module 模块 | +| `yunxi-module-mp` | 微信公众号的 Module 模块 | +| `yunxi-module-report` | 大屏报表 Module 模块 | + +### 框架 + +| 框架 | 说明 | 版本 | 学习指南 | +|---------------------------------------------------------------------------------------------|------------------|-------------|----------------------------------------------------------------| +| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 2.7.13 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | +| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | | +| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.18 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yunxi) | +| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.3.1 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yunxi) | +| [Dynamic Datasource](https://dynamic-datasource.com/) | 动态数据源 | 3.6.1 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yunxi) | +| [Redis](https://redis.io/) | key-value 数据库 | 5.0 / 6.0 | | +| [Redisson](https://github.com/redisson/redisson) | Redis 客户端 | 3.18.0 | [文档](http://www.iocoder.cn/Spring-Boot/Redis/?yunxi) | +| [Spring MVC](https://github.com/spring-projects/spring-framework/tree/master/spring-webmvc) | MVC 框架 | 5.3.24 | [文档](http://www.iocoder.cn/SpringMVC/MVC/?yunxi) | +| [Spring Security](https://github.com/spring-projects/spring-security) | Spring 安全框架 | 5.7.6 | [文档](http://www.iocoder.cn/Spring-Boot/Spring-Security/?yunxi) | +| [Hibernate Validator](https://github.com/hibernate/hibernate-validator) | 参数校验组件 | 6.2.5 | [文档](http://www.iocoder.cn/Spring-Boot/Validation/?yunxi) | +| [Flowable](https://github.com/flowable/flowable-engine) | 工作流引擎 | 6.8.0 | [文档](https://doc.iocoder.cn/bpm/) | +| [Quartz](https://github.com/quartz-scheduler) | 任务调度组件 | 2.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Job/?yunxi) | +| [Springdoc](https://springdoc.org/) | Swagger 文档 | 1.6.15 | [文档](http://www.iocoder.cn/Spring-Boot/Swagger/?yunxi) | +| [Resilience4j](https://github.com/resilience4j/resilience4j) | 服务保障组件 | 1.7.1 | [文档](http://www.iocoder.cn/Spring-Boot/Resilience4j/?yunxi) | +| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 8.12.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yunxi) | +| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 2.7.10 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yunxi) | +| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.13.3 | | +| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.5.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yunxi) | +| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.28 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yunxi) | +| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.8.2 | - | +| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 4.8.0 | - | + +## 🐷 演示图 + +### 系统功能 + +| 模块 | biu | biu | biu | +|------------|--------------------------------------------------------------------|------------------------------------------------------------------|------------------------------------------------------------------| +| 登录 & 首页 | ![登录](https://static.iocoder.cn/images/ruoyi-vue-pro/登录.jpg?imageView2/2/format/webp/w/1280) | ![首页](https://static.iocoder.cn/images/ruoyi-vue-pro/首页.jpg?imageView2/2/format/webp/w/1280) | ![个人中心](https://static.iocoder.cn/images/ruoyi-vue-pro/个人中心.jpg?imageView2/2/format/webp/w/1280) | +| 用户 & 应用 | ![用户管理](https://static.iocoder.cn/images/ruoyi-vue-pro/用户管理.jpg?imageView2/2/format/webp/w/1280) | ![令牌管理](https://static.iocoder.cn/images/ruoyi-vue-pro/令牌管理.jpg?imageView2/2/format/webp/w/1280) | ![应用管理](https://static.iocoder.cn/images/ruoyi-vue-pro/应用管理.jpg?imageView2/2/format/webp/w/1280) | +| 租户 & 套餐 | ![租户管理](https://static.iocoder.cn/images/ruoyi-vue-pro/租户管理.jpg?imageView2/2/format/webp/w/1280) | ![租户套餐](https://static.iocoder.cn/images/ruoyi-vue-pro/租户套餐.png) | - | +| 部门 & 岗位 | ![部门管理](https://static.iocoder.cn/images/ruoyi-vue-pro/部门管理.jpg?imageView2/2/format/webp/w/1280) | ![岗位管理](https://static.iocoder.cn/images/ruoyi-vue-pro/岗位管理.jpg?imageView2/2/format/webp/w/1280) | - | +| 菜单 & 角色 | ![菜单管理](https://static.iocoder.cn/images/ruoyi-vue-pro/菜单管理.jpg?imageView2/2/format/webp/w/1280) | ![角色管理](https://static.iocoder.cn/images/ruoyi-vue-pro/角色管理.jpg?imageView2/2/format/webp/w/1280) | - | +| 审计日志 | ![操作日志](https://static.iocoder.cn/images/ruoyi-vue-pro/操作日志.jpg?imageView2/2/format/webp/w/1280) | ![登录日志](https://static.iocoder.cn/images/ruoyi-vue-pro/登录日志.jpg?imageView2/2/format/webp/w/1280) | - | +| 短信 | ![短信渠道](https://static.iocoder.cn/images/ruoyi-vue-pro/短信渠道.jpg?imageView2/2/format/webp/w/1280) | ![短信模板](https://static.iocoder.cn/images/ruoyi-vue-pro/短信模板.jpg?imageView2/2/format/webp/w/1280) | ![短信日志](https://static.iocoder.cn/images/ruoyi-vue-pro/短信日志.jpg?imageView2/2/format/webp/w/1280) | +| 字典 & 敏感词 | ![字典类型](https://static.iocoder.cn/images/ruoyi-vue-pro/字典类型.jpg?imageView2/2/format/webp/w/1280) | ![字典数据](https://static.iocoder.cn/images/ruoyi-vue-pro/字典数据.jpg?imageView2/2/format/webp/w/1280) | ![敏感词](https://static.iocoder.cn/images/ruoyi-vue-pro/敏感词.jpg?imageView2/2/format/webp/w/1280) | +| 错误码 & 通知 | ![错误码管理](https://static.iocoder.cn/images/ruoyi-vue-pro/错误码管理.jpg?imageView2/2/format/webp/w/1280) | ![通知公告](https://static.iocoder.cn/images/ruoyi-vue-pro/通知公告.jpg?imageView2/2/format/webp/w/1280) | - | + +### 工作流程 + +| 模块 | biu | biu | biu | +|---------|------------------------------------------------------------------------|------------------------------------------------------------------------|------------------------------------------------------------------------| +| 流程模型 | ![流程模型-列表](https://static.iocoder.cn/images/ruoyi-vue-pro/流程模型-列表.jpg?imageView2/2/format/webp/w/1280) | ![流程模型-设计](https://static.iocoder.cn/images/ruoyi-vue-pro/流程模型-设计.jpg?imageView2/2/format/webp/w/1280) | ![流程模型-定义](https://static.iocoder.cn/images/ruoyi-vue-pro/流程模型-定义.jpg?imageView2/2/format/webp/w/1280) | +| 表单 & 分组 | ![流程表单](https://static.iocoder.cn/images/ruoyi-vue-pro/流程表单.jpg?imageView2/2/format/webp/w/1280) | ![用户分组](https://static.iocoder.cn/images/ruoyi-vue-pro/用户分组.jpg?imageView2/2/format/webp/w/1280) | - | +| 我的流程 | ![我的流程-列表](https://static.iocoder.cn/images/ruoyi-vue-pro/我的流程-列表.jpg?imageView2/2/format/webp/w/1280) | ![我的流程-发起](https://static.iocoder.cn/images/ruoyi-vue-pro/我的流程-发起.jpg?imageView2/2/format/webp/w/1280) | ![我的流程-详情](https://static.iocoder.cn/images/ruoyi-vue-pro/我的流程-详情.jpg?imageView2/2/format/webp/w/1280) | +| 待办 & 已办 | ![任务列表-审批](https://static.iocoder.cn/images/ruoyi-vue-pro/任务列表-审批.jpg?imageView2/2/format/webp/w/1280) | ![任务列表-待办](https://static.iocoder.cn/images/ruoyi-vue-pro/任务列表-待办.jpg?imageView2/2/format/webp/w/1280) | ![任务列表-已办](https://static.iocoder.cn/images/ruoyi-vue-pro/任务列表-已办.jpg?imageView2/2/format/webp/w/1280) | +| OA 请假 | ![OA请假-列表](https://static.iocoder.cn/images/ruoyi-vue-pro/OA请假-列表.jpg?imageView2/2/format/webp/w/1280) | ![OA请假-发起](https://static.iocoder.cn/images/ruoyi-vue-pro/OA请假-发起.jpg?imageView2/2/format/webp/w/1280) | ![OA请假-详情](https://static.iocoder.cn/images/ruoyi-vue-pro/OA请假-详情.jpg?imageView2/2/format/webp/w/1280) | + +### 基础设施 + +| 模块 | biu | biu | biu | +|---------------|----------------------------------------------------------------------|--------------------------------------------------------------------|------------------------------------------------------------------| +| 代码生成 | ![代码生成](https://static.iocoder.cn/images/ruoyi-vue-pro/代码生成.jpg?imageView2/2/format/webp/w/1280) | ![生成效果](https://static.iocoder.cn/images/ruoyi-vue-pro/生成效果.jpg?imageView2/2/format/webp/w/1280) | - | +| 文档 | ![系统接口](https://static.iocoder.cn/images/ruoyi-vue-pro/系统接口.jpg?imageView2/2/format/webp/w/1280) | ![数据库文档](https://static.iocoder.cn/images/ruoyi-vue-pro/数据库文档.jpg?imageView2/2/format/webp/w/1280) | - | +| 文件 & 配置 | ![文件配置](https://static.iocoder.cn/images/ruoyi-vue-pro/文件配置.jpg?imageView2/2/format/webp/w/1280) | ![文件管理](https://static.iocoder.cn/images/ruoyi-vue-pro/文件管理2.jpg?imageView2/2/format/webp/w/1280) | ![配置管理](https://static.iocoder.cn/images/ruoyi-vue-pro/配置管理.jpg?imageView2/2/format/webp/w/1280) | +| 定时任务 | ![定时任务](https://static.iocoder.cn/images/ruoyi-vue-pro/定时任务.jpg?imageView2/2/format/webp/w/1280) | ![任务日志](https://static.iocoder.cn/images/ruoyi-vue-pro/任务日志.jpg?imageView2/2/format/webp/w/1280) | - | +| API 日志 | ![访问日志](https://static.iocoder.cn/images/ruoyi-vue-pro/访问日志.jpg?imageView2/2/format/webp/w/1280) | ![错误日志](https://static.iocoder.cn/images/ruoyi-vue-pro/错误日志.jpg?imageView2/2/format/webp/w/1280) | - | +| MySQL & Redis | ![MySQL](https://static.iocoder.cn/images/ruoyi-vue-pro/MySQL.jpg?imageView2/2/format/webp/w/1280) | ![Redis](https://static.iocoder.cn/images/ruoyi-vue-pro/Redis.jpg?imageView2/2/format/webp/w/1280) | - | +| 监控平台 | ![Java监控](https://static.iocoder.cn/images/ruoyi-vue-pro/Java监控.jpg?imageView2/2/format/webp/w/1280) | ![链路追踪](https://static.iocoder.cn/images/ruoyi-vue-pro/链路追踪.jpg?imageView2/2/format/webp/w/1280) | ![日志中心](https://static.iocoder.cn/images/ruoyi-vue-pro/日志中心.jpg?imageView2/2/format/webp/w/1280) | + +### 支付系统 + +| 模块 | biu | biu | biu | +|---------|------------------------------------------------------------------|------------------------------------------------------------------------|------------------------------------------------------------------------| +| 商家 & 应用 | ![商户信息](https://static.iocoder.cn/images/ruoyi-vue-pro/商户信息.jpg?imageView2/2/format/webp/w/1280) | ![应用信息-列表](https://static.iocoder.cn/images/ruoyi-vue-pro/应用信息-列表.jpg?imageView2/2/format/webp/w/1280) | ![应用信息-编辑](https://static.iocoder.cn/images/ruoyi-vue-pro/应用信息-编辑.jpg?imageView2/2/format/webp/w/1280) | +| 支付 & 退款 | ![支付订单](https://static.iocoder.cn/images/ruoyi-vue-pro/支付订单.jpg?imageView2/2/format/webp/w/1280) | ![退款订单](https://static.iocoder.cn/images/ruoyi-vue-pro/退款订单.jpg?imageView2/2/format/webp/w/1280) | --- | + +### 数据报表 + +| 模块 | biu | biu | biu | +|-------|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------| +| 报表设计器 | ![数据报表](https://static.iocoder.cn/images/ruoyi-vue-pro/报表设计器-数据报表.jpg?imageView2/2/format/webp/w/1280) | ![图形报表](https://static.iocoder.cn/images/ruoyi-vue-pro/报表设计器-图形报表.jpg?imageView2/2/format/webp/w/1280) | ![报表设计器-打印设计](https://static.iocoder.cn/images/ruoyi-vue-pro/报表设计器-打印设计.jpg?imageView2/2/format/webp/w/1280) | +| 大屏设计器 | ![大屏列表](https://static.iocoder.cn/images/ruoyi-vue-pro/大屏设计器-列表.jpg?imageView2/2/format/webp/w/1280) | ![大屏预览](https://static.iocoder.cn/images/ruoyi-vue-pro/大屏设计器-预览.jpg?imageView2/2/format/webp/w/1280) | ![大屏编辑](https://static.iocoder.cn/images/ruoyi-vue-pro/大屏设计器-编辑.jpg?imageView2/2/format/webp/w/1280) | + +### 移动端(管理后台) + +| biu | biu | biu | +|------------------------------------------------------------------|------------------------------------------------------------------------|------------------------------------------------------------------------| +| ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/01.png?imageView2/2/format/webp) | ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/02.png?imageView2/2/format/webp) | ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/03.png?imageView2/2/format/webp) | +| ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/04.png?imageView2/2/format/webp) | ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/05.png?imageView2/2/format/webp) | ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/06.png?imageView2/2/format/webp) | +| ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/07.png?imageView2/2/format/webp) | ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/08.png?imageView2/2/format/webp) | ![](https://static.iocoder.cn/images/ruoyi-vue-pro/admin-uniapp/09.png?imageView2/2/format/webp) | + +目前已经实现登录、我的、工作台、编辑资料、头像修改、密码修改、常见问题、关于我们等基础功能。 diff --git a/bin/deploy.sh b/bin/deploy.sh new file mode 100644 index 0000000..2c71541 --- /dev/null +++ b/bin/deploy.sh @@ -0,0 +1,160 @@ +#!/bin/bash +set -e + +DATE=$(date +%Y%m%d%H%M) +# 基础路径 +BASE_PATH=/work/projects/yunxi-server +# 编译后 jar 的地址。部署时,Jenkins 会上传 jar 包到该目录下 +SOURCE_PATH=$BASE_PATH/build +# 服务名称。同时约定部署服务的 jar 包名字也为它。 +SERVER_NAME=yunxi-server +# 环境 +PROFILES_ACTIVE=development +# 健康检查 URL +HEALTH_CHECK_URL=http://127.0.0.1:48080/actuator/health/ + +# heapError 存放路径 +HEAP_ERROR_PATH=$BASE_PATH/heapError +# JVM 参数 +JAVA_OPS="-Xms512m -Xmx512m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$HEAP_ERROR_PATH" + +# SkyWalking Agent 配置 +#export SW_AGENT_NAME=$SERVER_NAME +#export SW_AGENT_COLLECTOR_BACKEND_SERVICES=192.168.0.84:11800 +#export SW_GRPC_LOG_SERVER_HOST=192.168.0.84 +#export SW_AGENT_TRACE_IGNORE_PATH="Redisson/PING,/actuator/**,/admin/**" +#export JAVA_AGENT=-javaagent:/work/skywalking/apache-skywalking-apm-bin/agent/skywalking-agent.jar + +# 备份 +function backup() { + # 如果不存在,则无需备份 + if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then + echo "[backup] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过备份" + # 如果存在,则备份到 backup 目录下,使用时间作为后缀 + else + echo "[backup] 开始备份 $SERVER_NAME ..." + cp $BASE_PATH/$SERVER_NAME.jar $BASE_PATH/backup/$SERVER_NAME-$DATE.jar + echo "[backup] 备份 $SERVER_NAME 完成" + fi +} + +# 最新构建代码 移动到项目环境 +function transfer() { + echo "[transfer] 开始转移 $SERVER_NAME.jar" + + # 删除原 jar 包 + if [ ! -f "$BASE_PATH/$SERVER_NAME.jar" ]; then + echo "[transfer] $BASE_PATH/$SERVER_NAME.jar 不存在,跳过删除" + else + echo "[transfer] 移除 $BASE_PATH/$SERVER_NAME.jar 完成" + rm $BASE_PATH/$SERVER_NAME.jar + fi + + # 复制新 jar 包 + echo "[transfer] 从 $SOURCE_PATH 中获取 $SERVER_NAME.jar 并迁移至 $BASE_PATH ...." + cp $SOURCE_PATH/$SERVER_NAME.jar $BASE_PATH + + echo "[transfer] 转移 $SERVER_NAME.jar 完成" +} + +# 停止:优雅关闭之前已经启动的服务 +function stop() { + echo "[stop] 开始停止 $BASE_PATH/$SERVER_NAME" + PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') + # 如果 Java 服务启动中,则进行关闭 + if [ -n "$PID" ]; then + # 正常关闭 + echo "[stop] $BASE_PATH/$SERVER_NAME 运行中,开始 kill [$PID]" + kill -15 $PID + # 等待最大 120 秒,直到关闭完成。 + for ((i = 0; i < 120; i++)) + do + sleep 1 + PID=$(ps -ef | grep $BASE_PATH/$SERVER_NAME | grep -v "grep" | awk '{print $2}') + if [ -n "$PID" ]; then + echo -e ".\c" + else + echo '[stop] 停止 $BASE_PATH/$SERVER_NAME 成功' + break + fi + done + + # 如果正常关闭失败,那么进行强制 kill -9 进行关闭 + if [ -n "$PID" ]; then + echo "[stop] $BASE_PATH/$SERVER_NAME 失败,强制 kill -9 $PID" + kill -9 $PID + fi + # 如果 Java 服务未启动,则无需关闭 + else + echo "[stop] $BASE_PATH/$SERVER_NAME 未启动,无需停止" + fi +} + +# 启动:启动后端项目 +function start() { + # 开启启动前,打印启动参数 + echo "[start] 开始启动 $BASE_PATH/$SERVER_NAME" + echo "[start] JAVA_OPS: $JAVA_OPS" + echo "[start] JAVA_AGENT: $JAVA_AGENT" + echo "[start] PROFILES: $PROFILES_ACTIVE" + + # 开始启动 + BUILD_ID=dontKillMe nohup java -server $JAVA_OPS $JAVA_AGENT -jar $BASE_PATH/$SERVER_NAME.jar --spring.profiles.active=$PROFILES_ACTIVE & + echo "[start] 启动 $BASE_PATH/$SERVER_NAME 完成" +} + +# 健康检查:自动判断后端项目是否正常启动 +function healthCheck() { + # 如果配置健康检查,则进行健康检查 + if [ -n "$HEALTH_CHECK_URL" ]; then + # 健康检查最大 120 秒,直到健康检查通过 + echo "[healthCheck] 开始通过 $HEALTH_CHECK_URL 地址,进行健康检查"; + for ((i = 0; i < 120; i++)) + do + # 请求健康检查地址,只获取状态码。 + result=`curl -I -m 10 -o /dev/null -s -w %{http_code} $HEALTH_CHECK_URL || echo "000"` + # 如果状态码为 200,则说明健康检查通过 + if [ "$result" == "200" ]; then + echo "[healthCheck] 健康检查通过"; + break + # 如果状态码非 200,则说明未通过。sleep 1 秒后,继续重试 + else + echo -e ".\c" + sleep 1 + fi + done + + # 健康检查未通过,则异常退出 shell 脚本,不继续部署。 + if [ ! "$result" == "200" ]; then + echo "[healthCheck] 健康检查不通过,可能部署失败。查看日志,自行判断是否启动成功"; + tail -n 10 nohup.out + exit 1; + # 健康检查通过,打印最后 10 行日志,可能部署的人想看下日志。 + else + tail -n 10 nohup.out + fi + # 如果未配置健康检查,则 sleep 120 秒,人工看日志是否部署成功。 + else + echo "[healthCheck] HEALTH_CHECK_URL 未配置,开始 sleep 120 秒"; + sleep 120 + echo "[healthCheck] sleep 120 秒完成,查看日志,自行判断是否启动成功"; + tail -n 50 nohup.out + fi +} + +# 部署 +function deploy() { + cd $BASE_PATH + # 备份原 jar + backup + # 停止 Java 服务 + stop + # 部署新 jar + transfer + # 启动 Java 服务 + start + # 健康检查 + healthCheck +} + +deploy diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6c53e31 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,84 @@ +version: "3.4" + +name: yunxi-system + +services: + mysql: + container_name: yunxi-mysql + image: mysql:8 + restart: unless-stopped + tty: true + ports: + - "3306:3306" + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE:-ruoyi-vue-pro} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456} + volumes: + - mysql:/var/lib/mysql/ + - ./sql/mysql/ruoyi-vue-pro.sql:/docker-entrypoint-initdb.d/ruoyi-vue-pro.sql:ro + + redis: + container_name: yunxi-redis + image: redis:6-alpine + restart: unless-stopped + ports: + - "6379:6379" + volumes: + - redis:/data + + server: + container_name: yunxi-server + build: + context: ./yunxi-server/ + image: yunxi-server + restart: unless-stopped + ports: + - "48080:48080" + environment: + # https://github.com/polovyivan/docker-pass-configs-to-container + SPRING_PROFILES_ACTIVE: local + JAVA_OPTS: + ${JAVA_OPTS:- + -Xms512m + -Xmx512m + -Djava.security.egd=file:/dev/./urandom + } + ARGS: + --spring.datasource.dynamic.datasource.master.url=${MASTER_DATASOURCE_URL:-jdbc:mysql://yunxi-mysql:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true} + --spring.datasource.dynamic.datasource.master.username=${MASTER_DATASOURCE_USERNAME:-root} + --spring.datasource.dynamic.datasource.master.password=${MASTER_DATASOURCE_PASSWORD:-123456} + --spring.datasource.dynamic.datasource.slave.url=${SLAVE_DATASOURCE_URL:-jdbc:mysql://yunxi-mysql:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true} + --spring.datasource.dynamic.datasource.slave.username=${SLAVE_DATASOURCE_USERNAME:-root} + --spring.datasource.dynamic.datasource.slave.password=${SLAVE_DATASOURCE_PASSWORD:-123456} + --spring.redis.host=${REDIS_HOST:-yunxi-redis} + depends_on: + - mysql + - redis + + admin: + container_name: yunxi-admin + build: + context: ./yunxi-ui-admin + args: + NODE_ENV: + ENV=${NODE_ENV:-production} + PUBLIC_PATH=${PUBLIC_PATH:-/} + VUE_APP_TITLE=${VUE_APP_TITLE:-云息供应链管理平台} + VUE_APP_BASE_API=${VUE_APP_BASE_API:-/prod-api} + VUE_APP_APP_NAME=${VUE_APP_APP_NAME:-/} + VUE_APP_TENANT_ENABLE=${VUE_APP_TENANT_ENABLE:-true} + VUE_APP_CAPTCHA_ENABLE=${VUE_APP_CAPTCHA_ENABLE:-true} + VUE_APP_DOC_ENABLE=${VUE_APP_DOC_ENABLE:-true} + VUE_APP_BAIDU_CODE=${VUE_APP_BAIDU_CODE:-fadc1bd5db1a1d6f581df60a1807f8ab} + image: yunxi-admin + restart: unless-stopped + ports: + - "8080:80" + depends_on: + - server + +volumes: + mysql: + driver: local + redis: + driver: local diff --git a/docker.env b/docker.env new file mode 100644 index 0000000..57e8b71 --- /dev/null +++ b/docker.env @@ -0,0 +1,25 @@ +## mysql +MYSQL_DATABASE=ruoyi-vue-pro +MYSQL_ROOT_PASSWORD=123456 + +## server +JAVA_OPTS=-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom + +MASTER_DATASOURCE_URL=jdbc:mysql://yunxi-mysql:3306/${MYSQL_DATABASE}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true +MASTER_DATASOURCE_USERNAME=root +MASTER_DATASOURCE_PASSWORD=${MYSQL_ROOT_PASSWORD} +SLAVE_DATASOURCE_URL=${MASTER_DATASOURCE_URL} +SLAVE_DATASOURCE_USERNAME=${MASTER_DATASOURCE_USERNAME} +SLAVE_DATASOURCE_PASSWORD=${MASTER_DATASOURCE_PASSWORD} +REDIS_HOST=yunxi-redis + +## admin +NODE_ENV=production +PUBLIC_PATH=/ +VUE_APP_TITLE=云息供应链管理平台 +VUE_APP_BASE_API=/prod-api +VUE_APP_APP_NAME=/ +VUE_APP_TENANT_ENABLE=true +VUE_APP_CAPTCHA_ENABLE=true +VUE_APP_DOC_ENABLE=true +VUE_APP_BAIDU_CODE=fadc1bd5db1a1d6f581df60a1807f8ab diff --git a/http-client.env.json b/http-client.env.json new file mode 100644 index 0000000..4a4cb52 --- /dev/null +++ b/http-client.env.json @@ -0,0 +1,20 @@ +{ + "local": { + "baseUrl": "http://127.0.0.1:48080/admin-api", + "token": "test1", + "adminTenentId": "1", + + "appApi": "http://127.0.0.1:48080/app-api", + "appToken": "test247", + "appTenentId": "1" + }, + "gateway": { + "baseUrl": "http://127.0.0.1:8888/admin-api", + "token": "test1", + "adminTenentId": "1", + + "appApi": "http://127.0.0.1:8888/app-api", + "appToken": "test1", + "appTenantId": "1" + } +} diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..a8e8ce6 --- /dev/null +++ b/lombok.config @@ -0,0 +1,4 @@ +config.stopBubbling = true +lombok.tostring.callsuper=CALL +lombok.equalsandhashcode.callsuper=CALL +lombok.accessors.chain=true diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..4549280 --- /dev/null +++ b/pom.xml @@ -0,0 +1,113 @@ + + + 4.0.0 + com.yunxi.scm + yunxi + ${revision} + pom + + yunxi-dependencies + yunxi-framework + + yunxi-server + + yunxi-module-member + yunxi-module-system + yunxi-module-infra + + + + + + + yunxi-example + + + ${project.artifactId} + 芋道项目基础脚手架 + https://github.com/YunaiV/ruoyi-vue-pro + + + 1.8.0-snapshot + + 1.8 + ${java.version} + ${java.version} + 3.0.0-M5 + 3.8.1 + + 1.18.28 + 2.7.13 + 1.5.5.Final + UTF-8 + + + + + + com.yunxi.scm + yunxi-dependencies + ${revision} + pom + import + + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + + + org.springframework.boot + spring-boot-configuration-processor + ${spring.boot.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + + + + + + + + + huaweicloud + huawei + https://mirrors.huaweicloud.com/repository/maven/ + + + aliyunmaven + aliyun + https://maven.aliyun.com/repository/public + + + + diff --git a/sql/mysql/yunxi-scm.sql b/sql/mysql/yunxi-scm.sql new file mode 100644 index 0000000..cc6eceb --- /dev/null +++ b/sql/mysql/yunxi-scm.sql @@ -0,0 +1,3250 @@ +/* + Navicat Premium Data Transfer + + Source Server : 127.0.0.1 MySQL + Source Server Type : MySQL + Source Server Version : 80026 + Source Host : localhost:3306 + Source Schema : ruoyi-vue-pro + + Target Server Type : MySQL + Target Server Version : 80026 + File Encoding : 65001 + + Date: 24/07/2023 08:51:31 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for QRTZ_BLOB_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_BLOB_TRIGGERS`; +CREATE TABLE `QRTZ_BLOB_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `BLOB_DATA` blob NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + INDEX `SCHED_NAME`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE, + CONSTRAINT `qrtz_blob_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_BLOB_TRIGGERS +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_CALENDARS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_CALENDARS`; +CREATE TABLE `QRTZ_CALENDARS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `CALENDAR_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `CALENDAR` blob NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `CALENDAR_NAME`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_CALENDARS +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_CRON_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_CRON_TRIGGERS`; +CREATE TABLE `QRTZ_CRON_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `CRON_EXPRESSION` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TIME_ZONE_ID` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `qrtz_cron_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_CRON_TRIGGERS +-- ---------------------------- +BEGIN; +INSERT INTO `QRTZ_CRON_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `CRON_EXPRESSION`, `TIME_ZONE_ID`) VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', '* * * * * ?', 'Asia/Shanghai'); +INSERT INTO `QRTZ_CRON_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `CRON_EXPRESSION`, `TIME_ZONE_ID`) VALUES ('schedulerName', 'payOrderExpireJob', 'DEFAULT', '0 0/1 * * * ?', 'Asia/Shanghai'); +INSERT INTO `QRTZ_CRON_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `CRON_EXPRESSION`, `TIME_ZONE_ID`) VALUES ('schedulerName', 'payOrderSyncJob', 'DEFAULT', '0 0/1 * * * ?', 'Asia/Shanghai'); +INSERT INTO `QRTZ_CRON_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `CRON_EXPRESSION`, `TIME_ZONE_ID`) VALUES ('schedulerName', 'payRefundSyncJob', 'DEFAULT', '0 0/1 * * * ?', 'Asia/Shanghai'); +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_FIRED_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_FIRED_TRIGGERS`; +CREATE TABLE `QRTZ_FIRED_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `ENTRY_ID` varchar(95) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `INSTANCE_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `FIRED_TIME` bigint NOT NULL, + `SCHED_TIME` bigint NOT NULL, + `PRIORITY` int NOT NULL, + `STATE` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`, `ENTRY_ID`) USING BTREE, + INDEX `IDX_QRTZ_FT_TRIG_INST_NAME`(`SCHED_NAME` ASC, `INSTANCE_NAME` ASC) USING BTREE, + INDEX `IDX_QRTZ_FT_INST_JOB_REQ_RCVRY`(`SCHED_NAME` ASC, `INSTANCE_NAME` ASC, `REQUESTS_RECOVERY` ASC) USING BTREE, + INDEX `IDX_QRTZ_FT_J_G`(`SCHED_NAME` ASC, `JOB_NAME` ASC, `JOB_GROUP` ASC) USING BTREE, + INDEX `IDX_QRTZ_FT_JG`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE, + INDEX `IDX_QRTZ_FT_T_G`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE, + INDEX `IDX_QRTZ_FT_TG`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_FIRED_TRIGGERS +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_JOB_DETAILS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_JOB_DETAILS`; +CREATE TABLE `QRTZ_JOB_DETAILS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `DESCRIPTION` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `JOB_CLASS_NAME` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `IS_DURABLE` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `IS_NONCONCURRENT` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `IS_UPDATE_DATA` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `REQUESTS_RECOVERY` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `JOB_DATA` blob NULL, + PRIMARY KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) USING BTREE, + INDEX `IDX_QRTZ_J_REQ_RECOVERY`(`SCHED_NAME` ASC, `REQUESTS_RECOVERY` ASC) USING BTREE, + INDEX `IDX_QRTZ_J_GRP`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_JOB_DETAILS +-- ---------------------------- +BEGIN; +INSERT INTO `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `JOB_CLASS_NAME`, `IS_DURABLE`, `IS_NONCONCURRENT`, `IS_UPDATE_DATA`, `REQUESTS_RECOVERY`, `JOB_DATA`) VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', NULL, 'com.yunxi.scm.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000057400104A4F425F48414E444C45525F4E414D4574000C7061794E6F746966794A6F627800); +INSERT INTO `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `JOB_CLASS_NAME`, `IS_DURABLE`, `IS_NONCONCURRENT`, `IS_UPDATE_DATA`, `REQUESTS_RECOVERY`, `JOB_DATA`) VALUES ('schedulerName', 'payOrderExpireJob', 'DEFAULT', NULL, 'com.yunxi.scm.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000127400104A4F425F48414E444C45525F4E414D457400117061794F726465724578706972654A6F627800); +INSERT INTO `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `JOB_CLASS_NAME`, `IS_DURABLE`, `IS_NONCONCURRENT`, `IS_UPDATE_DATA`, `REQUESTS_RECOVERY`, `JOB_DATA`) VALUES ('schedulerName', 'payOrderSyncJob', 'DEFAULT', NULL, 'com.yunxi.scm.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000117400104A4F425F48414E444C45525F4E414D4574000F7061794F7264657253796E634A6F627800); +INSERT INTO `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `JOB_CLASS_NAME`, `IS_DURABLE`, `IS_NONCONCURRENT`, `IS_UPDATE_DATA`, `REQUESTS_RECOVERY`, `JOB_DATA`) VALUES ('schedulerName', 'payRefundSyncJob', 'DEFAULT', NULL, 'com.yunxi.scm.framework.quartz.core.handler.JobHandlerInvoker', '0', '1', '1', '0', 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000027400064A4F425F49447372000E6A6176612E6C616E672E4C6F6E673B8BE490CC8F23DF0200014A000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B020000787000000000000000137400104A4F425F48414E444C45525F4E414D45740010706179526566756E6453796E634A6F627800); +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_LOCKS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_LOCKS`; +CREATE TABLE `QRTZ_LOCKS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `LOCK_NAME` varchar(40) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `LOCK_NAME`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_LOCKS +-- ---------------------------- +BEGIN; +INSERT INTO `QRTZ_LOCKS` (`SCHED_NAME`, `LOCK_NAME`) VALUES ('schedulerName', 'STATE_ACCESS'); +INSERT INTO `QRTZ_LOCKS` (`SCHED_NAME`, `LOCK_NAME`) VALUES ('schedulerName', 'TRIGGER_ACCESS'); +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_PAUSED_TRIGGER_GRPS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_PAUSED_TRIGGER_GRPS`; +CREATE TABLE `QRTZ_PAUSED_TRIGGER_GRPS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_GROUP`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_PAUSED_TRIGGER_GRPS +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_SCHEDULER_STATE +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_SCHEDULER_STATE`; +CREATE TABLE `QRTZ_SCHEDULER_STATE` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `INSTANCE_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `LAST_CHECKIN_TIME` bigint NOT NULL, + `CHECKIN_INTERVAL` bigint NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `INSTANCE_NAME`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_SCHEDULER_STATE +-- ---------------------------- +BEGIN; +INSERT INTO `QRTZ_SCHEDULER_STATE` (`SCHED_NAME`, `INSTANCE_NAME`, `LAST_CHECKIN_TIME`, `CHECKIN_INTERVAL`) VALUES ('schedulerName', 'Yunai1690117495401', 1690119854263, 15000); +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_SIMPLE_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_SIMPLE_TRIGGERS`; +CREATE TABLE `QRTZ_SIMPLE_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `REPEAT_COUNT` bigint NOT NULL, + `REPEAT_INTERVAL` bigint NOT NULL, + `TIMES_TRIGGERED` bigint NOT NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `qrtz_simple_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_SIMPLE_TRIGGERS +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_SIMPROP_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_SIMPROP_TRIGGERS`; +CREATE TABLE `QRTZ_SIMPROP_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `STR_PROP_1` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `STR_PROP_2` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `STR_PROP_3` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `INT_PROP_1` int NULL DEFAULT NULL, + `INT_PROP_2` int NULL DEFAULT NULL, + `LONG_PROP_1` bigint NULL DEFAULT NULL, + `LONG_PROP_2` bigint NULL DEFAULT NULL, + `DEC_PROP_1` decimal(13, 4) NULL DEFAULT NULL, + `DEC_PROP_2` decimal(13, 4) NULL DEFAULT NULL, + `BOOL_PROP_1` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `BOOL_PROP_2` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + CONSTRAINT `qrtz_simprop_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) REFERENCES `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_SIMPROP_TRIGGERS +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for QRTZ_TRIGGERS +-- ---------------------------- +DROP TABLE IF EXISTS `QRTZ_TRIGGERS`; +CREATE TABLE `QRTZ_TRIGGERS` ( + `SCHED_NAME` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `JOB_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `JOB_GROUP` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `DESCRIPTION` varchar(250) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `NEXT_FIRE_TIME` bigint NULL DEFAULT NULL, + `PREV_FIRE_TIME` bigint NULL DEFAULT NULL, + `PRIORITY` int NULL DEFAULT NULL, + `TRIGGER_STATE` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `TRIGGER_TYPE` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `START_TIME` bigint NOT NULL, + `END_TIME` bigint NULL DEFAULT NULL, + `CALENDAR_NAME` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL, + `MISFIRE_INSTR` smallint NULL DEFAULT NULL, + `JOB_DATA` blob NULL, + PRIMARY KEY (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`) USING BTREE, + INDEX `IDX_QRTZ_T_J`(`SCHED_NAME` ASC, `JOB_NAME` ASC, `JOB_GROUP` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_JG`(`SCHED_NAME` ASC, `JOB_GROUP` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_C`(`SCHED_NAME` ASC, `CALENDAR_NAME` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_G`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_STATE`(`SCHED_NAME` ASC, `TRIGGER_STATE` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_N_STATE`(`SCHED_NAME` ASC, `TRIGGER_NAME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_N_G_STATE`(`SCHED_NAME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_NEXT_FIRE_TIME`(`SCHED_NAME` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_NFT_ST`(`SCHED_NAME` ASC, `TRIGGER_STATE` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_NFT_MISFIRE`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_NFT_ST_MISFIRE`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC, `TRIGGER_STATE` ASC) USING BTREE, + INDEX `IDX_QRTZ_T_NFT_ST_MISFIRE_GRP`(`SCHED_NAME` ASC, `MISFIRE_INSTR` ASC, `NEXT_FIRE_TIME` ASC, `TRIGGER_GROUP` ASC, `TRIGGER_STATE` ASC) USING BTREE, + CONSTRAINT `qrtz_triggers_ibfk_1` FOREIGN KEY (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) REFERENCES `QRTZ_JOB_DETAILS` (`SCHED_NAME`, `JOB_NAME`, `JOB_GROUP`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; + +-- ---------------------------- +-- Records of QRTZ_TRIGGERS +-- ---------------------------- +BEGIN; +INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payNotifyJob', 'DEFAULT', 'payNotifyJob', 'DEFAULT', NULL, 1688907102000, 1688907101000, 5, 'PAUSED', 'CRON', 1635294882000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800); +INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payOrderExpireJob', 'DEFAULT', 'payOrderExpireJob', 'DEFAULT', NULL, 1690011600000, -1, 5, 'PAUSED', 'CRON', 1690011553000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800); +INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payOrderSyncJob', 'DEFAULT', 'payOrderSyncJob', 'DEFAULT', NULL, 1690011600000, 1690011540000, 5, 'PAUSED', 'CRON', 1690007785000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800); +INSERT INTO `QRTZ_TRIGGERS` (`SCHED_NAME`, `TRIGGER_NAME`, `TRIGGER_GROUP`, `JOB_NAME`, `JOB_GROUP`, `DESCRIPTION`, `NEXT_FIRE_TIME`, `PREV_FIRE_TIME`, `PRIORITY`, `TRIGGER_STATE`, `TRIGGER_TYPE`, `START_TIME`, `END_TIME`, `CALENDAR_NAME`, `MISFIRE_INSTR`, `JOB_DATA`) VALUES ('schedulerName', 'payRefundSyncJob', 'DEFAULT', 'payRefundSyncJob', 'DEFAULT', NULL, 1690117560000, 1690117500000, 5, 'PAUSED', 'CRON', 1690117424000, 0, NULL, 0, 0xACED0005737200156F72672E71756172747A2E4A6F62446174614D61709FB083E8BFA9B0CB020000787200266F72672E71756172747A2E7574696C732E537472696E674B65794469727479466C61674D61708208E8C3FBC55D280200015A0013616C6C6F77735472616E7369656E74446174617872001D6F72672E71756172747A2E7574696C732E4469727479466C61674D617013E62EAD28760ACE0200025A000564697274794C00036D617074000F4C6A6176612F7574696C2F4D61703B787001737200116A6176612E7574696C2E486173684D61700507DAC1C31660D103000246000A6C6F6164466163746F724900097468726573686F6C6478703F4000000000000C770800000010000000037400114A4F425F48414E444C45525F504152414D707400124A4F425F52455452595F494E54455256414C737200116A6176612E6C616E672E496E746567657212E2A0A4F781873802000149000576616C7565787200106A6176612E6C616E672E4E756D62657286AC951D0B94E08B02000078700000000074000F4A4F425F52455452595F434F554E5471007E000B7800); +COMMIT; + +-- ---------------------------- +-- Table structure for infra_api_access_log +-- ---------------------------- +DROP TABLE IF EXISTS `infra_api_access_log`; +CREATE TABLE `infra_api_access_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志主键', + `trace_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '链路追踪编号', + `user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户编号', + `user_type` tinyint NOT NULL DEFAULT 0 COMMENT '用户类型', + `application_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '应用名', + `request_method` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '请求方法名', + `request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '请求地址', + `request_params` varchar(8000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '请求参数', + `user_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户 IP', + `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '浏览器 UA', + `begin_time` datetime NOT NULL COMMENT '开始请求时间', + `end_time` datetime NOT NULL COMMENT '结束请求时间', + `duration` int NOT NULL COMMENT '执行时长', + `result_code` int NOT NULL DEFAULT 0 COMMENT '结果码', + `result_msg` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '结果提示', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 35832 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'API 访问日志表'; + +-- ---------------------------- +-- Records of infra_api_access_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for infra_api_error_log +-- ---------------------------- +DROP TABLE IF EXISTS `infra_api_error_log`; +CREATE TABLE `infra_api_error_log` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '编号', + `trace_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '链路追踪编号\n *\n * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。', + `user_id` int NOT NULL DEFAULT 0 COMMENT '用户编号', + `user_type` tinyint NOT NULL DEFAULT 0 COMMENT '用户类型', + `application_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '应用名\n *\n * 目前读取 spring.application.name', + `request_method` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请求方法名', + `request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请求地址', + `request_params` varchar(8000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '请求参数', + `user_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户 IP', + `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '浏览器 UA', + `exception_time` datetime NOT NULL COMMENT '异常发生时间', + `exception_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '异常名\n *\n * {@link Throwable#getClass()} 的类全名', + `exception_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '异常导致的消息\n *\n * {@link cn.iocoder.common.framework.util.ExceptionUtil#getMessage(Throwable)}', + `exception_root_cause_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '异常导致的根消息\n *\n * {@link cn.iocoder.common.framework.util.ExceptionUtil#getRootCauseMessage(Throwable)}', + `exception_stack_trace` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '异常的栈轨迹\n *\n * {@link cn.iocoder.common.framework.util.ExceptionUtil#getServiceException(Exception)}', + `exception_class_name` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '异常发生的类全名\n *\n * {@link StackTraceElement#getClassName()}', + `exception_file_name` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '异常发生的类文件\n *\n * {@link StackTraceElement#getFileName()}', + `exception_method_name` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '异常发生的方法名\n *\n * {@link StackTraceElement#getMethodName()}', + `exception_line_number` int NOT NULL COMMENT '异常发生的方法所在行\n *\n * {@link StackTraceElement#getLineNumber()}', + `process_status` tinyint NOT NULL COMMENT '处理状态', + `process_time` datetime NULL DEFAULT NULL COMMENT '处理时间', + `process_user_id` int NULL DEFAULT 0 COMMENT '处理用户编号', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1391 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志'; + +-- ---------------------------- +-- Records of infra_api_error_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for infra_codegen_column +-- ---------------------------- +DROP TABLE IF EXISTS `infra_codegen_column`; +CREATE TABLE `infra_codegen_column` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `table_id` bigint NOT NULL COMMENT '表编号', + `column_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字段名', + `data_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字段类型', + `column_comment` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '字段描述', + `nullable` bit(1) NOT NULL COMMENT '是否允许为空', + `primary_key` bit(1) NOT NULL COMMENT '是否主键', + `auto_increment` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '是否自增', + `ordinal_position` int NOT NULL COMMENT '排序', + `java_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Java 属性类型', + `java_field` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'Java 属性名', + `dict_type` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '字典类型', + `example` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '数据示例', + `create_operation` bit(1) NOT NULL COMMENT '是否为 Create 创建操作的字段', + `update_operation` bit(1) NOT NULL COMMENT '是否为 Update 更新操作的字段', + `list_operation` bit(1) NOT NULL COMMENT '是否为 List 查询操作的字段', + `list_operation_condition` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '=' COMMENT 'List 查询操作的条件类型', + `list_operation_result` bit(1) NOT NULL COMMENT '是否为 List 查询操作的返回字段', + `html_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '显示类型', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1715 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义'; + +-- ---------------------------- +-- Records of infra_codegen_column +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for infra_codegen_table +-- ---------------------------- +DROP TABLE IF EXISTS `infra_codegen_table`; +CREATE TABLE `infra_codegen_table` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `data_source_config_id` bigint NOT NULL COMMENT '数据源配置的编号', + `scene` tinyint NOT NULL DEFAULT 1 COMMENT '生成场景', + `table_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '表名称', + `table_comment` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '表描述', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `module_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模块名', + `business_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务名', + `class_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '类名称', + `class_comment` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '类描述', + `author` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '作者', + `template_type` tinyint NOT NULL DEFAULT 1 COMMENT '模板类型', + `front_type` tinyint NOT NULL COMMENT '前端类型', + `parent_menu_id` bigint NULL DEFAULT NULL COMMENT '父菜单编号', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 132 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义'; + +-- ---------------------------- +-- Records of infra_codegen_table +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for infra_config +-- ---------------------------- +DROP TABLE IF EXISTS `infra_config`; +CREATE TABLE `infra_config` ( + `id` int NOT NULL AUTO_INCREMENT COMMENT '参数主键', + `category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数分组', + `type` tinyint NOT NULL COMMENT '参数类型', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '参数名称', + `config_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '参数键名', + `value` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '参数键值', + `visible` bit(1) NOT NULL COMMENT '是否可见', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '参数配置表'; + +-- ---------------------------- +-- Records of infra_config +-- ---------------------------- +BEGIN; +INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'biz', 1, '用户管理-账号初始密码', 'sys.user.init-password', '123456', b'0', '初始化密码 123456', 'admin', '2021-01-05 17:03:48', '1', '2022-03-20 02:25:51', b'0'); +INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 'url', 2, 'MySQL 监控的地址', 'url.druid', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:33:38', b'0'); +INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 'url', 2, 'SkyWalking 监控的地址', 'url.skywalking', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:57:03', b'0'); +INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 'url', 2, 'Spring Boot Admin 监控的地址', 'url.spring-boot-admin', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:52:07', b'0'); +INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (10, 'url', 2, 'Swagger 接口文档的地址', 'url.swagger', '', b'1', '', '1', '2023-04-07 13:41:16', '1', '2023-04-07 14:59:00', b'0'); +INSERT INTO `infra_config` (`id`, `category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 'ui', 2, '腾讯地图 key', 'tencent.lbs.key', 'TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E', b'1', '腾讯地图 key', '1', '2023-06-03 19:16:27', '1', '2023-06-03 19:16:27', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for infra_data_source_config +-- ---------------------------- +DROP TABLE IF EXISTS `infra_data_source_config`; +CREATE TABLE `infra_data_source_config` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键编号', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '参数名称', + `url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据源连接', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '密码', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '数据源配置表'; + +-- ---------------------------- +-- Records of infra_data_source_config +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for infra_file +-- ---------------------------- +DROP TABLE IF EXISTS `infra_file`; +CREATE TABLE `infra_file` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '文件编号', + `config_id` bigint NULL DEFAULT NULL COMMENT '配置编号', + `name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件名', + `path` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件路径', + `url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件 URL', + `type` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件类型', + `size` int NOT NULL COMMENT '文件大小', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 953 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; + +-- ---------------------------- +-- Records of infra_file +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for infra_file_config +-- ---------------------------- +DROP TABLE IF EXISTS `infra_file_config`; +CREATE TABLE `infra_file_config` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `name` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '配置名', + `storage` tinyint NOT NULL COMMENT '存储器', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `master` bit(1) NOT NULL COMMENT '是否为主配置', + `config` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '存储配置', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 18 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件配置表'; + +-- ---------------------------- +-- Records of infra_file_config +-- ---------------------------- +BEGIN; +INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '数据库', 1, '我是数据库', b'1', '{\"@class\":\"com.yunxi.scm.framework.file.core.client.db.DBFileClientConfig\",\"domain\":\"http://127.0.0.1:48080\"}', '1', '2022-03-15 23:56:24', '1', '2023-04-08 09:44:47', b'0'); +INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5, '本地磁盘', 10, '测试下本地存储', b'0', '{\"@class\":\"com.yunxi.scm.framework.file.core.client.local.LocalFileClientConfig\",\"basePath\":\"/Users/yunai/file_test\",\"domain\":\"http://127.0.0.1:48080\"}', '1', '2022-03-15 23:57:00', '1', '2023-04-08 09:44:47', b'0'); +INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 'S3 - 七牛云', 20, NULL, b'0', '{\"@class\":\"com.yunxi.scm.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3-cn-south-1.qiniucs.com\",\"domain\":\"http://test.yunxi.iocoder.cn\",\"bucket\":\"ruoyi-vue-pro\",\"accessKey\":\"b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8\",\"accessSecret\":\"kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP\"}', '1', '2022-03-19 18:00:03', '1', '2023-04-08 09:44:47', b'0'); +INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (15, 'S3 - 七牛云', 20, '', b'0', '{\"@class\":\"com.yunxi.scm.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3-cn-south-1.qiniucs.com\",\"domain\":\"http://test.yunxi.iocoder.cn\",\"bucket\":\"ruoyi-vue-pro\",\"accessKey\":\"b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8\",\"accessSecret\":\"kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP\"}', '1', '2022-06-10 20:50:41', '1', '2023-04-08 09:44:47', b'0'); +INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 'S3 - 七牛云', 20, '', b'0', '{\"@class\":\"com.yunxi.scm.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3-cn-south-1.qiniucs.com\",\"domain\":\"http://test.yunxi.iocoder.cn\",\"bucket\":\"ruoyi-vue-pro\",\"accessKey\":\"b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8\",\"accessSecret\":\"kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP\"}', '1', '2022-06-11 20:32:08', '1', '2023-04-08 09:44:47', b'0'); +INSERT INTO `infra_file_config` (`id`, `name`, `storage`, `remark`, `master`, `config`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, 'S3 - 七牛云', 20, '', b'0', '{\"@class\":\"com.yunxi.scm.framework.file.core.client.s3.S3FileClientConfig\",\"endpoint\":\"s3-cn-south-1.qiniucs.com\",\"domain\":\"http://test.yunxi.iocoder.cn\",\"bucket\":\"ruoyi-vue-pro\",\"accessKey\":\"b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8\",\"accessSecret\":\"kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP\"}', '1', '2022-06-11 20:32:47', '1', '2023-04-08 09:44:47', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for infra_file_content +-- ---------------------------- +DROP TABLE IF EXISTS `infra_file_content`; +CREATE TABLE `infra_file_content` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `config_id` bigint NOT NULL COMMENT '配置编号', + `path` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件路径', + `content` mediumblob NOT NULL COMMENT '文件内容', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 44 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表'; + +-- ---------------------------- +-- Records of infra_file_content +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for infra_job +-- ---------------------------- +DROP TABLE IF EXISTS `infra_job`; +CREATE TABLE `infra_job` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '任务编号', + `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '任务名称', + `status` tinyint NOT NULL COMMENT '任务状态', + `handler_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '处理器的名字', + `handler_param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '处理器的参数', + `cron_expression` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'CRON 表达式', + `retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数', + `retry_interval` int NOT NULL DEFAULT 0 COMMENT '重试间隔', + `monitor_timeout` int NOT NULL DEFAULT 0 COMMENT '监控超时时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务表'; + +-- ---------------------------- +-- Records of infra_job +-- ---------------------------- +BEGIN; +INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5, '支付通知 Job', 2, 'payNotifyJob', NULL, '* * * * * ?', 0, 0, 0, '1', '2021-10-27 08:34:42', '1', '2023-07-09 20:51:41', b'0'); +INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 'Job 示例', 1, 'demoJob', NULL, '* * * L * ?', 1, 1, 0, '1', '2022-09-24 22:31:41', '1', '2022-09-24 22:31:42', b'0'); +INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, '支付订单同步 Job', 2, 'payOrderSyncJob', NULL, '0 0/1 * * * ?', 0, 0, 0, '1', '2023-07-22 14:36:26', '1', '2023-07-22 15:39:08', b'0'); +INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (18, '支付订单过期 Job', 2, 'payOrderExpireJob', NULL, '0 0/1 * * * ?', 0, 0, 0, '1', '2023-07-22 15:36:23', '1', '2023-07-22 15:39:54', b'0'); +INSERT INTO `infra_job` (`id`, `name`, `status`, `handler_name`, `handler_param`, `cron_expression`, `retry_count`, `retry_interval`, `monitor_timeout`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (19, '退款订单的同步 Job', 2, 'payRefundSyncJob', NULL, '0 0/1 * * * ?', 0, 0, 0, '1', '2023-07-23 21:03:44', '1', '2023-07-23 21:09:00', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for infra_job_log +-- ---------------------------- +DROP TABLE IF EXISTS `infra_job_log`; +CREATE TABLE `infra_job_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志编号', + `job_id` bigint NOT NULL COMMENT '任务编号', + `handler_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '处理器的名字', + `handler_param` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '处理器的参数', + `execute_index` tinyint NOT NULL DEFAULT 1 COMMENT '第几次执行', + `begin_time` datetime NOT NULL COMMENT '开始执行时间', + `end_time` datetime NULL DEFAULT NULL COMMENT '结束执行时间', + `duration` int NULL DEFAULT NULL COMMENT '执行时长', + `status` tinyint NOT NULL COMMENT '任务状态', + `result` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '结果数据', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 161 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '定时任务日志表'; + +-- ---------------------------- +-- Records of infra_job_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for infra_test_demo +-- ---------------------------- +DROP TABLE IF EXISTS `infra_test_demo`; +CREATE TABLE `infra_test_demo` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态', + `type` tinyint NOT NULL COMMENT '类型', + `category` tinyint NOT NULL COMMENT '分类', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; + +-- ---------------------------- +-- Records of infra_test_demo +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for member_user +-- ---------------------------- +DROP TABLE IF EXISTS `member_user`; +CREATE TABLE `member_user` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `nickname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户昵称', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '头像', + `status` tinyint NOT NULL COMMENT '状态', + `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '手机号', + `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码', + `register_ip` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '注册 IP', + `login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '最后登录IP', + `login_date` datetime NULL DEFAULT NULL COMMENT '最后登录时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uk_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号' +) ENGINE = InnoDB AUTO_INCREMENT = 248 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户'; + +-- ---------------------------- +-- Records of member_user +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_dept +-- ---------------------------- +DROP TABLE IF EXISTS `system_dept`; +CREATE TABLE `system_dept` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门id', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '部门名称', + `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父部门id', + `sort` int NOT NULL DEFAULT 0 COMMENT '显示顺序', + `leader_user_id` bigint NULL DEFAULT NULL COMMENT '负责人', + `phone` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系电话', + `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱', + `status` tinyint NOT NULL COMMENT '部门状态(0正常 1停用)', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 112 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门表'; + +-- ---------------------------- +-- Records of system_dept +-- ---------------------------- +BEGIN; +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, '芋道源码', 0, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-06-19 00:29:10', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '深圳总公司', 100, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-05-16 20:25:23', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (102, '长沙分公司', 100, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:40', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, '研发部门', 101, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '103', '2022-01-14 01:04:14', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, '市场部门', 101, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:38', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (105, '测试部门', 101, 3, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-05-16 20:25:15', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (106, '财务部门', 101, 4, 103, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '103', '2022-01-15 21:32:22', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, '运维部门', 101, 5, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:33', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, '市场部门', 102, 1, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-02-16 08:35:45', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '财务部门', 102, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:29', b'0', 1); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (110, '新部门', 0, 1, NULL, NULL, NULL, 0, '110', '2022-02-23 20:46:30', '110', '2022-02-23 20:46:30', b'0', 121); +INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '顶级部门', 0, 1, NULL, NULL, NULL, 0, '113', '2022-03-07 21:44:50', '113', '2022-03-07 21:44:50', b'0', 122); +COMMIT; + +-- ---------------------------- +-- Table structure for system_dict_data +-- ---------------------------- +DROP TABLE IF EXISTS `system_dict_data`; +CREATE TABLE `system_dict_data` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '字典编码', + `sort` int NOT NULL DEFAULT 0 COMMENT '字典排序', + `label` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '字典标签', + `value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '字典键值', + `dict_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '字典类型', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0正常 1停用)', + `color_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '颜色类型', + `css_class` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT 'css 样式', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1348 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表'; + +-- ---------------------------- +-- Records of system_dict_data +-- ---------------------------- +BEGIN; +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 1, '男', '1', 'system_user_sex', 0, 'default', 'A', '性别男', 'admin', '2021-01-05 17:03:48', '1', '2022-03-29 00:14:39', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 2, '女', '2', 'system_user_sex', 1, 'success', '', '性别女', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 01:30:51', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, '正常', '1', 'infra_job_status', 0, 'success', '', '正常状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:33:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 2, '暂停', '2', 'infra_job_status', 0, 'danger', '', '停用状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:33:45', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 1, '系统内置', '1', 'infra_config_type', 0, 'danger', '', '参数类型 - 系统内置', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:06:02', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (13, 2, '自定义', '2', 'infra_config_type', 0, 'primary', '', '参数类型 - 自定义', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:06:07', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (14, 1, '通知', '1', 'system_notice_type', 0, 'success', '', '通知', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 13:05:57', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (15, 2, '公告', '2', 'system_notice_type', 0, 'info', '', '公告', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 13:06:01', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (16, 0, '其它', '0', 'system_operate_type', 0, 'default', '', '其它操作', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:32:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (17, 1, '查询', '1', 'system_operate_type', 0, 'info', '', '查询操作', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:33:16', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (18, 2, '新增', '2', 'system_operate_type', 0, 'primary', '', '新增操作', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:33:13', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (19, 3, '修改', '3', 'system_operate_type', 0, 'warning', '', '修改操作', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:33:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (20, 4, '删除', '4', 'system_operate_type', 0, 'danger', '', '删除操作', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:33:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (22, 5, '导出', '5', 'system_operate_type', 0, 'default', '', '导出操作', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:33:32', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (23, 6, '导入', '6', 'system_operate_type', 0, 'default', '', '导入操作', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:33:35', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (27, 1, '开启', '0', 'common_status', 0, 'primary', '', '开启状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 08:00:39', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (28, 2, '关闭', '1', 'common_status', 0, 'info', '', '关闭状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 08:00:44', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (29, 1, '目录', '1', 'system_menu_type', 0, '', '', '目录', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:43:45', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (30, 2, '菜单', '2', 'system_menu_type', 0, '', '', '菜单', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:43:41', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (31, 3, '按钮', '3', 'system_menu_type', 0, '', '', '按钮', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:43:39', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (32, 1, '内置', '1', 'system_role_type', 0, 'danger', '', '内置角色', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 13:02:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (33, 2, '自定义', '2', 'system_role_type', 0, 'primary', '', '自定义角色', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 13:02:12', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (34, 1, '全部数据权限', '1', 'system_data_scope', 0, '', '', '全部数据权限', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:47:17', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (35, 2, '指定部门数据权限', '2', 'system_data_scope', 0, '', '', '指定部门数据权限', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:47:18', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (36, 3, '本部门数据权限', '3', 'system_data_scope', 0, '', '', '本部门数据权限', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:47:16', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (37, 4, '本部门及以下数据权限', '4', 'system_data_scope', 0, '', '', '本部门及以下数据权限', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:47:21', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (38, 5, '仅本人数据权限', '5', 'system_data_scope', 0, '', '', '仅本人数据权限', 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:47:23', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (39, 0, '成功', '0', 'system_login_result', 0, 'success', '', '登陆结果 - 成功', '', '2021-01-18 06:17:36', '1', '2022-02-16 13:23:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 10, '账号或密码不正确', '10', 'system_login_result', 0, 'primary', '', '登陆结果 - 账号或密码不正确', '', '2021-01-18 06:17:54', '1', '2022-02-16 13:24:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (41, 20, '用户被禁用', '20', 'system_login_result', 0, 'warning', '', '登陆结果 - 用户被禁用', '', '2021-01-18 06:17:54', '1', '2022-02-16 13:23:57', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (42, 30, '验证码不存在', '30', 'system_login_result', 0, 'info', '', '登陆结果 - 验证码不存在', '', '2021-01-18 06:17:54', '1', '2022-02-16 13:24:07', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (43, 31, '验证码不正确', '31', 'system_login_result', 0, 'info', '', '登陆结果 - 验证码不正确', '', '2021-01-18 06:17:54', '1', '2022-02-16 13:24:11', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (44, 100, '未知异常', '100', 'system_login_result', 0, 'danger', '', '登陆结果 - 未知异常', '', '2021-01-18 06:17:54', '1', '2022-02-16 13:24:23', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (45, 1, '是', 'true', 'infra_boolean_string', 0, 'danger', '', 'Boolean 是否类型 - 是', '', '2021-01-19 03:20:55', '1', '2022-03-15 23:01:45', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (46, 1, '否', 'false', 'infra_boolean_string', 0, 'info', '', 'Boolean 是否类型 - 否', '', '2021-01-19 03:20:55', '1', '2022-03-15 23:09:45', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (47, 1, '永不超时', '1', 'infra_redis_timeout_type', 0, 'primary', '', 'Redis 未设置超时的情况', '', '2021-01-26 00:53:17', '1', '2022-02-16 19:03:35', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (48, 1, '动态超时', '2', 'infra_redis_timeout_type', 0, 'info', '', '程序里动态传入超时时间,无法固定', '', '2021-01-26 00:55:00', '1', '2022-02-16 19:03:41', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (49, 3, '固定超时', '3', 'infra_redis_timeout_type', 0, 'success', '', 'Redis 设置了过期时间', '', '2021-01-26 00:55:26', '1', '2022-02-16 19:03:45', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (50, 1, '单表(增删改查)', '1', 'infra_codegen_template_type', 0, '', '', NULL, '', '2021-02-05 07:09:06', '', '2022-03-10 16:33:15', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (51, 2, '树表(增删改查)', '2', 'infra_codegen_template_type', 0, '', '', NULL, '', '2021-02-05 07:14:46', '', '2022-03-10 16:33:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (53, 0, '初始化中', '0', 'infra_job_status', 0, 'primary', '', NULL, '', '2021-02-07 07:46:49', '1', '2022-02-16 19:33:29', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (57, 0, '运行中', '0', 'infra_job_log_status', 0, 'primary', '', 'RUNNING', '', '2021-02-08 10:04:24', '1', '2022-02-16 19:07:48', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (58, 1, '成功', '1', 'infra_job_log_status', 0, 'success', '', NULL, '', '2021-02-08 10:06:57', '1', '2022-02-16 19:07:52', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (59, 2, '失败', '2', 'infra_job_log_status', 0, 'warning', '', '失败', '', '2021-02-08 10:07:38', '1', '2022-02-16 19:07:56', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (60, 1, '会员', '1', 'user_type', 0, 'primary', '', NULL, '', '2021-02-26 00:16:27', '1', '2022-02-16 10:22:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (61, 2, '管理员', '2', 'user_type', 0, 'success', '', NULL, '', '2021-02-26 00:16:34', '1', '2022-02-16 10:22:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (62, 0, '未处理', '0', 'infra_api_error_log_process_status', 0, 'primary', '', NULL, '', '2021-02-26 07:07:19', '1', '2022-02-16 20:14:17', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (63, 1, '已处理', '1', 'infra_api_error_log_process_status', 0, 'success', '', NULL, '', '2021-02-26 07:07:26', '1', '2022-02-16 20:14:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (64, 2, '已忽略', '2', 'infra_api_error_log_process_status', 0, 'danger', '', NULL, '', '2021-02-26 07:07:34', '1', '2022-02-16 20:14:14', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (66, 2, '阿里云', 'ALIYUN', 'system_sms_channel_code', 0, 'primary', '', NULL, '1', '2021-04-05 01:05:26', '1', '2022-02-16 10:09:52', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (67, 1, '验证码', '1', 'system_sms_template_type', 0, 'warning', '', NULL, '1', '2021-04-05 21:50:57', '1', '2022-02-16 12:48:30', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (68, 2, '通知', '2', 'system_sms_template_type', 0, 'primary', '', NULL, '1', '2021-04-05 21:51:08', '1', '2022-02-16 12:48:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (69, 0, '营销', '3', 'system_sms_template_type', 0, 'danger', '', NULL, '1', '2021-04-05 21:51:15', '1', '2022-02-16 12:48:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (70, 0, '初始化', '0', 'system_sms_send_status', 0, 'primary', '', NULL, '1', '2021-04-11 20:18:33', '1', '2022-02-16 10:26:07', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (71, 1, '发送成功', '10', 'system_sms_send_status', 0, 'success', '', NULL, '1', '2021-04-11 20:18:43', '1', '2022-02-16 10:25:56', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (72, 2, '发送失败', '20', 'system_sms_send_status', 0, 'danger', '', NULL, '1', '2021-04-11 20:18:49', '1', '2022-02-16 10:26:03', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (73, 3, '不发送', '30', 'system_sms_send_status', 0, 'info', '', NULL, '1', '2021-04-11 20:19:44', '1', '2022-02-16 10:26:10', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (74, 0, '等待结果', '0', 'system_sms_receive_status', 0, 'primary', '', NULL, '1', '2021-04-11 20:27:43', '1', '2022-02-16 10:28:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (75, 1, '接收成功', '10', 'system_sms_receive_status', 0, 'success', '', NULL, '1', '2021-04-11 20:29:25', '1', '2022-02-16 10:28:28', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (76, 2, '接收失败', '20', 'system_sms_receive_status', 0, 'danger', '', NULL, '1', '2021-04-11 20:29:31', '1', '2022-02-16 10:28:32', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (77, 0, '调试(钉钉)', 'DEBUG_DING_TALK', 'system_sms_channel_code', 0, 'info', '', NULL, '1', '2021-04-13 00:20:37', '1', '2022-02-16 10:10:00', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (78, 1, '自动生成', '1', 'system_error_code_type', 0, 'warning', '', NULL, '1', '2021-04-21 00:06:48', '1', '2022-02-16 13:57:20', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (79, 2, '手动编辑', '2', 'system_error_code_type', 0, 'primary', '', NULL, '1', '2021-04-21 00:07:14', '1', '2022-02-16 13:57:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (80, 100, '账号登录', '100', 'system_login_type', 0, 'primary', '', '账号登录', '1', '2021-10-06 00:52:02', '1', '2022-02-16 13:11:34', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (81, 101, '社交登录', '101', 'system_login_type', 0, 'info', '', '社交登录', '1', '2021-10-06 00:52:17', '1', '2022-02-16 13:11:40', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (83, 200, '主动登出', '200', 'system_login_type', 0, 'primary', '', '主动登出', '1', '2021-10-06 00:52:58', '1', '2022-02-16 13:11:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (85, 202, '强制登出', '202', 'system_login_type', 0, 'danger', '', '强制退出', '1', '2021-10-06 00:53:41', '1', '2022-02-16 13:11:57', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (86, 0, '病假', '1', 'bpm_oa_leave_type', 0, 'primary', '', NULL, '1', '2021-09-21 22:35:28', '1', '2022-02-16 10:00:41', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (87, 1, '事假', '2', 'bpm_oa_leave_type', 0, 'info', '', NULL, '1', '2021-09-21 22:36:11', '1', '2022-02-16 10:00:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (88, 2, '婚假', '3', 'bpm_oa_leave_type', 0, 'warning', '', NULL, '1', '2021-09-21 22:36:38', '1', '2022-02-16 10:00:53', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (113, 1, '微信公众号支付', 'wx_pub', 'pay_channel_code', 0, 'success', '', '微信公众号支付', '1', '2021-12-03 10:40:24', '1', '2023-07-19 20:08:47', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (114, 2, '微信小程序支付', 'wx_lite', 'pay_channel_code', 0, 'success', '', '微信小程序支付', '1', '2021-12-03 10:41:06', '1', '2023-07-19 20:08:50', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (115, 3, '微信 App 支付', 'wx_app', 'pay_channel_code', 0, 'success', '', '微信 App 支付', '1', '2021-12-03 10:41:20', '1', '2023-07-19 20:08:56', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (116, 10, '支付宝 PC 网站支付', 'alipay_pc', 'pay_channel_code', 0, 'primary', '', '支付宝 PC 网站支付', '1', '2021-12-03 10:42:09', '1', '2023-07-19 20:09:12', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2023-07-19 20:09:16', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2023-07-19 20:09:20', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2023-07-19 20:09:28', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2023-07-19 10:08:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2023-07-19 10:08:21', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2023-07-19 10:08:24', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (123, 10, '支付成功', '10', 'pay_order_status', 0, 'success', '', '支付成功', '1', '2021-12-03 11:18:29', '1', '2023-07-19 18:04:28', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (124, 30, '支付关闭', '30', 'pay_order_status', 0, 'info', '', '支付关闭', '1', '2021-12-03 11:18:42', '1', '2023-07-19 18:05:07', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (125, 0, '等待支付', '0', 'pay_order_status', 0, 'info', '', '未支付', '1', '2021-12-03 11:18:18', '1', '2023-07-19 18:04:15', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1118, 0, '等待退款', '0', 'pay_refund_status', 0, 'info', '', '等待退款', '1', '2021-12-10 16:44:59', '1', '2023-07-19 10:14:39', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1119, 20, '退款失败', '20', 'pay_refund_status', 0, 'danger', '', '退款失败', '1', '2021-12-10 16:45:10', '1', '2023-07-19 10:15:10', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1124, 10, '退款成功', '10', 'pay_refund_status', 0, 'success', '', '退款成功', '1', '2021-12-10 16:46:26', '1', '2023-07-19 10:15:00', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1125, 0, '默认', '1', 'bpm_model_category', 0, 'primary', '', '流程分类 - 默认', '1', '2022-01-02 08:41:11', '1', '2022-02-16 20:01:42', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1126, 0, 'OA', '2', 'bpm_model_category', 0, 'success', '', '流程分类 - OA', '1', '2022-01-02 08:41:22', '1', '2022-02-16 20:01:50', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1127, 0, '进行中', '1', 'bpm_process_instance_status', 0, 'primary', '', '流程实例的状态 - 进行中', '1', '2022-01-07 23:47:22', '1', '2022-02-16 20:07:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1128, 2, '已完成', '2', 'bpm_process_instance_status', 0, 'success', '', '流程实例的状态 - 已完成', '1', '2022-01-07 23:47:49', '1', '2022-02-16 20:07:54', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1129, 1, '处理中', '1', 'bpm_process_instance_result', 0, 'primary', '', '流程实例的结果 - 处理中', '1', '2022-01-07 23:48:32', '1', '2022-02-16 09:53:26', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1130, 2, '通过', '2', 'bpm_process_instance_result', 0, 'success', '', '流程实例的结果 - 通过', '1', '2022-01-07 23:48:45', '1', '2022-02-16 09:53:31', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1131, 3, '不通过', '3', 'bpm_process_instance_result', 0, 'danger', '', '流程实例的结果 - 不通过', '1', '2022-01-07 23:48:55', '1', '2022-02-16 09:53:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1132, 4, '已取消', '4', 'bpm_process_instance_result', 0, 'info', '', '流程实例的结果 - 撤销', '1', '2022-01-07 23:49:06', '1', '2022-02-16 09:53:42', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1133, 10, '流程表单', '10', 'bpm_model_form_type', 0, '', '', '流程的表单类型 - 流程表单', '103', '2022-01-11 23:51:30', '103', '2022-01-11 23:51:30', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1134, 20, '业务表单', '20', 'bpm_model_form_type', 0, '', '', '流程的表单类型 - 业务表单', '103', '2022-01-11 23:51:47', '103', '2022-01-11 23:51:47', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1135, 10, '角色', '10', 'bpm_task_assign_rule_type', 0, 'info', '', '任务分配规则的类型 - 角色', '103', '2022-01-12 23:21:22', '1', '2022-02-16 20:06:14', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1136, 20, '部门的成员', '20', 'bpm_task_assign_rule_type', 0, 'primary', '', '任务分配规则的类型 - 部门的成员', '103', '2022-01-12 23:21:47', '1', '2022-02-16 20:05:28', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1137, 21, '部门的负责人', '21', 'bpm_task_assign_rule_type', 0, 'primary', '', '任务分配规则的类型 - 部门的负责人', '103', '2022-01-12 23:33:36', '1', '2022-02-16 20:05:31', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1138, 30, '用户', '30', 'bpm_task_assign_rule_type', 0, 'info', '', '任务分配规则的类型 - 用户', '103', '2022-01-12 23:34:02', '1', '2022-02-16 20:05:50', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1139, 40, '用户组', '40', 'bpm_task_assign_rule_type', 0, 'warning', '', '任务分配规则的类型 - 用户组', '103', '2022-01-12 23:34:21', '1', '2022-02-16 20:05:57', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1140, 50, '自定义脚本', '50', 'bpm_task_assign_rule_type', 0, 'danger', '', '任务分配规则的类型 - 自定义脚本', '103', '2022-01-12 23:34:43', '1', '2022-02-16 20:06:01', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1141, 22, '岗位', '22', 'bpm_task_assign_rule_type', 0, 'success', '', '任务分配规则的类型 - 岗位', '103', '2022-01-14 18:41:55', '1', '2022-02-16 20:05:39', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1142, 10, '流程发起人', '10', 'bpm_task_assign_script', 0, '', '', '任务分配自定义脚本 - 流程发起人', '103', '2022-01-15 00:10:57', '103', '2022-01-15 21:24:10', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1143, 20, '流程发起人的一级领导', '20', 'bpm_task_assign_script', 0, '', '', '任务分配自定义脚本 - 流程发起人的一级领导', '103', '2022-01-15 21:24:31', '103', '2022-01-15 21:24:31', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1144, 21, '流程发起人的二级领导', '21', 'bpm_task_assign_script', 0, '', '', '任务分配自定义脚本 - 流程发起人的二级领导', '103', '2022-01-15 21:24:46', '103', '2022-01-15 21:24:57', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1145, 1, '管理后台', '1', 'infra_codegen_scene', 0, '', '', '代码生成的场景枚举 - 管理后台', '1', '2022-02-02 13:15:06', '1', '2022-03-10 16:32:59', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1146, 2, '用户 APP', '2', 'infra_codegen_scene', 0, '', '', '代码生成的场景枚举 - 用户 APP', '1', '2022-02-02 13:15:19', '1', '2022-03-10 16:33:03', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1150, 1, '数据库', '1', 'infra_file_storage', 0, 'default', '', NULL, '1', '2022-03-15 00:25:28', '1', '2022-03-15 00:25:28', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1151, 10, '本地磁盘', '10', 'infra_file_storage', 0, 'default', '', NULL, '1', '2022-03-15 00:25:41', '1', '2022-03-15 00:25:56', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1152, 11, 'FTP 服务器', '11', 'infra_file_storage', 0, 'default', '', NULL, '1', '2022-03-15 00:26:06', '1', '2022-03-15 00:26:10', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1153, 12, 'SFTP 服务器', '12', 'infra_file_storage', 0, 'default', '', NULL, '1', '2022-03-15 00:26:22', '1', '2022-03-15 00:26:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1154, 20, 'S3 对象存储', '20', 'infra_file_storage', 0, 'default', '', NULL, '1', '2022-03-15 00:26:31', '1', '2022-03-15 00:26:45', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1155, 103, '短信登录', '103', 'system_login_type', 0, 'default', '', NULL, '1', '2022-05-09 23:57:58', '1', '2022-05-09 23:58:09', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1156, 1, 'password', 'password', 'system_oauth2_grant_type', 0, 'default', '', '密码模式', '1', '2022-05-12 00:22:05', '1', '2022-05-11 16:26:01', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1157, 2, 'authorization_code', 'authorization_code', 'system_oauth2_grant_type', 0, 'primary', '', '授权码模式', '1', '2022-05-12 00:22:59', '1', '2022-05-11 16:26:02', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1158, 3, 'implicit', 'implicit', 'system_oauth2_grant_type', 0, 'success', '', '简化模式', '1', '2022-05-12 00:23:40', '1', '2022-05-11 16:26:05', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1159, 4, 'client_credentials', 'client_credentials', 'system_oauth2_grant_type', 0, 'default', '', '客户端模式', '1', '2022-05-12 00:23:51', '1', '2022-05-11 16:26:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1160, 5, 'refresh_token', 'refresh_token', 'system_oauth2_grant_type', 0, 'info', '', '刷新模式', '1', '2022-05-12 00:24:02', '1', '2022-05-11 16:26:11', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1162, 1, '销售中', '1', 'product_spu_status', 0, 'success', '', '商品 SPU 状态 - 销售中', '1', '2022-10-24 21:19:47', '1', '2022-10-24 21:20:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1163, 0, '仓库中', '0', 'product_spu_status', 0, 'info', '', '商品 SPU 状态 - 仓库中', '1', '2022-10-24 21:20:54', '1', '2022-10-24 21:21:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1164, 0, '回收站', '-1', 'product_spu_status', 0, 'default', '', '商品 SPU 状态 - 回收站', '1', '2022-10-24 21:21:11', '1', '2022-10-24 21:21:11', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1165, 1, '满减', '1', 'promotion_discount_type', 0, 'success', '', '优惠类型 - 满减', '1', '2022-11-01 12:46:41', '1', '2022-11-01 12:50:11', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1166, 2, '折扣', '2', 'promotion_discount_type', 0, 'primary', '', '优惠类型 - 折扣', '1', '2022-11-01 12:46:51', '1', '2022-11-01 12:50:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1167, 1, '固定日期', '1', 'promotion_coupon_template_validity_type', 0, 'default', '', '优惠劵模板的有限期类型 - 固定日期', '1', '2022-11-02 00:07:34', '1', '2022-11-04 00:07:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1168, 2, '领取之后', '2', 'promotion_coupon_template_validity_type', 0, 'default', '', '优惠劵模板的有限期类型 - 领取之后', '1', '2022-11-02 00:07:54', '1', '2022-11-04 00:07:52', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1169, 1, '全部商品参与', '1', 'promotion_product_scope', 0, 'default', '', '营销的商品范围 - 全部商品参与', '1', '2022-11-02 00:28:22', '1', '2022-11-02 00:28:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1170, 2, '指定商品参与', '2', 'promotion_product_scope', 0, 'default', '', '营销的商品范围 - 指定商品参与', '1', '2022-11-02 00:28:34', '1', '2022-11-02 00:28:40', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1171, 1, '已领取', '1', 'promotion_coupon_status', 0, 'primary', '', '优惠劵的状态 - 已领取', '1', '2022-11-04 00:15:08', '1', '2022-11-04 19:16:04', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1172, 2, '已使用', '2', 'promotion_coupon_status', 0, 'success', '', '优惠劵的状态 - 已使用', '1', '2022-11-04 00:15:21', '1', '2022-11-04 19:16:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1173, 3, '已过期', '3', 'promotion_coupon_status', 0, 'info', '', '优惠劵的状态 - 已过期', '1', '2022-11-04 00:15:43', '1', '2022-11-04 19:16:12', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1174, 1, '直接领取', '1', 'promotion_coupon_take_type', 0, 'primary', '', '优惠劵的领取方式 - 直接领取', '1', '2022-11-04 19:13:00', '1', '2022-11-04 19:13:25', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1175, 2, '指定发放', '2', 'promotion_coupon_take_type', 0, 'success', '', '优惠劵的领取方式 - 指定发放', '1', '2022-11-04 19:13:13', '1', '2022-11-04 19:14:48', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1176, 10, '未开始', '10', 'promotion_activity_status', 0, 'primary', '', '促销活动的状态枚举 - 未开始', '1', '2022-11-04 22:54:49', '1', '2022-11-04 22:55:53', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1177, 20, '进行中', '20', 'promotion_activity_status', 0, 'success', '', '促销活动的状态枚举 - 进行中', '1', '2022-11-04 22:55:06', '1', '2022-11-04 22:55:20', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1178, 30, '已结束', '30', 'promotion_activity_status', 0, 'info', '', '促销活动的状态枚举 - 已结束', '1', '2022-11-04 22:55:41', '1', '2022-11-04 22:55:41', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1179, 40, '已关闭', '40', 'promotion_activity_status', 0, 'warning', '', '促销活动的状态枚举 - 已关闭', '1', '2022-11-04 22:56:10', '1', '2022-11-04 22:56:18', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1180, 10, '满 N 元', '10', 'promotion_condition_type', 0, 'primary', '', '营销的条件类型 - 满 N 元', '1', '2022-11-04 22:59:45', '1', '2022-11-04 22:59:45', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1181, 20, '满 N 件', '20', 'promotion_condition_type', 0, 'success', '', '营销的条件类型 - 满 N 件', '1', '2022-11-04 23:00:02', '1', '2022-11-04 23:00:02', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1182, 10, '申请售后', '10', 'trade_after_sale_status', 0, 'primary', '', '交易售后状态 - 申请售后', '1', '2022-11-19 20:53:33', '1', '2022-11-19 20:54:42', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1183, 20, '商品待退货', '20', 'trade_after_sale_status', 0, 'primary', '', '交易售后状态 - 商品待退货', '1', '2022-11-19 20:54:36', '1', '2022-11-19 20:58:58', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1184, 30, '商家待收货', '30', 'trade_after_sale_status', 0, 'primary', '', '交易售后状态 - 商家待收货', '1', '2022-11-19 20:56:56', '1', '2022-11-19 20:59:20', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1185, 40, '等待退款', '40', 'trade_after_sale_status', 0, 'primary', '', '交易售后状态 - 等待退款', '1', '2022-11-19 20:59:54', '1', '2022-11-19 21:00:01', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1186, 50, '退款成功', '50', 'trade_after_sale_status', 0, 'default', '', '交易售后状态 - 退款成功', '1', '2022-11-19 21:00:33', '1', '2022-11-19 21:00:33', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1187, 61, '买家取消', '61', 'trade_after_sale_status', 0, 'info', '', '交易售后状态 - 买家取消', '1', '2022-11-19 21:01:29', '1', '2022-11-19 21:01:29', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1188, 62, '商家拒绝', '62', 'trade_after_sale_status', 0, 'info', '', '交易售后状态 - 商家拒绝', '1', '2022-11-19 21:02:17', '1', '2022-11-19 21:02:17', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1189, 63, '商家拒收货', '63', 'trade_after_sale_status', 0, 'info', '', '交易售后状态 - 商家拒收货', '1', '2022-11-19 21:02:37', '1', '2022-11-19 21:03:07', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1190, 10, '售中退款', '10', 'trade_after_sale_type', 0, 'success', '', '交易售后的类型 - 售中退款', '1', '2022-11-19 21:05:05', '1', '2022-11-19 21:38:23', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1191, 20, '售后退款', '20', 'trade_after_sale_type', 0, 'primary', '', '交易售后的类型 - 售后退款', '1', '2022-11-19 21:05:32', '1', '2022-11-19 21:38:32', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1192, 10, '仅退款', '10', 'trade_after_sale_way', 0, 'primary', '', '交易售后的方式 - 仅退款', '1', '2022-11-19 21:39:19', '1', '2022-11-19 21:39:19', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1193, 20, '退货退款', '20', 'trade_after_sale_way', 0, 'success', '', '交易售后的方式 - 退货退款', '1', '2022-11-19 21:39:38', '1', '2022-11-19 21:39:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1194, 10, '微信小程序', '10', 'terminal', 0, 'default', '', '终端 - 微信小程序', '1', '2022-12-10 10:51:11', '1', '2022-12-10 10:51:57', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1195, 20, 'H5 网页', '20', 'terminal', 0, 'default', '', '终端 - H5 网页', '1', '2022-12-10 10:51:30', '1', '2022-12-10 10:51:59', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1196, 11, '微信公众号', '11', 'terminal', 0, 'default', '', '终端 - 微信公众号', '1', '2022-12-10 10:54:16', '1', '2022-12-10 10:52:01', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1197, 31, '苹果 App', '31', 'terminal', 0, 'default', '', '终端 - 苹果 App', '1', '2022-12-10 10:54:42', '1', '2022-12-10 10:52:18', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1198, 32, '安卓 App', '32', 'terminal', 0, 'default', '', '终端 - 安卓 App', '1', '2022-12-10 10:55:02', '1', '2022-12-10 10:59:17', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1199, 0, '普通订单', '0', 'trade_order_type', 0, 'default', '', '交易订单的类型 - 普通订单', '1', '2022-12-10 16:34:14', '1', '2022-12-10 16:34:14', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1200, 1, '秒杀订单', '1', 'trade_order_type', 0, 'default', '', '交易订单的类型 - 秒杀订单', '1', '2022-12-10 16:34:26', '1', '2022-12-10 16:34:26', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1201, 2, '拼团订单', '2', 'trade_order_type', 0, 'default', '', '交易订单的类型 - 拼团订单', '1', '2022-12-10 16:34:36', '1', '2022-12-10 16:34:36', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1202, 3, '砍价订单', '3', 'trade_order_type', 0, 'default', '', '交易订单的类型 - 砍价订单', '1', '2022-12-10 16:34:48', '1', '2022-12-10 16:34:48', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1203, 0, '待支付', '0', 'trade_order_status', 0, 'default', '', '交易订单状态 - 待支付', '1', '2022-12-10 16:49:29', '1', '2022-12-10 16:49:29', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1204, 10, '待发货', '10', 'trade_order_status', 0, 'primary', '', '交易订单状态 - 待发货', '1', '2022-12-10 16:49:53', '1', '2022-12-10 16:51:17', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1205, 20, '已发货', '20', 'trade_order_status', 0, 'primary', '', '交易订单状态 - 已发货', '1', '2022-12-10 16:50:13', '1', '2022-12-10 16:51:31', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1206, 30, '已完成', '30', 'trade_order_status', 0, 'success', '', '交易订单状态 - 已完成', '1', '2022-12-10 16:50:30', '1', '2022-12-10 16:51:06', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1207, 40, '已取消', '40', 'trade_order_status', 0, 'danger', '', '交易订单状态 - 已取消', '1', '2022-12-10 16:50:50', '1', '2022-12-10 16:51:00', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1208, 0, '未售后', '0', 'trade_order_item_after_sale_status', 0, 'info', '', '交易订单项的售后状态 - 未售后', '1', '2022-12-10 20:58:42', '1', '2022-12-10 20:59:29', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1209, 1, '售后中', '1', 'trade_order_item_after_sale_status', 0, 'primary', '', '交易订单项的售后状态 - 售后中', '1', '2022-12-10 20:59:21', '1', '2022-12-10 20:59:21', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1210, 2, '已退款', '2', 'trade_order_item_after_sale_status', 0, 'success', '', '交易订单项的售后状态 - 已退款', '1', '2022-12-10 20:59:46', '1', '2022-12-10 20:59:46', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1211, 1, '完全匹配', '1', 'mp_auto_reply_request_match', 0, 'primary', '', '公众号自动回复的请求关键字匹配模式 - 完全匹配', '1', '2023-01-16 23:30:39', '1', '2023-01-16 23:31:00', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1212, 2, '半匹配', '2', 'mp_auto_reply_request_match', 0, 'success', '', '公众号自动回复的请求关键字匹配模式 - 半匹配', '1', '2023-01-16 23:30:55', '1', '2023-01-16 23:31:10', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1213, 1, '文本', 'text', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 文本', '1', '2023-01-17 22:17:32', '1', '2023-01-17 22:17:39', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1214, 2, '图片', 'image', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 图片', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:19:47', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1215, 3, '语音', 'voice', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 语音', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:20:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1216, 4, '视频', 'video', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 视频', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:21:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1217, 5, '小视频', 'shortvideo', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 小视频', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:19:59', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1218, 6, '图文', 'news', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 图文', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:22:54', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1219, 7, '音乐', 'music', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 音乐', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:22:54', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1220, 8, '地理位置', 'location', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 地理位置', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:23:51', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1221, 9, '链接', 'link', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 链接', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:24:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1222, 10, '事件', 'event', 'mp_message_type', 0, 'default', '', '公众号的消息类型 - 事件', '1', '2023-01-17 22:17:32', '1', '2023-01-17 14:24:49', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1223, 0, '初始化', '0', 'system_mail_send_status', 0, 'primary', '', '邮件发送状态 - 初始化\n', '1', '2023-01-26 09:53:49', '1', '2023-01-26 16:36:14', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1224, 10, '发送成功', '10', 'system_mail_send_status', 0, 'success', '', '邮件发送状态 - 发送成功', '1', '2023-01-26 09:54:28', '1', '2023-01-26 16:36:22', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1225, 20, '发送失败', '20', 'system_mail_send_status', 0, 'danger', '', '邮件发送状态 - 发送失败', '1', '2023-01-26 09:54:50', '1', '2023-01-26 16:36:26', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1226, 30, '不发送', '30', 'system_mail_send_status', 0, 'info', '', '邮件发送状态 - 不发送', '1', '2023-01-26 09:55:06', '1', '2023-01-26 16:36:36', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1227, 1, '通知公告', '1', 'system_notify_template_type', 0, 'primary', '', '站内信模版的类型 - 通知公告', '1', '2023-01-28 10:35:59', '1', '2023-01-28 10:35:59', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1228, 2, '系统消息', '2', 'system_notify_template_type', 0, 'success', '', '站内信模版的类型 - 系统消息', '1', '2023-01-28 10:36:20', '1', '2023-01-28 10:36:25', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1229, 0, '模拟支付', 'mock', 'pay_channel_code', 0, 'default', '', '模拟支付', '1', '2023-02-12 21:50:22', '1', '2023-07-10 10:11:02', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1230, 13, '支付宝条码支付', 'alipay_bar', 'pay_channel_code', 0, 'primary', '', '支付宝条码支付', '1', '2023-02-18 23:32:24', '1', '2023-07-19 20:09:23', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1231, 10, 'Vue2 Element UI 标准模版', '10', 'infra_codegen_front_type', 0, '', '', '', '1', '2023-04-13 00:03:55', '1', '2023-04-13 00:03:55', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1232, 20, 'Vue3 Element Plus 标准模版', '20', 'infra_codegen_front_type', 0, '', '', '', '1', '2023-04-13 00:04:08', '1', '2023-04-13 00:04:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1233, 21, 'Vue3 Element Plus Schema 模版', '21', 'infra_codegen_front_type', 0, '', '', '', '1', '2023-04-13 00:04:26', '1', '2023-04-13 00:04:26', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1234, 30, 'Vue3 vben 模版', '30', 'infra_codegen_front_type', 0, '', '', '', '1', '2023-04-13 00:04:26', '1', '2023-04-13 00:04:26', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1235, 1, '个', '1', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1236, 1, '件', '2', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1237, 1, '盒', '3', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1238, 1, '袋', '4', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1239, 1, '箱', '5', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1240, 1, '套', '6', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1241, 1, '包', '7', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1242, 1, '双', '8', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1243, 1, '卷', '9', 'product_unit', 0, '', '', '', '1', '2023-05-23 14:38:38', '1', '2023-05-23 14:38:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1244, 0, '按件', '1', 'trade_delivery_express_charge_mode', 0, '', '', '', '1', '2023-05-21 22:46:40', '1', '2023-05-21 22:46:40', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1245, 1, '按重量', '2', 'trade_delivery_express_charge_mode', 0, '', '', '', '1', '2023-05-21 22:46:58', '1', '2023-05-21 22:46:58', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1246, 2, '按体积', '3', 'trade_delivery_express_charge_mode', 0, '', '', '', '1', '2023-05-21 22:47:18', '1', '2023-05-21 22:47:18', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1335, 1, '购物', '1', 'member_point_biz_type', 0, '', '', '', '1', '2023-06-10 12:15:27', '1', '2023-06-28 13:48:28', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1336, 2, '签到', '2', 'member_point_biz_type', 0, '', '', '', '1', '2023-06-10 12:15:48', '1', '2023-06-28 13:48:31', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1337, 1, '订单创建', '1', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:16:42', '1', '2023-06-28 13:48:34', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1338, 2, '冻结期', '2', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:16:58', '1', '2023-06-28 13:48:36', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1339, 3, '完成', '3', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:17:07', '1', '2023-06-28 13:48:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1340, 4, '失效(订单退款)', '4', 'member_point_status', 0, '', '', '', '1', '2023-06-10 12:17:21', '1', '2023-06-28 13:48:42', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1341, 20, '已退款', '20', 'pay_order_status', 0, 'danger', '', '已退款', '1', '2023-07-19 18:05:37', '1', '2023-07-19 18:05:37', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1342, 21, '请求成功,但是结果失败', '21', 'pay_notify_status', 0, 'warning', '', '请求成功,但是结果失败', '1', '2023-07-19 18:10:47', '1', '2023-07-19 18:11:38', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1343, 22, '请求失败', '22', 'pay_notify_status', 0, 'warning', '', NULL, '1', '2023-07-19 18:11:05', '1', '2023-07-19 18:11:27', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1344, 4, '微信扫码支付', 'wx_native', 'pay_channel_code', 0, 'success', '', '微信扫码支付', '1', '2023-07-19 20:07:47', '1', '2023-07-19 20:09:03', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1345, 5, '微信条码支付', 'wx_bar', 'pay_channel_code', 0, 'success', '', '微信条码支付\n', '1', '2023-07-19 20:08:06', '1', '2023-07-19 20:09:08', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1346, 1, '支付单', '1', 'pay_notify_type', 0, 'primary', '', '支付单', '1', '2023-07-20 12:23:17', '1', '2023-07-20 12:23:17', b'0'); +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1347, 2, '退款单', '2', 'pay_notify_type', 0, 'danger', '', NULL, '1', '2023-07-20 12:23:26', '1', '2023-07-20 12:23:26', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_dict_type +-- ---------------------------- +DROP TABLE IF EXISTS `system_dict_type`; +CREATE TABLE `system_dict_type` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '字典主键', + `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '字典名称', + `type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '字典类型', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态(0正常 1停用)', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `dict_type`(`type` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 174 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表'; + +-- ---------------------------- +-- Records of system_dict_type +-- ---------------------------- +BEGIN; +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (1, '用户性别', 'system_user_sex', 0, NULL, 'admin', '2021-01-05 17:03:48', '1', '2022-05-16 20:29:32', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (6, '参数类型', 'infra_config_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:36:54', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (7, '通知类型', 'system_notice_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:35:26', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (9, '操作类型', 'system_operate_type', 0, NULL, 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 09:32:21', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (10, '系统状态', 'common_status', 0, NULL, 'admin', '2021-01-05 17:03:48', '', '2022-02-01 16:21:28', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (11, 'Boolean 是否类型', 'infra_boolean_string', 0, 'boolean 转是否', '', '2021-01-19 03:20:08', '', '2022-02-01 16:37:10', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (104, '登陆结果', 'system_login_result', 0, '登陆结果', '', '2021-01-18 06:17:11', '', '2022-02-01 16:36:00', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (105, 'Redis 超时类型', 'infra_redis_timeout_type', 0, 'RedisKeyDefine.TimeoutTypeEnum', '', '2021-01-26 00:52:50', '', '2022-02-01 16:50:29', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (106, '代码生成模板类型', 'infra_codegen_template_type', 0, NULL, '', '2021-02-05 07:08:06', '1', '2022-05-16 20:26:50', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (107, '定时任务状态', 'infra_job_status', 0, NULL, '', '2021-02-07 07:44:16', '', '2022-02-01 16:51:11', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (108, '定时任务日志状态', 'infra_job_log_status', 0, NULL, '', '2021-02-08 10:03:51', '', '2022-02-01 16:50:43', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (109, '用户类型', 'user_type', 0, NULL, '', '2021-02-26 00:15:51', '', '2021-02-26 00:15:51', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (110, 'API 异常数据的处理状态', 'infra_api_error_log_process_status', 0, NULL, '', '2021-02-26 07:07:01', '', '2022-02-01 16:50:53', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (111, '短信渠道编码', 'system_sms_channel_code', 0, NULL, '1', '2021-04-05 01:04:50', '1', '2022-02-16 02:09:08', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (112, '短信模板的类型', 'system_sms_template_type', 0, NULL, '1', '2021-04-05 21:50:43', '1', '2022-02-01 16:35:06', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (113, '短信发送状态', 'system_sms_send_status', 0, NULL, '1', '2021-04-11 20:18:03', '1', '2022-02-01 16:35:09', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (114, '短信接收状态', 'system_sms_receive_status', 0, NULL, '1', '2021-04-11 20:27:14', '1', '2022-02-01 16:35:14', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (115, '错误码的类型', 'system_error_code_type', 0, NULL, '1', '2021-04-21 00:06:30', '1', '2022-02-01 16:36:49', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (116, '登陆日志的类型', 'system_login_type', 0, '登陆日志的类型', '1', '2021-10-06 00:50:46', '1', '2022-02-01 16:35:56', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (117, 'OA 请假类型', 'bpm_oa_leave_type', 0, NULL, '1', '2021-09-21 22:34:33', '1', '2022-01-22 10:41:37', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (130, '支付渠道编码类型', 'pay_channel_code', 0, '支付渠道的编码', '1', '2021-12-03 10:35:08', '1', '2023-07-10 10:11:39', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (131, '支付回调状态', 'pay_notify_status', 0, '支付回调状态(包括退款回调)', '1', '2021-12-03 10:53:29', '1', '2023-07-19 18:09:43', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (132, '支付订单状态', 'pay_order_status', 0, '支付订单状态', '1', '2021-12-03 11:17:50', '1', '2021-12-03 11:17:50', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (134, '退款订单状态', 'pay_refund_status', 0, '退款订单状态', '1', '2021-12-10 16:42:50', '1', '2023-07-19 10:13:17', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (138, '流程分类', 'bpm_model_category', 0, '流程分类', '1', '2022-01-02 08:40:45', '1', '2022-01-02 08:40:45', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (139, '流程实例的状态', 'bpm_process_instance_status', 0, '流程实例的状态', '1', '2022-01-07 23:46:42', '1', '2022-01-07 23:46:42', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (140, '流程实例的结果', 'bpm_process_instance_result', 0, '流程实例的结果', '1', '2022-01-07 23:48:10', '1', '2022-01-07 23:48:10', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (141, '流程的表单类型', 'bpm_model_form_type', 0, '流程的表单类型', '103', '2022-01-11 23:50:45', '103', '2022-01-11 23:50:45', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (142, '任务分配规则的类型', 'bpm_task_assign_rule_type', 0, '任务分配规则的类型', '103', '2022-01-12 23:21:04', '103', '2022-01-12 15:46:10', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (143, '任务分配自定义脚本', 'bpm_task_assign_script', 0, '任务分配自定义脚本', '103', '2022-01-15 00:10:35', '103', '2022-01-15 00:10:35', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (144, '代码生成的场景枚举', 'infra_codegen_scene', 0, '代码生成的场景枚举', '1', '2022-02-02 13:14:45', '1', '2022-03-10 16:33:46', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (145, '角色类型', 'system_role_type', 0, '角色类型', '1', '2022-02-16 13:01:46', '1', '2022-02-16 13:01:46', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (146, '文件存储器', 'infra_file_storage', 0, '文件存储器', '1', '2022-03-15 00:24:38', '1', '2022-03-15 00:24:38', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (147, 'OAuth 2.0 授权类型', 'system_oauth2_grant_type', 0, 'OAuth 2.0 授权类型(模式)', '1', '2022-05-12 00:20:52', '1', '2022-05-11 16:25:49', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (149, '商品 SPU 状态', 'product_spu_status', 0, '商品 SPU 状态', '1', '2022-10-24 21:19:04', '1', '2022-10-24 21:19:08', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (150, '优惠类型', 'promotion_discount_type', 0, '优惠类型', '1', '2022-11-01 12:46:06', '1', '2022-11-01 12:46:06', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (151, '优惠劵模板的有限期类型', 'promotion_coupon_template_validity_type', 0, '优惠劵模板的有限期类型', '1', '2022-11-02 00:06:20', '1', '2022-11-04 00:08:26', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (152, '营销的商品范围', 'promotion_product_scope', 0, '营销的商品范围', '1', '2022-11-02 00:28:01', '1', '2022-11-02 00:28:01', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (153, '优惠劵的状态', 'promotion_coupon_status', 0, '优惠劵的状态', '1', '2022-11-04 00:14:49', '1', '2022-11-04 00:14:49', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (154, '优惠劵的领取方式', 'promotion_coupon_take_type', 0, '优惠劵的领取方式', '1', '2022-11-04 19:12:27', '1', '2022-11-04 19:12:27', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (155, '促销活动的状态', 'promotion_activity_status', 0, '促销活动的状态', '1', '2022-11-04 22:54:23', '1', '2022-11-04 22:54:23', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (156, '营销的条件类型', 'promotion_condition_type', 0, '营销的条件类型', '1', '2022-11-04 22:59:23', '1', '2022-11-04 22:59:23', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (157, '交易售后状态', 'trade_after_sale_status', 0, '交易售后状态', '1', '2022-11-19 20:52:56', '1', '2022-11-19 20:52:56', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (158, '交易售后的类型', 'trade_after_sale_type', 0, '交易售后的类型', '1', '2022-11-19 21:04:09', '1', '2022-11-19 21:04:09', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (159, '交易售后的方式', 'trade_after_sale_way', 0, '交易售后的方式', '1', '2022-11-19 21:39:04', '1', '2022-11-19 21:39:04', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (160, '终端', 'terminal', 0, '终端', '1', '2022-12-10 10:50:50', '1', '2022-12-10 10:53:11', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (161, '交易订单的类型', 'trade_order_type', 0, '交易订单的类型', '1', '2022-12-10 16:33:54', '1', '2022-12-10 16:33:54', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (162, '交易订单的状态', 'trade_order_status', 0, '交易订单的状态', '1', '2022-12-10 16:48:44', '1', '2022-12-10 16:48:44', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (163, '交易订单项的售后状态', 'trade_order_item_after_sale_status', 0, '交易订单项的售后状态', '1', '2022-12-10 20:58:08', '1', '2022-12-10 20:58:08', b'0', NULL); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (164, '公众号自动回复的请求关键字匹配模式', 'mp_auto_reply_request_match', 0, '公众号自动回复的请求关键字匹配模式', '1', '2023-01-16 23:29:56', '1', '2023-01-16 23:29:56', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (165, '公众号的消息类型', 'mp_message_type', 0, '公众号的消息类型', '1', '2023-01-17 22:17:09', '1', '2023-01-17 22:17:09', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (166, '邮件发送状态', 'system_mail_send_status', 0, '邮件发送状态', '1', '2023-01-26 09:53:13', '1', '2023-01-26 09:53:13', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (167, '站内信模版的类型', 'system_notify_template_type', 0, '站内信模版的类型', '1', '2023-01-28 10:35:10', '1', '2023-01-28 10:35:10', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (168, '代码生成的前端类型', 'infra_codegen_front_type', 0, '', '1', '2023-04-12 23:57:52', '1', '2023-04-12 23:57:52', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (169, '商品的单位', 'product_unit', 0, '商品的单位', '1', '2023-05-24 21:23:59', '1', '2023-05-24 21:23:59', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (170, '快递计费方式', 'trade_delivery_express_charge_mode', 0, '用于商城交易模块配送管理', '1', '2023-05-21 22:45:03', '1', '2023-05-21 22:45:03', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (171, '积分业务类型', 'member_point_biz_type', 0, '', '1', '2023-06-10 12:15:00', '1', '2023-06-28 13:48:20', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (172, '积分订单状态', 'member_point_status', 0, '', '1', '2023-06-10 12:16:27', '1', '2023-06-28 13:48:17', b'0', '1970-01-01 00:00:00'); +INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (173, '支付通知类型', 'pay_notify_type', 0, NULL, '1', '2023-07-20 12:23:03', '1', '2023-07-20 12:23:03', b'0', '1970-01-01 00:00:00'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_error_code +-- ---------------------------- +DROP TABLE IF EXISTS `system_error_code`; +CREATE TABLE `system_error_code` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '错误码编号', + `type` tinyint NOT NULL DEFAULT 0 COMMENT '错误码类型', + `application_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '应用名', + `code` int NOT NULL DEFAULT 0 COMMENT '错误码编码', + `message` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '错误码错误提示', + `memo` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5833 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '错误码表'; + +-- ---------------------------- +-- Records of system_error_code +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_login_log +-- ---------------------------- +DROP TABLE IF EXISTS `system_login_log`; +CREATE TABLE `system_login_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '访问ID', + `log_type` bigint NOT NULL COMMENT '日志类型', + `trace_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '链路追踪编号', + `user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户编号', + `user_type` tinyint NOT NULL DEFAULT 0 COMMENT '用户类型', + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '用户账号', + `result` tinyint NOT NULL COMMENT '登陆结果', + `user_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户 IP', + `user_agent` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '浏览器 UA', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2243 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录'; + +-- ---------------------------- +-- Records of system_login_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_mail_account +-- ---------------------------- +DROP TABLE IF EXISTS `system_mail_account`; +CREATE TABLE `system_mail_account` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮箱', + `username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名', + `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码', + `host` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'SMTP 服务器域名', + `port` int NOT NULL COMMENT 'SMTP 服务器端口', + `ssl_enable` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否开启 SSL', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮箱账号表'; + +-- ---------------------------- +-- Records of system_mail_account +-- ---------------------------- +BEGIN; +INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '7684413@qq.com', '7684413@qq.com', '123457', '127.0.0.1', 8080, b'0', '1', '2023-01-25 17:39:52', '1', '2023-04-12 23:04:49', b'0'); +INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'ydym_test@163.com', 'ydym_test@163.com', 'WBZTEINMIFVRYSOE', 'smtp.163.com', 465, b'1', '1', '2023-01-26 01:26:03', '1', '2023-04-12 22:39:38', b'0'); +INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3, '76854114@qq.com', '3335', '11234', 'yunai1.cn', 466, b'0', '1', '2023-01-27 15:06:38', '1', '2023-01-27 07:08:36', b'1'); +INSERT INTO `system_mail_account` (`id`, `mail`, `username`, `password`, `host`, `port`, `ssl_enable`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '7685413x@qq.com', '2', '3', '4', 5, b'1', '1', '2023-04-12 23:05:06', '1', '2023-04-12 15:05:11', b'1'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_mail_log +-- ---------------------------- +DROP TABLE IF EXISTS `system_mail_log`; +CREATE TABLE `system_mail_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `user_id` bigint NULL DEFAULT NULL COMMENT '用户编号', + `user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型', + `to_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '接收邮箱地址', + `account_id` bigint NOT NULL COMMENT '邮箱账号编号', + `from_mail` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送邮箱地址', + `template_id` bigint NOT NULL COMMENT '模板编号', + `template_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码', + `template_nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模版发送人名称', + `template_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件标题', + `template_content` varchar(10240) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件内容', + `template_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮件参数', + `send_status` tinyint NOT NULL DEFAULT 0 COMMENT '发送状态', + `send_time` datetime NULL DEFAULT NULL COMMENT '发送时间', + `send_message_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发送返回的消息 ID', + `send_exception` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发送异常', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 354 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮件日志表'; + +-- ---------------------------- +-- Records of system_mail_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_mail_template +-- ---------------------------- +DROP TABLE IF EXISTS `system_mail_template`; +CREATE TABLE `system_mail_template` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `name` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板名称', + `code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码', + `account_id` bigint NOT NULL COMMENT '发送的邮箱账号编号', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发送人名称', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板标题', + `content` varchar(10240) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板内容', + `params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数数组', + `status` tinyint NOT NULL COMMENT '开启状态', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮件模版表'; + +-- ---------------------------- +-- Records of system_mail_template +-- ---------------------------- +BEGIN; +INSERT INTO `system_mail_template` (`id`, `name`, `code`, `account_id`, `nickname`, `title`, `content`, `params`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (13, '后台用户短信登录', 'admin-sms-login', 1, '奥特曼', '你猜我猜', '

您的验证码是{code},名字是{name}

', '[\"code\",\"name\"]', 0, '3', '1', '2021-10-11 08:10:00', '1', '2023-01-26 23:22:05', b'0'); +INSERT INTO `system_mail_template` (`id`, `name`, `code`, `account_id`, `nickname`, `title`, `content`, `params`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (14, '测试模版', 'test_01', 2, '芋艿', '一个标题', '

你是 {key01} 吗?


是的话,赶紧 {key02} 一下!

', '[\"key01\",\"key02\"]', 0, NULL, '1', '2023-01-26 01:27:40', '1', '2023-01-27 10:32:16', b'0'); +INSERT INTO `system_mail_template` (`id`, `name`, `code`, `account_id`, `nickname`, `title`, `content`, `params`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (15, '3', '2', 2, '7', '4', '

45

', '[]', 1, '80', '1', '2023-01-27 15:50:35', '1', '2023-01-27 16:34:49', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_menu +-- ---------------------------- +DROP TABLE IF EXISTS `system_menu`; +CREATE TABLE `system_menu` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称', + `permission` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '权限标识', + `type` tinyint NOT NULL COMMENT '菜单类型', + `sort` int NOT NULL DEFAULT 0 COMMENT '显示顺序', + `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '父菜单ID', + `path` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '路由地址', + `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '#' COMMENT '菜单图标', + `component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '组件路径', + `component_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '组件名', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '菜单状态', + `visible` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否可见', + `keep_alive` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否缓存', + `always_show` bit(1) NOT NULL DEFAULT b'1' COMMENT '是否总是显示', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2303 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表'; + +-- ---------------------------- +-- Records of system_menu +-- ---------------------------- +BEGIN; +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '系统管理', '', 1, 10, 0, '/system', 'system', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, '基础设施', '', 1, 20, 0, '/infra', 'monitor', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5, 'OA 示例', '', 1, 40, 1185, 'oa', 'people', NULL, NULL, 0, b'1', b'1', b'1', 'admin', '2021-09-20 16:26:19', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (100, '用户管理', 'system:user:list', 2, 1, 1, 'user', 'user', 'system/user/index', 'SystemUser', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:31:59', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (101, '角色管理', '', 2, 2, 1, 'role', 'peoples', 'system/role/index', 'SystemRole', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:33:59', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (102, '菜单管理', '', 2, 3, 1, 'menu', 'tree-table', 'system/menu/index', 'SystemMenu', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:34:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (103, '部门管理', '', 2, 4, 1, 'dept', 'tree', 'system/dept/index', 'SystemDept', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:35:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (104, '岗位管理', '', 2, 5, 1, 'post', 'post', 'system/post/index', 'SystemPost', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:36:21', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (105, '字典管理', '', 2, 6, 1, 'dict', 'dict', 'system/dict/index', 'SystemDictType', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:36:45', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (106, '配置管理', '', 2, 6, 2, 'config', 'edit', 'infra/config/index', 'InfraConfig', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 10:31:17', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (107, '通知公告', '', 2, 8, 1, 'notice', 'message', 'system/notice/index', 'SystemNotice', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:45:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (108, '审计日志', '', 1, 9, 1, 'log', 'log', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (109, '令牌管理', '', 2, 2, 1261, 'token', 'online', 'system/oauth2/token/index', 'SystemTokenClient', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:47:41', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (110, '定时任务', '', 2, 12, 2, 'job', 'job', 'infra/job/index', 'InfraJob', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 10:36:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (111, 'MySQL 监控', '', 2, 9, 2, 'druid', 'druid', 'infra/druid/index', 'InfraDruid', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 09:09:30', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (112, 'Java 监控', '', 2, 11, 2, 'admin-server', 'server', 'infra/server/index', 'InfraAdminServer', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 10:34:08', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (113, 'Redis 监控', '', 2, 10, 2, 'redis', 'redis', 'infra/redis/index', 'InfraRedis', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 10:33:30', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (114, '表单构建', 'infra:build:list', 2, 2, 2, 'build', 'build', 'infra/build/index', 'InfraBuild', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 09:06:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (115, '代码生成', 'infra:codegen:query', 2, 1, 2, 'codegen', 'code', 'infra/codegen/index', 'InfraCodegen', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 09:02:24', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (116, '系统接口', 'infra:swagger:list', 2, 3, 2, 'swagger', 'swagger', 'infra/swagger/index', 'InfraSwagger', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 09:11:28', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (500, '操作日志', '', 2, 1, 108, 'operate-log', 'form', 'system/operatelog/index', 'SystemOperateLog', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:47:00', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (501, '登录日志', '', 2, 2, 108, 'login-log', 'logininfor', 'system/loginlog/index', 'SystemLoginLog', 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2023-04-08 08:46:18', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1001, '用户查询', 'system:user:query', 3, 1, 100, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1002, '用户新增', 'system:user:create', 3, 2, 100, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1003, '用户修改', 'system:user:update', 3, 3, 100, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1004, '用户删除', 'system:user:delete', 3, 4, 100, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1005, '用户导出', 'system:user:export', 3, 5, 100, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1006, '用户导入', 'system:user:import', 3, 6, 100, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1007, '重置密码', 'system:user:update-password', 3, 7, 100, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1008, '角色查询', 'system:role:query', 3, 1, 101, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1009, '角色新增', 'system:role:create', 3, 2, 101, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1010, '角色修改', 'system:role:update', 3, 3, 101, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1011, '角色删除', 'system:role:delete', 3, 4, 101, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1012, '角色导出', 'system:role:export', 3, 5, 101, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1013, '菜单查询', 'system:menu:query', 3, 1, 102, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1014, '菜单新增', 'system:menu:create', 3, 2, 102, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1015, '菜单修改', 'system:menu:update', 3, 3, 102, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1016, '菜单删除', 'system:menu:delete', 3, 4, 102, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1017, '部门查询', 'system:dept:query', 3, 1, 103, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1018, '部门新增', 'system:dept:create', 3, 2, 103, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1019, '部门修改', 'system:dept:update', 3, 3, 103, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1020, '部门删除', 'system:dept:delete', 3, 4, 103, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1021, '岗位查询', 'system:post:query', 3, 1, 104, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1022, '岗位新增', 'system:post:create', 3, 2, 104, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1023, '岗位修改', 'system:post:update', 3, 3, 104, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1024, '岗位删除', 'system:post:delete', 3, 4, 104, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1025, '岗位导出', 'system:post:export', 3, 5, 104, '', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1026, '字典查询', 'system:dict:query', 3, 1, 105, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1027, '字典新增', 'system:dict:create', 3, 2, 105, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1028, '字典修改', 'system:dict:update', 3, 3, 105, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1029, '字典删除', 'system:dict:delete', 3, 4, 105, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1030, '字典导出', 'system:dict:export', 3, 5, 105, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1031, '配置查询', 'infra:config:query', 3, 1, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1032, '配置新增', 'infra:config:create', 3, 2, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1033, '配置修改', 'infra:config:update', 3, 3, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1034, '配置删除', 'infra:config:delete', 3, 4, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1035, '配置导出', 'infra:config:export', 3, 5, 106, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1036, '公告查询', 'system:notice:query', 3, 1, 107, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1037, '公告新增', 'system:notice:create', 3, 2, 107, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1038, '公告修改', 'system:notice:update', 3, 3, 107, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1039, '公告删除', 'system:notice:delete', 3, 4, 107, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1040, '操作查询', 'system:operate-log:query', 3, 1, 500, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1042, '日志导出', 'system:operate-log:export', 3, 2, 500, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1043, '登录查询', 'system:login-log:query', 3, 1, 501, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1045, '日志导出', 'system:login-log:export', 3, 3, 501, '#', '#', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1046, '令牌列表', 'system:oauth2-token:page', 3, 1, 109, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-05-09 23:54:42', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1048, '令牌删除', 'system:oauth2-token:delete', 3, 2, 109, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-05-09 23:54:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1050, '任务新增', 'infra:job:create', 3, 2, 110, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1051, '任务修改', 'infra:job:update', 3, 3, 110, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1052, '任务删除', 'infra:job:delete', 3, 4, 110, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1053, '状态修改', 'infra:job:update', 3, 5, 110, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1054, '任务导出', 'infra:job:export', 3, 7, 110, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1056, '生成修改', 'infra:codegen:update', 3, 2, 115, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1057, '生成删除', 'infra:codegen:delete', 3, 3, 115, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1058, '导入代码', 'infra:codegen:create', 3, 2, 115, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1059, '预览代码', 'infra:codegen:preview', 3, 4, 115, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1060, '生成代码', 'infra:codegen:download', 3, 5, 115, '', '', '', NULL, 0, b'1', b'1', b'1', 'admin', '2021-01-05 17:03:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1063, '设置角色菜单权限', 'system:permission:assign-role-menu', 3, 6, 101, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-06 17:53:44', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1064, '设置角色数据权限', 'system:permission:assign-role-data-scope', 3, 7, 101, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-06 17:56:31', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1065, '设置用户角色', 'system:permission:assign-user-role', 3, 8, 101, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-07 10:23:28', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1066, '获得 Redis 监控信息', 'infra:redis:get-monitor-info', 3, 1, 113, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-26 01:02:31', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1067, '获得 Redis Key 列表', 'infra:redis:get-key-list', 3, 2, 113, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-26 01:02:52', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1070, '代码生成示例', 'infra:test-demo:query', 2, 1, 2, 'test-demo', 'validCode', 'infra/testDemo/index', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1071, '测试示例表创建', 'infra:test-demo:create', 3, 1, 1070, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1072, '测试示例表更新', 'infra:test-demo:update', 3, 2, 1070, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1073, '测试示例表删除', 'infra:test-demo:delete', 3, 3, 1070, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1074, '测试示例表导出', 'infra:test-demo:export', 3, 4, 1070, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1075, '任务触发', 'infra:job:trigger', 3, 8, 110, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-07 13:03:10', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1076, '数据库文档', '', 2, 4, 2, 'db-doc', 'table', 'infra/dbDoc/index', 'InfraDBDoc', 0, b'1', b'1', b'1', '', '2021-02-08 01:41:47', '1', '2023-04-08 09:13:38', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1077, '监控平台', '', 2, 13, 2, 'skywalking', 'eye-open', 'infra/skywalking/index', 'InfraSkyWalking', 0, b'1', b'1', b'1', '', '2021-02-08 20:41:31', '1', '2023-04-08 10:39:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1078, '访问日志', '', 2, 1, 1083, 'api-access-log', 'log', 'infra/apiAccessLog/index', 'InfraApiAccessLog', 0, b'1', b'1', b'1', '', '2021-02-26 01:32:59', '1', '2023-04-08 10:31:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1082, '日志导出', 'infra:api-access-log:export', 3, 2, 1078, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-26 01:32:59', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1083, 'API 日志', '', 2, 8, 2, 'log', 'log', NULL, NULL, 0, b'1', b'1', b'1', '', '2021-02-26 02:18:24', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1084, '错误日志', 'infra:api-error-log:query', 2, 2, 1083, 'api-error-log', 'log', 'infra/apiErrorLog/index', 'InfraApiErrorLog', 0, b'1', b'1', b'1', '', '2021-02-26 07:53:20', '1', '2023-04-08 10:32:25', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1085, '日志处理', 'infra:api-error-log:update-status', 3, 2, 1084, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-26 07:53:20', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1086, '日志导出', 'infra:api-error-log:export', 3, 3, 1084, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-26 07:53:20', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1087, '任务查询', 'infra:job:query', 3, 1, 110, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2021-03-10 01:26:19', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1088, '日志查询', 'infra:api-access-log:query', 3, 1, 1078, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2021-03-10 01:28:04', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1089, '日志查询', 'infra:api-error-log:query', 3, 1, 1084, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2021-03-10 01:29:09', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1090, '文件列表', '', 2, 5, 1243, 'file', 'upload', 'infra/file/index', 'InfraFile', 0, b'1', b'1', b'1', '', '2021-03-12 20:16:20', '1', '2023-04-08 09:21:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1091, '文件查询', 'infra:file:query', 3, 1, 1090, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-03-12 20:16:20', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1092, '文件删除', 'infra:file:delete', 3, 4, 1090, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-03-12 20:16:20', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1093, '短信管理', '', 1, 11, 1, 'sms', 'validCode', NULL, NULL, 0, b'1', b'1', b'1', '1', '2021-04-05 01:10:16', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1094, '短信渠道', '', 2, 0, 1093, 'sms-channel', 'phone', 'system/sms/channel/index', 'SystemSmsChannel', 0, b'1', b'1', b'1', '', '2021-04-01 11:07:15', '1', '2023-04-08 08:50:41', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1095, '短信渠道查询', 'system:sms-channel:query', 3, 1, 1094, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 11:07:15', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1096, '短信渠道创建', 'system:sms-channel:create', 3, 2, 1094, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 11:07:15', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1097, '短信渠道更新', 'system:sms-channel:update', 3, 3, 1094, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 11:07:15', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1098, '短信渠道删除', 'system:sms-channel:delete', 3, 4, 1094, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 11:07:15', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1100, '短信模板', '', 2, 1, 1093, 'sms-template', 'phone', 'system/sms/template/index', 'SystemSmsTemplate', 0, b'1', b'1', b'1', '', '2021-04-01 17:35:17', '1', '2023-04-08 08:50:50', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1101, '短信模板查询', 'system:sms-template:query', 3, 1, 1100, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 17:35:17', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1102, '短信模板创建', 'system:sms-template:create', 3, 2, 1100, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 17:35:17', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1103, '短信模板更新', 'system:sms-template:update', 3, 3, 1100, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 17:35:17', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1104, '短信模板删除', 'system:sms-template:delete', 3, 4, 1100, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 17:35:17', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1105, '短信模板导出', 'system:sms-template:export', 3, 5, 1100, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-01 17:35:17', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1106, '发送测试短信', 'system:sms-template:send-sms', 3, 6, 1100, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2021-04-11 00:26:40', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1107, '短信日志', '', 2, 2, 1093, 'sms-log', 'phone', 'system/sms/log/index', 'SystemSmsLog', 0, b'1', b'1', b'1', '', '2021-04-11 08:37:05', '1', '2023-04-08 08:50:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1108, '短信日志查询', 'system:sms-log:query', 3, 1, 1107, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-11 08:37:05', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1109, '短信日志导出', 'system:sms-log:export', 3, 5, 1107, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-11 08:37:05', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1110, '错误码管理', '', 2, 12, 1, 'error-code', 'code', 'system/errorCode/index', 'SystemErrorCode', 0, b'1', b'1', b'1', '', '2021-04-13 21:46:42', '1', '2023-04-08 09:01:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1111, '错误码查询', 'system:error-code:query', 3, 1, 1110, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-13 21:46:42', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1112, '错误码创建', 'system:error-code:create', 3, 2, 1110, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-13 21:46:42', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1113, '错误码更新', 'system:error-code:update', 3, 3, 1110, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-13 21:46:42', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1114, '错误码删除', 'system:error-code:delete', 3, 4, 1110, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-13 21:46:42', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1115, '错误码导出', 'system:error-code:export', 3, 5, 1110, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-04-13 21:46:42', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1117, '支付管理', '', 1, 30, 0, '/pay', 'money', NULL, NULL, 0, b'1', b'1', b'1', '1', '2021-12-25 16:43:41', '1', '2022-12-10 16:33:19', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1118, '请假查询', '', 2, 0, 5, 'leave', 'user', 'bpm/oa/leave/index', 'BpmOALeave', 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2023-04-08 11:30:40', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1119, '请假申请查询', 'bpm:oa-leave:query', 3, 1, 1118, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1120, '请假申请创建', 'bpm:oa-leave:create', 3, 2, 1118, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-09-20 08:51:03', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1126, '应用信息', '', 2, 1, 1117, 'app', 'table', 'pay/app/index', 'PayApp', 0, b'1', b'1', b'1', '', '2021-11-10 01:13:30', '1', '2023-07-20 12:13:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1127, '支付应用信息查询', 'pay:app:query', 3, 1, 1126, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:31', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1128, '支付应用信息创建', 'pay:app:create', 3, 2, 1126, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:31', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1129, '支付应用信息更新', 'pay:app:update', 3, 3, 1126, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:31', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1130, '支付应用信息删除', 'pay:app:delete', 3, 4, 1126, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:31', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1132, '秘钥解析', 'pay:channel:parsing', 3, 6, 1129, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2021-11-08 15:15:47', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1133, '支付商户信息查询', 'pay:merchant:query', 3, 1, 1132, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:41', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1134, '支付商户信息创建', 'pay:merchant:create', 3, 2, 1132, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:41', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1135, '支付商户信息更新', 'pay:merchant:update', 3, 3, 1132, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:41', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1136, '支付商户信息删除', 'pay:merchant:delete', 3, 4, 1132, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:41', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1137, '支付商户信息导出', 'pay:merchant:export', 3, 5, 1132, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-11-10 01:13:41', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1138, '租户列表', '', 2, 0, 1224, 'list', 'peoples', 'system/tenant/index', 'SystemTenant', 0, b'1', b'1', b'1', '', '2021-12-14 12:31:43', '1', '2023-04-08 08:29:08', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1139, '租户查询', 'system:tenant:query', 3, 1, 1138, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-14 12:31:44', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1140, '租户创建', 'system:tenant:create', 3, 2, 1138, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-14 12:31:44', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1141, '租户更新', 'system:tenant:update', 3, 3, 1138, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-14 12:31:44', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1142, '租户删除', 'system:tenant:delete', 3, 4, 1138, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-14 12:31:44', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1143, '租户导出', 'system:tenant:export', 3, 5, 1138, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-14 12:31:44', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1150, '秘钥解析', '', 3, 6, 1129, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2021-11-08 15:15:47', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1161, '退款订单', '', 2, 3, 1117, 'refund', 'order', 'pay/refund/index', 'PayRefund', 0, b'1', b'1', b'1', '', '2021-12-25 08:29:07', '1', '2023-04-08 10:46:02', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1162, '退款订单查询', 'pay:refund:query', 3, 1, 1161, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:29:07', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1163, '退款订单创建', 'pay:refund:create', 3, 2, 1161, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:29:07', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1164, '退款订单更新', 'pay:refund:update', 3, 3, 1161, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:29:07', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1165, '退款订单删除', 'pay:refund:delete', 3, 4, 1161, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:29:07', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1166, '退款订单导出', 'pay:refund:export', 3, 5, 1161, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:29:07', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1173, '支付订单', '', 2, 2, 1117, 'order', 'pay', 'pay/order/index', 'PayOrder', 0, b'1', b'1', b'1', '', '2021-12-25 08:49:43', '1', '2023-04-08 10:43:37', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1174, '支付订单查询', 'pay:order:query', 3, 1, 1173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:49:43', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1175, '支付订单创建', 'pay:order:create', 3, 2, 1173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:49:43', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1176, '支付订单更新', 'pay:order:update', 3, 3, 1173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:49:43', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1177, '支付订单删除', 'pay:order:delete', 3, 4, 1173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:49:43', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1178, '支付订单导出', 'pay:order:export', 3, 5, 1173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-25 08:49:43', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1185, '工作流程', '', 1, 50, 0, '/bpm', 'tool', NULL, NULL, 0, b'1', b'1', b'1', '1', '2021-12-30 20:26:36', '103', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1186, '流程管理', '', 1, 10, 1185, 'manager', 'nested', NULL, NULL, 0, b'1', b'1', b'1', '1', '2021-12-30 20:28:30', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1187, '流程表单', '', 2, 0, 1186, 'form', 'form', 'bpm/form/index', 'BpmForm', 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2023-04-08 10:50:37', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1188, '表单查询', 'bpm:form:query', 3, 1, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1189, '表单创建', 'bpm:form:create', 3, 2, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1190, '表单更新', 'bpm:form:update', 3, 3, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1191, '表单删除', 'bpm:form:delete', 3, 4, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1192, '表单导出', 'bpm:form:export', 3, 5, 1187, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-12-30 12:38:22', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1193, '流程模型', '', 2, 5, 1186, 'model', 'guide', 'bpm/model/index', 'BpmModel', 0, b'1', b'1', b'1', '1', '2021-12-31 23:24:58', '1', '2023-04-08 10:53:38', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1194, '模型查询', 'bpm:model:query', 3, 1, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:10', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1195, '模型创建', 'bpm:model:create', 3, 2, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:24', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1196, '模型导入', 'bpm:model:import', 3, 3, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:01:35', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1197, '模型更新', 'bpm:model:update', 3, 4, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:28', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1198, '模型删除', 'bpm:model:delete', 3, 5, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:02:43', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1199, '模型发布', 'bpm:model:deploy', 3, 6, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-03 19:03:24', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1200, '任务管理', '', 1, 20, 1185, 'task', 'cascader', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-01-07 23:51:48', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1201, '我的流程', '', 2, 0, 1200, 'my', 'people', 'bpm/processInstance/index', 'BpmProcessInstance', 0, b'1', b'1', b'1', '', '2022-01-07 15:53:44', '1', '2023-04-08 11:16:55', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1202, '流程实例的查询', 'bpm:process-instance:query', 3, 1, 1201, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-07 15:53:44', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1207, '待办任务', '', 2, 10, 1200, 'todo', 'eye-open', 'bpm/task/todo/index', 'BpmTodoTask', 0, b'1', b'1', b'1', '1', '2022-01-08 10:33:37', '1', '2023-04-08 11:29:08', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1208, '已办任务', '', 2, 20, 1200, 'done', 'eye', 'bpm/task/done/index', 'BpmDoneTask', 0, b'1', b'1', b'1', '1', '2022-01-08 10:34:13', '1', '2023-04-08 11:29:00', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1209, '用户分组', '', 2, 2, 1186, 'user-group', 'people', 'bpm/group/index', 'BpmUserGroup', 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '1', '2023-04-08 10:51:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1210, '用户组查询', 'bpm:user-group:query', 3, 1, 1209, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1211, '用户组创建', 'bpm:user-group:create', 3, 2, 1209, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1212, '用户组更新', 'bpm:user-group:update', 3, 3, 1209, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1213, '用户组删除', 'bpm:user-group:delete', 3, 4, 1209, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-01-14 02:14:20', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1215, '流程定义查询', 'bpm:process-definition:query', 3, 10, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:21:43', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1216, '流程任务分配规则查询', 'bpm:task-assign-rule:query', 3, 20, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:26:53', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1217, '流程任务分配规则创建', 'bpm:task-assign-rule:create', 3, 21, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:28:15', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1218, '流程任务分配规则更新', 'bpm:task-assign-rule:update', 3, 22, 1193, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:28:41', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1219, '流程实例的创建', 'bpm:process-instance:create', 3, 2, 1201, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:36:15', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1220, '流程实例的取消', 'bpm:process-instance:cancel', 3, 3, 1201, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:36:33', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1221, '流程任务的查询', 'bpm:task:query', 3, 1, 1207, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:38:52', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1222, '流程任务的更新', 'bpm:task:update', 3, 2, 1207, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-01-23 00:39:24', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1224, '租户管理', '', 2, 0, 1, 'tenant', 'peoples', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-02-20 01:41:13', '1', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1225, '租户套餐', '', 2, 0, 1224, 'package', 'eye', 'system/tenantPackage/index', 'SystemTenantPackage', 0, b'1', b'1', b'1', '', '2022-02-19 17:44:06', '1', '2023-04-08 08:17:08', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1226, '租户套餐查询', 'system:tenant-package:query', 3, 1, 1225, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-02-19 17:44:06', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1227, '租户套餐创建', 'system:tenant-package:create', 3, 2, 1225, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-02-19 17:44:06', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1228, '租户套餐更新', 'system:tenant-package:update', 3, 3, 1225, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-02-19 17:44:06', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1229, '租户套餐删除', 'system:tenant-package:delete', 3, 4, 1225, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-02-19 17:44:06', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1237, '文件配置', '', 2, 0, 1243, 'file-config', 'config', 'infra/fileConfig/index', 'InfraFileConfig', 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '1', '2023-04-08 09:16:05', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1238, '文件配置查询', 'infra:file-config:query', 3, 1, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1239, '文件配置创建', 'infra:file-config:create', 3, 2, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1240, '文件配置更新', 'infra:file-config:update', 3, 3, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1241, '文件配置删除', 'infra:file-config:delete', 3, 4, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1242, '文件配置导出', 'infra:file-config:export', 3, 5, 1237, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-03-15 14:35:28', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1243, '文件管理', '', 2, 5, 2, 'file', 'download', NULL, '', 0, b'1', b'1', b'1', '1', '2022-03-16 23:47:40', '1', '2023-02-10 13:47:46', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1247, '敏感词管理', '', 2, 13, 1, 'sensitive-word', 'education', 'system/sensitiveWord/index', 'SystemSensitiveWord', 0, b'1', b'1', b'1', '', '2022-04-07 16:55:03', '1', '2023-04-08 09:00:40', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1248, '敏感词查询', 'system:sensitive-word:query', 3, 1, 1247, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-07 16:55:03', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1249, '敏感词创建', 'system:sensitive-word:create', 3, 2, 1247, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-07 16:55:03', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1250, '敏感词更新', 'system:sensitive-word:update', 3, 3, 1247, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-07 16:55:03', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1251, '敏感词删除', 'system:sensitive-word:delete', 3, 4, 1247, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-07 16:55:03', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1252, '敏感词导出', 'system:sensitive-word:export', 3, 5, 1247, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-07 16:55:03', '', '2022-04-20 17:03:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1254, '作者动态', '', 1, 0, 0, 'https://www.iocoder.cn', 'people', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-04-23 01:03:15', '1', '2023-02-10 00:06:52', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1255, '数据源配置', '', 2, 1, 2, 'data-source-config', 'rate', 'infra/dataSourceConfig/index', 'InfraDataSourceConfig', 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '1', '2023-04-08 09:05:21', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1256, '数据源配置查询', 'infra:data-source-config:query', 3, 1, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1257, '数据源配置创建', 'infra:data-source-config:create', 3, 2, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1258, '数据源配置更新', 'infra:data-source-config:update', 3, 3, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1259, '数据源配置删除', 'infra:data-source-config:delete', 3, 4, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1260, '数据源配置导出', 'infra:data-source-config:export', 3, 5, 1255, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-04-27 14:37:32', '', '2022-04-27 14:37:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1261, 'OAuth 2.0', '', 1, 10, 1, 'oauth2', 'people', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-05-09 23:38:17', '1', '2022-05-11 23:51:46', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1263, '应用管理', '', 2, 0, 1261, 'oauth2/application', 'tool', 'system/oauth2/client/index', 'SystemOAuth2Client', 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2023-04-08 08:47:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1264, '客户端查询', 'system:oauth2-client:query', 3, 1, 1263, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1265, '客户端创建', 'system:oauth2-client:create', 3, 2, 1263, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:23', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1266, '客户端更新', 'system:oauth2-client:update', 3, 3, 1263, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:28', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1267, '客户端删除', 'system:oauth2-client:delete', 3, 4, 1263, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-05-10 16:26:33', '1', '2022-05-11 00:31:33', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1281, '报表管理', '', 1, 40, 0, '/report', 'chart', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-07-10 20:22:15', '1', '2023-02-07 17:16:40', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1282, '报表设计器', '', 2, 1, 1281, 'jimu-report', 'example', 'report/jmreport/index', 'GoView', 0, b'1', b'1', b'1', '1', '2022-07-10 20:26:36', '1', '2023-04-08 10:47:59', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2000, '商品中心', '', 1, 60, 0, '/product', 'merchant', NULL, NULL, 0, b'1', b'1', b'1', '', '2022-07-29 15:53:53', '1', '2022-07-30 22:26:19', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2002, '商品分类', '', 2, 2, 2000, 'category', 'dict', 'mall/product/category/index', 'ProductCategory', 0, b'1', b'1', b'1', '', '2022-07-29 15:53:53', '1', '2023-04-08 11:34:59', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2003, '分类查询', 'product:category:query', 3, 1, 2002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-29 15:53:53', '', '2022-07-29 15:53:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2004, '分类创建', 'product:category:create', 3, 2, 2002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-29 15:53:53', '', '2022-07-29 15:53:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2005, '分类更新', 'product:category:update', 3, 3, 2002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-29 15:53:53', '', '2022-07-29 15:53:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2006, '分类删除', 'product:category:delete', 3, 4, 2002, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-29 15:53:53', '', '2022-07-29 15:53:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2008, '商品品牌', '', 2, 3, 2000, 'brand', 'dashboard', 'mall/product/brand/index', 'ProductBrand', 0, b'1', b'1', b'1', '', '2022-07-30 13:52:44', '1', '2023-04-08 11:35:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2009, '品牌查询', 'product:brand:query', 3, 1, 2008, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 13:52:44', '', '2022-07-30 13:52:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2010, '品牌创建', 'product:brand:create', 3, 2, 2008, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 13:52:44', '', '2022-07-30 13:52:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2011, '品牌更新', 'product:brand:update', 3, 3, 2008, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 13:52:44', '', '2022-07-30 13:52:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2012, '品牌删除', 'product:brand:delete', 3, 4, 2008, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 13:52:44', '', '2022-07-30 13:52:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2014, '商品列表', '', 2, 1, 2000, 'spu', 'list', 'mall/product/spu/index', 'ProductSpu', 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '1', '2023-04-08 11:34:47', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2015, '商品查询', 'product:spu:query', 3, 1, 2014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '', '2022-07-30 14:22:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2016, '商品创建', 'product:spu:create', 3, 2, 2014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '', '2022-07-30 14:22:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2017, '商品更新', 'product:spu:update', 3, 3, 2014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '', '2022-07-30 14:22:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2018, '商品删除', 'product:spu:delete', 3, 4, 2014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '', '2022-07-30 14:22:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2019, '商品属性', '', 2, 3, 2000, 'property', 'eye', 'mall/product/property/index', 'ProductProperty', 0, b'1', b'1', b'1', '', '2022-08-01 14:55:35', '1', '2023-04-08 11:35:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2020, '规格查询', 'product:property:query', 3, 1, 2019, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:55:35', '', '2022-12-12 20:26:24', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2021, '规格创建', 'product:property:create', 3, 2, 2019, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:55:35', '', '2022-12-12 20:26:30', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2022, '规格更新', 'product:property:update', 3, 3, 2019, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:55:35', '', '2022-12-12 20:26:33', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2023, '规格删除', 'product:property:delete', 3, 4, 2019, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:55:35', '', '2022-12-12 20:26:37', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2025, 'Banner管理', '', 2, 100, 2000, 'banner', '', 'mall/market/banner/index', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:56:14', '1', '2022-10-24 22:29:39', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2026, 'Banner查询', 'market:banner:query', 3, 1, 2025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:56:14', '', '2022-08-01 14:56:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2027, 'Banner创建', 'market:banner:create', 3, 2, 2025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:56:14', '', '2022-08-01 14:56:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2028, 'Banner更新', 'market:banner:update', 3, 3, 2025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:56:14', '', '2022-08-01 14:56:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2029, 'Banner删除', 'market:banner:delete', 3, 4, 2025, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-08-01 14:56:14', '', '2022-08-01 14:56:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2030, '营销中心', '', 1, 70, 0, '/promotion', 'rate', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-10-31 21:25:09', '1', '2022-10-31 21:25:09', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2032, '优惠劵', '', 2, 2, 2030, 'coupon-template', 'textarea', 'mall/promotion/couponTemplate/index', 'PromotionCouponTemplate', 0, b'1', b'1', b'1', '', '2022-10-31 22:27:14', '1', '2023-04-08 11:44:23', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2033, '优惠劵模板查询', 'promotion:coupon-template:query', 3, 1, 2032, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-10-31 22:27:14', '', '2022-10-31 22:27:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2034, '优惠劵模板创建', 'promotion:coupon-template:create', 3, 2, 2032, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-10-31 22:27:14', '', '2022-10-31 22:27:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2035, '优惠劵模板更新', 'promotion:coupon-template:update', 3, 3, 2032, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-10-31 22:27:14', '', '2022-10-31 22:27:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2036, '优惠劵模板删除', 'promotion:coupon-template:delete', 3, 4, 2032, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-10-31 22:27:14', '', '2022-10-31 22:27:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2038, '会员优惠劵', '', 2, 2, 2030, 'coupon', '', 'mall/promotion/coupon/index', 'PromotionCoupon', 0, b'0', b'1', b'1', '', '2022-11-03 23:21:31', '1', '2023-04-08 11:44:17', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2039, '优惠劵查询', 'promotion:coupon:query', 3, 1, 2038, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-03 23:21:31', '', '2022-11-03 23:21:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2040, '优惠劵删除', 'promotion:coupon:delete', 3, 4, 2038, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-03 23:21:31', '', '2022-11-03 23:21:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2041, '满减送活动', '', 2, 10, 2030, 'reward-activity', 'radio', 'mall/promotion/rewardActivity/index', 'PromotionRewardActivity', 0, b'1', b'1', b'1', '', '2022-11-04 23:47:49', '1', '2023-04-08 11:45:35', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2042, '满减送活动查询', 'promotion:reward-activity:query', 3, 1, 2041, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-04 23:47:49', '', '2022-11-04 23:47:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2043, '满减送活动创建', 'promotion:reward-activity:create', 3, 2, 2041, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-04 23:47:49', '', '2022-11-04 23:47:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2044, '满减送活动更新', 'promotion:reward-activity:update', 3, 3, 2041, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-04 23:47:50', '', '2022-11-04 23:47:50', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2045, '满减送活动删除', 'promotion:reward-activity:delete', 3, 4, 2041, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-04 23:47:50', '', '2022-11-04 23:47:50', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2046, '满减送活动关闭', 'promotion:reward-activity:close', 3, 5, 2041, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-11-05 10:42:53', '1', '2022-11-05 10:42:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2047, '限时折扣活动', '', 2, 7, 2030, 'discount-activity', 'time', 'mall/promotion/discountActivity/index', 'PromotionDiscountActivity', 0, b'1', b'1', b'1', '', '2022-11-05 17:12:15', '1', '2023-04-08 11:45:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2048, '限时折扣活动查询', 'promotion:discount-activity:query', 3, 1, 2047, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-05 17:12:15', '', '2022-11-05 17:12:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2049, '限时折扣活动创建', 'promotion:discount-activity:create', 3, 2, 2047, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-05 17:12:15', '', '2022-11-05 17:12:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2050, '限时折扣活动更新', 'promotion:discount-activity:update', 3, 3, 2047, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-05 17:12:16', '', '2022-11-05 17:12:16', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2051, '限时折扣活动删除', 'promotion:discount-activity:delete', 3, 4, 2047, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-05 17:12:16', '', '2022-11-05 17:12:16', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2052, '限时折扣活动关闭', 'promotion:discount-activity:close', 3, 5, 2047, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-05 17:12:16', '', '2022-11-05 17:12:16', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2059, '秒杀商品', '', 2, 2, 2209, 'activity', 'ep:basketball', 'mall/promotion/seckill/activity/index', 'PromotionSeckillActivity', 0, b'1', b'1', b'1', '', '2022-11-06 22:24:49', '1', '2023-06-24 18:57:25', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2060, '秒杀活动查询', 'promotion:seckill-activity:query', 3, 1, 2059, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-06 22:24:49', '', '2022-11-06 22:24:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2061, '秒杀活动创建', 'promotion:seckill-activity:create', 3, 2, 2059, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-06 22:24:49', '', '2022-11-06 22:24:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2062, '秒杀活动更新', 'promotion:seckill-activity:update', 3, 3, 2059, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-06 22:24:49', '', '2022-11-06 22:24:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2063, '秒杀活动删除', 'promotion:seckill-activity:delete', 3, 4, 2059, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-06 22:24:49', '', '2022-11-06 22:24:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2064, '秒杀活动导出', 'promotion:seckill-activity:export', 3, 5, 2059, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-06 22:24:49', '', '2022-11-06 22:24:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2066, '秒杀时段', '', 2, 1, 2209, 'config', 'ep:baseball', 'mall/promotion/seckill/config/index', 'PromotionSeckillConfig', 0, b'1', b'1', b'1', '', '2022-11-15 19:46:50', '1', '2023-06-24 18:57:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2067, '秒杀时段查询', 'promotion:seckill-config:query', 3, 1, 2066, '', '', '', '', 0, b'1', b'1', b'1', '', '2022-11-15 19:46:51', '1', '2023-06-24 17:50:25', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2068, '秒杀时段创建', 'promotion:seckill-config:create', 3, 2, 2066, '', '', '', '', 0, b'1', b'1', b'1', '', '2022-11-15 19:46:51', '1', '2023-06-24 17:48:39', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2069, '秒杀时段更新', 'promotion:seckill-config:update', 3, 3, 2066, '', '', '', '', 0, b'1', b'1', b'1', '', '2022-11-15 19:46:51', '1', '2023-06-24 17:50:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2070, '秒杀时段删除', 'promotion:seckill-config:delete', 3, 4, 2066, '', '', '', '', 0, b'1', b'1', b'1', '', '2022-11-15 19:46:51', '1', '2023-06-24 17:50:32', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2072, '订单中心', '', 1, 65, 0, '/trade', 'order', NULL, NULL, 0, b'1', b'1', b'1', '1', '2022-11-19 18:57:19', '1', '2022-12-10 16:32:57', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2073, '售后退款', '', 2, 1, 2072, 'trade/after-sale', 'education', 'mall/trade/afterSale/index', 'TradeAfterSale', 0, b'1', b'1', b'1', '', '2022-11-19 20:15:32', '1', '2023-04-08 11:43:19', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2074, '售后查询', 'trade:after-sale:query', 3, 1, 2073, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-11-19 20:15:33', '1', '2022-12-10 21:04:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2075, '秒杀活动关闭', 'promotion:sekill-activity:close', 3, 6, 2059, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2022-11-28 20:20:15', '1', '2022-11-28 20:20:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2076, '订单列表', '', 2, 0, 2072, 'trade/order', 'list', 'mall/trade/order/index', 'TradeOrder', 0, b'1', b'1', b'1', '1', '2022-12-10 21:05:44', '1', '2023-04-08 11:42:23', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2083, '地区管理', '', 2, 14, 1, 'area', 'row', 'system/area/index', 'SystemArea', 0, b'1', b'1', b'1', '1', '2022-12-23 17:35:05', '1', '2023-04-08 09:01:37', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2084, '公众号管理', '', 1, 100, 0, '/mp', 'wechat', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-01-01 20:11:04', '1', '2023-01-15 11:28:57', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2085, '账号管理', '', 2, 1, 2084, 'account', 'phone', 'mp/account/index', 'MpAccount', 0, b'1', b'1', b'1', '1', '2023-01-01 20:13:31', '1', '2023-02-09 23:56:39', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2086, '新增账号', 'mp:account:create', 3, 1, 2085, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-01 20:21:40', '1', '2023-01-07 17:32:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2087, '修改账号', 'mp:account:update', 3, 2, 2085, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-07 17:32:46', '1', '2023-01-07 17:32:46', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2088, '查询账号', 'mp:account:query', 3, 0, 2085, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-07 17:33:07', '1', '2023-01-07 17:33:07', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2089, '删除账号', 'mp:account:delete', 3, 3, 2085, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-07 17:33:21', '1', '2023-01-07 17:33:21', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2090, '生成二维码', 'mp:account:qr-code', 3, 4, 2085, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-07 17:33:58', '1', '2023-01-07 17:33:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2091, '清空 API 配额', 'mp:account:clear-quota', 3, 5, 2085, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-07 18:20:32', '1', '2023-01-07 18:20:59', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2092, '数据统计', 'mp:statistics:query', 2, 2, 2084, 'statistics', 'chart', 'mp/statistics/index', 'MpStatistics', 0, b'1', b'1', b'1', '1', '2023-01-07 20:17:36', '1', '2023-02-09 23:58:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2093, '标签管理', '', 2, 3, 2084, 'tag', 'rate', 'mp/tag/index', 'MpTag', 0, b'1', b'1', b'1', '1', '2023-01-08 11:37:32', '1', '2023-02-09 23:58:47', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2094, '查询标签', 'mp:tag:query', 3, 0, 2093, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-08 11:59:03', '1', '2023-01-08 11:59:03', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2095, '新增标签', 'mp:tag:create', 3, 1, 2093, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-08 11:59:23', '1', '2023-01-08 11:59:23', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2096, '修改标签', 'mp:tag:update', 3, 2, 2093, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-08 11:59:41', '1', '2023-01-08 11:59:41', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2097, '删除标签', 'mp:tag:delete', 3, 3, 2093, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-08 12:00:04', '1', '2023-01-08 12:00:13', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2098, '同步标签', 'mp:tag:sync', 3, 4, 2093, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-08 12:00:29', '1', '2023-01-08 12:00:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2099, '粉丝管理', '', 2, 4, 2084, 'user', 'people', 'mp/user/index', 'MpUser', 0, b'1', b'1', b'1', '1', '2023-01-08 16:51:20', '1', '2023-02-09 23:58:21', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2100, '查询粉丝', 'mp:user:query', 3, 0, 2099, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-08 17:16:59', '1', '2023-01-08 17:17:23', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2101, '修改粉丝', 'mp:user:update', 3, 1, 2099, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-08 17:17:11', '1', '2023-01-08 17:17:11', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2102, '同步粉丝', 'mp:user:sync', 3, 2, 2099, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-08 17:17:40', '1', '2023-01-08 17:17:40', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2103, '消息管理', '', 2, 5, 2084, 'message', 'email', 'mp/message/index', 'MpMessage', 0, b'1', b'1', b'1', '1', '2023-01-08 18:44:19', '1', '2023-02-09 23:58:02', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2104, '图文发表记录', '', 2, 10, 2084, 'free-publish', 'education', 'mp/freePublish/index', 'MpFreePublish', 0, b'1', b'1', b'1', '1', '2023-01-13 00:30:50', '1', '2023-02-09 23:57:22', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2105, '查询发布列表', 'mp:free-publish:query', 3, 1, 2104, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-13 07:19:17', '1', '2023-01-13 07:19:17', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2106, '发布草稿', 'mp:free-publish:submit', 3, 2, 2104, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-13 07:19:46', '1', '2023-01-13 07:19:46', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2107, '删除发布记录', 'mp:free-publish:delete', 3, 3, 2104, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-13 07:20:01', '1', '2023-01-13 07:20:01', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2108, '图文草稿箱', '', 2, 9, 2084, 'draft', 'edit', 'mp/draft/index', 'MpDraft', 0, b'1', b'1', b'1', '1', '2023-01-13 07:40:21', '1', '2023-02-09 23:56:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2109, '新建草稿', 'mp:draft:create', 3, 1, 2108, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-13 23:15:30', '1', '2023-01-13 23:15:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2110, '修改草稿', 'mp:draft:update', 3, 2, 2108, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-14 10:08:47', '1', '2023-01-14 10:08:47', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2111, '查询草稿', 'mp:draft:query', 3, 0, 2108, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-14 10:09:01', '1', '2023-01-14 10:09:01', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2112, '删除草稿', 'mp:draft:delete', 3, 3, 2108, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-14 10:09:19', '1', '2023-01-14 10:09:19', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2113, '素材管理', '', 2, 8, 2084, 'material', 'skill', 'mp/material/index', 'MpMaterial', 0, b'1', b'1', b'1', '1', '2023-01-14 14:12:07', '1', '2023-02-09 23:57:36', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2114, '上传临时素材', 'mp:material:upload-temporary', 3, 1, 2113, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-14 15:33:55', '1', '2023-01-14 15:33:55', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2115, '上传永久素材', 'mp:material:upload-permanent', 3, 2, 2113, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-14 15:34:14', '1', '2023-01-14 15:34:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2116, '删除素材', 'mp:material:delete', 3, 3, 2113, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-14 15:35:37', '1', '2023-01-14 15:35:37', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2117, '上传图文图片', 'mp:material:upload-news-image', 3, 4, 2113, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-14 15:36:31', '1', '2023-01-14 15:36:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2118, '查询素材', 'mp:material:query', 3, 5, 2113, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-14 15:39:22', '1', '2023-01-14 15:39:22', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2119, '菜单管理', '', 2, 6, 2084, 'menu', 'button', 'mp/menu/index', 'MpMenu', 0, b'1', b'1', b'1', '1', '2023-01-14 17:43:54', '1', '2023-02-09 23:57:50', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2120, '自动回复', '', 2, 7, 2084, 'auto-reply', 'eye', 'mp/autoReply/index', 'MpAutoReply', 0, b'1', b'1', b'1', '1', '2023-01-15 22:13:09', '1', '2023-02-09 23:56:28', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2121, '查询回复', 'mp:auto-reply:query', 3, 0, 2120, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-16 22:28:41', '1', '2023-01-16 22:28:41', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2122, '新增回复', 'mp:auto-reply:create', 3, 1, 2120, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-16 22:28:54', '1', '2023-01-16 22:28:54', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2123, '修改回复', 'mp:auto-reply:update', 3, 2, 2120, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-16 22:29:05', '1', '2023-01-16 22:29:05', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2124, '删除回复', 'mp:auto-reply:delete', 3, 3, 2120, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-16 22:29:34', '1', '2023-01-16 22:29:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2125, '查询菜单', 'mp:menu:query', 3, 0, 2119, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-17 23:05:41', '1', '2023-01-17 23:05:41', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2126, '保存菜单', 'mp:menu:save', 3, 1, 2119, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-17 23:06:01', '1', '2023-01-17 23:06:01', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2127, '删除菜单', 'mp:menu:delete', 3, 2, 2119, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-17 23:06:16', '1', '2023-01-17 23:06:16', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2128, '查询消息', 'mp:message:query', 3, 0, 2103, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-17 23:07:14', '1', '2023-01-17 23:07:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2129, '发送消息', 'mp:message:send', 3, 1, 2103, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-17 23:07:26', '1', '2023-01-17 23:07:26', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2130, '邮箱管理', '', 2, 11, 1, 'mail', 'email', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-01-25 17:27:44', '1', '2023-01-25 17:27:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2131, '邮箱账号', '', 2, 0, 2130, 'mail-account', 'user', 'system/mail/account/index', 'SystemMailAccount', 0, b'1', b'1', b'1', '', '2023-01-25 09:33:48', '1', '2023-04-08 08:53:43', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2132, '账号查询', 'system:mail-account:query', 3, 1, 2131, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-25 09:33:48', '', '2023-01-25 09:33:48', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2133, '账号创建', 'system:mail-account:create', 3, 2, 2131, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-25 09:33:48', '', '2023-01-25 09:33:48', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2134, '账号更新', 'system:mail-account:update', 3, 3, 2131, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-25 09:33:48', '', '2023-01-25 09:33:48', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2135, '账号删除', 'system:mail-account:delete', 3, 4, 2131, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-25 09:33:48', '', '2023-01-25 09:33:48', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2136, '邮件模版', '', 2, 0, 2130, 'mail-template', 'education', 'system/mail/template/index', 'SystemMailTemplate', 0, b'1', b'1', b'1', '', '2023-01-25 12:05:31', '1', '2023-04-08 08:53:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2137, '模版查询', 'system:mail-template:query', 3, 1, 2136, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-25 12:05:31', '', '2023-01-25 12:05:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2138, '模版创建', 'system:mail-template:create', 3, 2, 2136, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-25 12:05:31', '', '2023-01-25 12:05:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2139, '模版更新', 'system:mail-template:update', 3, 3, 2136, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-25 12:05:31', '', '2023-01-25 12:05:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2140, '模版删除', 'system:mail-template:delete', 3, 4, 2136, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-25 12:05:31', '', '2023-01-25 12:05:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2141, '邮件记录', '', 2, 0, 2130, 'mail-log', 'log', 'system/mail/log/index', 'SystemMailLog', 0, b'1', b'1', b'1', '', '2023-01-26 02:16:50', '1', '2023-04-08 08:53:49', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2142, '日志查询', 'system:mail-log:query', 3, 1, 2141, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-26 02:16:50', '', '2023-01-26 02:16:50', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2143, '发送测试邮件', 'system:mail-template:send-mail', 3, 5, 2136, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-26 23:29:15', '1', '2023-01-26 23:29:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2144, '站内信管理', '', 1, 11, 1, 'notify', 'message', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-01-28 10:25:18', '1', '2023-01-28 10:25:46', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2145, '模板管理', '', 2, 0, 2144, 'notify-template', 'education', 'system/notify/template/index', 'SystemNotifyTemplate', 0, b'1', b'1', b'1', '', '2023-01-28 02:26:42', '1', '2023-04-08 08:54:39', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2146, '站内信模板查询', 'system:notify-template:query', 3, 1, 2145, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-28 02:26:42', '', '2023-01-28 02:26:42', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2147, '站内信模板创建', 'system:notify-template:create', 3, 2, 2145, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-28 02:26:42', '', '2023-01-28 02:26:42', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2148, '站内信模板更新', 'system:notify-template:update', 3, 3, 2145, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-28 02:26:42', '', '2023-01-28 02:26:42', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2149, '站内信模板删除', 'system:notify-template:delete', 3, 4, 2145, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-28 02:26:42', '', '2023-01-28 02:26:42', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2150, '发送测试站内信', 'system:notify-template:send-notify', 3, 5, 2145, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-01-28 10:54:43', '1', '2023-01-28 10:54:43', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2151, '消息记录', '', 2, 0, 2144, 'notify-message', 'edit', 'system/notify/message/index', 'SystemNotifyMessage', 0, b'1', b'1', b'1', '', '2023-01-28 04:28:22', '1', '2023-04-08 08:54:11', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2152, '站内信消息查询', 'system:notify-message:query', 3, 1, 2151, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-01-28 04:28:22', '', '2023-01-28 04:28:22', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2153, '大屏设计器', '', 2, 2, 1281, 'go-view', 'dashboard', 'report/goview/index', 'JimuReport', 0, b'1', b'1', b'1', '1', '2023-02-07 00:03:19', '1', '2023-04-08 10:48:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2154, '创建项目', 'report:go-view-project:create', 3, 1, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:25:14', '1', '2023-02-07 19:25:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2155, '更新项目', 'report:go-view-project:delete', 3, 2, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:25:34', '1', '2023-02-07 19:25:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2156, '查询项目', 'report:go-view-project:query', 3, 0, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:25:53', '1', '2023-02-07 19:25:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2157, '使用 SQL 查询数据', 'report:go-view-data:get-by-sql', 3, 3, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:26:15', '1', '2023-02-07 19:26:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2158, '使用 HTTP 查询数据', 'report:go-view-data:get-by-http', 3, 4, 2153, '', '', '', NULL, 0, b'1', b'1', b'1', '1', '2023-02-07 19:26:35', '1', '2023-02-07 19:26:35', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2159, 'Boot 开发文档', '', 1, 1, 0, 'https://doc.iocoder.cn/', 'education', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:46:28', '1', '2023-02-10 22:46:28', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2160, 'Cloud 开发文档', '', 1, 2, 0, 'https://cloud.iocoder.cn', 'documentation', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-02-10 22:47:07', '1', '2023-02-10 22:47:07', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2161, '接入示例', '', 2, 99, 1117, 'demo-order', 'drag', 'pay/demo/index', NULL, 0, b'1', b'1', b'1', '', '2023-02-11 14:21:42', '1', '2023-02-11 22:26:35', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2162, '商品导出', 'product:spu:export', 3, 5, 2014, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2022-07-30 14:22:58', '', '2022-07-30 14:22:58', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2164, '配送管理', '', 1, 2, 2072, 'delivery', '', '', '', 0, b'1', b'1', b'1', '1', '2023-05-18 09:18:02', '1', '2023-05-24 23:24:13', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2165, '快递发货', '', 1, 0, 2164, 'express', '', '', '', 0, b'1', b'1', b'1', '1', '2023-05-18 09:22:06', '1', '2023-05-18 09:22:06', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2166, '门店自提', '', 1, 1, 2164, 'pick-up-store', '', '', '', 0, b'1', b'1', b'1', '1', '2023-05-18 09:23:14', '1', '2023-05-18 09:23:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2167, '快递公司', '', 2, 0, 2165, 'express', '', 'mall/trade/delivery/express/index', 'Express', 0, b'1', b'1', b'1', '1', '2023-05-18 09:27:21', '1', '2023-05-18 22:11:14', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2168, '快递公司查询', 'trade:delivery:express:query', 3, 1, 2167, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-18 09:37:53', '', '2023-05-18 09:37:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2169, '快递公司创建', 'trade:delivery:express:create', 3, 2, 2167, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-18 09:37:53', '', '2023-05-18 09:37:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2170, '快递公司更新', 'trade:delivery:express:update', 3, 3, 2167, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-18 09:37:53', '', '2023-05-18 09:37:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2171, '快递公司删除', 'trade:delivery:express:delete', 3, 4, 2167, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-18 09:37:53', '', '2023-05-18 09:37:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2172, '快递公司导出', 'trade:delivery:express:export', 3, 5, 2167, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-18 09:37:53', '', '2023-05-18 09:37:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2173, '运费模版', 'trade:delivery:express-template:query', 2, 1, 2165, 'express-template', '', 'mall/trade/delivery/expressTemplate/index', 'ExpressTemplate', 0, b'1', b'1', b'1', '1', '2023-05-20 06:48:10', '1', '2023-05-20 06:48:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2174, '快递运费模板查询', 'trade:delivery:express-template:query', 3, 1, 2173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-20 06:49:53', '', '2023-05-20 06:49:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2175, '快递运费模板创建', 'trade:delivery:express-template:create', 3, 2, 2173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-20 06:49:53', '', '2023-05-20 06:49:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2176, '快递运费模板更新', 'trade:delivery:express-template:update', 3, 3, 2173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-20 06:49:53', '', '2023-05-20 06:49:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2177, '快递运费模板删除', 'trade:delivery:express-template:delete', 3, 4, 2173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-20 06:49:53', '', '2023-05-20 06:49:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2178, '快递运费模板导出', 'trade:delivery:express-template:export', 3, 5, 2173, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-20 06:49:53', '', '2023-05-20 06:49:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2179, '门店管理', '', 2, 1, 2166, 'pick-up-store', '', 'mall/trade/delivery/pickUpStore/index', 'PickUpStore', 0, b'1', b'1', b'1', '1', '2023-05-25 10:50:00', '1', '2023-05-25 10:50:00', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2180, '自提门店查询', 'trade:delivery:pick-up-store:query', 3, 1, 2179, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-25 10:53:29', '', '2023-05-25 10:53:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2181, '自提门店创建', 'trade:delivery:pick-up-store:create', 3, 2, 2179, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-25 10:53:29', '', '2023-05-25 10:53:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2182, '自提门店更新', 'trade:delivery:pick-up-store:update', 3, 3, 2179, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-25 10:53:29', '', '2023-05-25 10:53:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2183, '自提门店删除', 'trade:delivery:pick-up-store:delete', 3, 4, 2179, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-25 10:53:29', '', '2023-05-25 10:53:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2184, '自提门店导出', 'trade:delivery:pick-up-store:export', 3, 5, 2179, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-05-25 10:53:29', '', '2023-05-25 10:53:29', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2209, '秒杀活动', '', 2, 3, 2030, 'seckill', 'ep:place', '', '', 0, b'1', b'1', b'1', '1', '2023-06-24 17:39:13', '1', '2023-06-24 18:55:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2262, '会员中心', '', 1, 55, 0, '/member', 'date-range', NULL, NULL, 0, b'1', b'1', b'1', '1', '2023-06-10 00:42:03', '1', '2023-06-28 22:52:34', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2275, '积分配置', '', 2, 0, 2299, 'config', '', 'member/point/config/index', 'PointConfig', 0, b'1', b'1', b'1', '', '2023-06-10 02:07:44', '1', '2023-06-27 22:50:59', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2276, '积分设置查询', 'point:config:query', 3, 1, 2275, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 02:07:44', '', '2023-06-10 02:07:44', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2277, '积分设置创建', 'point:config:save', 3, 2, 2275, '', '', '', '', 0, b'1', b'1', b'1', '', '2023-06-10 02:07:44', '1', '2023-06-27 20:32:31', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2281, '签到配置', '', 2, 2, 2300, 'sign-in-config', '', 'member/signin/config/index', 'SignInConfig', 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '1', '2023-07-02 15:04:15', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2282, '积分签到规则查询', 'point:sign-in-config:query', 3, 1, 2281, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2283, '积分签到规则创建', 'point:sign-in-config:create', 3, 2, 2281, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2284, '积分签到规则更新', 'point:sign-in-config:update', 3, 3, 2281, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2285, '积分签到规则删除', 'point:sign-in-config:delete', 3, 4, 2281, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 03:26:12', '', '2023-06-10 03:26:12', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2287, '积分记录', '', 2, 1, 2299, 'record', '', 'member/point/record/index', 'PointRecord', 0, b'1', b'1', b'1', '', '2023-06-10 04:18:50', '1', '2023-06-27 22:51:07', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2288, '用户积分记录查询', 'point:record:query', 3, 1, 2287, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:18:50', '', '2023-06-10 04:18:50', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2293, '签到记录', '', 2, 3, 2300, 'sign-in-record', '', 'member/signin/record/index', 'SignInRecord', 0, b'1', b'1', b'1', '', '2023-06-10 04:48:22', '1', '2023-07-02 15:04:10', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2294, '用户签到积分查询', 'point:sign-in-record:query', 3, 1, 2293, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:48:22', '', '2023-06-10 04:48:22', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2297, '用户签到积分删除', 'point:sign-in-record:delete', 3, 4, 2293, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-06-10 04:48:22', '', '2023-06-10 04:48:22', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2299, '会员积分', '', 1, 1, 2262, 'point', '', '', '', 0, b'1', b'1', b'1', '1', '2023-06-27 22:48:51', '1', '2023-06-27 22:48:51', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2300, '会员签到', '', 1, 2, 2262, 'signin', '', '', '', 0, b'1', b'1', b'1', '1', '2023-06-27 22:49:53', '1', '2023-06-27 22:49:53', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2301, '回调通知', '', 2, 4, 1117, 'notify', 'example', 'pay/notify/index', 'PayNotify', 0, b'1', b'1', b'1', '', '2023-07-20 04:41:32', '1', '2023-07-20 13:45:08', b'0'); +INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2302, '支付通知查询', 'pay:notify:query', 3, 1, 2301, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-07-20 04:41:32', '', '2023-07-20 04:41:32', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_notice +-- ---------------------------- +DROP TABLE IF EXISTS `system_notice`; +CREATE TABLE `system_notice` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '公告ID', + `title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '公告标题', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '公告内容', + `type` tinyint NOT NULL COMMENT '公告类型(1通知 2公告)', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '公告状态(0正常 1关闭)', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '通知公告表'; + +-- ---------------------------- +-- Records of system_notice +-- ---------------------------- +BEGIN; +INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '芋道的公众', '

新版本内容133

', 1, 0, 'admin', '2021-01-05 17:03:48', '1', '2022-05-04 21:00:20', b'0', 1); +INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '维护通知:2018-07-01 若依系统凌晨维护', '

维护内容

', 2, 1, 'admin', '2021-01-05 17:03:48', '1', '2022-05-11 12:34:24', b'0', 1); +INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, '我是测试标题', '

哈哈哈哈123

', 1, 0, '110', '2022-02-22 01:01:25', '110', '2022-02-22 01:01:46', b'0', 121); +COMMIT; + +-- ---------------------------- +-- Table structure for system_notify_message +-- ---------------------------- +DROP TABLE IF EXISTS `system_notify_message`; +CREATE TABLE `system_notify_message` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `user_id` bigint NOT NULL COMMENT '用户id', + `user_type` tinyint NOT NULL COMMENT '用户类型', + `template_id` bigint NOT NULL COMMENT '模版编号', + `template_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码', + `template_nickname` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模版发送人名称', + `template_content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模版内容', + `template_type` int NOT NULL COMMENT '模版类型', + `template_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模版参数', + `read_status` bit(1) NOT NULL COMMENT '是否已读', + `read_time` datetime NULL DEFAULT NULL COMMENT '阅读时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '站内信消息表'; + +-- ---------------------------- +-- Records of system_notify_message +-- ---------------------------- +BEGIN; +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 1, 2, 1, 'test', '123', '我是 1,我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2023-02-10 00:47:04', '1', '2023-01-28 11:44:08', '1', '2023-02-10 00:47:04', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 1, 2, 1, 'test', '123', '我是 1,我开始 2 了', 1, '{\"name\":\"1\",\"what\":\"2\"}', b'1', '2023-02-10 00:47:04', '1', '2023-01-28 11:45:04', '1', '2023-02-10 00:47:04', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 103, 2, 2, 'register', '系统消息', '你好,欢迎 哈哈 加入大家庭!', 2, '{\"name\":\"哈哈\"}', b'0', NULL, '1', '2023-01-28 21:02:20', '1', '2023-01-28 21:02:20', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2023-02-10 00:47:04', '1', '2023-01-28 22:21:42', '1', '2023-02-10 00:47:04', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, 1, 2, 1, 'test', '123', '我是 芋艿,我开始 写代码 了', 1, '{\"name\":\"芋艿\",\"what\":\"写代码\"}', b'1', '2023-01-29 10:52:06', '1', '2023-01-28 22:22:07', '1', '2023-01-29 10:52:06', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 1, 2, 1, 'test', '123', '我是 2,我开始 3 了', 1, '{\"name\":\"2\",\"what\":\"3\"}', b'1', '2023-01-29 10:52:06', '1', '2023-01-28 23:45:21', '1', '2023-01-29 10:52:06', b'0', 1); +INSERT INTO `system_notify_message` (`id`, `user_id`, `user_type`, `template_id`, `template_code`, `template_nickname`, `template_content`, `template_type`, `template_params`, `read_status`, `read_time`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (8, 1, 2, 2, 'register', '系统消息', '你好,欢迎 123 加入大家庭!', 2, '{\"name\":\"123\"}', b'1', '2023-01-29 10:52:06', '1', '2023-01-28 23:50:21', '1', '2023-01-29 10:52:06', b'0', 1); +COMMIT; + +-- ---------------------------- +-- Table structure for system_notify_template +-- ---------------------------- +DROP TABLE IF EXISTS `system_notify_template`; +CREATE TABLE `system_notify_template` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板名称', + `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模版编码', + `nickname` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送人名称', + `content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模版内容', + `type` tinyint NOT NULL COMMENT '类型', + `params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '参数数组', + `status` tinyint NOT NULL COMMENT '状态', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '站内信模板表'; + +-- ---------------------------- +-- Records of system_notify_template +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_oauth2_access_token +-- ---------------------------- +DROP TABLE IF EXISTS `system_oauth2_access_token`; +CREATE TABLE `system_oauth2_access_token` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `user_id` bigint NOT NULL COMMENT '用户编号', + `user_type` tinyint NOT NULL COMMENT '用户类型', + `access_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '访问令牌', + `refresh_token` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '刷新令牌', + `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号', + `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权范围', + `expires_time` datetime NOT NULL COMMENT '过期时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2231 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌'; + +-- ---------------------------- +-- Records of system_oauth2_access_token +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_oauth2_approve +-- ---------------------------- +DROP TABLE IF EXISTS `system_oauth2_approve`; +CREATE TABLE `system_oauth2_approve` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `user_id` bigint NOT NULL COMMENT '用户编号', + `user_type` tinyint NOT NULL COMMENT '用户类型', + `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号', + `scope` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '授权范围', + `approved` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否接受', + `expires_time` datetime NOT NULL COMMENT '过期时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 82 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 批准表'; + +-- ---------------------------- +-- Records of system_oauth2_approve +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_oauth2_client +-- ---------------------------- +DROP TABLE IF EXISTS `system_oauth2_client`; +CREATE TABLE `system_oauth2_client` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号', + `secret` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端密钥', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '应用名', + `logo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '应用图标', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '应用描述', + `status` tinyint NOT NULL COMMENT '状态', + `access_token_validity_seconds` int NOT NULL COMMENT '访问令牌的有效期', + `refresh_token_validity_seconds` int NOT NULL COMMENT '刷新令牌的有效期', + `redirect_uris` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '可重定向的 URI 地址', + `authorized_grant_types` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '授权类型', + `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权范围', + `auto_approve_scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '自动通过的授权范围', + `authorities` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '权限', + `resource_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '资源', + `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '附加信息', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 43 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 客户端表'; + +-- ---------------------------- +-- Records of system_oauth2_client +-- ---------------------------- +BEGIN; +INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 'default', 'admin123', '芋道源码', 'http://test.yunxi.iocoder.cn/a5e2e244368878a366b516805a4aabf1.png', '我是描述', 0, 1800, 43200, '[\"https://www.iocoder.cn\",\"https://doc.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[\"user.read\",\"user.write\"]', '[]', '{}', '1', '2022-05-11 21:47:12', '1', '2022-07-05 16:23:52', b'0'); +INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (40, 'test', 'test2', 'biubiu', 'http://test.yunxi.iocoder.cn/277a899d573723f1fcdfb57340f00379.png', NULL, 0, 1800, 43200, '[\"https://www.iocoder.cn\"]', '[\"password\",\"authorization_code\",\"implicit\"]', '[\"user_info\",\"projects\"]', '[\"user_info\"]', '[]', '[]', '{}', '1', '2022-05-12 00:28:20', '1', '2022-06-19 00:26:13', b'0'); +INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (41, 'yunxi-sso-demo-by-code', 'test', '基于授权码模式,如何实现 SSO 单点登录?', 'http://test.yunxi.iocoder.cn/fe4ed36596adad5120036ef61a6d0153654544d44af8dd4ad3ffe8f759933d6f.png', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"authorization_code\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-09-29 13:28:31', '1', '2022-09-29 13:28:31', b'0'); +INSERT INTO `system_oauth2_client` (`id`, `client_id`, `secret`, `name`, `logo`, `description`, `status`, `access_token_validity_seconds`, `refresh_token_validity_seconds`, `redirect_uris`, `authorized_grant_types`, `scopes`, `auto_approve_scopes`, `authorities`, `resource_ids`, `additional_information`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (42, 'yunxi-sso-demo-by-password', 'test', '基于密码模式,如何实现 SSO 单点登录?', 'http://test.yunxi.iocoder.cn/604bdc695e13b3b22745be704d1f2aa8ee05c5f26f9fead6d1ca49005afbc857.jpeg', NULL, 0, 1800, 43200, '[\"http://127.0.0.1:18080\"]', '[\"password\",\"refresh_token\"]', '[\"user.read\",\"user.write\"]', '[]', '[]', '[]', NULL, '1', '2022-10-04 17:40:16', '1', '2022-10-04 20:31:21', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_oauth2_code +-- ---------------------------- +DROP TABLE IF EXISTS `system_oauth2_code`; +CREATE TABLE `system_oauth2_code` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `user_id` bigint NOT NULL COMMENT '用户编号', + `user_type` tinyint NOT NULL COMMENT '用户类型', + `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '授权码', + `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号', + `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '授权范围', + `expires_time` datetime NOT NULL COMMENT '过期时间', + `redirect_uri` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '可重定向的 URI 地址', + `state` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '状态', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 147 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 授权码表'; + +-- ---------------------------- +-- Records of system_oauth2_code +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_oauth2_refresh_token +-- ---------------------------- +DROP TABLE IF EXISTS `system_oauth2_refresh_token`; +CREATE TABLE `system_oauth2_refresh_token` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `user_id` bigint NOT NULL COMMENT '用户编号', + `refresh_token` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '刷新令牌', + `user_type` tinyint NOT NULL COMMENT '用户类型', + `client_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户端编号', + `scopes` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '授权范围', + `expires_time` datetime NOT NULL COMMENT '过期时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 804 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌'; + +-- ---------------------------- +-- Records of system_oauth2_refresh_token +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_operate_log +-- ---------------------------- +DROP TABLE IF EXISTS `system_operate_log`; +CREATE TABLE `system_operate_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志主键', + `trace_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '链路追踪编号', + `user_id` bigint NOT NULL COMMENT '用户编号', + `user_type` tinyint NOT NULL DEFAULT 0 COMMENT '用户类型', + `module` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模块标题', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作名', + `type` bigint NOT NULL DEFAULT 0 COMMENT '操作分类', + `content` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '操作内容', + `exts` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '拓展字段', + `request_method` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求方法名', + `request_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求地址', + `user_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户 IP', + `user_agent` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '浏览器 UA', + `java_method` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT 'Java 方法名', + `java_method_args` varchar(8000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT 'Java 方法的参数', + `start_time` datetime NOT NULL COMMENT '操作时间', + `duration` int NOT NULL COMMENT '执行时长', + `result_code` int NOT NULL DEFAULT 0 COMMENT '结果码', + `result_msg` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '结果提示', + `result_data` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '结果数据', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7134 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录'; + +-- ---------------------------- +-- Records of system_operate_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_post +-- ---------------------------- +DROP TABLE IF EXISTS `system_post`; +CREATE TABLE `system_post` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '岗位ID', + `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '岗位编码', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '岗位名称', + `sort` int NOT NULL COMMENT '显示顺序', + `status` tinyint NOT NULL COMMENT '状态(0正常 1停用)', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表'; + +-- ---------------------------- +-- Records of system_post +-- ---------------------------- +BEGIN; +INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'ceo', '董事长', 1, 0, '', 'admin', '2021-01-06 17:03:48', '1', '2023-02-11 15:19:04', b'0', 1); +INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 'se', '项目经理', 2, 0, '', 'admin', '2021-01-05 17:03:48', '1', '2021-12-12 10:47:47', b'0', 1); +INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 'user', '普通员工', 4, 0, '111', 'admin', '2021-01-05 17:03:48', '1', '2023-02-11 15:19:00', b'0', 1); +COMMIT; + +-- ---------------------------- +-- Table structure for system_role +-- ---------------------------- +DROP TABLE IF EXISTS `system_role`; +CREATE TABLE `system_role` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称', + `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色权限字符串', + `sort` int NOT NULL COMMENT '显示顺序', + `data_scope` tinyint NOT NULL DEFAULT 1 COMMENT '数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)', + `data_scope_dept_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '数据范围(指定部门数组)', + `status` tinyint NOT NULL COMMENT '角色状态(0正常 1停用)', + `type` tinyint NOT NULL COMMENT '角色类型', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 139 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表'; + +-- ---------------------------- +-- Records of system_role +-- ---------------------------- +BEGIN; +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '超级管理员', 'super_admin', 1, 1, '', 0, 1, '超级管理员', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:21', b'0', 1); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '普通角色', 'common', 2, 2, '', 0, 1, '普通角色', 'admin', '2021-01-05 17:03:48', '', '2022-02-22 05:08:20', b'0', 1); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '测试账号', 'test', 0, 1, '[]', 0, 2, '132', '', '2021-01-06 13:49:35', '1', '2022-09-25 12:09:38', b'0', 1); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (110, '测试角色', 'test', 0, 1, '[]', 0, 2, '嘿嘿', '110', '2022-02-23 00:14:34', '110', '2022-02-23 13:14:58', b'0', 121); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (116, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (136, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (137, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role` (`id`, `name`, `code`, `sort`, `data_scope`, `data_scope_dept_ids`, `status`, `type`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (138, '租户管理员', 'tenant_admin', 0, 1, '', 0, 1, '系统自动生成', '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +COMMIT; + +-- ---------------------------- +-- Table structure for system_role_menu +-- ---------------------------- +DROP TABLE IF EXISTS `system_role_menu`; +CREATE TABLE `system_role_menu` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增编号', + `role_id` bigint NOT NULL COMMENT '角色ID', + `menu_id` bigint NOT NULL COMMENT '菜单ID', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2873 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色和菜单关联表'; + +-- ---------------------------- +-- Records of system_role_menu +-- ---------------------------- +BEGIN; +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (263, 109, 1, '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (434, 2, 1, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (454, 2, 1093, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (455, 2, 1094, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (460, 2, 1100, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (467, 2, 1107, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (470, 2, 1110, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (476, 2, 1117, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (477, 2, 100, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (478, 2, 101, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (479, 2, 102, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (480, 2, 1126, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (481, 2, 103, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (483, 2, 104, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (485, 2, 105, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (488, 2, 107, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (490, 2, 108, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (492, 2, 109, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (498, 2, 1138, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (523, 2, 1224, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (524, 2, 1225, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (541, 2, 500, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (543, 2, 501, '1', '2022-02-22 13:09:12', '1', '2022-02-22 13:09:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (675, 2, 2, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (689, 2, 1077, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (690, 2, 1078, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (692, 2, 1083, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (693, 2, 1084, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (699, 2, 1090, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (703, 2, 106, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (704, 2, 110, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (705, 2, 111, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (706, 2, 112, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (707, 2, 113, '1', '2022-02-22 13:16:57', '1', '2022-02-22 13:16:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1296, 110, 1, '110', '2022-02-23 00:23:55', '110', '2022-02-23 00:23:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1489, 1, 1, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1490, 1, 2, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1494, 1, 1077, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1495, 1, 1078, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1496, 1, 1083, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1497, 1, 1084, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1498, 1, 1090, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1499, 1, 1093, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1500, 1, 1094, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1501, 1, 1100, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1502, 1, 1107, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1503, 1, 1110, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1505, 1, 1117, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1506, 1, 100, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1507, 1, 101, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1508, 1, 102, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1509, 1, 1126, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1510, 1, 103, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1511, 1, 104, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1512, 1, 105, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1513, 1, 106, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1514, 1, 107, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1515, 1, 108, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1516, 1, 109, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1517, 1, 110, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1518, 1, 111, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1519, 1, 112, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1520, 1, 113, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1522, 1, 1138, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1525, 1, 1224, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1526, 1, 1225, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1527, 1, 500, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1528, 1, 501, '1', '2022-02-23 20:03:57', '1', '2022-02-23 20:03:57', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1578, 111, 1, '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1604, 101, 1216, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1605, 101, 1217, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1606, 101, 1218, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1607, 101, 1219, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1608, 101, 1220, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1609, 101, 1221, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1610, 101, 5, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1611, 101, 1222, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1612, 101, 1118, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1613, 101, 1119, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1614, 101, 1120, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1615, 101, 1185, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1616, 101, 1186, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1617, 101, 1187, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1618, 101, 1188, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1619, 101, 1189, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1620, 101, 1190, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1621, 101, 1191, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1622, 101, 1192, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1623, 101, 1193, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1624, 101, 1194, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1625, 101, 1195, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1626, 101, 1196, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1627, 101, 1197, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1628, 101, 1198, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1629, 101, 1199, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1630, 101, 1200, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1631, 101, 1201, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1632, 101, 1202, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1633, 101, 1207, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1634, 101, 1208, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1635, 101, 1209, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1636, 101, 1210, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1637, 101, 1211, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1638, 101, 1212, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1639, 101, 1213, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1640, 101, 1215, '1', '2022-03-19 21:45:52', '1', '2022-03-19 21:45:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1641, 101, 2, '1', '2022-04-01 22:21:24', '1', '2022-04-01 22:21:24', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1642, 101, 1031, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1643, 101, 1032, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1644, 101, 1033, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1645, 101, 1034, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1646, 101, 1035, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1647, 101, 1050, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1648, 101, 1051, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1649, 101, 1052, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1650, 101, 1053, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1651, 101, 1054, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1652, 101, 1056, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1653, 101, 1057, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1654, 101, 1058, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1655, 101, 1059, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1656, 101, 1060, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1657, 101, 1066, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1658, 101, 1067, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1659, 101, 1070, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1660, 101, 1071, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1661, 101, 1072, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1662, 101, 1073, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1663, 101, 1074, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1664, 101, 1075, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1665, 101, 1076, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1666, 101, 1077, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1667, 101, 1078, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1668, 101, 1082, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1669, 101, 1083, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1670, 101, 1084, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1671, 101, 1085, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1672, 101, 1086, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1673, 101, 1087, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1674, 101, 1088, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1675, 101, 1089, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1679, 101, 1237, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1680, 101, 1238, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1681, 101, 1239, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1682, 101, 1240, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1683, 101, 1241, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1684, 101, 1242, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1685, 101, 1243, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1687, 101, 106, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1688, 101, 110, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1689, 101, 111, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1690, 101, 112, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1691, 101, 113, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1692, 101, 114, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1693, 101, 115, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1694, 101, 116, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1712, 113, 1024, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1713, 113, 1025, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1714, 113, 1, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1715, 113, 102, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1716, 113, 103, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1717, 113, 104, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1718, 113, 1013, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1719, 113, 1014, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1720, 113, 1015, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1721, 113, 1016, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1722, 113, 1017, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1723, 113, 1018, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1724, 113, 1019, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1725, 113, 1020, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1726, 113, 1021, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1727, 113, 1022, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1728, 113, 1023, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1729, 109, 100, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1730, 109, 101, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1731, 109, 1063, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1732, 109, 1064, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1733, 109, 1001, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1734, 109, 1065, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1735, 109, 1002, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1736, 109, 1003, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1737, 109, 1004, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1738, 109, 1005, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1739, 109, 1006, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1740, 109, 1007, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1741, 109, 1008, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1742, 109, 1009, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1743, 109, 1010, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1744, 109, 1011, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1745, 109, 1012, '1', '2022-09-21 22:08:51', '1', '2022-09-21 22:08:51', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1746, 111, 100, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1747, 111, 101, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1748, 111, 1063, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1749, 111, 1064, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1750, 111, 1001, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1751, 111, 1065, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1752, 111, 1002, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1753, 111, 1003, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1754, 111, 1004, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1755, 111, 1005, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1756, 111, 1006, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1757, 111, 1007, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1758, 111, 1008, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1759, 111, 1009, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1760, 111, 1010, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1761, 111, 1011, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1762, 111, 1012, '1', '2022-09-21 22:08:52', '1', '2022-09-21 22:08:52', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1763, 109, 100, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1764, 109, 101, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1765, 109, 1063, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1766, 109, 1064, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1767, 109, 1001, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1768, 109, 1065, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1769, 109, 1002, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1770, 109, 1003, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1771, 109, 1004, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1772, 109, 1005, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1773, 109, 1006, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1774, 109, 1007, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1775, 109, 1008, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1776, 109, 1009, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1777, 109, 1010, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1778, 109, 1011, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1779, 109, 1012, '1', '2022-09-21 22:08:53', '1', '2022-09-21 22:08:53', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1780, 111, 100, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1781, 111, 101, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1782, 111, 1063, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1783, 111, 1064, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1784, 111, 1001, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1785, 111, 1065, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1786, 111, 1002, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1787, 111, 1003, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1788, 111, 1004, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1789, 111, 1005, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1790, 111, 1006, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1791, 111, 1007, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1792, 111, 1008, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1793, 111, 1009, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1794, 111, 1010, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1795, 111, 1011, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1796, 111, 1012, '1', '2022-09-21 22:08:54', '1', '2022-09-21 22:08:54', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1797, 109, 100, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1798, 109, 101, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1799, 109, 1063, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1800, 109, 1064, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1801, 109, 1001, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1802, 109, 1065, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1803, 109, 1002, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1804, 109, 1003, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1805, 109, 1004, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1806, 109, 1005, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1807, 109, 1006, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1808, 109, 1007, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1809, 109, 1008, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1810, 109, 1009, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1811, 109, 1010, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1812, 109, 1011, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1813, 109, 1012, '1', '2022-09-21 22:08:55', '1', '2022-09-21 22:08:55', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1814, 111, 100, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1815, 111, 101, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1816, 111, 1063, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1817, 111, 1064, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1818, 111, 1001, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1819, 111, 1065, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1820, 111, 1002, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1821, 111, 1003, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1822, 111, 1004, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1823, 111, 1005, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1824, 111, 1006, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1825, 111, 1007, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1826, 111, 1008, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1827, 111, 1009, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1828, 111, 1010, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1829, 111, 1011, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1830, 111, 1012, '1', '2022-09-21 22:08:56', '1', '2022-09-21 22:08:56', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1831, 109, 103, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1832, 109, 1017, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1833, 109, 1018, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1834, 109, 1019, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1835, 109, 1020, '1', '2022-09-21 22:43:23', '1', '2022-09-21 22:43:23', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1836, 111, 103, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1837, 111, 1017, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1838, 111, 1018, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1839, 111, 1019, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1840, 111, 1020, '1', '2022-09-21 22:43:24', '1', '2022-09-21 22:43:24', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1841, 109, 1036, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1842, 109, 1037, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1843, 109, 1038, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1844, 109, 1039, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1845, 109, 107, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 121); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1846, 111, 1036, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1847, 111, 1037, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1848, 111, 1038, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1849, 111, 1039, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1850, 111, 107, '1', '2022-09-21 22:48:13', '1', '2022-09-21 22:48:13', b'0', 122); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1851, 114, 1, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1852, 114, 1036, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1853, 114, 1037, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1854, 114, 1038, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1855, 114, 1039, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1856, 114, 100, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1857, 114, 101, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1858, 114, 1063, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1859, 114, 103, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1860, 114, 1064, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1861, 114, 1001, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1862, 114, 1065, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1863, 114, 1002, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1864, 114, 1003, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1865, 114, 107, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1866, 114, 1004, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1867, 114, 1005, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1868, 114, 1006, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1869, 114, 1007, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1870, 114, 1008, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1871, 114, 1009, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1872, 114, 1010, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1873, 114, 1011, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1874, 114, 1012, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1875, 114, 1017, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1876, 114, 1018, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1877, 114, 1019, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1878, 114, 1020, '1', '2022-12-30 11:32:03', '1', '2022-12-30 11:32:03', b'0', 125); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1879, 115, 1, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1880, 115, 1036, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1881, 115, 1037, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1882, 115, 1038, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1883, 115, 1039, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1884, 115, 100, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1885, 115, 101, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1886, 115, 1063, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1887, 115, 103, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1888, 115, 1064, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1889, 115, 1001, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1890, 115, 1065, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1891, 115, 1002, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1892, 115, 1003, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1893, 115, 107, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1894, 115, 1004, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1895, 115, 1005, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1896, 115, 1006, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1897, 115, 1007, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1898, 115, 1008, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1899, 115, 1009, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1900, 115, 1010, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1901, 115, 1011, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1902, 115, 1012, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1903, 115, 1017, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1904, 115, 1018, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1905, 115, 1019, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1906, 115, 1020, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1907, 116, 1, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1908, 116, 1036, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1909, 116, 1037, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1910, 116, 1038, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1911, 116, 1039, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1912, 116, 100, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1913, 116, 101, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1914, 116, 1063, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1915, 116, 103, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1916, 116, 1064, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1917, 116, 1001, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1918, 116, 1065, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1919, 116, 1002, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1920, 116, 1003, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1921, 116, 107, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1922, 116, 1004, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1923, 116, 1005, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1924, 116, 1006, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1925, 116, 1007, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1926, 116, 1008, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1927, 116, 1009, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1928, 116, 1010, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1929, 116, 1011, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1930, 116, 1012, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1931, 116, 1017, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1932, 116, 1018, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1933, 116, 1019, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1934, 116, 1020, '1', '2022-12-30 11:33:48', '1', '2022-12-30 11:33:48', b'0', 127); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1963, 118, 1, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1964, 118, 1036, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1965, 118, 1037, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1966, 118, 1038, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1967, 118, 1039, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1968, 118, 100, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1969, 118, 101, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1970, 118, 1063, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1971, 118, 103, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1972, 118, 1064, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1973, 118, 1001, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1974, 118, 1065, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1975, 118, 1002, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1976, 118, 1003, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1977, 118, 107, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1978, 118, 1004, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1979, 118, 1005, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1980, 118, 1006, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1981, 118, 1007, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1982, 118, 1008, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1983, 118, 1009, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1984, 118, 1010, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1985, 118, 1011, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1986, 118, 1012, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1987, 118, 1017, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1988, 118, 1018, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1989, 118, 1019, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1990, 118, 1020, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1991, 2, 1024, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1992, 2, 1025, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1993, 2, 1026, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1994, 2, 1027, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1995, 2, 1028, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1996, 2, 1029, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1997, 2, 1030, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1998, 2, 1031, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1999, 2, 1032, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2000, 2, 1033, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2001, 2, 1034, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2002, 2, 1035, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2003, 2, 1036, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2004, 2, 1037, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2005, 2, 1038, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2006, 2, 1039, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2007, 2, 1040, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2008, 2, 1042, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2009, 2, 1043, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2010, 2, 1045, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2011, 2, 1046, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2012, 2, 1048, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2013, 2, 1050, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2014, 2, 1051, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2015, 2, 1052, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2016, 2, 1053, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2017, 2, 1054, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2018, 2, 1056, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2019, 2, 1057, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2020, 2, 1058, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2021, 2, 2083, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2022, 2, 1059, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2023, 2, 1060, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2024, 2, 1063, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2025, 2, 1064, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2026, 2, 1065, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2027, 2, 1066, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2028, 2, 1067, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2029, 2, 1070, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2030, 2, 1071, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2031, 2, 1072, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2032, 2, 1073, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2033, 2, 1074, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2034, 2, 1075, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2035, 2, 1076, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2036, 2, 1082, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2037, 2, 1085, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2038, 2, 1086, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2039, 2, 1087, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2040, 2, 1088, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2041, 2, 1089, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2042, 2, 1091, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2043, 2, 1092, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2044, 2, 1095, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2045, 2, 1096, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2046, 2, 1097, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2047, 2, 1098, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2048, 2, 1101, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2049, 2, 1102, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2050, 2, 1103, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2051, 2, 1104, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2052, 2, 1105, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2053, 2, 1106, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2054, 2, 1108, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2055, 2, 1109, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2056, 2, 1111, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2057, 2, 1112, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2058, 2, 1113, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2059, 2, 1114, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2060, 2, 1115, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2061, 2, 1127, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2062, 2, 1128, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2063, 2, 1129, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2064, 2, 1130, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2066, 2, 1132, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2067, 2, 1133, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2068, 2, 1134, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2069, 2, 1135, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2070, 2, 1136, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2071, 2, 1137, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2072, 2, 114, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2073, 2, 1139, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2074, 2, 115, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2075, 2, 1140, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2076, 2, 116, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2077, 2, 1141, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2078, 2, 1142, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2079, 2, 1143, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2080, 2, 1150, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2081, 2, 1161, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2082, 2, 1162, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2083, 2, 1163, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2084, 2, 1164, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2085, 2, 1165, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2086, 2, 1166, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2087, 2, 1173, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2088, 2, 1174, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2089, 2, 1175, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2090, 2, 1176, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2091, 2, 1177, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2092, 2, 1178, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2099, 2, 1226, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2100, 2, 1227, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2101, 2, 1228, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2102, 2, 1229, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2103, 2, 1237, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2104, 2, 1238, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2105, 2, 1239, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2106, 2, 1240, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2107, 2, 1241, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2108, 2, 1242, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2109, 2, 1243, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2110, 2, 1247, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2111, 2, 1248, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2112, 2, 1249, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2113, 2, 1250, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2114, 2, 1251, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2115, 2, 1252, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2116, 2, 1254, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2117, 2, 1255, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2118, 2, 1256, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2119, 2, 1257, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2120, 2, 1258, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2121, 2, 1259, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2122, 2, 1260, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2123, 2, 1261, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2124, 2, 1263, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2125, 2, 1264, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2126, 2, 1265, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2127, 2, 1266, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2128, 2, 1267, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2129, 2, 1001, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2130, 2, 1002, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2131, 2, 1003, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2132, 2, 1004, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2133, 2, 1005, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2134, 2, 1006, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2135, 2, 1007, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2136, 2, 1008, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2137, 2, 1009, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2138, 2, 1010, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2139, 2, 1011, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2140, 2, 1012, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2141, 2, 1013, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2142, 2, 1014, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2143, 2, 1015, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2144, 2, 1016, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2145, 2, 1017, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2146, 2, 1018, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2147, 2, 1019, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2148, 2, 1020, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2149, 2, 1021, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2150, 2, 1022, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2151, 2, 1023, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2152, 2, 1281, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2153, 2, 1282, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2154, 2, 2000, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2155, 2, 2002, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2156, 2, 2003, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2157, 2, 2004, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2158, 2, 2005, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2159, 2, 2006, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2160, 2, 2008, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2161, 2, 2009, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2162, 2, 2010, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2163, 2, 2011, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2164, 2, 2012, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2170, 2, 2019, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2171, 2, 2020, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2172, 2, 2021, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2173, 2, 2022, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2174, 2, 2023, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2175, 2, 2025, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2177, 2, 2027, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2178, 2, 2028, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2179, 2, 2029, '1', '2023-01-25 08:42:58', '1', '2023-01-25 08:42:58', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2180, 2, 2014, '1', '2023-01-25 08:43:12', '1', '2023-01-25 08:43:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2181, 2, 2015, '1', '2023-01-25 08:43:12', '1', '2023-01-25 08:43:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2182, 2, 2016, '1', '2023-01-25 08:43:12', '1', '2023-01-25 08:43:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2183, 2, 2017, '1', '2023-01-25 08:43:12', '1', '2023-01-25 08:43:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2184, 2, 2018, '1', '2023-01-25 08:43:12', '1', '2023-01-25 08:43:12', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2188, 101, 1024, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2189, 101, 1, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2190, 101, 1025, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2191, 101, 1026, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2192, 101, 1027, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2193, 101, 1028, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2194, 101, 1029, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2195, 101, 1030, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2196, 101, 1036, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2197, 101, 1037, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2198, 101, 1038, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2199, 101, 1039, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2200, 101, 1040, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2201, 101, 1042, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2202, 101, 1043, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2203, 101, 1045, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2204, 101, 1046, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2205, 101, 1048, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2206, 101, 2083, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2207, 101, 1063, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2208, 101, 1064, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2209, 101, 1065, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2210, 101, 1093, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2211, 101, 1094, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2212, 101, 1095, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2213, 101, 1096, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2214, 101, 1097, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2215, 101, 1098, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2216, 101, 1100, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2217, 101, 1101, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2218, 101, 1102, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2219, 101, 1103, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2220, 101, 1104, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2221, 101, 1105, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2222, 101, 1106, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2223, 101, 2130, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2224, 101, 1107, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2225, 101, 2131, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2226, 101, 1108, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2227, 101, 2132, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2228, 101, 1109, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2229, 101, 2133, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2230, 101, 2134, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2231, 101, 1110, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2232, 101, 2135, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2233, 101, 1111, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2234, 101, 2136, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2235, 101, 1112, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2236, 101, 2137, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2237, 101, 1113, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2238, 101, 2138, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2239, 101, 1114, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2240, 101, 2139, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2241, 101, 1115, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2242, 101, 2140, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2243, 101, 2141, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2244, 101, 2142, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2245, 101, 2143, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2246, 101, 2144, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2247, 101, 2145, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2248, 101, 2146, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2249, 101, 2147, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2250, 101, 100, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2251, 101, 2148, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2252, 101, 101, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2253, 101, 2149, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2254, 101, 102, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2255, 101, 2150, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2256, 101, 103, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2257, 101, 2151, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2258, 101, 104, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2259, 101, 2152, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2260, 101, 105, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2261, 101, 107, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2262, 101, 108, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2263, 101, 109, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2264, 101, 1138, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2265, 101, 1139, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2266, 101, 1140, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2267, 101, 1141, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2268, 101, 1142, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2269, 101, 1143, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2270, 101, 1224, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2271, 101, 1225, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2272, 101, 1226, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2273, 101, 1227, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2274, 101, 1228, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2275, 101, 1229, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2276, 101, 1247, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2277, 101, 1248, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2278, 101, 1249, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2279, 101, 1250, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2280, 101, 1251, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2281, 101, 1252, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2282, 101, 1261, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2283, 101, 1263, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2284, 101, 1264, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2285, 101, 1265, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2286, 101, 1266, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2287, 101, 1267, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2288, 101, 1001, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2289, 101, 1002, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2290, 101, 1003, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2291, 101, 1004, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2292, 101, 1005, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2293, 101, 1006, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2294, 101, 1007, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2295, 101, 1008, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2296, 101, 1009, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2297, 101, 1010, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2298, 101, 1011, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2299, 101, 1012, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2300, 101, 500, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2301, 101, 1013, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2302, 101, 501, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2303, 101, 1014, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2304, 101, 1015, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2305, 101, 1016, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2306, 101, 1017, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2307, 101, 1018, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2308, 101, 1019, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2309, 101, 1020, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2310, 101, 1021, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2311, 101, 1022, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2312, 101, 1023, '1', '2023-02-09 23:49:46', '1', '2023-02-09 23:49:46', b'0', 1); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2789, 136, 1, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2790, 136, 1036, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2791, 136, 1037, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2792, 136, 1038, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2793, 136, 1039, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2794, 136, 100, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2795, 136, 101, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2796, 136, 1063, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2797, 136, 103, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2798, 136, 1064, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2799, 136, 1001, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2800, 136, 1065, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2801, 136, 1002, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2802, 136, 1003, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2803, 136, 107, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2804, 136, 1004, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2805, 136, 1005, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2806, 136, 1006, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2807, 136, 1007, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2808, 136, 1008, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2809, 136, 1009, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2810, 136, 1010, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2811, 136, 1011, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2812, 136, 1012, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2813, 136, 1017, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2814, 136, 1018, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2815, 136, 1019, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2816, 136, 1020, '1', '2023-03-05 21:23:32', '1', '2023-03-05 21:23:32', b'0', 147); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2817, 137, 1, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2818, 137, 1036, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2819, 137, 1037, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2820, 137, 1038, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2821, 137, 1039, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2822, 137, 100, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2823, 137, 101, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2824, 137, 1063, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2825, 137, 103, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2826, 137, 1064, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2827, 137, 1001, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2828, 137, 1065, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2829, 137, 1002, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2830, 137, 1003, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2831, 137, 107, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2832, 137, 1004, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2833, 137, 1005, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2834, 137, 1006, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2835, 137, 1007, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2836, 137, 1008, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2837, 137, 1009, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2838, 137, 1010, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2839, 137, 1011, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2840, 137, 1012, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2841, 137, 1017, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2842, 137, 1018, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2843, 137, 1019, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2844, 137, 1020, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2845, 138, 1, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2846, 138, 1036, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2847, 138, 1037, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2848, 138, 1038, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2849, 138, 1039, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2850, 138, 100, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2851, 138, 101, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2852, 138, 1063, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2853, 138, 103, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2854, 138, 1064, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2855, 138, 1001, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2856, 138, 1065, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2857, 138, 1002, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2858, 138, 1003, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2859, 138, 107, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2860, 138, 1004, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2861, 138, 1005, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2862, 138, 1006, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2863, 138, 1007, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2864, 138, 1008, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2865, 138, 1009, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2866, 138, 1010, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2867, 138, 1011, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2868, 138, 1012, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2869, 138, 1017, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2870, 138, 1018, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2871, 138, 1019, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2872, 138, 1020, '1', '2023-03-05 21:59:02', '1', '2023-03-05 21:59:02', b'0', 149); +COMMIT; + +-- ---------------------------- +-- Table structure for system_sensitive_word +-- ---------------------------- +DROP TABLE IF EXISTS `system_sensitive_word`; +CREATE TABLE `system_sensitive_word` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '敏感词', + `description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '描述', + `tags` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标签数组', + `status` tinyint NOT NULL COMMENT '状态', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '敏感词'; + +-- ---------------------------- +-- Records of system_sensitive_word +-- ---------------------------- +BEGIN; +INSERT INTO `system_sensitive_word` (`id`, `name`, `description`, `tags`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3, '土豆', '好呀', '蔬菜,短信', 0, '1', '2022-04-08 21:07:12', '1', '2022-04-09 10:28:14', b'0'); +INSERT INTO `system_sensitive_word` (`id`, `name`, `description`, `tags`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, 'XXX', NULL, '短信', 0, '1', '2022-04-08 21:27:49', '1', '2022-06-19 00:36:50', b'0'); +INSERT INTO `system_sensitive_word` (`id`, `name`, `description`, `tags`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (5, '白痴', NULL, '测试', 0, '1', '2022-12-31 19:08:25', '1', '2022-12-31 19:08:25', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_sms_channel +-- ---------------------------- +DROP TABLE IF EXISTS `system_sms_channel`; +CREATE TABLE `system_sms_channel` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `signature` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信签名', + `code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '渠道编码', + `status` tinyint NOT NULL COMMENT '开启状态', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `api_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信 API 的账号', + `api_secret` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 的秘钥', + `callback_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信发送回调 URL', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信渠道'; + +-- ---------------------------- +-- Records of system_sms_channel +-- ---------------------------- +BEGIN; +INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 'Ballcat', 'ALIYUN', 0, '啦啦啦', 'LTAI5tCnKso2uG3kJ5gRav88', 'fGJ5SNXL7P1NHNRmJ7DJaMJGPyE55C', NULL, '', '2021-03-31 11:53:10', '1', '2021-04-14 00:08:37', b'0'); +INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (4, '测试渠道', 'DEBUG_DING_TALK', 0, '123', '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2021-04-13 00:23:14', '1', '2022-03-27 20:29:49', b'0'); +INSERT INTO `system_sms_channel` (`id`, `signature`, `code`, `status`, `remark`, `api_key`, `api_secret`, `callback_url`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, '测试演示', 'DEBUG_DING_TALK', 0, NULL, '696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859', 'SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67', NULL, '1', '2022-04-10 23:07:59', '1', '2022-06-19 00:33:54', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_sms_code +-- ---------------------------- +DROP TABLE IF EXISTS `system_sms_code`; +CREATE TABLE `system_sms_code` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '手机号', + `code` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '验证码', + `create_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '创建 IP', + `scene` tinyint NOT NULL COMMENT '发送场景', + `today_index` tinyint NOT NULL COMMENT '今日发送的第几条', + `used` tinyint NOT NULL COMMENT '是否使用', + `used_time` datetime NULL DEFAULT NULL COMMENT '使用时间', + `used_ip` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '使用 IP', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_mobile`(`mobile` ASC) USING BTREE COMMENT '手机号' +) ENGINE = InnoDB AUTO_INCREMENT = 484 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '手机验证码'; + +-- ---------------------------- +-- Records of system_sms_code +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_sms_log +-- ---------------------------- +DROP TABLE IF EXISTS `system_sms_log`; +CREATE TABLE `system_sms_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `channel_id` bigint NOT NULL COMMENT '短信渠道编号', + `channel_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信渠道编码', + `template_id` bigint NOT NULL COMMENT '模板编号', + `template_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码', + `template_type` tinyint NOT NULL COMMENT '短信类型', + `template_content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信内容', + `template_params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信参数', + `api_template_id` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信 API 的模板编号', + `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '手机号', + `user_id` bigint NULL DEFAULT NULL COMMENT '用户编号', + `user_type` tinyint NULL DEFAULT NULL COMMENT '用户类型', + `send_status` tinyint NOT NULL DEFAULT 0 COMMENT '发送状态', + `send_time` datetime NULL DEFAULT NULL COMMENT '发送时间', + `send_code` int NULL DEFAULT NULL COMMENT '发送结果的编码', + `send_msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '发送结果的提示', + `api_send_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送结果的编码', + `api_send_msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送失败的提示', + `api_request_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送返回的唯一请求 ID', + `api_serial_no` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信 API 发送返回的序号', + `receive_status` tinyint NOT NULL DEFAULT 0 COMMENT '接收状态', + `receive_time` datetime NULL DEFAULT NULL COMMENT '接收时间', + `api_receive_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'API 接收结果的编码', + `api_receive_msg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'API 接收结果的说明', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 349 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志'; + +-- ---------------------------- +-- Records of system_sms_log +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_sms_template +-- ---------------------------- +DROP TABLE IF EXISTS `system_sms_template`; +CREATE TABLE `system_sms_template` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号', + `type` tinyint NOT NULL COMMENT '短信签名', + `status` tinyint NOT NULL COMMENT '开启状态', + `code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板编码', + `name` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板名称', + `content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模板内容', + `params` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '参数数组', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `api_template_id` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信 API 的模板编号', + `channel_id` bigint NOT NULL COMMENT '短信渠道编号', + `channel_code` varchar(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '短信渠道编码', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板'; + +-- ---------------------------- +-- Records of system_sms_template +-- ---------------------------- +BEGIN; +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 1, 0, 'test_01', '测试验证码短信', '正在进行登录操作{operation},您的验证码是{code}', '[\"operation\",\"code\"]', NULL, '4383920', 6, 'DEBUG_DING_TALK', '', '2021-03-31 10:49:38', '1', '2022-12-10 21:26:20', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (3, 1, 0, 'test_02', '公告通知', '您的验证码{code},该验证码5分钟内有效,请勿泄漏于他人!', '[\"code\"]', NULL, 'SMS_207945135', 2, 'ALIYUN', '', '2021-03-31 11:56:30', '1', '2021-04-10 01:22:02', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (6, 3, 0, 'test-01', '测试模板', '哈哈哈 {name}', '[\"name\"]', 'f哈哈哈', '4383920', 6, 'DEBUG_DING_TALK', '1', '2021-04-10 01:07:21', '1', '2022-12-10 21:26:09', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (7, 3, 0, 'test-04', '测试下', '老鸡{name},牛逼{code}', '[\"name\",\"code\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2021-04-13 00:29:53', '1', '2021-04-14 00:30:38', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, 0, 'user-sms-login', '前台用户短信登录', '您的验证码是{code}', '[\"code\"]', NULL, '4372216', 6, 'DEBUG_DING_TALK', '1', '2021-10-11 08:10:00', '1', '2022-12-10 21:25:59', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 2, 0, 'bpm_task_assigned', '【工作流】任务被分配', '您收到了一条新的待办任务:{processInstanceName}-{taskName},申请人:{startUserNickname},处理链接:{detailUrl}', '[\"processInstanceName\",\"taskName\",\"startUserNickname\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-21 22:31:19', '1', '2022-01-22 00:03:36', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (10, 2, 0, 'bpm_process_instance_reject', '【工作流】流程被不通过', '您的流程被审批不通过:{processInstanceName},原因:{reason},查看链接:{detailUrl}', '[\"processInstanceName\",\"reason\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-22 00:03:31', '1', '2022-05-01 12:33:14', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (11, 2, 0, 'bpm_process_instance_approve', '【工作流】流程被通过', '您的流程被审批通过:{processInstanceName},查看链接:{detailUrl}', '[\"processInstanceName\",\"detailUrl\"]', NULL, 'suibian', 4, 'DEBUG_DING_TALK', '1', '2022-01-22 00:04:31', '1', '2022-03-27 20:32:21', b'0'); +INSERT INTO `system_sms_template` (`id`, `type`, `status`, `code`, `name`, `content`, `params`, `remark`, `api_template_id`, `channel_id`, `channel_code`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 2, 0, 'demo', '演示模板', '我就是测试一下下', '[]', NULL, 'biubiubiu', 6, 'DEBUG_DING_TALK', '1', '2022-04-10 23:22:49', '1', '2023-03-24 23:45:07', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_social_user +-- ---------------------------- +DROP TABLE IF EXISTS `system_social_user`; +CREATE TABLE `system_social_user` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键(自增策略)', + `type` tinyint NOT NULL COMMENT '社交平台的类型', + `openid` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '社交 openid', + `token` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '社交 token', + `raw_token_info` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原始 Token 数据,一般是 JSON 格式', + `nickname` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户头像', + `raw_user_info` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '原始用户数据,一般是 JSON 格式', + `code` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '最后一次的认证 code', + `state` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后一次的认证 state', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交用户表'; + +-- ---------------------------- +-- Records of system_social_user +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_social_user_bind +-- ---------------------------- +DROP TABLE IF EXISTS `system_social_user_bind`; +CREATE TABLE `system_social_user_bind` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键(自增策略)', + `user_id` bigint NOT NULL COMMENT '用户编号', + `user_type` tinyint NOT NULL COMMENT '用户类型', + `social_type` tinyint NOT NULL COMMENT '社交平台的类型', + `social_user_id` bigint NOT NULL COMMENT '社交用户的编号', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 39 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '社交绑定表'; + +-- ---------------------------- +-- Records of system_social_user_bind +-- ---------------------------- +BEGIN; +COMMIT; + +-- ---------------------------- +-- Table structure for system_tenant +-- ---------------------------- +DROP TABLE IF EXISTS `system_tenant`; +CREATE TABLE `system_tenant` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '租户编号', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '租户名', + `contact_user_id` bigint NULL DEFAULT NULL COMMENT '联系人的用户编号', + `contact_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联系人', + `contact_mobile` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系手机', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '租户状态(0正常 1停用)', + `domain` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '绑定域名', + `package_id` bigint NOT NULL COMMENT '租户套餐编号', + `expire_time` datetime NOT NULL COMMENT '过期时间', + `account_count` int NOT NULL COMMENT '账号数量', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 150 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '租户表'; + +-- ---------------------------- +-- Records of system_tenant +-- ---------------------------- +BEGIN; +INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `domain`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'https://www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2022-02-23 12:15:11', b'0'); +INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `domain`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'http://www.iocoder.cn', 111, '2024-03-11 00:00:00', 20, '1', '2022-02-22 00:56:14', '1', '2022-05-17 10:03:59', b'0'); +INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `domain`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'https://www.iocoder.cn', 111, '2022-04-30 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2023-04-15 09:17:54', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_tenant_package +-- ---------------------------- +DROP TABLE IF EXISTS `system_tenant_package`; +CREATE TABLE `system_tenant_package` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '套餐编号', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐名', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '租户状态(0正常 1停用)', + `remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '备注', + `menu_ids` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关联的菜单编号', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 112 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '租户套餐表'; + +-- ---------------------------- +-- Records of system_tenant_package +-- ---------------------------- +BEGIN; +INSERT INTO `system_tenant_package` (`id`, `name`, `status`, `remark`, `menu_ids`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (111, '普通套餐', 0, '小功能', '[1,1036,1037,1038,1039,100,101,1063,103,1064,1001,1065,1002,1003,107,1004,1005,1006,1007,1008,1009,1010,1011,1012,1017,1018,1019,1020]', '1', '2022-02-22 00:54:00', '1', '2022-09-21 22:48:12', b'0'); +COMMIT; + +-- ---------------------------- +-- Table structure for system_user_post +-- ---------------------------- +DROP TABLE IF EXISTS `system_user_post`; +CREATE TABLE `system_user_post` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id', + `user_id` bigint NOT NULL DEFAULT 0 COMMENT '用户ID', + `post_id` bigint NOT NULL DEFAULT 0 COMMENT '岗位ID', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 118 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户岗位表'; + +-- ---------------------------- +-- Records of system_user_post +-- ---------------------------- +BEGIN; +INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, 1, 1, 'admin', '2022-05-02 07:25:24', 'admin', '2022-05-02 07:25:24', b'0', 1); +INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 100, 1, 'admin', '2022-05-02 07:25:24', 'admin', '2022-05-02 07:25:24', b'0', 1); +INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 114, 3, 'admin', '2022-05-02 07:25:24', 'admin', '2022-05-02 07:25:24', b'0', 1); +INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 104, 1, '1', '2022-05-16 19:36:28', '1', '2022-05-16 19:36:28', b'0', 1); +INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (116, 117, 2, '1', '2022-07-09 17:40:26', '1', '2022-07-09 17:40:26', b'0', 1); +INSERT INTO `system_user_post` (`id`, `user_id`, `post_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 118, 1, '1', '2022-07-09 17:44:44', '1', '2022-07-09 17:44:44', b'0', 1); +COMMIT; + +-- ---------------------------- +-- Table structure for system_user_role +-- ---------------------------- +DROP TABLE IF EXISTS `system_user_role`; +CREATE TABLE `system_user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增编号', + `user_id` bigint NOT NULL COMMENT '用户ID', + `role_id` bigint NOT NULL COMMENT '角色ID', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户和角色关联表'; + +-- ---------------------------- +-- Records of system_user_role +-- ---------------------------- +BEGIN; +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 1, 1, '', '2022-01-11 13:19:45', '', '2022-05-12 12:35:17', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 2, 2, '', '2022-01-11 13:19:45', '', '2022-05-12 12:35:13', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 100, 101, '', '2022-01-11 13:19:45', '', '2022-05-12 12:35:13', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, 100, 1, '', '2022-01-11 13:19:45', '', '2022-05-12 12:35:12', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, 100, 2, '', '2022-01-11 13:19:45', '', '2022-05-12 12:35:11', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (10, 103, 1, '1', '2022-01-11 13:19:45', '1', '2022-01-11 13:19:45', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (11, 107, 106, '1', '2022-02-20 22:59:33', '1', '2022-02-20 22:59:33', b'0', 118); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (12, 108, 107, '1', '2022-02-20 23:00:50', '1', '2022-02-20 23:00:50', b'0', 119); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (13, 109, 108, '1', '2022-02-20 23:11:50', '1', '2022-02-20 23:11:50', b'0', 120); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (14, 110, 109, '1', '2022-02-22 00:56:14', '1', '2022-02-22 00:56:14', b'0', 121); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (15, 111, 110, '110', '2022-02-23 13:14:38', '110', '2022-02-23 13:14:38', b'0', 121); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (16, 113, 111, '1', '2022-03-07 21:37:58', '1', '2022-03-07 21:37:58', b'0', 122); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (17, 114, 101, '1', '2022-03-19 21:51:13', '1', '2022-03-19 21:51:13', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (18, 1, 2, '1', '2022-05-12 20:39:29', '1', '2022-05-12 20:39:29', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (19, 116, 113, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (20, 104, 101, '1', '2022-05-28 15:43:57', '1', '2022-05-28 15:43:57', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (22, 115, 2, '1', '2022-07-21 22:08:30', '1', '2022-07-21 22:08:30', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (23, 119, 114, '1', '2022-12-30 11:32:04', '1', '2022-12-30 11:32:04', b'0', 125); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (24, 120, 115, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (25, 121, 116, '1', '2022-12-30 11:33:49', '1', '2022-12-30 11:33:49', b'0', 127); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (26, 122, 118, '1', '2022-12-30 11:47:53', '1', '2022-12-30 11:47:53', b'0', 129); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (27, 112, 101, '1', '2023-02-09 23:18:51', '1', '2023-02-09 23:18:51', b'0', 1); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (28, 123, 136, '1', '2023-03-05 21:23:35', '1', '2023-03-05 21:23:35', b'0', 147); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (29, 124, 137, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_user_role` (`id`, `user_id`, `role_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (30, 125, 138, '1', '2023-03-05 21:59:03', '1', '2023-03-05 21:59:03', b'0', 149); +COMMIT; + +-- ---------------------------- +-- Table structure for system_users +-- ---------------------------- +DROP TABLE IF EXISTS `system_users`; +CREATE TABLE `system_users` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户账号', + `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '密码', + `nickname` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户昵称', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注', + `dept_id` bigint NULL DEFAULT NULL COMMENT '部门ID', + `post_ids` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '岗位编号数组', + `email` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '用户邮箱', + `mobile` varchar(11) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '手机号码', + `sex` tinyint NULL DEFAULT 0 COMMENT '用户性别', + `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '头像地址', + `status` tinyint NOT NULL DEFAULT 0 COMMENT '帐号状态(0正常 1停用)', + `login_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '最后登录IP', + `login_date` datetime NULL DEFAULT NULL COMMENT '最后登录时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `idx_username`(`username` ASC, `update_time` ASC, `tenant_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 126 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户信息表'; + +-- ---------------------------- +-- Records of system_users +-- ---------------------------- +BEGIN; +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://test.yunxi.iocoder.cn/e1fdd7271685ec143a0900681606406621717a666ad0b2798b096df41422b32f.png', 0, '127.0.0.1', '2023-07-24 08:41:23', 'admin', '2021-01-05 17:03:47', NULL, '2023-07-24 08:41:23', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yunxi', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yunxi@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '127.0.0.1', '2022-07-08 01:26:27', '', '2021-01-13 23:50:35', NULL, '2022-07-08 01:26:27', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$10$GP8zvqHB//TekuzYZSBYAuBQJiNq1.fxQVDYJ.uBCOnWCtDVKE4H6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '127.0.0.1', '2022-05-28 15:43:17', '', '2021-01-21 02:13:53', NULL, '2022-07-09 09:00:33', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (107, 'admin107', '$2a$10$dYOOBKMO93v/.ReCqzyFg.o67Tqk.bbc2bhrpyBGkIw9aypCtr2pm', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 22:59:33', '1', '2022-02-27 08:26:51', b'0', 118); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (108, 'admin108', '$2a$10$y6mfvKoNYL1GXWak8nYwVOH.kCWqjactkzdoIDgiKl93WN3Ejg.Lu', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:00:50', '1', '2022-02-27 08:26:53', b'0', 119); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (109, 'admin109', '$2a$10$JAqvH0tEc0I7dfDVBI7zyuB4E3j.uH6daIjV53.vUS6PknFkDJkuK', '芋艿', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '', NULL, '1', '2022-02-20 23:11:50', '1', '2022-02-27 08:26:56', b'0', 120); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (110, 'admin110', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '小王', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '127.0.0.1', '2022-09-25 22:47:33', '1', '2022-02-22 00:56:14', NULL, '2022-09-25 22:47:33', b'0', 121); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (111, 'test', '$2a$10$mExveopHUx9Q4QiLtAzhDeH3n4/QlNLzEsM4AqgxKrU.ciUZDXZCy', '测试用户', NULL, NULL, '[]', '', '', 0, '', 0, '', NULL, '110', '2022-02-23 13:14:33', '110', '2022-02-23 13:14:33', b'0', 121); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (112, 'newobject', '$2a$10$3alwklxqfq8/hKoW6oUV0OJp0IdQpBDauLy4633SpUjrRsStl6kMa', '新对象', NULL, 100, '[]', '', '', 1, '', 0, '0:0:0:0:0:0:0:1', '2023-02-10 13:48:13', '1', '2022-02-23 19:08:03', NULL, '2023-02-10 13:48:13', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (113, 'aoteman', '$2a$10$0acJOIk2D25/oC87nyclE..0lzeu9DtQ/n3geP4fkun/zIVRhHJIO', '芋道', NULL, NULL, NULL, '', '15601691300', 0, '', 0, '127.0.0.1', '2022-03-19 18:38:51', '1', '2022-03-07 21:37:58', NULL, '2022-03-19 18:38:51', b'0', 122); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (114, 'hrmgr', '$2a$10$TR4eybBioGRhBmDBWkqWLO6NIh3mzYa8KBKDDB5woiGYFVlRAi.fu', 'hr 小姐姐', NULL, NULL, '[3]', '', '', 0, '', 0, '127.0.0.1', '2022-03-19 22:15:43', '1', '2022-03-19 21:50:58', NULL, '2022-03-19 22:15:43', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (115, 'aotemane', '$2a$10$/WCwGHu1eq0wOVDd/u8HweJ0gJCHyLS6T7ndCqI8UXZAQom1etk2e', '1', '11', 101, '[]', '', '', 1, '', 0, '', NULL, '1', '2022-04-30 02:55:43', '1', '2022-06-22 13:34:58', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (116, '15601691302', '$2a$10$L5C4S0U6adBWMvFv1Wwl4.DI/NwYS3WIfLj5Q.Naqr5II8CmqsDZ6', '小豆', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2022-05-17 10:07:10', '1', '2022-05-17 10:07:10', b'0', 124); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (117, 'admin123', '$2a$10$WI8Gg/lpZQIrOEZMHqka7OdFaD4Nx.B/qY8ZGTTUKrOJwaHFqibaC', '测试号', '1111', 100, '[2]', '', '15601691234', 1, '', 0, '', NULL, '1', '2022-07-09 17:40:26', '1', '2022-07-09 17:40:26', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (118, 'goudan', '$2a$10$Lrb71muL.s5/AFjQ2IHkzOFlAFwUToH.zQL7bnghvTDt/QptjGgF6', '狗蛋', NULL, 103, '[1]', '', '', 2, '', 0, '', NULL, '1', '2022-07-09 17:44:43', '1', '2022-12-31 17:29:13', b'0', 1); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (119, 'admin', '$2a$10$AheSOpxeWQYhEO/gGZhDz.oifdX5zt.kprWNHptPiiStUx4mXmHb.', '12', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2022-12-30 11:32:04', '1', '2022-12-30 11:32:04', b'0', 125); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (120, 'admin', '$2a$10$D.xFtcgma/NJ3SyYlUj3bORcs0mwOD6Zu.4I7GCI/8/25/QSn4qJC', '12', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2022-12-30 11:33:42', '1', '2022-12-30 11:33:42', b'0', 126); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (121, 'admin', '$2a$10$R2guBf7TyERjjW9lm0Pd0Osut6vt7NuH2Vx6fkOI5.VgSvJK2Xb82', '12', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2022-12-30 11:33:49', '1', '2022-12-30 11:33:49', b'0', 127); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (122, 'admin', '$2a$10$pwxqUUza61HBgx3FTjp2d.Mc2UKalikXxP91wUdP4bFe7Hl.lfmeq', '12', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2022-12-30 11:47:52', '1', '2022-12-30 11:47:52', b'0', 129); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (123, 'tudou', '$2a$10$m33ROHSPa9lshwQIaiVlFeoG1TZjCoQmfvExn4QWS8r5X59AEsTz2', '15601691234', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2023-03-05 21:23:35', '1', '2023-03-05 21:23:35', b'0', 147); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (124, 'tudou', '$2a$10$1pzAJAEIRf/vYyMy8FTFiOzX40Q/NnozXixun/ExPZwv8A/CQkR4q', '15601691234', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2023-03-05 21:42:27', '1', '2023-03-05 21:42:27', b'0', 148); +INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (125, 'admin', '$2a$10$E49momkI6Uf9v6pkfjoRP.dHzK4RjDIK39AWHz9eXRmqUR5sbJpoy', '秃头', NULL, NULL, NULL, '', '', 0, '', 0, '', NULL, '1', '2023-03-05 21:59:03', '1', '2023-03-05 21:59:03', b'0', 149); +COMMIT; + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/yunxi-dependencies/pom.xml b/yunxi-dependencies/pom.xml new file mode 100644 index 0000000..29b3f4d --- /dev/null +++ b/yunxi-dependencies/pom.xml @@ -0,0 +1,648 @@ + + + 4.0.0 + + com.yunxi.scm + yunxi-dependencies + ${revision} + pom + + ${project.artifactId} + 基础 bom 文件,管理整个项目的依赖版本 + https://github.com/YunaiV/ruoyi-vue-pro + + + 1.8.0-snapshot + + 2.7.13 + + 1.6.15 + 4.1.0 + 2.5 + + 1.2.18 + 3.5.3.1 + 3.5.3.1 + 3.6.1 + 1.4.5 + 3.18.0 + 8.1.2.141 + + 2.2.3 + 1.7.1 + + 8.12.0 + 2.7.10 + 0.33.0 + + 7.2.11.RELEASE + 1.0.7 + 4.11.0 + + 6.8.0 + + 1.0.5 + 1.15.4 + 1.18.28 + 1.5.5.Final + 5.8.20 + 3.3.2 + 2.3 + 1.0.5 + 1.2.83 + 32.0.1-jre + 5.1.0 + 2.14.2 + 3.9.0 + 0.1.55 + 2.7.0 + 4.1.90.Final + 2.7.0 + + 3.0.0 + 4.10.0 + 2.11.0 + 8.5.4 + 4.6.3 + 2.2.1 + 3.1.758 + 1.0.1 + 1.5.8 + 2.12.2 + 4.5.0 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + com.yunxi.scm + yunxi-spring-boot-starter-banner + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-trade + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-dict + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-sms + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-pay + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-weixin + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-tenant + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-data-permission + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-social + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-error-code + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-ip + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-captcha + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-desensitize + ${revision} + + + + + + org.springframework.boot + spring-boot-configuration-processor + ${spring.boot.version} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + ${revision} + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + ${revision} + + + + com.github.xiaoymin + knife4j-openapi3-spring-boot-starter + ${knife4j.version} + + + org.springdoc + springdoc-openapi-ui + ${springdoc.version} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + ${revision} + + + + com.alibaba + druid-spring-boot-starter + ${druid.version} + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.baomidou + mybatis-plus-generator + ${mybatis-plus-generator.version} + + + com.baomidou + dynamic-datasource-spring-boot-starter + ${dynamic-datasource.version} + + + com.github.yulichang + mybatis-plus-join-boot-starter + ${mybatis-plus-join-boot-starter.version} + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + ${revision} + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + + + com.dameng + DmJdbcDriver18 + ${dm8.jdbc.version} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-job + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mq + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-protection + ${revision} + + + + com.baomidou + lock4j-redisson-spring-boot-starter + ${lock4j.version} + + + redisson-spring-boot-starter + org.redisson + + + + + + io.github.resilience4j + resilience4j-ratelimiter + ${resilience4j.version} + + + io.github.resilience4j + resilience4j-spring-boot2 + ${resilience4j.version} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-monitor + ${revision} + + + + org.apache.skywalking + apm-toolkit-trace + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-logback-1.x + ${skywalking.version} + + + org.apache.skywalking + apm-toolkit-opentracing + ${skywalking.version} + + + + + + + + + + + + + io.opentracing + opentracing-api + ${opentracing.version} + + + io.opentracing + opentracing-util + ${opentracing.version} + + + io.opentracing + opentracing-noop + ${opentracing.version} + + + + de.codecentric + spring-boot-admin-starter-server + ${spring-boot-admin.version} + + + de.codecentric + spring-boot-admin-starter-client + ${spring-boot-admin.version} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + ${revision} + test + + + + org.mockito + mockito-inline + ${mockito-inline.version} + + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + + + asm + org.ow2.asm + + + org.mockito + mockito-core + + + + + + com.github.fppt + jedis-mock + ${jedis-mock.version} + + + + uk.co.jemos.podam + podam + ${podam.version} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-flowable + ${revision} + + + org.flowable + flowable-spring-boot-starter-process + ${flowable.version} + + + org.flowable + flowable-spring-boot-starter-actuator + ${flowable.version} + + + + + + com.yunxi.scm + yunxi-common + ${revision} + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + ${revision} + + + + org.projectlombok + lombok + ${lombok.version} + + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.mapstruct + mapstruct-jdk8 + ${mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${mapstruct.version} + + + + cn.hutool + hutool-all + ${hutool.version} + + + + com.alibaba + easyexcel + ${easyexcel.verion} + + + commons-io + commons-io + ${commons-io.version} + + + org.apache.tika + tika-core + ${tika-core.version} + + + + org.apache.velocity + velocity-engine-core + ${velocity.version} + + + + com.alibaba + fastjson + ${fastjson.version} + + + + cn.smallbun.screw + screw-core + ${screw.version} + + + org.freemarker + freemarker + + + com.alibaba + fastjson + + + + + + com.google.guava + guava + ${guava.version} + + + + com.google.inject + guice + ${guice.version} + + + + com.alibaba + transmittable-thread-local + ${transmittable-thread-local.version} + + + + commons-net + commons-net + ${commons-net.version} + + + + com.jcraft + jsch + ${jsch.version} + + + + io.netty + netty-all + ${netty-all.version} + + + + com.xingyuv + spring-boot-starter-captcha-plus + ${captcha-plus.version} + + + + org.lionsoul + ip2region + ${ip2region.version} + + + + org.jsoup + jsoup + ${jsoup.version} + + + + + com.squareup.okio + okio + ${okio.version} + + + com.squareup.okhttp3 + okhttp + ${okhttp3.version} + + + com.yunxi.scm + yunxi-spring-boot-starter-file + ${revision} + + + io.minio + minio + ${minio.version} + + + + + com.aliyun + aliyun-java-sdk-core + ${aliyun-java-sdk-core.version} + + + opentracing-api + io.opentracing + + + opentracing-util + io.opentracing + + + + + com.aliyun + aliyun-java-sdk-dysmsapi + ${aliyun-java-sdk-dysmsapi.version} + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + ${tencentcloud-sdk-java.version} + + + + + com.xingyuv + spring-boot-starter-justauth + ${justauth.version} + + + + com.github.binarywang + weixin-java-pay + ${weixin-java.version} + + + com.github.binarywang + weixin-java-mp + ${weixin-java.version} + + + com.github.binarywang + wx-java-mp-spring-boot-starter + ${weixin-java.version} + + + com.github.binarywang + wx-java-miniapp-spring-boot-starter + ${weixin-java.version} + + + + + org.jeecgframework.jimureport + jimureport-spring-boot-starter + ${jimureport.version} + + + com.alibaba + druid + + + + + xerces + xercesImpl + ${xercesImpl.version} + + + + org.springframework.boot + spring-boot-starter-websocket + ${spring.boot.version} + + + + + diff --git a/yunxi-example/pom.xml b/yunxi-example/pom.xml new file mode 100644 index 0000000..03a41c0 --- /dev/null +++ b/yunxi-example/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + + + com.yunxi.scm + yunxi-example + 1.0.0-snapshot + pom + + yunxi-sso-demo-by-code + yunxi-sso-demo-by-password + + + ${project.artifactId} + 提供各种示例,例如说:SSO 单点登录 + https://github.com/YunaiV/ruoyi-vue-pro + + diff --git a/yunxi-example/yunxi-sso-demo-by-code/pom.xml b/yunxi-example/yunxi-sso-demo-by-code/pom.xml new file mode 100644 index 0000000..fa84493 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + + com.yunxi.scm + yunxi-sso-demo-by-code + 1.0.0-snapshot + jar + + ${project.artifactId} + 基于授权码模式,如何实现 SSO 单点登录? + https://github.com/YunaiV/ruoyi-vue-pro + + + + 8 + 8 + UTF-8 + + 2.7.13 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + cn.hutool + hutool-all + 5.8.20 + + + + org.projectlombok + lombok + true + + + + diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/SSODemoApplication.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/SSODemoApplication.java new file mode 100644 index 0000000..c1dce1b --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/SSODemoApplication.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.ssodemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SSODemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SSODemoApplication.class, args); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/OAuth2Client.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/OAuth2Client.java new file mode 100644 index 0000000..da3436e --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/OAuth2Client.java @@ -0,0 +1,157 @@ +package com.yunxi.scm.ssodemo.client; + +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; +import com.yunxi.scm.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.Base64Utils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; + +/** + * OAuth 2.0 客户端 + * + * 对应调用 OAuth2OpenController 接口 + */ +@Component +public class OAuth2Client { + + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2"; + + /** + * 租户编号 + * + * 默认使用 1;如果使用别的租户,可以调整 + */ + public static final Long TENANT_ID = 1L; + + private static final String CLIENT_ID = "yunxi-sso-demo-by-code"; + private static final String CLIENT_SECRET = "test"; + + +// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 使用 code 授权码,获得访问令牌 + * + * @param code 授权码 + * @param redirectUri 重定向 URI + * @return 访问令牌 + */ + public CommonResult postAccessToken(String code, String redirectUri) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "authorization_code"); + body.add("code", code); + body.add("redirect_uri", redirectUri); +// body.add("state", ""); // 选填;填了会校验 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 校验访问令牌,并返回它的基本信息 + * + * @param token 访问令牌 + * @return 访问令牌的基本信息 + */ + public CommonResult checkToken(String token) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/check-token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌 + */ + public CommonResult refreshToken(String refreshToken) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "refresh_token"); + body.add("refresh_token", refreshToken); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 删除访问令牌 + * + * @param token 访问令牌 + * @return 成功 + */ + public CommonResult revokeToken(String token) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.DELETE, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + private static void addClientHeader(HttpHeaders headers) { + // client 拼接,需要 BASE64 编码 + String client = CLIENT_ID + ":" + CLIENT_SECRET; + client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", "Basic " + client); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/UserClient.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/UserClient.java new file mode 100644 index 0000000..c0ed36e --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/UserClient.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.ssodemo.client; + +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.user.UserInfoRespDTO; +import com.yunxi.scm.ssodemo.client.dto.user.UserUpdateReqDTO; +import com.yunxi.scm.ssodemo.framework.core.LoginUser; +import com.yunxi.scm.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +/** + * 用户 User 信息的客户端 + * + * 对应调用 OAuth2UserController 接口 + */ +@Component +public class UserClient { + + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; + + // @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 + private final RestTemplate restTemplate = new RestTemplate(); + + public CommonResult getUser() { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/get", + HttpMethod.GET, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + public CommonResult updateUser(UserUpdateReqDTO updateReqDTO) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + // 使用 updateReqDTO 即可 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/update", + HttpMethod.PUT, + new HttpEntity<>(updateReqDTO, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + + private static void addTokenHeader(HttpHeaders headers) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Assert.notNull(loginUser, "登录用户不能为空"); + headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); + } +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/CommonResult.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/CommonResult.java new file mode 100644 index 0000000..5104af0 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/CommonResult.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.ssodemo.client.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + */ + private String msg; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java new file mode 100644 index 0000000..2bb114f --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.ssodemo.client.dto.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 访问令牌 Response DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2AccessTokenRespDTO { + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 刷新令牌 + */ + @JsonProperty("refresh_token") + private String refreshToken; + + /** + * 令牌类型 + */ + @JsonProperty("token_type") + private String tokenType; + + /** + * 过期时间;单位:秒 + */ + @JsonProperty("expires_in") + private Long expiresIn; + + /** + * 授权范围;如果多个授权范围,使用空格分隔 + */ + private String scope; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java new file mode 100644 index 0000000..d41e11f --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.ssodemo.client.dto.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 校验令牌 Response DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2CheckTokenRespDTO { + + /** + * 用户编号 + */ + @JsonProperty("user_id") + private Long userId; + /** + * 用户类型 + */ + @JsonProperty("user_type") + private Integer userType; + /** + * 租户编号 + */ + @JsonProperty("tenant_id") + private Long tenantId; + + /** + * 客户端编号 + */ + @JsonProperty("client_id") + private String clientId; + /** + * 授权范围 + */ + private List scopes; + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 过期时间 + * + * 时间戳 / 1000,即单位:秒 + */ + private Long exp; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserInfoRespDTO.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserInfoRespDTO.java new file mode 100644 index 0000000..06c23b1 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserInfoRespDTO.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 获得用户基本信息 Response dto + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserInfoRespDTO { + + /** + * 用户编号 + */ + private Long id; + + /** + * 用户账号 + */ + private String username; + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + + /** + * 用户头像 + */ + private String avatar; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + + /** + * 部门 + */ + @Data + public static class Dept { + + /** + * 部门编号 + */ + private Long id; + + /** + * 部门名称 + */ + private String name; + + } + + /** + * 岗位 + */ + @Data + public static class Post { + + /** + * 岗位编号 + */ + private Long id; + + /** + * 岗位名称 + */ + private String name; + + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserUpdateReqDTO.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserUpdateReqDTO.java new file mode 100644 index 0000000..6d283cd --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserUpdateReqDTO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 更新用户基本信息 Request DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserUpdateReqDTO { + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/controller/AuthController.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/controller/AuthController.java new file mode 100644 index 0000000..db736a2 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/controller/AuthController.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.ssodemo.controller; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.ssodemo.client.OAuth2Client; +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; +import com.yunxi.scm.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Resource + private OAuth2Client oauth2Client; + + /** + * 使用 code 访问令牌,获得访问令牌 + * + * @param code 授权码 + * @param redirectUri 重定向 URI + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @PostMapping("/login-by-code") + public CommonResult loginByCode(@RequestParam("code") String code, + @RequestParam("redirectUri") String redirectUri) { + return oauth2Client.postAccessToken(code, redirectUri); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @PostMapping("/refresh-token") + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return oauth2Client.refreshToken(refreshToken); + } + + /** + * 退出登录 + * + * @param request 请求 + * @return 成功 + */ + @PostMapping("/logout") + public CommonResult logout(HttpServletRequest request) { + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StrUtil.isNotBlank(token)) { + return oauth2Client.revokeToken(token); + } + // 返回成功 + return new CommonResult<>(); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/controller/UserController.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/controller/UserController.java new file mode 100644 index 0000000..d769726 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/controller/UserController.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.ssodemo.controller; + +import com.yunxi.scm.ssodemo.client.UserClient; +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.user.UserInfoRespDTO; +import com.yunxi.scm.ssodemo.client.dto.user.UserUpdateReqDTO; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/user") +public class UserController { + + @Resource + private UserClient userClient; + + /** + * 获得当前登录用户的基本信息 + * + * @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @GetMapping("/get") + public CommonResult getUser() { + return userClient.getUser(); + } + + /** + * 更新当前登录用户的昵称 + * + * @param nickname 昵称 + * @return 成功 + */ + @PutMapping("/update") + public CommonResult updateUser(@RequestParam("nickname") String nickname) { + UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); + return userClient.updateUser(updateReqDTO); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/config/SecurityConfiguration.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/config/SecurityConfiguration.java new file mode 100644 index 0000000..2004c44 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/config/SecurityConfiguration.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.ssodemo.framework.config; + +import com.yunxi.scm.ssodemo.framework.core.filter.TokenAuthenticationFilter; +import com.yunxi.scm.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.annotation.Resource; + +@Configuration(proxyBeanMethods = false) +@EnableWebSecurity +public class SecurityConfiguration{ + + @Resource + private TokenAuthenticationFilter tokenAuthenticationFilter; + + @Resource + private AccessDeniedHandlerImpl accessDeniedHandler; + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + // 设置 URL 安全权限 + httpSecurity.csrf().disable() // 禁用 CSRF 保护 + .authorizeRequests() + // 1. 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + // 2. 登录相关的接口,可匿名访问 + .antMatchers("/auth/login-by-code").permitAll() + .antMatchers("/auth/refresh-token").permitAll() + .antMatchers("/auth/logout").permitAll() + // last. 兜底规则,必须认证 + .and().authorizeRequests() + .anyRequest().authenticated(); + + // 设置处理器 + httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) + .authenticationEntryPoint(authenticationEntryPoint); + + // 添加 Token Filter + httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return httpSecurity.build(); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/LoginUser.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/LoginUser.java new file mode 100644 index 0000000..d158e35 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/LoginUser.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.ssodemo.framework.core; + +import lombok.Data; + +import java.util.List; + +/** + * 登录用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser { + + /** + * 用户编号 + */ + private Long id; + /** + * 用户类型 + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围 + */ + private List scopes; + + /** + * 访问令牌 + */ + private String accessToken; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/filter/TokenAuthenticationFilter.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 0000000..0f9bc5a --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.ssodemo.framework.core.filter; + +import com.yunxi.scm.ssodemo.client.OAuth2Client; +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import com.yunxi.scm.ssodemo.framework.core.LoginUser; +import com.yunxi.scm.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Resource; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Token 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author 芋道源码 + */ +@Component +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + @Resource + private OAuth2Client oauth2Client; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // 1. 获得访问令牌 + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StringUtils.hasText(token)) { + // 2. 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token); + // 3. 设置当前用户 + if (loginUser != null) { + SecurityUtils.setLoginUser(loginUser, request); + } + } + + // 继续过滤链 + filterChain.doFilter(request, response); + } + + private LoginUser buildLoginUserByToken(String token) { + try { + CommonResult accessTokenResult = oauth2Client.checkToken(token); + OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); + if (accessToken == null) { + return null; + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) + .setAccessToken(accessToken.getAccessToken()); + } catch (Exception exception) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 0000000..cff3728 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.ssodemo.framework.core.handler; + +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.framework.core.util.SecurityUtils; +import com.yunxi.scm.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Component +@SuppressWarnings("JavadocReference") +@Slf4j +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityUtils.getLoginUserId(), e); + // 返回 403 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.FORBIDDEN.value()); + result.setMsg("没有该操作权限"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..e774cc6 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.ssodemo.framework.core.handler; + +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + */ +@Component +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.UNAUTHORIZED.value()); + result.setMsg("账号未登录"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/SecurityUtils.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/SecurityUtils.java new file mode 100644 index 0000000..10d593b --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/SecurityUtils.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.ssodemo.framework.core.util; + +import com.yunxi.scm.ssodemo.framework.core.LoginUser; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityUtils { + + public static final String AUTHORIZATION_BEARER = "Bearer"; + + private SecurityUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param header 认证 Token 对应的 Header 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, String header) { + String authorization = request.getHeader(header); + if (!StringUtils.hasText(authorization)) { + return null; + } + int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); + if (index == -1) { // 未找到 + return null; + } + return authorization.substring(index + 7).trim(); + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 Authentication,并设置到上下文 + Authentication authentication = buildAuthentication(loginUser, request); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + return authenticationToken; + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/ServletUtils.java b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/ServletUtils.java new file mode 100644 index 0000000..bda4b70 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/ServletUtils.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.ssodemo.framework.core.util; + +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.json.JSONUtil; +import org.springframework.http.MediaType; + +import javax.servlet.http.HttpServletResponse; + +/** + * 客户端工具类 + * + * @author 芋道源码 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JSONUtil.toJsonStr(object); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + public static void write(HttpServletResponse response, String text, String contentType) { + ServletUtil.write(response, text, contentType); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/application.yaml b/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/application.yaml new file mode 100644 index 0000000..a62cf97 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 18080 diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/static/callback.html b/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/static/callback.html new file mode 100644 index 0000000..123a1af --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/static/callback.html @@ -0,0 +1,61 @@ + + + + + SSO 授权后的回调页 + + + + + + + + +正在使用 code 授权码,进行 accessToken 访问令牌的获取 + + diff --git a/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/static/index.html b/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/static/index.html new file mode 100644 index 0000000..866f6ee --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-code/src/main/resources/static/index.html @@ -0,0 +1,159 @@ + + + + + 首页 + + + + + + + + + + + + + + diff --git a/yunxi-example/yunxi-sso-demo-by-password/pom.xml b/yunxi-example/yunxi-sso-demo-by-password/pom.xml new file mode 100644 index 0000000..99e22f0 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + + com.yunxi.scm + yunxi-sso-demo-by-password + 1.0.0-snapshot + jar + + ${project.artifactId} + 基于密码模式,如何实现 SSO 单点登录? + https://github.com/YunaiV/ruoyi-vue-pro + + + + 8 + 8 + UTF-8 + + 2.7.13 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + cn.hutool + hutool-all + 5.8.20 + + + + org.projectlombok + lombok + true + + + + diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/SSODemoApplication.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/SSODemoApplication.java new file mode 100644 index 0000000..c1dce1b --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/SSODemoApplication.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.ssodemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SSODemoApplication { + + public static void main(String[] args) { + SpringApplication.run(SSODemoApplication.class, args); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/OAuth2Client.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/OAuth2Client.java new file mode 100644 index 0000000..2d4ddab --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/OAuth2Client.java @@ -0,0 +1,127 @@ +package com.yunxi.scm.ssodemo.client; + +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; +import com.yunxi.scm.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.Base64Utils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.nio.charset.StandardCharsets; + +/** + * OAuth 2.0 客户端 + * + * 对应调用 OAuth2OpenController 接口 + */ +@Component +public class OAuth2Client { + + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api/system/oauth2"; + + /** + * 租户编号 + * + * 默认使用 1;如果使用别的租户,可以调整 + */ + public static final Long TENANT_ID = 1L; + + private static final String CLIENT_ID = "yunxi-sso-demo-by-password"; + private static final String CLIENT_SECRET = "test"; + + +// @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 + private final RestTemplate restTemplate = new RestTemplate(); + + /** + * 校验访问令牌,并返回它的基本信息 + * + * @param token 访问令牌 + * @return 访问令牌的基本信息 + */ + public CommonResult checkToken(String token) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/check-token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌 + */ + public CommonResult refreshToken(String refreshToken) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", "refresh_token"); + body.add("refresh_token", refreshToken); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.POST, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + /** + * 删除访问令牌 + * + * @param token 访问令牌 + * @return 成功 + */ + public CommonResult revokeToken(String token) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", TENANT_ID.toString()); + addClientHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("token", token); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/token", + HttpMethod.DELETE, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + private static void addClientHeader(HttpHeaders headers) { + // client 拼接,需要 BASE64 编码 + String client = CLIENT_ID + ":" + CLIENT_SECRET; + client = Base64Utils.encodeToString(client.getBytes(StandardCharsets.UTF_8)); + headers.add("Authorization", "Basic " + client); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/UserClient.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/UserClient.java new file mode 100644 index 0000000..c0ed36e --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/UserClient.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.ssodemo.client; + +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.user.UserInfoRespDTO; +import com.yunxi.scm.ssodemo.client.dto.user.UserUpdateReqDTO; +import com.yunxi.scm.ssodemo.framework.core.LoginUser; +import com.yunxi.scm.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +/** + * 用户 User 信息的客户端 + * + * 对应调用 OAuth2UserController 接口 + */ +@Component +public class UserClient { + + private static final String BASE_URL = "http://127.0.0.1:48080/admin-api//system/oauth2/user"; + + // @Resource // 可优化,注册一个 RestTemplate Bean,然后注入 + private final RestTemplate restTemplate = new RestTemplate(); + + public CommonResult getUser() { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + MultiValueMap body = new LinkedMultiValueMap<>(); + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/get", + HttpMethod.GET, + new HttpEntity<>(body, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + public CommonResult updateUser(UserUpdateReqDTO updateReqDTO) { + // 1.1 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("tenant-id", OAuth2Client.TENANT_ID.toString()); + addTokenHeader(headers); + // 1.2 构建请求参数 + // 使用 updateReqDTO 即可 + + // 2. 执行请求 + ResponseEntity> exchange = restTemplate.exchange( + BASE_URL + "/update", + HttpMethod.PUT, + new HttpEntity<>(updateReqDTO, headers), + new ParameterizedTypeReference>() {}); // 解决 CommonResult 的泛型丢失 + Assert.isTrue(exchange.getStatusCode().is2xxSuccessful(), "响应必须是 200 成功"); + return exchange.getBody(); + } + + + private static void addTokenHeader(HttpHeaders headers) { + LoginUser loginUser = SecurityUtils.getLoginUser(); + Assert.notNull(loginUser, "登录用户不能为空"); + headers.add("Authorization", "Bearer " + loginUser.getAccessToken()); + } +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/CommonResult.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/CommonResult.java new file mode 100644 index 0000000..5104af0 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/CommonResult.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.ssodemo.client.dto; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + */ + private String msg; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java new file mode 100644 index 0000000..2bb114f --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.ssodemo.client.dto.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 访问令牌 Response DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2AccessTokenRespDTO { + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 刷新令牌 + */ + @JsonProperty("refresh_token") + private String refreshToken; + + /** + * 令牌类型 + */ + @JsonProperty("token_type") + private String tokenType; + + /** + * 过期时间;单位:秒 + */ + @JsonProperty("expires_in") + private Long expiresIn; + + /** + * 授权范围;如果多个授权范围,使用空格分隔 + */ + private String scope; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java new file mode 100644 index 0000000..d41e11f --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/oauth2/OAuth2CheckTokenRespDTO.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.ssodemo.client.dto.oauth2; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 校验令牌 Response DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2CheckTokenRespDTO { + + /** + * 用户编号 + */ + @JsonProperty("user_id") + private Long userId; + /** + * 用户类型 + */ + @JsonProperty("user_type") + private Integer userType; + /** + * 租户编号 + */ + @JsonProperty("tenant_id") + private Long tenantId; + + /** + * 客户端编号 + */ + @JsonProperty("client_id") + private String clientId; + /** + * 授权范围 + */ + private List scopes; + + /** + * 访问令牌 + */ + @JsonProperty("access_token") + private String accessToken; + + /** + * 过期时间 + * + * 时间戳 / 1000,即单位:秒 + */ + private Long exp; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserInfoRespDTO.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserInfoRespDTO.java new file mode 100644 index 0000000..06c23b1 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserInfoRespDTO.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 获得用户基本信息 Response dto + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserInfoRespDTO { + + /** + * 用户编号 + */ + private Long id; + + /** + * 用户账号 + */ + private String username; + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + + /** + * 用户头像 + */ + private String avatar; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + + /** + * 部门 + */ + @Data + public static class Dept { + + /** + * 部门编号 + */ + private Long id; + + /** + * 部门名称 + */ + private String name; + + } + + /** + * 岗位 + */ + @Data + public static class Post { + + /** + * 岗位编号 + */ + private Long id; + + /** + * 岗位名称 + */ + private String name; + + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserUpdateReqDTO.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserUpdateReqDTO.java new file mode 100644 index 0000000..6d283cd --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/client/dto/user/UserUpdateReqDTO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.ssodemo.client.dto.user; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 更新用户基本信息 Request DTO + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserUpdateReqDTO { + + /** + * 用户昵称 + */ + private String nickname; + + /** + * 用户邮箱 + */ + private String email; + + /** + * 手机号码 + */ + private String mobile; + + /** + * 用户性别 + */ + private Integer sex; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/controller/AuthController.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/controller/AuthController.java new file mode 100644 index 0000000..6ca02fa --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/controller/AuthController.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.ssodemo.controller; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.ssodemo.client.OAuth2Client; +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.oauth2.OAuth2AccessTokenRespDTO; +import com.yunxi.scm.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Resource + private OAuth2Client oauth2Client; + + /** + * 使用刷新令牌,获得(刷新)访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 访问令牌;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @PostMapping("/refresh-token") + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return oauth2Client.refreshToken(refreshToken); + } + + /** + * 退出登录 + * + * @param request 请求 + * @return 成功 + */ + @PostMapping("/logout") + public CommonResult logout(HttpServletRequest request) { + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StrUtil.isNotBlank(token)) { + return oauth2Client.revokeToken(token); + } + // 返回成功 + return new CommonResult<>(); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/controller/UserController.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/controller/UserController.java new file mode 100644 index 0000000..d769726 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/controller/UserController.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.ssodemo.controller; + +import com.yunxi.scm.ssodemo.client.UserClient; +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.user.UserInfoRespDTO; +import com.yunxi.scm.ssodemo.client.dto.user.UserUpdateReqDTO; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +@RestController +@RequestMapping("/user") +public class UserController { + + @Resource + private UserClient userClient; + + /** + * 获得当前登录用户的基本信息 + * + * @return 用户信息;注意,实际项目中,最好创建对应的 ResponseVO 类,只返回必要的字段 + */ + @GetMapping("/get") + public CommonResult getUser() { + return userClient.getUser(); + } + + /** + * 更新当前登录用户的昵称 + * + * @param nickname 昵称 + * @return 成功 + */ + @PutMapping("/update") + public CommonResult updateUser(@RequestParam("nickname") String nickname) { + UserUpdateReqDTO updateReqDTO = new UserUpdateReqDTO(nickname, null, null, null); + return userClient.updateUser(updateReqDTO); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/config/SecurityConfiguration.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/config/SecurityConfiguration.java new file mode 100644 index 0000000..8d24761 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/config/SecurityConfiguration.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.ssodemo.framework.config; + +import com.yunxi.scm.ssodemo.framework.core.filter.TokenAuthenticationFilter; +import com.yunxi.scm.ssodemo.framework.core.handler.AccessDeniedHandlerImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +import javax.annotation.Resource; + +@Configuration(proxyBeanMethods = false) +@EnableWebSecurity +public class SecurityConfiguration { + + @Resource + private TokenAuthenticationFilter tokenAuthenticationFilter; + + @Resource + private AccessDeniedHandlerImpl accessDeniedHandler; + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; + + @Bean + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + // 设置 URL 安全权限 + httpSecurity.csrf().disable() // 禁用 CSRF 保护 + .authorizeRequests() + // 1. 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + // 2. 登录相关的接口,可匿名访问 + .antMatchers("/auth/login-by-code").permitAll() + .antMatchers("/auth/refresh-token").permitAll() + .antMatchers("/auth/logout").permitAll() + // last. 兜底规则,必须认证 + .and().authorizeRequests() + .anyRequest().authenticated(); + + // 设置处理器 + httpSecurity.exceptionHandling().accessDeniedHandler(accessDeniedHandler) + .authenticationEntryPoint(authenticationEntryPoint); + + // 添加 Token Filter + httpSecurity.addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + return httpSecurity.build(); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/LoginUser.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/LoginUser.java new file mode 100644 index 0000000..d158e35 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/LoginUser.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.ssodemo.framework.core; + +import lombok.Data; + +import java.util.List; + +/** + * 登录用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser { + + /** + * 用户编号 + */ + private Long id; + /** + * 用户类型 + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围 + */ + private List scopes; + + /** + * 访问令牌 + */ + private String accessToken; + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/filter/TokenAuthenticationFilter.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 0000000..0f9bc5a --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.ssodemo.framework.core.filter; + +import com.yunxi.scm.ssodemo.client.OAuth2Client; +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.client.dto.oauth2.OAuth2CheckTokenRespDTO; +import com.yunxi.scm.ssodemo.framework.core.LoginUser; +import com.yunxi.scm.ssodemo.framework.core.util.SecurityUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.annotation.Resource; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Token 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author 芋道源码 + */ +@Component +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + @Resource + private OAuth2Client oauth2Client; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain filterChain) throws ServletException, IOException { + // 1. 获得访问令牌 + String token = SecurityUtils.obtainAuthorization(request, "Authorization"); + if (StringUtils.hasText(token)) { + // 2. 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token); + // 3. 设置当前用户 + if (loginUser != null) { + SecurityUtils.setLoginUser(loginUser, request); + } + } + + // 继续过滤链 + filterChain.doFilter(request, response); + } + + private LoginUser buildLoginUserByToken(String token) { + try { + CommonResult accessTokenResult = oauth2Client.checkToken(token); + OAuth2CheckTokenRespDTO accessToken = accessTokenResult.getData(); + if (accessToken == null) { + return null; + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()) + .setAccessToken(accessToken.getAccessToken()); + } catch (Exception exception) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 0000000..cff3728 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.ssodemo.framework.core.handler; + +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.framework.core.util.SecurityUtils; +import com.yunxi.scm.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Component +@SuppressWarnings("JavadocReference") +@Slf4j +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityUtils.getLoginUserId(), e); + // 返回 403 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.FORBIDDEN.value()); + result.setMsg("没有该操作权限"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..e774cc6 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.ssodemo.framework.core.handler; + +import com.yunxi.scm.ssodemo.client.dto.CommonResult; +import com.yunxi.scm.ssodemo.framework.core.util.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + */ +@Component +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + CommonResult result = new CommonResult<>(); + result.setCode(HttpStatus.UNAUTHORIZED.value()); + result.setMsg("账号未登录"); + ServletUtils.writeJSON(response, result); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/SecurityUtils.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/SecurityUtils.java new file mode 100644 index 0000000..10d593b --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/SecurityUtils.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.ssodemo.framework.core.util; + +import com.yunxi.scm.ssodemo.framework.core.LoginUser; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityUtils { + + public static final String AUTHORIZATION_BEARER = "Bearer"; + + private SecurityUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param header 认证 Token 对应的 Header 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, String header) { + String authorization = request.getHeader(header); + if (!StringUtils.hasText(authorization)) { + return null; + } + int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); + if (index == -1) { // 未找到 + return null; + } + return authorization.substring(index + 7).trim(); + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 Authentication,并设置到上下文 + Authentication authentication = buildAuthentication(loginUser, request); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + return authenticationToken; + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/ServletUtils.java b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/ServletUtils.java new file mode 100644 index 0000000..bda4b70 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/java/com/yunxi/scm/ssodemo/framework/core/util/ServletUtils.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.ssodemo.framework.core.util; + +import cn.hutool.extra.servlet.ServletUtil; +import cn.hutool.json.JSONUtil; +import org.springframework.http.MediaType; + +import javax.servlet.http.HttpServletResponse; + +/** + * 客户端工具类 + * + * @author 芋道源码 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JSONUtil.toJsonStr(object); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + public static void write(HttpServletResponse response, String text, String contentType) { + ServletUtil.write(response, text, contentType); + } + +} diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/application.yaml b/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/application.yaml new file mode 100644 index 0000000..a62cf97 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/application.yaml @@ -0,0 +1,2 @@ +server: + port: 18080 diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/static/index.html b/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/static/index.html new file mode 100644 index 0000000..8a53729 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/static/index.html @@ -0,0 +1,154 @@ + + + + + 首页 + + + + + + + + + + + + + + diff --git a/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/static/login.html b/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/static/login.html new file mode 100644 index 0000000..a892b77 --- /dev/null +++ b/yunxi-example/yunxi-sso-demo-by-password/src/main/resources/static/login.html @@ -0,0 +1,74 @@ + + + + + 登录 + + + + + + +账号:
+密码:
+ + + + diff --git a/yunxi-framework/pom.xml b/yunxi-framework/pom.xml new file mode 100644 index 0000000..d9d0df9 --- /dev/null +++ b/yunxi-framework/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + yunxi + com.yunxi.scm + ${revision} + + pom + + yunxi-common + yunxi-spring-boot-starter-banner + yunxi-spring-boot-starter-mybatis + yunxi-spring-boot-starter-redis + yunxi-spring-boot-starter-web + yunxi-spring-boot-starter-security + + yunxi-spring-boot-starter-file + yunxi-spring-boot-starter-monitor + yunxi-spring-boot-starter-protection + yunxi-spring-boot-starter-job + yunxi-spring-boot-starter-mq + + yunxi-spring-boot-starter-excel + yunxi-spring-boot-starter-test + + yunxi-spring-boot-starter-biz-operatelog + yunxi-spring-boot-starter-biz-dict + yunxi-spring-boot-starter-biz-sms + + yunxi-spring-boot-starter-biz-pay + yunxi-spring-boot-starter-biz-weixin + yunxi-spring-boot-starter-biz-social + yunxi-spring-boot-starter-biz-tenant + yunxi-spring-boot-starter-biz-data-permission + yunxi-spring-boot-starter-biz-error-code + yunxi-spring-boot-starter-biz-ip + + yunxi-spring-boot-starter-flowable + yunxi-spring-boot-starter-captcha + yunxi-spring-boot-starter-websocket + yunxi-spring-boot-starter-desensitize + + + yunxi-framework + + 该包是技术组件,每个子包,代表一个组件。每个组件包括两部分: + 1. core 包:是该组件的核心封装 + 2. config 包:是该组件基于 Spring 的配置 + + 技术组件,也分成两类: + 1. 框架组件:和我们熟悉的 MyBatis、Redis 等等的拓展 + 2. 业务组件:和业务相关的组件的封装,例如说数据字典、操作日志等等。 + 如果是业务组件,Maven 名字会包含 biz + + https://github.com/YunaiV/ruoyi-vue-pro + + diff --git a/yunxi-framework/yunxi-common/pom.xml b/yunxi-framework/yunxi-common/pom.xml new file mode 100644 index 0000000..6cb0857 --- /dev/null +++ b/yunxi-framework/yunxi-common/pom.xml @@ -0,0 +1,138 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-common + jar + + ${project.artifactId} + 定义基础 pojo 类、枚举、工具类等等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + org.springframework + spring-core + provided + + + org.springframework + spring-expression + provided + + + org.springframework + spring-aop + provided + + + org.aspectj + aspectjweaver + provided + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + org.springdoc + springdoc-openapi-ui + provided + + + + + org.apache.skywalking + apm-toolkit-trace + + + + + org.projectlombok + lombok + + + + org.mapstruct + mapstruct + + + org.mapstruct + mapstruct-jdk8 + + + org.mapstruct + mapstruct-processor + + + + com.google.guava + guava + provided + + + + com.fasterxml.jackson.core + jackson-databind + provided + + + com.fasterxml.jackson.core + jackson-core + provided + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + provided + + + + org.slf4j + slf4j-api + provided + + + + jakarta.validation + jakarta.validation-api + provided + + + + cn.hutool + hutool-all + + + + com.alibaba + transmittable-thread-local + + + + + diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/core/IntArrayValuable.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/core/IntArrayValuable.java new file mode 100644 index 0000000..ef27570 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/core/IntArrayValuable.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.framework.common.core; + +/** + * 可生成 Int 数组的接口 + * + * @author 芋道源码 + */ +public interface IntArrayValuable { + + /** + * @return int 数组 + */ + int[] array(); + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/core/KeyValue.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/core/KeyValue.java new file mode 100644 index 0000000..1d26ee4 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/core/KeyValue.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.framework.common.core; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Key Value 的键值对 + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class KeyValue { + + private K key; + private V value; + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/CommonStatusEnum.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/CommonStatusEnum.java new file mode 100644 index 0000000..ae2c2c9 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/CommonStatusEnum.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.framework.common.enums; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 通用状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum CommonStatusEnum implements IntArrayValuable { + + ENABLE(0, "开启"), + DISABLE(1, "关闭"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CommonStatusEnum::getStatus).toArray(); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/DocumentEnum.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/DocumentEnum.java new file mode 100644 index 0000000..78aeee8 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/DocumentEnum.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.common.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 文档地址 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DocumentEnum { + + REDIS_INSTALL("https://gitee.com/zhijiantianya/ruoyi-vue-pro/issues/I4VCSJ", "Redis 安装文档"), + TENANT("https://doc.iocoder.cn", "SaaS 多租户文档"); + + private final String url; + private final String memo; + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/TerminalEnum.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/TerminalEnum.java new file mode 100644 index 0000000..af1581f --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/TerminalEnum.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.common.enums; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 终端的枚举 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum TerminalEnum implements IntArrayValuable { + + WECHAT_MINI_PROGRAM(10, "微信小程序"), + WECHAT_WAP(11, "微信公众号"), + H5(20, "H5 网页"), + IOS(31, "苹果 App"), + ANDROID(32, "安卓 App"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TerminalEnum::getTerminal).toArray(); + + /** + * 终端 + */ + private final Integer terminal; + /** + * 终端名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/UserTypeEnum.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/UserTypeEnum.java new file mode 100644 index 0000000..7316968 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/UserTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.framework.common.enums; + +import cn.hutool.core.util.ArrayUtil; +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 全局用户类型枚举 + */ +@AllArgsConstructor +@Getter +public enum UserTypeEnum implements IntArrayValuable { + + MEMBER(1, "会员"), // 面向 c 端,普通用户 + ADMIN(2, "管理员"); // 面向 b 端,管理后台 + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(UserTypeEnum::getValue).toArray(); + + /** + * 类型 + */ + private final Integer value; + /** + * 类型名 + */ + private final String name; + + public static UserTypeEnum valueOf(Integer value) { + return ArrayUtil.firstMatch(userType -> userType.getValue().equals(value), UserTypeEnum.values()); + } + + @Override + public int[] array() { + return ARRAYS; + } +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/WebFilterOrderEnum.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/WebFilterOrderEnum.java new file mode 100644 index 0000000..5b87930 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/enums/WebFilterOrderEnum.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.framework.common.enums; + +/** + * Web 过滤器顺序的枚举类,保证过滤器按照符合我们的预期 + * + * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 enums 包下 + * + * @author 芋道源码 + */ +public interface WebFilterOrderEnum { + + int CORS_FILTER = Integer.MIN_VALUE; + + int TRACE_FILTER = CORS_FILTER + 1; + + int REQUEST_BODY_CACHE_FILTER = Integer.MIN_VALUE + 500; + + // OrderedRequestContextFilter 默认为 -105,用于国际化上下文等等 + + int TENANT_CONTEXT_FILTER = - 104; // 需要保证在 ApiAccessLogFilter 前面 + + int API_ACCESS_LOG_FILTER = -103; // 需要保证在 RequestBodyCacheFilter 后面 + + int XSS_FILTER = -102; // 需要保证在 RequestBodyCacheFilter 后面 + + // Spring Security Filter 默认为 -100,可见 org.springframework.boot.autoconfigure.security.SecurityProperties 配置属性类 + + int TENANT_SECURITY_FILTER = -99; // 需要保证在 Spring Security 过滤器后面 + + int FLOWABLE_FILTER = -98; // 需要保证在 Spring Security 过滤后面 + + int DEMO_FILTER = Integer.MAX_VALUE; + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ErrorCode.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ErrorCode.java new file mode 100644 index 0000000..82a0d56 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ErrorCode.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.framework.common.exception; + +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.exception.enums.ServiceErrorCodeRange; +import lombok.Data; + +/** + * 错误码对象 + * + * 全局错误码,占用 [0, 999], 参见 {@link GlobalErrorCodeConstants} + * 业务异常错误码,占用 [1 000 000 000, +∞),参见 {@link ServiceErrorCodeRange} + * + * TODO 错误码设计成对象的原因,为未来的 i18 国际化做准备 + */ +@Data +public class ErrorCode { + + /** + * 错误码 + */ + private final Integer code; + /** + * 错误提示 + */ + private final String msg; + + public ErrorCode(Integer code, String message) { + this.code = code; + this.msg = message; + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ServerException.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ServerException.java new file mode 100644 index 0000000..f74b037 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ServerException.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.framework.common.exception; + +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 服务器异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServerException extends RuntimeException { + + /** + * 全局错误码 + * + * @see GlobalErrorCodeConstants + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServerException() { + } + + public ServerException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServerException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServerException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServerException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ServiceException.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ServiceException.java new file mode 100644 index 0000000..33f5344 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/ServiceException.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.framework.common.exception; + +import com.yunxi.scm.framework.common.exception.enums.ServiceErrorCodeRange; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 业务逻辑异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public final class ServiceException extends RuntimeException { + + /** + * 业务错误码 + * + * @see ServiceErrorCodeRange + */ + private Integer code; + /** + * 错误提示 + */ + private String message; + + /** + * 空构造方法,避免反序列化问题 + */ + public ServiceException() { + } + + public ServiceException(ErrorCode errorCode) { + this.code = errorCode.getCode(); + this.message = errorCode.getMsg(); + } + + public ServiceException(Integer code, String message) { + this.code = code; + this.message = message; + } + + public Integer getCode() { + return code; + } + + public ServiceException setCode(Integer code) { + this.code = code; + return this; + } + + @Override + public String getMessage() { + return message; + } + + public ServiceException setMessage(String message) { + this.message = message; + return this; + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/enums/GlobalErrorCodeConstants.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/enums/GlobalErrorCodeConstants.java new file mode 100644 index 0000000..691fd0e --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/enums/GlobalErrorCodeConstants.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.common.exception.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * 全局错误码枚举 + * 0-999 系统异常编码保留 + * + * 一般情况下,使用 HTTP 响应状态码 https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status + * 虽然说,HTTP 响应状态码作为业务使用表达能力偏弱,但是使用在系统层面还是非常不错的 + * 比较特殊的是,因为之前一直使用 0 作为成功,就不使用 200 啦。 + * + * @author 芋道源码 + */ +public interface GlobalErrorCodeConstants { + + ErrorCode SUCCESS = new ErrorCode(0, "成功"); + + // ========== 客户端错误段 ========== + + ErrorCode BAD_REQUEST = new ErrorCode(400, "请求参数不正确"); + ErrorCode UNAUTHORIZED = new ErrorCode(401, "账号未登录"); + ErrorCode FORBIDDEN = new ErrorCode(403, "没有该操作权限"); + ErrorCode NOT_FOUND = new ErrorCode(404, "请求未找到"); + ErrorCode METHOD_NOT_ALLOWED = new ErrorCode(405, "请求方法不正确"); + ErrorCode LOCKED = new ErrorCode(423, "请求失败,请稍后重试"); // 并发请求,不允许 + ErrorCode TOO_MANY_REQUESTS = new ErrorCode(429, "请求过于频繁,请稍后重试"); + + // ========== 服务端错误段 ========== + + ErrorCode INTERNAL_SERVER_ERROR = new ErrorCode(500, "系统异常"); + ErrorCode NOT_IMPLEMENTED = new ErrorCode(501, "功能未实现/未开启"); + + // ========== 自定义错误段 ========== + ErrorCode REPEATED_REQUESTS = new ErrorCode(900, "重复请求,请稍后重试"); // 重复请求 + ErrorCode DEMO_DENY = new ErrorCode(901, "演示模式,禁止写操作"); + + ErrorCode UNKNOWN = new ErrorCode(999, "未知错误"); + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/enums/ServiceErrorCodeRange.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/enums/ServiceErrorCodeRange.java new file mode 100644 index 0000000..e5fa55f --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/enums/ServiceErrorCodeRange.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.common.exception.enums; + +/** + * 业务异常的错误码区间,解决:解决各模块错误码定义,避免重复,在此只声明不做实际使用 + * + * 一共 10 位,分成四段 + * + * 第一段,1 位,类型 + * 1 - 业务级别异常 + * x - 预留 + * 第二段,3 位,系统类型 + * 001 - 用户系统 + * 002 - 商品系统 + * 003 - 订单系统 + * 004 - 支付系统 + * 005 - 优惠劵系统 + * ... - ... + * 第三段,3 位,模块 + * 不限制规则。 + * 一般建议,每个系统里面,可能有多个模块,可以再去做分段。以用户系统为例子: + * 001 - OAuth2 模块 + * 002 - User 模块 + * 003 - MobileCode 模块 + * 第四段,3 位,错误码 + * 不限制规则。 + * 一般建议,每个模块自增。 + * + * @author 芋道源码 + */ +public class ServiceErrorCodeRange { + + // 模块 infra 错误码区间 [1-001-000-000 ~ 1-002-000-000) + // 模块 system 错误码区间 [1-002-000-000 ~ 1-003-000-000) + // 模块 report 错误码区间 [1-003-000-000 ~ 1-004-000-000) + // 模块 member 错误码区间 [1-004-000-000 ~ 1-005-000-000) + // 模块 mp 错误码区间 [1-006-000-000 ~ 1-007-000-000) + // 模块 pay 错误码区间 [1-007-000-000 ~ 1-008-000-000) + // 模块 product 错误码区间 [1-008-000-000 ~ 1-009-000-000) + // 模块 bpm 错误码区间 [1-009-000-000 ~ 1-010-000-000) + // 模块 trade 错误码区间 [1-011-000-000 ~ 1-012-000-000) + // 模块 promotion 错误码区间 [1-013-000-000 ~ 1-014-000-000) + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/util/ServiceExceptionUtil.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/util/ServiceExceptionUtil.java new file mode 100644 index 0000000..a0b688d --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/exception/util/ServiceExceptionUtil.java @@ -0,0 +1,127 @@ +package com.yunxi.scm.framework.common.exception.util; + +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * {@link ServiceException} 工具类 + * + * 目的在于,格式化异常信息提示。 + * 考虑到 String.format 在参数不正确时会报错,因此使用 {} 作为占位符,并使用 {@link #doFormat(int, String, Object...)} 方法来格式化 + * + * 因为 {@link #MESSAGES} 里面默认是没有异常信息提示的模板的,所以需要使用方自己初始化进去。目前想到的有几种方式: + * + * 1. 异常提示信息,写在枚举类中,例如说,cn.iocoder.oceans.user.api.constants.ErrorCodeEnum 类 + ServiceExceptionConfiguration + * 2. 异常提示信息,写在 .properties 等等配置文件 + * 3. 异常提示信息,写在 Apollo 等等配置中心中,从而实现可动态刷新 + * 4. 异常提示信息,存储在 db 等等数据库中,从而实现可动态刷新 + */ +@Slf4j +public class ServiceExceptionUtil { + + /** + * 错误码提示模板 + */ + private static final ConcurrentMap MESSAGES = new ConcurrentHashMap<>(); + + public static void putAll(Map messages) { + ServiceExceptionUtil.MESSAGES.putAll(messages); + } + + public static void put(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.put(code, message); + } + + public static void delete(Integer code, String message) { + ServiceExceptionUtil.MESSAGES.remove(code, message); + } + + // ========== 和 ServiceException 的集成 ========== + + public static ServiceException exception(ErrorCode errorCode) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern); + } + + public static ServiceException exception(ErrorCode errorCode, Object... params) { + String messagePattern = MESSAGES.getOrDefault(errorCode.getCode(), errorCode.getMsg()); + return exception0(errorCode.getCode(), messagePattern, params); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @return 异常 + */ + public static ServiceException exception(Integer code) { + return exception0(code, MESSAGES.get(code)); + } + + /** + * 创建指定编号的 ServiceException 的异常 + * + * @param code 编号 + * @param params 消息提示的占位符对应的参数 + * @return 异常 + */ + public static ServiceException exception(Integer code, Object... params) { + return exception0(code, MESSAGES.get(code), params); + } + + public static ServiceException exception0(Integer code, String messagePattern, Object... params) { + String message = doFormat(code, messagePattern, params); + return new ServiceException(code, message); + } + + public static ServiceException invalidParamException(String messagePattern, Object... params) { + return exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), messagePattern, params); + } + + // ========== 格式化方法 ========== + + /** + * 将错误编号对应的消息使用 params 进行格式化。 + * + * @param code 错误编号 + * @param messagePattern 消息模版 + * @param params 参数 + * @return 格式化后的提示 + */ + @VisibleForTesting + public static String doFormat(int code, String messagePattern, Object... params) { + StringBuilder sbuf = new StringBuilder(messagePattern.length() + 50); + int i = 0; + int j; + int l; + for (l = 0; l < params.length; l++) { + j = messagePattern.indexOf("{}", i); + if (j == -1) { + log.error("[doFormat][参数过多:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + if (i == 0) { + return messagePattern; + } else { + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + } else { + sbuf.append(messagePattern, i, j); + sbuf.append(params[l]); + i = j + 2; + } + } + if (messagePattern.indexOf("{}", i) != -1) { + log.error("[doFormat][参数过少:错误码({})|错误内容({})|参数({})", code, messagePattern, params); + } + sbuf.append(messagePattern.substring(i)); + return sbuf.toString(); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/package-info.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/package-info.java new file mode 100644 index 0000000..b504eca --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/package-info.java @@ -0,0 +1,6 @@ +/** + * 基础的通用类,和框架无关 + * + * 例如说,CommonResult 为通用返回 + */ +package com.yunxi.scm.framework.common; diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/CommonResult.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/CommonResult.java new file mode 100644 index 0000000..605a3a6 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/CommonResult.java @@ -0,0 +1,112 @@ +package com.yunxi.scm.framework.common.pojo; + +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.springframework.util.Assert; + +import java.io.Serializable; +import java.util.Objects; + +/** + * 通用返回 + * + * @param 数据泛型 + */ +@Data +public class CommonResult implements Serializable { + + /** + * 错误码 + * + * @see ErrorCode#getCode() + */ + private Integer code; + /** + * 返回数据 + */ + private T data; + /** + * 错误提示,用户可阅读 + * + * @see ErrorCode#getMsg() () + */ + private String msg; + + /** + * 将传入的 result 对象,转换成另外一个泛型结果的对象 + * + * 因为 A 方法返回的 CommonResult 对象,不满足调用其的 B 方法的返回,所以需要进行转换。 + * + * @param result 传入的 result 对象 + * @param 返回的泛型 + * @return 新的 CommonResult 对象 + */ + public static CommonResult error(CommonResult result) { + return error(result.getCode(), result.getMsg()); + } + + public static CommonResult error(Integer code, String message) { + Assert.isTrue(!GlobalErrorCodeConstants.SUCCESS.getCode().equals(code), "code 必须是错误的!"); + CommonResult result = new CommonResult<>(); + result.code = code; + result.msg = message; + return result; + } + + public static CommonResult error(ErrorCode errorCode) { + return error(errorCode.getCode(), errorCode.getMsg()); + } + + public static CommonResult success(T data) { + CommonResult result = new CommonResult<>(); + result.code = GlobalErrorCodeConstants.SUCCESS.getCode(); + result.data = data; + result.msg = ""; + return result; + } + + public static boolean isSuccess(Integer code) { + return Objects.equals(code, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isSuccess() { + return isSuccess(code); + } + + @JsonIgnore // 避免 jackson 序列化 + public boolean isError() { + return !isSuccess(); + } + + // ========= 和 Exception 异常体系集成 ========= + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + */ + public void checkError() throws ServiceException { + if (isSuccess()) { + return; + } + // 业务异常 + throw new ServiceException(code, msg); + } + + /** + * 判断是否有异常。如果有,则抛出 {@link ServiceException} 异常 + * 如果没有,则返回 {@link #data} 数据 + */ + @JsonIgnore // 避免 jackson 序列化 + public T getCheckedData() { + checkError(); + return data; + } + + public static CommonResult error(ServiceException serviceException) { + return error(serviceException.getCode(), serviceException.getMessage()); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/PageParam.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/PageParam.java new file mode 100644 index 0000000..1020eff --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/PageParam.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.framework.common.pojo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.Max; +import javax.validation.constraints.NotNull; +import java.io.Serializable; + +@Schema(description="分页参数") +@Data +public class PageParam implements Serializable { + + private static final Integer PAGE_NO = 1; + private static final Integer PAGE_SIZE = 10; + + @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1") + @NotNull(message = "页码不能为空") + @Min(value = 1, message = "页码最小值为 1") + private Integer pageNo = PAGE_NO; + + @Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "每页条数不能为空") + @Min(value = 1, message = "每页条数最小值为 1") + @Max(value = 100, message = "每页条数最大值为 100") + private Integer pageSize = PAGE_SIZE; + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/PageResult.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/PageResult.java new file mode 100644 index 0000000..748ef9f --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/PageResult.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.framework.common.pojo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +@Schema(description = "分页结果") +@Data +public final class PageResult implements Serializable { + + @Schema(description = "数据", requiredMode = Schema.RequiredMode.REQUIRED) + private List list; + + @Schema(description = "总量", requiredMode = Schema.RequiredMode.REQUIRED) + private Long total; + + public PageResult() { + } + + public PageResult(List list, Long total) { + this.list = list; + this.total = total; + } + + public PageResult(Long total) { + this.list = new ArrayList<>(); + this.total = total; + } + + public static PageResult empty() { + return new PageResult<>(0L); + } + + public static PageResult empty(Long total) { + return new PageResult<>(total); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/SortingField.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/SortingField.java new file mode 100644 index 0000000..a9eaf13 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/pojo/SortingField.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.framework.common.pojo; + +import java.io.Serializable; + +/** + * 排序字段 DTO + * + * 类名加了 ing 的原因是,避免和 ES SortField 重名。 + */ +public class SortingField implements Serializable { + + /** + * 顺序 - 升序 + */ + public static final String ORDER_ASC = "asc"; + /** + * 顺序 - 降序 + */ + public static final String ORDER_DESC = "desc"; + + /** + * 字段 + */ + private String field; + /** + * 顺序 + */ + private String order; + + // 空构造方法,解决反序列化 + public SortingField() { + } + + public SortingField(String field, String order) { + this.field = field; + this.order = order; + } + + public String getField() { + return field; + } + + public SortingField setField(String field) { + this.field = field; + return this; + } + + public String getOrder() { + return order; + } + + public SortingField setOrder(String order) { + this.order = order; + return this; + } +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/cache/CacheUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/cache/CacheUtils.java new file mode 100644 index 0000000..11e6bb4 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/cache/CacheUtils.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.common.util.cache; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; + +import java.time.Duration; +import java.util.concurrent.Executors; + +/** + * Cache 工具类 + * + * @author 芋道源码 + */ +public class CacheUtils { + + public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) { + return CacheBuilder.newBuilder() + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 + .build(CacheLoader.asyncReloading(loader, Executors.newCachedThreadPool())); // TODO 芋艿:可能要思考下,未来要不要做成可配置 + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/ArrayUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/ArrayUtils.java new file mode 100644 index 0000000..ed6b938 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/ArrayUtils.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.framework.common.util.collection; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.collection.IterUtil; +import cn.hutool.core.util.ArrayUtil; + +import java.util.Collection; +import java.util.function.Consumer; +import java.util.function.Function; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; + +/** + * Array 工具类 + * + * @author 芋道源码 + */ +public class ArrayUtils { + + /** + * 将 object 和 newElements 合并成一个数组 + * + * @param object 对象 + * @param newElements 数组 + * @param 泛型 + * @return 结果数组 + */ + @SafeVarargs + public static Consumer[] append(Consumer object, Consumer... newElements) { + if (object == null) { + return newElements; + } + Consumer[] result = ArrayUtil.newArray(Consumer.class, 1 + newElements.length); + result[0] = object; + System.arraycopy(newElements, 0, result, 1, newElements.length); + return result; + } + + public static V[] toArray(Collection from, Function mapper) { + return toArray(convertList(from, mapper)); + } + + @SuppressWarnings("unchecked") + public static T[] toArray(Collection from) { + if (CollectionUtil.isEmpty(from)) { + return (T[]) (new Object[0]); + } + return ArrayUtil.toArray(from, (Class) IterUtil.getElementType(from.iterator())); + } + + public static T get(T[] array, int index) { + if (null == array || index >= array.length) { + return null; + } + return array[index]; + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/CollectionUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/CollectionUtils.java new file mode 100644 index 0000000..0c5879f --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/CollectionUtils.java @@ -0,0 +1,204 @@ +package com.yunxi.scm.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.google.common.collect.ImmutableMap; + +import java.util.*; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +/** + * Collection 工具类 + * + * @author 芋道源码 + */ +public class CollectionUtils { + + public static boolean containsAny(Object source, Object... targets) { + return Arrays.asList(targets).contains(source); + } + + public static boolean isAnyEmpty(Collection... collections) { + return Arrays.stream(collections).anyMatch(CollectionUtil::isEmpty); + } + + public static List filterList(Collection from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(predicate).collect(Collectors.toList()); + } + + public static List distinct(Collection from, Function keyMapper) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return distinct(from, keyMapper, (t1, t2) -> t1); + } + + public static List distinct(Collection from, Function keyMapper, BinaryOperator cover) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return new ArrayList<>(convertMap(from, keyMapper, Function.identity(), cover).values()); + } + + public static List convertList(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static List convertList(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new ArrayList<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toList()); + } + + public static Set convertSet(Collection from, Function func) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Set convertSet(Collection from, Function func, Predicate filter) { + if (CollUtil.isEmpty(from)) { + return new HashSet<>(); + } + return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); + } + + public static Map convertMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, Function.identity()); + } + + public static Map convertMap(Collection from, Function keyFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, Function.identity(), supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return convertMap(from, keyFunc, valueFunc, mergeFunction, HashMap::new); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return supplier.get(); + } + return convertMap(from, keyFunc, valueFunc, (v1, v2) -> v1, supplier); + } + + public static Map convertMap(Collection from, Function keyFunc, Function valueFunc, BinaryOperator mergeFunction, Supplier> supplier) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.toMap(keyFunc, valueFunc, mergeFunction, supplier)); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(t -> t, Collectors.toList()))); + } + + public static Map> convertMultiMap(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream() + .collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toList()))); + } + + // 暂时没想好名字,先以 2 结尾噶 + public static Map> convertMultiMap2(Collection from, Function keyFunc, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return new HashMap<>(); + } + return from.stream().collect(Collectors.groupingBy(keyFunc, Collectors.mapping(valueFunc, Collectors.toSet()))); + } + + public static Map convertImmutableMap(Collection from, Function keyFunc) { + if (CollUtil.isEmpty(from)) { + return Collections.emptyMap(); + } + ImmutableMap.Builder builder = ImmutableMap.builder(); + from.forEach(item -> builder.put(keyFunc.apply(item), item)); + return builder.build(); + } + + public static boolean containsAny(Collection source, Collection candidates) { + return org.springframework.util.CollectionUtils.containsAny(source, candidates); + } + + public static T getFirst(List from) { + return !CollectionUtil.isEmpty(from) ? from.get(0) : null; + } + + public static T findFirst(List from, Predicate predicate) { + if (CollUtil.isEmpty(from)) { + return null; + } + return from.stream().filter(predicate).findFirst().orElse(null); + } + + public static > V getMaxValue(Collection from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + T t = from.stream().max(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > V getMinValue(List from, Function valueFunc) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + T t = from.stream().min(Comparator.comparing(valueFunc)).get(); + return valueFunc.apply(t); + } + + public static > V getSumValue(List from, Function valueFunc, BinaryOperator accumulator) { + if (CollUtil.isEmpty(from)) { + return null; + } + assert from.size() > 0; // 断言,避免告警 + return from.stream().map(valueFunc).reduce(accumulator).get(); + } + + public static void addIfNotNull(Collection coll, T item) { + if (item == null) { + return; + } + coll.add(item); + } + + public static Collection singleton(T deptId) { + return deptId == null ? Collections.emptyList() : Collections.singleton(deptId); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/MapUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/MapUtils.java new file mode 100644 index 0000000..4982d92 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/MapUtils.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * Map 工具类 + * + * @author 芋道源码 + */ +public class MapUtils { + + /** + * 从哈希表表中,获得 keys 对应的所有 value 数组 + * + * @param multimap 哈希表 + * @param keys keys + * @return value 数组 + */ + public static List getList(Multimap multimap, Collection keys) { + List result = new ArrayList<>(); + keys.forEach(k -> { + Collection values = multimap.get(k); + if (CollectionUtil.isEmpty(values)) { + return; + } + result.addAll(values); + }); + return result; + } + + /** + * 从哈希表查找到 key 对应的 value,然后进一步处理 + * 注意,如果查找到的 value 为 null 时,不进行处理 + * + * @param map 哈希表 + * @param key key + * @param consumer 进一步处理的逻辑 + */ + public static void findAndThen(Map map, K key, Consumer consumer) { + if (CollUtil.isEmpty(map)) { + return; + } + V value = map.get(key); + if (value == null) { + return; + } + consumer.accept(value); + } + + public static Map convertMap(List> keyValues) { + Map map = Maps.newLinkedHashMapWithExpectedSize(keyValues.size()); + keyValues.forEach(keyValue -> map.put(keyValue.getKey(), keyValue.getValue())); + return map; + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/SetUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/SetUtils.java new file mode 100644 index 0000000..03d8869 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/collection/SetUtils.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.framework.common.util.collection; + +import cn.hutool.core.collection.CollUtil; + +import java.util.Set; + +/** + * Set 工具类 + * + * @author 芋道源码 + */ +public class SetUtils { + + @SafeVarargs + public static Set asSet(T... objs) { + return CollUtil.newHashSet(objs); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/date/DateUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/date/DateUtils.java new file mode 100644 index 0000000..e8bfc09 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/date/DateUtils.java @@ -0,0 +1,172 @@ +package com.yunxi.scm.framework.common.util.date; + +import cn.hutool.core.date.LocalDateTimeUtil; + +import java.time.*; +import java.util.Calendar; +import java.util.Date; + +/** + * 时间工具类 + * + * @author 芋道源码 + */ +public class DateUtils { + + /** + * 时区 - 默认 + */ + public static final String TIME_ZONE_DEFAULT = "GMT+8"; + + /** + * 秒转换成毫秒 + */ + public static final long SECOND_MILLIS = 1000; + + public static final String FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND = "yyyy-MM-dd HH:mm:ss"; + + public static final String FORMAT_HOUR_MINUTE_SECOND = "HH:mm:ss"; + + /** + * 将 LocalDateTime 转换成 Date + * + * @param date LocalDateTime + * @return LocalDateTime + */ + public static Date of(LocalDateTime date) { + // 将此日期时间与时区相结合以创建 ZonedDateTime + ZonedDateTime zonedDateTime = date.atZone(ZoneId.systemDefault()); + // 本地时间线 LocalDateTime 到即时时间线 Instant 时间戳 + Instant instant = zonedDateTime.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return Date.from(instant); + } + + /** + * 将 Date 转换成 LocalDateTime + * + * @param date Date + * @return LocalDateTime + */ + public static LocalDateTime of(Date date) { + // 转为时间戳 + Instant instant = date.toInstant(); + // UTC时间(世界协调时间,UTC + 00:00)转北京(北京,UTC + 8:00)时间 + return LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); + } + + public static Date addTime(Duration duration) { + return new Date(System.currentTimeMillis() + duration.toMillis()); + } + + public static boolean isExpired(Date time) { + return System.currentTimeMillis() > time.getTime(); + } + + public static boolean isExpired(LocalDateTime time) { + LocalDateTime now = LocalDateTime.now(); + return now.isAfter(time); + } + + public static long diff(Date endTime, Date startTime) { + return endTime.getTime() - startTime.getTime(); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @return 指定时间 + */ + public static Date buildTime(int year, int mouth, int day) { + return buildTime(year, mouth, day, 0, 0, 0); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @param hour 小时 + * @param minute 分钟 + * @param second 秒 + * @return 指定时间 + */ + public static Date buildTime(int year, int mouth, int day, + int hour, int minute, int second) { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, mouth - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minute); + calendar.set(Calendar.SECOND, second); + calendar.set(Calendar.MILLISECOND, 0); // 一般情况下,都是 0 毫秒 + return calendar.getTime(); + } + + public static Date max(Date a, Date b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.compareTo(b) > 0 ? a : b; + } + + public static LocalDateTime max(LocalDateTime a, LocalDateTime b) { + if (a == null) { + return b; + } + if (b == null) { + return a; + } + return a.isAfter(b) ? a : b; + } + + /** + * 计算当期时间相差的日期 + * + * @param field 日历字段.
eg:Calendar.MONTH,Calendar.DAY_OF_MONTH,
Calendar.HOUR_OF_DAY等. + * @param amount 相差的数值 + * @return 计算后的日志 + */ + public static Date addDate(int field, int amount) { + return addDate(null, field, amount); + } + + /** + * 计算当期时间相差的日期 + * + * @param date 设置时间 + * @param field 日历字段 例如说,{@link Calendar#DAY_OF_MONTH} 等 + * @param amount 相差的数值 + * @return 计算后的日志 + */ + public static Date addDate(Date date, int field, int amount) { + if (amount == 0) { + return date; + } + Calendar c = Calendar.getInstance(); + if (date != null) { + c.setTime(date); + } + c.add(field, amount); + return c.getTime(); + } + + /** + * 是否今天 + * + * @param date 日期 + * @return 是否 + */ + public static boolean isToday(LocalDateTime date) { + return LocalDateTimeUtil.isSameDay(date, LocalDateTime.now()); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/date/LocalDateTimeUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/date/LocalDateTimeUtils.java new file mode 100644 index 0000000..963ceff --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/date/LocalDateTimeUtils.java @@ -0,0 +1,84 @@ +package com.yunxi.scm.framework.common.util.date; + +import cn.hutool.core.date.LocalDateTimeUtil; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; + +/** + * 时间工具类,用于 {@link java.time.LocalDateTime} + * + * @author 芋道源码 + */ +public class LocalDateTimeUtils { + + /** + * 空的 LocalDateTime 对象,主要用于 DB 唯一索引的默认值 + */ + public static LocalDateTime EMPTY = buildTime(1970, 1, 1); + + public static LocalDateTime addTime(Duration duration) { + return LocalDateTime.now().plus(duration); + } + + public static boolean beforeNow(LocalDateTime date) { + return date.isBefore(LocalDateTime.now()); + } + + public static boolean afterNow(LocalDateTime date) { + return date.isAfter(LocalDateTime.now()); + } + + /** + * 创建指定时间 + * + * @param year 年 + * @param mouth 月 + * @param day 日 + * @return 指定时间 + */ + public static LocalDateTime buildTime(int year, int mouth, int day) { + return LocalDateTime.of(year, mouth, day, 0, 0, 0); + } + + public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, + int year2, int mouth2, int day2) { + return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; + } + + /** + * 判断当前时间是否在该时间范围内 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 是否 + */ + public static boolean isBetween(LocalDateTime startTime, LocalDateTime endTime) { + if (startTime == null || endTime == null) { + return false; + } + return LocalDateTimeUtil.isIn(LocalDateTime.now(), startTime, endTime); + } + + /** + * 检查时间重叠 不包含日期 + * + * @param startTime1 需要校验的开始时间 + * @param endTime1 需要校验的结束时间 + * @param startTime2 校验所需的开始时间 + * @param endTime2 校验所需的结束时间 + * @return 是否重叠 + */ + // TODO @puhui999:LocalDateTimeUtil.isOverlap() 是不是可以满足呀? + public static boolean checkTimeOverlap(LocalTime startTime1, LocalTime endTime1, LocalTime startTime2, LocalTime endTime2) { + // 判断时间是否重叠 + // 开始时间在已配置时段的结束时间之前 且 结束时间在已配置时段的开始时间之后 [] + return startTime1.isBefore(endTime2) && endTime1.isAfter(startTime2) + // 开始时间在已配置时段的开始时间之前 且 结束时间在已配置时段的开始时间之后 (] 或 () + || startTime1.isBefore(startTime2) && endTime1.isAfter(startTime2) + // 开始时间在已配置时段的结束时间之前 且 结束时间在已配值时段的结束时间之后 [) 或 () + || startTime1.isBefore(endTime2) && endTime1.isAfter(endTime2); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/http/HttpUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/http/HttpUtils.java new file mode 100644 index 0000000..52b1cc8 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/http/HttpUtils.java @@ -0,0 +1,126 @@ +package com.yunxi.scm.framework.common.util.http; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.map.TableMap; +import cn.hutool.core.net.url.UrlBuilder; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import org.springframework.util.StringUtils; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.servlet.http.HttpServletRequest; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Map; + +/** + * HTTP 工具类 + * + * @author 芋道源码 + */ +public class HttpUtils { + + @SuppressWarnings("unchecked") + public static String replaceUrlQuery(String url, String key, String value) { + UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); + // 先移除 + TableMap query = (TableMap) + ReflectUtil.getFieldValue(builder.getQuery(), "query"); + query.remove(key); + // 后添加 + builder.addQuery(key, value); + return builder.build(); + } + + private String append(String base, Map query, boolean fragment) { + return append(base, query, null, fragment); + } + + /** + * 拼接 URL + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 append 方法 + * + * @param base 基础 URL + * @param query 查询参数 + * @param keys query 的 key,对应的原本的 key 的映射。例如说 query 里有个 key 是 xx,实际它的 key 是 extra_xx,则通过 keys 里添加这个映射 + * @param fragment URL 的 fragment,即拼接到 # 中 + * @return 拼接后的 URL + */ + public static String append(String base, Map query, Map keys, boolean fragment) { + UriComponentsBuilder template = UriComponentsBuilder.newInstance(); + UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(base); + URI redirectUri; + try { + // assume it's encoded to start with (if it came in over the wire) + redirectUri = builder.build(true).toUri(); + } catch (Exception e) { + // ... but allow client registrations to contain hard-coded non-encoded values + redirectUri = builder.build().toUri(); + builder = UriComponentsBuilder.fromUri(redirectUri); + } + template.scheme(redirectUri.getScheme()).port(redirectUri.getPort()).host(redirectUri.getHost()) + .userInfo(redirectUri.getUserInfo()).path(redirectUri.getPath()); + + if (fragment) { + StringBuilder values = new StringBuilder(); + if (redirectUri.getFragment() != null) { + String append = redirectUri.getFragment(); + values.append(append); + } + for (String key : query.keySet()) { + if (values.length() > 0) { + values.append("&"); + } + String name = key; + if (keys != null && keys.containsKey(key)) { + name = keys.get(key); + } + values.append(name).append("={").append(key).append("}"); + } + if (values.length() > 0) { + template.fragment(values.toString()); + } + UriComponents encoded = template.build().expand(query).encode(); + builder.fragment(encoded.getFragment()); + } else { + for (String key : query.keySet()) { + String name = key; + if (keys != null && keys.containsKey(key)) { + name = keys.get(key); + } + template.queryParam(name, "{" + key + "}"); + } + template.fragment(redirectUri.getFragment()); + UriComponents encoded = template.build().expand(query).encode(); + builder.query(encoded.getQuery()); + } + return builder.build().toUriString(); + } + + public static String[] obtainBasicAuthorization(HttpServletRequest request) { + String clientId; + String clientSecret; + // 先从 Header 中获取 + String authorization = request.getHeader("Authorization"); + authorization = StrUtil.subAfter(authorization, "Basic ", true); + if (StringUtils.hasText(authorization)) { + authorization = Base64.decodeStr(authorization); + clientId = StrUtil.subBefore(authorization, ":", false); + clientSecret = StrUtil.subAfter(authorization, ":", false); + // 再从 Param 中获取 + } else { + clientId = request.getParameter("client_id"); + clientSecret = request.getParameter("client_secret"); + } + + // 如果两者非空,则返回 + if (StrUtil.isNotEmpty(clientId) && StrUtil.isNotEmpty(clientSecret)) { + return new String[]{clientId, clientSecret}; + } + return null; + } + + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/io/FileUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/io/FileUtils.java new file mode 100644 index 0000000..6a26dc5 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/io/FileUtils.java @@ -0,0 +1,84 @@ +package com.yunxi.scm.framework.common.util.io; + +import cn.hutool.core.io.FileTypeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import lombok.SneakyThrows; + +import java.io.ByteArrayInputStream; +import java.io.File; + +/** + * 文件工具类 + * + * @author 芋道源码 + */ +public class FileUtils { + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(String data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeUtf8String(data, file); + return file; + } + + /** + * 创建临时文件 + * 该文件会在 JVM 退出时,进行删除 + * + * @param data 文件内容 + * @return 文件 + */ + @SneakyThrows + public static File createTempFile(byte[] data) { + File file = createTempFile(); + // 写入内容 + FileUtil.writeBytes(data, file); + return file; + } + + /** + * 创建临时文件,无内容 + * 该文件会在 JVM 退出时,进行删除 + * + * @return 文件 + */ + @SneakyThrows + public static File createTempFile() { + // 创建文件,通过 UUID 保证唯一 + File file = File.createTempFile(IdUtil.simpleUUID(), null); + // 标记 JVM 退出时,自动删除 + file.deleteOnExit(); + return file; + } + + /** + * 生成文件路径 + * + * @param content 文件内容 + * @param originalName 原始文件名 + * @return path,唯一不可重复 + */ + public static String generatePath(byte[] content, String originalName) { + String sha256Hex = DigestUtil.sha256Hex(content); + // 情况一:如果存在 name,则优先使用 name 的后缀 + if (StrUtil.isNotBlank(originalName)) { + String extName = FileNameUtil.extName(originalName); + return StrUtil.isBlank(extName) ? sha256Hex : sha256Hex + "." + extName; + } + // 情况二:基于 content 计算 + return sha256Hex + '.' + FileTypeUtil.getType(new ByteArrayInputStream(content)); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/io/IoUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/io/IoUtils.java new file mode 100644 index 0000000..4288fca --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/io/IoUtils.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.common.util.io; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; + +import java.io.InputStream; + +/** + * IO 工具类,用于 {@link cn.hutool.core.io.IoUtil} 缺失的方法 + * + * @author 芋道源码 + */ +public class IoUtils { + + /** + * 从流中读取 UTF8 编码的内容 + * + * @param in 输入流 + * @param isClose 是否关闭 + * @return 内容 + * @throws IORuntimeException IO 异常 + */ + public static String readUtf8(InputStream in, boolean isClose) throws IORuntimeException { + return StrUtil.utf8Str(IoUtil.read(in, isClose)); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/json/JsonUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/json/JsonUtils.java new file mode 100644 index 0000000..db662e3 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/json/JsonUtils.java @@ -0,0 +1,159 @@ +package com.yunxi.scm.framework.common.util.json; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import lombok.SneakyThrows; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +/** + * JSON 工具类 + * + * @author 芋道源码 + */ +@UtilityClass +@Slf4j +public class JsonUtils { + + private static ObjectMapper objectMapper = new ObjectMapper(); + + static { + objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化 + } + + /** + * 初始化 objectMapper 属性 + *

+ * 通过这样的方式,使用 Spring 创建的 ObjectMapper Bean + * + * @param objectMapper ObjectMapper 对象 + */ + public static void init(ObjectMapper objectMapper) { + JsonUtils.objectMapper = objectMapper; + } + + @SneakyThrows + public static String toJsonString(Object object) { + return objectMapper.writeValueAsString(object); + } + + @SneakyThrows + public static byte[] toJsonByte(Object object) { + return objectMapper.writeValueAsBytes(object); + } + + @SneakyThrows + public static String toJsonPrettyString(Object object) { + return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(object); + } + + public static T parseObject(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, Type type) { + if (StrUtil.isEmpty(text)) { + return null; + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructType(type)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + /** + * 将字符串解析成指定类型的对象 + * 使用 {@link #parseObject(String, Class)} 时,在@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) 的场景下, + * 如果 text 没有 class 属性,则会报错。此时,使用这个方法,可以解决。 + * + * @param text 字符串 + * @param clazz 类型 + * @return 对象 + */ + public static T parseObject2(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return null; + } + return JSONUtil.toBean(text, clazz); + } + + public static T parseObject(byte[] bytes, Class clazz) { + if (ArrayUtil.isEmpty(bytes)) { + return null; + } + try { + return objectMapper.readValue(bytes, clazz); + } catch (IOException e) { + log.error("json parse err,json:{}", bytes, e); + throw new RuntimeException(e); + } + } + + public static T parseObject(String text, TypeReference typeReference) { + try { + return objectMapper.readValue(text, typeReference); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static List parseArray(String text, Class clazz) { + if (StrUtil.isEmpty(text)) { + return new ArrayList<>(); + } + try { + return objectMapper.readValue(text, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(String text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static JsonNode parseTree(byte[] text) { + try { + return objectMapper.readTree(text); + } catch (IOException e) { + log.error("json parse err,json:{}", text, e); + throw new RuntimeException(e); + } + } + + public static boolean isJson(String text) { + return JSONUtil.isTypeJSON(text); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/monitor/TracerUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/monitor/TracerUtils.java new file mode 100644 index 0000000..fd4453c --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/monitor/TracerUtils.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.framework.common.util.monitor; + +import org.apache.skywalking.apm.toolkit.trace.TraceContext; + +/** + * 链路追踪工具类 + * + * 考虑到每个 starter 都需要用到该工具类,所以放到 common 模块下的 util 包下 + * + * @author 芋道源码 + */ +public class TracerUtils { + + /** + * 私有化构造方法 + */ + private TracerUtils() { + } + + /** + * 获得链路追踪编号,直接返回 SkyWalking 的 TraceId。 + * 如果不存在的话为空字符串!!! + * + * @return 链路追踪编号 + */ + public static String getTraceId() { + return TraceContext.traceId(); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/number/NumberUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/number/NumberUtils.java new file mode 100644 index 0000000..868ff7a --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/number/NumberUtils.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.framework.common.util.number; + +import cn.hutool.core.util.StrUtil; + +/** + * 数字的工具类,补全 {@link cn.hutool.core.util.NumberUtil} 的功能 + * + * @author 芋道源码 + */ +public class NumberUtils { + + public static Long parseLong(String str) { + return StrUtil.isNotEmpty(str) ? Long.valueOf(str) : null; + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/object/ObjectUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/object/ObjectUtils.java new file mode 100644 index 0000000..fad3480 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/object/ObjectUtils.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.framework.common.util.object; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.ReflectUtil; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.function.Consumer; + +/** + * Object 工具类 + * + * @author 芋道源码 + */ +public class ObjectUtils { + + /** + * 复制对象,并忽略 Id 编号 + * + * @param object 被复制对象 + * @param consumer 消费者,可以二次编辑被复制对象 + * @return 复制后的对象 + */ + public static T cloneIgnoreId(T object, Consumer consumer) { + T result = ObjectUtil.clone(object); + // 忽略 id 编号 + Field field = ReflectUtil.getField(object.getClass(), "id"); + if (field != null) { + ReflectUtil.setFieldValue(result, field, null); + } + // 二次编辑 + if (result != null) { + consumer.accept(result); + } + return result; + } + + public static > T max(T obj1, T obj2) { + if (obj1 == null) { + return obj2; + } + if (obj2 == null) { + return obj1; + } + return obj1.compareTo(obj2) > 0 ? obj1 : obj2; + } + + @SafeVarargs + public static T defaultIfNull(T... array) { + for (T item : array) { + if (item != null) { + return item; + } + } + return null; + } + + @SafeVarargs + public static boolean equalsAny(T obj, T... array) { + return Arrays.asList(array).contains(obj); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/object/PageUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/object/PageUtils.java new file mode 100644 index 0000000..5666cd9 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/object/PageUtils.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.framework.common.util.object; + +import com.yunxi.scm.framework.common.pojo.PageParam; + +/** + * {@link com.yunxi.scm.framework.common.pojo.PageParam} 工具类 + * + * @author 芋道源码 + */ +public class PageUtils { + + public static int getStart(PageParam pageParam) { + return (pageParam.getPageNo() - 1) * pageParam.getPageSize(); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/package-info.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/package-info.java new file mode 100644 index 0000000..ad2f5a1 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/package-info.java @@ -0,0 +1,7 @@ +/** + * 对于工具类的选择,优先查找 Hutool 中有没对应的方法 + * 如果没有,则自己封装对应的工具类,以 Utils 结尾,用于区分 + * + * ps:如果担心 Hutool 存在坑的问题,可以阅读 Hutool 的实现源码,以确保可靠性。并且,可以补充相关的单元测试。 + */ +package com.yunxi.scm.framework.common.util; diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/servlet/ServletUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/servlet/ServletUtils.java new file mode 100644 index 0000000..017485f --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/servlet/ServletUtils.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.framework.common.util.servlet; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.servlet.ServletUtil; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import org.springframework.http.MediaType; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.Map; + +/** + * 客户端工具类 + * + * @author 芋道源码 + */ +public class ServletUtils { + + /** + * 返回 JSON 字符串 + * + * @param response 响应 + * @param object 对象,会序列化成 JSON 字符串 + */ + @SuppressWarnings("deprecation") // 必须使用 APPLICATION_JSON_UTF8_VALUE,否则会乱码 + public static void writeJSON(HttpServletResponse response, Object object) { + String content = JsonUtils.toJsonString(object); + ServletUtil.write(response, content, MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + /** + * 返回附件 + * + * @param response 响应 + * @param filename 文件名 + * @param content 附件内容 + */ + public static void writeAttachment(HttpServletResponse response, String filename, byte[] content) throws IOException { + // 设置 header 和 contentType + response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE); + // 输出附件 + IoUtil.write(response.getOutputStream(), false, content); + } + + /** + * @param request 请求 + * @return ua + */ + public static String getUserAgent(HttpServletRequest request) { + String ua = request.getHeader("User-Agent"); + return ua != null ? ua : ""; + } + + /** + * 获得请求 + * + * @return HttpServletRequest + */ + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + return ((ServletRequestAttributes) requestAttributes).getRequest(); + } + + public static String getUserAgent() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return getUserAgent(request); + } + + public static String getClientIP() { + HttpServletRequest request = getRequest(); + if (request == null) { + return null; + } + return ServletUtil.getClientIP(request); + } + + public static boolean isJsonRequest(ServletRequest request) { + return StrUtil.startWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE); + } + + public static String getBody(HttpServletRequest request) { + return ServletUtil.getBody(request); + } + + public static byte[] getBodyBytes(HttpServletRequest request) { + return ServletUtil.getBodyBytes(request); + } + + public static String getClientIP(HttpServletRequest request) { + return ServletUtil.getClientIP(request); + } + + public static Map getParamMap(HttpServletRequest request) { + return ServletUtil.getParamMap(request); + } +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/spring/SpringAopUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/spring/SpringAopUtils.java new file mode 100644 index 0000000..b0415d5 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/spring/SpringAopUtils.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.framework.common.util.spring; + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.support.AopUtils; + +/** + * Spring AOP 工具类 + * + * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现 + */ +public class SpringAopUtils { + + /** + * 获取代理的目标对象 + * + * @param proxy 代理对象 + * @return 目标对象 + */ + public static Object getTarget(Object proxy) throws Exception { + // 不是代理对象 + if (!AopUtils.isAopProxy(proxy)) { + return proxy; + } + // Jdk 代理 + if (AopUtils.isJdkDynamicProxy(proxy)) { + return getJdkDynamicProxyTargetObject(proxy); + } + // Cglib 代理 + return getCglibProxyTargetObject(proxy); + } + + private static Object getCglibProxyTargetObject(Object proxy) throws Exception { + Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + + private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception { + AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h"); + AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised"); + return advisedSupport.getTargetSource().getTarget(); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/spring/SpringExpressionUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/spring/SpringExpressionUtils.java new file mode 100644 index 0000000..df46bbc --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/spring/SpringExpressionUtils.java @@ -0,0 +1,133 @@ +package com.yunxi.scm.framework.common.util.spring; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ArrayUtil; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Spring EL 表达式的工具类 + * + * @author mashu + */ +public class SpringExpressionUtils { + + /** + * spel表达式解析器 + */ + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + /** + * 参数名发现器 + */ + private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); + + private SpringExpressionUtils() { + } + + /** + * 从切面中,单个解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionString EL 表达式数组 + * @return 执行界面 + */ + public static Object parseExpression(ProceedingJoinPoint joinPoint, String expressionString) { + Map result = parseExpressions(joinPoint, Collections.singletonList(expressionString)); + return result.get(expressionString); + } + + /** + * 从切面中,批量解析 EL 表达式的结果 + * + * @param joinPoint 切面点 + * @param expressionStrings EL 表达式数组 + * @return 结果,key 为表达式,value 为对应值 + */ + public static Map parseExpressions(ProceedingJoinPoint joinPoint, List expressionStrings) { + // 如果为空,则不进行解析 + if (CollUtil.isEmpty(expressionStrings)) { + return MapUtil.newHashMap(); + } + + // 第一步,构建解析的上下文 EvaluationContext + // 通过 joinPoint 获取被注解方法 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + Method method = methodSignature.getMethod(); + // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组 + String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + // Spring 的表达式上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + // 给上下文赋值 + if (ArrayUtil.isNotEmpty(paramNames)) { + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < paramNames.length; i++) { + context.setVariable(paramNames[i], args[i]); + } + } + + // 第二步,逐个参数解析 + Map result = MapUtil.newHashMap(expressionStrings.size(), true); + expressionStrings.forEach(key -> { + Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); + result.put(key, value); + }); + return result; + } + + /** + * JoinPoint 切面 批量解析 EL 表达式,转换 jspl参数 + * + * @param joinPoint 切面点 + * @param info 返回值 + * @param expressionStrings EL 表达式数组 + * @return Map 结果 + * @author 陈賝 + * @since 2023/6/18 11:20 + */ + // TODO @chenchen: 这个方法,和 parseExpressions 比较接近,是不是可以合并下; + public static Map parseExpression(JoinPoint joinPoint, Object info, List expressionStrings) { + // 如果为空,则不进行解析 + if (CollUtil.isEmpty(expressionStrings)) { + return MapUtil.newHashMap(); + } + + // 第一步,构建解析的上下文 EvaluationContext + // 通过 joinPoint 获取被注解方法 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + // 使用 spring 的 ParameterNameDiscoverer 获取方法形参名数组 + String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method); + // Spring 的表达式上下文对象 + EvaluationContext context = new StandardEvaluationContext(); + if (ArrayUtil.isNotEmpty(parameterNames)) { + //获取方法参数值 + Object[] args = joinPoint.getArgs(); + for (int i = 0; i < args.length; i++) { + // 替换 SP EL 里的变量值为实际值, 比如 #user --> user对象 + context.setVariable(parameterNames[i], args[i]); + } + context.setVariable("info", info); + } + // 第二步,逐个参数解析 + Map result = MapUtil.newHashMap(expressionStrings.size(), true); + expressionStrings.forEach(key -> { + Object value = EXPRESSION_PARSER.parseExpression(key).getValue(context); + result.put(key, value); + }); + return result; + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/string/StrUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/string/StrUtils.java new file mode 100644 index 0000000..d06f63a --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/string/StrUtils.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.framework.common.util.string; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 字符串工具类 + * + * @author 芋道源码 + */ +public class StrUtils { + + public static String maxLength(CharSequence str, int maxLength) { + return StrUtil.maxLength(str, maxLength - 3); // -3 的原因,是该方法会补充 ... 恰好 + } + + /** + * 给定字符串是否以任何一个字符串开始 + * 给定字符串和数组为空都返回 false + * + * @param str 给定字符串 + * @param prefixes 需要检测的开始字符串 + * @since 3.0.6 + */ + public static boolean startWithAny(String str, Collection prefixes) { + if (StrUtil.isEmpty(str) || ArrayUtil.isEmpty(prefixes)) { + return false; + } + + for (CharSequence suffix : prefixes) { + if (StrUtil.startWith(str, suffix, false)) { + return true; + } + } + return false; + } + + public static List splitToLong(String value, CharSequence separator) { + long[] longs = StrUtil.splitToLong(value, separator); + return Arrays.stream(longs).boxed().collect(Collectors.toList()); + } + + public static List splitToInteger(String value, CharSequence separator) { + int[] integers = StrUtil.splitToInt(value, separator); + return Arrays.stream(integers).boxed().collect(Collectors.toList()); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/validation/ValidationUtils.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/validation/ValidationUtils.java new file mode 100644 index 0000000..121b74d --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/util/validation/ValidationUtils.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.framework.common.util.validation; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import org.springframework.util.StringUtils; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * 校验工具类 + * + * @author 芋道源码 + */ +public class ValidationUtils { + + private static final Pattern PATTERN_MOBILE = Pattern.compile("^(?:(?:\\+|00)86)?1(?:(?:3[\\d])|(?:4[0,1,4-9])|(?:5[0-3,5-9])|(?:6[2,5-7])|(?:7[0-8])|(?:8[\\d])|(?:9[0-3,5-9]))\\d{8}$"); + + private static final Pattern PATTERN_URL = Pattern.compile("^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]"); + + private static final Pattern PATTERN_XML_NCNAME = Pattern.compile("[a-zA-Z_][\\-_.0-9_a-zA-Z$]*"); + + public static boolean isMobile(String mobile) { + return StringUtils.hasText(mobile) + && PATTERN_MOBILE.matcher(mobile).matches(); + } + + public static boolean isURL(String url) { + return StringUtils.hasText(url) + && PATTERN_URL.matcher(url).matches(); + } + + public static boolean isXmlNCName(String str) { + return StringUtils.hasText(str) + && PATTERN_XML_NCNAME.matcher(str).matches(); + } + + public static void validate(Object object, Class... groups) { + Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + Assert.notNull(validator); + validate(validator, object, groups); + } + + public static void validate(Validator validator, Object object, Class... groups) { + Set> constraintViolations = validator.validate(object, groups); + if (CollUtil.isNotEmpty(constraintViolations)) { + throw new ConstraintViolationException(constraintViolations); + } + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/InEnum.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/InEnum.java new file mode 100644 index 0000000..8ca56c0 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/InEnum.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.framework.common.validation; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint( + validatedBy = InEnumValidator.class +) +public @interface InEnum { + + /** + * @return 实现 EnumValuable 接口的 + */ + Class value(); + + String message() default "必须在指定范围 {value}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/InEnumValidator.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/InEnumValidator.java new file mode 100644 index 0000000..72dbb27 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/InEnumValidator.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.framework.common.validation; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class InEnumValidator implements ConstraintValidator { + + private List values; + + @Override + public void initialize(InEnum annotation) { + IntArrayValuable[] values = annotation.value().getEnumConstants(); + if (values.length == 0) { + this.values = Collections.emptyList(); + } else { + this.values = Arrays.stream(values[0].array()).boxed().collect(Collectors.toList()); + } + } + + @Override + public boolean isValid(Integer value, ConstraintValidatorContext context) { + // 为空时,默认不校验,即认为通过 + if (value == null) { + return true; + } + // 校验通过 + if (values.contains(value)) { + return true; + } + // 校验不通过,自定义提示语句(因为,注解上的 value 是枚举类,无法获得枚举类的实际值) + context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值 + context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate() + .replaceAll("\\{value}", values.toString())).addConstraintViolation(); // 重新添加错误提示语句 + return false; + } + +} + diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/Mobile.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/Mobile.java new file mode 100644 index 0000000..49625e3 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/Mobile.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.common.validation; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.*; + +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.ANNOTATION_TYPE, + ElementType.CONSTRUCTOR, + ElementType.PARAMETER, + ElementType.TYPE_USE +}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint( + validatedBy = MobileValidator.class +) +public @interface Mobile { + + String message() default "手机号格式不正确"; + + Class[] groups() default {}; + + Class[] payload() default {}; + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/MobileValidator.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/MobileValidator.java new file mode 100644 index 0000000..fc43f1a --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/MobileValidator.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.common.validation; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class MobileValidator implements ConstraintValidator { + + @Override + public void initialize(Mobile annotation) { + } + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + // 如果手机号为空,默认不校验,即校验通过 + if (StrUtil.isEmpty(value)) { + return true; + } + // 校验手机 + return ValidationUtils.isMobile(value); + } + +} diff --git a/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/package-info.java b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/package-info.java new file mode 100644 index 0000000..0572974 --- /dev/null +++ b/yunxi-framework/yunxi-common/src/main/java/com/yunxi/scm/framework/common/validation/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Hibernate Validator 实现参数校验 + */ +package com.yunxi.scm.framework.common.validation; diff --git a/yunxi-framework/yunxi-common/《芋道 Spring Boot 参数校验 Validation 入门》.md b/yunxi-framework/yunxi-common/《芋道 Spring Boot 参数校验 Validation 入门》.md new file mode 100644 index 0000000..eec62f4 --- /dev/null +++ b/yunxi-framework/yunxi-common/《芋道 Spring Boot 参数校验 Validation 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-banner/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-banner/pom.xml new file mode 100644 index 0000000..9f4ce64 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-banner/pom.xml @@ -0,0 +1,30 @@ + + + + yunxi-framework + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-banner + jar + + ${project.artifactId} + Banner 用于在 console 控制台,打印开发文档、接口文档等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + org.springframework.boot + spring-boot-starter + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/config/YunxiBannerAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/config/YunxiBannerAutoConfiguration.java new file mode 100644 index 0000000..477531e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/config/YunxiBannerAutoConfiguration.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.framework.banner.config; + +import com.yunxi.scm.framework.banner.core.BannerApplicationRunner; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * Banner 的自动配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class YunxiBannerAutoConfiguration { + + @Bean + public BannerApplicationRunner bannerApplicationRunner() { + return new BannerApplicationRunner(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/core/BannerApplicationRunner.java b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/core/BannerApplicationRunner.java new file mode 100644 index 0000000..36c25f1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/core/BannerApplicationRunner.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.framework.banner.core; + +import cn.hutool.core.thread.ThreadUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.util.ClassUtils; + +import java.util.concurrent.TimeUnit; + +/** + * 项目启动成功后,提供文档相关的地址 + * + * @author 芋道源码 + */ +@Slf4j +public class BannerApplicationRunner implements ApplicationRunner { + + @Override + public void run(ApplicationArguments args) { + ThreadUtil.execute(() -> { + ThreadUtil.sleep(1, TimeUnit.SECONDS); // 延迟 1 秒,保证输出到结尾 + log.info("\n----------------------------------------------------------\n\t" + + "项目启动成功!\n\t" + + "接口文档: \t{} \n\t" + + "开发文档: \t{} \n\t" + + "视频教程: \t{} \n" + + "----------------------------------------------------------", + "https://doc.iocoder.cn/api-doc/", + "https://doc.iocoder.cn", + "https://t.zsxq.com/02Yf6M7Qn"); + + // 数据报表 + if (isNotPresent("com.yunxi.scm.module.report.framework.security.config.SecurityConfiguration")) { + System.out.println("[报表模块 yunxi-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]"); + } + // 工作流 + if (isNotPresent("com.yunxi.scm.framework.flowable.config.YunxiFlowableConfiguration")) { + System.out.println("[工作流模块 yunxi-module-bpm - 已禁用][参考 https://doc.iocoder.cn/bpm/ 开启]"); + } + // 微信公众号 + if (isNotPresent("com.yunxi.scm.module.mp.framework.mp.config.MpConfiguration")) { + System.out.println("[微信公众号 yunxi-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + // 商城系统 + if (isNotPresent("com.yunxi.scm.module.trade.framework.web.config.TradeWebConfiguration")) { + System.out.println("[商城系统 yunxi-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + } + // 支付平台 + if (isNotPresent("com.yunxi.scm.module.pay.framework.pay.config.PayConfiguration")) { + System.out.println("[支付系统 yunxi-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + } + }); + } + + private static boolean isNotPresent(String className) { + return !ClassUtils.isPresent(className, ClassUtils.getDefaultClassLoader()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/package-info.java new file mode 100644 index 0000000..4610ba1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/java/com/yunxi/scm/framework/banner/package-info.java @@ -0,0 +1,6 @@ +/** + * Banner 用于在 console 控制台,打印开发文档、接口文档等 + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.banner; diff --git a/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..170edb9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.banner.config.YunxiBannerAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/resources/banner.txt b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/resources/banner.txt new file mode 100644 index 0000000..c54aabd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-banner/src/main/resources/banner.txt @@ -0,0 +1,17 @@ +芋道源码 http://www.iocoder.cn +Application Version: ${yunxi.info.version} +Spring Boot Version: ${spring-boot.version} + +.__ __. ______ .______ __ __ _______ +| \ | | / __ \ | _ \ | | | | / _____| +| \| | | | | | | |_) | | | | | | | __ +| . ` | | | | | | _ < | | | | | | |_ | +| |\ | | `--' | | |_) | | `--' | | |__| | +|__| \__| \______/ |______/ \______/ \______| + +███╗ ██╗ ██████╗ ██████╗ ██╗ ██╗ ██████╗ +████╗ ██║██╔═══██╗ ██╔══██╗██║ ██║██╔════╝ +██╔██╗ ██║██║ ██║ ██████╔╝██║ ██║██║ ███╗ +██║╚██╗██║██║ ██║ ██╔══██╗██║ ██║██║ ██║ +██║ ╚████║╚██████╔╝ ██████╔╝╚██████╔╝╚██████╔╝ +╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/pom.xml new file mode 100644 index 0000000..8c52f52 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/pom.xml @@ -0,0 +1,52 @@ + + + + yunxi-framework + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-biz-data-permission + jar + + ${project.artifactId} + 数据权限 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + true + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/config/YunxiDataPermissionAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/config/YunxiDataPermissionAutoConfiguration.java new file mode 100644 index 0000000..6959225 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/config/YunxiDataPermissionAutoConfiguration.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.framework.datapermission.config; + +import com.yunxi.scm.framework.datapermission.core.aop.DataPermissionAnnotationAdvisor; +import com.yunxi.scm.framework.datapermission.core.db.DataPermissionDatabaseInterceptor; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRule; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRuleFactory; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRuleFactoryImpl; +import com.yunxi.scm.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +/** + * 数据权限的自动配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class YunxiDataPermissionAutoConfiguration { + + @Bean + public DataPermissionRuleFactory dataPermissionRuleFactory(List rules) { + return new DataPermissionRuleFactoryImpl(rules); + } + + @Bean + public DataPermissionDatabaseInterceptor dataPermissionDatabaseInterceptor(MybatisPlusInterceptor interceptor, + DataPermissionRuleFactory ruleFactory) { + // 创建 DataPermissionDatabaseInterceptor 拦截器 + DataPermissionDatabaseInterceptor inner = new DataPermissionDatabaseInterceptor(ruleFactory); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return inner; + } + + @Bean + public DataPermissionAnnotationAdvisor dataPermissionAnnotationAdvisor() { + return new DataPermissionAnnotationAdvisor(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/config/YunxiDeptDataPermissionAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/config/YunxiDeptDataPermissionAutoConfiguration.java new file mode 100644 index 0000000..6440bcc --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/config/YunxiDeptDataPermissionAutoConfiguration.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.framework.datapermission.config; + +import com.yunxi.scm.framework.datapermission.core.rule.dept.DeptDataPermissionRule; +import com.yunxi.scm.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.module.system.api.permission.PermissionApi; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; + +import java.util.List; + +/** + * 基于部门的数据权限 AutoConfiguration + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnClass(LoginUser.class) +@ConditionalOnBean(value = {PermissionApi.class, DeptDataPermissionRuleCustomizer.class}) +public class YunxiDeptDataPermissionAutoConfiguration { + + @Bean + public DeptDataPermissionRule deptDataPermissionRule(PermissionApi permissionApi, + List customizers) { + // 创建 DeptDataPermissionRule 对象 + DeptDataPermissionRule rule = new DeptDataPermissionRule(permissionApi); + // 补全表配置 + customizers.forEach(customizer -> customizer.customize(rule)); + return rule; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/annotation/DataPermission.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/annotation/DataPermission.java new file mode 100644 index 0000000..f9bed17 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/annotation/DataPermission.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.framework.datapermission.core.annotation; + +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRule; + +import java.lang.annotation.*; + +/** + * 数据权限注解 + * 可声明在类或者方法上,标识使用的数据权限规则 + * + * @author 芋道源码 + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface DataPermission { + + /** + * 当前类或方法是否开启数据权限 + * 即使不添加 @DataPermission 注解,默认是开启状态 + * 可通过设置 enable 为 false 禁用 + */ + boolean enable() default true; + + /** + * 生效的数据权限规则数组,优先级高于 {@link #excludeRules()} + */ + Class[] includeRules() default {}; + + /** + * 排除的数据权限规则数组,优先级最低 + */ + Class[] excludeRules() default {}; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java new file mode 100644 index 0000000..d81f00f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationAdvisor.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.datapermission.core.aop; + +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import org.aopalliance.aop.Advice; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractPointcutAdvisor; +import org.springframework.aop.support.ComposablePointcut; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; + +/** + * {@link com.yunxi.scm.framework.datapermission.core.annotation.DataPermission} 注解的 Advisor 实现类 + * + * @author 芋道源码 + */ +@Getter +@EqualsAndHashCode(callSuper = true) +public class DataPermissionAnnotationAdvisor extends AbstractPointcutAdvisor { + + private final Advice advice; + + private final Pointcut pointcut; + + public DataPermissionAnnotationAdvisor() { + this.advice = new DataPermissionAnnotationInterceptor(); + this.pointcut = this.buildPointcut(); + } + + protected Pointcut buildPointcut() { + Pointcut classPointcut = new AnnotationMatchingPointcut(DataPermission.class, true); + Pointcut methodPointcut = new AnnotationMatchingPointcut(null, DataPermission.class, true); + return new ComposablePointcut(classPointcut).union(methodPointcut); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java new file mode 100644 index 0000000..eea776e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationInterceptor.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.framework.datapermission.core.aop; + +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import lombok.Getter; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.core.MethodClassKey; +import org.springframework.core.annotation.AnnotationUtils; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@link DataPermission} 注解的拦截器 + * 1. 在执行方法前,将 @DataPermission 注解入栈 + * 2. 在执行方法后,将 @DataPermission 注解出栈 + * + * @author 芋道源码 + */ +@DataPermission // 该注解,用于 {@link DATA_PERMISSION_NULL} 的空对象 +public class DataPermissionAnnotationInterceptor implements MethodInterceptor { + + /** + * DataPermission 空对象,用于方法无 {@link DataPermission} 注解时,使用 DATA_PERMISSION_NULL 进行占位 + */ + static final DataPermission DATA_PERMISSION_NULL = DataPermissionAnnotationInterceptor.class.getAnnotation(DataPermission.class); + + @Getter + private final Map dataPermissionCache = new ConcurrentHashMap<>(); + + @Override + public Object invoke(MethodInvocation methodInvocation) throws Throwable { + // 入栈 + DataPermission dataPermission = this.findAnnotation(methodInvocation); + if (dataPermission != null) { + DataPermissionContextHolder.add(dataPermission); + } + try { + // 执行逻辑 + return methodInvocation.proceed(); + } finally { + // 出栈 + if (dataPermission != null) { + DataPermissionContextHolder.remove(); + } + } + } + + private DataPermission findAnnotation(MethodInvocation methodInvocation) { + // 1. 从缓存中获取 + Method method = methodInvocation.getMethod(); + Object targetObject = methodInvocation.getThis(); + Class clazz = targetObject != null ? targetObject.getClass() : method.getDeclaringClass(); + MethodClassKey methodClassKey = new MethodClassKey(method, clazz); + DataPermission dataPermission = dataPermissionCache.get(methodClassKey); + if (dataPermission != null) { + return dataPermission != DATA_PERMISSION_NULL ? dataPermission : null; + } + + // 2.1 从方法中获取 + dataPermission = AnnotationUtils.findAnnotation(method, DataPermission.class); + // 2.2 从类上获取 + if (dataPermission == null) { + dataPermission = AnnotationUtils.findAnnotation(clazz, DataPermission.class); + } + // 2.3 添加到缓存中 + dataPermissionCache.put(methodClassKey, dataPermission != null ? dataPermission : DATA_PERMISSION_NULL); + return dataPermission; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionContextHolder.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionContextHolder.java new file mode 100644 index 0000000..5d7ddff --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionContextHolder.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.framework.datapermission.core.aop; + +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.alibaba.ttl.TransmittableThreadLocal; + +import java.util.LinkedList; +import java.util.List; + +/** + * {@link DataPermission} 注解的 Context 上下文 + * + * @author 芋道源码 + */ +public class DataPermissionContextHolder { + + /** + * 使用 List 的原因,可能存在方法的嵌套调用 + */ + private static final ThreadLocal> DATA_PERMISSIONS = + TransmittableThreadLocal.withInitial(LinkedList::new); + + /** + * 获得当前的 DataPermission 注解 + * + * @return DataPermission 注解 + */ + public static DataPermission get() { + return DATA_PERMISSIONS.get().peekLast(); + } + + /** + * 入栈 DataPermission 注解 + * + * @param dataPermission DataPermission 注解 + */ + public static void add(DataPermission dataPermission) { + DATA_PERMISSIONS.get().addLast(dataPermission); + } + + /** + * 出栈 DataPermission 注解 + * + * @return DataPermission 注解 + */ + public static DataPermission remove() { + DataPermission dataPermission = DATA_PERMISSIONS.get().removeLast(); + // 无元素时,清空 ThreadLocal + if (DATA_PERMISSIONS.get().isEmpty()) { + DATA_PERMISSIONS.remove(); + } + return dataPermission; + } + + /** + * 获得所有 DataPermission + * + * @return DataPermission 队列 + */ + public static List getAll() { + return DATA_PERMISSIONS.get(); + } + + /** + * 清空上下文 + * + * 目前仅仅用于单测 + */ + public static void clear() { + DATA_PERMISSIONS.remove(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java new file mode 100644 index 0000000..fb70009 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptor.java @@ -0,0 +1,639 @@ +package com.yunxi.scm.framework.datapermission.core.db; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRule; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRuleFactory; +import com.yunxi.scm.framework.mybatis.core.util.MyBatisUtils; +import com.alibaba.ttl.TransmittableThreadLocal; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.sf.jsqlparser.expression.*; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +import net.sf.jsqlparser.expression.operators.relational.ExistsExpression; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.delete.Delete; +import net.sf.jsqlparser.statement.select.*; +import net.sf.jsqlparser.statement.update.Update; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlCommandType; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.sql.Connection; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 数据权限拦截器,通过 {@link DataPermissionRule} 数据权限规则,重写 SQL 的方式来实现 + * 主要的 SQL 重写方法,可见 {@link #builderExpression(Expression, List)} 方法 + * + * 整体的代码实现上,参考 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor} 实现。 + * 所以每次 MyBatis Plus 升级时,需要 Review 下其具体的实现是否有变更! + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionDatabaseInterceptor extends JsqlParserSupport implements InnerInterceptor { + + private final DataPermissionRuleFactory ruleFactory; + + @Getter + private final MappedStatementCache mappedStatementCache = new MappedStatementCache(); + + @Override // SELECT 场景 + public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(ms.getId()); + if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 + return; + } + + PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); + try { + // 初始化上下文 + ContextHolder.init(rules); + // 处理 SQL + mpBs.sql(parserSingle(mpBs.sql(), null)); + } finally { + // 添加是否需要重写的缓存 + addMappedStatementCache(ms); + // 清空上下文 + ContextHolder.clear(); + } + } + + @Override // 只处理 UPDATE / DELETE 场景,不处理 INSERT 场景(因为 INSERT 不需要数据权限) + public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { + PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); + MappedStatement ms = mpSh.mappedStatement(); + SqlCommandType sct = ms.getSqlCommandType(); + if (sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { + // 获得 Mapper 对应的数据权限的规则 + List rules = ruleFactory.getDataPermissionRule(ms.getId()); + if (mappedStatementCache.noRewritable(ms, rules)) { // 如果无需重写,则跳过 + return; + } + + PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql(); + try { + // 初始化上下文 + ContextHolder.init(rules); + // 处理 SQL + mpBs.sql(parserMulti(mpBs.sql(), null)); + } finally { + // 添加是否需要重写的缓存 + addMappedStatementCache(ms); + // 清空上下文 + ContextHolder.clear(); + } + } + } + + @Override + protected void processSelect(Select select, int index, String sql, Object obj) { + processSelectBody(select.getSelectBody()); + List withItemsList = select.getWithItemsList(); + if (!CollectionUtils.isEmpty(withItemsList)) { + withItemsList.forEach(this::processSelectBody); + } + } + + /** + * update 语句处理 + */ + @Override + protected void processUpdate(Update update, int index, String sql, Object obj) { + final Table table = update.getTable(); + update.setWhere(this.builderExpression(update.getWhere(), table)); + } + + /** + * delete 语句处理 + */ + @Override + protected void processDelete(Delete delete, int index, String sql, Object obj) { + delete.setWhere(this.builderExpression(delete.getWhere(), delete.getTable())); + } + + // ========== 和 TenantLineInnerInterceptor 一致的逻辑 ========== + + protected void processSelectBody(SelectBody selectBody) { + if (selectBody == null) { + return; + } + if (selectBody instanceof PlainSelect) { + processPlainSelect((PlainSelect) selectBody); + } else if (selectBody instanceof WithItem) { + WithItem withItem = (WithItem) selectBody; + processSelectBody(withItem.getSubSelect().getSelectBody()); + } else { + SetOperationList operationList = (SetOperationList) selectBody; + List selectBodyList = operationList.getSelects(); + if (CollectionUtils.isNotEmpty(selectBodyList)) { + selectBodyList.forEach(this::processSelectBody); + } + } + } + + /** + * 处理 PlainSelect + */ + protected void processPlainSelect(PlainSelect plainSelect) { + //#3087 github + List selectItems = plainSelect.getSelectItems(); + if (CollectionUtils.isNotEmpty(selectItems)) { + selectItems.forEach(this::processSelectItem); + } + + // 处理 where 中的子查询 + Expression where = plainSelect.getWhere(); + processWhereSubSelect(where); + + // 处理 fromItem + FromItem fromItem = plainSelect.getFromItem(); + List list = processFromItem(fromItem); + List
mainTables = new ArrayList<>(list); + + // 处理 join + List joins = plainSelect.getJoins(); + if (CollectionUtils.isNotEmpty(joins)) { + mainTables = processJoins(mainTables, joins); + } + + // 当有 mainTable 时,进行 where 条件追加 + if (CollectionUtils.isNotEmpty(mainTables)) { + plainSelect.setWhere(builderExpression(where, mainTables)); + } + } + + private List
processFromItem(FromItem fromItem) { + // 处理括号括起来的表达式 + while (fromItem instanceof ParenthesisFromItem) { + fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); + } + + List
mainTables = new ArrayList<>(); + // 无 join 时的处理逻辑 + if (fromItem instanceof Table) { + Table fromTable = (Table) fromItem; + mainTables.add(fromTable); + } else if (fromItem instanceof SubJoin) { + // SubJoin 类型则还需要添加上 where 条件 + List
tables = processSubJoin((SubJoin) fromItem); + mainTables.addAll(tables); + } else { + // 处理下 fromItem + processOtherFromItem(fromItem); + } + return mainTables; + } + + /** + * 处理where条件内的子查询 + *

+ * 支持如下: + * 1. in + * 2. = + * 3. > + * 4. < + * 5. >= + * 6. <= + * 7. <> + * 8. EXISTS + * 9. NOT EXISTS + *

+ * 前提条件: + * 1. 子查询必须放在小括号中 + * 2. 子查询一般放在比较操作符的右边 + * + * @param where where 条件 + */ + protected void processWhereSubSelect(Expression where) { + if (where == null) { + return; + } + if (where instanceof FromItem) { + processOtherFromItem((FromItem) where); + return; + } + if (where.toString().indexOf("SELECT") > 0) { + // 有子查询 + if (where instanceof BinaryExpression) { + // 比较符号 , and , or , 等等 + BinaryExpression expression = (BinaryExpression) where; + processWhereSubSelect(expression.getLeftExpression()); + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof InExpression) { + // in + InExpression expression = (InExpression) where; + Expression inExpression = expression.getRightExpression(); + if (inExpression instanceof SubSelect) { + processSelectBody(((SubSelect) inExpression).getSelectBody()); + } + } else if (where instanceof ExistsExpression) { + // exists + ExistsExpression expression = (ExistsExpression) where; + processWhereSubSelect(expression.getRightExpression()); + } else if (where instanceof NotExpression) { + // not exists + NotExpression expression = (NotExpression) where; + processWhereSubSelect(expression.getExpression()); + } else if (where instanceof Parenthesis) { + Parenthesis expression = (Parenthesis) where; + processWhereSubSelect(expression.getExpression()); + } + } + } + + protected void processSelectItem(SelectItem selectItem) { + if (selectItem instanceof SelectExpressionItem) { + SelectExpressionItem selectExpressionItem = (SelectExpressionItem) selectItem; + if (selectExpressionItem.getExpression() instanceof SubSelect) { + processSelectBody(((SubSelect) selectExpressionItem.getExpression()).getSelectBody()); + } else if (selectExpressionItem.getExpression() instanceof Function) { + processFunction((Function) selectExpressionItem.getExpression()); + } + } + } + + /** + * 处理函数 + *

支持: 1. select fun(args..) 2. select fun1(fun2(args..),args..)

+ *

fixed gitee pulls/141

+ * + * @param function + */ + protected void processFunction(Function function) { + ExpressionList parameters = function.getParameters(); + if (parameters != null) { + parameters.getExpressions().forEach(expression -> { + if (expression instanceof SubSelect) { + processSelectBody(((SubSelect) expression).getSelectBody()); + } else if (expression instanceof Function) { + processFunction((Function) expression); + } + }); + } + } + + /** + * 处理子查询等 + */ + protected void processOtherFromItem(FromItem fromItem) { + // 去除括号 + while (fromItem instanceof ParenthesisFromItem) { + fromItem = ((ParenthesisFromItem) fromItem).getFromItem(); + } + + if (fromItem instanceof SubSelect) { + SubSelect subSelect = (SubSelect) fromItem; + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } else if (fromItem instanceof ValuesList) { + logger.debug("Perform a subQuery, if you do not give us feedback"); + } else if (fromItem instanceof LateralSubSelect) { + LateralSubSelect lateralSubSelect = (LateralSubSelect) fromItem; + if (lateralSubSelect.getSubSelect() != null) { + SubSelect subSelect = lateralSubSelect.getSubSelect(); + if (subSelect.getSelectBody() != null) { + processSelectBody(subSelect.getSelectBody()); + } + } + } + } + + /** + * 处理 sub join + * + * @param subJoin subJoin + * @return Table subJoin 中的主表 + */ + private List
processSubJoin(SubJoin subJoin) { + List
mainTables = new ArrayList<>(); + if (subJoin.getJoinList() != null) { + List
list = processFromItem(subJoin.getLeft()); + mainTables.addAll(list); + mainTables = processJoins(mainTables, subJoin.getJoinList()); + } + return mainTables; + } + + /** + * 处理 joins + * + * @param mainTables 可以为 null + * @param joins join 集合 + * @return List
右连接查询的 Table 列表 + */ + private List
processJoins(List
mainTables, List joins) { + // join 表达式中最终的主表 + Table mainTable = null; + // 当前 join 的左表 + Table leftTable = null; + + if (mainTables == null) { + mainTables = new ArrayList<>(); + } else if (mainTables.size() == 1) { + mainTable = mainTables.get(0); + leftTable = mainTable; + } + + //对于 on 表达式写在最后的 join,需要记录下前面多个 on 的表名 + Deque> onTableDeque = new LinkedList<>(); + for (Join join : joins) { + // 处理 on 表达式 + FromItem joinItem = join.getRightItem(); + + // 获取当前 join 的表,subJoint 可以看作是一张表 + List
joinTables = null; + if (joinItem instanceof Table) { + joinTables = new ArrayList<>(); + joinTables.add((Table) joinItem); + } else if (joinItem instanceof SubJoin) { + joinTables = processSubJoin((SubJoin) joinItem); + } + + if (joinTables != null) { + + // 如果是隐式内连接 + if (join.isSimple()) { + mainTables.addAll(joinTables); + continue; + } + + // 当前表是否忽略 + Table joinTable = joinTables.get(0); + + List
onTables = null; + // 如果不要忽略,且是右连接,则记录下当前表 + if (join.isRight()) { + mainTable = joinTable; + if (leftTable != null) { + onTables = Collections.singletonList(leftTable); + } + } else if (join.isLeft()) { + onTables = Collections.singletonList(joinTable); + } else if (join.isInner()) { + if (mainTable == null) { + onTables = Collections.singletonList(joinTable); + } else { + onTables = Arrays.asList(mainTable, joinTable); + } + mainTable = null; + } + + mainTables = new ArrayList<>(); + if (mainTable != null) { + mainTables.add(mainTable); + } + + // 获取 join 尾缀的 on 表达式列表 + Collection originOnExpressions = join.getOnExpressions(); + // 正常 join on 表达式只有一个,立刻处理 + if (originOnExpressions.size() == 1 && onTables != null) { + List onExpressions = new LinkedList<>(); + onExpressions.add(builderExpression(originOnExpressions.iterator().next(), onTables)); + join.setOnExpressions(onExpressions); + leftTable = joinTable; + continue; + } + // 表名压栈,忽略的表压入 null,以便后续不处理 + onTableDeque.push(onTables); + // 尾缀多个 on 表达式的时候统一处理 + if (originOnExpressions.size() > 1) { + Collection onExpressions = new LinkedList<>(); + for (Expression originOnExpression : originOnExpressions) { + List
currentTableList = onTableDeque.poll(); + if (CollectionUtils.isEmpty(currentTableList)) { + onExpressions.add(originOnExpression); + } else { + onExpressions.add(builderExpression(originOnExpression, currentTableList)); + } + } + join.setOnExpressions(onExpressions); + } + leftTable = joinTable; + } else { + processOtherFromItem(joinItem); + leftTable = null; + } + } + + return mainTables; + } + + // ========== 和 TenantLineInnerInterceptor 存在差异的逻辑:关键,实现权限条件的拼接 ========== + + /** + * 处理条件 + * + * @param currentExpression 当前 where 条件 + * @param table 单个表 + */ + protected Expression builderExpression(Expression currentExpression, Table table) { + return this.builderExpression(currentExpression, Collections.singletonList(table)); + } + + /** + * 处理条件 + * + * @param currentExpression 当前 where 条件 + * @param tables 多个表 + */ + protected Expression builderExpression(Expression currentExpression, List
tables) { + // 没有表需要处理直接返回 + if (CollectionUtils.isEmpty(tables)) { + return currentExpression; + } + + // 第一步,获得 Table 对应的数据权限条件 + Expression dataPermissionExpression = null; + for (Table table : tables) { + // 构建每个表的权限 Expression 条件 + Expression expression = buildDataPermissionExpression(table); + if (expression == null) { + continue; + } + // 合并到 dataPermissionExpression 中 + dataPermissionExpression = dataPermissionExpression == null ? expression + : new AndExpression(dataPermissionExpression, expression); + } + + // 第二步,合并多个 Expression 条件 + if (dataPermissionExpression == null) { + return currentExpression; + } + if (currentExpression == null) { + return dataPermissionExpression; + } + // ① 如果表达式为 Or,则需要 (currentExpression) AND dataPermissionExpression + if (currentExpression instanceof OrExpression) { + return new AndExpression(new Parenthesis(currentExpression), dataPermissionExpression); + } + // ② 如果表达式为 And,则直接返回 where AND dataPermissionExpression + return new AndExpression(currentExpression, dataPermissionExpression); + } + + /** + * 构建指定表的数据权限的 Expression 过滤条件 + * + * @param table 表 + * @return Expression 过滤条件 + */ + private Expression buildDataPermissionExpression(Table table) { + // 生成条件 + Expression allExpression = null; + for (DataPermissionRule rule : ContextHolder.getRules()) { + // 判断表名是否匹配 + if (!rule.getTableNames().contains(table.getName())) { + continue; + } + // 如果有匹配的规则,说明可重写。 + // 为什么不是有 allExpression 非空才重写呢?在生成 column = value 过滤条件时,会因为 value 不存在,导致未重写。 + // 这样导致第一次无 value,被标记成无需重写;但是第二次有 value,此时会需要重写。 + ContextHolder.setRewrite(true); + + // 单条规则的条件 + String tableName = MyBatisUtils.getTableName(table); + Expression oneExpress = rule.getExpression(tableName, table.getAlias()); + // 拼接到 allExpression 中 + allExpression = allExpression == null ? oneExpress + : new AndExpression(allExpression, oneExpress); + } + + return allExpression; + } + + /** + * 判断 SQL 是否重写。如果没有重写,则添加到 {@link MappedStatementCache} 中 + * + * @param ms MappedStatement + */ + private void addMappedStatementCache(MappedStatement ms) { + if (ContextHolder.getRewrite()) { + return; + } + // 无重写,进行添加 + mappedStatementCache.addNoRewritable(ms, ContextHolder.getRules()); + } + + /** + * SQL 解析上下文,方便透传 {@link DataPermissionRule} 规则 + * + * @author 芋道源码 + */ + static final class ContextHolder { + + /** + * 该 {@link MappedStatement} 对应的规则 + */ + private static final ThreadLocal> RULES = ThreadLocal.withInitial(Collections::emptyList); + /** + * SQL 是否进行重写 + */ + private static final ThreadLocal REWRITE = ThreadLocal.withInitial(() -> Boolean.FALSE); + + public static void init(List rules) { + RULES.set(rules); + REWRITE.set(false); + } + + public static void clear() { + RULES.remove(); + REWRITE.remove(); + } + + public static boolean getRewrite() { + return REWRITE.get(); + } + + public static void setRewrite(boolean rewrite) { + REWRITE.set(rewrite); + } + + public static List getRules() { + return RULES.get(); + } + + } + + /** + * {@link MappedStatement} 缓存 + * 目前主要用于,记录 {@link DataPermissionRule} 是否对指定 {@link MappedStatement} 无效 + * 如果无效,则可以避免 SQL 的解析,加快速度 + * + * @author 芋道源码 + */ + static final class MappedStatementCache { + + /** + * 指定数据权限规则,对指定 MappedStatement 无需重写(不生效)的缓存 + * + * value:{@link MappedStatement#getId()} 编号 + */ + @Getter + private final Map, Set> noRewritableMappedStatements = new ConcurrentHashMap<>(); + + /** + * 判断是否无需重写 + * ps:虽然有点中文式英语,但是容易读懂即可 + * + * @param ms MappedStatement + * @param rules 数据权限规则数组 + * @return 是否无需重写 + */ + public boolean noRewritable(MappedStatement ms, List rules) { + // 如果规则为空,说明无需重写 + if (CollUtil.isEmpty(rules)) { + return true; + } + // 任一规则不在 noRewritableMap 中,则说明可能需要重写 + for (DataPermissionRule rule : rules) { + Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); + if (!CollUtil.contains(mappedStatementIds, ms.getId())) { + return false; + } + } + return true; + } + + /** + * 添加无需重写的 MappedStatement + * + * @param ms MappedStatement + * @param rules 数据权限规则数组 + */ + public void addNoRewritable(MappedStatement ms, List rules) { + for (DataPermissionRule rule : rules) { + Set mappedStatementIds = noRewritableMappedStatements.get(rule.getClass()); + if (CollUtil.isNotEmpty(mappedStatementIds)) { + mappedStatementIds.add(ms.getId()); + } else { + noRewritableMappedStatements.put(rule.getClass(), SetUtils.asSet(ms.getId())); + } + } + } + + /** + * 清空缓存 + * 目前主要提供给单元测试 + */ + public void clear() { + noRewritableMappedStatements.clear(); + } + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRule.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRule.java new file mode 100644 index 0000000..533a56f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRule.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.datapermission.core.rule; + +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; + +import java.util.Set; + +/** + * 数据权限规则接口 + * 通过实现接口,自定义数据规则。例如说, + * + * @author 芋道源码 + */ +public interface DataPermissionRule { + + /** + * 返回需要生效的表名数组 + * 为什么需要该方法?Data Permission 数组基于 SQL 重写,通过 Where 返回只有权限的数据 + * + * 如果需要基于实体名获得表名,可调用 {@link TableInfoHelper#getTableInfo(Class)} 获得 + * + * @return 表名数组 + */ + Set getTableNames(); + + /** + * 根据表名和别名,生成对应的 WHERE / OR 过滤条件 + * + * @param tableName 表名 + * @param tableAlias 别名,可能为空 + * @return 过滤条件 Expression 表达式 + */ + Expression getExpression(String tableName, Alias tableAlias); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactory.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactory.java new file mode 100644 index 0000000..ea67019 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactory.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.datapermission.core.rule; + +import java.util.List; + +/** + * {@link DataPermissionRule} 工厂接口 + * 作为 {@link DataPermissionRule} 的容器,提供管理能力 + * + * @author 芋道源码 + */ +public interface DataPermissionRuleFactory { + + /** + * 获得所有数据权限规则数组 + * + * @return 数据权限规则数组 + */ + List getDataPermissionRules(); + + /** + * 获得指定 Mapper 的数据权限规则数组 + * + * @param mappedStatementId 指定 Mapper 的编号 + * @return 数据权限规则数组 + */ + List getDataPermissionRule(String mappedStatementId); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java new file mode 100644 index 0000000..691d225 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactoryImpl.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.framework.datapermission.core.rule; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ArrayUtil; +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.framework.datapermission.core.aop.DataPermissionContextHolder; +import lombok.RequiredArgsConstructor; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 默认的 DataPermissionRuleFactoryImpl 实现类 + * 支持通过 {@link DataPermissionContextHolder} 过滤数据权限 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class DataPermissionRuleFactoryImpl implements DataPermissionRuleFactory { + + /** + * 数据权限规则数组 + */ + private final List rules; + + @Override + public List getDataPermissionRules() { + return rules; + } + + @Override // mappedStatementId 参数,暂时没有用。以后,可以基于 mappedStatementId + DataPermission 进行缓存 + public List getDataPermissionRule(String mappedStatementId) { + // 1. 无数据权限 + if (CollUtil.isEmpty(rules)) { + return Collections.emptyList(); + } + // 2. 未配置,则默认开启 + DataPermission dataPermission = DataPermissionContextHolder.get(); + if (dataPermission == null) { + return rules; + } + // 3. 已配置,但禁用 + if (!dataPermission.enable()) { + return Collections.emptyList(); + } + + // 4. 已配置,只选择部分规则 + if (ArrayUtil.isNotEmpty(dataPermission.includeRules())) { + return rules.stream().filter(rule -> ArrayUtil.contains(dataPermission.includeRules(), rule.getClass())) + .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 + } + // 5. 已配置,只排除部分规则 + if (ArrayUtil.isNotEmpty(dataPermission.excludeRules())) { + return rules.stream().filter(rule -> !ArrayUtil.contains(dataPermission.excludeRules(), rule.getClass())) + .collect(Collectors.toList()); // 一般规则不会太多,所以不采用 HashSet 查询 + } + // 6. 已配置,全部规则 + return rules; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java new file mode 100644 index 0000000..0d76227 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRule.java @@ -0,0 +1,208 @@ +package com.yunxi.scm.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRule; +import com.yunxi.scm.framework.expression.OrExpressionX; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.util.MyBatisUtils; +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.module.system.api.permission.PermissionApi; +import com.yunxi.scm.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.NullValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * 基于部门的 {@link DataPermissionRule} 数据权限规则实现 + * + * 注意,使用 DeptDataPermissionRule 时,需要保证表中有 dept_id 部门编号的字段,可自定义。 + * + * 实际业务场景下,会存在一个经典的问题?当用户修改部门时,冗余的 dept_id 是否需要修改? + * 1. 一般情况下,dept_id 不进行修改,则会导致用户看不到之前的数据。【yunxi-server 采用该方案】 + * 2. 部分情况下,希望该用户还是能看到之前的数据,则有两种方式解决:【需要你改造该 DeptDataPermissionRule 的实现代码】 + * 1)编写洗数据的脚本,将 dept_id 修改成新部门的编号;【建议】 + * 最终过滤条件是 WHERE dept_id = ? + * 2)洗数据的话,可能涉及的数据量较大,也可以采用 user_id 进行过滤的方式,此时需要获取到 dept_id 对应的所有 user_id 用户编号; + * 最终过滤条件是 WHERE user_id IN (?, ?, ? ...) + * 3)想要保证原 dept_id 和 user_id 都可以看的到,此时使用 dept_id 和 user_id 一起过滤; + * 最终过滤条件是 WHERE dept_id = ? OR user_id IN (?, ?, ? ...) + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Slf4j +public class DeptDataPermissionRule implements DataPermissionRule { + + /** + * LoginUser 的 Context 缓存 Key + */ + protected static final String CONTEXT_KEY = DeptDataPermissionRule.class.getSimpleName(); + + private static final String DEPT_COLUMN_NAME = "dept_id"; + private static final String USER_COLUMN_NAME = "user_id"; + + static final Expression EXPRESSION_NULL = new NullValue(); + + private final PermissionApi permissionApi; + + /** + * 基于部门的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map deptColumns = new HashMap<>(); + /** + * 基于用户的表字段配置 + * 一般情况下,每个表的部门编号字段是 dept_id,通过该配置自定义。 + * + * key:表名 + * value:字段名 + */ + private final Map userColumns = new HashMap<>(); + /** + * 所有表名,是 {@link #deptColumns} 和 {@link #userColumns} 的合集 + */ + private final Set TABLE_NAMES = new HashSet<>(); + + @Override + public Set getTableNames() { + return TABLE_NAMES; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + // 只有有登陆用户的情况下,才进行数据权限的处理 + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + if (loginUser == null) { + return null; + } + // 只有管理员类型的用户,才进行数据权限的处理 + if (ObjectUtil.notEqual(loginUser.getUserType(), UserTypeEnum.ADMIN.getValue())) { + return null; + } + + // 获得数据权限 + DeptDataPermissionRespDTO deptDataPermission = loginUser.getContext(CONTEXT_KEY, DeptDataPermissionRespDTO.class); + // 从上下文中拿不到,则调用逻辑进行获取 + if (deptDataPermission == null) { + deptDataPermission = permissionApi.getDeptDataPermission(loginUser.getId()); + if (deptDataPermission == null) { + log.error("[getExpression][LoginUser({}) 获取数据权限为 null]", JsonUtils.toJsonString(loginUser)); + throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 未返回数据权限", + loginUser.getId(), tableName, tableAlias.getName())); + } + // 添加到上下文中,避免重复计算 + loginUser.setContext(CONTEXT_KEY, deptDataPermission); + } + + // 情况一,如果是 ALL 可查看全部,则无需拼接条件 + if (deptDataPermission.getAll()) { + return null; + } + + // 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限 + if (CollUtil.isEmpty(deptDataPermission.getDeptIds()) + && Boolean.FALSE.equals(deptDataPermission.getSelf())) { + return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空 + } + + // 情况三,拼接 Dept 和 User 的条件,最后组合 + Expression deptExpression = buildDeptExpression(tableName,tableAlias, deptDataPermission.getDeptIds()); + Expression userExpression = buildUserExpression(tableName, tableAlias, deptDataPermission.getSelf(), loginUser.getId()); + if (deptExpression == null && userExpression == null) { + // TODO 芋艿:获得不到条件的时候,暂时不抛出异常,而是不返回数据 + log.warn("[getExpression][LoginUser({}) Table({}/{}) DeptDataPermission({}) 构建的条件为空]", + JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission)); +// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空", +// loginUser.getId(), tableName, tableAlias.getName())); + return EXPRESSION_NULL; + } + if (deptExpression == null) { + return userExpression; + } + if (userExpression == null) { + return deptExpression; + } + // 目前,如果有指定部门 + 可查看自己,采用 OR 条件。即,WHERE (dept_id IN ? OR user_id = ?) + return new OrExpressionX(deptExpression, userExpression); + } + + private Expression buildDeptExpression(String tableName, Alias tableAlias, Set deptIds) { + // 如果不存在配置,则无需作为条件 + String columnName = deptColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 如果为空,则无条件 + if (CollUtil.isEmpty(deptIds)) { + return null; + } + // 拼接条件 + return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), + new ExpressionList(CollectionUtils.convertList(deptIds, LongValue::new))); + } + + private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { + // 如果不查看自己,则无需作为条件 + if (Boolean.FALSE.equals(self)) { + return null; + } + String columnName = userColumns.get(tableName); + if (StrUtil.isEmpty(columnName)) { + return null; + } + // 拼接条件 + return new EqualsTo(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), new LongValue(userId)); + } + + // ==================== 添加配置 ==================== + + public void addDeptColumn(Class entityClass) { + addDeptColumn(entityClass, DEPT_COLUMN_NAME); + } + + public void addDeptColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addDeptColumn(tableName, columnName); + } + + public void addDeptColumn(String tableName, String columnName) { + deptColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + + public void addUserColumn(Class entityClass) { + addUserColumn(entityClass, USER_COLUMN_NAME); + } + + public void addUserColumn(Class entityClass, String columnName) { + String tableName = TableInfoHelper.getTableInfo(entityClass).getTableName(); + addUserColumn(tableName, columnName); + } + + public void addUserColumn(String tableName, String columnName) { + userColumns.put(tableName, columnName); + TABLE_NAMES.add(tableName); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java new file mode 100644 index 0000000..6aa5eb8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRuleCustomizer.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.framework.datapermission.core.rule.dept; + +/** + * {@link DeptDataPermissionRule} 的自定义配置接口 + * + * @author 芋道源码 + */ +@FunctionalInterface +public interface DeptDataPermissionRuleCustomizer { + + /** + * 自定义该权限规则 + * 1. 调用 {@link DeptDataPermissionRule#addDeptColumn(Class, String)} 方法,配置基于 dept_id 的过滤规则 + * 2. 调用 {@link DeptDataPermissionRule#addUserColumn(Class, String)} 方法,配置基于 user_id 的过滤规则 + * + * @param rule 权限规则 + */ + void customize(DeptDataPermissionRule rule); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/package-info.java new file mode 100644 index 0000000..2b5f4e0 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/rule/dept/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于部门的数据权限规则 + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.datapermission.core.rule.dept; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/util/DataPermissionUtils.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/util/DataPermissionUtils.java new file mode 100644 index 0000000..df83f47 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/core/util/DataPermissionUtils.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.datapermission.core.util; + +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.framework.datapermission.core.aop.DataPermissionContextHolder; +import lombok.SneakyThrows; + +/** + * 数据权限 Util + * + * @author 芋道源码 + */ +public class DataPermissionUtils { + + private static DataPermission DATA_PERMISSION_DISABLE; + + @DataPermission(enable = false) + @SneakyThrows + private static DataPermission getDisableDataPermissionDisable() { + if (DATA_PERMISSION_DISABLE == null) { + DATA_PERMISSION_DISABLE = DataPermissionUtils.class + .getDeclaredMethod("getDisableDataPermissionDisable") + .getAnnotation(DataPermission.class); + } + return DATA_PERMISSION_DISABLE; + } + + /** + * 忽略数据权限,执行对应的逻辑 + * + * @param runnable 逻辑 + */ + public static void executeIgnore(Runnable runnable) { + DataPermission dataPermission = getDisableDataPermissionDisable(); + DataPermissionContextHolder.add(dataPermission); + try { + // 执行 runnable + runnable.run(); + } finally { + DataPermissionContextHolder.remove(); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/package-info.java new file mode 100644 index 0000000..520f47f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/java/com/yunxi/scm/framework/datapermission/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 JSqlParser 解析 SQL,增加数据权限的 WHERE 条件 + */ +package com.yunxi.scm.framework.datapermission; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..8c066de --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.yunxi.scm.framework.datapermission.config.YunxiDataPermissionAutoConfiguration +com.yunxi.scm.framework.datapermission.config.YunxiDeptDataPermissionAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java new file mode 100644 index 0000000..7c1f3b4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionAnnotationInterceptorTest.java @@ -0,0 +1,108 @@ +package com.yunxi.scm.framework.datapermission.core.aop; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import org.aopalliance.intercept.MethodInvocation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.lang.reflect.Method; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +/** + * {@link DataPermissionAnnotationInterceptor} 的单元测试 + * + * @author 芋道源码 + */ +public class DataPermissionAnnotationInterceptorTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionAnnotationInterceptor interceptor; + + @Mock + private MethodInvocation methodInvocation; + + @BeforeEach + public void setUp() { + interceptor.getDataPermissionCache().clear(); + } + + @Test // 无 @DataPermission 注解 + public void testInvoke_none() throws Throwable { + // 参数 + mockMethodInvocation(TestNone.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("none", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertTrue(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + @Test // 在 Method 上有 @DataPermission 注解 + public void testInvoke_method() throws Throwable { + // 参数 + mockMethodInvocation(TestMethod.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("method", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + @Test // 在 Class 上有 @DataPermission 注解 + public void testInvoke_class() throws Throwable { + // 参数 + mockMethodInvocation(TestClass.class); + + // 调用 + Object result = interceptor.invoke(methodInvocation); + // 断言 + assertEquals("class", result); + assertEquals(1, interceptor.getDataPermissionCache().size()); + assertFalse(CollUtil.getFirst(interceptor.getDataPermissionCache().values()).enable()); + } + + private void mockMethodInvocation(Class clazz) throws Throwable { + Object targetObject = clazz.newInstance(); + Method method = targetObject.getClass().getMethod("echo"); + when(methodInvocation.getThis()).thenReturn(targetObject); + when(methodInvocation.getMethod()).thenReturn(method); + when(methodInvocation.proceed()).then(invocationOnMock -> method.invoke(targetObject)); + } + + static class TestMethod { + + @DataPermission(enable = false) + public String echo() { + return "method"; + } + + } + + @DataPermission(enable = false) + static class TestClass { + + public String echo() { + return "class"; + } + + } + + static class TestNone { + + public String echo() { + return "none"; + } + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionContextHolderTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionContextHolderTest.java new file mode 100644 index 0000000..981e4f9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/aop/DataPermissionContextHolderTest.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.framework.datapermission.core.aop; + +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.Mockito.mock; + +/** + * {@link DataPermissionContextHolder} 的单元测试 + * + * @author 芋道源码 + */ +class DataPermissionContextHolderTest { + + @BeforeEach + public void setUp() { + DataPermissionContextHolder.clear(); + } + + @Test + public void testGet() { + // mock 方法 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + + // 调用 + DataPermission result = DataPermissionContextHolder.get(); + // 断言 + assertSame(result, dataPermission02); + } + + @Test + public void testPush() { + // 调用 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + // 断言 + DataPermission first = DataPermissionContextHolder.getAll().get(0); + DataPermission second = DataPermissionContextHolder.getAll().get(1); + assertSame(dataPermission01, first); + assertSame(dataPermission02, second); + } + + @Test + public void testRemove() { + // mock 方法 + DataPermission dataPermission01 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission01); + DataPermission dataPermission02 = mock(DataPermission.class); + DataPermissionContextHolder.add(dataPermission02); + + // 调用 + DataPermission result = DataPermissionContextHolder.remove(); + // 断言 + assertSame(result, dataPermission02); + assertEquals(1, DataPermissionContextHolder.getAll().size()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java new file mode 100644 index 0000000..ba294d8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest.java @@ -0,0 +1,190 @@ +package com.yunxi.scm.framework.datapermission.core.db; + +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRule; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRuleFactory; +import com.yunxi.scm.framework.mybatis.core.util.MyBatisUtils; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.baomidou.mybatisplus.core.toolkit.PluginUtils; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.schema.Column; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.sql.Connection; +import java.util.*; + +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * {@link DataPermissionDatabaseInterceptor} 的单元测试 + * 主要测试 {@link DataPermissionDatabaseInterceptor#beforePrepare(StatementHandler, Connection, Integer)} + * 和 {@link DataPermissionDatabaseInterceptor#beforeUpdate(Executor, MappedStatement, Object)} + * 以及在这个过程中,ContextHolder 和 MappedStatementCache + * + * @author 芋道源码 + */ +public class DataPermissionDatabaseInterceptorTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionDatabaseInterceptor interceptor; + + @Mock + private DataPermissionRuleFactory ruleFactory; + + @BeforeEach + public void setUp() { + // 清理上下文 + DataPermissionDatabaseInterceptor.ContextHolder.clear(); + // 清空缓存 + interceptor.getMappedStatementCache().clear(); + } + + @Test // 不存在规则,且不匹配 + public void testBeforeQuery_withoutRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + pluginUtilsMock.verify(() -> PluginUtils.mpBoundSql(boundSql), never()); + } + } + + @Test // 存在规则,且不匹配 + public void testBeforeQuery_withMatchRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + // mock 方法(数据权限) + when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) + .thenReturn(singletonList(new DeptDataPermissionRule())); + // mock 方法(MPBoundSql) + PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); + pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); + // mock 方法(SQL) + String sql = "select * from t_user where id = 1"; + when(mpBs.sql()).thenReturn(sql); + // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + verify(mpBs, times(1)).sql( + eq("SELECT * FROM t_user WHERE id = 1 AND t_user.dept_id = 100")); + // 断言缓存 + assertTrue(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); + } + } + + @Test // 存在规则,但不匹配 + public void testBeforeQuery_withoutMatchRule() { + try (MockedStatic pluginUtilsMock = mockStatic(PluginUtils.class)) { + // 准备参数 + MappedStatement mappedStatement = mock(MappedStatement.class); + BoundSql boundSql = mock(BoundSql.class); + // mock 方法(数据权限) + when(ruleFactory.getDataPermissionRule(same(mappedStatement.getId()))) + .thenReturn(singletonList(new DeptDataPermissionRule())); + // mock 方法(MPBoundSql) + PluginUtils.MPBoundSql mpBs = mock(PluginUtils.MPBoundSql.class); + pluginUtilsMock.when(() -> PluginUtils.mpBoundSql(same(boundSql))).thenReturn(mpBs); + // mock 方法(SQL) + String sql = "select * from t_role where id = 1"; + when(mpBs.sql()).thenReturn(sql); + // 针对 ContextHolder 和 MappedStatementCache 暂时不 mock,主要想校验过程中,数据是否正确 + + // 调用 + interceptor.beforeQuery(null, mappedStatement, null, null, null, boundSql); + // 断言 + verify(mpBs, times(1)).sql( + eq("SELECT * FROM t_role WHERE id = 1")); + // 断言缓存 + assertFalse(interceptor.getMappedStatementCache().getNoRewritableMappedStatements().isEmpty()); + } + } + + @Test + public void testAddNoRewritable() { + // 准备参数 + MappedStatement ms = mock(MappedStatement.class); + List rules = singletonList(new DeptDataPermissionRule()); + // mock 方法 + when(ms.getId()).thenReturn("selectById"); + + // 调用 + interceptor.getMappedStatementCache().addNoRewritable(ms, rules); + // 断言 + Map, Set> noRewritableMappedStatements = + interceptor.getMappedStatementCache().getNoRewritableMappedStatements(); + assertEquals(1, noRewritableMappedStatements.size()); + assertEquals(SetUtils.asSet("selectById"), noRewritableMappedStatements.get(DeptDataPermissionRule.class)); + } + + @Test + public void testNoRewritable() { + // 准备参数 + MappedStatement ms = mock(MappedStatement.class); + // mock 方法 + when(ms.getId()).thenReturn("selectById"); + // mock 数据 + List rules = singletonList(new DeptDataPermissionRule()); + interceptor.getMappedStatementCache().addNoRewritable(ms, rules); + + // 场景一,rules 为空 + assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, null)); + // 场景二,rules 非空,可重写 + assertFalse(interceptor.getMappedStatementCache().noRewritable(ms, singletonList(new EmptyDataPermissionRule()))); + // 场景三,rule 非空,不可重写 + assertTrue(interceptor.getMappedStatementCache().noRewritable(ms, rules)); + } + + private static class DeptDataPermissionRule implements DataPermissionRule { + + private static final String COLUMN = "dept_id"; + + @Override + public Set getTableNames() { + return SetUtils.asSet("t_user"); + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + LongValue value = new LongValue(100L); + return new EqualsTo(column, value); + } + + } + + private static class EmptyDataPermissionRule implements DataPermissionRule { + + @Override + public Set getTableNames() { + return Collections.emptySet(); + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java new file mode 100644 index 0000000..21ed1a1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/db/DataPermissionDatabaseInterceptorTest2.java @@ -0,0 +1,533 @@ +package com.yunxi.scm.framework.datapermission.core.db; + +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRule; +import com.yunxi.scm.framework.datapermission.core.rule.DataPermissionRuleFactory; +import com.yunxi.scm.framework.mybatis.core.util.MyBatisUtils; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.operators.relational.EqualsTo; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.expression.operators.relational.InExpression; +import net.sf.jsqlparser.schema.Column; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Arrays; +import java.util.Set; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link DataPermissionDatabaseInterceptor} 的单元测试 + * 主要复用了 MyBatis Plus 的 TenantLineInnerInterceptorTest 的单元测试 + * 不过它的单元测试不是很规范,考虑到是复用的,所以暂时不进行修改~ + * + * @author 芋道源码 + */ +public class DataPermissionDatabaseInterceptorTest2 extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionDatabaseInterceptor interceptor; + + @Mock + private DataPermissionRuleFactory ruleFactory; + + @BeforeEach + public void setUp() { + // 租户的数据权限规则 + DataPermissionRule tenantRule = new DataPermissionRule() { + + private static final String COLUMN = "tenant_id"; + + @Override + public Set getTableNames() { + return asSet("entity", "entity1", "entity2", "entity3", "t1", "t2", "sys_dict_item", // 支持 MyBatis Plus 的单元测试 + "t_user", "t_role"); // 满足自己的单元测试 + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + LongValue value = new LongValue(1L); + return new EqualsTo(column, value); + } + + }; + // 部门的数据权限规则 + DataPermissionRule deptRule = new DataPermissionRule() { + + private static final String COLUMN = "dept_id"; + + @Override + public Set getTableNames() { + return asSet("t_user"); // 满足自己的单元测试 + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); + ExpressionList values = new ExpressionList(new LongValue(10L), + new LongValue(20L)); + return new InExpression(column, values); + } + + }; + // 设置到上下文,保证 + DataPermissionDatabaseInterceptor.ContextHolder.init(Arrays.asList(tenantRule, deptRule)); + } + + @Test + void delete() { + assertSql("delete from entity where id = ?", + "DELETE FROM entity WHERE id = ? AND entity.tenant_id = 1"); + } + + @Test + void update() { + assertSql("update entity set name = ? where id = ?", + "UPDATE entity SET name = ? WHERE id = ? AND entity.tenant_id = 1"); + } + + @Test + void selectSingle() { + // 单表 + assertSql("select * from entity where id = ?", + "SELECT * FROM entity WHERE id = ? AND entity.tenant_id = 1"); + + assertSql("select * from entity where id = ? or name = ?", + "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1"); + + assertSql("SELECT * FROM entity WHERE (id = ? OR name = ?)", + "SELECT * FROM entity WHERE (id = ? OR name = ?) AND entity.tenant_id = 1"); + + /* not */ + assertSql("SELECT * FROM entity WHERE not (id = ? OR name = ?)", + "SELECT * FROM entity WHERE NOT (id = ? OR name = ?) AND entity.tenant_id = 1"); + } + + @Test + void selectSubSelectIn() { + /* in */ + assertSql("SELECT * FROM entity e WHERE e.id IN (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id IN (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + // 在最前 + assertSql("SELECT * FROM entity e WHERE e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", + "SELECT * FROM entity e WHERE e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); + // 在最后 + assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + // 在中间 + assertSql("SELECT * FROM entity e WHERE e.id = ? and e.id IN " + + "(select e1.id from entity1 e1 where e1.id = ?) and e.id = ?", + "SELECT * FROM entity e WHERE e.id = ? AND e.id IN " + + "(SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ? AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectEq() { + /* = */ + assertSql("SELECT * FROM entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectInnerNotEq() { + /* inner not = */ + assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?))", + "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1)) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e WHERE not (e.id = (select e1.id from entity1 e1 where e1.id = ?) and e.id = ?)", + "SELECT * FROM entity e WHERE NOT (e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.id = ?) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelectExists() { + /* EXISTS */ + assertSql("SELECT * FROM entity e WHERE EXISTS (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + + /* NOT EXISTS */ + assertSql("SELECT * FROM entity e WHERE NOT EXISTS (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE NOT EXISTS (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectSubSelect() { + /* >= */ + assertSql("SELECT * FROM entity e WHERE e.id >= (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id >= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + + /* <= */ + assertSql("SELECT * FROM entity e WHERE e.id <= (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id <= (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + + + /* <> */ + assertSql("SELECT * FROM entity e WHERE e.id <> (select e1.id from entity1 e1 where e1.id = ?)", + "SELECT * FROM entity e WHERE e.id <> (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1"); + } + + @Test + void selectFromSelect() { + assertSql("SELECT * FROM (select e.id from entity e WHERE e.id = (select e1.id from entity1 e1 where e1.id = ?))", + "SELECT * FROM (SELECT e.id FROM entity e WHERE e.id = (SELECT e1.id FROM entity1 e1 WHERE e1.id = ? AND e1.tenant_id = 1) AND e.tenant_id = 1)"); + } + + @Test + void selectBodySubSelect() { + assertSql("select t1.col1,(select t2.col2 from t2 t2 where t1.col1=t2.col1) from t1 t1", + "SELECT t1.col1, (SELECT t2.col2 FROM t2 t2 WHERE t1.col1 = t2.col1 AND t2.tenant_id = 1) FROM t1 t1 WHERE t1.tenant_id = 1"); + } + + @Test + void selectLeftJoin() { + // left join + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "left join entity2 e2 on e1.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " + + "WHERE e.tenant_id = 1"); + } + + @Test + void selectRightJoin() { + // right join + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + + "WHERE e1.tenant_id = 1"); + + assertSql("SELECT * FROM with_as_1 e " + + "right join entity1 e1 on e1.id = e.id", + "SELECT * FROM with_as_1 e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id " + + "WHERE e1.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id " + + "right join entity2 e2 on e1.id = e2.id ", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + + "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " + + "WHERE e2.tenant_id = 1"); + } + + @Test + void selectMixJoin() { + assertSql("SELECT * FROM entity e " + + "right join entity1 e1 on e1.id = e.id " + + "left join entity2 e2 on e1.id = e2.id", + "SELECT * FROM entity e " + + "RIGHT JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 " + + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1 " + + "WHERE e1.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "right join entity2 e2 on e1.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1 " + + "WHERE e2.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "left join entity1 e1 on e1.id = e.id " + + "inner join entity2 e2 on e1.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "INNER JOIN entity2 e2 ON e1.id = e2.id AND e.tenant_id = 1 AND e2.tenant_id = 1"); + } + + + @Test + void selectJoinSubSelect() { + assertSql("select * from (select * from entity) e1 " + + "left join entity2 e2 on e1.id = e2.id", + "SELECT * FROM (SELECT * FROM entity WHERE entity.tenant_id = 1) e1 " + + "LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1"); + + assertSql("select * from entity1 e1 " + + "left join (select * from entity2) e2 " + + "on e1.id = e2.id", + "SELECT * FROM entity1 e1 " + + "LEFT JOIN (SELECT * FROM entity2 WHERE entity2.tenant_id = 1) e2 " + + "ON e1.id = e2.id " + + "WHERE e1.tenant_id = 1"); + } + + @Test + void selectSubJoin() { + + assertSql("select * FROM " + + "(entity1 e1 right JOIN entity2 e2 ON e1.id = e2.id)", + "SELECT * FROM " + + "(entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " + + "WHERE e2.tenant_id = 1"); + + assertSql("select * FROM " + + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id)", + "SELECT * FROM " + + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + + "WHERE e1.tenant_id = 1"); + + + assertSql("select * FROM " + + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id) " + + "right join entity3 e3 on e1.id = e3.id", + "SELECT * FROM " + + "(entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + + "RIGHT JOIN entity3 e3 ON e1.id = e3.id AND e1.tenant_id = 1 " + + "WHERE e3.tenant_id = 1"); + + + assertSql("select * FROM entity e " + + "LEFT JOIN (entity1 e1 right join entity2 e2 ON e1.id = e2.id) " + + "on e.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN (entity1 e1 RIGHT JOIN entity2 e2 ON e1.id = e2.id AND e1.tenant_id = 1) " + + "ON e.id = e2.id AND e2.tenant_id = 1 " + + "WHERE e.tenant_id = 1"); + + assertSql("select * FROM entity e " + + "LEFT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " + + "on e.id = e2.id", + "SELECT * FROM entity e " + + "LEFT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + + "ON e.id = e2.id AND e1.tenant_id = 1 " + + "WHERE e.tenant_id = 1"); + + assertSql("select * FROM entity e " + + "RIGHT JOIN (entity1 e1 left join entity2 e2 ON e1.id = e2.id) " + + "on e.id = e2.id", + "SELECT * FROM entity e " + + "RIGHT JOIN (entity1 e1 LEFT JOIN entity2 e2 ON e1.id = e2.id AND e2.tenant_id = 1) " + + "ON e.id = e2.id AND e.tenant_id = 1 " + + "WHERE e1.tenant_id = 1"); + } + + + @Test + void selectLeftJoinMultipleTrailingOn() { + // 多个 on 尾缀的 + assertSql("SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN entity2 e2 ON e2.id = e1.id " + + "ON e1.id = e.id " + + "WHERE (e.id = ? OR e.NAME = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN entity2 e2 ON e2.id = e1.id AND e2.tenant_id = 1 " + + "ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); + + assertSql("SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + + "ON e1.id = e.id " + + "WHERE (e.id = ? OR e.NAME = ?)", + "SELECT * FROM entity e " + + "LEFT JOIN entity1 e1 " + + "LEFT JOIN with_as_A e2 ON e2.id = e1.id " + + "ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.NAME = ?) AND e.tenant_id = 1"); + } + + @Test + void selectInnerJoin() { + // inner join + assertSql("SELECT * FROM entity e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM entity e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " + + "WHERE e.id = ? OR e.name = ?"); + + assertSql("SELECT * FROM entity e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM entity e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?)"); + + // 隐式内连接 + assertSql("SELECT * FROM entity,entity1 " + + "WHERE entity.id = entity1.id", + "SELECT * FROM entity, entity1 " + + "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); + + // 隐式内连接 + assertSql("SELECT * FROM entity a, with_as_entity1 b " + + "WHERE a.id = b.id", + "SELECT * FROM entity a, with_as_entity1 b " + + "WHERE a.id = b.id AND a.tenant_id = 1"); + + assertSql("SELECT * FROM with_as_entity a, with_as_entity1 b " + + "WHERE a.id = b.id", + "SELECT * FROM with_as_entity a, with_as_entity1 b " + + "WHERE a.id = b.id"); + + // SubJoin with 隐式内连接 + assertSql("SELECT * FROM (entity,entity1) " + + "WHERE entity.id = entity1.id", + "SELECT * FROM (entity, entity1) " + + "WHERE entity.id = entity1.id " + + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); + + assertSql("SELECT * FROM ((entity,entity1),entity2) " + + "WHERE entity.id = entity1.id and entity.id = entity2.id", + "SELECT * FROM ((entity, entity1), entity2) " + + "WHERE entity.id = entity1.id AND entity.id = entity2.id " + + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1"); + + assertSql("SELECT * FROM (entity,(entity1,entity2)) " + + "WHERE entity.id = entity1.id and entity.id = entity2.id", + "SELECT * FROM (entity, (entity1, entity2)) " + + "WHERE entity.id = entity1.id AND entity.id = entity2.id " + + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1 AND entity2.tenant_id = 1"); + + // 沙雕的括号写法 + assertSql("SELECT * FROM (((entity,entity1))) " + + "WHERE entity.id = entity1.id", + "SELECT * FROM (((entity, entity1))) " + + "WHERE entity.id = entity1.id " + + "AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); + + } + + + @Test + void selectWithAs() { + assertSql("with with_as_A as (select * from entity) select * from with_as_A", + "WITH with_as_A AS (SELECT * FROM entity WHERE entity.tenant_id = 1) SELECT * FROM with_as_A"); + } + + + @Test + void selectIgnoreTable() { + assertSql(" SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)", + "SELECT dict.dict_code, item.item_text AS \"text\", item.item_value AS \"value\" FROM sys_dict_item item INNER JOIN sys_dict dict ON dict.id = item.dict_id AND item.tenant_id = 1 WHERE dict.dict_code IN (1, 2, 3) AND item.item_value IN (1, 2, 3)"); + } + + private void assertSql(String sql, String targetSql) { + assertEquals(targetSql, interceptor.parserSingle(sql, null)); + } + + + // ========== 额外的测试 ========== + + @Test + public void testSelectSingle() { + // 单表 + assertSql("select * from t_user where id = ?", + "SELECT * FROM t_user WHERE id = ? AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); + + assertSql("select * from t_user where id = ? or name = ?", + "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); + + assertSql("SELECT * FROM t_user WHERE (id = ? OR name = ?)", + "SELECT * FROM t_user WHERE (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); + + /* not */ + assertSql("SELECT * FROM t_user WHERE not (id = ? OR name = ?)", + "SELECT * FROM t_user WHERE NOT (id = ? OR name = ?) AND t_user.tenant_id = 1 AND t_user.dept_id IN (10, 20)"); + } + + @Test + public void testSelectLeftJoin() { + // left join + assertSql("SELECT * FROM t_user e " + + "left join t_role e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "left join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "LEFT JOIN t_role e1 ON e1.id = e.id AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?) AND e.tenant_id = 1 AND e.dept_id IN (10, 20)"); + } + + @Test + public void testSelectRightJoin() { + // right join + assertSql("SELECT * FROM t_user e " + + "right join t_role e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " + + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "right join t_role e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "RIGHT JOIN t_role e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) " + + "WHERE (e.id = ? OR e.name = ?) AND e1.tenant_id = 1"); + } + + @Test + public void testSelectInnerJoin() { + // inner join + assertSql("SELECT * FROM t_user e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE e.id = ? OR e.name = ?", + "SELECT * FROM t_user e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " + + "WHERE e.id = ? OR e.name = ?"); + + // 条件 e.id = ? OR e.name = ? 带括号 + assertSql("SELECT * FROM t_user e " + + "inner join entity1 e1 on e1.id = e.id " + + "WHERE (e.id = ? OR e.name = ?)", + "SELECT * FROM t_user e " + + "INNER JOIN entity1 e1 ON e1.id = e.id AND e.tenant_id = 1 AND e.dept_id IN (10, 20) AND e1.tenant_id = 1 " + + "WHERE (e.id = ? OR e.name = ?)"); + + // 没有 On 的 inner join + assertSql("SELECT * FROM entity,entity1 " + + "WHERE entity.id = entity1.id", + "SELECT * FROM entity, entity1 " + + "WHERE entity.id = entity1.id AND entity.tenant_id = 1 AND entity1.tenant_id = 1"); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java new file mode 100644 index 0000000..82c9fed --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/rule/DataPermissionRuleFactoryImplTest.java @@ -0,0 +1,145 @@ +package com.yunxi.scm.framework.datapermission.core.rule; + +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.framework.datapermission.core.aop.DataPermissionContextHolder; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.springframework.core.annotation.AnnotationUtils; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DataPermissionRuleFactoryImpl} 单元测试 + * + * @author 芋道源码 + */ +class DataPermissionRuleFactoryImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private DataPermissionRuleFactoryImpl dataPermissionRuleFactory; + + @Spy + private List rules = Arrays.asList(new DataPermissionRule01(), + new DataPermissionRule02()); + + @BeforeEach + public void setUp() { + DataPermissionContextHolder.clear(); + } + + @Test + public void testGetDataPermissionRule_02() { + // 准备参数 + String mappedStatementId = randomString(); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertSame(rules, result); + } + + @Test + public void testGetDataPermissionRule_03() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass03.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertTrue(result.isEmpty()); + } + + @Test + public void testGetDataPermissionRule_04() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass04.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertEquals(1, result.size()); + assertEquals(DataPermissionRule01.class, result.get(0).getClass()); + } + + @Test + public void testGetDataPermissionRule_05() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass05.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertEquals(1, result.size()); + assertEquals(DataPermissionRule02.class, result.get(0).getClass()); + } + + @Test + public void testGetDataPermissionRule_06() { + // 准备参数 + String mappedStatementId = randomString(); + // mock 方法 + DataPermissionContextHolder.add(AnnotationUtils.findAnnotation(TestClass06.class, DataPermission.class)); + + // 调用 + List result = dataPermissionRuleFactory.getDataPermissionRule(mappedStatementId); + // 断言 + assertSame(rules, result); + } + + @DataPermission(enable = false) + static class TestClass03 {} + + @DataPermission(includeRules = DataPermissionRule01.class) + static class TestClass04 {} + + @DataPermission(excludeRules = DataPermissionRule01.class) + static class TestClass05 {} + + @DataPermission + static class TestClass06 {} + + static class DataPermissionRule01 implements DataPermissionRule { + + @Override + public Set getTableNames() { + return null; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + + static class DataPermissionRule02 implements DataPermissionRule { + + @Override + public Set getTableNames() { + return null; + } + + @Override + public Expression getExpression(String tableName, Alias tableAlias) { + return null; + } + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java new file mode 100644 index 0000000..251c564 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/rule/dept/DeptDataPermissionRuleTest.java @@ -0,0 +1,238 @@ +package com.yunxi.scm.framework.datapermission.core.rule.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ReflectUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.system.api.permission.PermissionApi; +import com.yunxi.scm.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.expression.Expression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.Map; + +import static com.yunxi.scm.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * {@link DeptDataPermissionRule} 的单元测试 + * + * @author 芋道源码 + */ +class DeptDataPermissionRuleTest extends BaseMockitoUnitTest { + + @InjectMocks + private DeptDataPermissionRule rule; + + @Mock + private PermissionApi permissionApi; + + @BeforeEach + @SuppressWarnings("unchecked") + public void setUp() { + // 清空 rule + rule.getTableNames().clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + ((Map) ReflectUtil.getFieldValue(rule, "deptColumns")).clear(); + } + + @Test // 无 LoginUser + public void testGetExpression_noLoginUser() { + // 准备参数 + String tableName = randomString(); + Alias tableAlias = new Alias(randomString()); + // mock 方法 + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + } + + @Test // 无数据权限时 + public void testGetExpression_noDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法 + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(permissionApi 返回 null) + when(permissionApi.getDeptDataPermission(eq(loginUser.getId()))).thenReturn(null); + + // 调用 + NullPointerException exception = assertThrows(NullPointerException.class, + () -> rule.getExpression(tableName, tableAlias)); + // 断言 + assertEquals("LoginUser(1) Table(t_user/u) 未返回数据权限", exception.getMessage()); + } + } + + @Test // 全部数据权限 + public void testGetExpression_allDeptDataPermission() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO().setAll(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertNull(expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 即不能查看部门,又不能查看自己,则说明 100% 无权限 + public void testGetExpression_noDept_noSelf() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO(); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("null = null", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(字段都不符合) + public void testGetExpression_noDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(SetUtils.asSet(10L, 20L)).setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertSame(EXPRESSION_NULL, expression); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(self 符合) + public void testGetExpression_noDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.id = 1", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept 符合) + public void testGetExpression_yesDeptColumn_noSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("u.dept_id IN (10, 20)", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + + @Test // 拼接 Dept 和 User 的条件(dept + self 符合) + public void testGetExpression_yesDeptColumn_yesSelfColumn() { + try (MockedStatic securityFrameworkUtilsMock + = mockStatic(SecurityFrameworkUtils.class)) { + // 准备参数 + String tableName = "t_user"; + Alias tableAlias = new Alias("u"); + // mock 方法(LoginUser) + LoginUser loginUser = randomPojo(LoginUser.class, o -> o.setId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + securityFrameworkUtilsMock.when(SecurityFrameworkUtils::getLoginUser).thenReturn(loginUser); + // mock 方法(DeptDataPermissionRespDTO) + DeptDataPermissionRespDTO deptDataPermission = new DeptDataPermissionRespDTO() + .setDeptIds(CollUtil.newLinkedHashSet(10L, 20L)).setSelf(true); + when(permissionApi.getDeptDataPermission(same(1L))).thenReturn(deptDataPermission); + // 添加 user 字段配置 + rule.addUserColumn("t_user", "id"); + // 添加 dept 字段配置 + rule.addDeptColumn("t_user", "dept_id"); + + // 调用 + Expression expression = rule.getExpression(tableName, tableAlias); + // 断言 + assertEquals("(u.dept_id IN (10, 20) OR u.id = 1)", expression.toString()); + assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class)); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/util/DataPermissionUtilsTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/util/DataPermissionUtilsTest.java new file mode 100644 index 0000000..01b03d8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-data-permission/src/test/java/com/yunxi/scm/framework/datapermission/core/util/DataPermissionUtilsTest.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.framework.datapermission.core.util; + +import com.yunxi.scm.framework.datapermission.core.aop.DataPermissionContextHolder; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class DataPermissionUtilsTest { + + @Test + public void testExecuteIgnore() { + DataPermissionUtils.executeIgnore(() -> assertFalse(DataPermissionContextHolder.get().enable())); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-dict/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/pom.xml new file mode 100644 index 0000000..0d9215f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/pom.xml @@ -0,0 +1,50 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-biz-dict + jar + + ${project.artifactId} + 字典类型、数据 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter + + + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + + + com.google.guava + guava + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/config/YunxiDictAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/config/YunxiDictAutoConfiguration.java new file mode 100644 index 0000000..02f5303 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/config/YunxiDictAutoConfiguration.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.framework.dict.config; + +import com.yunxi.scm.framework.dict.core.util.DictFrameworkUtils; +import com.yunxi.scm.module.system.api.dict.DictDataApi; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class YunxiDictAutoConfiguration { + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public DictFrameworkUtils dictUtils(DictDataApi dictDataApi) { + DictFrameworkUtils.init(dictDataApi); + return new DictFrameworkUtils(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/core/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/core/package-info.java new file mode 100644 index 0000000..e289fd7 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.framework.dict.core; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/core/util/DictFrameworkUtils.java b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/core/util/DictFrameworkUtils.java new file mode 100644 index 0000000..d8616ba --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/core/util/DictFrameworkUtils.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.framework.dict.core.util; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.util.cache.CacheUtils; +import com.yunxi.scm.module.system.api.dict.DictDataApi; +import com.yunxi.scm.module.system.api.dict.dto.DictDataRespDTO; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; + +/** + * 字典工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class DictFrameworkUtils { + + private static DictDataApi dictDataApi; + + private static final DictDataRespDTO DICT_DATA_NULL = new DictDataRespDTO(); + + /** + * 针对 {@link #getDictDataLabel(String, String)} 的缓存 + */ + private static final LoadingCache, DictDataRespDTO> GET_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader, DictDataRespDTO>() { + + @Override + public DictDataRespDTO load(KeyValue key) { + return ObjectUtil.defaultIfNull(dictDataApi.getDictData(key.getKey(), key.getValue()), DICT_DATA_NULL); + } + + }); + + /** + * 针对 {@link #parseDictDataValue(String, String)} 的缓存 + */ + private static final LoadingCache, DictDataRespDTO> PARSE_DICT_DATA_CACHE = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader, DictDataRespDTO>() { + + @Override + public DictDataRespDTO load(KeyValue key) { + return ObjectUtil.defaultIfNull(dictDataApi.parseDictData(key.getKey(), key.getValue()), DICT_DATA_NULL); + } + + }); + + public static void init(DictDataApi dictDataApi) { + DictFrameworkUtils.dictDataApi = dictDataApi; + log.info("[init][初始化 DictFrameworkUtils 成功]"); + } + + @SneakyThrows + public static String getDictDataLabel(String dictType, Integer value) { + return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, String.valueOf(value))).getLabel(); + } + + @SneakyThrows + public static String getDictDataLabel(String dictType, String value) { + return GET_DICT_DATA_CACHE.get(new KeyValue<>(dictType, value)).getLabel(); + } + + @SneakyThrows + public static String parseDictDataValue(String dictType, String label) { + return PARSE_DICT_DATA_CACHE.get(new KeyValue<>(dictType, label)).getValue(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/package-info.java new file mode 100644 index 0000000..b910963 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/java/com/yunxi/scm/framework/dict/package-info.java @@ -0,0 +1,6 @@ +/** + * 字典数据模块,提供 {@link com.yunxi.scm.framework.dict.core.util.DictFrameworkUtils} 工具类 + * + * 通过将字典缓存在内存中,保证性能 + */ +package com.yunxi.scm.framework.dict; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..b7f1901 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.dict.config.YunxiDictAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/test/java/com/yunxi/scm/framework/dict/core/util/DictFrameworkUtilsTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/test/java/com/yunxi/scm/framework/dict/core/util/DictFrameworkUtilsTest.java new file mode 100644 index 0000000..353f71b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-dict/src/test/java/com/yunxi/scm/framework/dict/core/util/DictFrameworkUtilsTest.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.framework.dict.core.util; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.system.api.dict.DictDataApi; +import com.yunxi.scm.module.system.api.dict.dto.DictDataRespDTO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; + +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +/** + * {@link DictFrameworkUtils} 的单元测试 + */ +public class DictFrameworkUtilsTest extends BaseMockitoUnitTest { + + @Mock + private DictDataApi dictDataApi; + + @BeforeEach + public void setUp() { + DictFrameworkUtils.init(dictDataApi); + } + + @Test + public void testGetDictDataLabel() { + // mock 数据 + DictDataRespDTO dataRespDTO = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + // mock 方法 + when(dictDataApi.getDictData(dataRespDTO.getDictType(), dataRespDTO.getValue())).thenReturn(dataRespDTO); + // 断言返回值 + assertEquals(dataRespDTO.getLabel(), DictFrameworkUtils.getDictDataLabel(dataRespDTO.getDictType(), dataRespDTO.getValue())); + } + + @Test + public void testParseDictDataValue() { + // mock 数据 + DictDataRespDTO resp = randomPojo(DictDataRespDTO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + // mock 方法 + when(dictDataApi.parseDictData(resp.getDictType(), resp.getLabel())).thenReturn(resp); + // 断言返回值 + assertEquals(resp.getValue(), DictFrameworkUtils.parseDictDataValue(resp.getDictType(), resp.getLabel())); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/pom.xml new file mode 100644 index 0000000..a4e3f1d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/pom.xml @@ -0,0 +1,49 @@ + + + + yunxi-framework + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-biz-error-code + jar + + ${project.artifactId} + + 错误码 ErrorCode 的自动配置功能,提供如下功能: + 1. 远程读取:项目启动时,从 system-server 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置; + 2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-server 服务加载最新的 ErrorCode 错误码; + 3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑; + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter + + + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + + jakarta.validation + jakarta.validation-api + provided + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/config/ErrorCodeProperties.java b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/config/ErrorCodeProperties.java new file mode 100644 index 0000000..c6b2dab --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/config/ErrorCodeProperties.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.framework.errorcode.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 错误码的配置属性类 + * + * @author dlyan + */ +@ConfigurationProperties("yunxi.error-code") +@Data +@Validated +public class ErrorCodeProperties { + + /** + * 是否开启 + */ + private Boolean enable = true; + /** + * 错误码枚举类 + */ + @NotNull(message = "错误码枚举类不能为空") + private List constantsClassList; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/config/YunxiErrorCodeConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/config/YunxiErrorCodeConfiguration.java new file mode 100644 index 0000000..5779e17 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/config/YunxiErrorCodeConfiguration.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.framework.errorcode.config; + +import com.yunxi.scm.framework.errorcode.core.generator.ErrorCodeAutoGenerator; +import com.yunxi.scm.framework.errorcode.core.generator.ErrorCodeAutoGeneratorImpl; +import com.yunxi.scm.framework.errorcode.core.loader.ErrorCodeLoader; +import com.yunxi.scm.framework.errorcode.core.loader.ErrorCodeLoaderImpl; +import com.yunxi.scm.module.system.api.errorcode.ErrorCodeApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 错误码配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnProperty(prefix = "yunxi.error-code", value = "enable", matchIfMissing = true) // 允许使用 yunxi.error-code.enable=false 禁用访问日志 +@EnableConfigurationProperties(ErrorCodeProperties.class) +@EnableScheduling // 开启调度任务的功能,因为 ErrorCodeRemoteLoader 通过定时刷新错误码 +public class YunxiErrorCodeConfiguration { + + @Bean + public ErrorCodeAutoGenerator errorCodeAutoGenerator(@Value("${spring.application.name}") String applicationName, + ErrorCodeProperties errorCodeProperties, + ErrorCodeApi errorCodeApi) { + return new ErrorCodeAutoGeneratorImpl(applicationName, errorCodeProperties.getConstantsClassList(), errorCodeApi); + } + + @Bean + public ErrorCodeLoader errorCodeLoader(@Value("${spring.application.name}") String applicationName, + ErrorCodeApi errorCodeApi) { + return new ErrorCodeLoaderImpl(applicationName, errorCodeApi); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java new file mode 100644 index 0000000..c0b1026 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/generator/ErrorCodeAutoGenerator.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.framework.errorcode.core.generator; + +/** + * 错误码的自动生成器 + * + * @author dylan + */ +public interface ErrorCodeAutoGenerator { + + /** + * 将配置类到错误码写入数据库 + */ + void execute(); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java new file mode 100644 index 0000000..5ef0781 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/generator/ErrorCodeAutoGeneratorImpl.java @@ -0,0 +1,104 @@ +package com.yunxi.scm.framework.errorcode.core.generator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.module.system.api.errorcode.ErrorCodeApi; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * ErrorCodeAutoGenerator 的实现类 + * 目的是,扫描指定的 {@link #constantsClassList} 类,写入到 system 服务中 + * + * @author dylan + */ +@RequiredArgsConstructor +@Slf4j +public class ErrorCodeAutoGeneratorImpl implements ErrorCodeAutoGenerator { + + /** + * 应用分组 + */ + private final String applicationName; + /** + * 错误码枚举类 + */ + private final List constantsClassList; + /** + * 错误码 Api + */ + private final ErrorCodeApi errorCodeApi; + + @Override + @EventListener(ApplicationReadyEvent.class) + @Async // 异步,保证项目的启动过程,毕竟非关键流程 + public void execute() { + // 第一步,解析错误码 + List autoGenerateDTOs = parseErrorCode(); + log.info("[execute][解析到错误码数量为 ({}) 个]", autoGenerateDTOs.size()); + + // 第二步,写入到 system 服务 + errorCodeApi.autoGenerateErrorCodeList(autoGenerateDTOs); + log.info("[execute][写入到 system 组件完成]"); + } + + /** + * 解析 constantsClassList 变量,转换成错误码数组 + * + * @return 错误码数组 + */ + private List parseErrorCode() { + // 校验 errorCodeConstantsClass 参数 + if (CollUtil.isEmpty(constantsClassList)) { + log.info("[execute][未配置 yunxi.error-code.constants-class-list 配置项,不进行自动写入到 system 服务中]"); + return new ArrayList<>(); + } + + // 解析错误码 + List autoGenerateDTOs = new ArrayList<>(); + constantsClassList.forEach(constantsClass -> { + try { + // 解析错误码枚举类 + Class errorCodeConstantsClazz = ClassUtil.loadClass(constantsClass); + // 解析错误码 + autoGenerateDTOs.addAll(parseErrorCode(errorCodeConstantsClazz)); + } catch (Exception ex) { + log.warn("[parseErrorCode][constantsClass({}) 加载失败({})]", constantsClass, + ExceptionUtil.getRootCauseMessage(ex)); + } + }); + return autoGenerateDTOs; + } + + /** + * 解析错误码类,获得错误码数组 + * + * @return 错误码数组 + */ + private List parseErrorCode(Class constantsClass) { + List autoGenerateDTOs = new ArrayList<>(); + Arrays.stream(constantsClass.getFields()).forEach(field -> { + if (field.getType() != ErrorCode.class) { + return; + } + // 转换成 ErrorCodeAutoGenerateReqDTO 对象 + ErrorCode errorCode = (ErrorCode) ReflectUtil.getFieldValue(constantsClass, field); + autoGenerateDTOs.add(new ErrorCodeAutoGenerateReqDTO().setApplicationName(applicationName) + .setCode(errorCode.getCode()).setMessage(errorCode.getMsg())); + }); + return autoGenerateDTOs; + } + +} + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/loader/ErrorCodeLoader.java b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/loader/ErrorCodeLoader.java new file mode 100644 index 0000000..479091a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/loader/ErrorCodeLoader.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.framework.errorcode.core.loader; + +import com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil; + +/** + * 错误码加载器 + * + * 注意,错误码最终加载到 {@link ServiceExceptionUtil} 的 MESSAGES 变量中! + * + * @author dlyan + */ +public interface ErrorCodeLoader { + + /** + * 添加错误码 + * + * @param code 错误码的编号 + * @param msg 错误码的提示 + */ + default void putErrorCode(Integer code, String msg) { + ServiceExceptionUtil.put(code, msg); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java new file mode 100644 index 0000000..5f69258 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/core/loader/ErrorCodeLoaderImpl.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.framework.errorcode.core.loader; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.module.system.api.errorcode.ErrorCodeApi; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Scheduled; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * ErrorCodeLoader 的实现类,从 infra 的数据库中,加载错误码。 + * + * 考虑到错误码会刷新,所以按照 {@link #REFRESH_ERROR_CODE_PERIOD} 频率,增量加载错误码。 + * + * @author dlyan + */ +@RequiredArgsConstructor +@Slf4j +public class ErrorCodeLoaderImpl implements ErrorCodeLoader { + + /** + * 刷新错误码的频率,单位:毫秒 + */ + private static final int REFRESH_ERROR_CODE_PERIOD = 60 * 1000; + + /** + * 应用分组 + */ + private final String applicationName; + /** + * 错误码 Api + */ + private final ErrorCodeApi errorCodeApi; + + /** + * 缓存错误码的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private LocalDateTime maxUpdateTime; + + @EventListener(ApplicationReadyEvent.class) + public void loadErrorCodes() { + this.loadErrorCodes0(); + } + + @Scheduled(fixedDelay = REFRESH_ERROR_CODE_PERIOD, initialDelay = REFRESH_ERROR_CODE_PERIOD) + public void refreshErrorCodes() { + this.loadErrorCodes0(); + } + + private void loadErrorCodes0() { + // 加载错误码 + List errorCodeRespDTOs = errorCodeApi.getErrorCodeList(applicationName, maxUpdateTime); + if (CollUtil.isEmpty(errorCodeRespDTOs)) { + return; + } + log.info("[loadErrorCodes0][加载到 ({}) 个错误码]", errorCodeRespDTOs.size()); + + // 刷新错误码的缓存 + errorCodeRespDTOs.forEach(errorCodeRespDTO -> { + // 写入到错误码的缓存 + putErrorCode(errorCodeRespDTO.getCode(), errorCodeRespDTO.getMessage()); + // 记录下更新时间,方便增量更新 + maxUpdateTime = DateUtils.max(maxUpdateTime, errorCodeRespDTO.getUpdateTime()); + }); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/package-info.java new file mode 100644 index 0000000..2caafe0 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/java/com/yunxi/scm/framework/errorcode/package-info.java @@ -0,0 +1,10 @@ +/** + * 错误码 ErrorCode 的自动配置功能,提供如下功能: + * + * 1. 远程读取:项目启动时,从 system-service 服务,读取数据库中的 ErrorCode 错误码,实现错误码的提水可配置; + * 2. 自动更新:管理员在管理后台修数据库中的 ErrorCode 错误码时,项目自动从 system-service 服务加载最新的 ErrorCode 错误码; + * 3. 自动写入:项目启动时,将项目本地的错误码写到 system-server 服务中,方便管理员在管理后台编辑; + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.errorcode; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..77c1f57 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-error-code/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.errorcode.config.YunxiErrorCodeConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/pom.xml new file mode 100644 index 0000000..0f55a9c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/pom.xml @@ -0,0 +1,54 @@ + + + + yunxi-framework + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-biz-ip + jar + + ${project.artifactId} + IP 拓展,支持如下功能: + 1. IP 功能:查询 IP 对应的城市信息 + 基于 https://gitee.com/lionsoul/ip2region 实现 + 2. 城市功能:查询城市编码对应的城市信息 + 基于 https://github.com/modood/Administrative-divisions-of-China 实现 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.lionsoul + ip2region + + + + org.projectlombok + lombok + + + + org.slf4j + slf4j-api + provided + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/Area.java b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/Area.java new file mode 100644 index 0000000..f409433 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/Area.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.framework.ip.core; + +import com.yunxi.scm.framework.ip.core.enums.AreaTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 区域节点,包括国家、省份、城市、地区等信息 + * + * 数据可见 resources/area.csv 文件 + * + * @author 芋道源码 + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Area { + + /** + * 编号 - 全球,即根目录 + */ + public static final Integer ID_GLOBAL = 0; + /** + * 编号 - 中国 + */ + public static final Integer ID_CHINA = 1; + + /** + * 编号 + */ + private Integer id; + /** + * 名字 + */ + private String name; + /** + * 类型 + * + * 枚举 {@link AreaTypeEnum} + */ + private Integer type; + + /** + * 父节点 + */ + private Area parent; + /** + * 子节点 + */ + private List children; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/enums/AreaTypeEnum.java b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/enums/AreaTypeEnum.java new file mode 100644 index 0000000..c476a5c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/enums/AreaTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.framework.ip.core.enums; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 区域类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum AreaTypeEnum implements IntArrayValuable { + + COUNTRY(1, "国家"), + PROVINCE(2, "省份"), + CITY(3, "城市"), + DISTRICT(4, "地区"), // 县、镇、区等 + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(AreaTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/utils/AreaUtils.java b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/utils/AreaUtils.java new file mode 100644 index 0000000..e2dddf1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/utils/AreaUtils.java @@ -0,0 +1,119 @@ +package com.yunxi.scm.framework.ip.core.utils; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.text.csv.CsvRow; +import cn.hutool.core.text.csv.CsvUtil; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.ip.core.Area; +import com.yunxi.scm.framework.ip.core.enums.AreaTypeEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 区域工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class AreaUtils { + + /** + * 初始化 SEARCHER + */ + @SuppressWarnings("InstantiationOfUtilityClass") + private final static AreaUtils INSTANCE = new AreaUtils(); + + /** + * Area 内存缓存,提升访问速度 + */ + private static Map areas; + + private AreaUtils() { + long now = System.currentTimeMillis(); + areas = new HashMap<>(); + areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, + null, new ArrayList<>())); + // 从 csv 中加载数据 + List rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); + rows.remove(0); // 删除 header + for (CsvRow row : rows) { + // 创建 Area 对象 + Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), + null, new ArrayList<>()); + // 添加到 areas 中 + areas.put(area.getId(), area); + } + + // 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取 + for (CsvRow row : rows) { + Area area = areas.get(Integer.valueOf(row.get(0))); // 自己 + Area parent = areas.get(Integer.valueOf(row.get(3))); // 父 + Assert.isTrue(area != parent, "{}:父子节点相同", area.getName()); + area.setParent(parent); + parent.getChildren().add(area); + } + log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); + } + + /** + * 获得指定编号对应的区域 + * + * @param id 区域编号 + * @return 区域 + */ + public static Area getArea(Integer id) { + return areas.get(id); + } + + /** + * 格式化区域 + * + * @param id 区域编号 + * @return 格式化后的区域 + */ + public static String format(Integer id) { + return format(id, " "); + } + + /** + * 格式化区域 + * + * 例如说: + * 1. id = “静安区”时:上海 上海市 静安区 + * 2. id = “上海市”时:上海 上海市 + * 3. id = “上海”时:上海 + * 4. id = “美国”时:美国 + * 当区域在中国时,默认不显示中国 + * + * @param id 区域编号 + * @param separator 分隔符 + * @return 格式化后的区域 + */ + public static String format(Integer id, String separator) { + // 获得区域 + Area area = areas.get(id); + if (area == null) { + return null; + } + + // 格式化 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < AreaTypeEnum.values().length; i++) { // 避免死循环 + sb.insert(0, area.getName()); + // “递归”父节点 + area = area.getParent(); + if (area == null + || ObjectUtils.equalsAny(area.getId(), Area.ID_GLOBAL, Area.ID_CHINA)) { // 跳过父节点为中国的情况 + break; + } + sb.insert(0, separator); + } + return sb.toString(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/utils/IPUtils.java b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/utils/IPUtils.java new file mode 100644 index 0000000..f96a66c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/core/utils/IPUtils.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.framework.ip.core.utils; + +import cn.hutool.core.io.resource.ResourceUtil; +import com.yunxi.scm.framework.ip.core.Area; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.lionsoul.ip2region.xdb.Searcher; + +import java.io.IOException; + +/** + * IP 工具类 + * + * IP 数据源来自 ip2region.xdb 精简版,基于 项目 + * + * @author wanglhup + */ +@Slf4j +public class IPUtils { + + /** + * 初始化 SEARCHER + */ + @SuppressWarnings("InstantiationOfUtilityClass") + private final static IPUtils INSTANCE = new IPUtils(); + + /** + * IP 查询器,启动加载到内存中 + */ + private static Searcher SEARCHER; + + /** + * 私有化构造 + */ + private IPUtils() { + try { + long now = System.currentTimeMillis(); + byte[] bytes = ResourceUtil.readBytes("ip2region.xdb"); + SEARCHER = Searcher.newWithBuffer(bytes); + log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); + } catch (IOException e) { + log.error("启动加载 IPUtils 失败", e); + } + } + + /** + * 查询 IP 对应的地区编号 + * + * @param ip IP 地址,格式为 127.0.0.1 + * @return 地区id + */ + @SneakyThrows + public static Integer getAreaId(String ip) { + return Integer.parseInt(SEARCHER.search(ip.trim())); + } + + /** + * 查询 IP 对应的地区编号 + * + * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 + * @return 地区编号 + */ + @SneakyThrows + public static Integer getAreaId(long ip) { + return Integer.parseInt(SEARCHER.search(ip)); + } + + /** + * 查询 IP 对应的地区 + * + * @param ip IP 地址,格式为 127.0.0.1 + * @return 地区 + */ + public static Area getArea(String ip) { + return AreaUtils.getArea(getAreaId(ip)); + } + + /** + * 查询 IP 对应的地区 + * + * @param ip IP 地址的时间戳,格式参考{@link Searcher#checkIP(String)} 的返回 + * @return 地区 + */ + public static Area getArea(long ip) { + return AreaUtils.getArea(getAreaId(ip)); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/package-info.java new file mode 100644 index 0000000..722856d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/java/com/yunxi/scm/framework/ip/package-info.java @@ -0,0 +1,11 @@ +/** + * IP 拓展,支持如下功能: + * + * 1. IP 功能:查询 IP 对应的城市信息 + * 基于 https://gitee.com/lionsoul/ip2region 实现 + * 2. 城市功能:查询城市编码对应的城市信息 + * 基于 https://github.com/modood/Administrative-divisions-of-China 实现 + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.ip; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/resources/area.csv b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/resources/area.csv new file mode 100644 index 0000000..27e753c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/resources/area.csv @@ -0,0 +1,3608 @@ +id,name,type,parentId +1,中国,1,0 +2,蒙古,1,0 +3,朝鲜,1,0 +4,韩国,1,0 +5,日本,1,0 +6,菲律宾,1,0 +7,越南,1,0 +8,老挝,1,0 +9,柬埔寨,1,0 +10,缅甸,1,0 +11,泰国,1,0 +12,马来西亚,1,0 +13,文莱,1,0 +14,新加坡,1,0 +15,印度尼西亚,1,0 +16,东帝汶,1,0 +17,尼泊尔,1,0 +18,不丹,1,0 +19,孟加拉国,1,0 +20,印度,1,0 +21,巴基斯坦,1,0 +22,斯里兰卡,1,0 +23,马尔代夫,1,0 +24,哈萨克斯坦,1,0 +25,吉尔吉斯斯坦,1,0 +26,塔吉克斯坦,1,0 +27,乌兹别克斯坦,1,0 +28,土库曼斯坦,1,0 +29,阿富汗,1,0 +30,伊拉克,1,0 +31,伊朗,1,0 +32,叙利亚,1,0 +33,约旦,1,0 +34,黎巴嫩,1,0 +35,以色列,1,0 +36,巴勒斯坦,1,0 +37,沙特阿拉伯,1,0 +38,巴林,1,0 +39,卡塔尔,1,0 +40,科威特,1,0 +41,阿拉伯联合酋长国,1,0 +42,阿曼,1,0 +43,也门,1,0 +44,格鲁吉亚,1,0 +45,亚美尼亚,1,0 +46,阿塞拜疆,1,0 +47,土耳其,1,0 +48,塞浦路斯,1,0 +49,芬兰,1,0 +50,瑞典,1,0 +51,挪威,1,0 +52,冰岛,1,0 +53,丹麦,1,0 +54,爱沙尼亚,1,0 +55,拉脱维亚,1,0 +56,立陶宛,1,0 +57,白俄罗斯,1,0 +58,俄罗斯,1,0 +59,乌克兰,1,0 +60,摩尔多瓦,1,0 +61,波兰,1,0 +62,捷克,1,0 +63,斯洛伐克,1,0 +64,匈牙利,1,0 +65,德国,1,0 +66,奥地利,1,0 +67,瑞士,1,0 +68,列支敦士登,1,0 +69,英国,1,0 +70,爱尔兰,1,0 +71,荷兰,1,0 +72,比利时,1,0 +73,卢森堡,1,0 +74,法国,1,0 +75,摩纳哥,1,0 +76,罗马尼亚,1,0 +77,保加利亚,1,0 +78,塞尔维亚,1,0 +79,马其顿,1,0 +80,阿尔巴尼亚,1,0 +81,希腊,1,0 +82,斯洛文尼亚,1,0 +83,克罗地亚,1,0 +84,波斯尼亚和墨塞哥维那,1,0 +85,意大利,1,0 +86,梵蒂冈,1,0 +87,圣马力诺,1,0 +88,马耳他,1,0 +89,西班牙,1,0 +90,葡萄牙,1,0 +91,安道尔共和国,1,0 +92,埃及,1,0 +93,利比亚,1,0 +94,苏丹,1,0 +95,突尼斯,1,0 +96,阿尔及利亚,1,0 +97,摩洛哥,1,0 +98,亚速尔群岛,1,0 +99,马德拉群岛,1,0 +100,埃塞俄比亚,1,0 +101,厄立特里亚,1,0 +102,索马里,1,0 +103,吉布提,1,0 +104,肯尼亚,1,0 +105,坦桑尼亚,1,0 +106,乌干达,1,0 +107,卢旺达,1,0 +108,布隆迪,1,0 +109,塞舌尔,1,0 +110,圣多美及普林西比,1,0 +111,塞内加尔,1,0 +112,冈比亚,1,0 +113,马里,1,0 +114,布基纳法索,1,0 +115,几内亚,1,0 +116,几内亚比绍,1,0 +117,佛得角,1,0 +118,塞拉利昂,1,0 +119,利比里亚,1,0 +120,科特迪瓦,1,0 +121,加纳,1,0 +122,多哥,1,0 +123,贝宁,1,0 +124,尼日尔,1,0 +125,加那利群岛,1,0 +126,赞比亚,1,0 +127,安哥拉,1,0 +128,津巴布韦,1,0 +129,马拉维,1,0 +130,莫桑比克,1,0 +131,博茨瓦纳,1,0 +132,纳米比亚,1,0 +133,南非,1,0 +134,斯威士兰,1,0 +135,莱索托,1,0 +136,马达加斯加,1,0 +137,科摩罗,1,0 +138,毛里求斯,1,0 +139,留尼旺,1,0 +140,圣赫勒拿,1,0 +141,澳大利亚,1,0 +142,新西兰,1,0 +143,巴布亚新几内亚,1,0 +144,所罗门群岛,1,0 +145,瓦努阿图共和国,1,0 +146,密克罗尼西亚,1,0 +147,马绍尔群岛,1,0 +148,帕劳,1,0 +149,瑙鲁,1,0 +150,基里巴斯,1,0 +151,图瓦卢,1,0 +152,萨摩亚,1,0 +153,斐济,1,0 +154,汤加,1,0 +155,库克群岛,1,0 +156,关岛,1,0 +157,新喀里多尼亚,1,0 +158,法属波利尼西亚,1,0 +159,皮特凯恩岛,1,0 +160,瓦利斯与富图纳,1,0 +161,纽埃,1,0 +162,托克劳,1,0 +163,美属萨摩亚,1,0 +164,北马里亚纳,1,0 +165,加拿大,1,0 +166,美国,1,0 +167,墨西哥,1,0 +168,格陵兰,1,0 +169,危地马拉,1,0 +170,伯利兹,1,0 +171,萨尔瓦多,1,0 +172,洪都拉斯,1,0 +173,尼加拉瓜,1,0 +174,哥斯达黎加,1,0 +175,巴拿马,1,0 +176,巴哈马,1,0 +177,古巴,1,0 +178,牙买加,1,0 +179,海地,1,0 +180,多米尼加共和国,1,0 +181,安提瓜和巴布达,1,0 +182,圣基茨和尼维斯,1,0 +183,多米尼克,1,0 +184,圣卢西亚,1,0 +185,圣文森特和格林纳丁斯,1,0 +186,格林纳达,1,0 +187,巴巴多斯,1,0 +188,特立尼达和多巴哥,1,0 +189,波多黎各,1,0 +190,英属维尔京群岛,1,0 +191,美属维尔京群岛,1,0 +192,安圭拉,1,0 +193,蒙特塞拉特岛,1,0 +194,瓜德罗普,1,0 +195,马提尼克,1,0 +196,荷属安的列斯,1,0 +197,阿鲁巴,1,0 +198,特克斯和凯科斯群岛,1,0 +199,开曼群岛,1,0 +200,百慕大,1,0 +201,哥伦比亚,1,0 +202,委内瑞拉,1,0 +203,圭亚那,1,0 +204,法属圭亚那,1,0 +205,苏里南,1,0 +206,厄瓜多尔,1,0 +207,秘鲁,1,0 +208,玻利维亚,1,0 +209,巴西,1,0 +210,智利,1,0 +211,阿根廷,1,0 +212,乌拉圭,1,0 +213,巴拉圭,1,0 +214,波黑,1,0 +215,直布罗陀,1,0 +216,新喀里多尼亚群岛,1,0 +217,瓦利斯和富图纳群岛,1,0 +218,泽西岛,1,0 +219,黑山,1,0 +220,英属马恩岛,1,0 +221,尼日利亚,1,0 +222,喀麦隆,1,0 +223,加蓬,1,0 +224,乍得,1,0 +225,刚果共和国,1,0 +226,中非共和国,1,0 +227,南苏丹,1,0 +228,赤道几内亚,1,0 +229,毛里塔尼亚,1,0 +230,刚果民主共和国,1,0 +231,留尼汪岛,1,0 +232,格陵兰岛,1,0 +233,法罗群岛,1,0 +234,根西岛,1,0 +235,百慕大群岛,1,0 +236,圣皮埃尔和密克隆群岛,1,0 +237,法属圣马丁,1,0 +238,奥兰群岛,1,0 +239,北马里亚纳群岛,1,0 +240,库拉索,1,0 +241,博内尔岛,1,0 +242,圣马丁岛,1,0 +243,圣巴泰勒米岛,1,0 +244,福克兰群岛,1,0 +245,圣多美和普林西比,1,0 +246,英属印度洋领地,1,0 +247,东萨摩亚,1,0 +248,诺福克岛,1,0 +110000,北京,2,1 +120000,天津,2,1 +130000,河北省,2,1 +140000,山西省,2,1 +150000,内蒙古自治区,2,1 +210000,辽宁省,2,1 +220000,吉林省,2,1 +230000,黑龙江省,2,1 +310000,上海,2,1 +320000,江苏省,2,1 +330000,浙江省,2,1 +340000,安徽省,2,1 +350000,福建省,2,1 +360000,江西省,2,1 +370000,山东省,2,1 +410000,河南省,2,1 +420000,湖北省,2,1 +430000,湖南省,2,1 +440000,广东省,2,1 +450000,广西壮族自治区,2,1 +460000,海南省,2,1 +500000,重庆,2,1 +510000,四川省,2,1 +520000,贵州省,2,1 +530000,云南省,2,1 +540000,西藏自治区,2,1 +610000,陕西省,2,1 +620000,甘肃省,2,1 +630000,青海省,2,1 +640000,宁夏回族自治区,2,1 +650000,新疆维吾尔自治区,2,1 +110100,北京市,3,110000 +120100,天津市,3,120000 +130100,石家庄市,3,130000 +130200,唐山市,3,130000 +130300,秦皇岛市,3,130000 +130400,邯郸市,3,130000 +130500,邢台市,3,130000 +130600,保定市,3,130000 +130700,张家口市,3,130000 +130800,承德市,3,130000 +130900,沧州市,3,130000 +131000,廊坊市,3,130000 +131100,衡水市,3,130000 +140100,太原市,3,140000 +140200,大同市,3,140000 +140300,阳泉市,3,140000 +140400,长治市,3,140000 +140500,晋城市,3,140000 +140600,朔州市,3,140000 +140700,晋中市,3,140000 +140800,运城市,3,140000 +140900,忻州市,3,140000 +141000,临汾市,3,140000 +141100,吕梁市,3,140000 +150100,呼和浩特市,3,150000 +150200,包头市,3,150000 +150300,乌海市,3,150000 +150400,赤峰市,3,150000 +150500,通辽市,3,150000 +150600,鄂尔多斯市,3,150000 +150700,呼伦贝尔市,3,150000 +150800,巴彦淖尔市,3,150000 +150900,乌兰察布市,3,150000 +152200,兴安盟,3,150000 +152500,锡林郭勒盟,3,150000 +152900,阿拉善盟,3,150000 +210100,沈阳市,3,210000 +210200,大连市,3,210000 +210300,鞍山市,3,210000 +210400,抚顺市,3,210000 +210500,本溪市,3,210000 +210600,丹东市,3,210000 +210700,锦州市,3,210000 +210800,营口市,3,210000 +210900,阜新市,3,210000 +211000,辽阳市,3,210000 +211100,盘锦市,3,210000 +211200,铁岭市,3,210000 +211300,朝阳市,3,210000 +211400,葫芦岛市,3,210000 +220100,长春市,3,220000 +220200,吉林市,3,220000 +220300,四平市,3,220000 +220400,辽源市,3,220000 +220500,通化市,3,220000 +220600,白山市,3,220000 +220700,松原市,3,220000 +220800,白城市,3,220000 +222400,延边朝鲜族自治州,3,220000 +230100,哈尔滨市,3,230000 +230200,齐齐哈尔市,3,230000 +230300,鸡西市,3,230000 +230400,鹤岗市,3,230000 +230500,双鸭山市,3,230000 +230600,大庆市,3,230000 +230700,伊春市,3,230000 +230800,佳木斯市,3,230000 +230900,七台河市,3,230000 +231000,牡丹江市,3,230000 +231100,黑河市,3,230000 +231200,绥化市,3,230000 +232700,大兴安岭地区,3,230000 +310100,上海市,3,310000 +320100,南京市,3,320000 +320200,无锡市,3,320000 +320300,徐州市,3,320000 +320400,常州市,3,320000 +320500,苏州市,3,320000 +320600,南通市,3,320000 +320700,连云港市,3,320000 +320800,淮安市,3,320000 +320900,盐城市,3,320000 +321000,扬州市,3,320000 +321100,镇江市,3,320000 +321200,泰州市,3,320000 +321300,宿迁市,3,320000 +330100,杭州市,3,330000 +330200,宁波市,3,330000 +330300,温州市,3,330000 +330400,嘉兴市,3,330000 +330500,湖州市,3,330000 +330600,绍兴市,3,330000 +330700,金华市,3,330000 +330800,衢州市,3,330000 +330900,舟山市,3,330000 +331000,台州市,3,330000 +331100,丽水市,3,330000 +340100,合肥市,3,340000 +340200,芜湖市,3,340000 +340300,蚌埠市,3,340000 +340400,淮南市,3,340000 +340500,马鞍山市,3,340000 +340600,淮北市,3,340000 +340700,铜陵市,3,340000 +340800,安庆市,3,340000 +341000,黄山市,3,340000 +341100,滁州市,3,340000 +341200,阜阳市,3,340000 +341300,宿州市,3,340000 +341500,六安市,3,340000 +341600,亳州市,3,340000 +341700,池州市,3,340000 +341800,宣城市,3,340000 +350100,福州市,3,350000 +350200,厦门市,3,350000 +350300,莆田市,3,350000 +350400,三明市,3,350000 +350500,泉州市,3,350000 +350600,漳州市,3,350000 +350700,南平市,3,350000 +350800,龙岩市,3,350000 +350900,宁德市,3,350000 +360100,南昌市,3,360000 +360200,景德镇市,3,360000 +360300,萍乡市,3,360000 +360400,九江市,3,360000 +360500,新余市,3,360000 +360600,鹰潭市,3,360000 +360700,赣州市,3,360000 +360800,吉安市,3,360000 +360900,宜春市,3,360000 +361000,抚州市,3,360000 +361100,上饶市,3,360000 +370100,济南市,3,370000 +370200,青岛市,3,370000 +370300,淄博市,3,370000 +370400,枣庄市,3,370000 +370500,东营市,3,370000 +370600,烟台市,3,370000 +370700,潍坊市,3,370000 +370800,济宁市,3,370000 +370900,泰安市,3,370000 +371000,威海市,3,370000 +371100,日照市,3,370000 +371300,临沂市,3,370000 +371400,德州市,3,370000 +371500,聊城市,3,370000 +371600,滨州市,3,370000 +371700,菏泽市,3,370000 +410100,郑州市,3,410000 +410200,开封市,3,410000 +410300,洛阳市,3,410000 +410400,平顶山市,3,410000 +410500,安阳市,3,410000 +410600,鹤壁市,3,410000 +410700,新乡市,3,410000 +410800,焦作市,3,410000 +410900,濮阳市,3,410000 +411000,许昌市,3,410000 +411100,漯河市,3,410000 +411200,三门峡市,3,410000 +411300,南阳市,3,410000 +411400,商丘市,3,410000 +411500,信阳市,3,410000 +411600,周口市,3,410000 +411700,驻马店市,3,410000 +419000,省直辖县级行政区划,3,410000 +420100,武汉市,3,420000 +420200,黄石市,3,420000 +420300,十堰市,3,420000 +420500,宜昌市,3,420000 +420600,襄阳市,3,420000 +420700,鄂州市,3,420000 +420800,荆门市,3,420000 +420900,孝感市,3,420000 +421000,荆州市,3,420000 +421100,黄冈市,3,420000 +421200,咸宁市,3,420000 +421300,随州市,3,420000 +422800,恩施土家族苗族自治州,3,420000 +429000,省直辖县级行政区划,3,420000 +430100,长沙市,3,430000 +430200,株洲市,3,430000 +430300,湘潭市,3,430000 +430400,衡阳市,3,430000 +430500,邵阳市,3,430000 +430600,岳阳市,3,430000 +430700,常德市,3,430000 +430800,张家界市,3,430000 +430900,益阳市,3,430000 +431000,郴州市,3,430000 +431100,永州市,3,430000 +431200,怀化市,3,430000 +431300,娄底市,3,430000 +433100,湘西土家族苗族自治州,3,430000 +440100,广州市,3,440000 +440200,韶关市,3,440000 +440300,深圳市,3,440000 +440400,珠海市,3,440000 +440500,汕头市,3,440000 +440600,佛山市,3,440000 +440700,江门市,3,440000 +440800,湛江市,3,440000 +440900,茂名市,3,440000 +441200,肇庆市,3,440000 +441300,惠州市,3,440000 +441400,梅州市,3,440000 +441500,汕尾市,3,440000 +441600,河源市,3,440000 +441700,阳江市,3,440000 +441800,清远市,3,440000 +441900,东莞市,3,440000 +442000,中山市,3,440000 +445100,潮州市,3,440000 +445200,揭阳市,3,440000 +445300,云浮市,3,440000 +450100,南宁市,3,450000 +450200,柳州市,3,450000 +450300,桂林市,3,450000 +450400,梧州市,3,450000 +450500,北海市,3,450000 +450600,防城港市,3,450000 +450700,钦州市,3,450000 +450800,贵港市,3,450000 +450900,玉林市,3,450000 +451000,百色市,3,450000 +451100,贺州市,3,450000 +451200,河池市,3,450000 +451300,来宾市,3,450000 +451400,崇左市,3,450000 +460100,海口市,3,460000 +460200,三亚市,3,460000 +460300,三沙市,3,460000 +460400,儋州市,3,460000 +469000,省直辖县级行政区划,3,460000 +500100,重庆市,3,500000 +510100,成都市,3,510000 +510300,自贡市,3,510000 +510400,攀枝花市,3,510000 +510500,泸州市,3,510000 +510600,德阳市,3,510000 +510700,绵阳市,3,510000 +510800,广元市,3,510000 +510900,遂宁市,3,510000 +511000,内江市,3,510000 +511100,乐山市,3,510000 +511300,南充市,3,510000 +511400,眉山市,3,510000 +511500,宜宾市,3,510000 +511600,广安市,3,510000 +511700,达州市,3,510000 +511800,雅安市,3,510000 +511900,巴中市,3,510000 +512000,资阳市,3,510000 +513200,阿坝藏族羌族自治州,3,510000 +513300,甘孜藏族自治州,3,510000 +513400,凉山彝族自治州,3,510000 +520100,贵阳市,3,520000 +520200,六盘水市,3,520000 +520300,遵义市,3,520000 +520400,安顺市,3,520000 +520500,毕节市,3,520000 +520600,铜仁市,3,520000 +522300,黔西南布依族苗族自治州,3,520000 +522600,黔东南苗族侗族自治州,3,520000 +522700,黔南布依族苗族自治州,3,520000 +530100,昆明市,3,530000 +530300,曲靖市,3,530000 +530400,玉溪市,3,530000 +530500,保山市,3,530000 +530600,昭通市,3,530000 +530700,丽江市,3,530000 +530800,普洱市,3,530000 +530900,临沧市,3,530000 +532300,楚雄彝族自治州,3,530000 +532500,红河哈尼族彝族自治州,3,530000 +532600,文山壮族苗族自治州,3,530000 +532800,西双版纳傣族自治州,3,530000 +532900,大理白族自治州,3,530000 +533100,德宏傣族景颇族自治州,3,530000 +533300,怒江傈僳族自治州,3,530000 +533400,迪庆藏族自治州,3,530000 +540100,拉萨市,3,540000 +540200,日喀则市,3,540000 +540300,昌都市,3,540000 +540400,林芝市,3,540000 +540500,山南市,3,540000 +540600,那曲市,3,540000 +542500,阿里地区,3,540000 +610100,西安市,3,610000 +610200,铜川市,3,610000 +610300,宝鸡市,3,610000 +610400,咸阳市,3,610000 +610500,渭南市,3,610000 +610600,延安市,3,610000 +610700,汉中市,3,610000 +610800,榆林市,3,610000 +610900,安康市,3,610000 +611000,商洛市,3,610000 +620100,兰州市,3,620000 +620200,嘉峪关市,3,620000 +620300,金昌市,3,620000 +620400,白银市,3,620000 +620500,天水市,3,620000 +620600,武威市,3,620000 +620700,张掖市,3,620000 +620800,平凉市,3,620000 +620900,酒泉市,3,620000 +621000,庆阳市,3,620000 +621100,定西市,3,620000 +621200,陇南市,3,620000 +622900,临夏回族自治州,3,620000 +623000,甘南藏族自治州,3,620000 +630100,西宁市,3,630000 +630200,海东市,3,630000 +632200,海北藏族自治州,3,630000 +632300,黄南藏族自治州,3,630000 +632500,海南藏族自治州,3,630000 +632600,果洛藏族自治州,3,630000 +632700,玉树藏族自治州,3,630000 +632800,海西蒙古族藏族自治州,3,630000 +640100,银川市,3,640000 +640200,石嘴山市,3,640000 +640300,吴忠市,3,640000 +640400,固原市,3,640000 +640500,中卫市,3,640000 +650100,乌鲁木齐市,3,650000 +650200,克拉玛依市,3,650000 +650400,吐鲁番市,3,650000 +650500,哈密市,3,650000 +652300,昌吉回族自治州,3,650000 +652700,博尔塔拉蒙古自治州,3,650000 +652800,巴音郭楞蒙古自治州,3,650000 +652900,阿克苏地区,3,650000 +653000,克孜勒苏柯尔克孜自治州,3,650000 +653100,喀什地区,3,650000 +653200,和田地区,3,650000 +654000,伊犁哈萨克自治州,3,650000 +654200,塔城地区,3,650000 +654300,阿勒泰地区,3,650000 +659000,自治区直辖县级行政区划,3,650000 +110101,东城区,4,110100 +110102,西城区,4,110100 +110105,朝阳区,4,110100 +110106,丰台区,4,110100 +110107,石景山区,4,110100 +110108,海淀区,4,110100 +110109,门头沟区,4,110100 +110111,房山区,4,110100 +110112,通州区,4,110100 +110113,顺义区,4,110100 +110114,昌平区,4,110100 +110115,大兴区,4,110100 +110116,怀柔区,4,110100 +110117,平谷区,4,110100 +110118,密云区,4,110100 +110119,延庆区,4,110100 +120101,和平区,4,120100 +120102,河东区,4,120100 +120103,河西区,4,120100 +120104,南开区,4,120100 +120105,河北区,4,120100 +120106,红桥区,4,120100 +120110,东丽区,4,120100 +120111,西青区,4,120100 +120112,津南区,4,120100 +120113,北辰区,4,120100 +120114,武清区,4,120100 +120115,宝坻区,4,120100 +120116,滨海新区,4,120100 +120117,宁河区,4,120100 +120118,静海区,4,120100 +120119,蓟州区,4,120100 +130102,长安区,4,130100 +130104,桥西区,4,130100 +130105,新华区,4,130100 +130107,井陉矿区,4,130100 +130108,裕华区,4,130100 +130109,藁城区,4,130100 +130110,鹿泉区,4,130100 +130111,栾城区,4,130100 +130121,井陉县,4,130100 +130123,正定县,4,130100 +130125,行唐县,4,130100 +130126,灵寿县,4,130100 +130127,高邑县,4,130100 +130128,深泽县,4,130100 +130129,赞皇县,4,130100 +130130,无极县,4,130100 +130131,平山县,4,130100 +130132,元氏县,4,130100 +130133,赵县,4,130100 +130171,石家庄高新技术产业开发区,4,130100 +130172,石家庄循环化工园区,4,130100 +130181,辛集市,4,130100 +130183,晋州市,4,130100 +130184,新乐市,4,130100 +130202,路南区,4,130200 +130203,路北区,4,130200 +130204,古冶区,4,130200 +130205,开平区,4,130200 +130207,丰南区,4,130200 +130208,丰润区,4,130200 +130209,曹妃甸区,4,130200 +130224,滦南县,4,130200 +130225,乐亭县,4,130200 +130227,迁西县,4,130200 +130229,玉田县,4,130200 +130271,河北唐山芦台经济开发区,4,130200 +130272,唐山市汉沽管理区,4,130200 +130273,唐山高新技术产业开发区,4,130200 +130274,河北唐山海港经济开发区,4,130200 +130281,遵化市,4,130200 +130283,迁安市,4,130200 +130284,滦州市,4,130200 +130302,海港区,4,130300 +130303,山海关区,4,130300 +130304,北戴河区,4,130300 +130306,抚宁区,4,130300 +130321,青龙满族自治县,4,130300 +130322,昌黎县,4,130300 +130324,卢龙县,4,130300 +130371,秦皇岛市经济技术开发区,4,130300 +130372,北戴河新区,4,130300 +130402,邯山区,4,130400 +130403,丛台区,4,130400 +130404,复兴区,4,130400 +130406,峰峰矿区,4,130400 +130407,肥乡区,4,130400 +130408,永年区,4,130400 +130423,临漳县,4,130400 +130424,成安县,4,130400 +130425,大名县,4,130400 +130426,涉县,4,130400 +130427,磁县,4,130400 +130430,邱县,4,130400 +130431,鸡泽县,4,130400 +130432,广平县,4,130400 +130433,馆陶县,4,130400 +130434,魏县,4,130400 +130435,曲周县,4,130400 +130471,邯郸经济技术开发区,4,130400 +130473,邯郸冀南新区,4,130400 +130481,武安市,4,130400 +130502,襄都区,4,130500 +130503,信都区,4,130500 +130505,任泽区,4,130500 +130506,南和区,4,130500 +130522,临城县,4,130500 +130523,内丘县,4,130500 +130524,柏乡县,4,130500 +130525,隆尧县,4,130500 +130528,宁晋县,4,130500 +130529,巨鹿县,4,130500 +130530,新河县,4,130500 +130531,广宗县,4,130500 +130532,平乡县,4,130500 +130533,威县,4,130500 +130534,清河县,4,130500 +130535,临西县,4,130500 +130571,河北邢台经济开发区,4,130500 +130581,南宫市,4,130500 +130582,沙河市,4,130500 +130602,竞秀区,4,130600 +130606,莲池区,4,130600 +130607,满城区,4,130600 +130608,清苑区,4,130600 +130609,徐水区,4,130600 +130623,涞水县,4,130600 +130624,阜平县,4,130600 +130626,定兴县,4,130600 +130627,唐县,4,130600 +130628,高阳县,4,130600 +130629,容城县,4,130600 +130630,涞源县,4,130600 +130631,望都县,4,130600 +130632,安新县,4,130600 +130633,易县,4,130600 +130634,曲阳县,4,130600 +130635,蠡县,4,130600 +130636,顺平县,4,130600 +130637,博野县,4,130600 +130638,雄县,4,130600 +130671,保定高新技术产业开发区,4,130600 +130672,保定白沟新城,4,130600 +130681,涿州市,4,130600 +130682,定州市,4,130600 +130683,安国市,4,130600 +130684,高碑店市,4,130600 +130702,桥东区,4,130700 +130703,桥西区,4,130700 +130705,宣化区,4,130700 +130706,下花园区,4,130700 +130708,万全区,4,130700 +130709,崇礼区,4,130700 +130722,张北县,4,130700 +130723,康保县,4,130700 +130724,沽源县,4,130700 +130725,尚义县,4,130700 +130726,蔚县,4,130700 +130727,阳原县,4,130700 +130728,怀安县,4,130700 +130730,怀来县,4,130700 +130731,涿鹿县,4,130700 +130732,赤城县,4,130700 +130771,张家口经济开发区,4,130700 +130772,张家口市察北管理区,4,130700 +130773,张家口市塞北管理区,4,130700 +130802,双桥区,4,130800 +130803,双滦区,4,130800 +130804,鹰手营子矿区,4,130800 +130821,承德县,4,130800 +130822,兴隆县,4,130800 +130824,滦平县,4,130800 +130825,隆化县,4,130800 +130826,丰宁满族自治县,4,130800 +130827,宽城满族自治县,4,130800 +130828,围场满族蒙古族自治县,4,130800 +130871,承德高新技术产业开发区,4,130800 +130881,平泉市,4,130800 +130902,新华区,4,130900 +130903,运河区,4,130900 +130921,沧县,4,130900 +130922,青县,4,130900 +130923,东光县,4,130900 +130924,海兴县,4,130900 +130925,盐山县,4,130900 +130926,肃宁县,4,130900 +130927,南皮县,4,130900 +130928,吴桥县,4,130900 +130929,献县,4,130900 +130930,孟村回族自治县,4,130900 +130971,河北沧州经济开发区,4,130900 +130972,沧州高新技术产业开发区,4,130900 +130973,沧州渤海新区,4,130900 +130981,泊头市,4,130900 +130982,任丘市,4,130900 +130983,黄骅市,4,130900 +130984,河间市,4,130900 +131002,安次区,4,131000 +131003,广阳区,4,131000 +131022,固安县,4,131000 +131023,永清县,4,131000 +131024,香河县,4,131000 +131025,大城县,4,131000 +131026,文安县,4,131000 +131028,大厂回族自治县,4,131000 +131071,廊坊经济技术开发区,4,131000 +131081,霸州市,4,131000 +131082,三河市,4,131000 +131102,桃城区,4,131100 +131103,冀州区,4,131100 +131121,枣强县,4,131100 +131122,武邑县,4,131100 +131123,武强县,4,131100 +131124,饶阳县,4,131100 +131125,安平县,4,131100 +131126,故城县,4,131100 +131127,景县,4,131100 +131128,阜城县,4,131100 +131171,河北衡水高新技术产业开发区,4,131100 +131172,衡水滨湖新区,4,131100 +131182,深州市,4,131100 +140105,小店区,4,140100 +140106,迎泽区,4,140100 +140107,杏花岭区,4,140100 +140108,尖草坪区,4,140100 +140109,万柏林区,4,140100 +140110,晋源区,4,140100 +140121,清徐县,4,140100 +140122,阳曲县,4,140100 +140123,娄烦县,4,140100 +140171,山西转型综合改革示范区,4,140100 +140181,古交市,4,140100 +140212,新荣区,4,140200 +140213,平城区,4,140200 +140214,云冈区,4,140200 +140215,云州区,4,140200 +140221,阳高县,4,140200 +140222,天镇县,4,140200 +140223,广灵县,4,140200 +140224,灵丘县,4,140200 +140225,浑源县,4,140200 +140226,左云县,4,140200 +140271,山西大同经济开发区,4,140200 +140302,城区,4,140300 +140303,矿区,4,140300 +140311,郊区,4,140300 +140321,平定县,4,140300 +140322,盂县,4,140300 +140403,潞州区,4,140400 +140404,上党区,4,140400 +140405,屯留区,4,140400 +140406,潞城区,4,140400 +140423,襄垣县,4,140400 +140425,平顺县,4,140400 +140426,黎城县,4,140400 +140427,壶关县,4,140400 +140428,长子县,4,140400 +140429,武乡县,4,140400 +140430,沁县,4,140400 +140431,沁源县,4,140400 +140471,山西长治高新技术产业园区,4,140400 +140502,城区,4,140500 +140521,沁水县,4,140500 +140522,阳城县,4,140500 +140524,陵川县,4,140500 +140525,泽州县,4,140500 +140581,高平市,4,140500 +140602,朔城区,4,140600 +140603,平鲁区,4,140600 +140621,山阴县,4,140600 +140622,应县,4,140600 +140623,右玉县,4,140600 +140671,山西朔州经济开发区,4,140600 +140681,怀仁市,4,140600 +140702,榆次区,4,140700 +140703,太谷区,4,140700 +140721,榆社县,4,140700 +140722,左权县,4,140700 +140723,和顺县,4,140700 +140724,昔阳县,4,140700 +140725,寿阳县,4,140700 +140727,祁县,4,140700 +140728,平遥县,4,140700 +140729,灵石县,4,140700 +140781,介休市,4,140700 +140802,盐湖区,4,140800 +140821,临猗县,4,140800 +140822,万荣县,4,140800 +140823,闻喜县,4,140800 +140824,稷山县,4,140800 +140825,新绛县,4,140800 +140826,绛县,4,140800 +140827,垣曲县,4,140800 +140828,夏县,4,140800 +140829,平陆县,4,140800 +140830,芮城县,4,140800 +140881,永济市,4,140800 +140882,河津市,4,140800 +140902,忻府区,4,140900 +140921,定襄县,4,140900 +140922,五台县,4,140900 +140923,代县,4,140900 +140924,繁峙县,4,140900 +140925,宁武县,4,140900 +140926,静乐县,4,140900 +140927,神池县,4,140900 +140928,五寨县,4,140900 +140929,岢岚县,4,140900 +140930,河曲县,4,140900 +140931,保德县,4,140900 +140932,偏关县,4,140900 +140971,五台山风景名胜区,4,140900 +140981,原平市,4,140900 +141002,尧都区,4,141000 +141021,曲沃县,4,141000 +141022,翼城县,4,141000 +141023,襄汾县,4,141000 +141024,洪洞县,4,141000 +141025,古县,4,141000 +141026,安泽县,4,141000 +141027,浮山县,4,141000 +141028,吉县,4,141000 +141029,乡宁县,4,141000 +141030,大宁县,4,141000 +141031,隰县,4,141000 +141032,永和县,4,141000 +141033,蒲县,4,141000 +141034,汾西县,4,141000 +141081,侯马市,4,141000 +141082,霍州市,4,141000 +141102,离石区,4,141100 +141121,文水县,4,141100 +141122,交城县,4,141100 +141123,兴县,4,141100 +141124,临县,4,141100 +141125,柳林县,4,141100 +141126,石楼县,4,141100 +141127,岚县,4,141100 +141128,方山县,4,141100 +141129,中阳县,4,141100 +141130,交口县,4,141100 +141181,孝义市,4,141100 +141182,汾阳市,4,141100 +150102,新城区,4,150100 +150103,回民区,4,150100 +150104,玉泉区,4,150100 +150105,赛罕区,4,150100 +150121,土默特左旗,4,150100 +150122,托克托县,4,150100 +150123,和林格尔县,4,150100 +150124,清水河县,4,150100 +150125,武川县,4,150100 +150172,呼和浩特经济技术开发区,4,150100 +150202,东河区,4,150200 +150203,昆都仑区,4,150200 +150204,青山区,4,150200 +150205,石拐区,4,150200 +150206,白云鄂博矿区,4,150200 +150207,九原区,4,150200 +150221,土默特右旗,4,150200 +150222,固阳县,4,150200 +150223,达尔罕茂明安联合旗,4,150200 +150271,包头稀土高新技术产业开发区,4,150200 +150302,海勃湾区,4,150300 +150303,海南区,4,150300 +150304,乌达区,4,150300 +150402,红山区,4,150400 +150403,元宝山区,4,150400 +150404,松山区,4,150400 +150421,阿鲁科尔沁旗,4,150400 +150422,巴林左旗,4,150400 +150423,巴林右旗,4,150400 +150424,林西县,4,150400 +150425,克什克腾旗,4,150400 +150426,翁牛特旗,4,150400 +150428,喀喇沁旗,4,150400 +150429,宁城县,4,150400 +150430,敖汉旗,4,150400 +150502,科尔沁区,4,150500 +150521,科尔沁左翼中旗,4,150500 +150522,科尔沁左翼后旗,4,150500 +150523,开鲁县,4,150500 +150524,库伦旗,4,150500 +150525,奈曼旗,4,150500 +150526,扎鲁特旗,4,150500 +150571,通辽经济技术开发区,4,150500 +150581,霍林郭勒市,4,150500 +150602,东胜区,4,150600 +150603,康巴什区,4,150600 +150621,达拉特旗,4,150600 +150622,准格尔旗,4,150600 +150623,鄂托克前旗,4,150600 +150624,鄂托克旗,4,150600 +150625,杭锦旗,4,150600 +150626,乌审旗,4,150600 +150627,伊金霍洛旗,4,150600 +150702,海拉尔区,4,150700 +150703,扎赉诺尔区,4,150700 +150721,阿荣旗,4,150700 +150722,莫力达瓦达斡尔族自治旗,4,150700 +150723,鄂伦春自治旗,4,150700 +150724,鄂温克族自治旗,4,150700 +150725,陈巴尔虎旗,4,150700 +150726,新巴尔虎左旗,4,150700 +150727,新巴尔虎右旗,4,150700 +150781,满洲里市,4,150700 +150782,牙克石市,4,150700 +150783,扎兰屯市,4,150700 +150784,额尔古纳市,4,150700 +150785,根河市,4,150700 +150802,临河区,4,150800 +150821,五原县,4,150800 +150822,磴口县,4,150800 +150823,乌拉特前旗,4,150800 +150824,乌拉特中旗,4,150800 +150825,乌拉特后旗,4,150800 +150826,杭锦后旗,4,150800 +150902,集宁区,4,150900 +150921,卓资县,4,150900 +150922,化德县,4,150900 +150923,商都县,4,150900 +150924,兴和县,4,150900 +150925,凉城县,4,150900 +150926,察哈尔右翼前旗,4,150900 +150927,察哈尔右翼中旗,4,150900 +150928,察哈尔右翼后旗,4,150900 +150929,四子王旗,4,150900 +150981,丰镇市,4,150900 +152201,乌兰浩特市,4,152200 +152202,阿尔山市,4,152200 +152221,科尔沁右翼前旗,4,152200 +152222,科尔沁右翼中旗,4,152200 +152223,扎赉特旗,4,152200 +152224,突泉县,4,152200 +152501,二连浩特市,4,152500 +152502,锡林浩特市,4,152500 +152522,阿巴嘎旗,4,152500 +152523,苏尼特左旗,4,152500 +152524,苏尼特右旗,4,152500 +152525,东乌珠穆沁旗,4,152500 +152526,西乌珠穆沁旗,4,152500 +152527,太仆寺旗,4,152500 +152528,镶黄旗,4,152500 +152529,正镶白旗,4,152500 +152530,正蓝旗,4,152500 +152531,多伦县,4,152500 +152571,乌拉盖管委会,4,152500 +152921,阿拉善左旗,4,152900 +152922,阿拉善右旗,4,152900 +152923,额济纳旗,4,152900 +152971,内蒙古阿拉善高新技术产业开发区,4,152900 +210102,和平区,4,210100 +210103,沈河区,4,210100 +210104,大东区,4,210100 +210105,皇姑区,4,210100 +210106,铁西区,4,210100 +210111,苏家屯区,4,210100 +210112,浑南区,4,210100 +210113,沈北新区,4,210100 +210114,于洪区,4,210100 +210115,辽中区,4,210100 +210123,康平县,4,210100 +210124,法库县,4,210100 +210181,新民市,4,210100 +210202,中山区,4,210200 +210203,西岗区,4,210200 +210204,沙河口区,4,210200 +210211,甘井子区,4,210200 +210212,旅顺口区,4,210200 +210213,金州区,4,210200 +210214,普兰店区,4,210200 +210224,长海县,4,210200 +210281,瓦房店市,4,210200 +210283,庄河市,4,210200 +210302,铁东区,4,210300 +210303,铁西区,4,210300 +210304,立山区,4,210300 +210311,千山区,4,210300 +210321,台安县,4,210300 +210323,岫岩满族自治县,4,210300 +210381,海城市,4,210300 +210402,新抚区,4,210400 +210403,东洲区,4,210400 +210404,望花区,4,210400 +210411,顺城区,4,210400 +210421,抚顺县,4,210400 +210422,新宾满族自治县,4,210400 +210423,清原满族自治县,4,210400 +210502,平山区,4,210500 +210503,溪湖区,4,210500 +210504,明山区,4,210500 +210505,南芬区,4,210500 +210521,本溪满族自治县,4,210500 +210522,桓仁满族自治县,4,210500 +210602,元宝区,4,210600 +210603,振兴区,4,210600 +210604,振安区,4,210600 +210624,宽甸满族自治县,4,210600 +210681,东港市,4,210600 +210682,凤城市,4,210600 +210702,古塔区,4,210700 +210703,凌河区,4,210700 +210711,太和区,4,210700 +210726,黑山县,4,210700 +210727,义县,4,210700 +210781,凌海市,4,210700 +210782,北镇市,4,210700 +210802,站前区,4,210800 +210803,西市区,4,210800 +210804,鲅鱼圈区,4,210800 +210811,老边区,4,210800 +210881,盖州市,4,210800 +210882,大石桥市,4,210800 +210902,海州区,4,210900 +210903,新邱区,4,210900 +210904,太平区,4,210900 +210905,清河门区,4,210900 +210911,细河区,4,210900 +210921,阜新蒙古族自治县,4,210900 +210922,彰武县,4,210900 +211002,白塔区,4,211000 +211003,文圣区,4,211000 +211004,宏伟区,4,211000 +211005,弓长岭区,4,211000 +211011,太子河区,4,211000 +211021,辽阳县,4,211000 +211081,灯塔市,4,211000 +211102,双台子区,4,211100 +211103,兴隆台区,4,211100 +211104,大洼区,4,211100 +211122,盘山县,4,211100 +211202,银州区,4,211200 +211204,清河区,4,211200 +211221,铁岭县,4,211200 +211223,西丰县,4,211200 +211224,昌图县,4,211200 +211281,调兵山市,4,211200 +211282,开原市,4,211200 +211302,双塔区,4,211300 +211303,龙城区,4,211300 +211321,朝阳县,4,211300 +211322,建平县,4,211300 +211324,喀喇沁左翼蒙古族自治县,4,211300 +211381,北票市,4,211300 +211382,凌源市,4,211300 +211402,连山区,4,211400 +211403,龙港区,4,211400 +211404,南票区,4,211400 +211421,绥中县,4,211400 +211422,建昌县,4,211400 +211481,兴城市,4,211400 +220102,南关区,4,220100 +220103,宽城区,4,220100 +220104,朝阳区,4,220100 +220105,二道区,4,220100 +220106,绿园区,4,220100 +220112,双阳区,4,220100 +220113,九台区,4,220100 +220122,农安县,4,220100 +220171,长春经济技术开发区,4,220100 +220172,长春净月高新技术产业开发区,4,220100 +220173,长春高新技术产业开发区,4,220100 +220174,长春汽车经济技术开发区,4,220100 +220182,榆树市,4,220100 +220183,德惠市,4,220100 +220184,公主岭市,4,220100 +220202,昌邑区,4,220200 +220203,龙潭区,4,220200 +220204,船营区,4,220200 +220211,丰满区,4,220200 +220221,永吉县,4,220200 +220271,吉林经济开发区,4,220200 +220272,吉林高新技术产业开发区,4,220200 +220273,吉林中国新加坡食品区,4,220200 +220281,蛟河市,4,220200 +220282,桦甸市,4,220200 +220283,舒兰市,4,220200 +220284,磐石市,4,220200 +220302,铁西区,4,220300 +220303,铁东区,4,220300 +220322,梨树县,4,220300 +220323,伊通满族自治县,4,220300 +220382,双辽市,4,220300 +220402,龙山区,4,220400 +220403,西安区,4,220400 +220421,东丰县,4,220400 +220422,东辽县,4,220400 +220502,东昌区,4,220500 +220503,二道江区,4,220500 +220521,通化县,4,220500 +220523,辉南县,4,220500 +220524,柳河县,4,220500 +220581,梅河口市,4,220500 +220582,集安市,4,220500 +220602,浑江区,4,220600 +220605,江源区,4,220600 +220621,抚松县,4,220600 +220622,靖宇县,4,220600 +220623,长白朝鲜族自治县,4,220600 +220681,临江市,4,220600 +220702,宁江区,4,220700 +220721,前郭尔罗斯蒙古族自治县,4,220700 +220722,长岭县,4,220700 +220723,乾安县,4,220700 +220771,吉林松原经济开发区,4,220700 +220781,扶余市,4,220700 +220802,洮北区,4,220800 +220821,镇赉县,4,220800 +220822,通榆县,4,220800 +220871,吉林白城经济开发区,4,220800 +220881,洮南市,4,220800 +220882,大安市,4,220800 +222401,延吉市,4,222400 +222402,图们市,4,222400 +222403,敦化市,4,222400 +222404,珲春市,4,222400 +222405,龙井市,4,222400 +222406,和龙市,4,222400 +222424,汪清县,4,222400 +222426,安图县,4,222400 +230102,道里区,4,230100 +230103,南岗区,4,230100 +230104,道外区,4,230100 +230108,平房区,4,230100 +230109,松北区,4,230100 +230110,香坊区,4,230100 +230111,呼兰区,4,230100 +230112,阿城区,4,230100 +230113,双城区,4,230100 +230123,依兰县,4,230100 +230124,方正县,4,230100 +230125,宾县,4,230100 +230126,巴彦县,4,230100 +230127,木兰县,4,230100 +230128,通河县,4,230100 +230129,延寿县,4,230100 +230183,尚志市,4,230100 +230184,五常市,4,230100 +230202,龙沙区,4,230200 +230203,建华区,4,230200 +230204,铁锋区,4,230200 +230205,昂昂溪区,4,230200 +230206,富拉尔基区,4,230200 +230207,碾子山区,4,230200 +230208,梅里斯达斡尔族区,4,230200 +230221,龙江县,4,230200 +230223,依安县,4,230200 +230224,泰来县,4,230200 +230225,甘南县,4,230200 +230227,富裕县,4,230200 +230229,克山县,4,230200 +230230,克东县,4,230200 +230231,拜泉县,4,230200 +230281,讷河市,4,230200 +230302,鸡冠区,4,230300 +230303,恒山区,4,230300 +230304,滴道区,4,230300 +230305,梨树区,4,230300 +230306,城子河区,4,230300 +230307,麻山区,4,230300 +230321,鸡东县,4,230300 +230381,虎林市,4,230300 +230382,密山市,4,230300 +230402,向阳区,4,230400 +230403,工农区,4,230400 +230404,南山区,4,230400 +230405,兴安区,4,230400 +230406,东山区,4,230400 +230407,兴山区,4,230400 +230421,萝北县,4,230400 +230422,绥滨县,4,230400 +230502,尖山区,4,230500 +230503,岭东区,4,230500 +230505,四方台区,4,230500 +230506,宝山区,4,230500 +230521,集贤县,4,230500 +230522,友谊县,4,230500 +230523,宝清县,4,230500 +230524,饶河县,4,230500 +230602,萨尔图区,4,230600 +230603,龙凤区,4,230600 +230604,让胡路区,4,230600 +230605,红岗区,4,230600 +230606,大同区,4,230600 +230621,肇州县,4,230600 +230622,肇源县,4,230600 +230623,林甸县,4,230600 +230624,杜尔伯特蒙古族自治县,4,230600 +230671,大庆高新技术产业开发区,4,230600 +230717,伊美区,4,230700 +230718,乌翠区,4,230700 +230719,友好区,4,230700 +230722,嘉荫县,4,230700 +230723,汤旺县,4,230700 +230724,丰林县,4,230700 +230725,大箐山县,4,230700 +230726,南岔县,4,230700 +230751,金林区,4,230700 +230781,铁力市,4,230700 +230803,向阳区,4,230800 +230804,前进区,4,230800 +230805,东风区,4,230800 +230811,郊区,4,230800 +230822,桦南县,4,230800 +230826,桦川县,4,230800 +230828,汤原县,4,230800 +230881,同江市,4,230800 +230882,富锦市,4,230800 +230883,抚远市,4,230800 +230902,新兴区,4,230900 +230903,桃山区,4,230900 +230904,茄子河区,4,230900 +230921,勃利县,4,230900 +231002,东安区,4,231000 +231003,阳明区,4,231000 +231004,爱民区,4,231000 +231005,西安区,4,231000 +231025,林口县,4,231000 +231071,牡丹江经济技术开发区,4,231000 +231081,绥芬河市,4,231000 +231083,海林市,4,231000 +231084,宁安市,4,231000 +231085,穆棱市,4,231000 +231086,东宁市,4,231000 +231102,爱辉区,4,231100 +231123,逊克县,4,231100 +231124,孙吴县,4,231100 +231181,北安市,4,231100 +231182,五大连池市,4,231100 +231183,嫩江市,4,231100 +231202,北林区,4,231200 +231221,望奎县,4,231200 +231222,兰西县,4,231200 +231223,青冈县,4,231200 +231224,庆安县,4,231200 +231225,明水县,4,231200 +231226,绥棱县,4,231200 +231281,安达市,4,231200 +231282,肇东市,4,231200 +231283,海伦市,4,231200 +232701,漠河市,4,232700 +232721,呼玛县,4,232700 +232722,塔河县,4,232700 +232761,加格达奇区,4,232700 +232762,松岭区,4,232700 +232763,新林区,4,232700 +232764,呼中区,4,232700 +310101,黄浦区,4,310100 +310104,徐汇区,4,310100 +310105,长宁区,4,310100 +310106,静安区,4,310100 +310107,普陀区,4,310100 +310109,虹口区,4,310100 +310110,杨浦区,4,310100 +310112,闵行区,4,310100 +310113,宝山区,4,310100 +310114,嘉定区,4,310100 +310115,浦东新区,4,310100 +310116,金山区,4,310100 +310117,松江区,4,310100 +310118,青浦区,4,310100 +310120,奉贤区,4,310100 +310151,崇明区,4,310100 +320102,玄武区,4,320100 +320104,秦淮区,4,320100 +320105,建邺区,4,320100 +320106,鼓楼区,4,320100 +320111,浦口区,4,320100 +320113,栖霞区,4,320100 +320114,雨花台区,4,320100 +320115,江宁区,4,320100 +320116,六合区,4,320100 +320117,溧水区,4,320100 +320118,高淳区,4,320100 +320205,锡山区,4,320200 +320206,惠山区,4,320200 +320211,滨湖区,4,320200 +320213,梁溪区,4,320200 +320214,新吴区,4,320200 +320281,江阴市,4,320200 +320282,宜兴市,4,320200 +320302,鼓楼区,4,320300 +320303,云龙区,4,320300 +320305,贾汪区,4,320300 +320311,泉山区,4,320300 +320312,铜山区,4,320300 +320321,丰县,4,320300 +320322,沛县,4,320300 +320324,睢宁县,4,320300 +320371,徐州经济技术开发区,4,320300 +320381,新沂市,4,320300 +320382,邳州市,4,320300 +320402,天宁区,4,320400 +320404,钟楼区,4,320400 +320411,新北区,4,320400 +320412,武进区,4,320400 +320413,金坛区,4,320400 +320481,溧阳市,4,320400 +320505,虎丘区,4,320500 +320506,吴中区,4,320500 +320507,相城区,4,320500 +320508,姑苏区,4,320500 +320509,吴江区,4,320500 +320571,苏州工业园区,4,320500 +320581,常熟市,4,320500 +320582,张家港市,4,320500 +320583,昆山市,4,320500 +320585,太仓市,4,320500 +320612,通州区,4,320600 +320613,崇川区,4,320600 +320614,海门区,4,320600 +320623,如东县,4,320600 +320671,南通经济技术开发区,4,320600 +320681,启东市,4,320600 +320682,如皋市,4,320600 +320685,海安市,4,320600 +320703,连云区,4,320700 +320706,海州区,4,320700 +320707,赣榆区,4,320700 +320722,东海县,4,320700 +320723,灌云县,4,320700 +320724,灌南县,4,320700 +320771,连云港经济技术开发区,4,320700 +320772,连云港高新技术产业开发区,4,320700 +320803,淮安区,4,320800 +320804,淮阴区,4,320800 +320812,清江浦区,4,320800 +320813,洪泽区,4,320800 +320826,涟水县,4,320800 +320830,盱眙县,4,320800 +320831,金湖县,4,320800 +320871,淮安经济技术开发区,4,320800 +320902,亭湖区,4,320900 +320903,盐都区,4,320900 +320904,大丰区,4,320900 +320921,响水县,4,320900 +320922,滨海县,4,320900 +320923,阜宁县,4,320900 +320924,射阳县,4,320900 +320925,建湖县,4,320900 +320971,盐城经济技术开发区,4,320900 +320981,东台市,4,320900 +321002,广陵区,4,321000 +321003,邗江区,4,321000 +321012,江都区,4,321000 +321023,宝应县,4,321000 +321071,扬州经济技术开发区,4,321000 +321081,仪征市,4,321000 +321084,高邮市,4,321000 +321102,京口区,4,321100 +321111,润州区,4,321100 +321112,丹徒区,4,321100 +321171,镇江新区,4,321100 +321181,丹阳市,4,321100 +321182,扬中市,4,321100 +321183,句容市,4,321100 +321202,海陵区,4,321200 +321203,高港区,4,321200 +321204,姜堰区,4,321200 +321271,泰州医药高新技术产业开发区,4,321200 +321281,兴化市,4,321200 +321282,靖江市,4,321200 +321283,泰兴市,4,321200 +321302,宿城区,4,321300 +321311,宿豫区,4,321300 +321322,沭阳县,4,321300 +321323,泗阳县,4,321300 +321324,泗洪县,4,321300 +321371,宿迁经济技术开发区,4,321300 +330102,上城区,4,330100 +330105,拱墅区,4,330100 +330106,西湖区,4,330100 +330108,滨江区,4,330100 +330109,萧山区,4,330100 +330110,余杭区,4,330100 +330111,富阳区,4,330100 +330112,临安区,4,330100 +330113,临平区,4,330100 +330114,钱塘区,4,330100 +330122,桐庐县,4,330100 +330127,淳安县,4,330100 +330182,建德市,4,330100 +330203,海曙区,4,330200 +330205,江北区,4,330200 +330206,北仑区,4,330200 +330211,镇海区,4,330200 +330212,鄞州区,4,330200 +330213,奉化区,4,330200 +330225,象山县,4,330200 +330226,宁海县,4,330200 +330281,余姚市,4,330200 +330282,慈溪市,4,330200 +330302,鹿城区,4,330300 +330303,龙湾区,4,330300 +330304,瓯海区,4,330300 +330305,洞头区,4,330300 +330324,永嘉县,4,330300 +330326,平阳县,4,330300 +330327,苍南县,4,330300 +330328,文成县,4,330300 +330329,泰顺县,4,330300 +330371,温州经济技术开发区,4,330300 +330381,瑞安市,4,330300 +330382,乐清市,4,330300 +330383,龙港市,4,330300 +330402,南湖区,4,330400 +330411,秀洲区,4,330400 +330421,嘉善县,4,330400 +330424,海盐县,4,330400 +330481,海宁市,4,330400 +330482,平湖市,4,330400 +330483,桐乡市,4,330400 +330502,吴兴区,4,330500 +330503,南浔区,4,330500 +330521,德清县,4,330500 +330522,长兴县,4,330500 +330523,安吉县,4,330500 +330602,越城区,4,330600 +330603,柯桥区,4,330600 +330604,上虞区,4,330600 +330624,新昌县,4,330600 +330681,诸暨市,4,330600 +330683,嵊州市,4,330600 +330702,婺城区,4,330700 +330703,金东区,4,330700 +330723,武义县,4,330700 +330726,浦江县,4,330700 +330727,磐安县,4,330700 +330781,兰溪市,4,330700 +330782,义乌市,4,330700 +330783,东阳市,4,330700 +330784,永康市,4,330700 +330802,柯城区,4,330800 +330803,衢江区,4,330800 +330822,常山县,4,330800 +330824,开化县,4,330800 +330825,龙游县,4,330800 +330881,江山市,4,330800 +330902,定海区,4,330900 +330903,普陀区,4,330900 +330921,岱山县,4,330900 +330922,嵊泗县,4,330900 +331002,椒江区,4,331000 +331003,黄岩区,4,331000 +331004,路桥区,4,331000 +331022,三门县,4,331000 +331023,天台县,4,331000 +331024,仙居县,4,331000 +331081,温岭市,4,331000 +331082,临海市,4,331000 +331083,玉环市,4,331000 +331102,莲都区,4,331100 +331121,青田县,4,331100 +331122,缙云县,4,331100 +331123,遂昌县,4,331100 +331124,松阳县,4,331100 +331125,云和县,4,331100 +331126,庆元县,4,331100 +331127,景宁畲族自治县,4,331100 +331181,龙泉市,4,331100 +340102,瑶海区,4,340100 +340103,庐阳区,4,340100 +340104,蜀山区,4,340100 +340111,包河区,4,340100 +340121,长丰县,4,340100 +340122,肥东县,4,340100 +340123,肥西县,4,340100 +340124,庐江县,4,340100 +340171,合肥高新技术产业开发区,4,340100 +340172,合肥经济技术开发区,4,340100 +340173,合肥新站高新技术产业开发区,4,340100 +340181,巢湖市,4,340100 +340202,镜湖区,4,340200 +340207,鸠江区,4,340200 +340209,弋江区,4,340200 +340210,湾沚区,4,340200 +340212,繁昌区,4,340200 +340223,南陵县,4,340200 +340271,芜湖经济技术开发区,4,340200 +340272,安徽芜湖三山经济开发区,4,340200 +340281,无为市,4,340200 +340302,龙子湖区,4,340300 +340303,蚌山区,4,340300 +340304,禹会区,4,340300 +340311,淮上区,4,340300 +340321,怀远县,4,340300 +340322,五河县,4,340300 +340323,固镇县,4,340300 +340371,蚌埠市高新技术开发区,4,340300 +340372,蚌埠市经济开发区,4,340300 +340402,大通区,4,340400 +340403,田家庵区,4,340400 +340404,谢家集区,4,340400 +340405,八公山区,4,340400 +340406,潘集区,4,340400 +340421,凤台县,4,340400 +340422,寿县,4,340400 +340503,花山区,4,340500 +340504,雨山区,4,340500 +340506,博望区,4,340500 +340521,当涂县,4,340500 +340522,含山县,4,340500 +340523,和县,4,340500 +340602,杜集区,4,340600 +340603,相山区,4,340600 +340604,烈山区,4,340600 +340621,濉溪县,4,340600 +340705,铜官区,4,340700 +340706,义安区,4,340700 +340711,郊区,4,340700 +340722,枞阳县,4,340700 +340802,迎江区,4,340800 +340803,大观区,4,340800 +340811,宜秀区,4,340800 +340822,怀宁县,4,340800 +340825,太湖县,4,340800 +340826,宿松县,4,340800 +340827,望江县,4,340800 +340828,岳西县,4,340800 +340871,安徽安庆经济开发区,4,340800 +340881,桐城市,4,340800 +340882,潜山市,4,340800 +341002,屯溪区,4,341000 +341003,黄山区,4,341000 +341004,徽州区,4,341000 +341021,歙县,4,341000 +341022,休宁县,4,341000 +341023,黟县,4,341000 +341024,祁门县,4,341000 +341102,琅琊区,4,341100 +341103,南谯区,4,341100 +341122,来安县,4,341100 +341124,全椒县,4,341100 +341125,定远县,4,341100 +341126,凤阳县,4,341100 +341171,中新苏滁高新技术产业开发区,4,341100 +341172,滁州经济技术开发区,4,341100 +341181,天长市,4,341100 +341182,明光市,4,341100 +341202,颍州区,4,341200 +341203,颍东区,4,341200 +341204,颍泉区,4,341200 +341221,临泉县,4,341200 +341222,太和县,4,341200 +341225,阜南县,4,341200 +341226,颍上县,4,341200 +341271,阜阳合肥现代产业园区,4,341200 +341272,阜阳经济技术开发区,4,341200 +341282,界首市,4,341200 +341302,埇桥区,4,341300 +341321,砀山县,4,341300 +341322,萧县,4,341300 +341323,灵璧县,4,341300 +341324,泗县,4,341300 +341371,宿州马鞍山现代产业园区,4,341300 +341372,宿州经济技术开发区,4,341300 +341502,金安区,4,341500 +341503,裕安区,4,341500 +341504,叶集区,4,341500 +341522,霍邱县,4,341500 +341523,舒城县,4,341500 +341524,金寨县,4,341500 +341525,霍山县,4,341500 +341602,谯城区,4,341600 +341621,涡阳县,4,341600 +341622,蒙城县,4,341600 +341623,利辛县,4,341600 +341702,贵池区,4,341700 +341721,东至县,4,341700 +341722,石台县,4,341700 +341723,青阳县,4,341700 +341802,宣州区,4,341800 +341821,郎溪县,4,341800 +341823,泾县,4,341800 +341824,绩溪县,4,341800 +341825,旌德县,4,341800 +341871,宣城市经济开发区,4,341800 +341881,宁国市,4,341800 +341882,广德市,4,341800 +350102,鼓楼区,4,350100 +350103,台江区,4,350100 +350104,仓山区,4,350100 +350105,马尾区,4,350100 +350111,晋安区,4,350100 +350112,长乐区,4,350100 +350121,闽侯县,4,350100 +350122,连江县,4,350100 +350123,罗源县,4,350100 +350124,闽清县,4,350100 +350125,永泰县,4,350100 +350128,平潭县,4,350100 +350181,福清市,4,350100 +350203,思明区,4,350200 +350205,海沧区,4,350200 +350206,湖里区,4,350200 +350211,集美区,4,350200 +350212,同安区,4,350200 +350213,翔安区,4,350200 +350302,城厢区,4,350300 +350303,涵江区,4,350300 +350304,荔城区,4,350300 +350305,秀屿区,4,350300 +350322,仙游县,4,350300 +350404,三元区,4,350400 +350405,沙县区,4,350400 +350421,明溪县,4,350400 +350423,清流县,4,350400 +350424,宁化县,4,350400 +350425,大田县,4,350400 +350426,尤溪县,4,350400 +350428,将乐县,4,350400 +350429,泰宁县,4,350400 +350430,建宁县,4,350400 +350481,永安市,4,350400 +350502,鲤城区,4,350500 +350503,丰泽区,4,350500 +350504,洛江区,4,350500 +350505,泉港区,4,350500 +350521,惠安县,4,350500 +350524,安溪县,4,350500 +350525,永春县,4,350500 +350526,德化县,4,350500 +350527,金门县,4,350500 +350581,石狮市,4,350500 +350582,晋江市,4,350500 +350583,南安市,4,350500 +350602,芗城区,4,350600 +350603,龙文区,4,350600 +350604,龙海区,4,350600 +350605,长泰区,4,350600 +350622,云霄县,4,350600 +350623,漳浦县,4,350600 +350624,诏安县,4,350600 +350626,东山县,4,350600 +350627,南靖县,4,350600 +350628,平和县,4,350600 +350629,华安县,4,350600 +350702,延平区,4,350700 +350703,建阳区,4,350700 +350721,顺昌县,4,350700 +350722,浦城县,4,350700 +350723,光泽县,4,350700 +350724,松溪县,4,350700 +350725,政和县,4,350700 +350781,邵武市,4,350700 +350782,武夷山市,4,350700 +350783,建瓯市,4,350700 +350802,新罗区,4,350800 +350803,永定区,4,350800 +350821,长汀县,4,350800 +350823,上杭县,4,350800 +350824,武平县,4,350800 +350825,连城县,4,350800 +350881,漳平市,4,350800 +350902,蕉城区,4,350900 +350921,霞浦县,4,350900 +350922,古田县,4,350900 +350923,屏南县,4,350900 +350924,寿宁县,4,350900 +350925,周宁县,4,350900 +350926,柘荣县,4,350900 +350981,福安市,4,350900 +350982,福鼎市,4,350900 +360102,东湖区,4,360100 +360103,西湖区,4,360100 +360104,青云谱区,4,360100 +360111,青山湖区,4,360100 +360112,新建区,4,360100 +360113,红谷滩区,4,360100 +360121,南昌县,4,360100 +360123,安义县,4,360100 +360124,进贤县,4,360100 +360202,昌江区,4,360200 +360203,珠山区,4,360200 +360222,浮梁县,4,360200 +360281,乐平市,4,360200 +360302,安源区,4,360300 +360313,湘东区,4,360300 +360321,莲花县,4,360300 +360322,上栗县,4,360300 +360323,芦溪县,4,360300 +360402,濂溪区,4,360400 +360403,浔阳区,4,360400 +360404,柴桑区,4,360400 +360423,武宁县,4,360400 +360424,修水县,4,360400 +360425,永修县,4,360400 +360426,德安县,4,360400 +360428,都昌县,4,360400 +360429,湖口县,4,360400 +360430,彭泽县,4,360400 +360481,瑞昌市,4,360400 +360482,共青城市,4,360400 +360483,庐山市,4,360400 +360502,渝水区,4,360500 +360521,分宜县,4,360500 +360602,月湖区,4,360600 +360603,余江区,4,360600 +360681,贵溪市,4,360600 +360702,章贡区,4,360700 +360703,南康区,4,360700 +360704,赣县区,4,360700 +360722,信丰县,4,360700 +360723,大余县,4,360700 +360724,上犹县,4,360700 +360725,崇义县,4,360700 +360726,安远县,4,360700 +360728,定南县,4,360700 +360729,全南县,4,360700 +360730,宁都县,4,360700 +360731,于都县,4,360700 +360732,兴国县,4,360700 +360733,会昌县,4,360700 +360734,寻乌县,4,360700 +360735,石城县,4,360700 +360781,瑞金市,4,360700 +360783,龙南市,4,360700 +360802,吉州区,4,360800 +360803,青原区,4,360800 +360821,吉安县,4,360800 +360822,吉水县,4,360800 +360823,峡江县,4,360800 +360824,新干县,4,360800 +360825,永丰县,4,360800 +360826,泰和县,4,360800 +360827,遂川县,4,360800 +360828,万安县,4,360800 +360829,安福县,4,360800 +360830,永新县,4,360800 +360881,井冈山市,4,360800 +360902,袁州区,4,360900 +360921,奉新县,4,360900 +360922,万载县,4,360900 +360923,上高县,4,360900 +360924,宜丰县,4,360900 +360925,靖安县,4,360900 +360926,铜鼓县,4,360900 +360981,丰城市,4,360900 +360982,樟树市,4,360900 +360983,高安市,4,360900 +361002,临川区,4,361000 +361003,东乡区,4,361000 +361021,南城县,4,361000 +361022,黎川县,4,361000 +361023,南丰县,4,361000 +361024,崇仁县,4,361000 +361025,乐安县,4,361000 +361026,宜黄县,4,361000 +361027,金溪县,4,361000 +361028,资溪县,4,361000 +361030,广昌县,4,361000 +361102,信州区,4,361100 +361103,广丰区,4,361100 +361104,广信区,4,361100 +361123,玉山县,4,361100 +361124,铅山县,4,361100 +361125,横峰县,4,361100 +361126,弋阳县,4,361100 +361127,余干县,4,361100 +361128,鄱阳县,4,361100 +361129,万年县,4,361100 +361130,婺源县,4,361100 +361181,德兴市,4,361100 +370102,历下区,4,370100 +370103,市中区,4,370100 +370104,槐荫区,4,370100 +370105,天桥区,4,370100 +370112,历城区,4,370100 +370113,长清区,4,370100 +370114,章丘区,4,370100 +370115,济阳区,4,370100 +370116,莱芜区,4,370100 +370117,钢城区,4,370100 +370124,平阴县,4,370100 +370126,商河县,4,370100 +370171,济南高新技术产业开发区,4,370100 +370202,市南区,4,370200 +370203,市北区,4,370200 +370211,黄岛区,4,370200 +370212,崂山区,4,370200 +370213,李沧区,4,370200 +370214,城阳区,4,370200 +370215,即墨区,4,370200 +370271,青岛高新技术产业开发区,4,370200 +370281,胶州市,4,370200 +370283,平度市,4,370200 +370285,莱西市,4,370200 +370302,淄川区,4,370300 +370303,张店区,4,370300 +370304,博山区,4,370300 +370305,临淄区,4,370300 +370306,周村区,4,370300 +370321,桓台县,4,370300 +370322,高青县,4,370300 +370323,沂源县,4,370300 +370402,市中区,4,370400 +370403,薛城区,4,370400 +370404,峄城区,4,370400 +370405,台儿庄区,4,370400 +370406,山亭区,4,370400 +370481,滕州市,4,370400 +370502,东营区,4,370500 +370503,河口区,4,370500 +370505,垦利区,4,370500 +370522,利津县,4,370500 +370523,广饶县,4,370500 +370571,东营经济技术开发区,4,370500 +370572,东营港经济开发区,4,370500 +370602,芝罘区,4,370600 +370611,福山区,4,370600 +370612,牟平区,4,370600 +370613,莱山区,4,370600 +370614,蓬莱区,4,370600 +370671,烟台高新技术产业开发区,4,370600 +370672,烟台经济技术开发区,4,370600 +370681,龙口市,4,370600 +370682,莱阳市,4,370600 +370683,莱州市,4,370600 +370685,招远市,4,370600 +370686,栖霞市,4,370600 +370687,海阳市,4,370600 +370702,潍城区,4,370700 +370703,寒亭区,4,370700 +370704,坊子区,4,370700 +370705,奎文区,4,370700 +370724,临朐县,4,370700 +370725,昌乐县,4,370700 +370772,潍坊滨海经济技术开发区,4,370700 +370781,青州市,4,370700 +370782,诸城市,4,370700 +370783,寿光市,4,370700 +370784,安丘市,4,370700 +370785,高密市,4,370700 +370786,昌邑市,4,370700 +370811,任城区,4,370800 +370812,兖州区,4,370800 +370826,微山县,4,370800 +370827,鱼台县,4,370800 +370828,金乡县,4,370800 +370829,嘉祥县,4,370800 +370830,汶上县,4,370800 +370831,泗水县,4,370800 +370832,梁山县,4,370800 +370871,济宁高新技术产业开发区,4,370800 +370881,曲阜市,4,370800 +370883,邹城市,4,370800 +370902,泰山区,4,370900 +370911,岱岳区,4,370900 +370921,宁阳县,4,370900 +370923,东平县,4,370900 +370982,新泰市,4,370900 +370983,肥城市,4,370900 +371002,环翠区,4,371000 +371003,文登区,4,371000 +371071,威海火炬高技术产业开发区,4,371000 +371072,威海经济技术开发区,4,371000 +371073,威海临港经济技术开发区,4,371000 +371082,荣成市,4,371000 +371083,乳山市,4,371000 +371102,东港区,4,371100 +371103,岚山区,4,371100 +371121,五莲县,4,371100 +371122,莒县,4,371100 +371171,日照经济技术开发区,4,371100 +371302,兰山区,4,371300 +371311,罗庄区,4,371300 +371312,河东区,4,371300 +371321,沂南县,4,371300 +371322,郯城县,4,371300 +371323,沂水县,4,371300 +371324,兰陵县,4,371300 +371325,费县,4,371300 +371326,平邑县,4,371300 +371327,莒南县,4,371300 +371328,蒙阴县,4,371300 +371329,临沭县,4,371300 +371371,临沂高新技术产业开发区,4,371300 +371402,德城区,4,371400 +371403,陵城区,4,371400 +371422,宁津县,4,371400 +371423,庆云县,4,371400 +371424,临邑县,4,371400 +371425,齐河县,4,371400 +371426,平原县,4,371400 +371427,夏津县,4,371400 +371428,武城县,4,371400 +371471,德州经济技术开发区,4,371400 +371472,德州运河经济开发区,4,371400 +371481,乐陵市,4,371400 +371482,禹城市,4,371400 +371502,东昌府区,4,371500 +371503,茌平区,4,371500 +371521,阳谷县,4,371500 +371522,莘县,4,371500 +371524,东阿县,4,371500 +371525,冠县,4,371500 +371526,高唐县,4,371500 +371581,临清市,4,371500 +371602,滨城区,4,371600 +371603,沾化区,4,371600 +371621,惠民县,4,371600 +371622,阳信县,4,371600 +371623,无棣县,4,371600 +371625,博兴县,4,371600 +371681,邹平市,4,371600 +371702,牡丹区,4,371700 +371703,定陶区,4,371700 +371721,曹县,4,371700 +371722,单县,4,371700 +371723,成武县,4,371700 +371724,巨野县,4,371700 +371725,郓城县,4,371700 +371726,鄄城县,4,371700 +371728,东明县,4,371700 +371771,菏泽经济技术开发区,4,371700 +371772,菏泽高新技术开发区,4,371700 +410102,中原区,4,410100 +410103,二七区,4,410100 +410104,管城回族区,4,410100 +410105,金水区,4,410100 +410106,上街区,4,410100 +410108,惠济区,4,410100 +410122,中牟县,4,410100 +410171,郑州经济技术开发区,4,410100 +410172,郑州高新技术产业开发区,4,410100 +410173,郑州航空港经济综合实验区,4,410100 +410181,巩义市,4,410100 +410182,荥阳市,4,410100 +410183,新密市,4,410100 +410184,新郑市,4,410100 +410185,登封市,4,410100 +410202,龙亭区,4,410200 +410203,顺河回族区,4,410200 +410204,鼓楼区,4,410200 +410205,禹王台区,4,410200 +410212,祥符区,4,410200 +410221,杞县,4,410200 +410222,通许县,4,410200 +410223,尉氏县,4,410200 +410225,兰考县,4,410200 +410302,老城区,4,410300 +410303,西工区,4,410300 +410304,瀍河回族区,4,410300 +410305,涧西区,4,410300 +410307,偃师区,4,410300 +410308,孟津区,4,410300 +410311,洛龙区,4,410300 +410323,新安县,4,410300 +410324,栾川县,4,410300 +410325,嵩县,4,410300 +410326,汝阳县,4,410300 +410327,宜阳县,4,410300 +410328,洛宁县,4,410300 +410329,伊川县,4,410300 +410371,洛阳高新技术产业开发区,4,410300 +410402,新华区,4,410400 +410403,卫东区,4,410400 +410404,石龙区,4,410400 +410411,湛河区,4,410400 +410421,宝丰县,4,410400 +410422,叶县,4,410400 +410423,鲁山县,4,410400 +410425,郏县,4,410400 +410471,平顶山高新技术产业开发区,4,410400 +410472,平顶山市城乡一体化示范区,4,410400 +410481,舞钢市,4,410400 +410482,汝州市,4,410400 +410502,文峰区,4,410500 +410503,北关区,4,410500 +410505,殷都区,4,410500 +410506,龙安区,4,410500 +410522,安阳县,4,410500 +410523,汤阴县,4,410500 +410526,滑县,4,410500 +410527,内黄县,4,410500 +410571,安阳高新技术产业开发区,4,410500 +410581,林州市,4,410500 +410602,鹤山区,4,410600 +410603,山城区,4,410600 +410611,淇滨区,4,410600 +410621,浚县,4,410600 +410622,淇县,4,410600 +410671,鹤壁经济技术开发区,4,410600 +410702,红旗区,4,410700 +410703,卫滨区,4,410700 +410704,凤泉区,4,410700 +410711,牧野区,4,410700 +410721,新乡县,4,410700 +410724,获嘉县,4,410700 +410725,原阳县,4,410700 +410726,延津县,4,410700 +410727,封丘县,4,410700 +410771,新乡高新技术产业开发区,4,410700 +410772,新乡经济技术开发区,4,410700 +410773,新乡市平原城乡一体化示范区,4,410700 +410781,卫辉市,4,410700 +410782,辉县市,4,410700 +410783,长垣市,4,410700 +410802,解放区,4,410800 +410803,中站区,4,410800 +410804,马村区,4,410800 +410811,山阳区,4,410800 +410821,修武县,4,410800 +410822,博爱县,4,410800 +410823,武陟县,4,410800 +410825,温县,4,410800 +410871,焦作城乡一体化示范区,4,410800 +410882,沁阳市,4,410800 +410883,孟州市,4,410800 +410902,华龙区,4,410900 +410922,清丰县,4,410900 +410923,南乐县,4,410900 +410926,范县,4,410900 +410927,台前县,4,410900 +410928,濮阳县,4,410900 +410971,河南濮阳工业园区,4,410900 +410972,濮阳经济技术开发区,4,410900 +411002,魏都区,4,411000 +411003,建安区,4,411000 +411024,鄢陵县,4,411000 +411025,襄城县,4,411000 +411071,许昌经济技术开发区,4,411000 +411081,禹州市,4,411000 +411082,长葛市,4,411000 +411102,源汇区,4,411100 +411103,郾城区,4,411100 +411104,召陵区,4,411100 +411121,舞阳县,4,411100 +411122,临颍县,4,411100 +411171,漯河经济技术开发区,4,411100 +411202,湖滨区,4,411200 +411203,陕州区,4,411200 +411221,渑池县,4,411200 +411224,卢氏县,4,411200 +411271,河南三门峡经济开发区,4,411200 +411281,义马市,4,411200 +411282,灵宝市,4,411200 +411302,宛城区,4,411300 +411303,卧龙区,4,411300 +411321,南召县,4,411300 +411322,方城县,4,411300 +411323,西峡县,4,411300 +411324,镇平县,4,411300 +411325,内乡县,4,411300 +411326,淅川县,4,411300 +411327,社旗县,4,411300 +411328,唐河县,4,411300 +411329,新野县,4,411300 +411330,桐柏县,4,411300 +411371,南阳高新技术产业开发区,4,411300 +411372,南阳市城乡一体化示范区,4,411300 +411381,邓州市,4,411300 +411402,梁园区,4,411400 +411403,睢阳区,4,411400 +411421,民权县,4,411400 +411422,睢县,4,411400 +411423,宁陵县,4,411400 +411424,柘城县,4,411400 +411425,虞城县,4,411400 +411426,夏邑县,4,411400 +411471,豫东综合物流产业聚集区,4,411400 +411472,河南商丘经济开发区,4,411400 +411481,永城市,4,411400 +411502,浉河区,4,411500 +411503,平桥区,4,411500 +411521,罗山县,4,411500 +411522,光山县,4,411500 +411523,新县,4,411500 +411524,商城县,4,411500 +411525,固始县,4,411500 +411526,潢川县,4,411500 +411527,淮滨县,4,411500 +411528,息县,4,411500 +411571,信阳高新技术产业开发区,4,411500 +411602,川汇区,4,411600 +411603,淮阳区,4,411600 +411621,扶沟县,4,411600 +411622,西华县,4,411600 +411623,商水县,4,411600 +411624,沈丘县,4,411600 +411625,郸城县,4,411600 +411627,太康县,4,411600 +411628,鹿邑县,4,411600 +411671,河南周口经济开发区,4,411600 +411681,项城市,4,411600 +411702,驿城区,4,411700 +411721,西平县,4,411700 +411722,上蔡县,4,411700 +411723,平舆县,4,411700 +411724,正阳县,4,411700 +411725,确山县,4,411700 +411726,泌阳县,4,411700 +411727,汝南县,4,411700 +411728,遂平县,4,411700 +411729,新蔡县,4,411700 +411771,河南驻马店经济开发区,4,411700 +419001,济源市,4,419000 +420102,江岸区,4,420100 +420103,江汉区,4,420100 +420104,硚口区,4,420100 +420105,汉阳区,4,420100 +420106,武昌区,4,420100 +420107,青山区,4,420100 +420111,洪山区,4,420100 +420112,东西湖区,4,420100 +420113,汉南区,4,420100 +420114,蔡甸区,4,420100 +420115,江夏区,4,420100 +420116,黄陂区,4,420100 +420117,新洲区,4,420100 +420202,黄石港区,4,420200 +420203,西塞山区,4,420200 +420204,下陆区,4,420200 +420205,铁山区,4,420200 +420222,阳新县,4,420200 +420281,大冶市,4,420200 +420302,茅箭区,4,420300 +420303,张湾区,4,420300 +420304,郧阳区,4,420300 +420322,郧西县,4,420300 +420323,竹山县,4,420300 +420324,竹溪县,4,420300 +420325,房县,4,420300 +420381,丹江口市,4,420300 +420502,西陵区,4,420500 +420503,伍家岗区,4,420500 +420504,点军区,4,420500 +420505,猇亭区,4,420500 +420506,夷陵区,4,420500 +420525,远安县,4,420500 +420526,兴山县,4,420500 +420527,秭归县,4,420500 +420528,长阳土家族自治县,4,420500 +420529,五峰土家族自治县,4,420500 +420581,宜都市,4,420500 +420582,当阳市,4,420500 +420583,枝江市,4,420500 +420602,襄城区,4,420600 +420606,樊城区,4,420600 +420607,襄州区,4,420600 +420624,南漳县,4,420600 +420625,谷城县,4,420600 +420626,保康县,4,420600 +420682,老河口市,4,420600 +420683,枣阳市,4,420600 +420684,宜城市,4,420600 +420702,梁子湖区,4,420700 +420703,华容区,4,420700 +420704,鄂城区,4,420700 +420802,东宝区,4,420800 +420804,掇刀区,4,420800 +420822,沙洋县,4,420800 +420881,钟祥市,4,420800 +420882,京山市,4,420800 +420902,孝南区,4,420900 +420921,孝昌县,4,420900 +420922,大悟县,4,420900 +420923,云梦县,4,420900 +420981,应城市,4,420900 +420982,安陆市,4,420900 +420984,汉川市,4,420900 +421002,沙市区,4,421000 +421003,荆州区,4,421000 +421022,公安县,4,421000 +421024,江陵县,4,421000 +421071,荆州经济技术开发区,4,421000 +421081,石首市,4,421000 +421083,洪湖市,4,421000 +421087,松滋市,4,421000 +421088,监利市,4,421000 +421102,黄州区,4,421100 +421121,团风县,4,421100 +421122,红安县,4,421100 +421123,罗田县,4,421100 +421124,英山县,4,421100 +421125,浠水县,4,421100 +421126,蕲春县,4,421100 +421127,黄梅县,4,421100 +421171,龙感湖管理区,4,421100 +421181,麻城市,4,421100 +421182,武穴市,4,421100 +421202,咸安区,4,421200 +421221,嘉鱼县,4,421200 +421222,通城县,4,421200 +421223,崇阳县,4,421200 +421224,通山县,4,421200 +421281,赤壁市,4,421200 +421303,曾都区,4,421300 +421321,随县,4,421300 +421381,广水市,4,421300 +422801,恩施市,4,422800 +422802,利川市,4,422800 +422822,建始县,4,422800 +422823,巴东县,4,422800 +422825,宣恩县,4,422800 +422826,咸丰县,4,422800 +422827,来凤县,4,422800 +422828,鹤峰县,4,422800 +429004,仙桃市,4,429000 +429005,潜江市,4,429000 +429006,天门市,4,429000 +429021,神农架林区,4,429000 +430102,芙蓉区,4,430100 +430103,天心区,4,430100 +430104,岳麓区,4,430100 +430105,开福区,4,430100 +430111,雨花区,4,430100 +430112,望城区,4,430100 +430121,长沙县,4,430100 +430181,浏阳市,4,430100 +430182,宁乡市,4,430100 +430202,荷塘区,4,430200 +430203,芦淞区,4,430200 +430204,石峰区,4,430200 +430211,天元区,4,430200 +430212,渌口区,4,430200 +430223,攸县,4,430200 +430224,茶陵县,4,430200 +430225,炎陵县,4,430200 +430271,云龙示范区,4,430200 +430281,醴陵市,4,430200 +430302,雨湖区,4,430300 +430304,岳塘区,4,430300 +430321,湘潭县,4,430300 +430371,湖南湘潭高新技术产业园区,4,430300 +430372,湘潭昭山示范区,4,430300 +430373,湘潭九华示范区,4,430300 +430381,湘乡市,4,430300 +430382,韶山市,4,430300 +430405,珠晖区,4,430400 +430406,雁峰区,4,430400 +430407,石鼓区,4,430400 +430408,蒸湘区,4,430400 +430412,南岳区,4,430400 +430421,衡阳县,4,430400 +430422,衡南县,4,430400 +430423,衡山县,4,430400 +430424,衡东县,4,430400 +430426,祁东县,4,430400 +430471,衡阳综合保税区,4,430400 +430472,湖南衡阳高新技术产业园区,4,430400 +430473,湖南衡阳松木经济开发区,4,430400 +430481,耒阳市,4,430400 +430482,常宁市,4,430400 +430502,双清区,4,430500 +430503,大祥区,4,430500 +430511,北塔区,4,430500 +430522,新邵县,4,430500 +430523,邵阳县,4,430500 +430524,隆回县,4,430500 +430525,洞口县,4,430500 +430527,绥宁县,4,430500 +430528,新宁县,4,430500 +430529,城步苗族自治县,4,430500 +430581,武冈市,4,430500 +430582,邵东市,4,430500 +430602,岳阳楼区,4,430600 +430603,云溪区,4,430600 +430611,君山区,4,430600 +430621,岳阳县,4,430600 +430623,华容县,4,430600 +430624,湘阴县,4,430600 +430626,平江县,4,430600 +430671,岳阳市屈原管理区,4,430600 +430681,汨罗市,4,430600 +430682,临湘市,4,430600 +430702,武陵区,4,430700 +430703,鼎城区,4,430700 +430721,安乡县,4,430700 +430722,汉寿县,4,430700 +430723,澧县,4,430700 +430724,临澧县,4,430700 +430725,桃源县,4,430700 +430726,石门县,4,430700 +430771,常德市西洞庭管理区,4,430700 +430781,津市市,4,430700 +430802,永定区,4,430800 +430811,武陵源区,4,430800 +430821,慈利县,4,430800 +430822,桑植县,4,430800 +430902,资阳区,4,430900 +430903,赫山区,4,430900 +430921,南县,4,430900 +430922,桃江县,4,430900 +430923,安化县,4,430900 +430971,益阳市大通湖管理区,4,430900 +430972,湖南益阳高新技术产业园区,4,430900 +430981,沅江市,4,430900 +431002,北湖区,4,431000 +431003,苏仙区,4,431000 +431021,桂阳县,4,431000 +431022,宜章县,4,431000 +431023,永兴县,4,431000 +431024,嘉禾县,4,431000 +431025,临武县,4,431000 +431026,汝城县,4,431000 +431027,桂东县,4,431000 +431028,安仁县,4,431000 +431081,资兴市,4,431000 +431102,零陵区,4,431100 +431103,冷水滩区,4,431100 +431122,东安县,4,431100 +431123,双牌县,4,431100 +431124,道县,4,431100 +431125,江永县,4,431100 +431126,宁远县,4,431100 +431127,蓝山县,4,431100 +431128,新田县,4,431100 +431129,江华瑶族自治县,4,431100 +431171,永州经济技术开发区,4,431100 +431173,永州市回龙圩管理区,4,431100 +431181,祁阳市,4,431100 +431202,鹤城区,4,431200 +431221,中方县,4,431200 +431222,沅陵县,4,431200 +431223,辰溪县,4,431200 +431224,溆浦县,4,431200 +431225,会同县,4,431200 +431226,麻阳苗族自治县,4,431200 +431227,新晃侗族自治县,4,431200 +431228,芷江侗族自治县,4,431200 +431229,靖州苗族侗族自治县,4,431200 +431230,通道侗族自治县,4,431200 +431271,怀化市洪江管理区,4,431200 +431281,洪江市,4,431200 +431302,娄星区,4,431300 +431321,双峰县,4,431300 +431322,新化县,4,431300 +431381,冷水江市,4,431300 +431382,涟源市,4,431300 +433101,吉首市,4,433100 +433122,泸溪县,4,433100 +433123,凤凰县,4,433100 +433124,花垣县,4,433100 +433125,保靖县,4,433100 +433126,古丈县,4,433100 +433127,永顺县,4,433100 +433130,龙山县,4,433100 +440103,荔湾区,4,440100 +440104,越秀区,4,440100 +440105,海珠区,4,440100 +440106,天河区,4,440100 +440111,白云区,4,440100 +440112,黄埔区,4,440100 +440113,番禺区,4,440100 +440114,花都区,4,440100 +440115,南沙区,4,440100 +440117,从化区,4,440100 +440118,增城区,4,440100 +440203,武江区,4,440200 +440204,浈江区,4,440200 +440205,曲江区,4,440200 +440222,始兴县,4,440200 +440224,仁化县,4,440200 +440229,翁源县,4,440200 +440232,乳源瑶族自治县,4,440200 +440233,新丰县,4,440200 +440281,乐昌市,4,440200 +440282,南雄市,4,440200 +440303,罗湖区,4,440300 +440304,福田区,4,440300 +440305,南山区,4,440300 +440306,宝安区,4,440300 +440307,龙岗区,4,440300 +440308,盐田区,4,440300 +440309,龙华区,4,440300 +440310,坪山区,4,440300 +440311,光明区,4,440300 +440402,香洲区,4,440400 +440403,斗门区,4,440400 +440404,金湾区,4,440400 +440507,龙湖区,4,440500 +440511,金平区,4,440500 +440512,濠江区,4,440500 +440513,潮阳区,4,440500 +440514,潮南区,4,440500 +440515,澄海区,4,440500 +440523,南澳县,4,440500 +440604,禅城区,4,440600 +440605,南海区,4,440600 +440606,顺德区,4,440600 +440607,三水区,4,440600 +440608,高明区,4,440600 +440703,蓬江区,4,440700 +440704,江海区,4,440700 +440705,新会区,4,440700 +440781,台山市,4,440700 +440783,开平市,4,440700 +440784,鹤山市,4,440700 +440785,恩平市,4,440700 +440802,赤坎区,4,440800 +440803,霞山区,4,440800 +440804,坡头区,4,440800 +440811,麻章区,4,440800 +440823,遂溪县,4,440800 +440825,徐闻县,4,440800 +440881,廉江市,4,440800 +440882,雷州市,4,440800 +440883,吴川市,4,440800 +440902,茂南区,4,440900 +440904,电白区,4,440900 +440981,高州市,4,440900 +440982,化州市,4,440900 +440983,信宜市,4,440900 +441202,端州区,4,441200 +441203,鼎湖区,4,441200 +441204,高要区,4,441200 +441223,广宁县,4,441200 +441224,怀集县,4,441200 +441225,封开县,4,441200 +441226,德庆县,4,441200 +441284,四会市,4,441200 +441302,惠城区,4,441300 +441303,惠阳区,4,441300 +441322,博罗县,4,441300 +441323,惠东县,4,441300 +441324,龙门县,4,441300 +441402,梅江区,4,441400 +441403,梅县区,4,441400 +441422,大埔县,4,441400 +441423,丰顺县,4,441400 +441424,五华县,4,441400 +441426,平远县,4,441400 +441427,蕉岭县,4,441400 +441481,兴宁市,4,441400 +441502,城区,4,441500 +441521,海丰县,4,441500 +441523,陆河县,4,441500 +441581,陆丰市,4,441500 +441602,源城区,4,441600 +441621,紫金县,4,441600 +441622,龙川县,4,441600 +441623,连平县,4,441600 +441624,和平县,4,441600 +441625,东源县,4,441600 +441702,江城区,4,441700 +441704,阳东区,4,441700 +441721,阳西县,4,441700 +441781,阳春市,4,441700 +441802,清城区,4,441800 +441803,清新区,4,441800 +441821,佛冈县,4,441800 +441823,阳山县,4,441800 +441825,连山壮族瑶族自治县,4,441800 +441826,连南瑶族自治县,4,441800 +441881,英德市,4,441800 +441882,连州市,4,441800 +445102,湘桥区,4,445100 +445103,潮安区,4,445100 +445122,饶平县,4,445100 +445202,榕城区,4,445200 +445203,揭东区,4,445200 +445222,揭西县,4,445200 +445224,惠来县,4,445200 +445281,普宁市,4,445200 +445302,云城区,4,445300 +445303,云安区,4,445300 +445321,新兴县,4,445300 +445322,郁南县,4,445300 +445381,罗定市,4,445300 +450102,兴宁区,4,450100 +450103,青秀区,4,450100 +450105,江南区,4,450100 +450107,西乡塘区,4,450100 +450108,良庆区,4,450100 +450109,邕宁区,4,450100 +450110,武鸣区,4,450100 +450123,隆安县,4,450100 +450124,马山县,4,450100 +450125,上林县,4,450100 +450126,宾阳县,4,450100 +450181,横州市,4,450100 +450202,城中区,4,450200 +450203,鱼峰区,4,450200 +450204,柳南区,4,450200 +450205,柳北区,4,450200 +450206,柳江区,4,450200 +450222,柳城县,4,450200 +450223,鹿寨县,4,450200 +450224,融安县,4,450200 +450225,融水苗族自治县,4,450200 +450226,三江侗族自治县,4,450200 +450302,秀峰区,4,450300 +450303,叠彩区,4,450300 +450304,象山区,4,450300 +450305,七星区,4,450300 +450311,雁山区,4,450300 +450312,临桂区,4,450300 +450321,阳朔县,4,450300 +450323,灵川县,4,450300 +450324,全州县,4,450300 +450325,兴安县,4,450300 +450326,永福县,4,450300 +450327,灌阳县,4,450300 +450328,龙胜各族自治县,4,450300 +450329,资源县,4,450300 +450330,平乐县,4,450300 +450332,恭城瑶族自治县,4,450300 +450381,荔浦市,4,450300 +450403,万秀区,4,450400 +450405,长洲区,4,450400 +450406,龙圩区,4,450400 +450421,苍梧县,4,450400 +450422,藤县,4,450400 +450423,蒙山县,4,450400 +450481,岑溪市,4,450400 +450502,海城区,4,450500 +450503,银海区,4,450500 +450512,铁山港区,4,450500 +450521,合浦县,4,450500 +450602,港口区,4,450600 +450603,防城区,4,450600 +450621,上思县,4,450600 +450681,东兴市,4,450600 +450702,钦南区,4,450700 +450703,钦北区,4,450700 +450721,灵山县,4,450700 +450722,浦北县,4,450700 +450802,港北区,4,450800 +450803,港南区,4,450800 +450804,覃塘区,4,450800 +450821,平南县,4,450800 +450881,桂平市,4,450800 +450902,玉州区,4,450900 +450903,福绵区,4,450900 +450921,容县,4,450900 +450922,陆川县,4,450900 +450923,博白县,4,450900 +450924,兴业县,4,450900 +450981,北流市,4,450900 +451002,右江区,4,451000 +451003,田阳区,4,451000 +451022,田东县,4,451000 +451024,德保县,4,451000 +451026,那坡县,4,451000 +451027,凌云县,4,451000 +451028,乐业县,4,451000 +451029,田林县,4,451000 +451030,西林县,4,451000 +451031,隆林各族自治县,4,451000 +451081,靖西市,4,451000 +451082,平果市,4,451000 +451102,八步区,4,451100 +451103,平桂区,4,451100 +451121,昭平县,4,451100 +451122,钟山县,4,451100 +451123,富川瑶族自治县,4,451100 +451202,金城江区,4,451200 +451203,宜州区,4,451200 +451221,南丹县,4,451200 +451222,天峨县,4,451200 +451223,凤山县,4,451200 +451224,东兰县,4,451200 +451225,罗城仫佬族自治县,4,451200 +451226,环江毛南族自治县,4,451200 +451227,巴马瑶族自治县,4,451200 +451228,都安瑶族自治县,4,451200 +451229,大化瑶族自治县,4,451200 +451302,兴宾区,4,451300 +451321,忻城县,4,451300 +451322,象州县,4,451300 +451323,武宣县,4,451300 +451324,金秀瑶族自治县,4,451300 +451381,合山市,4,451300 +451402,江州区,4,451400 +451421,扶绥县,4,451400 +451422,宁明县,4,451400 +451423,龙州县,4,451400 +451424,大新县,4,451400 +451425,天等县,4,451400 +451481,凭祥市,4,451400 +460105,秀英区,4,460100 +460106,龙华区,4,460100 +460107,琼山区,4,460100 +460108,美兰区,4,460100 +460202,海棠区,4,460200 +460203,吉阳区,4,460200 +460204,天涯区,4,460200 +460205,崖州区,4,460200 +460321,西沙群岛,4,460300 +460322,南沙群岛,4,460300 +460323,中沙群岛的岛礁及其海域,4,460300 +469001,五指山市,4,469000 +469002,琼海市,4,469000 +469005,文昌市,4,469000 +469006,万宁市,4,469000 +469007,东方市,4,469000 +469021,定安县,4,469000 +469022,屯昌县,4,469000 +469023,澄迈县,4,469000 +469024,临高县,4,469000 +469025,白沙黎族自治县,4,469000 +469026,昌江黎族自治县,4,469000 +469027,乐东黎族自治县,4,469000 +469028,陵水黎族自治县,4,469000 +469029,保亭黎族苗族自治县,4,469000 +469030,琼中黎族苗族自治县,4,469000 +500101,万州区,4,500100 +500102,涪陵区,4,500100 +500103,渝中区,4,500100 +500104,大渡口区,4,500100 +500105,江北区,4,500100 +500106,沙坪坝区,4,500100 +500107,九龙坡区,4,500100 +500108,南岸区,4,500100 +500109,北碚区,4,500100 +500110,綦江区,4,500100 +500111,大足区,4,500100 +500112,渝北区,4,500100 +500113,巴南区,4,500100 +500114,黔江区,4,500100 +500115,长寿区,4,500100 +500116,江津区,4,500100 +500117,合川区,4,500100 +500118,永川区,4,500100 +500119,南川区,4,500100 +500120,璧山区,4,500100 +500151,铜梁区,4,500100 +500152,潼南区,4,500100 +500153,荣昌区,4,500100 +500154,开州区,4,500100 +500155,梁平区,4,500100 +500156,武隆区,4,500100 +500229,城口县,4,500100 +500230,丰都县,4,500100 +500231,垫江县,4,500100 +500233,忠县,4,500100 +500235,云阳县,4,500100 +500236,奉节县,4,500100 +500237,巫山县,4,500100 +500238,巫溪县,4,500100 +500240,石柱土家族自治县,4,500100 +500241,秀山土家族苗族自治县,4,500100 +500242,酉阳土家族苗族自治县,4,500100 +500243,彭水苗族土家族自治县,4,500100 +510104,锦江区,4,510100 +510105,青羊区,4,510100 +510106,金牛区,4,510100 +510107,武侯区,4,510100 +510108,成华区,4,510100 +510112,龙泉驿区,4,510100 +510113,青白江区,4,510100 +510114,新都区,4,510100 +510115,温江区,4,510100 +510116,双流区,4,510100 +510117,郫都区,4,510100 +510118,新津区,4,510100 +510121,金堂县,4,510100 +510129,大邑县,4,510100 +510131,蒲江县,4,510100 +510181,都江堰市,4,510100 +510182,彭州市,4,510100 +510183,邛崃市,4,510100 +510184,崇州市,4,510100 +510185,简阳市,4,510100 +510302,自流井区,4,510300 +510303,贡井区,4,510300 +510304,大安区,4,510300 +510311,沿滩区,4,510300 +510321,荣县,4,510300 +510322,富顺县,4,510300 +510402,东区,4,510400 +510403,西区,4,510400 +510411,仁和区,4,510400 +510421,米易县,4,510400 +510422,盐边县,4,510400 +510502,江阳区,4,510500 +510503,纳溪区,4,510500 +510504,龙马潭区,4,510500 +510521,泸县,4,510500 +510522,合江县,4,510500 +510524,叙永县,4,510500 +510525,古蔺县,4,510500 +510603,旌阳区,4,510600 +510604,罗江区,4,510600 +510623,中江县,4,510600 +510681,广汉市,4,510600 +510682,什邡市,4,510600 +510683,绵竹市,4,510600 +510703,涪城区,4,510700 +510704,游仙区,4,510700 +510705,安州区,4,510700 +510722,三台县,4,510700 +510723,盐亭县,4,510700 +510725,梓潼县,4,510700 +510726,北川羌族自治县,4,510700 +510727,平武县,4,510700 +510781,江油市,4,510700 +510802,利州区,4,510800 +510811,昭化区,4,510800 +510812,朝天区,4,510800 +510821,旺苍县,4,510800 +510822,青川县,4,510800 +510823,剑阁县,4,510800 +510824,苍溪县,4,510800 +510903,船山区,4,510900 +510904,安居区,4,510900 +510921,蓬溪县,4,510900 +510923,大英县,4,510900 +510981,射洪市,4,510900 +511002,市中区,4,511000 +511011,东兴区,4,511000 +511024,威远县,4,511000 +511025,资中县,4,511000 +511071,内江经济开发区,4,511000 +511083,隆昌市,4,511000 +511102,市中区,4,511100 +511111,沙湾区,4,511100 +511112,五通桥区,4,511100 +511113,金口河区,4,511100 +511123,犍为县,4,511100 +511124,井研县,4,511100 +511126,夹江县,4,511100 +511129,沐川县,4,511100 +511132,峨边彝族自治县,4,511100 +511133,马边彝族自治县,4,511100 +511181,峨眉山市,4,511100 +511302,顺庆区,4,511300 +511303,高坪区,4,511300 +511304,嘉陵区,4,511300 +511321,南部县,4,511300 +511322,营山县,4,511300 +511323,蓬安县,4,511300 +511324,仪陇县,4,511300 +511325,西充县,4,511300 +511381,阆中市,4,511300 +511402,东坡区,4,511400 +511403,彭山区,4,511400 +511421,仁寿县,4,511400 +511423,洪雅县,4,511400 +511424,丹棱县,4,511400 +511425,青神县,4,511400 +511502,翠屏区,4,511500 +511503,南溪区,4,511500 +511504,叙州区,4,511500 +511523,江安县,4,511500 +511524,长宁县,4,511500 +511525,高县,4,511500 +511526,珙县,4,511500 +511527,筠连县,4,511500 +511528,兴文县,4,511500 +511529,屏山县,4,511500 +511602,广安区,4,511600 +511603,前锋区,4,511600 +511621,岳池县,4,511600 +511622,武胜县,4,511600 +511623,邻水县,4,511600 +511681,华蓥市,4,511600 +511702,通川区,4,511700 +511703,达川区,4,511700 +511722,宣汉县,4,511700 +511723,开江县,4,511700 +511724,大竹县,4,511700 +511725,渠县,4,511700 +511771,达州经济开发区,4,511700 +511781,万源市,4,511700 +511802,雨城区,4,511800 +511803,名山区,4,511800 +511822,荥经县,4,511800 +511823,汉源县,4,511800 +511824,石棉县,4,511800 +511825,天全县,4,511800 +511826,芦山县,4,511800 +511827,宝兴县,4,511800 +511902,巴州区,4,511900 +511903,恩阳区,4,511900 +511921,通江县,4,511900 +511922,南江县,4,511900 +511923,平昌县,4,511900 +511971,巴中经济开发区,4,511900 +512002,雁江区,4,512000 +512021,安岳县,4,512000 +512022,乐至县,4,512000 +513201,马尔康市,4,513200 +513221,汶川县,4,513200 +513222,理县,4,513200 +513223,茂县,4,513200 +513224,松潘县,4,513200 +513225,九寨沟县,4,513200 +513226,金川县,4,513200 +513227,小金县,4,513200 +513228,黑水县,4,513200 +513230,壤塘县,4,513200 +513231,阿坝县,4,513200 +513232,若尔盖县,4,513200 +513233,红原县,4,513200 +513301,康定市,4,513300 +513322,泸定县,4,513300 +513323,丹巴县,4,513300 +513324,九龙县,4,513300 +513325,雅江县,4,513300 +513326,道孚县,4,513300 +513327,炉霍县,4,513300 +513328,甘孜县,4,513300 +513329,新龙县,4,513300 +513330,德格县,4,513300 +513331,白玉县,4,513300 +513332,石渠县,4,513300 +513333,色达县,4,513300 +513334,理塘县,4,513300 +513335,巴塘县,4,513300 +513336,乡城县,4,513300 +513337,稻城县,4,513300 +513338,得荣县,4,513300 +513401,西昌市,4,513400 +513402,会理市,4,513400 +513422,木里藏族自治县,4,513400 +513423,盐源县,4,513400 +513424,德昌县,4,513400 +513426,会东县,4,513400 +513427,宁南县,4,513400 +513428,普格县,4,513400 +513429,布拖县,4,513400 +513430,金阳县,4,513400 +513431,昭觉县,4,513400 +513432,喜德县,4,513400 +513433,冕宁县,4,513400 +513434,越西县,4,513400 +513435,甘洛县,4,513400 +513436,美姑县,4,513400 +513437,雷波县,4,513400 +520102,南明区,4,520100 +520103,云岩区,4,520100 +520111,花溪区,4,520100 +520112,乌当区,4,520100 +520113,白云区,4,520100 +520115,观山湖区,4,520100 +520121,开阳县,4,520100 +520122,息烽县,4,520100 +520123,修文县,4,520100 +520181,清镇市,4,520100 +520201,钟山区,4,520200 +520203,六枝特区,4,520200 +520204,水城区,4,520200 +520281,盘州市,4,520200 +520302,红花岗区,4,520300 +520303,汇川区,4,520300 +520304,播州区,4,520300 +520322,桐梓县,4,520300 +520323,绥阳县,4,520300 +520324,正安县,4,520300 +520325,道真仡佬族苗族自治县,4,520300 +520326,务川仡佬族苗族自治县,4,520300 +520327,凤冈县,4,520300 +520328,湄潭县,4,520300 +520329,余庆县,4,520300 +520330,习水县,4,520300 +520381,赤水市,4,520300 +520382,仁怀市,4,520300 +520402,西秀区,4,520400 +520403,平坝区,4,520400 +520422,普定县,4,520400 +520423,镇宁布依族苗族自治县,4,520400 +520424,关岭布依族苗族自治县,4,520400 +520425,紫云苗族布依族自治县,4,520400 +520502,七星关区,4,520500 +520521,大方县,4,520500 +520523,金沙县,4,520500 +520524,织金县,4,520500 +520525,纳雍县,4,520500 +520526,威宁彝族回族苗族自治县,4,520500 +520527,赫章县,4,520500 +520581,黔西市,4,520500 +520602,碧江区,4,520600 +520603,万山区,4,520600 +520621,江口县,4,520600 +520622,玉屏侗族自治县,4,520600 +520623,石阡县,4,520600 +520624,思南县,4,520600 +520625,印江土家族苗族自治县,4,520600 +520626,德江县,4,520600 +520627,沿河土家族自治县,4,520600 +520628,松桃苗族自治县,4,520600 +522301,兴义市,4,522300 +522302,兴仁市,4,522300 +522323,普安县,4,522300 +522324,晴隆县,4,522300 +522325,贞丰县,4,522300 +522326,望谟县,4,522300 +522327,册亨县,4,522300 +522328,安龙县,4,522300 +522601,凯里市,4,522600 +522622,黄平县,4,522600 +522623,施秉县,4,522600 +522624,三穗县,4,522600 +522625,镇远县,4,522600 +522626,岑巩县,4,522600 +522627,天柱县,4,522600 +522628,锦屏县,4,522600 +522629,剑河县,4,522600 +522630,台江县,4,522600 +522631,黎平县,4,522600 +522632,榕江县,4,522600 +522633,从江县,4,522600 +522634,雷山县,4,522600 +522635,麻江县,4,522600 +522636,丹寨县,4,522600 +522701,都匀市,4,522700 +522702,福泉市,4,522700 +522722,荔波县,4,522700 +522723,贵定县,4,522700 +522725,瓮安县,4,522700 +522726,独山县,4,522700 +522727,平塘县,4,522700 +522728,罗甸县,4,522700 +522729,长顺县,4,522700 +522730,龙里县,4,522700 +522731,惠水县,4,522700 +522732,三都水族自治县,4,522700 +530102,五华区,4,530100 +530103,盘龙区,4,530100 +530111,官渡区,4,530100 +530112,西山区,4,530100 +530113,东川区,4,530100 +530114,呈贡区,4,530100 +530115,晋宁区,4,530100 +530124,富民县,4,530100 +530125,宜良县,4,530100 +530126,石林彝族自治县,4,530100 +530127,嵩明县,4,530100 +530128,禄劝彝族苗族自治县,4,530100 +530129,寻甸回族彝族自治县,4,530100 +530181,安宁市,4,530100 +530302,麒麟区,4,530300 +530303,沾益区,4,530300 +530304,马龙区,4,530300 +530322,陆良县,4,530300 +530323,师宗县,4,530300 +530324,罗平县,4,530300 +530325,富源县,4,530300 +530326,会泽县,4,530300 +530381,宣威市,4,530300 +530402,红塔区,4,530400 +530403,江川区,4,530400 +530423,通海县,4,530400 +530424,华宁县,4,530400 +530425,易门县,4,530400 +530426,峨山彝族自治县,4,530400 +530427,新平彝族傣族自治县,4,530400 +530428,元江哈尼族彝族傣族自治县,4,530400 +530481,澄江市,4,530400 +530502,隆阳区,4,530500 +530521,施甸县,4,530500 +530523,龙陵县,4,530500 +530524,昌宁县,4,530500 +530581,腾冲市,4,530500 +530602,昭阳区,4,530600 +530621,鲁甸县,4,530600 +530622,巧家县,4,530600 +530623,盐津县,4,530600 +530624,大关县,4,530600 +530625,永善县,4,530600 +530626,绥江县,4,530600 +530627,镇雄县,4,530600 +530628,彝良县,4,530600 +530629,威信县,4,530600 +530681,水富市,4,530600 +530702,古城区,4,530700 +530721,玉龙纳西族自治县,4,530700 +530722,永胜县,4,530700 +530723,华坪县,4,530700 +530724,宁蒗彝族自治县,4,530700 +530802,思茅区,4,530800 +530821,宁洱哈尼族彝族自治县,4,530800 +530822,墨江哈尼族自治县,4,530800 +530823,景东彝族自治县,4,530800 +530824,景谷傣族彝族自治县,4,530800 +530825,镇沅彝族哈尼族拉祜族自治县,4,530800 +530826,江城哈尼族彝族自治县,4,530800 +530827,孟连傣族拉祜族佤族自治县,4,530800 +530828,澜沧拉祜族自治县,4,530800 +530829,西盟佤族自治县,4,530800 +530902,临翔区,4,530900 +530921,凤庆县,4,530900 +530922,云县,4,530900 +530923,永德县,4,530900 +530924,镇康县,4,530900 +530925,双江拉祜族佤族布朗族傣族自治县,4,530900 +530926,耿马傣族佤族自治县,4,530900 +530927,沧源佤族自治县,4,530900 +532301,楚雄市,4,532300 +532302,禄丰市,4,532300 +532322,双柏县,4,532300 +532323,牟定县,4,532300 +532324,南华县,4,532300 +532325,姚安县,4,532300 +532326,大姚县,4,532300 +532327,永仁县,4,532300 +532328,元谋县,4,532300 +532329,武定县,4,532300 +532501,个旧市,4,532500 +532502,开远市,4,532500 +532503,蒙自市,4,532500 +532504,弥勒市,4,532500 +532523,屏边苗族自治县,4,532500 +532524,建水县,4,532500 +532525,石屏县,4,532500 +532527,泸西县,4,532500 +532528,元阳县,4,532500 +532529,红河县,4,532500 +532530,金平苗族瑶族傣族自治县,4,532500 +532531,绿春县,4,532500 +532532,河口瑶族自治县,4,532500 +532601,文山市,4,532600 +532622,砚山县,4,532600 +532623,西畴县,4,532600 +532624,麻栗坡县,4,532600 +532625,马关县,4,532600 +532626,丘北县,4,532600 +532627,广南县,4,532600 +532628,富宁县,4,532600 +532801,景洪市,4,532800 +532822,勐海县,4,532800 +532823,勐腊县,4,532800 +532901,大理市,4,532900 +532922,漾濞彝族自治县,4,532900 +532923,祥云县,4,532900 +532924,宾川县,4,532900 +532925,弥渡县,4,532900 +532926,南涧彝族自治县,4,532900 +532927,巍山彝族回族自治县,4,532900 +532928,永平县,4,532900 +532929,云龙县,4,532900 +532930,洱源县,4,532900 +532931,剑川县,4,532900 +532932,鹤庆县,4,532900 +533102,瑞丽市,4,533100 +533103,芒市,4,533100 +533122,梁河县,4,533100 +533123,盈江县,4,533100 +533124,陇川县,4,533100 +533301,泸水市,4,533300 +533323,福贡县,4,533300 +533324,贡山独龙族怒族自治县,4,533300 +533325,兰坪白族普米族自治县,4,533300 +533401,香格里拉市,4,533400 +533422,德钦县,4,533400 +533423,维西傈僳族自治县,4,533400 +540102,城关区,4,540100 +540103,堆龙德庆区,4,540100 +540104,达孜区,4,540100 +540121,林周县,4,540100 +540122,当雄县,4,540100 +540123,尼木县,4,540100 +540124,曲水县,4,540100 +540127,墨竹工卡县,4,540100 +540171,格尔木藏青工业园区,4,540100 +540172,拉萨经济技术开发区,4,540100 +540173,西藏文化旅游创意园区,4,540100 +540174,达孜工业园区,4,540100 +540202,桑珠孜区,4,540200 +540221,南木林县,4,540200 +540222,江孜县,4,540200 +540223,定日县,4,540200 +540224,萨迦县,4,540200 +540225,拉孜县,4,540200 +540226,昂仁县,4,540200 +540227,谢通门县,4,540200 +540228,白朗县,4,540200 +540229,仁布县,4,540200 +540230,康马县,4,540200 +540231,定结县,4,540200 +540232,仲巴县,4,540200 +540233,亚东县,4,540200 +540234,吉隆县,4,540200 +540235,聂拉木县,4,540200 +540236,萨嘎县,4,540200 +540237,岗巴县,4,540200 +540302,卡若区,4,540300 +540321,江达县,4,540300 +540322,贡觉县,4,540300 +540323,类乌齐县,4,540300 +540324,丁青县,4,540300 +540325,察雅县,4,540300 +540326,八宿县,4,540300 +540327,左贡县,4,540300 +540328,芒康县,4,540300 +540329,洛隆县,4,540300 +540330,边坝县,4,540300 +540402,巴宜区,4,540400 +540421,工布江达县,4,540400 +540422,米林县,4,540400 +540423,墨脱县,4,540400 +540424,波密县,4,540400 +540425,察隅县,4,540400 +540426,朗县,4,540400 +540502,乃东区,4,540500 +540521,扎囊县,4,540500 +540522,贡嘎县,4,540500 +540523,桑日县,4,540500 +540524,琼结县,4,540500 +540525,曲松县,4,540500 +540526,措美县,4,540500 +540527,洛扎县,4,540500 +540528,加查县,4,540500 +540529,隆子县,4,540500 +540530,错那县,4,540500 +540531,浪卡子县,4,540500 +540602,色尼区,4,540600 +540621,嘉黎县,4,540600 +540622,比如县,4,540600 +540623,聂荣县,4,540600 +540624,安多县,4,540600 +540625,申扎县,4,540600 +540626,索县,4,540600 +540627,班戈县,4,540600 +540628,巴青县,4,540600 +540629,尼玛县,4,540600 +540630,双湖县,4,540600 +542521,普兰县,4,542500 +542522,札达县,4,542500 +542523,噶尔县,4,542500 +542524,日土县,4,542500 +542525,革吉县,4,542500 +542526,改则县,4,542500 +542527,措勤县,4,542500 +610102,新城区,4,610100 +610103,碑林区,4,610100 +610104,莲湖区,4,610100 +610111,灞桥区,4,610100 +610112,未央区,4,610100 +610113,雁塔区,4,610100 +610114,阎良区,4,610100 +610115,临潼区,4,610100 +610116,长安区,4,610100 +610117,高陵区,4,610100 +610118,鄠邑区,4,610100 +610122,蓝田县,4,610100 +610124,周至县,4,610100 +610202,王益区,4,610200 +610203,印台区,4,610200 +610204,耀州区,4,610200 +610222,宜君县,4,610200 +610302,渭滨区,4,610300 +610303,金台区,4,610300 +610304,陈仓区,4,610300 +610305,凤翔区,4,610300 +610323,岐山县,4,610300 +610324,扶风县,4,610300 +610326,眉县,4,610300 +610327,陇县,4,610300 +610328,千阳县,4,610300 +610329,麟游县,4,610300 +610330,凤县,4,610300 +610331,太白县,4,610300 +610402,秦都区,4,610400 +610403,杨陵区,4,610400 +610404,渭城区,4,610400 +610422,三原县,4,610400 +610423,泾阳县,4,610400 +610424,乾县,4,610400 +610425,礼泉县,4,610400 +610426,永寿县,4,610400 +610428,长武县,4,610400 +610429,旬邑县,4,610400 +610430,淳化县,4,610400 +610431,武功县,4,610400 +610481,兴平市,4,610400 +610482,彬州市,4,610400 +610502,临渭区,4,610500 +610503,华州区,4,610500 +610522,潼关县,4,610500 +610523,大荔县,4,610500 +610524,合阳县,4,610500 +610525,澄城县,4,610500 +610526,蒲城县,4,610500 +610527,白水县,4,610500 +610528,富平县,4,610500 +610581,韩城市,4,610500 +610582,华阴市,4,610500 +610602,宝塔区,4,610600 +610603,安塞区,4,610600 +610621,延长县,4,610600 +610622,延川县,4,610600 +610625,志丹县,4,610600 +610626,吴起县,4,610600 +610627,甘泉县,4,610600 +610628,富县,4,610600 +610629,洛川县,4,610600 +610630,宜川县,4,610600 +610631,黄龙县,4,610600 +610632,黄陵县,4,610600 +610681,子长市,4,610600 +610702,汉台区,4,610700 +610703,南郑区,4,610700 +610722,城固县,4,610700 +610723,洋县,4,610700 +610724,西乡县,4,610700 +610725,勉县,4,610700 +610726,宁强县,4,610700 +610727,略阳县,4,610700 +610728,镇巴县,4,610700 +610729,留坝县,4,610700 +610730,佛坪县,4,610700 +610802,榆阳区,4,610800 +610803,横山区,4,610800 +610822,府谷县,4,610800 +610824,靖边县,4,610800 +610825,定边县,4,610800 +610826,绥德县,4,610800 +610827,米脂县,4,610800 +610828,佳县,4,610800 +610829,吴堡县,4,610800 +610830,清涧县,4,610800 +610831,子洲县,4,610800 +610881,神木市,4,610800 +610902,汉滨区,4,610900 +610921,汉阴县,4,610900 +610922,石泉县,4,610900 +610923,宁陕县,4,610900 +610924,紫阳县,4,610900 +610925,岚皋县,4,610900 +610926,平利县,4,610900 +610927,镇坪县,4,610900 +610929,白河县,4,610900 +610981,旬阳市,4,610900 +611002,商州区,4,611000 +611021,洛南县,4,611000 +611022,丹凤县,4,611000 +611023,商南县,4,611000 +611024,山阳县,4,611000 +611025,镇安县,4,611000 +611026,柞水县,4,611000 +620102,城关区,4,620100 +620103,七里河区,4,620100 +620104,西固区,4,620100 +620105,安宁区,4,620100 +620111,红古区,4,620100 +620121,永登县,4,620100 +620122,皋兰县,4,620100 +620123,榆中县,4,620100 +620171,兰州新区,4,620100 +620201,嘉峪关市,4,620200 +620302,金川区,4,620300 +620321,永昌县,4,620300 +620402,白银区,4,620400 +620403,平川区,4,620400 +620421,靖远县,4,620400 +620422,会宁县,4,620400 +620423,景泰县,4,620400 +620502,秦州区,4,620500 +620503,麦积区,4,620500 +620521,清水县,4,620500 +620522,秦安县,4,620500 +620523,甘谷县,4,620500 +620524,武山县,4,620500 +620525,张家川回族自治县,4,620500 +620602,凉州区,4,620600 +620621,民勤县,4,620600 +620622,古浪县,4,620600 +620623,天祝藏族自治县,4,620600 +620702,甘州区,4,620700 +620721,肃南裕固族自治县,4,620700 +620722,民乐县,4,620700 +620723,临泽县,4,620700 +620724,高台县,4,620700 +620725,山丹县,4,620700 +620802,崆峒区,4,620800 +620821,泾川县,4,620800 +620822,灵台县,4,620800 +620823,崇信县,4,620800 +620825,庄浪县,4,620800 +620826,静宁县,4,620800 +620881,华亭市,4,620800 +620902,肃州区,4,620900 +620921,金塔县,4,620900 +620922,瓜州县,4,620900 +620923,肃北蒙古族自治县,4,620900 +620924,阿克塞哈萨克族自治县,4,620900 +620981,玉门市,4,620900 +620982,敦煌市,4,620900 +621002,西峰区,4,621000 +621021,庆城县,4,621000 +621022,环县,4,621000 +621023,华池县,4,621000 +621024,合水县,4,621000 +621025,正宁县,4,621000 +621026,宁县,4,621000 +621027,镇原县,4,621000 +621102,安定区,4,621100 +621121,通渭县,4,621100 +621122,陇西县,4,621100 +621123,渭源县,4,621100 +621124,临洮县,4,621100 +621125,漳县,4,621100 +621126,岷县,4,621100 +621202,武都区,4,621200 +621221,成县,4,621200 +621222,文县,4,621200 +621223,宕昌县,4,621200 +621224,康县,4,621200 +621225,西和县,4,621200 +621226,礼县,4,621200 +621227,徽县,4,621200 +621228,两当县,4,621200 +622901,临夏市,4,622900 +622921,临夏县,4,622900 +622922,康乐县,4,622900 +622923,永靖县,4,622900 +622924,广河县,4,622900 +622925,和政县,4,622900 +622926,东乡族自治县,4,622900 +622927,积石山保安族东乡族撒拉族自治县,4,622900 +623001,合作市,4,623000 +623021,临潭县,4,623000 +623022,卓尼县,4,623000 +623023,舟曲县,4,623000 +623024,迭部县,4,623000 +623025,玛曲县,4,623000 +623026,碌曲县,4,623000 +623027,夏河县,4,623000 +630102,城东区,4,630100 +630103,城中区,4,630100 +630104,城西区,4,630100 +630105,城北区,4,630100 +630106,湟中区,4,630100 +630121,大通回族土族自治县,4,630100 +630123,湟源县,4,630100 +630202,乐都区,4,630200 +630203,平安区,4,630200 +630222,民和回族土族自治县,4,630200 +630223,互助土族自治县,4,630200 +630224,化隆回族自治县,4,630200 +630225,循化撒拉族自治县,4,630200 +632221,门源回族自治县,4,632200 +632222,祁连县,4,632200 +632223,海晏县,4,632200 +632224,刚察县,4,632200 +632301,同仁市,4,632300 +632322,尖扎县,4,632300 +632323,泽库县,4,632300 +632324,河南蒙古族自治县,4,632300 +632521,共和县,4,632500 +632522,同德县,4,632500 +632523,贵德县,4,632500 +632524,兴海县,4,632500 +632525,贵南县,4,632500 +632621,玛沁县,4,632600 +632622,班玛县,4,632600 +632623,甘德县,4,632600 +632624,达日县,4,632600 +632625,久治县,4,632600 +632626,玛多县,4,632600 +632701,玉树市,4,632700 +632722,杂多县,4,632700 +632723,称多县,4,632700 +632724,治多县,4,632700 +632725,囊谦县,4,632700 +632726,曲麻莱县,4,632700 +632801,格尔木市,4,632800 +632802,德令哈市,4,632800 +632803,茫崖市,4,632800 +632821,乌兰县,4,632800 +632822,都兰县,4,632800 +632823,天峻县,4,632800 +632857,大柴旦行政委员会,4,632800 +640104,兴庆区,4,640100 +640105,西夏区,4,640100 +640106,金凤区,4,640100 +640121,永宁县,4,640100 +640122,贺兰县,4,640100 +640181,灵武市,4,640100 +640202,大武口区,4,640200 +640205,惠农区,4,640200 +640221,平罗县,4,640200 +640302,利通区,4,640300 +640303,红寺堡区,4,640300 +640323,盐池县,4,640300 +640324,同心县,4,640300 +640381,青铜峡市,4,640300 +640402,原州区,4,640400 +640422,西吉县,4,640400 +640423,隆德县,4,640400 +640424,泾源县,4,640400 +640425,彭阳县,4,640400 +640502,沙坡头区,4,640500 +640521,中宁县,4,640500 +640522,海原县,4,640500 +650102,天山区,4,650100 +650103,沙依巴克区,4,650100 +650104,新市区,4,650100 +650105,水磨沟区,4,650100 +650106,头屯河区,4,650100 +650107,达坂城区,4,650100 +650109,米东区,4,650100 +650121,乌鲁木齐县,4,650100 +650202,独山子区,4,650200 +650203,克拉玛依区,4,650200 +650204,白碱滩区,4,650200 +650205,乌尔禾区,4,650200 +650402,高昌区,4,650400 +650421,鄯善县,4,650400 +650422,托克逊县,4,650400 +650502,伊州区,4,650500 +650521,巴里坤哈萨克自治县,4,650500 +650522,伊吾县,4,650500 +652301,昌吉市,4,652300 +652302,阜康市,4,652300 +652323,呼图壁县,4,652300 +652324,玛纳斯县,4,652300 +652325,奇台县,4,652300 +652327,吉木萨尔县,4,652300 +652328,木垒哈萨克自治县,4,652300 +652701,博乐市,4,652700 +652702,阿拉山口市,4,652700 +652722,精河县,4,652700 +652723,温泉县,4,652700 +652801,库尔勒市,4,652800 +652822,轮台县,4,652800 +652823,尉犁县,4,652800 +652824,若羌县,4,652800 +652825,且末县,4,652800 +652826,焉耆回族自治县,4,652800 +652827,和静县,4,652800 +652828,和硕县,4,652800 +652829,博湖县,4,652800 +652871,库尔勒经济技术开发区,4,652800 +652901,阿克苏市,4,652900 +652902,库车市,4,652900 +652922,温宿县,4,652900 +652924,沙雅县,4,652900 +652925,新和县,4,652900 +652926,拜城县,4,652900 +652927,乌什县,4,652900 +652928,阿瓦提县,4,652900 +652929,柯坪县,4,652900 +653001,阿图什市,4,653000 +653022,阿克陶县,4,653000 +653023,阿合奇县,4,653000 +653024,乌恰县,4,653000 +653101,喀什市,4,653100 +653121,疏附县,4,653100 +653122,疏勒县,4,653100 +653123,英吉沙县,4,653100 +653124,泽普县,4,653100 +653125,莎车县,4,653100 +653126,叶城县,4,653100 +653127,麦盖提县,4,653100 +653128,岳普湖县,4,653100 +653129,伽师县,4,653100 +653130,巴楚县,4,653100 +653131,塔什库尔干塔吉克自治县,4,653100 +653201,和田市,4,653200 +653221,和田县,4,653200 +653222,墨玉县,4,653200 +653223,皮山县,4,653200 +653224,洛浦县,4,653200 +653225,策勒县,4,653200 +653226,于田县,4,653200 +653227,民丰县,4,653200 +654002,伊宁市,4,654000 +654003,奎屯市,4,654000 +654004,霍尔果斯市,4,654000 +654021,伊宁县,4,654000 +654022,察布查尔锡伯自治县,4,654000 +654023,霍城县,4,654000 +654024,巩留县,4,654000 +654025,新源县,4,654000 +654026,昭苏县,4,654000 +654027,特克斯县,4,654000 +654028,尼勒克县,4,654000 +654201,塔城市,4,654200 +654202,乌苏市,4,654200 +654203,沙湾市,4,654200 +654221,额敏县,4,654200 +654224,托里县,4,654200 +654225,裕民县,4,654200 +654226,和布克赛尔蒙古自治县,4,654200 +654301,阿勒泰市,4,654300 +654321,布尔津县,4,654300 +654322,富蕴县,4,654300 +654323,福海县,4,654300 +654324,哈巴河县,4,654300 +654325,青河县,4,654300 +654326,吉木乃县,4,654300 +659001,石河子市,4,659000 +659002,阿拉尔市,4,659000 +659003,图木舒克市,4,659000 +659004,五家渠市,4,659000 +659005,北屯市,4,659000 +659006,铁门关市,4,659000 +659007,双河市,4,659000 +659008,可克达拉市,4,659000 +659009,昆玉市,4,659000 +659010,胡杨河市,4,659000 +659011,新星市,4,659000 diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb new file mode 100644 index 0000000..2552273 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/main/resources/ip2region.xdb differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/test/java/com/yunxi/scm/framework/ip/core/utils/AreaUtilsTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/test/java/com/yunxi/scm/framework/ip/core/utils/AreaUtilsTest.java new file mode 100644 index 0000000..aaf165c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/test/java/com/yunxi/scm/framework/ip/core/utils/AreaUtilsTest.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.ip.core.utils; + + +import com.yunxi.scm.framework.ip.core.Area; +import com.yunxi.scm.framework.ip.core.enums.AreaTypeEnum; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link AreaUtils} 的单元测试 + * + * @author 芋道源码 + */ +public class AreaUtilsTest { + + @Test + public void testGetArea() { + // 调用:北京 + Area area = AreaUtils.getArea(110100); + // 断言 + assertEquals(area.getId(), 110100); + assertEquals(area.getName(), "北京市"); + assertEquals(area.getType(), AreaTypeEnum.CITY.getType()); + assertEquals(area.getParent().getId(), 110000); + assertEquals(area.getChildren().size(), 16); + } + + @Test + public void testFormat() { + assertEquals(AreaUtils.format(110105), "北京 北京市 朝阳区"); + assertEquals(AreaUtils.format(1), "中国"); + assertEquals(AreaUtils.format(2), "蒙古"); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/test/java/com/yunxi/scm/framework/ip/core/utils/IPUtilsTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/test/java/com/yunxi/scm/framework/ip/core/utils/IPUtilsTest.java new file mode 100644 index 0000000..d711a8e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-ip/src/test/java/com/yunxi/scm/framework/ip/core/utils/IPUtilsTest.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.framework.ip.core.utils; + +import com.yunxi.scm.framework.ip.core.Area; +import org.junit.jupiter.api.Test; +import org.lionsoul.ip2region.xdb.Searcher; + + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link IPUtils} 的单元测试 + * + * @author wanglhup + */ +public class IPUtilsTest { + + @Test + public void testGetAreaId_string() { + // 120.202.4.0|120.202.4.255|420600 + Integer areaId = IPUtils.getAreaId("120.202.4.50"); + assertEquals(420600, areaId); + } + + @Test + public void testGetAreaId_long() throws Exception { + // 120.203.123.0|120.203.133.255|360900 + long ip = Searcher.checkIP("120.203.123.250"); + Integer areaId = IPUtils.getAreaId(ip); + assertEquals(360900, areaId); + } + + @Test + public void testGetArea_string() { + // 120.202.4.0|120.202.4.255|420600 + Area area = IPUtils.getArea("120.202.4.50"); + assertEquals("襄阳市", area.getName()); + } + + @Test + public void testGetArea_long() throws Exception { + // 120.203.123.0|120.203.133.255|360900 + long ip = Searcher.checkIP("120.203.123.252"); + Area area = IPUtils.getArea(ip); + assertEquals("宜春市", area.getName()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/pom.xml new file mode 100644 index 0000000..33127b1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/pom.xml @@ -0,0 +1,51 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-biz-operatelog + jar + + ${project.artifactId} + 操作日志 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + provided + + + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + + + com.google.guava + guava + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/config/YunxiOperateLogAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/config/YunxiOperateLogAutoConfiguration.java new file mode 100644 index 0000000..e2a17f2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/config/YunxiOperateLogAutoConfiguration.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.framework.operatelog.config; + +import com.yunxi.scm.framework.operatelog.core.aop.OperateLogAspect; +import com.yunxi.scm.framework.operatelog.core.service.OperateLogFrameworkService; +import com.yunxi.scm.framework.operatelog.core.service.OperateLogFrameworkServiceImpl; +import com.yunxi.scm.module.system.api.logger.OperateLogApi; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class YunxiOperateLogAutoConfiguration { + + @Bean + public OperateLogAspect operateLogAspect() { + return new OperateLogAspect(); + } + + @Bean + public OperateLogFrameworkService operateLogFrameworkService(OperateLogApi operateLogApi) { + return new OperateLogFrameworkServiceImpl(operateLogApi); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/annotations/OperateLog.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/annotations/OperateLog.java new file mode 100644 index 0000000..2325ca9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/annotations/OperateLog.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.framework.operatelog.core.annotations; + +import com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 操作日志注解 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface OperateLog { + + // ========== 模块字段 ========== + + /** + * 操作模块 + * + * 为空时,会尝试读取 {@link Tag#name()} 属性 + */ + String module() default ""; + /** + * 操作名 + * + * 为空时,会尝试读取 {@link Operation#summary()} 属性 + */ + String name() default ""; + /** + * 操作分类 + * + * 实际并不是数组,因为枚举不能设置 null 作为默认值 + */ + OperateTypeEnum[] type() default {}; + + // ========== 开关字段 ========== + + /** + * 是否记录操作日志 + */ + boolean enable() default true; + /** + * 是否记录方法参数 + */ + boolean logArgs() default true; + /** + * 是否记录方法结果的数据 + */ + boolean logResultData() default true; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/aop/OperateLogAspect.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/aop/OperateLogAspect.java new file mode 100644 index 0000000..774bcc4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/aop/OperateLogAspect.java @@ -0,0 +1,375 @@ +package com.yunxi.scm.framework.operatelog.core.aop; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.common.util.monitor.TracerUtils; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum; +import com.yunxi.scm.framework.operatelog.core.service.OperateLog; +import com.yunxi.scm.framework.operatelog.core.service.OperateLogFrameworkService; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.annotation.Annotation; +import java.lang.reflect.Array; +import java.time.LocalDateTime; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.IntStream; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR; +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; + +/** + * 拦截使用 @OperateLog 注解,如果满足条件,则生成操作日志。 + * 满足如下任一条件,则会进行记录: + * 1. 使用 @ApiOperation + 非 @GetMapping + * 2. 使用 @OperateLog 注解 + *

+ * 但是,如果声明 @OperateLog 注解时,将 enable 属性设置为 false 时,强制不记录。 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class OperateLogAspect { + + /** + * 用于记录操作内容的上下文 + * + * @see OperateLog#getContent() + */ + private static final ThreadLocal CONTENT = new ThreadLocal<>(); + /** + * 用于记录拓展字段的上下文 + * + * @see OperateLog#getExts() + */ + private static final ThreadLocal> EXTS = new ThreadLocal<>(); + + @Resource + private OperateLogFrameworkService operateLogFrameworkService; + + @Around("@annotation(operation)") + public Object around(ProceedingJoinPoint joinPoint, Operation operation) throws Throwable { + // 可能也添加了 @ApiOperation 注解 + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog operateLog = getMethodAnnotation(joinPoint, + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog.class); + return around0(joinPoint, operateLog, operation); + } + + @Around("!@annotation(io.swagger.v3.oas.annotations.Operation) && @annotation(operateLog)") + // 兼容处理,只添加 @OperateLog 注解的情况 + public Object around(ProceedingJoinPoint joinPoint, + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog operateLog) throws Throwable { + return around0(joinPoint, operateLog, null); + } + + private Object around0(ProceedingJoinPoint joinPoint, + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog operateLog, + Operation operation) throws Throwable { + // 目前,只有管理员,才记录操作日志!所以非管理员,直接调用,不进行记录 + Integer userType = WebFrameworkUtils.getLoginUserType(); + if (!Objects.equals(userType, UserTypeEnum.ADMIN.getValue())) { + return joinPoint.proceed(); + } + + // 记录开始时间 + LocalDateTime startTime = LocalDateTime.now(); + try { + // 执行原有方法 + Object result = joinPoint.proceed(); + // 记录正常执行时的操作日志 + this.log(joinPoint, operateLog, operation, startTime, result, null); + return result; + } catch (Throwable exception) { + this.log(joinPoint, operateLog, operation, startTime, null, exception); + throw exception; + } finally { + clearThreadLocal(); + } + } + + public static void setContent(String content) { + CONTENT.set(content); + } + + public static void addExt(String key, Object value) { + if (EXTS.get() == null) { + EXTS.set(new HashMap<>()); + } + EXTS.get().put(key, value); + } + + private static void clearThreadLocal() { + CONTENT.remove(); + EXTS.remove(); + } + + private void log(ProceedingJoinPoint joinPoint, + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog operateLog, + Operation operation, + LocalDateTime startTime, Object result, Throwable exception) { + try { + // 判断不记录的情况 + if (!isLogEnable(joinPoint, operateLog)) { + return; + } + // 真正记录操作日志 + this.log0(joinPoint, operateLog, operation, startTime, result, exception); + } catch (Throwable ex) { + log.error("[log][记录操作日志时,发生异常,其中参数是 joinPoint({}) operateLog({}) apiOperation({}) result({}) exception({}) ]", + joinPoint, operateLog, operation, result, exception, ex); + } + } + + private void log0(ProceedingJoinPoint joinPoint, + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog operateLog, + Operation operation, + LocalDateTime startTime, Object result, Throwable exception) { + OperateLog operateLogObj = new OperateLog(); + // 补全通用字段 + operateLogObj.setTraceId(TracerUtils.getTraceId()); + operateLogObj.setStartTime(startTime); + // 补充用户信息 + fillUserFields(operateLogObj); + // 补全模块信息 + fillModuleFields(operateLogObj, joinPoint, operateLog, operation); + // 补全请求信息 + fillRequestFields(operateLogObj); + // 补全方法信息 + fillMethodFields(operateLogObj, joinPoint, operateLog, startTime, result, exception); + + // 异步记录日志 + operateLogFrameworkService.createOperateLog(operateLogObj); + } + + private static void fillUserFields(OperateLog operateLogObj) { + operateLogObj.setUserId(WebFrameworkUtils.getLoginUserId()); + operateLogObj.setUserType(WebFrameworkUtils.getLoginUserType()); + } + + private static void fillModuleFields(OperateLog operateLogObj, + ProceedingJoinPoint joinPoint, + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog operateLog, + Operation operation) { + // module 属性 + if (operateLog != null) { + operateLogObj.setModule(operateLog.module()); + } + if (StrUtil.isEmpty(operateLogObj.getModule())) { + Tag tag = getClassAnnotation(joinPoint, Tag.class); + if (tag != null) { + // 优先读取 @Tag 的 name 属性 + if (StrUtil.isNotEmpty(tag.name())) { + operateLogObj.setModule(tag.name()); + } + // 没有的话,读取 @API 的 description 属性 + if (StrUtil.isEmpty(operateLogObj.getModule()) && ArrayUtil.isNotEmpty(tag.description())) { + operateLogObj.setModule(tag.description()); + } + } + } + // name 属性 + if (operateLog != null) { + operateLogObj.setName(operateLog.name()); + } + if (StrUtil.isEmpty(operateLogObj.getName()) && operation != null) { + operateLogObj.setName(operation.summary()); + } + // type 属性 + if (operateLog != null && ArrayUtil.isNotEmpty(operateLog.type())) { + operateLogObj.setType(operateLog.type()[0].getType()); + } + if (operateLogObj.getType() == null) { + RequestMethod requestMethod = obtainFirstMatchRequestMethod(obtainRequestMethod(joinPoint)); + OperateTypeEnum operateLogType = convertOperateLogType(requestMethod); + operateLogObj.setType(operateLogType != null ? operateLogType.getType() : null); + } + // content 和 exts 属性 + operateLogObj.setContent(CONTENT.get()); + operateLogObj.setExts(EXTS.get()); + } + + private static void fillRequestFields(OperateLog operateLogObj) { + // 获得 Request 对象 + HttpServletRequest request = ServletUtils.getRequest(); + if (request == null) { + return; + } + // 补全请求信息 + operateLogObj.setRequestMethod(request.getMethod()); + operateLogObj.setRequestUrl(request.getRequestURI()); + operateLogObj.setUserIp(ServletUtils.getClientIP(request)); + operateLogObj.setUserAgent(ServletUtils.getUserAgent(request)); + } + + private static void fillMethodFields(OperateLog operateLogObj, + ProceedingJoinPoint joinPoint, + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog operateLog, + LocalDateTime startTime, Object result, Throwable exception) { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + operateLogObj.setJavaMethod(methodSignature.toString()); + if (operateLog == null || operateLog.logArgs()) { + operateLogObj.setJavaMethodArgs(obtainMethodArgs(joinPoint)); + } + if (operateLog == null || operateLog.logResultData()) { + operateLogObj.setResultData(obtainResultData(result)); + } + operateLogObj.setDuration((int) (LocalDateTimeUtil.between(startTime, LocalDateTime.now()).toMillis())); + // (正常)处理 resultCode 和 resultMsg 字段 + if (result instanceof CommonResult) { + CommonResult commonResult = (CommonResult) result; + operateLogObj.setResultCode(commonResult.getCode()); + operateLogObj.setResultMsg(commonResult.getMsg()); + } else { + operateLogObj.setResultCode(SUCCESS.getCode()); + } + // (异常)处理 resultCode 和 resultMsg 字段 + if (exception != null) { + operateLogObj.setResultCode(INTERNAL_SERVER_ERROR.getCode()); + operateLogObj.setResultMsg(ExceptionUtil.getRootCauseMessage(exception)); + } + } + + private static boolean isLogEnable(ProceedingJoinPoint joinPoint, + com.yunxi.scm.framework.operatelog.core.annotations.OperateLog operateLog) { + // 有 @OperateLog 注解的情况下 + if (operateLog != null) { + return operateLog.enable(); + } + // 没有 @ApiOperation 注解的情况下,只记录 POST、PUT、DELETE 的情况 + return obtainFirstLogRequestMethod(obtainRequestMethod(joinPoint)) != null; + } + + private static RequestMethod obtainFirstLogRequestMethod(RequestMethod[] requestMethods) { + if (ArrayUtil.isEmpty(requestMethods)) { + return null; + } + return Arrays.stream(requestMethods).filter(requestMethod -> + requestMethod == RequestMethod.POST + || requestMethod == RequestMethod.PUT + || requestMethod == RequestMethod.DELETE) + .findFirst().orElse(null); + } + + private static RequestMethod obtainFirstMatchRequestMethod(RequestMethod[] requestMethods) { + if (ArrayUtil.isEmpty(requestMethods)) { + return null; + } + // 优先,匹配最优的 POST、PUT、DELETE + RequestMethod result = obtainFirstLogRequestMethod(requestMethods); + if (result != null) { + return result; + } + // 然后,匹配次优的 GET + result = Arrays.stream(requestMethods).filter(requestMethod -> requestMethod == RequestMethod.GET) + .findFirst().orElse(null); + if (result != null) { + return result; + } + // 兜底,获得第一个 + return requestMethods[0]; + } + + private static OperateTypeEnum convertOperateLogType(RequestMethod requestMethod) { + if (requestMethod == null) { + return null; + } + switch (requestMethod) { + case GET: + return OperateTypeEnum.GET; + case POST: + return OperateTypeEnum.CREATE; + case PUT: + return OperateTypeEnum.UPDATE; + case DELETE: + return OperateTypeEnum.DELETE; + default: + return OperateTypeEnum.OTHER; + } + } + + private static RequestMethod[] obtainRequestMethod(ProceedingJoinPoint joinPoint) { + RequestMapping requestMapping = AnnotationUtils.getAnnotation( // 使用 Spring 的工具类,可以处理 @RequestMapping 别名注解 + ((MethodSignature) joinPoint.getSignature()).getMethod(), RequestMapping.class); + return requestMapping != null ? requestMapping.method() : new RequestMethod[]{}; + } + + @SuppressWarnings("SameParameterValue") + private static T getMethodAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(annotationClass); + } + + @SuppressWarnings("SameParameterValue") + private static T getClassAnnotation(ProceedingJoinPoint joinPoint, Class annotationClass) { + return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass); + } + + private static String obtainMethodArgs(ProceedingJoinPoint joinPoint) { + // TODO 提升:参数脱敏和忽略 + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + String[] argNames = methodSignature.getParameterNames(); + Object[] argValues = joinPoint.getArgs(); + // 拼接参数 + Map args = Maps.newHashMapWithExpectedSize(argValues.length); + for (int i = 0; i < argNames.length; i++) { + String argName = argNames[i]; + Object argValue = argValues[i]; + // 被忽略时,标记为 ignore 字符串,避免和 null 混在一起 + args.put(argName, !isIgnoreArgs(argValue) ? argValue : "[ignore]"); + } + return JsonUtils.toJsonString(args); + } + + private static String obtainResultData(Object result) { + // TODO 提升:结果脱敏和忽略 + if (result instanceof CommonResult) { + result = ((CommonResult) result).getData(); + } + return JsonUtils.toJsonString(result); + } + + private static boolean isIgnoreArgs(Object object) { + Class clazz = object.getClass(); + // 处理数组的情况 + if (clazz.isArray()) { + return IntStream.range(0, Array.getLength(object)) + .anyMatch(index -> isIgnoreArgs(Array.get(object, index))); + } + // 递归,处理数组、Collection、Map 的情况 + if (Collection.class.isAssignableFrom(clazz)) { + return ((Collection) object).stream() + .anyMatch((Predicate) OperateLogAspect::isIgnoreArgs); + } + if (Map.class.isAssignableFrom(clazz)) { + return isIgnoreArgs(((Map) object).values()); + } + // obj + return object instanceof MultipartFile + || object instanceof HttpServletRequest + || object instanceof HttpServletResponse + || object instanceof BindingResult; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/enums/OperateTypeEnum.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/enums/OperateTypeEnum.java new file mode 100644 index 0000000..63cac7f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/enums/OperateTypeEnum.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.framework.operatelog.core.enums; + +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 操作日志的操作类型 + * + * @author ruoyi + */ +@Getter +@AllArgsConstructor +public enum OperateTypeEnum { + + /** + * 查询 + * + * 绝大多数情况下,不会记录查询动作,因为过于大量显得没有意义。 + * 在有需要的时候,通过声明 {@link OperateLog} 注解来记录 + */ + GET(1), + /** + * 新增 + */ + CREATE(2), + /** + * 修改 + */ + UPDATE(3), + /** + * 删除 + */ + DELETE(4), + /** + * 导出 + */ + EXPORT(5), + /** + * 导入 + */ + IMPORT(6), + /** + * 其它 + * + * 在无法归类时,可以选择使用其它。因为还有操作名可以进一步标识 + */ + OTHER(0); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/package-info.java new file mode 100644 index 0000000..e58e8ad --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.framework.operatelog.core; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLog.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLog.java new file mode 100644 index 0000000..ebbda2e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLog.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.framework.operatelog.core.service; + +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 操作日志 + * + * @author 芋道源码 + */ +@Data +public class OperateLog { + + /** + * 链路追踪编号 + */ + private String traceId; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + + /** + * 操作模块 + */ + private String module; + + /** + * 操作名 + */ + private String name; + + /** + * 操作分类 + */ + private Integer type; + + /** + * 操作明细 + */ + private String content; + + /** + * 拓展字段 + */ + private Map exts; + + /** + * 请求方法名 + */ + private String requestMethod; + + /** + * 请求地址 + */ + private String requestUrl; + + /** + * 用户 IP + */ + private String userIp; + + /** + * 浏览器 UserAgent + */ + private String userAgent; + + /** + * Java 方法名 + */ + private String javaMethod; + + /** + * Java 方法的参数 + */ + private String javaMethodArgs; + + /** + * 开始时间 + */ + private LocalDateTime startTime; + + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + + /** + * 结果码 + */ + private Integer resultCode; + + /** + * 结果提示 + */ + private String resultMsg; + + /** + * 结果数据 + */ + private String resultData; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLogFrameworkService.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLogFrameworkService.java new file mode 100644 index 0000000..b682285 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLogFrameworkService.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.framework.operatelog.core.service; + +/** + * 操作日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface OperateLogFrameworkService { + + /** + * 记录操作日志 + * + * @param operateLog 操作日志请求 + */ + void createOperateLog(OperateLog operateLog); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java new file mode 100644 index 0000000..6c348c2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/service/OperateLogFrameworkServiceImpl.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.operatelog.core.service; + +import cn.hutool.core.bean.BeanUtil; +import com.yunxi.scm.module.system.api.logger.OperateLogApi; +import com.yunxi.scm.module.system.api.logger.dto.OperateLogCreateReqDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; + +/** + * 操作日志 Framework Service 实现类 + * + * 基于 {@link OperateLogApi} 实现,记录操作日志 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class OperateLogFrameworkServiceImpl implements OperateLogFrameworkService { + + private final OperateLogApi operateLogApi; + + @Override + @Async + public void createOperateLog(OperateLog operateLog) { + OperateLogCreateReqDTO reqDTO = BeanUtil.copyProperties(operateLog, OperateLogCreateReqDTO.class); + operateLogApi.createOperateLog(reqDTO); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/util/OperateLogUtils.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/util/OperateLogUtils.java new file mode 100644 index 0000000..3008be1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/core/util/OperateLogUtils.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.operatelog.core.util; + +import com.yunxi.scm.framework.operatelog.core.aop.OperateLogAspect; + +/** + * 操作日志工具类 + * 目前主要的作用,是提供给业务代码,记录操作明细和拓展字段 + * + * @author 芋道源码 + */ +public class OperateLogUtils { + + public static void setContent(String content) { + OperateLogAspect.setContent(content); + } + + public static void addExt(String key, Object value) { + OperateLogAspect.addExt(key, value); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/package-info.java new file mode 100644 index 0000000..bec1ddc --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/java/com/yunxi/scm/framework/operatelog/package-info.java @@ -0,0 +1,6 @@ +/** + * 用户操作日志:记录用户的操作,用于对用户的操作的审计与追溯,永久保存。 + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.operatelog; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3476cd5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-operatelog/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.operatelog.config.YunxiOperateLogAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/pom.xml new file mode 100644 index 0000000..4b6d866 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/pom.xml @@ -0,0 +1,76 @@ + + + + yunxi-framework + com.yunxi.scm + ${revision} + + 4.0.0 + + yunxi-spring-boot-starter-biz-pay + ${project.artifactId} + 支付拓展,接入国内多个支付渠道 + 1. 支付宝,基于官方 SDK 接入 + 2. 微信支付,基于 weixin-java-pay 接入 + + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.slf4j + slf4j-api + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + + com.alipay.sdk + alipay-sdk-java + 4.35.79.ALL + + + org.bouncycastle + bcprov-jdk15on + + + + + com.github.binarywang + weixin-java-pay + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/config/YunxiPayAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/config/YunxiPayAutoConfiguration.java new file mode 100644 index 0000000..85f759a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/config/YunxiPayAutoConfiguration.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.framework.pay.config; + +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.client.impl.PayClientFactoryImpl; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; + +/** + * 支付配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class YunxiPayAutoConfiguration { + + @Bean + public PayClientFactory payClientFactory() { + return new PayClientFactoryImpl(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClient.java new file mode 100644 index 0000000..56e8957 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClient.java @@ -0,0 +1,79 @@ +package com.yunxi.scm.framework.pay.core.client; + +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; + +import java.util.Map; + +/** + * 支付客户端,用于对接各支付渠道的 SDK,实现发起支付、退款等功能 + * + * @author 芋道源码 + */ +public interface PayClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + // ============ 支付相关 ========== + + /** + * 调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 支付订单信息 + */ + PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO); + + /** + * 解析 order 回调数据 + * + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + * @param body HTTP 回调接口的 request body + * @return 支付订单信息 + */ + PayOrderRespDTO parseOrderNotify(Map params, String body); + + /** + * 获得支付订单信息 + * + * @param outTradeNo 外部订单号 + * @return 支付订单信息 + */ + PayOrderRespDTO getOrder(String outTradeNo); + + // ============ 退款相关 ========== + + /** + * 调用支付渠道,进行退款 + * + * @param reqDTO 统一退款请求信息 + * @return 退款信息 + */ + PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO); + + /** + * 解析 refund 回调数据 + * + * @param params HTTP 回调接口 content type 为 application/x-www-form-urlencoded 的所有参数 + * @param body HTTP 回调接口的 request body + * @return 支付订单信息 + */ + PayRefundRespDTO parseRefundNotify(Map params, String body); + + /** + * 获得退款订单信息 + * + * @param outTradeNo 外部订单号 + * @param outRefundNo 外部退款号 + * @return 退款订单信息 + */ + PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClientConfig.java new file mode 100644 index 0000000..a23bdf9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClientConfig.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.framework.pay.core.client; + +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; +import java.util.Set; + +/** + * 支付客户端的配置,本质是支付渠道的配置 + * 每个不同的渠道,需要不同的配置,通过子类来定义 + * + * @author 芋道源码 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +// @JsonTypeInfo 注解的作用,Jackson 多态 +// 1. 序列化到时数据库时,增加 @class 属性。 +// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 +public interface PayClientConfig { + + /** + * 参数校验 + * + * @param validator 校验对象 + */ + void validate(Validator validator); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClientFactory.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClientFactory.java new file mode 100644 index 0000000..d5cd098 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/PayClientFactory.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.pay.core.client; + +/** + * 支付客户端的工厂接口 + * + * @author 芋道源码 + */ +public interface PayClientFactory { + + /** + * 获得支付客户端 + * + * @param channelId 渠道编号 + * @return 支付客户端 + */ + PayClient getPayClient(Long channelId); + + /** + * 创建支付客户端 + * + * @param channelId 渠道编号 + * @param channelCode 渠道编码 + * @param config 支付配置 + */ + void createOrUpdatePayClient(Long channelId, String channelCode, + Config config); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/order/PayOrderRespDTO.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/order/PayOrderRespDTO.java new file mode 100644 index 0000000..82ce55f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/order/PayOrderRespDTO.java @@ -0,0 +1,141 @@ +package com.yunxi.scm.framework.pay.core.client.dto.order; + +import com.yunxi.scm.framework.pay.core.client.exception.PayException; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 渠道支付订单 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderRespDTO { + + /** + * 支付状态 + * + * 枚举:{@link PayOrderStatusRespEnum} + */ + private Integer status; + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + private String outTradeNo; + + /** + * 支付渠道编号 + */ + private String channelOrderNo; + /** + * 支付渠道用户编号 + */ + private String channelUserId; + + /** + * 支付成功时间 + */ + private LocalDateTime successTime; + + /** + * 原始的同步/异步通知结果 + */ + private Object rawData; + + // ========== 主动发起支付时,会返回的字段 ========== + + /** + * 展示模式 + * + * 枚举 {@link PayOrderDisplayModeEnum} 类 + */ + private String displayMode; + /** + * 展示内容 + */ + private String displayContent; + + /** + * 调用渠道的错误码 + * + * 注意:这里返回的是业务异常,而是不系统异常。 + * 如果是系统异常,则会抛出 {@link PayException} + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + public PayOrderRespDTO() { + } + + /** + * 创建【WAITING】状态的订单返回 + */ + public static PayOrderRespDTO waitingOf(String displayMode, String displayContent, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusRespEnum.WAITING.getStatus(); + respDTO.displayMode = displayMode; + respDTO.displayContent = displayContent; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【SUCCESS】状态的订单返回 + */ + public static PayOrderRespDTO successOf(String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusRespEnum.SUCCESS.getStatus(); + respDTO.channelOrderNo = channelOrderNo; + respDTO.channelUserId = channelUserId; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建指定状态的订单返回,适合支付渠道回调时 + */ + public static PayOrderRespDTO of(Integer status, String channelOrderNo, String channelUserId, LocalDateTime successTime, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = status; + respDTO.channelOrderNo = channelOrderNo; + respDTO.channelUserId = channelUserId; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【CLOSED】状态的订单返回,适合调用支付渠道失败时 + */ + public static PayOrderRespDTO closedOf(String channelErrorCode, String channelErrorMsg, + String outTradeNo, Object rawData) { + PayOrderRespDTO respDTO = new PayOrderRespDTO(); + respDTO.status = PayOrderStatusRespEnum.CLOSED.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outTradeNo = outTradeNo; + respDTO.rawData = rawData; + return respDTO; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java new file mode 100644 index 0000000..c3c3e47 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/order/PayOrderUnifiedReqDTO.java @@ -0,0 +1,92 @@ +package com.yunxi.scm.framework.pay.core.client.dto.order; + +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import lombok.Data; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 统一下单 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderUnifiedReqDTO { + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + @NotEmpty(message = "外部订单编号不能为空") + private String outTradeNo; + /** + * 商品标题 + */ + @NotEmpty(message = "商品标题不能为空") + @Length(max = 32, message = "商品标题不能超过 32") + private String subject; + /** + * 商品描述信息 + */ + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + /** + * 支付结果的 notify 回调地址 + */ + @NotEmpty(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + /** + * 支付结果的 return 回调地址 + */ + @URL(message = "支付结果的 return 回调地址必须是 URL 格式") + private String returnUrl; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer price; + + /** + * 支付过期时间 + */ + @NotNull(message = "支付过期时间不能为空") + private LocalDateTime expireTime; + + // ========== 拓展参数 ========== + /** + * 支付渠道的额外参数 + * + * 例如说,微信公众号需要传递 openid 参数 + */ + private Map channelExtras; + + /** + * 展示模式 + * + * 如果不传递,则每个支付渠道使用默认的方式 + * + * 枚举 {@link PayOrderDisplayModeEnum} + */ + private String displayMode; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/refund/PayRefundRespDTO.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/refund/PayRefundRespDTO.java new file mode 100644 index 0000000..f7b38da --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/refund/PayRefundRespDTO.java @@ -0,0 +1,115 @@ +package com.yunxi.scm.framework.pay.core.client.dto.refund; + +import com.yunxi.scm.framework.pay.core.client.exception.PayException; +import com.yunxi.scm.framework.pay.core.enums.refund.PayRefundStatusRespEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 渠道退款订单 Response DTO + * + * @author jason + */ +@Data +public class PayRefundRespDTO { + + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusRespEnum} + */ + private Integer status; + + /** + * 外部退款号 + * + * 对应 PayRefundDO 的 no 字段 + */ + private String outRefundNo; + + /** + * 渠道退款单号 + * + * 对应 PayRefundDO.channelRefundNo 字段 + */ + private String channelRefundNo; + + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + + /** + * 原始的异步通知结果 + */ + private Object rawData; + + /** + * 调用渠道的错误码 + * + * 注意:这里返回的是业务异常,而是不系统异常。 + * 如果是系统异常,则会抛出 {@link PayException} + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + private PayRefundRespDTO() { + } + + /** + * 创建【WAITING】状态的退款返回 + */ + public static PayRefundRespDTO waitingOf(String channelRefundNo, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusRespEnum.WAITING.getStatus(); + respDTO.channelRefundNo = channelRefundNo; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【SUCCESS】状态的退款返回 + */ + public static PayRefundRespDTO successOf(String channelRefundNo, LocalDateTime successTime, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusRespEnum.SUCCESS.getStatus(); + respDTO.channelRefundNo = channelRefundNo; + respDTO.successTime = successTime; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + + /** + * 创建【FAILURE】状态的退款返回 + */ + public static PayRefundRespDTO failureOf(String outRefundNo, Object rawData) { + return failureOf(null, null, + outRefundNo, rawData); + } + + /** + * 创建【FAILURE】状态的退款返回 + */ + public static PayRefundRespDTO failureOf(String channelErrorCode, String channelErrorMsg, + String outRefundNo, Object rawData) { + PayRefundRespDTO respDTO = new PayRefundRespDTO(); + respDTO.status = PayRefundStatusRespEnum.FAILURE.getStatus(); + respDTO.channelErrorCode = channelErrorCode; + respDTO.channelErrorMsg = channelErrorMsg; + // 相对通用的字段 + respDTO.outRefundNo = outRefundNo; + respDTO.rawData = rawData; + return respDTO; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java new file mode 100644 index 0000000..445f7ba --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/dto/refund/PayRefundUnifiedReqDTO.java @@ -0,0 +1,70 @@ +package com.yunxi.scm.framework.pay.core.client.dto.refund; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 统一 退款 Request DTO + * + * @author jason + */ +@Accessors(chain = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Data +public class PayRefundUnifiedReqDTO { + + /** + * 外部订单号 + * + * 对应 PayOrderExtensionDO 的 no 字段 + */ + @NotEmpty(message = "外部订单编号不能为空") + private String outTradeNo; + + /** + * 外部退款号 + * + * 对应 PayRefundDO 的 no 字段 + */ + @NotEmpty(message = "退款请求单号不能为空") + private String outRefundNo; + + /** + * 退款原因 + */ + @NotEmpty(message = "退款原因不能为空") + private String reason; + + /** + * 支付金额,单位:分 + * + * 目前微信支付在退款的时候,必须传递该字段 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer payPrice; + /** + * 退款金额,单位:分 + */ + @NotNull(message = "退款金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer refundPrice; + + /** + * 退款结果的 notify 回调地址 + */ + @NotEmpty(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的 notify 回调地址必须是 URL 格式") + private String notifyUrl; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/exception/PayException.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/exception/PayException.java new file mode 100644 index 0000000..f1de3fa --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/exception/PayException.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.framework.pay.core.client.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 支付系统异常 Exception + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class PayException extends RuntimeException { + + public PayException(Throwable cause) { + super(cause); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/AbstractPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/AbstractPayClient.java new file mode 100644 index 0000000..f867d02 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/AbstractPayClient.java @@ -0,0 +1,193 @@ +package com.yunxi.scm.framework.pay.core.client.impl; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import com.yunxi.scm.framework.pay.core.client.PayClient; +import com.yunxi.scm.framework.pay.core.client.PayClientConfig; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.exception.PayException; +import lombok.extern.slf4j.Slf4j; + +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 支付客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractPayClient implements PayClient { + + /** + * 渠道编号 + */ + private final Long channelId; + /** + * 渠道编码 + */ + @SuppressWarnings("FieldCanBeLocal") + private final String channelCode; + /** + * 支付配置 + */ + protected Config config; + + public AbstractPayClient(Long channelId, String channelCode, Config config) { + this.channelId = channelId; + this.channelCode = channelCode; + this.config = config; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.info("[init][客户端({}) 初始化完成]", getId()); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(Config config) { + // 判断是否更新 + if (config.equals(this.config)) { + return; + } + log.info("[refresh][客户端({})发生变化,重新初始化]", getId()); + this.config = config; + // 初始化 + this.init(); + } + + @Override + public Long getId() { + return channelId; + } + + // ============ 支付相关 ========== + + @Override + public final PayOrderRespDTO unifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + ValidationUtils.validate(reqDTO); + // 执行统一下单 + PayOrderRespDTO resp; + try { + resp = doUnifiedOrder(reqDTO); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[unifiedOrder][客户端({}) request({}) 发起支付异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); + } + return resp; + } + + protected abstract PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) + throws Throwable; + + @Override + public final PayOrderRespDTO parseOrderNotify(Map params, String body) { + try { + return doParseOrderNotify(params, body); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[parseOrderNotify][客户端({}) params({}) body({}) 解析失败]", + getId(), params, body, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doParseOrderNotify(Map params, String body) + throws Throwable; + + @Override + public final PayOrderRespDTO getOrder(String outTradeNo) { + try { + return doGetOrder(outTradeNo); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[getOrder][客户端({}) outTradeNo({}) 查询支付单异常]", + getId(), outTradeNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayOrderRespDTO doGetOrder(String outTradeNo) + throws Throwable; + + // ============ 退款相关 ========== + + @Override + public final PayRefundRespDTO unifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + ValidationUtils.validate(reqDTO); + // 执行统一退款 + PayRefundRespDTO resp; + try { + resp = doUnifiedRefund(reqDTO); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + // 系统异常,则包装成 PayException 异常抛出 + log.error("[unifiedRefund][客户端({}) request({}) 发起退款异常]", + getId(), toJsonString(reqDTO), ex); + throw buildPayException(ex); + } + return resp; + } + + protected abstract PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable; + + @Override + public final PayRefundRespDTO parseRefundNotify(Map params, String body) { + try { + return doParseRefundNotify(params, body); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[parseRefundNotify][客户端({}) params({}) body({}) 解析失败]", + getId(), params, body, ex); + throw buildPayException(ex); + } + } + + protected abstract PayRefundRespDTO doParseRefundNotify(Map params, String body) + throws Throwable; + + @Override + public final PayRefundRespDTO getRefund(String outTradeNo, String outRefundNo) { + try { + return doGetRefund(outTradeNo, outRefundNo); + } catch (ServiceException ex) { // 业务异常,都是实现类已经翻译,所以直接抛出即可 + throw ex; + } catch (Throwable ex) { + log.error("[getRefund][客户端({}) outTradeNo({}) outRefundNo({}) 查询退款单异常]", + getId(), outTradeNo, outRefundNo, ex); + throw buildPayException(ex); + } + } + + protected abstract PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) + throws Throwable; + + // ========== 各种工具方法 ========== + + private PayException buildPayException(Throwable ex) { + if (ex instanceof PayException) { + return (PayException) ex; + } + throw new PayException(ex); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/PayClientFactoryImpl.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/PayClientFactoryImpl.java new file mode 100644 index 0000000..0759f4d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/PayClientFactoryImpl.java @@ -0,0 +1,81 @@ +package com.yunxi.scm.framework.pay.core.client.impl; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.pay.core.client.PayClient; +import com.yunxi.scm.framework.pay.core.client.PayClientConfig; +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.client.impl.alipay.*; +import com.yunxi.scm.framework.pay.core.client.impl.mock.MockPayClient; +import com.yunxi.scm.framework.pay.core.client.impl.mock.MockPayClientConfig; +import com.yunxi.scm.framework.pay.core.client.impl.weixin.*; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 支付客户端的工厂实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class PayClientFactoryImpl implements PayClientFactory { + + /** + * 支付客户端 Map + * key:渠道编号 + */ + private final ConcurrentMap> clients = new ConcurrentHashMap<>(); + + @Override + public PayClient getPayClient(Long channelId) { + AbstractPayClient client = clients.get(channelId); + if (client == null) { + log.error("[getPayClient][渠道编号({}) 找不到客户端]", channelId); + } + return client; + } + + @Override + @SuppressWarnings("unchecked") + public void createOrUpdatePayClient(Long channelId, String channelCode, + Config config) { + AbstractPayClient client = (AbstractPayClient) clients.get(channelId); + if (client == null) { + client = this.createPayClient(channelId, channelCode, config); + client.init(); + clients.put(client.getId(), client); + } else { + client.refresh(config); + } + } + + @SuppressWarnings("unchecked") + private AbstractPayClient createPayClient( + Long channelId, String channelCode, Config config) { + PayChannelEnum channelEnum = PayChannelEnum.getByCode(channelCode); + Assert.notNull(channelEnum, String.format("支付渠道(%s) 为空", channelEnum)); + // 创建客户端 + switch (channelEnum) { + // 微信支付 + case WX_PUB: return (AbstractPayClient) new WxPubPayClient(channelId, (WxPayClientConfig) config); + case WX_LITE: return (AbstractPayClient) new WxLitePayClient(channelId, (WxPayClientConfig) config); + case WX_APP: return (AbstractPayClient) new WxAppPayClient(channelId, (WxPayClientConfig) config); + case WX_BAR: return (AbstractPayClient) new WxBarPayClient(channelId, (WxPayClientConfig) config); + case WX_NATIVE: return (AbstractPayClient) new WxNativePayClient(channelId, (WxPayClientConfig) config); + // 支付宝支付 + case ALIPAY_WAP: return (AbstractPayClient) new AlipayWapPayClient(channelId, (AlipayPayClientConfig) config); + case ALIPAY_QR: return (AbstractPayClient) new AlipayQrPayClient(channelId, (AlipayPayClientConfig) config); + case ALIPAY_APP: return (AbstractPayClient) new AlipayAppPayClient(channelId, (AlipayPayClientConfig) config); + case ALIPAY_PC: return (AbstractPayClient) new AlipayPcPayClient(channelId, (AlipayPayClientConfig) config); + case ALIPAY_BAR: return (AbstractPayClient) new AlipayBarPayClient(channelId, (AlipayPayClientConfig) config); + // 其它支付 + case MOCK: return (AbstractPayClient) new MockPayClient(channelId, (MockPayClientConfig) config); + } + // 创建失败,错误日志 + 抛出异常 + log.error("[createPayClient][配置({}) 找不到合适的客户端实现]", config); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", config)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java new file mode 100644 index 0000000..b5246a4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AbstractAlipayPayClient.java @@ -0,0 +1,213 @@ +package com.yunxi.scm.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.impl.AbstractPayClient; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayConfig; +import com.alipay.api.AlipayResponse; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.domain.AlipayTradeFastpayRefundQueryModel; +import com.alipay.api.domain.AlipayTradeQueryModel; +import com.alipay.api.domain.AlipayTradeRefundModel; +import com.alipay.api.internal.util.AlipaySignature; +import com.alipay.api.request.AlipayTradeFastpayRefundQueryRequest; +import com.alipay.api.request.AlipayTradeQueryRequest; +import com.alipay.api.request.AlipayTradeRefundRequest; +import com.alipay.api.response.AlipayTradeFastpayRefundQueryResponse; +import com.alipay.api.response.AlipayTradeQueryResponse; +import com.alipay.api.response.AlipayTradeRefundResponse; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; + +import static cn.hutool.core.date.DatePattern.NORM_DATETIME_FORMATTER; + +/** + * 支付宝抽象类,实现支付宝统一的接口、以及部分实现(退款) + * + * @author jason + */ +@Slf4j +public abstract class AbstractAlipayPayClient extends AbstractPayClient { + + protected DefaultAlipayClient client; + + public AbstractAlipayPayClient(Long channelId, String channelCode, AlipayPayClientConfig config) { + super(channelId, channelCode, config); + } + + @Override + @SneakyThrows + protected void doInit() { + AlipayConfig alipayConfig = new AlipayConfig(); + BeanUtil.copyProperties(config, alipayConfig, false); + this.client = new DefaultAlipayClient(alipayConfig); + } + + // ============ 支付相关 ========== + + /** + * 构造支付关闭的 {@link PayOrderRespDTO} 对象 + * + * @return 支付关闭的 {@link PayOrderRespDTO} 对象 + */ + protected PayOrderRespDTO buildClosedPayOrderRespDTO(PayOrderUnifiedReqDTO reqDTO, AlipayResponse response) { + Assert.isFalse(response.isSuccess()); + return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + reqDTO.getOutTradeNo(), response); + } + + @Override + public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws Throwable { + // 1. 校验回调数据 + Map bodyObj = HttpUtil.decodeParamMap(body, StandardCharsets.UTF_8); + AlipaySignature.rsaCheckV1(bodyObj, config.getAlipayPublicKey(), + StandardCharsets.UTF_8.name(), config.getSignType()); + + // 2. 解析订单的状态 + // 额外说明:支付宝不仅仅支付成功会回调,再各种触发支付单数据变化时,都会进行回调,所以这里 status 的解析会写的比较复杂 + Integer status = parseStatus(bodyObj.get("trade_status")); + // 特殊逻辑: 支付宝没有退款成功的状态,所以,如果有退款金额,我们认为是退款成功 + if (MapUtil.getDouble(bodyObj, "refund_fee", 0D) > 0) { + status = PayOrderStatusRespEnum.REFUND.getStatus(); + } + Assert.notNull(status, (Supplier) () -> { + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", body)); + }); + return PayOrderRespDTO.of(status, bodyObj.get("trade_no"), bodyObj.get("seller_id"), parseTime(params.get("gmt_payment")), + bodyObj.get("out_trade_no"), body); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { + // 1.1 构建 AlipayTradeRefundModel 请求 + AlipayTradeQueryModel model = new AlipayTradeQueryModel(); + model.setOutTradeNo(outTradeNo); + // 1.2 构建 AlipayTradeQueryRequest 请求 + AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayTradeQueryResponse response = client.execute(request); + if (!response.isSuccess()) { // 不成功,例如说订单不存在 + return PayOrderRespDTO.closedOf(response.getSubCode(), response.getSubMsg(), + outTradeNo, response); + } + // 2.2 解析订单的状态 + Integer status = parseStatus(response.getTradeStatus()); + Assert.notNull(status, (Supplier) () -> { + throw new IllegalArgumentException(StrUtil.format("body({}) 的 trade_status 不正确", response.getBody())); + }); + return PayOrderRespDTO.of(status, response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getSendPayDate()), + outTradeNo, response); + } + + private static Integer parseStatus(String tradeStatus) { + return Objects.equals("WAIT_BUYER_PAY", tradeStatus) ? PayOrderStatusRespEnum.WAITING.getStatus() + : ObjectUtils.equalsAny(tradeStatus, "TRADE_FINISHED", "TRADE_SUCCESS") ? PayOrderStatusRespEnum.SUCCESS.getStatus() + : Objects.equals("TRADE_CLOSED", tradeStatus) ? PayOrderStatusRespEnum.CLOSED.getStatus() : null; + } + + // ============ 退款相关 ========== + + /** + * 支付宝统一的退款接口 alipay.trade.refund + * + * @param reqDTO 退款请求 request DTO + * @return 退款请求 Response + */ + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeRefundModel 请求 + AlipayTradeRefundModel model = new AlipayTradeRefundModel(); + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setOutRequestNo(reqDTO.getOutRefundNo()); + model.setRefundAmount(formatAmount(reqDTO.getRefundPrice())); + model.setRefundReason(reqDTO.getReason()); + // 1.2 构建 AlipayTradePayRequest 请求 + AlipayTradeRefundRequest request = new AlipayTradeRefundRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayTradeRefundResponse response = client.execute(request); + if (!response.isSuccess()) { + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); + } + // 2.2 创建返回结果 + // 支付宝只要退款调用返回 success,就认为退款成功,不需要回调。具体可见 parseNotify 方法的说明。 + // 另外,支付宝没有退款单号,所以不用设置 + return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), + reqDTO.getOutRefundNo(), response); + } + + @Override + public PayRefundRespDTO doParseRefundNotify(Map params, String body) { + // 补充说明:支付宝退款时,没有回调,这点和微信支付是不同的。并且,退款分成部分退款、和全部退款。 + // ① 部分退款:是会有回调,但是它回调的是订单状态的同步回调,不是退款订单的回调 + // ② 全部退款:Wap 支付有订单状态的同步回调,但是 PC/扫码又没有 + // 所以,这里在解析时,即使是退款导致的订单状态同步,我们也忽略不做为“退款同步”,而是订单的回调。 + // 实际上,支付宝退款只要发起成功,就可以认为退款成功,不需要等待回调。 + throw new UnsupportedOperationException("支付宝无退款回调"); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws AlipayApiException { + // 1.1 构建 AlipayTradeFastpayRefundQueryModel 请求 + AlipayTradeFastpayRefundQueryModel model = new AlipayTradeFastpayRefundQueryModel(); + model.setOutTradeNo(outTradeNo); + model.setOutRequestNo(outRefundNo); + model.setQueryOptions(Collections.singletonList("gmt_refund_pay")); + // 1.2 构建 AlipayTradeFastpayRefundQueryRequest 请求 + AlipayTradeFastpayRefundQueryRequest request = new AlipayTradeFastpayRefundQueryRequest(); + request.setBizModel(model); + + // 2.1 执行请求 + AlipayTradeFastpayRefundQueryResponse response = client.execute(request); + if (!response.isSuccess()) { + // 明确不存在的情况,应该就是失败,可进行关闭 + if (ObjectUtils.equalsAny(response.getSubCode(), "TRADE_NOT_EXIST", "ACQ.TRADE_NOT_EXIST")) { + return PayRefundRespDTO.failureOf(outRefundNo, response); + } + // 可能存在“ACQ.SYSTEM_ERROR”系统错误等情况,所以返回 WAIT 继续等待 + return PayRefundRespDTO.waitingOf(null, outRefundNo, response); + } + // 2.2 创建返回结果 + if (Objects.equals(response.getRefundStatus(), "REFUND_SUCCESS")) { + return PayRefundRespDTO.successOf(null, LocalDateTimeUtil.of(response.getGmtRefundPay()), + outRefundNo, response); + } + return PayRefundRespDTO.waitingOf(null, outRefundNo, response); + } + + // ========== 各种工具方法 ========== + + protected String formatAmount(Integer amount) { + return String.valueOf(amount / 100.0); + } + + protected String formatTime(LocalDateTime time) { + return LocalDateTimeUtil.format(time, NORM_DATETIME_FORMATTER); + } + + protected LocalDateTime parseTime(String str) { + return LocalDateTimeUtil.parse(str, NORM_DATETIME_FORMATTER); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java new file mode 100644 index 0000000..681ea87 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayAppPayClient.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.framework.pay.core.client.impl.alipay; + +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeAppPayModel; +import com.alipay.api.request.AlipayTradeAppPayRequest; +import com.alipay.api.response.AlipayTradeAppPayResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * 支付宝【App 支付】的 PayClient 实现类 + * + * 文档:App 支付 + * + * // TODO 芋艿:未详细测试,因为手头没 App + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayAppPayClient extends AbstractAlipayPayClient { + + public AlipayAppPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_APP.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeAppPayModel 请求 + AlipayTradeAppPayModel model = new AlipayTradeAppPayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody() + "test"); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setTimeExpire(formatTime(reqDTO.getExpireTime())); + model.setProductCode("QUICK_MSECURITY_PAY"); // 销售产品码:无线快捷支付产品 + // ② 个性化的参数【无】 + // ③ 支付宝扫码支付只有一种展示 + String displayMode = PayOrderDisplayModeEnum.APP.getMode(); + + // 1.2 构建 AlipayTradePrecreateRequest 请求 + AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradeAppPayResponse response = client.sdkExecute(request); + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java new file mode 100644 index 0000000..0563041 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayBarPayClient.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePayModel; +import com.alipay.api.request.AlipayTradePayRequest; +import com.alipay.api.response.AlipayTradePayResponse; +import lombok.extern.slf4j.Slf4j; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception0; + +/** + * 支付宝【条码支付】的 PayClient 实现类 + * + * 文档:当面付 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayBarPayClient extends AbstractAlipayPayClient { + + public AlipayBarPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_BAR.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "auth_code"); + if (StrUtil.isEmpty(authCode)) { + throw exception0(BAD_REQUEST.getCode(), "条形码不能为空"); + } + + // 1.1 构建 AlipayTradePayModel 请求 + AlipayTradePayModel model = new AlipayTradePayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setScene("bar_code"); // 当面付条码支付场景 + // ② 个性化的参数 + model.setAuthCode(authCode); + // ③ 支付宝条码支付只有一种展示 + String displayMode = PayOrderDisplayModeEnum.BAR_CODE.getMode(); + + // 1.2 构建 AlipayTradePayRequest 请求 + AlipayTradePayRequest request = new AlipayTradePayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePayResponse response = client.execute(request); + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + if ("10000".equals(response.getCode())) { // 免密支付 + return PayOrderRespDTO.successOf(response.getTradeNo(), response.getBuyerUserId(), LocalDateTimeUtil.of(response.getGmtPayment()), + response.getOutTradeNo(), response); + } + // 大额支付,需要用户输入密码,所以返回 waiting。此时,前端一般会进行轮询 + return PayOrderRespDTO.waitingOf(displayMode, "", + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java new file mode 100644 index 0000000..f5ba371 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayPayClientConfig.java @@ -0,0 +1,109 @@ +package com.yunxi.scm.framework.pay.core.client.impl.alipay; + +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import com.yunxi.scm.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.Set; + +/** + * 支付宝的 PayClientConfig 实现类 + * 属性主要来自 {@link com.alipay.api.AlipayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class AlipayPayClientConfig implements PayClientConfig { + + /** + * 公钥类型 - 公钥模式 + */ + public static final Integer MODE_PUBLIC_KEY = 1; + /** + * 公钥类型 - 证书模式 + */ + public static final Integer MODE_CERTIFICATE = 2; + + /** + * 签名算法类型 - RSA + */ + public static final String SIGN_TYPE_DEFAULT = "RSA2"; + + /** + * 网关地址 + * + * 1. 生产环境 + * 2. 沙箱环境 + */ + @NotBlank(message = "网关地址不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String serverUrl; + + /** + * 开放平台上创建的应用的 ID + */ + @NotBlank(message = "开放平台上创建的应用的 ID不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String appId; + + /** + * 签名算法类型,推荐:RSA2 + *

+ * {@link #SIGN_TYPE_DEFAULT} + */ + @NotBlank(message = "签名算法类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private String signType; + + /** + * 公钥类型 + * 1. {@link #MODE_PUBLIC_KEY} 情况,privateKey + alipayPublicKey + * 2. {@link #MODE_CERTIFICATE} 情况,appCertContent + alipayPublicCertContent + rootCertContent + */ + @NotNull(message = "公钥类型不能为空", groups = {ModePublicKey.class, ModeCertificate.class}) + private Integer mode; + + // ========== 公钥模式 ========== + /** + * 商户私钥 + */ + @NotBlank(message = "商户私钥不能为空", groups = {ModePublicKey.class}) + private String privateKey; + + /** + * 支付宝公钥字符串 + */ + @NotBlank(message = "支付宝公钥字符串不能为空", groups = {ModePublicKey.class}) + private String alipayPublicKey; + + // ========== 证书模式 ========== + /** + * 指定商户公钥应用证书内容字符串 + */ + @NotBlank(message = "指定商户公钥应用证书内容不能为空", groups = {ModeCertificate.class}) + private String appCertContent; + /** + * 指定支付宝公钥证书内容字符串 + */ + @NotBlank(message = "指定支付宝公钥证书内容不能为空", groups = {ModeCertificate.class}) + private String alipayPublicCertContent; + /** + * 指定根证书内容字符串 + */ + @NotBlank(message = "指定根证书内容字符串不能为空", groups = {ModeCertificate.class}) + private String rootCertContent; + + public interface ModePublicKey { + } + + public interface ModeCertificate { + } + + @Override + public void validate(Validator validator) { + ValidationUtils.validate(validator, this, + MODE_PUBLIC_KEY.equals(this.getMode()) ? ModePublicKey.class : ModeCertificate.class); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java new file mode 100644 index 0000000..f42fd81 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayPcPayClient.java @@ -0,0 +1,70 @@ +package com.yunxi.scm.framework.pay.core.client.impl.alipay; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.http.Method; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePagePayModel; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.response.AlipayTradePagePayResponse; +import lombok.extern.slf4j.Slf4j; + +import java.util.Objects; + +/** + * 支付宝【PC 网站】的 PayClient 实现类 + * + * 文档:电脑网站支付 + * + * @author XGD + */ +@Slf4j +public class AlipayPcPayClient extends AbstractAlipayPayClient { + + public AlipayPcPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_PC.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradePagePayModel 请求 + AlipayTradePagePayModel model = new AlipayTradePagePayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setTimeExpire(formatTime(reqDTO.getExpireTime())); + model.setProductCode("FAST_INSTANT_TRADE_PAY"); // 销售产品码. 目前 PC 支付场景下仅支持 FAST_INSTANT_TRADE_PAY + // ② 个性化的参数 + // 如果想弄更多个性化的参数,可参考 https://www.pingxx.com/api/支付渠道 extra 参数说明.html 的 alipay_pc_direct 部分进行拓展 + model.setQrPayMode("2"); // 跳转模式 - 订单码,效果参见:https://help.pingxx.com/article/1137360/ + // ③ 支付宝 PC 支付有两种展示模式:FORM、URL + String displayMode = ObjectUtil.defaultIfNull(reqDTO.getDisplayMode(), + PayOrderDisplayModeEnum.URL.getMode()); + + // 1.2 构建 AlipayTradePagePayRequest 请求 + AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePagePayResponse response; + if (Objects.equals(displayMode, PayOrderDisplayModeEnum.FORM.getMode())) { + response = client.pageExecute(request, Method.POST.name()); // 需要特殊使用 POST 请求 + } else { + response = client.pageExecute(request, Method.GET.name()); + } + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java new file mode 100644 index 0000000..9732cdf --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayQrPayClient.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.framework.pay.core.client.impl.alipay; + +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradePrecreateModel; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * 支付宝【扫码支付】的 PayClient 实现类 + * + * 文档:扫码支付 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayQrPayClient extends AbstractAlipayPayClient { + + public AlipayQrPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradePrecreateModel 请求 + AlipayTradePrecreateModel model = new AlipayTradePrecreateModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setProductCode("FACE_TO_FACE_PAYMENT"); // 销售产品码. 目前扫码支付场景下仅支持 FACE_TO_FACE_PAYMENT + // ② 个性化的参数【无】 + // ③ 支付宝扫码支付只有一种展示,考虑到前端可能希望二维码扫描后,手机打开 + String displayMode = PayOrderDisplayModeEnum.QR_CODE.getMode(); + + // 1.2 构建 AlipayTradePrecreateRequest 请求 + AlipayTradePrecreateRequest request = new AlipayTradePrecreateRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradePrecreateResponse response = client.execute(request); + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getQrCode(), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java new file mode 100644 index 0000000..8968fdc --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayWapPayClient.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.framework.pay.core.client.impl.alipay; + +import cn.hutool.http.Method; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.alipay.api.AlipayApiException; +import com.alipay.api.domain.AlipayTradeWapPayModel; +import com.alipay.api.request.AlipayTradeWapPayRequest; +import com.alipay.api.response.AlipayTradeWapPayResponse; +import lombok.extern.slf4j.Slf4j; + +/** + * 支付宝【Wap 网站】的 PayClient 实现类 + * + * 文档:手机网站支付接口 + * + * @author 芋道源码 + */ +@Slf4j +public class AlipayWapPayClient extends AbstractAlipayPayClient { + + public AlipayWapPayClient(Long channelId, AlipayPayClientConfig config) { + super(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); + } + + @Override + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException { + // 1.1 构建 AlipayTradeWapPayModel 请求 + AlipayTradeWapPayModel model = new AlipayTradeWapPayModel(); + // ① 通用的参数 + model.setOutTradeNo(reqDTO.getOutTradeNo()); + model.setSubject(reqDTO.getSubject()); + model.setBody(reqDTO.getBody()); + model.setTotalAmount(formatAmount(reqDTO.getPrice())); + model.setProductCode("QUICK_WAP_PAY"); // 销售产品码. 目前 Wap 支付场景下仅支持 QUICK_WAP_PAY + // ② 个性化的参数【无】 + // ③ 支付宝 Wap 支付只有一种展示:URL + String displayMode = PayOrderDisplayModeEnum.URL.getMode(); + + // 1.2 构建 AlipayTradeWapPayRequest 请求 + AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); + request.setBizModel(model); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + request.setReturnUrl(reqDTO.getReturnUrl()); + model.setQuitUrl(reqDTO.getReturnUrl()); + + // 2.1 执行请求 + AlipayTradeWapPayResponse response = client.pageExecute(request, Method.GET.name()); + // 2.2 处理结果 + if (!response.isSuccess()) { + return buildClosedPayOrderRespDTO(reqDTO, response); + } + return PayOrderRespDTO.waitingOf(displayMode, response.getBody(), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/mock/MockPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/mock/MockPayClient.java new file mode 100644 index 0000000..e534a9e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/mock/MockPayClient.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.framework.pay.core.client.impl.mock; + +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.impl.AbstractPayClient; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 模拟支付的 PayClient 实现类 + * + * 模拟支付返回结果都是成功,方便大家日常流畅 + * + * @author jason + */ +public class MockPayClient extends AbstractPayClient { + + private static final String MOCK_RESP_SUCCESS_DATA = "MOCK_SUCCESS"; + + public MockPayClient(Long channelId, MockPayClientConfig config) { + super(channelId, PayChannelEnum.MOCK.getCode(), config); + } + + @Override + protected void doInit() { + } + + @Override + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) { + return PayOrderRespDTO.successOf("MOCK-P-" + reqDTO.getOutTradeNo(), "", LocalDateTime.now(), + reqDTO.getOutTradeNo(), MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) { + return PayOrderRespDTO.successOf("MOCK-P-" + outTradeNo, "", LocalDateTime.now(), + outTradeNo, MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) { + return PayRefundRespDTO.successOf("MOCK-R-" + reqDTO.getOutRefundNo(), LocalDateTime.now(), + reqDTO.getOutRefundNo(), MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) { + return PayRefundRespDTO.successOf("MOCK-R-" + outRefundNo, LocalDateTime.now(), + outRefundNo, MOCK_RESP_SUCCESS_DATA); + } + + @Override + protected PayRefundRespDTO doParseRefundNotify(Map params, String body) { + throw new UnsupportedOperationException("模拟支付无退款回调"); + } + + @Override + protected PayOrderRespDTO doParseOrderNotify(Map params, String body) { + throw new UnsupportedOperationException("模拟支付无支付回调"); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/mock/MockPayClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/mock/MockPayClientConfig.java new file mode 100644 index 0000000..fedd9d8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/mock/MockPayClientConfig.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.pay.core.client.impl.mock; + +import com.yunxi.scm.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import javax.validation.Validator; + +/** + * 模拟支付的 PayClientConfig 实现类 + * + * @author jason + */ +@Data +public class MockPayClientConfig implements PayClientConfig { + + /** + * 配置名称 + * + * 如果不加任何属性,JsonUtils.parseObject2 解析会报错,所以暂时加个名称 + */ + private String name; + + @Override + public void validate(Validator validator) { + // 模拟支付配置无需校验 + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java new file mode 100644 index 0000000..0dbd6c1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/AbstractWxPayClient.java @@ -0,0 +1,470 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.date.TemporalAccessorUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.io.FileUtils; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.impl.AbstractPayClient; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult; +import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyV3Result; +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyV3Result; +import com.github.binarywang.wxpay.bean.request.*; +import com.github.binarywang.wxpay.bean.result.*; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Map; +import java.util.Objects; + +import static cn.hutool.core.date.DatePattern.*; +import static com.yunxi.scm.framework.pay.core.client.impl.weixin.WxPayClientConfig.API_VERSION_V2; + +/** + * 微信支付抽象类,实现微信统一的接口、以及部分实现(退款) + * + * @author 遇到源码 + */ +@Slf4j +public abstract class AbstractWxPayClient extends AbstractPayClient { + + protected WxPayService client; + + public AbstractWxPayClient(Long channelId, String channelCode, WxPayClientConfig config) { + super(channelId, channelCode, config); + } + + /** + * 初始化 client 客户端 + * + * @param tradeType 交易类型 + */ + protected void doInit(String tradeType) { + // 创建 config 配置 + WxPayConfig payConfig = new WxPayConfig(); + BeanUtil.copyProperties(config, payConfig, "keyContent"); + payConfig.setTradeType(tradeType); + // weixin-pay-java 无法设置内容,只允许读取文件,所以这里要创建临时文件来解决 + if (Base64.isBase64(config.getKeyContent())) { + payConfig.setKeyPath(FileUtils.createTempFile(Base64.decode(config.getKeyContent())).getPath()); + } + if (StrUtil.isNotEmpty(config.getPrivateKeyContent())) { + payConfig.setPrivateKeyPath(FileUtils.createTempFile(config.getPrivateKeyContent()).getPath()); + } + if (StrUtil.isNotEmpty(config.getPrivateCertContent())) { + payConfig.setPrivateCertPath(FileUtils.createTempFile(config.getPrivateCertContent()).getPath()); + } + + // 创建 client 客户端 + client = new WxPayServiceImpl(); + client.setConfig(payConfig); + } + + // ============ 支付相关 ========== + + @Override + protected PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws Exception { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doUnifiedOrderV2(reqDTO); + case WxPayClientConfig.API_VERSION_V3: + return doUnifiedOrderV3(reqDTO); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayOrderRespDTO.closedOf(errorCode, errorMessage, + reqDTO.getOutTradeNo(), e.getXmlString()); + } + } + + /** + * 【V2】调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + protected abstract PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) + throws Exception; + + /** + * 【V3】调用支付渠道,统一下单 + * + * @param reqDTO 下单信息 + * @return 各支付渠道的返回结果 + */ + protected abstract PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) + throws WxPayException; + + /** + * 【V2】创建微信下单请求 + * + * @param reqDTO 下信息 + * @return 下单请求 + */ + protected WxPayUnifiedOrderRequest buildPayUnifiedOrderRequestV2(PayOrderUnifiedReqDTO reqDTO) { + return WxPayUnifiedOrderRequest.newBuilder() + .outTradeNo(reqDTO.getOutTradeNo()) + .body(reqDTO.getSubject()) + .detail(reqDTO.getBody()) + .totalFee(reqDTO.getPrice()) // 单位分 + .timeExpire(formatDateV2(reqDTO.getExpireTime())) + .spbillCreateIp(reqDTO.getUserIp()) + .notifyUrl(reqDTO.getNotifyUrl()) + .build(); + } + + /** + * 【V3】创建微信下单请求 + * + * @param reqDTO 下信息 + * @return 下单请求 + */ + protected WxPayUnifiedOrderV3Request buildPayUnifiedOrderRequestV3(PayOrderUnifiedReqDTO reqDTO) { + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(reqDTO.getOutTradeNo()); + request.setDescription(reqDTO.getSubject()); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(reqDTO.getPrice())); // 单位分 + request.setTimeExpire(formatDateV3(reqDTO.getExpireTime())); + request.setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp(reqDTO.getUserIp())); + request.setNotifyUrl(reqDTO.getNotifyUrl()); + return request; + } + + @Override + public PayOrderRespDTO doParseOrderNotify(Map params, String body) throws WxPayException { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doParseOrderNotifyV2(body); + case WxPayClientConfig.API_VERSION_V3: + return doParseOrderNotifyV3(body); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayOrderRespDTO doParseOrderNotifyV2(String body) throws WxPayException { + // 1. 解析回调 + WxPayOrderNotifyResult response = client.parseOrderNotifyResult(body); + // 2. 构建结果 + // V2 微信支付的回调,只有 SUCCESS 支付成功、CLOSED 支付失败两种情况,无需像支付宝一样解析的比较复杂 + Integer status = Objects.equals(response.getResultCode(), "SUCCESS") ? + PayOrderStatusRespEnum.SUCCESS.getStatus() : PayOrderStatusRespEnum.CLOSED.getStatus(); + return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + response.getOutTradeNo(), body); + } + + private PayOrderRespDTO doParseOrderNotifyV3(String body) throws WxPayException { + // 1. 解析回调 + WxPayOrderNotifyV3Result response = client.parseOrderNotifyV3Result(body, null); + WxPayOrderNotifyV3Result.DecryptNotifyResult result = response.getResult(); + // 2. 构建结果 + Integer status = parseStatus(result.getTradeState()); + String openid = result.getPayer() != null ? result.getPayer().getOpenid() : null; + return PayOrderRespDTO.of(status, result.getTransactionId(), openid, parseDateV3(result.getSuccessTime()), + result.getOutTradeNo(), body); + } + + @Override + protected PayOrderRespDTO doGetOrder(String outTradeNo) throws Throwable { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doGetOrderV2(outTradeNo); + case WxPayClientConfig.API_VERSION_V3: + return doGetOrderV3(outTradeNo); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + if (ObjectUtils.equalsAny(e.getErrCode(), "ORDERNOTEXIST", "ORDER_NOT_EXIST")) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayOrderRespDTO.closedOf(errorCode, errorMessage, + outTradeNo, e.getXmlString()); + } + throw e; + } + } + + private PayOrderRespDTO doGetOrderV2(String outTradeNo) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayOrderQueryRequest request = WxPayOrderQueryRequest.newBuilder() + .outTradeNo(outTradeNo).build(); + // 执行请求 + WxPayOrderQueryResult response = client.queryOrder(request); + + // 转换结果 + Integer status = parseStatus(response.getTradeState()); + return PayOrderRespDTO.of(status, response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + outTradeNo, response); + } + + private PayOrderRespDTO doGetOrderV3(String outTradeNo) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayOrderQueryV3Request request = new WxPayOrderQueryV3Request() + .setOutTradeNo(outTradeNo); + // 执行请求 + WxPayOrderQueryV3Result response = client.queryOrderV3(request); + + // 转换结果 + Integer status = parseStatus(response.getTradeState()); + String openid = response.getPayer() != null ? response.getPayer().getOpenid() : null; + return PayOrderRespDTO.of(status, response.getTransactionId(), openid, parseDateV3(response.getSuccessTime()), + outTradeNo, response); + } + + private static Integer parseStatus(String tradeState) { + switch (tradeState) { + case "NOTPAY": + case "USERPAYING": // 支付中,等待用户输入密码(条码支付独有) + return PayOrderStatusRespEnum.WAITING.getStatus(); + case "SUCCESS": + return PayOrderStatusRespEnum.SUCCESS.getStatus(); + case "REFUND": + return PayOrderStatusRespEnum.REFUND.getStatus(); + case "CLOSED": + case "REVOKED": // 已撤销(刷卡支付独有) + case "PAYERROR": // 支付失败(其它原因,如银行返回失败) + return PayOrderStatusRespEnum.CLOSED.getStatus(); + default: + throw new IllegalArgumentException(StrUtil.format("未知的支付状态({})", tradeState)); + } + } + + // ============ 退款相关 ========== + + @Override + protected PayRefundRespDTO doUnifiedRefund(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doUnifiedRefundV2(reqDTO); + case WxPayClientConfig.API_VERSION_V3: + return doUnifiedRefundV3(reqDTO); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayRefundRespDTO.failureOf(errorCode, errorMessage, + reqDTO.getOutTradeNo(), e.getXmlString()); + } + } + + private PayRefundRespDTO doUnifiedRefundV2(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundRequest request = new WxPayRefundRequest() + .setOutTradeNo(reqDTO.getOutTradeNo()) + .setOutRefundNo(reqDTO.getOutRefundNo()) + .setRefundFee(reqDTO.getRefundPrice()) + .setRefundDesc(reqDTO.getReason()) + .setTotalFee(reqDTO.getPayPrice()) + .setNotifyUrl(reqDTO.getNotifyUrl()); + // 2.1 执行请求 + WxPayRefundResult response = client.refundV2(request); + // 2.2 创建返回结果 + if (Objects.equals("SUCCESS", response.getResultCode())) { // V2 情况下,不直接返回退款成功,而是等待异步通知 + return PayRefundRespDTO.waitingOf(response.getRefundId(), + reqDTO.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); + } + + private PayRefundRespDTO doUnifiedRefundV3(PayRefundUnifiedReqDTO reqDTO) throws Throwable { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo(reqDTO.getOutTradeNo()) + .setOutRefundNo(reqDTO.getOutRefundNo()) + .setAmount(new WxPayRefundV3Request.Amount().setRefund(reqDTO.getRefundPrice()) + .setTotal(reqDTO.getPayPrice()).setCurrency("CNY")) + .setReason(reqDTO.getReason()) + .setNotifyUrl(reqDTO.getNotifyUrl()); + // 2.1 执行请求 + WxPayRefundV3Result response = client.refundV3(request); + // 2.2 创建返回结果 + if (Objects.equals("SUCCESS", response.getStatus())) { + return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()), + reqDTO.getOutRefundNo(), response); + } + if (Objects.equals("PROCESSING", response.getStatus())) { + return PayRefundRespDTO.waitingOf(response.getRefundId(), + reqDTO.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(reqDTO.getOutRefundNo(), response); + } + + @Override + public PayRefundRespDTO doParseRefundNotify(Map params, String body) throws WxPayException { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doParseRefundNotifyV2(body); + case WxPayClientConfig.API_VERSION_V3: + return parseRefundNotifyV3(body); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } + + private PayRefundRespDTO doParseRefundNotifyV2(String body) throws WxPayException { + // 1. 解析回调 + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(body); + WxPayRefundNotifyResult.ReqInfo result = response.getReqInfo(); + // 2. 构建结果 + if (Objects.equals("SUCCESS", result.getRefundStatus())) { + return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV2B(result.getSuccessTime()), + result.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response); + } + + private PayRefundRespDTO parseRefundNotifyV3(String body) throws WxPayException { + // 1. 解析回调 + WxPayRefundNotifyV3Result response = client.parseRefundNotifyV3Result(body, null); + WxPayRefundNotifyV3Result.DecryptNotifyResult result = response.getResult(); + // 2. 构建结果 + if (Objects.equals("SUCCESS", result.getRefundStatus())) { + return PayRefundRespDTO.successOf(result.getRefundId(), parseDateV3(result.getSuccessTime()), + result.getOutRefundNo(), response); + } + return PayRefundRespDTO.failureOf(result.getOutRefundNo(), response); + } + + @Override + protected PayRefundRespDTO doGetRefund(String outTradeNo, String outRefundNo) throws WxPayException { + try { + switch (config.getApiVersion()) { + case API_VERSION_V2: + return doGetRefundV2(outTradeNo, outRefundNo); + case WxPayClientConfig.API_VERSION_V3: + return doGetRefundV3(outTradeNo, outRefundNo); + default: + throw new IllegalArgumentException(String.format("未知的 API 版本(%s)", config.getApiVersion())); + } + } catch (WxPayException e) { + if (ObjectUtils.equalsAny(e.getErrCode(), "REFUNDNOTEXIST", "RESOURCE_NOT_EXISTS")) { + String errorCode = getErrorCode(e); + String errorMessage = getErrorMessage(e); + return PayRefundRespDTO.failureOf(errorCode, errorMessage, + outRefundNo, e.getXmlString()); + } + throw e; + } + } + + private PayRefundRespDTO doGetRefundV2(String outTradeNo, String outRefundNo) throws WxPayException { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundQueryRequest request = WxPayRefundQueryRequest.newBuilder() + .outTradeNo(outTradeNo) + .outRefundNo(outRefundNo) + .build(); + // 2.1 执行请求 + WxPayRefundQueryResult response = client.refundQuery(request); + // 2.2 创建返回结果 + if (!Objects.equals("SUCCESS", response.getResultCode())) { + return PayRefundRespDTO.waitingOf(null, + outRefundNo, response); + } + WxPayRefundQueryResult.RefundRecord refund = CollUtil.findOne(response.getRefundRecords(), + record -> record.getOutRefundNo().equals(outRefundNo)); + if (refund == null) { + return PayRefundRespDTO.failureOf(outRefundNo, response); + } + switch (refund.getRefundStatus()) { + case "SUCCESS": + return PayRefundRespDTO.successOf(refund.getRefundId(), parseDateV2B(refund.getRefundSuccessTime()), + outRefundNo, response); + case "PROCESSING": + return PayRefundRespDTO.waitingOf(refund.getRefundId(), + outRefundNo, response); + case "CHANGE": // 退款到银行发现用户的卡作废或者冻结了,导致原路退款银行卡失败,资金回流到商户的现金帐号,需要商户人工干预,通过线下或者财付通转账的方式进行退款 + case "FAIL": + return PayRefundRespDTO.failureOf(outRefundNo, response); + default: + throw new IllegalArgumentException(String.format("未知的退款状态(%s)", refund.getRefundStatus())); + } + } + + private PayRefundRespDTO doGetRefundV3(String outTradeNo, String outRefundNo) throws WxPayException { + // 1. 构建 WxPayRefundRequest 请求 + WxPayRefundQueryV3Request request = new WxPayRefundQueryV3Request(); + request.setOutRefundNo(outRefundNo); + // 2.1 执行请求 + WxPayRefundQueryV3Result response = client.refundQueryV3(request); + // 2.2 创建返回结果 + switch (response.getStatus()) { + case "SUCCESS": + return PayRefundRespDTO.successOf(response.getRefundId(), parseDateV3(response.getSuccessTime()), + outRefundNo, response); + case "PROCESSING": + return PayRefundRespDTO.waitingOf(response.getRefundId(), + outRefundNo, response); + case "ABNORMAL": // 退款异常 + case "CLOSED": + return PayRefundRespDTO.failureOf(outRefundNo, response); + default: + throw new IllegalArgumentException(String.format("未知的退款状态(%s)", response.getStatus())); + } + } + + // ========== 各种工具方法 ========== + + static String formatDateV2(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), PURE_DATETIME_PATTERN); + } + + static LocalDateTime parseDateV2(String time) { + return LocalDateTimeUtil.parse(time, PURE_DATETIME_PATTERN); + } + + static LocalDateTime parseDateV2B(String time) { + return LocalDateTimeUtil.parse(time, NORM_DATETIME_PATTERN); + } + + static String formatDateV3(LocalDateTime time) { + return TemporalAccessorUtil.format(time.atZone(ZoneId.systemDefault()), UTC_WITH_XXX_OFFSET_PATTERN); + } + + static LocalDateTime parseDateV3(String time) { + return LocalDateTimeUtil.parse(time, UTC_WITH_XXX_OFFSET_PATTERN); + } + + static String getErrorCode(WxPayException e) { + if (StrUtil.isNotEmpty(e.getErrCode())) { + return e.getErrCode(); + } + if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) { + return "CUSTOM_ERROR"; + } + return e.getReturnCode(); + } + + static String getErrorMessage(WxPayException e) { + if (StrUtil.isNotEmpty(e.getErrCode())) { + return e.getErrCodeDes(); + } + if (StrUtil.isNotEmpty(e.getCustomErrorMsg())) { + return e.getCustomErrorMsg(); + } + return e.getReturnMsg(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxAppPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxAppPayClient.java new file mode 100644 index 0000000..9f8ac66 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxAppPayClient.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付【App 支付】的 PayClient 实现类 + * + * 文档:App 支付 + * + * // TODO 芋艿:未详细测试,因为手头没 App + * + * @author 芋道源码 + */ +@Slf4j +public class WxAppPayClient extends AbstractWxPayClient { + + public WxAppPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_APP.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.APP); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); + // 执行请求 + WxPayMpOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderV3Request 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); + // 执行请求 + WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.APP, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxBarPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxBarPayClient.java new file mode 100644 index 0000000..123e201 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxBarPayClient.java @@ -0,0 +1,107 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; +import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付【付款码支付】的 PayClient 实现类 + * + * 文档:付款码支付 + * + * @author 芋道源码 + */ +@Slf4j +public class WxBarPayClient extends AbstractWxPayClient { + + /** + * 微信付款码的过期时间 + */ + private static final Duration AUTH_CODE_EXPIRE = Duration.ofMinutes(3); + + public WxBarPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_BAR.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.MICROPAY); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 由于付款码需要不断轮询,所以需要在较短的时间完成支付 + LocalDateTime expireTime = LocalDateTimeUtils.addTime(AUTH_CODE_EXPIRE); + if (expireTime.isAfter(reqDTO.getExpireTime())) { + expireTime = reqDTO.getExpireTime(); + } + // 构建 WxPayMicropayRequest 对象 + WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() + .outTradeNo(reqDTO.getOutTradeNo()) + .body(reqDTO.getSubject()) + .detail(reqDTO.getBody()) + .totalFee(reqDTO.getPrice()) // 单位分 + .timeExpire(formatDateV2(expireTime)) + .spbillCreateIp(reqDTO.getUserIp()) + .authCode(getAuthCode(reqDTO)) + .build(); + // 执行请求,重试直到失败(过期),或者成功 + WxPayException lastWxPayException = null; + for (int i = 1; i < Byte.MAX_VALUE; i++) { + try { + WxPayMicropayResult response = client.micropay(request); + // 支付成功,例如说:1)用户输入了密码;2)用户免密支付 + return PayOrderRespDTO.successOf(response.getTransactionId(), response.getOpenid(), parseDateV2(response.getTimeEnd()), + response.getOutTradeNo(), response) + .setDisplayMode(PayOrderDisplayModeEnum.BAR_CODE.getMode()); + } catch (WxPayException ex) { + lastWxPayException = ex; + // 如果不满足这 3 种任一的,则直接抛出 WxPayException 异常,不仅需处理 + // 1. SYSTEMERROR:接口返回错误:请立即调用被扫订单结果查询API,查询当前订单状态,并根据订单的状态决定下一步的操作。 + // 2. USERPAYING:用户支付中,需要输入密码:等待 5 秒,然后调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 + // 3. BANKERROR:银行系统异常:请立即调用被扫订单结果查询 API,查询当前订单的不同状态,决定下一步的操作。 + if (!StrUtil.equalsAny(ex.getErrCode(), "SYSTEMERROR", "USERPAYING", "BANKERROR")) { + throw ex; + } + // 等待 5 秒,继续下一轮重新发起支付 + log.info("[doUnifiedOrderV2][发起微信 Bar 支付第({})失败,等待下一轮重试,请求({}),响应({})]", i, + toJsonString(request), ex.getMessage()); + ThreadUtil.sleep(5, TimeUnit.SECONDS); + } + } + throw lastWxPayException; + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + return doUnifiedOrderV2(reqDTO); + } + + // ========== 各种工具方法 ========== + + static String getAuthCode(PayOrderUnifiedReqDTO reqDTO) { + String authCode = MapUtil.getStr(reqDTO.getChannelExtras(), "authCode"); + if (StrUtil.isEmpty(authCode)) { + throw invalidParamException("支付请求的 authCode 不能为空!"); + } + return authCode; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxLitePayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxLitePayClient.java new file mode 100644 index 0000000..de92cbc --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxLitePayClient.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import lombok.extern.slf4j.Slf4j; + +/** + * 微信支付【小程序】的 PayClient 实现类 + * + * 由于公众号和小程序的微信支付逻辑一致,所以直接进行继承 + * + * 文档:JSAPI 下单 + * + * @author zwy + */ +@Slf4j +public class WxLitePayClient extends WxPubPayClient { + + public WxLitePayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_LITE.getCode(), config); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxNativePayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxNativePayClient.java new file mode 100644 index 0000000..2d1a7ac --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxNativePayClient.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayNativeOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +/** + * 微信支付【Native 二维码】的 PayClient 实现类 + * + * 文档:Native 下单 + * + * @author zwy + */ +@Slf4j +public class WxNativePayClient extends AbstractWxPayClient { + + public WxNativePayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_NATIVE.getCode(), config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.NATIVE); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO); + // 执行请求 + WxPayNativeOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response.getCodeUrl(), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderV3Request 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO); + // 执行请求 + String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.QR_CODE.getMode(), response, + reqDTO.getOutTradeNo(), response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxPayClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxPayClientConfig.java new file mode 100644 index 0000000..a64201c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxPayClientConfig.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.io.IoUtil; +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import com.yunxi.scm.framework.pay.core.client.PayClientConfig; +import lombok.Data; + +import javax.validation.Validator; +import javax.validation.constraints.NotBlank; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +/** + * 微信支付的 PayClientConfig 实现类 + * 属性主要来自 {@link com.github.binarywang.wxpay.config.WxPayConfig} 的必要属性 + * + * @author 芋道源码 + */ +@Data +public class WxPayClientConfig implements PayClientConfig { + + /** + * API 版本 - V2 + * + * V2 协议说明 + */ + public static final String API_VERSION_V2 = "v2"; + /** + * API 版本 - V3 + * + * V3 协议说明 + */ + public static final String API_VERSION_V3 = "v3"; + + /** + * 公众号或者小程序的 appid + * + * 只有公众号或小程序需要该字段 + */ + @NotBlank(message = "APPID 不能为空", groups = {V2.class, V3.class}) + private String appId; + /** + * 商户号 + */ + @NotBlank(message = "商户号不能为空", groups = {V2.class, V3.class}) + private String mchId; + /** + * API 版本 + */ + @NotBlank(message = "API 版本不能为空", groups = {V2.class, V3.class}) + private String apiVersion; + + // ========== V2 版本的参数 ========== + + /** + * 商户密钥 + */ + @NotBlank(message = "商户密钥不能为空", groups = V2.class) + private String mchKey; + /** + * apiclient_cert.p12 证书文件的对应字符串【base64 格式】 + * + * 为什么采用 base64 格式?因为 p12 读取后是二进制,需要转换成 base64 格式才好传输和存储 + */ + @NotBlank(message = "apiclient_cert.p12 不能为空", groups = V2.class) + private String keyContent; + + // ========== V3 版本的参数 ========== + /** + * apiclient_key.pem 证书文件的对应字符串 + */ + @NotBlank(message = "apiclient_key 不能为空", groups = V3.class) + private String privateKeyContent; + /** + * apiclient_cert.pem 证书文件的对应的字符串 + */ + @NotBlank(message = "apiclient_cert 不能为空", groups = V3.class) + private String privateCertContent; + /** + * apiV3 密钥值 + */ + @NotBlank(message = "apiV3 密钥值不能为空", groups = V3.class) + private String apiV3Key; + + /** + * 分组校验 v2版本 + */ + public interface V2 { + } + + /** + * 分组校验 v3版本 + */ + public interface V3 { + } + + @Override + public void validate(Validator validator) { + ValidationUtils.validate(validator, this, + API_VERSION_V2.equals(this.getApiVersion()) ? V2.class : V3.class); + } + + public static void main(String[] args) throws FileNotFoundException { + String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"; + /// String path = "/Users/yunai/Downloads/wx_pay/apiclient_key.pem"; + /// String path = "/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"; + System.out.println(IoUtil.readUtf8(new FileInputStream(path))); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxPubPayClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxPubPayClient.java new file mode 100644 index 0000000..312d94e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxPubPayClient.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.github.binarywang.wxpay.bean.order.WxPayMpOrderResult; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.constant.WxPayConstants; +import com.github.binarywang.wxpay.exception.WxPayException; +import lombok.extern.slf4j.Slf4j; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.invalidParamException; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 微信支付(公众号)的 PayClient 实现类 + * + * 文档:JSAPI 下单 + * + * @author 芋道源码 + */ +@Slf4j +public class WxPubPayClient extends AbstractWxPayClient { + + public WxPubPayClient(Long channelId, WxPayClientConfig config) { + super(channelId, PayChannelEnum.WX_PUB.getCode(), config); + } + + protected WxPubPayClient(Long channelId, String channelCode, WxPayClientConfig config) { + super(channelId, channelCode, config); + } + + @Override + protected void doInit() { + super.doInit(WxPayConstants.TradeType.JSAPI); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV2(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderRequest request = buildPayUnifiedOrderRequestV2(reqDTO) + .setOpenid(getOpenid(reqDTO)); + // 执行请求 + WxPayMpOrderResult response = client.createOrder(request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + @Override + protected PayOrderRespDTO doUnifiedOrderV3(PayOrderUnifiedReqDTO reqDTO) throws WxPayException { + // 构建 WxPayUnifiedOrderRequest 对象 + WxPayUnifiedOrderV3Request request = buildPayUnifiedOrderRequestV3(reqDTO) + .setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(getOpenid(reqDTO))); + // 执行请求 + WxPayUnifiedOrderV3Result.JsapiResult response = client.createOrderV3(TradeTypeEnum.JSAPI, request); + + // 转换结果 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), toJsonString(response), + reqDTO.getOutTradeNo(), response); + } + + // ========== 各种工具方法 ========== + + static String getOpenid(PayOrderUnifiedReqDTO reqDTO) { + String openid = MapUtil.getStr(reqDTO.getChannelExtras(), "openid"); + if (StrUtil.isEmpty(openid)) { + throw invalidParamException("支付请求的 openid 不能为空!"); + } + return openid; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/channel/PayChannelEnum.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/channel/PayChannelEnum.java new file mode 100644 index 0000000..ba25907 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/channel/PayChannelEnum.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.framework.pay.core.enums.channel; + +import cn.hutool.core.util.ArrayUtil; +import com.yunxi.scm.framework.pay.core.client.PayClientConfig; +import com.yunxi.scm.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import com.yunxi.scm.framework.pay.core.client.impl.mock.MockPayClientConfig; +import com.yunxi.scm.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付渠道的编码的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayChannelEnum { + + WX_PUB("wx_pub", "微信 JSAPI 支付", WxPayClientConfig.class), // 公众号网页 + WX_LITE("wx_lite", "微信小程序支付", WxPayClientConfig.class), + WX_APP("wx_app", "微信 App 支付", WxPayClientConfig.class), + WX_NATIVE("wx_native", "微信 Native 支付", WxPayClientConfig.class), + WX_BAR("wx_bar", "微信付款码支付", WxPayClientConfig.class), + + ALIPAY_PC("alipay_pc", "支付宝 PC 网站支付", AlipayPayClientConfig.class), + ALIPAY_WAP("alipay_wap", "支付宝 Wap 网站支付", AlipayPayClientConfig.class), + ALIPAY_APP("alipay_app", "支付宝App 支付", AlipayPayClientConfig.class), + ALIPAY_QR("alipay_qr", "支付宝扫码支付", AlipayPayClientConfig.class), + ALIPAY_BAR("alipay_bar", "支付宝条码支付", AlipayPayClientConfig.class), + + MOCK("mock", "模拟支付", MockPayClientConfig.class); + + /** + * 编码 + * + * 参考 支付渠道属性值 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + /** + * 配置类 + */ + private final Class configClass; + + /** + * 微信支付 + */ + public static final String WECHAT = "WECHAT"; + + /** + * 支付宝支付 + */ + public static final String ALIPAY = "ALIPAY"; + + public static PayChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java new file mode 100644 index 0000000..decf55c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/order/PayOrderDisplayModeEnum.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.framework.pay.core.enums.order; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付 UI 展示模式 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderDisplayModeEnum { + + URL("url"), // Redirect 跳转链接的方式 + IFRAME("iframe"), // IFrame 内嵌链接的方式【目前暂时用不到】 + FORM("form"), // HTML 表单提交 + QR_CODE("qr_code"), // 二维码的文字内容 + QR_CODE_URL("qr_code_url"), // 二维码的图片链接 + BAR_CODE("bar_code"), // 条形码 + APP("app"), // 应用:Android、iOS、微信小程序、微信公众号等,需要做自定义处理的 + ; + + /** + * 展示模式 + */ + private final String mode; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/order/PayOrderStatusRespEnum.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/order/PayOrderStatusRespEnum.java new file mode 100644 index 0000000..ff767f2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/order/PayOrderStatusRespEnum.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.framework.pay.core.enums.order; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的支付状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderStatusRespEnum { + + WAITING(0, "未支付"), + SUCCESS(10, "支付成功"), + REFUND(20, "已退款"), + CLOSED(30, "支付关闭"), + ; + + private final Integer status; + private final String name; + + /** + * 判断是否支付成功 + * + * @param status 状态 + * @return 是否支付成功 + */ + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + /** + * 判断是否已退款 + * + * @param status 状态 + * @return 是否支付成功 + */ + public static boolean isRefund(Integer status) { + return Objects.equals(status, REFUND.getStatus()); + } + + /** + * 判断是否支付关闭 + * + * @param status 状态 + * @return 是否支付关闭 + */ + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java new file mode 100644 index 0000000..daa7ca5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/java/com/yunxi/scm/framework/pay/core/enums/refund/PayRefundStatusRespEnum.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.framework.pay.core.enums.refund; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的退款状态枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum PayRefundStatusRespEnum { + + WAITING(0, "等待退款"), + SUCCESS(10, "退款成功"), + FAILURE(20, "退款失败"); + + private final Integer status; + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isFailure(Integer status) { + return Objects.equals(status, FAILURE.getStatus()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..e49fa8d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.pay.config.YunxiPayAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java new file mode 100644 index 0000000..1e8e8db --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/PayClientFactoryImplIntegrationTest.java @@ -0,0 +1,133 @@ +package com.yunxi.scm.framework.pay.core.client.impl; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.pay.core.client.PayClient; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import com.yunxi.scm.framework.pay.core.client.impl.alipay.AlipayQrPayClient; +import com.yunxi.scm.framework.pay.core.client.impl.alipay.AlipayWapPayClient; +import com.yunxi.scm.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import com.yunxi.scm.framework.pay.core.client.impl.weixin.WxPubPayClient; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +/** + * {@link PayClientFactoryImpl} 的集成测试 + * + * @author 芋道源码 + */ +@Disabled +public class PayClientFactoryImplIntegrationTest { + + private static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do"; + + private final PayClientFactoryImpl payClientFactory = new PayClientFactoryImpl(); + + /** + * {@link WxPubPayClient} 的 V2 版本 + */ + @Test + public void testCreatePayClient_WX_PUB_V2() { + // 创建配置 + WxPayClientConfig config = new WxPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WxPayClientConfig.API_VERSION_V2); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(result); + } + + /** + * {@link WxPubPayClient} 的 V3 版本 + */ + @Test + public void testCreatePayClient_WX_PUB_V3() throws FileNotFoundException { + // 创建配置 + WxPayClientConfig config = new WxPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WxPayClientConfig.API_VERSION_V3); + config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"))); + config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); + config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.WX_PUB.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(result); + } + + /** + * {@link AlipayQrPayClient} + */ + @Test + @SuppressWarnings("unchecked") + public void testCreatePayClient_ALIPAY_QR() { + // 创建配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); + config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_QR.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); + reqDTO.setNotifyUrl("http://yunai.natapp1.cc/admin-api/pay/notify/callback/18"); // TODO @tina: 这里改成你的 natapp 回调地址 +// CommonResult result = (CommonResult) client.unifiedOrder(reqDTO); +// System.out.println(JsonUtils.toJsonString(result)); +// System.out.println(result.getData().getQrCode()); + } + + /** + * {@link AlipayWapPayClient} + */ + @Test + public void testCreatePayClient_ALIPAY_WAP() { + // 创建配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); + config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + // 创建客户端 + Long channelId = RandomUtil.randomLong(); + payClientFactory.createOrUpdatePayClient(channelId, PayChannelEnum.ALIPAY_WAP.getCode(), config); + PayClient client = payClientFactory.getPayClient(channelId); + // 发起支付 + PayOrderUnifiedReqDTO reqDTO = buildPayOrderUnifiedReqDTO(); +// CommonResult result = client.unifiedOrder(reqDTO); +// System.out.println(JsonUtils.toJsonString(result)); + } + + private static PayOrderUnifiedReqDTO buildPayOrderUnifiedReqDTO() { + PayOrderUnifiedReqDTO reqDTO = new PayOrderUnifiedReqDTO(); + reqDTO.setPrice(123); + reqDTO.setSubject("IPhone 13"); + reqDTO.setBody("biubiubiu"); + reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis())); + reqDTO.setUserIp("127.0.0.1"); + reqDTO.setNotifyUrl("http://127.0.0.1:8080"); + return reqDTO; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java new file mode 100644 index 0000000..41ebcd7 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/alipay/AlipayQrPayClientTest.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.framework.pay.core.client.impl.alipay; +import cn.hutool.core.util.ReflectUtil; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.alipay.api.AlipayApiException; +import com.alipay.api.DefaultAlipayClient; +import com.alipay.api.request.AlipayTradePrecreateRequest; +import com.alipay.api.response.AlipayTradePrecreateResponse; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +public class AlipayQrPayClientTest extends BaseMockitoUnitTest { + + private static final String SERVER_URL_SANDBOX = "https://openapi.alipaydev.com/gateway.do"; + + private final AlipayPayClientConfig config = new AlipayPayClientConfig() + .setAppId("2021000118634035") + .setServerUrl(SERVER_URL_SANDBOX) + .setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT) + // TODO @tina:key 可以随机就好,简洁一点哈。 + .setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJ" + + "v890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T" + + "01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH" + + "6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScw" + + "lSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63tr" + + "epo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdk" + + "USmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr" + + "8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w" + + "0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENi" + + "vAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPw" + + "YcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQO" + + "LFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsm" + + "yX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i9" + + "5Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOU" + + "hVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD" + + "/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v1" + + "8p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ" + + "8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4e" + + "N0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6p" + + "bKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erx" + + "TRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8=") + .setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0" + + "gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBN" + + "lrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZ" + + "ikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + + @InjectMocks + AlipayQrPayClient client = new AlipayQrPayClient(10L,config); + + @Mock + private DefaultAlipayClient defaultAlipayClient; + + @Test + public void testDoInit(){ + client.doInit(); + assertNotSame(defaultAlipayClient, ReflectUtil.getFieldValue(client, "defaultAlipayClient")); + } + + @Test + @Disabled // TODO 芋艿:临时禁用 + public void create() throws AlipayApiException { + // TODO @tina:参数可以尽量随机一点,使用随机方法。这样的好处是,避免对固定参数的依赖,导致可能仅仅满足固定参数的结果 + // 这里,设置可以直接随机整个对象。 + Long shopOrderId = System.currentTimeMillis(); + PayOrderUnifiedReqDTO reqDTO=new PayOrderUnifiedReqDTO(); + reqDTO.setOutTradeNo(String.valueOf(System.currentTimeMillis())); + reqDTO.setPrice(1); + reqDTO.setBody("内容:" + shopOrderId); + reqDTO.setSubject("标题:"+shopOrderId); + String notify="http://niubi.natapp1.cc/api/pay/order/notify"; + reqDTO.setNotifyUrl(notify); + + AlipayTradePrecreateResponse response=randomPojo(AlipayTradePrecreateResponse.class,o->o.setQrCode("success")); + + when(defaultAlipayClient.execute(argThat((ArgumentMatcher) request ->{ + assertEquals(notify,request.getNotifyUrl()); + return true; + }))).thenReturn(response); + + +// PayCommonResult result = client.doUnifiedOrder(reqDTO); +// // 断言 +// assertEquals(response.getCode(), result.getApiCode()); +// assertEquals(response.getMsg(), result.getApiMsg()); +// // TODO @tina:这个断言木有过? +// assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); +// assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java new file mode 100644 index 0000000..94e39fb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxBarPayClientIntegrationTest.java @@ -0,0 +1,123 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.github.binarywang.wxpay.bean.notify.WxPayRefundNotifyResult; +import com.github.binarywang.wxpay.bean.request.WxPayMicropayRequest; +import com.github.binarywang.wxpay.bean.request.WxPayRefundRequest; +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayMicropayResult; +import com.github.binarywang.wxpay.bean.result.WxPayRefundResult; +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static com.yunxi.scm.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV2; + +/** + * {@link WxBarPayClient} 的集成测试,用于快速调试微信条码支付 + * + * @author 芋道源码 + */ +@Disabled +public class WxBarPayClientIntegrationTest { + + @Test + public void testPayV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起支付 + WxPayMicropayRequest request = WxPayMicropayRequest.newBuilder() + .outTradeNo(String.valueOf(System.currentTimeMillis())) + .body("测试支付-body") + .detail("测试支付-detail") + .totalFee(1) // 单位分 + .timeExpire(formatDateV2(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) + .spbillCreateIp("127.0.0.1") + .authCode("134298744426278497") + .build(); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayMicropayResult response = client.micropay(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testParseRefundNotifyV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行解析 + String xml = "SUCCESS"; + WxPayRefundNotifyResult response = client.parseRefundNotifyResult(xml); + System.out.println(response.getReqInfo()); + } + + @Test + public void testRefundV2() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundRequest request = new WxPayRefundRequest() + .setOutTradeNo("1689545667276") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setRefundFee(1) + .setRefundDesc("就是想退了") + .setTotalFee(1); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundResult response = client.refund(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testRefundV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV2(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo("1689506325635") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) + .setReason("就是想退了"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundV3Result response = client.refundV3(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + private WxPayConfig buildWxPayConfigV2() { + WxPayConfig config = new WxPayConfig(); + config.setAppId("wx62056c0d5e8db250"); + config.setMchId("1545083881"); + config.setMchKey("dS1ngeN63JLr3NRbvPH9AJy3MyUxZdim"); +// config.setSignType(WxPayConstants.SignType.MD5); + config.setKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.p12"); + return config; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java new file mode 100644 index 0000000..bfb4265 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-pay/src/test/java/com/yunxi/scm/framework/pay/core/client/impl/weixin/WxNativePayClientIntegrationTest.java @@ -0,0 +1,83 @@ +package com.yunxi.scm.framework.pay.core.client.impl.weixin; + +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static com.yunxi.scm.framework.pay.core.client.impl.weixin.AbstractWxPayClient.formatDateV3; + +/** + * {@link WxNativePayClient} 的集成测试,用于快速调试微信扫码支付 + * + * @author 芋道源码 + */ +@Disabled +public class WxNativePayClientIntegrationTest { + + @Test + public void testPayV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV3(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起支付 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request() + .setOutTradeNo(String.valueOf(System.currentTimeMillis())) + .setDescription("测试支付-body") + .setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(1)) // 单位分 + .setTimeExpire(formatDateV3(LocalDateTimeUtils.addTime(Duration.ofMinutes(2)))) + .setSceneInfo(new WxPayUnifiedOrderV3Request.SceneInfo().setPayerClientIp("127.0.0.1")) + .setNotifyUrl("http://127.0.0.1:48080"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + String response = client.createOrderV3(TradeTypeEnum.NATIVE, request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + @Test + public void testRefundV3() throws WxPayException { + // 创建 config 配置 + WxPayConfig config = buildWxPayConfigV3(); + // 创建 WxPayService 客户端 + WxPayService client = new WxPayServiceImpl(); + client.setConfig(config); + + // 执行发起退款 + WxPayRefundV3Request request = new WxPayRefundV3Request() + .setOutTradeNo("1689545729695") + .setOutRefundNo(String.valueOf(System.currentTimeMillis())) + .setAmount(new WxPayRefundV3Request.Amount().setTotal(1).setRefund(1).setCurrency("CNY")) + .setReason("就是想退了"); + System.out.println("========= request =========="); + System.out.println(JsonUtils.toJsonPrettyString(request)); + WxPayRefundV3Result response = client.refundV3(request); + System.out.println("========= response =========="); + System.out.println(JsonUtils.toJsonPrettyString(response)); + } + + private WxPayConfig buildWxPayConfigV3() { + WxPayConfig config = new WxPayConfig(); + config.setAppId("wx62056c0d5e8db250"); + config.setMchId("1545083881"); + config.setApiV3Key("459arNsYHl1mgkiO6H9ZH5KkhFXSxaA4"); +// config.setCertSerialNo(serialNo); + config.setPrivateCertPath("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"); + config.setPrivateKeyPath("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"); + return config; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/pom.xml new file mode 100644 index 0000000..5f39be6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/pom.xml @@ -0,0 +1,82 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-biz-sms + jar + + ${project.artifactId} + 短信拓展,支持阿里云、腾讯云 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter + + + + + io.opentracing + opentracing-util + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + + com.google.guava + guava + true + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + jakarta.validation + jakarta.validation-api + + + + + + + com.aliyun + aliyun-java-sdk-core + + + com.aliyun + aliyun-java-sdk-dysmsapi + + + com.tencentcloudapi + tencentcloud-sdk-java-sms + + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/config/YunxiSmsAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/config/YunxiSmsAutoConfiguration.java new file mode 100644 index 0000000..d74bdac --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/config/YunxiSmsAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.sms.config; + +import com.yunxi.scm.framework.sms.core.client.SmsClientFactory; +import com.yunxi.scm.framework.sms.core.client.impl.SmsClientFactoryImpl; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * 短信配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class YunxiSmsAutoConfiguration { + + @Bean + public SmsClientFactory smsClientFactory() { + return new SmsClientFactoryImpl(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsClient.java new file mode 100644 index 0000000..4177c58 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsClient.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.framework.sms.core.client; + +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; + +import java.util.List; + +/** + * 短信客户端,用于对接各短信平台的 SDK,实现短信发送等功能 + * + * @author zzf + * @since 2021/1/25 14:14 + */ +public interface SmsClient { + + /** + * 获得渠道编号 + * + * @return 渠道编号 + */ + Long getId(); + + /** + * 发送消息 + * + * @param logId 日志编号 + * @param mobile 手机号 + * @param apiTemplateId 短信 API 的模板编号 + * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 + * @return 短信发送结果 + */ + SmsCommonResult sendSms(Long logId, String mobile, String apiTemplateId, + List> templateParams); + + /** + * 解析接收短信的接收结果 + * + * @param text 结果 + * @return 结果内容 + * @throws Throwable 当解析 text 发生异常时,则会抛出异常 + */ + List parseSmsReceiveStatus(String text) throws Throwable; + + /** + * 查询指定的短信模板 + * + * @param apiTemplateId 短信 API 的模板编号 + * @return 短信模板 + */ + SmsCommonResult getSmsTemplate(String apiTemplateId); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsClientFactory.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsClientFactory.java new file mode 100644 index 0000000..5ee610d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsClientFactory.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.sms.core.client; + +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; + +/** + * 短信客户端的工厂接口 + * + * @author zzf + * @since 2021/1/28 14:01 + */ +public interface SmsClientFactory { + + /** + * 获得短信 Client + * + * @param channelId 渠道编号 + * @return 短信 Client + */ + SmsClient getSmsClient(Long channelId); + + /** + * 获得短信 Client + * + * @param channelCode 渠道编码 + * @return 短信 Client + */ + SmsClient getSmsClient(String channelCode); + + /** + * 创建短信 Client + * + * @param properties 配置对象 + */ + void createOrUpdateSmsClient(SmsChannelProperties properties); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsCodeMapping.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsCodeMapping.java new file mode 100644 index 0000000..1f8d1d5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsCodeMapping.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.framework.sms.core.client; + +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import java.util.function.Function; + +/** + * 将 API 的错误码,转换为通用的错误码 + * + * @see SmsCommonResult + * @see SmsFrameworkErrorCodeConstants + * + * @author 芋道源码 + */ +public interface SmsCodeMapping extends Function { +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsCommonResult.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsCommonResult.java new file mode 100644 index 0000000..a787c02 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/SmsCommonResult.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.framework.sms.core.client; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 短信的 CommonResult 拓展类 + * + * 考虑到不同的平台,返回的 code 和 msg 是不同的,所以统一额外返回 {@link #apiCode} 和 {@link #apiMsg} 字段 + * + * 另外,一些短信平台(例如说阿里云、腾讯云)会返回一个请求编号,用于排查请求失败的问题,我们设置到 {@link #apiRequestId} 字段 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsCommonResult extends CommonResult { + + /** + * API 返回错误码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiCode; + /** + * API 返回提示 + */ + private String apiMsg; + + /** + * API 请求编号 + */ + private String apiRequestId; + + private SmsCommonResult() { + } + + public static SmsCommonResult build(String apiCode, String apiMsg, String apiRequestId, + T data, SmsCodeMapping codeMapping) { + Assert.notNull(codeMapping, "参数 codeMapping 不能为空"); + SmsCommonResult result = new SmsCommonResult().setApiCode(apiCode).setApiMsg(apiMsg).setApiRequestId(apiRequestId); + result.setData(data); + // 翻译错误码 + if (codeMapping != null) { + ErrorCode errorCode = codeMapping.apply(apiCode); + if (errorCode == null) { + errorCode = SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + result.setCode(errorCode.getCode()).setMsg(errorCode.getMsg()); + } + return result; + } + + public static SmsCommonResult error(Throwable ex) { + SmsCommonResult result = new SmsCommonResult<>(); + result.setCode(SmsFrameworkErrorCodeConstants.EXCEPTION.getCode()); + result.setMsg(ExceptionUtil.getRootCauseMessage(ex)); + return result; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsReceiveRespDTO.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsReceiveRespDTO.java new file mode 100644 index 0000000..e7b2a21 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsReceiveRespDTO.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.framework.sms.core.client.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 消息接收 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsReceiveRespDTO { + + /** + * 是否接收成功 + */ + private Boolean success; + /** + * API 接收结果的编码 + */ + private String errorCode; + /** + * API 接收结果的说明 + */ + private String errorMsg; + + /** + * 手机号 + */ + private String mobile; + /** + * 用户接收时间 + */ + private LocalDateTime receiveTime; + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + /** + * 短信日志编号 + * + * 对应 SysSmsLogDO 的编号 + */ + private Long logId; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsSendRespDTO.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsSendRespDTO.java new file mode 100644 index 0000000..340f35a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsSendRespDTO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.framework.sms.core.client.dto; + +import lombok.Data; + +/** + * 短信发送 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsSendRespDTO { + + /** + * 短信 API 发送返回的序号 + */ + private String serialNo; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsTemplateRespDTO.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsTemplateRespDTO.java new file mode 100644 index 0000000..6599c72 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/dto/SmsTemplateRespDTO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.framework.sms.core.client.dto; + +import com.yunxi.scm.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import lombok.Data; + +/** + * 短信模板 Response DTO + * + * @author 芋道源码 + */ +@Data +public class SmsTemplateRespDTO { + + /** + * 模板编号 + */ + private String id; + /** + * 短信内容 + */ + private String content; + /** + * 审核状态 + * + * 枚举 {@link SmsTemplateAuditStatusEnum} + */ + private Integer auditStatus; + /** + * 审核未通过的理由 + */ + private String auditReason; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/AbstractSmsClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/AbstractSmsClient.java new file mode 100644 index 0000000..f46a00e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/AbstractSmsClient.java @@ -0,0 +1,127 @@ +package com.yunxi.scm.framework.sms.core.client.impl; + +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.sms.core.client.SmsClient; +import com.yunxi.scm.framework.sms.core.client.SmsCodeMapping; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 短信客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author zzf + * @since 2021/2/1 9:28 + */ +@Slf4j +public abstract class AbstractSmsClient implements SmsClient { + + /** + * 短信渠道配置 + */ + protected volatile SmsChannelProperties properties; + /** + * 错误码枚举类 + */ + protected final SmsCodeMapping codeMapping; + + public AbstractSmsClient(SmsChannelProperties properties, SmsCodeMapping codeMapping) { + this.properties = prepareProperties(properties); + this.codeMapping = codeMapping; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.info("[init][配置({}) 初始化完成]", properties); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(SmsChannelProperties properties) { + // 判断是否更新 + if (properties.equals(this.properties)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", properties); + this.properties = prepareProperties(properties); + // 初始化 + this.init(); + } + + /** + * 在赋值给{@link this#properties}前,子类可根据需要预处理短信渠道配置 + * + * @param properties 数据库中存储的短信渠道配置 + * @return 满足子类实现的短信渠道配置 + */ + protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) { + return properties; + } + + @Override + public Long getId() { + return properties.getId(); + } + + @Override + public final SmsCommonResult sendSms(Long logId, String mobile, + String apiTemplateId, List> templateParams) { + // 执行短信发送 + SmsCommonResult result; + try { + result = doSendSms(logId, mobile, apiTemplateId, templateParams); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[sendSms][发送短信异常,sendLogId({}) mobile({}) apiTemplateId({}) templateParams({})]", + logId, mobile, apiTemplateId, templateParams, ex); + // 封装返回 + return SmsCommonResult.error(ex); + } + return result; + } + + protected abstract SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) + throws Throwable; + + @Override + public List parseSmsReceiveStatus(String text) throws Throwable { + try { + return doParseSmsReceiveStatus(text); + } catch (Throwable ex) { + log.error("[parseSmsReceiveStatus][text({}) 解析发生异常]", text, ex); + throw ex; + } + } + + protected abstract List doParseSmsReceiveStatus(String text) throws Throwable; + + @Override + public SmsCommonResult getSmsTemplate(String apiTemplateId) { + // 执行短信发送 + SmsCommonResult result; + try { + result = doGetSmsTemplate(apiTemplateId); + } catch (Throwable ex) { + // 打印异常日志 + log.error("[getSmsTemplate][获得短信模板({}) 发生异常]", apiTemplateId, ex); + // 封装返回 + return SmsCommonResult.error(ex); + } + return result; + } + + protected abstract SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/SmsClientFactoryImpl.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/SmsClientFactoryImpl.java new file mode 100644 index 0000000..9eee1a4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/SmsClientFactoryImpl.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.framework.sms.core.client.impl; + +import com.yunxi.scm.framework.sms.core.client.SmsClient; +import com.yunxi.scm.framework.sms.core.client.SmsClientFactory; +import com.yunxi.scm.framework.sms.core.client.impl.aliyun.AliyunSmsClient; +import com.yunxi.scm.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; +import com.yunxi.scm.framework.sms.core.client.impl.tencent.TencentSmsClient; +import com.yunxi.scm.framework.sms.core.enums.SmsChannelEnum; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.Assert; +import org.springframework.validation.annotation.Validated; + +import java.util.Arrays; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 短信客户端工厂接口 + * + * @author zzf + */ +@Validated +@Slf4j +public class SmsClientFactoryImpl implements SmsClientFactory { + + /** + * 短信客户端 Map + * key:渠道编号,使用 {@link SmsChannelProperties#getId()} + */ + private final ConcurrentMap channelIdClients = new ConcurrentHashMap<>(); + + /** + * 短信客户端 Map + * key:渠道编码,使用 {@link SmsChannelProperties#getCode()} ()} + * + * 注意,一些场景下,需要获得某个渠道类型的客户端,所以需要使用它。 + * 例如说,解析短信接收结果,是相对通用的,不需要使用某个渠道编号的 {@link #channelIdClients} + */ + private final ConcurrentMap channelCodeClients = new ConcurrentHashMap<>(); + + public SmsClientFactoryImpl() { + // 初始化 channelCodeClients 集合 + Arrays.stream(SmsChannelEnum.values()).forEach(channel -> { + // 创建一个空的 SmsChannelProperties 对象 + SmsChannelProperties properties = new SmsChannelProperties().setCode(channel.getCode()) + .setApiKey("default default").setApiSecret("default"); + // 创建 Sms 客户端 + AbstractSmsClient smsClient = createSmsClient(properties); + channelCodeClients.put(channel.getCode(), smsClient); + }); + } + + @Override + public SmsClient getSmsClient(Long channelId) { + return channelIdClients.get(channelId); + } + + @Override + public SmsClient getSmsClient(String channelCode) { + return channelCodeClients.get(channelCode); + } + + @Override + public void createOrUpdateSmsClient(SmsChannelProperties properties) { + AbstractSmsClient client = channelIdClients.get(properties.getId()); + if (client == null) { + client = this.createSmsClient(properties); + client.init(); + channelIdClients.put(client.getId(), client); + } else { + client.refresh(properties); + } + } + + private AbstractSmsClient createSmsClient(SmsChannelProperties properties) { + SmsChannelEnum channelEnum = SmsChannelEnum.getByCode(properties.getCode()); + Assert.notNull(channelEnum, String.format("渠道类型(%s) 为空", channelEnum)); + // 创建客户端 + switch (channelEnum) { + case ALIYUN: return new AliyunSmsClient(properties); + case DEBUG_DING_TALK: return new DebugDingTalkSmsClient(properties); + case TENCENT: return new TencentSmsClient(properties); + } + // 创建失败,错误日志 + 抛出异常 + log.error("[createSmsClient][配置({}) 找不到合适的客户端实现]", properties); + throw new IllegalArgumentException(String.format("配置(%s) 找不到合适的客户端实现", properties)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java new file mode 100644 index 0000000..aa66677 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClient.java @@ -0,0 +1,212 @@ +package com.yunxi.scm.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.framework.sms.core.client.impl.AbstractSmsClient; +import com.yunxi.scm.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.aliyuncs.AcsRequest; +import com.aliyuncs.AcsResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static com.yunxi.scm.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 阿里短信客户端的实现类 + * + * @author zzf + * @since 2021/1/25 14:17 + */ +@Slf4j +public class AliyunSmsClient extends AbstractSmsClient { + + /** + * REGION, 使用杭州 + */ + private static final String ENDPOINT = "cn-hangzhou"; + + /** + * 阿里云客户端 + */ + private volatile IAcsClient client; + + public AliyunSmsClient(SmsChannelProperties properties) { + super(properties, new AliyunSmsCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + IClientProfile profile = DefaultProfile.getProfile(ENDPOINT, properties.getApiKey(), properties.getApiSecret()); + client = new DefaultAcsClient(profile); + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) { + // 构建参数 + SendSmsRequest request = new SendSmsRequest(); + request.setPhoneNumbers(mobile); + request.setSignName(properties.getSignature()); + request.setTemplateCode(apiTemplateId); + request.setTemplateParam(JsonUtils.toJsonString(MapUtils.convertMap(templateParams))); + request.setOutId(String.valueOf(sendLogId)); + // 执行请求 + return invoke(request, response -> new SmsSendRespDTO().setSerialNo(response.getBizId())); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List statuses = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return statuses.stream().map(status -> { + SmsReceiveRespDTO resp = new SmsReceiveRespDTO(); + resp.setSuccess(status.getSuccess()); + resp.setErrorCode(status.getErrCode()).setErrorMsg(status.getErrMsg()); + resp.setMobile(status.getPhoneNumber()).setReceiveTime(status.getReportTime()); + resp.setSerialNo(status.getBizId()).setLogId(Long.valueOf(status.getOutId())); + return resp; + }).collect(Collectors.toList()); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { + // 构建参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + request.setTemplateCode(apiTemplateId); + // 执行请求 + return invoke(request, response -> { + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); + data.setAuditStatus(convertSmsTemplateAuditStatus(response.getTemplateStatus())).setAuditReason(response.getReason()); + return data; + }); + } + + @VisibleForTesting + Integer convertSmsTemplateAuditStatus(Integer templateStatus) { + switch (templateStatus) { + case 0: return SmsTemplateAuditStatusEnum.CHECKING.getStatus(); + case 1: return SmsTemplateAuditStatusEnum.SUCCESS.getStatus(); + case 2: return SmsTemplateAuditStatusEnum.FAIL.getStatus(); + default: throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus)); + } + } + + @VisibleForTesting + SmsCommonResult invoke(AcsRequest request, Function responseConsumer) { + try { + // 执行发送. 由于阿里云 sms 短信没有统一的 Response,但是有统一的 code、message、requestId 属性,所以只好反射 + T sendResult = client.getAcsResponse(request); + String code = (String) ReflectUtil.getFieldValue(sendResult, "code"); + String message = (String) ReflectUtil.getFieldValue(sendResult, "message"); + String requestId = (String) ReflectUtil.getFieldValue(sendResult, "requestId"); + // 解析结果 + R data = null; + if (Objects.equals(code, "OK")) { // 请求成功的情况下 + data = responseConsumer.apply(sendResult); + } + // 拼接结果 + return SmsCommonResult.build(code, message, requestId, data, codeMapping); + } catch (ClientException ex) { + return SmsCommonResult.build(ex.getErrCode(), formatResultMsg(ex), ex.getRequestId(), null, codeMapping); + } + } + + private static String formatResultMsg(ClientException ex) { + if (StrUtil.isEmpty(ex.getErrorDescription())) { + return ex.getErrMsg(); + } + return ex.getErrMsg() + " => " + ex.getErrorDescription(); + } + + /** + * 短信接收状态 + * + * 参见 https://help.aliyun.com/document_detail/101867.html 文档 + * + * @author 芋道源码 + */ + @Data + public static class SmsReceiveStatus { + + /** + * 手机号 + */ + @JsonProperty("phone_number") + private String phoneNumber; + /** + * 发送时间 + */ + @JsonProperty("send_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime sendTime; + /** + * 状态报告时间 + */ + @JsonProperty("report_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime reportTime; + /** + * 是否接收成功 + */ + private Boolean success; + /** + * 状态报告说明 + */ + @JsonProperty("err_msg") + private String errMsg; + /** + * 状态报告编码 + */ + @JsonProperty("err_code") + private String errCode; + /** + * 发送序列号 + */ + @JsonProperty("biz_id") + private String bizId; + /** + * 用户序列号 + * + * 这里我们传递的是 SysSmsLogDO 的日志编号 + */ + @JsonProperty("out_id") + private String outId; + /** + * 短信长度,例如说 1、2、3 + * + * 140 字节算一条短信,短信长度超过 140 字节时会拆分成多条短信发送 + */ + @JsonProperty("sms_size") + private Integer smsSize; + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java new file mode 100644 index 0000000..88d3da4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMapping.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.framework.sms.core.client.impl.aliyun; + +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.sms.core.client.SmsCodeMapping; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +/** + * 阿里云的 SmsCodeMapping 实现类 + * + * 参见 https://help.aliyun.com/document_detail/101346.htm 文档 + * + * @author 芋道源码 + */ +public class AliyunSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + switch (apiCode) { + case "OK": return GlobalErrorCodeConstants.SUCCESS; + case "isv.ACCOUNT_NOT_EXISTS": + case "isv.ACCOUNT_ABNORMAL": + case "MissingAccessKeyId": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID; + case "isp.RAM_PERMISSION_DENY": return SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY; + case "isv.INVALID_JSON_PARAM": + case "isv.INVALID_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR; + case "isv.BUSINESS_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL; + case "isv.DAY_LIMIT_CONTROL": return SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL; + case "isv.SMS_CONTENT_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID; + case "isv.SMS_TEMPLATE_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID; + case "isv.SMS_SIGNATURE_ILLEGAL": + case "isv.SIGN_NAME_ILLEGAL": + case "isv.SMS_SIGN_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID; + case "isv.AMOUNT_NOT_ENOUGH": + case "isv.OUT_OF_SERVICE": return SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case "isv.MOBILE_NUMBER_ILLEGAL": return SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID; + case "isv.TEMPLATE_MISSING_PARAMETERS": return SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR; + default: return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java new file mode 100644 index 0000000..3ab2277 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkCodeMapping.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.framework.sms.core.client.impl.debug; + +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.sms.core.client.SmsCodeMapping; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import java.util.Objects; + +/** + * 钉钉的 SmsCodeMapping 实现类 + * + * @author 芋道源码 + */ +public class DebugDingTalkCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + return Objects.equals(apiCode, "0") ? GlobalErrorCodeConstants.SUCCESS : SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java new file mode 100644 index 0000000..4b41cb9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkSmsClient.java @@ -0,0 +1,96 @@ +package com.yunxi.scm.framework.sms.core.client.impl.debug; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.digest.DigestUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; +import cn.hutool.http.HttpUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.framework.sms.core.client.impl.AbstractSmsClient; +import com.yunxi.scm.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 基于钉钉 WebHook 实现的调试的短信客户端实现类 + * + * 考虑到省钱,我们使用钉钉 WebHook 模拟发送短信,方便调试。 + * + * @author 芋道源码 + */ +public class DebugDingTalkSmsClient extends AbstractSmsClient { + + public DebugDingTalkSmsClient(SmsChannelProperties properties) { + super(properties, new DebugDingTalkCodeMapping()); + Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空"); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, String mobile, + String apiTemplateId, List> templateParams) throws Throwable { + // 构建请求 + String url = buildUrl("robot/send"); + Map params = new HashMap<>(); + params.put("msgtype", "text"); + String content = String.format("【模拟短信】\n手机号:%s\n短信日志编号:%d\n模板参数:%s", + mobile, sendLogId, MapUtils.convertMap(templateParams)); + params.put("text", MapUtil.builder().put("content", content).build()); + // 执行请求 + String responseText = HttpUtil.post(url, JsonUtils.toJsonString(params)); + // 解析结果 + Map responseObj = JsonUtils.parseObject(responseText, Map.class); + return SmsCommonResult.build(MapUtil.getStr(responseObj, "errcode"), MapUtil.getStr(responseObj, "errorMsg"), + null, new SmsSendRespDTO().setSerialNo(StrUtil.uuid()), codeMapping); + } + + /** + * 构建请求地址 + * + * 参见 https://developers.dingtalk.com/document/app/custom-robot-access/title-nfv-794-g71 文档 + * + * @param path 请求路径 + * @return 请求地址 + */ + @SuppressWarnings("SameParameterValue") + private String buildUrl(String path) { + // 生成 timestamp + long timestamp = System.currentTimeMillis(); + // 生成 sign + String secret = properties.getApiSecret(); + String stringToSign = timestamp + "\n" + secret; + byte[] signData = DigestUtil.hmac(HmacAlgorithm.HmacSHA256, StrUtil.bytes(secret)).digest(stringToSign); + String sign = Base64.encode(signData); + // 构建最终 URL + return String.format("https://oapi.dingtalk.com/%s?access_token=%s×tamp=%d&sign=%s", + path, properties.getApiKey(), timestamp, sign); + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + throw new UnsupportedOperationException("模拟短信客户端,暂时无需解析回调"); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) { + SmsTemplateRespDTO data = new SmsTemplateRespDTO().setId(apiTemplateId).setContent("") + .setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(""); + return SmsCommonResult.build("0", "success", null, data, codeMapping); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java new file mode 100644 index 0000000..513249e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsChannelProperties.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.framework.sms.core.client.impl.tencent; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import lombok.Data; + +/** + * 腾讯云短信配置实现类 + * 腾讯云发送短信时,需要额外的参数 sdkAppId, + * + * @author shiwp + */ +@Data +public class TencentSmsChannelProperties extends SmsChannelProperties { + + /** + * 应用 id + */ + private String sdkAppId; + + /** + * 考虑到不破坏原有的 apiKey + apiSecret 的结构, + * 所以腾讯云短信存储时,将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 + * 因此在使用时,需要将 secretId 和 sdkAppId 解析出来,分别存储到对应字段中。 + */ + public static TencentSmsChannelProperties build(SmsChannelProperties properties) { + if (properties instanceof TencentSmsChannelProperties) { + return (TencentSmsChannelProperties) properties; + } + TencentSmsChannelProperties result = BeanUtil.toBean(properties, TencentSmsChannelProperties.class); + String combineKey = properties.getApiKey(); + Assert.notEmpty(combineKey, "apiKey 不能为空"); + String[] keys = combineKey.trim().split(" "); + Assert.isTrue(keys.length == 2, "腾讯云短信 apiKey 配置格式错误,请配置 为[secretId sdkAppId]"); + Assert.notBlank(keys[0], "腾讯云短信 secretId 不能为空"); + Assert.notBlank(keys[1], "腾讯云短信 sdkAppId 不能为空"); + result.setSdkAppId(keys[1]).setApiKey(keys[0]); + return result; + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsClient.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsClient.java new file mode 100644 index 0000000..aaa7ee6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsClient.java @@ -0,0 +1,302 @@ +package com.yunxi.scm.framework.sms.core.client.impl.tencent; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.framework.sms.core.client.impl.AbstractSmsClient; +import com.yunxi.scm.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.annotations.VisibleForTesting; +import com.tencentcloudapi.common.Credential; +import com.tencentcloudapi.common.exception.TencentCloudSDKException; +import com.tencentcloudapi.sms.v20210111.SmsClient; +import com.tencentcloudapi.sms.v20210111.models.*; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static com.yunxi.scm.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** + * 腾讯云短信功能实现 + *

+ * 参见 https://cloud.tencent.com/document/product/382/52077 + * + * @author shiwp + */ +public class TencentSmsClient extends AbstractSmsClient { + + /** + * 调用成功 code + */ + public static final String API_SUCCESS_CODE = "Ok"; + + /** + * REGION,使用南京 + */ + private static final String ENDPOINT = "ap-nanjing"; + + /** + * 是否国际/港澳台短信: + * 0:表示国内短信。 + * 1:表示国际/港澳台短信。 + */ + private static final long INTERNATIONAL = 0L; + + private SmsClient client; + + public TencentSmsClient(SmsChannelProperties properties) { + super(properties, new TencentSmsCodeMapping()); + Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空"); + } + + @Override + protected void doInit() { + // 实例化一个认证对象,入参需要传入腾讯云账户密钥对 secretId,secretKey + Credential credential = new Credential(properties.getApiKey(), properties.getApiSecret()); + client = new SmsClient(credential, ENDPOINT); + } + + @Override + protected SmsCommonResult doSendSms(Long sendLogId, + String mobile, + String apiTemplateId, + List> templateParams) throws Throwable { + return invoke(() -> buildSendSmsRequest(sendLogId, mobile, apiTemplateId, templateParams), + this::doSendSms0, + response -> { + SendStatus sendStatus = response.getSendStatusSet()[0]; + return SmsCommonResult.build(sendStatus.getCode(), sendStatus.getMessage(), response.getRequestId(), + new SmsSendRespDTO().setSerialNo(sendStatus.getSerialNo()), codeMapping); + }); + } + + + /** + * 腾讯云发放短信的时候,需要额外的参数 sdkAppId。 + * 考虑到不破坏原有的 apiKey + apiSecret 的结构,所以将 secretId 拼接到 apiKey 字段中,格式为 "secretId sdkAppId"。 + * 因此,这边需要使用 TencentSmsChannelProperties 做拆分,重新封装到 properties 内。 + * + * @param properties 数据库中存储的短信渠道配置 + * @return TencentSmsChannelProperties + */ + @Override + protected SmsChannelProperties prepareProperties(SmsChannelProperties properties) { + return TencentSmsChannelProperties.build(properties); + } + + /** + * 调用腾讯云 SDK 发送短信 + * + * @param request 发送短信请求 + * @return 发送短信响应 + * @throws TencentCloudSDKException SDK 用来封装发送短信失败 + */ + private SendSmsResponse doSendSms0(SendSmsRequest request) throws TencentCloudSDKException { + return client.SendSms(request); + } + + /** + * 封装腾讯云发送短信请求 + * + * @param sendLogId 日志编号 + * @param mobile 手机号 + * @param apiTemplateId 短信 API 的模板编号 + * @param templateParams 短信模板参数。通过 List 数组,保证参数的顺序 + * @return 腾讯云发送短信请求 + */ + private SendSmsRequest buildSendSmsRequest(Long sendLogId, + String mobile, + String apiTemplateId, + List> templateParams) { + SendSmsRequest request = new SendSmsRequest(); + request.setSmsSdkAppId(((TencentSmsChannelProperties) properties).getSdkAppId()); + request.setPhoneNumberSet(new String[]{mobile}); + request.setSignName(properties.getSignature()); + request.setTemplateId(apiTemplateId); + request.setTemplateParamSet(ArrayUtils.toArray(templateParams, e -> String.valueOf(e.getValue()))); + request.setSessionContext(JsonUtils.toJsonString(new SessionContext().setLogId(sendLogId))); + return request; + } + + @Override + protected List doParseSmsReceiveStatus(String text) throws Throwable { + List callback = JsonUtils.parseArray(text, SmsReceiveStatus.class); + return CollectionUtils.convertList(callback, status -> { + SmsReceiveRespDTO data = new SmsReceiveRespDTO(); + data.setErrorCode(status.getErrCode()).setErrorMsg(status.getDescription()); + data.setReceiveTime(status.getReceiveTime()).setSuccess(SmsReceiveStatus.SUCCESS_CODE.equalsIgnoreCase(status.getStatus())); + data.setMobile(status.getMobile()).setSerialNo(status.getSerialNo()); + SessionContext context; + Long logId; + Assert.notNull(context = status.getSessionContext(), "回执信息中未解析出 context,请联系腾讯云小助手"); + Assert.notNull(logId = context.getLogId(), "回执信息中未解析出 logId,请联系腾讯云小助手"); + data.setLogId(logId); + return data; + }); + } + + @Override + protected SmsCommonResult doGetSmsTemplate(String apiTemplateId) throws Throwable { + return invoke(() -> this.buildSmsTemplateStatusRequest(apiTemplateId), + this::doGetSmsTemplate0, + response -> { + SmsTemplateRespDTO data = convertTemplateStatusDTO(response.getDescribeTemplateStatusSet()[0]); + return SmsCommonResult.build(API_SUCCESS_CODE, null, response.getRequestId(), data, codeMapping); + }); + } + + @VisibleForTesting + SmsTemplateRespDTO convertTemplateStatusDTO(DescribeTemplateListStatus templateStatus) { + if (templateStatus == null) { + return null; + } + SmsTemplateAuditStatusEnum auditStatus; + Assert.notNull(templateStatus.getStatusCode(), + StrUtil.format("短信模版审核状态为 null,模版 id{}", templateStatus.getTemplateId())); + switch (templateStatus.getStatusCode().intValue()) { + case -1: + auditStatus = SmsTemplateAuditStatusEnum.FAIL; + break; + case 0: + auditStatus = SmsTemplateAuditStatusEnum.SUCCESS; + break; + case 1: + auditStatus = SmsTemplateAuditStatusEnum.CHECKING; + break; + default: + throw new IllegalStateException(StrUtil.format("不能解析短信模版审核状态{},模版 id{}", + templateStatus.getStatusCode(), templateStatus.getTemplateId())); + } + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(String.valueOf(templateStatus.getTemplateId())).setContent(templateStatus.getTemplateContent()); + data.setAuditStatus(auditStatus.getStatus()).setAuditReason(templateStatus.getReviewReply()); + return data; + } + + /** + * 封装查询模版审核状态请求 + * @param apiTemplateId api 的模版 id + * @return 查询模版审核状态请求 + */ + private DescribeSmsTemplateListRequest buildSmsTemplateStatusRequest(String apiTemplateId) { + DescribeSmsTemplateListRequest request = new DescribeSmsTemplateListRequest(); + request.setTemplateIdSet(new Long[]{Long.parseLong(apiTemplateId)}); + // 地区 0:表示国内短信。1:表示国际/港澳台短信。 + request.setInternational(INTERNATIONAL); + return request; + } + + /** + * 调用腾讯云 SDK 查询短信模版状态 + * + * @param request 查询短信模版状态请求 + * @return 查询短信模版状态响应 + * @throws TencentCloudSDKException SDK 用来封装查询短信模版状态失败 + */ + private DescribeSmsTemplateListResponse doGetSmsTemplate0(DescribeSmsTemplateListRequest request) throws TencentCloudSDKException { + return client.DescribeSmsTemplateList(request); + } + + SmsCommonResult invoke(Supplier requestSupplier, + SdkFunction responseSupplier, + Function> resultGen) { + // 构建请求body + Q request = requestSupplier.get(); + P response; + // 调用腾讯云发送短信 + try { + response = responseSupplier.apply(request); + } catch (TencentCloudSDKException e) { + // 调用异常,封装结果 + return SmsCommonResult.build(e.getErrorCode(), e.getMessage(), e.getRequestId(), null, codeMapping); + } + return resultGen.apply(response); + } + + @Data + private static class SmsReceiveStatus { + + /** + * 短信接受成功 code + */ + public static final String SUCCESS_CODE = "SUCCESS"; + + /** + * 用户实际接收到短信的时间 + */ + @JsonProperty("user_receive_time") + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime receiveTime; + + /** + * 国家(或地区)码 + */ + @JsonProperty("nationcode") + private String nationCode; + + /** + * 手机号码 + */ + private String mobile; + + /** + * 实际是否收到短信接收状态,SUCCESS(成功)、FAIL(失败) + */ + @JsonProperty("report_status") + private String status; + + /** + * 用户接收短信状态码错误信息 + */ + @JsonProperty("errmsg") + private String errCode; + + /** + * 用户接收短信状态描述 + */ + @JsonProperty("description") + private String description; + + /** + * 本次发送标识 ID(与发送接口返回的SerialNo对应) + */ + @JsonProperty("sid") + private String serialNo; + + /** + * 用户的 session 内容(与发送接口的请求参数SessionContext一致) + */ + @JsonProperty("ext") + private SessionContext sessionContext; + + } + + @VisibleForTesting + @Data + static class SessionContext { + + /** + * 发送短信记录id + */ + private Long logId; + } + + private interface SdkFunction { + R apply(T t) throws TencentCloudSDKException; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java new file mode 100644 index 0000000..18669ee --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsCodeMapping.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.framework.sms.core.client.impl.tencent; + +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.sms.core.client.SmsCodeMapping; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; + +import static com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants.*; + +/** + * 腾讯云的 SmsCodeMapping 实现类 + * + * 参见 https://cloud.tencent.com/document/api/382/52075#.E5.85.AC.E5.85.B1.E9.94.99.E8.AF.AF.E7.A0.81 + * + * @author : shiwp + */ +public class TencentSmsCodeMapping implements SmsCodeMapping { + + @Override + public ErrorCode apply(String apiCode) { + switch (apiCode) { + case TencentSmsClient.API_SUCCESS_CODE: return GlobalErrorCodeConstants.SUCCESS; + case "FailedOperation.ContainSensitiveWord": return SMS_SEND_CONTENT_INVALID; + case "FailedOperation.JsonParseFail": + case "MissingParameter.EmptyPhoneNumberSet": + case "LimitExceeded.PhoneNumberCountLimit": + case "FailedOperation.FailResolvePacket": return GlobalErrorCodeConstants.BAD_REQUEST; + case "FailedOperation.InsufficientBalanceInSmsPackage": return SMS_ACCOUNT_MONEY_NOT_ENOUGH; + case "FailedOperation.MarketingSendTimeConstraint": return SMS_SEND_MARKET_LIMIT_CONTROL; + case "FailedOperation.PhoneNumberInBlacklist": return SMS_MOBILE_BLACK; + case "FailedOperation.SignatureIncorrectOrUnapproved": return SMS_SIGN_INVALID; + case "FailedOperation.MissingTemplateToModify": + case "FailedOperation.TemplateIncorrectOrUnapproved": return SMS_TEMPLATE_INVALID; + case "InvalidParameterValue.IncorrectPhoneNumber": return SMS_MOBILE_INVALID; + case "InvalidParameterValue.SdkAppIdNotExist": return SMS_APP_ID_INVALID; + case "InvalidParameterValue.TemplateParameterLengthLimit": + case "InvalidParameterValue.TemplateParameterFormatError": return SMS_TEMPLATE_PARAM_ERROR; + case "LimitExceeded.PhoneNumberDailyLimit": return SMS_SEND_DAY_LIMIT_CONTROL; + case "LimitExceeded.PhoneNumberThirtySecondLimit": + case "LimitExceeded.PhoneNumberOneHourLimit": return SMS_SEND_BUSINESS_LIMIT_CONTROL; + case "UnauthorizedOperation.RequestPermissionDeny": + case "FailedOperation.ForbidAddMarketingTemplates": + case "FailedOperation.NotEnterpriseCertification": + case "UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny": return SMS_PERMISSION_DENY; + case "UnauthorizedOperation.RequestIpNotInWhitelist": return SMS_IP_DENY; + case "AuthFailure.SecretIdNotFound": return SMS_ACCOUNT_INVALID; + } + return SmsFrameworkErrorCodeConstants.SMS_UNKNOWN; + } +} \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsChannelEnum.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsChannelEnum.java new file mode 100644 index 0000000..587a939 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsChannelEnum.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.sms.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信渠道枚举 + * + * @author zzf + * @since 2021/1/25 10:56 + */ +@Getter +@AllArgsConstructor +public enum SmsChannelEnum { + + DEBUG_DING_TALK("DEBUG_DING_TALK", "调试(钉钉)"), + ALIYUN("ALIYUN", "阿里云"), + TENCENT("TENCENT", "腾讯云"), +// HUA_WEI("HUA_WEI", "华为云"), + ; + + /** + * 编码 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + public static SmsChannelEnum getByCode(String code) { + return ArrayUtil.firstMatch(o -> o.getCode().equals(code), values()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java new file mode 100644 index 0000000..495a5b2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsFrameworkErrorCodeConstants.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.framework.sms.core.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * 短信框架的错误码枚举 + * + * 短信框架,使用 2-001-000-000 段 + * + * @author 芋道源码 + */ +public interface SmsFrameworkErrorCodeConstants { + + ErrorCode SMS_UNKNOWN = new ErrorCode(2001000000, "未知错误,需要解析"); + + // ========== 权限 / 限流等相关 2001000100 ========== + + ErrorCode SMS_PERMISSION_DENY = new ErrorCode(2001000100, "没有发送短信的权限"); + ErrorCode SMS_IP_DENY = new ErrorCode(2001000100, "IP 不允许发送短信"); + + // 阿里云:将短信发送频率限制在正常的业务限流范围内。默认短信验证码:使用同一签名,对同一个手机号验证码,支持 1 条 / 分钟,5 条 / 小时,累计 10 条 / 天。 + ErrorCode SMS_SEND_BUSINESS_LIMIT_CONTROL = new ErrorCode(2001000102, "指定手机的发送限流"); + // 阿里云:已经达到您在控制台设置的短信日发送量限额值。在国内消息设置 > 安全设置,修改发送总量阈值。 + ErrorCode SMS_SEND_DAY_LIMIT_CONTROL = new ErrorCode(2001000103, "每天的发送限流"); + + ErrorCode SMS_SEND_CONTENT_INVALID = new ErrorCode(2001000104, "短信内容有敏感词"); + + // 腾讯云:为避免骚扰用户,营销短信只允许在8点到22点发送。 + ErrorCode SMS_SEND_MARKET_LIMIT_CONTROL = new ErrorCode(2001000105, "营销短信发送时间限制"); + + // ========== 模板相关 2001000200 ========== + ErrorCode SMS_TEMPLATE_INVALID = new ErrorCode(2001000200, "短信模板不合法"); // 包括短信模板不存在 + ErrorCode SMS_TEMPLATE_PARAM_ERROR = new ErrorCode(2001000201, "模板参数不正确"); + + // ========== 签名相关 2001000300 ========== + ErrorCode SMS_SIGN_INVALID = new ErrorCode(2001000300, "短信签名不可用"); + + // ========== 账户相关 2001000400 ========== + ErrorCode SMS_ACCOUNT_MONEY_NOT_ENOUGH = new ErrorCode(2001000400, "账户余额不足"); + ErrorCode SMS_ACCOUNT_INVALID = new ErrorCode(2001000401, "apiKey 不存在"); + + // ========== 其它相关 2001000900 开头 ========== + ErrorCode SMS_API_PARAM_ERROR = new ErrorCode(2001000900, "请求参数缺失"); + ErrorCode SMS_MOBILE_INVALID = new ErrorCode(2001000901, "手机格式不正确"); + ErrorCode SMS_MOBILE_BLACK = new ErrorCode(2001000902, "手机号在黑名单中"); + ErrorCode SMS_APP_ID_INVALID = new ErrorCode(2001000903, "SdkAppId不合法"); + + ErrorCode EXCEPTION = new ErrorCode(2001000999, "调用异常"); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java new file mode 100644 index 0000000..7173a46 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/enums/SmsTemplateAuditStatusEnum.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.sms.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信模板的审核状态枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum SmsTemplateAuditStatusEnum { + + CHECKING(1), + SUCCESS(2), + FAIL(3); + + private final Integer status; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/property/SmsChannelProperties.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/property/SmsChannelProperties.java new file mode 100644 index 0000000..2dfa5d5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/java/com/yunxi/scm/framework/sms/core/property/SmsChannelProperties.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.framework.sms.core.property; + +import com.yunxi.scm.framework.sms.core.enums.SmsChannelEnum; +import lombok.Data; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信渠道配置类 + * + * @author zzf + * @since 2021/1/25 17:01 + */ +@Data +@Validated +public class SmsChannelProperties { + + /** + * 渠道编号 + */ + @NotNull(message = "短信渠道 ID 不能为空") + private Long id; + /** + * 短信签名 + */ + @NotEmpty(message = "短信签名不能为空") + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + @NotEmpty(message = "渠道编码不能为空") + private String code; + /** + * 短信 API 的账号 + */ + @NotEmpty(message = "短信 API 的账号不能为空") + private String apiKey; + /** + * 短信 API 的密钥 + */ + @NotEmpty(message = "短信 API 的密钥不能为空") + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..f3d7acf --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.sms.config.YunxiSmsAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test-integration/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test-integration/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java new file mode 100644 index 0000000..0d71bd3 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test-integration/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClientIntegrationTest.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.framework.sms.core.client.impl.aliyun; + +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.framework.sms.core.client.impl.aliyun.AliyunSmsClient; +import com.yunxi.scm.framework.sms.core.enums.SmsChannelEnum; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link AliyunSmsClient} 的集成测试 + */ +public class AliyunSmsClientIntegrationTest { + + private static AliyunSmsClient smsClient; + + @BeforeAll + public static void before() { + // 创建配置类 + SmsChannelProperties properties = new SmsChannelProperties(); + properties.setId(1L); + properties.setSignature("Ballcat"); + properties.setCode(SmsChannelEnum.ALIYUN.getCode()); + properties.setApiKey(System.getenv("ALIYUN_ACCESS_KEY")); + properties.setApiSecret(System.getenv("ALIYUN_SECRET_KEY")); + // 创建客户端 + smsClient = new AliyunSmsClient(properties); + smsClient.init(); + } + + @Test + public void testSendSms() { + List> templateParams = new ArrayList<>(); + templateParams.add(new KeyValue<>("code", "1024")); +// templateParams.put("operation", "嘿嘿"); +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams); + SmsCommonResult result = smsClient.sendSms(1L, "15601691399", + "SMS_207945135", templateParams); + System.out.println(result); + } + + @Test + public void testGetSmsTemplate() { + String apiTemplateId = "SMS_2079451351"; + SmsCommonResult result = smsClient.getSmsTemplate(apiTemplateId); + System.out.println(result); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test-integration/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test-integration/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java new file mode 100644 index 0000000..bb64144 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test-integration/java/com/yunxi/scm/framework/sms/core/client/impl/debug/DebugDingTalkSmsClientIntegrationTest.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.framework.sms.core.client.impl.debug; + +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.impl.debug.DebugDingTalkSmsClient; +import com.yunxi.scm.framework.sms.core.enums.SmsChannelEnum; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link DebugDingTalkSmsClient} 的集成测试 + */ +public class DebugDingTalkSmsClientIntegrationTest { + + private static DebugDingTalkSmsClient smsClient; + + @BeforeAll + public static void init() { + // 创建配置类 + SmsChannelProperties properties = new SmsChannelProperties(); + properties.setId(1L); + properties.setSignature("芋道"); + properties.setCode(SmsChannelEnum.DEBUG_DING_TALK.getCode()); + properties.setApiKey("696b5d8ead48071237e4aa5861ff08dbadb2b4ded1c688a7b7c9afc615579859"); + properties.setApiSecret("SEC5c4e5ff888bc8a9923ae47f59e7ccd30af1f14d93c55b4e2c9cb094e35aeed67"); + // 创建客户端 + smsClient = new DebugDingTalkSmsClient(properties); + smsClient.init(); + } + + @Test + public void testSendSms() { + List> templateParams = new ArrayList<>(); + templateParams.add(new KeyValue<>("code", "1024")); + templateParams.add(new KeyValue<>("operation", "嘿嘿")); +// SmsResult result = smsClient.send(1L, "15601691399", "4372216", templateParams); + SmsCommonResult result = smsClient.sendSms(1L, "15601691399", "4383920", templateParams); + System.out.println(result); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java new file mode 100644 index 0000000..31393f1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsClientTest.java @@ -0,0 +1,225 @@ +package com.yunxi.scm.framework.sms.core.client.impl.aliyun; + +import cn.hutool.core.util.ReflectUtil; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import com.aliyuncs.AcsRequest; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateRequest; +import com.aliyuncs.dysmsapi.model.v20170525.QuerySmsTemplateResponse; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; +import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; +import com.aliyuncs.exceptions.ClientException; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentMatcher; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Function; + +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link AliyunSmsClient} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private final AliyunSmsClient smsClient = new AliyunSmsClient(properties); + + @Mock + private IAcsClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "acsClient")); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() throws ClientException { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("code", 1234), new KeyValue<>("op", "login")); + // mock 方法 + SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> o.setCode("OK")); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(mobile, acsRequest.getPhoneNumbers()); + assertEquals(properties.getSignature(), acsRequest.getSignName()); + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + assertEquals(toJsonString(MapUtils.convertMap(templateParams)), acsRequest.getTemplateParam()); + assertEquals(sendLogId.toString(), acsRequest.getOutId()); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getBizId(), result.getData().getSerialNo()); + } + + @Test + public void testDoTParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[\n" + + " {\n" + + " \"phone_number\" : \"13900000001\",\n" + + " \"send_time\" : \"2017-01-01 11:12:13\",\n" + + " \"report_time\" : \"2017-02-02 22:23:24\",\n" + + " \"success\" : true,\n" + + " \"err_code\" : \"DELIVERED\",\n" + + " \"err_msg\" : \"用户接收成功\",\n" + + " \"sms_size\" : \"1\",\n" + + " \"biz_id\" : \"12345\",\n" + + " \"out_id\" : \"67890\"\n" + + " }\n" + + "]"; + // mock 方法 + + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("DELIVERED", statuses.get(0).getErrorCode()); + assertEquals("用户接收成功", statuses.get(0).getErrorMsg()); + assertEquals("13900000001", statuses.get(0).getMobile()); + assertEquals(LocalDateTime.of(2017, 2, 2, 22, 23, 24), statuses.get(0).getReceiveTime()); + assertEquals("12345", statuses.get(0).getSerialNo()); + assertEquals(67890L, statuses.get(0).getLogId()); + } + + @Test + public void testDoGetSmsTemplate() throws ClientException { + // 准备参数 + String apiTemplateId = randomString(); + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(argThat((ArgumentMatcher) acsRequest -> { + assertEquals(apiTemplateId, acsRequest.getTemplateCode()); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getTemplateCode(), result.getData().getId()); + assertEquals(response.getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getReason(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSmsTemplateAuditStatus() { + assertEquals(SmsTemplateAuditStatusEnum.CHECKING.getStatus(), + smsClient.convertSmsTemplateAuditStatus(0)); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), + smsClient.convertSmsTemplateAuditStatus(1)); + assertEquals(SmsTemplateAuditStatusEnum.FAIL.getStatus(), + smsClient.convertSmsTemplateAuditStatus(2)); + assertThrows(IllegalArgumentException.class, () -> smsClient.convertSmsTemplateAuditStatus(3), + "未知审核状态(3)"); + } + + @Test + @SuppressWarnings("unchecked") + public void testInvoke_throwable() throws ClientException { + // 准备参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + // mock 方法 + ClientException ex = new ClientException("isv.INVALID_PARAMETERS", "参数不正确", randomString()); + when(client.getAcsResponse(any(AcsRequest.class))).thenThrow(ex); + + // 调用,并断言异常 + SmsCommonResult result = smsClient.invoke(request, null); + // 断言 + assertEquals(ex.getErrCode(), result.getApiCode()); + assertEquals(ex.getErrMsg(), result.getApiMsg()); + Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getCode(), result.getCode()); + Assertions.assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR.getMsg(), result.getMsg()); + assertEquals(ex.getRequestId(), result.getApiRequestId()); + } + + @Test + public void testInvoke_success() throws ClientException { + // 准备参数 + QuerySmsTemplateRequest request = new QuerySmsTemplateRequest(); + Function responseConsumer = response -> { + SmsTemplateRespDTO data = new SmsTemplateRespDTO(); + data.setId(response.getTemplateCode()).setContent(response.getTemplateContent()); + data.setAuditStatus(SmsTemplateAuditStatusEnum.SUCCESS.getStatus()).setAuditReason(response.getReason()); + return data; + }; + // mock 方法 + QuerySmsTemplateResponse response = randomPojo(QuerySmsTemplateResponse.class, o -> { + o.setCode("OK"); + o.setTemplateStatus(1); // 设置模板通过 + }); + when(client.getAcsResponse(any(AcsRequest.class))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.invoke(request, responseConsumer); + // 断言 + assertEquals(response.getCode(), result.getApiCode()); + assertEquals(response.getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getTemplateCode(), result.getData().getId()); + assertEquals(response.getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getReason(), result.getData().getAuditReason()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java new file mode 100644 index 0000000..e21a522 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/aliyun/AliyunSmsCodeMappingTest.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.sms.core.client.impl.aliyun; + +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link AliyunSmsCodeMapping} 的单元测试 + * + * @author 芋道源码 + */ +public class AliyunSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private AliyunSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply("OK")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("MissingAccessKeyId")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_NOT_EXISTS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("isv.ACCOUNT_ABNORMAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("isv.DAY_LIMIT_CONTROL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("isv.SMS_CONTENT_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGN_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SIGN_NAME_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("isp.RAM_PERMISSION_DENY")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.OUT_OF_SERVICE")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("isv.AMOUNT_NOT_ENOUGH")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("isv.SMS_TEMPLATE_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("isv.SMS_SIGNATURE_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_PARAMETERS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_API_PARAM_ERROR, codeMapping.apply("isv.INVALID_JSON_PARAM")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("isv.MOBILE_NUMBER_ILLEGAL")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("isv.TEMPLATE_MISSING_PARAMETERS")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("isv.BUSINESS_LIMIT_CONTROL")); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java new file mode 100644 index 0000000..0ac99dd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsClientTest.java @@ -0,0 +1,222 @@ +package com.yunxi.scm.framework.sms.core.client.impl.tencent; + +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.framework.sms.core.enums.SmsTemplateAuditStatusEnum; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.google.common.collect.Lists; +import com.tencentcloudapi.sms.v20210111.SmsClient; +import com.tencentcloudapi.sms.v20210111.models.DescribeSmsTemplateListResponse; +import com.tencentcloudapi.sms.v20210111.models.DescribeTemplateListStatus; +import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; +import com.tencentcloudapi.sms.v20210111.models.SendStatus; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.when; + +/** + * {@link TencentSmsClient} 的单元测试 + * + * @author shiwp + */ +public class TencentSmsClientTest extends BaseMockitoUnitTest { + + private final SmsChannelProperties properties = new SmsChannelProperties() + .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + + @InjectMocks + private TencentSmsClient smsClient = new TencentSmsClient(properties); + + @Mock + private SmsClient client; + + @Test + public void testDoInit() { + // 准备参数 + // mock 方法 + + // 调用 + smsClient.doInit(); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); + } + + @Test + public void testRefresh() { + // 准备参数 + SmsChannelProperties p = new SmsChannelProperties() + .setApiKey(randomString() + " " + randomString()) // 随机一个 apiKey,避免构建报错 + .setApiSecret(randomString()) // 随机一个 apiSecret,避免构建报错 + .setSignature("芋道源码"); + // 调用 + smsClient.refresh(p); + // 断言 + assertNotSame(client, ReflectUtil.getFieldValue(smsClient, "client")); + } + + @Test + public void testDoSendSms() throws Throwable { + // 准备参数 + Long sendLogId = randomLongId(); + String mobile = randomString(); + String apiTemplateId = randomString(); + List> templateParams = Lists.newArrayList( + new KeyValue<>("1", 1234), new KeyValue<>("2", "login")); + String requestId = randomString(); + String serialNo = randomString(); + // mock 方法 + SendSmsResponse response = randomPojo(SendSmsResponse.class, o -> { + o.setRequestId(requestId); + SendStatus[] sendStatuses = new SendStatus[1]; + o.setSendStatusSet(sendStatuses); + SendStatus sendStatus = new SendStatus(); + sendStatuses[0] = sendStatus; + sendStatus.setCode(TencentSmsClient.API_SUCCESS_CODE); + sendStatus.setMessage("send success"); + sendStatus.setSerialNo(serialNo); + }); + when(client.SendSms(argThat(request -> { + assertEquals(mobile, request.getPhoneNumberSet()[0]); + assertEquals(properties.getSignature(), request.getSignName()); + assertEquals(apiTemplateId, request.getTemplateId()); + assertEquals(toJsonString(ArrayUtils.toArray(new ArrayList<>(MapUtils.convertMap(templateParams).values()), String::valueOf)), + toJsonString(request.getTemplateParamSet())); + assertEquals(sendLogId, ReflectUtil.getFieldValue(JsonUtils.parseObject(request.getSessionContext(), TencentSmsClient.SessionContext.class), "logId")); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doSendSms(sendLogId, mobile, + apiTemplateId, templateParams); + // 断言 + assertEquals(response.getSendStatusSet()[0].getCode(), result.getApiCode()); + assertEquals(response.getSendStatusSet()[0].getMessage(), result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getSendStatusSet()[0].getSerialNo(), result.getData().getSerialNo()); + } + + @Test + public void testDoTParseSmsReceiveStatus() throws Throwable { + // 准备参数 + String text = "[\n" + + " {\n" + + " \"user_receive_time\": \"2015-10-17 08:03:04\",\n" + + " \"nationcode\": \"86\",\n" + + " \"mobile\": \"13900000001\",\n" + + " \"report_status\": \"SUCCESS\",\n" + + " \"errmsg\": \"DELIVRD\",\n" + + " \"description\": \"用户短信送达成功\",\n" + + " \"sid\": \"12345\",\n" + + " \"ext\": {\"logId\":\"67890\"}\n" + + " }\n" + + "]"; + // mock 方法 + + // 调用 + List statuses = smsClient.doParseSmsReceiveStatus(text); + // 断言 + assertEquals(1, statuses.size()); + assertTrue(statuses.get(0).getSuccess()); + assertEquals("DELIVRD", statuses.get(0).getErrorCode()); + assertEquals("用户短信送达成功", statuses.get(0).getErrorMsg()); + assertEquals("13900000001", statuses.get(0).getMobile()); + assertEquals(LocalDateTime.of(2015, 10, 17, 8, 3, 4), statuses.get(0).getReceiveTime()); + assertEquals("12345", statuses.get(0).getSerialNo()); + assertEquals(67890L, statuses.get(0).getLogId()); + } + + @Test + public void testDoGetSmsTemplate() throws Throwable { + // 准备参数 + Long apiTemplateId = randomLongId(); + String requestId = randomString(); + + // mock 方法 + DescribeSmsTemplateListResponse response = randomPojo(DescribeSmsTemplateListResponse.class, o -> { + DescribeTemplateListStatus[] describeTemplateListStatuses = new DescribeTemplateListStatus[1]; + DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); + templateStatus.setTemplateId(apiTemplateId); + templateStatus.setStatusCode(0L);// 设置模板通过 + describeTemplateListStatuses[0] = templateStatus; + o.setDescribeTemplateStatusSet(describeTemplateListStatuses); + o.setRequestId(requestId); + }); + when(client.DescribeSmsTemplateList(argThat(request -> { + assertEquals(apiTemplateId, request.getTemplateIdSet()[0]); + return true; + }))).thenReturn(response); + + // 调用 + SmsCommonResult result = smsClient.doGetSmsTemplate(apiTemplateId.toString()); + // 断言 + assertEquals(TencentSmsClient.API_SUCCESS_CODE, result.getApiCode()); + assertNull(result.getApiMsg()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getCode(), result.getCode()); + assertEquals(GlobalErrorCodeConstants.SUCCESS.getMsg(), result.getMsg()); + assertEquals(response.getRequestId(), result.getApiRequestId()); + // 断言结果 + assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateId().toString(), result.getData().getId()); + assertEquals(response.getDescribeTemplateStatusSet()[0].getTemplateContent(), result.getData().getContent()); + assertEquals(SmsTemplateAuditStatusEnum.SUCCESS.getStatus(), result.getData().getAuditStatus()); + assertEquals(response.getDescribeTemplateStatusSet()[0].getReviewReply(), result.getData().getAuditReason()); + } + + @Test + public void testConvertSuccessTemplateStatus() { + testTemplateStatus(SmsTemplateAuditStatusEnum.SUCCESS, 0L); + } + + @Test + public void testConvertCheckingTemplateStatus() { + testTemplateStatus(SmsTemplateAuditStatusEnum.CHECKING, 1L); + } + + @Test + public void testConvertFailTemplateStatus() { + testTemplateStatus(SmsTemplateAuditStatusEnum.FAIL, -1L); + } + + @Test + public void testConvertUnknownTemplateStatus() { + DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); + templateStatus.setStatusCode(3L); + Long templateId = randomLongId(); + // 调用,并断言结果 + assertThrows(IllegalStateException.class, () -> smsClient.convertTemplateStatusDTO(templateStatus), + StrUtil.format("不能解析短信模版审核状态[3],模版id[{}]", templateId)); + } + + private void testTemplateStatus(SmsTemplateAuditStatusEnum expected, Long value) { + DescribeTemplateListStatus templateStatus = new DescribeTemplateListStatus(); + templateStatus.setStatusCode(value); + SmsTemplateRespDTO result = smsClient.convertTemplateStatusDTO(templateStatus); + assertEquals(expected.getStatus(), result.getAuditStatus()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java new file mode 100644 index 0000000..62ec4d8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-sms/src/test/java/com/yunxi/scm/framework/sms/core/client/impl/tencent/TencentSmsCodeMappingTest.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.framework.sms.core.client.impl.tencent; + +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * {@link TencentSmsCodeMapping} 的单元测试 + * + * @author : shiwp + */ +public class TencentSmsCodeMappingTest extends BaseMockitoUnitTest { + + @InjectMocks + private TencentSmsCodeMapping codeMapping; + + @Test + public void testApply() { + assertEquals(GlobalErrorCodeConstants.SUCCESS, codeMapping.apply(TencentSmsClient.API_SUCCESS_CODE)); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_CONTENT_INVALID, codeMapping.apply("FailedOperation.ContainSensitiveWord")); + assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.JsonParseFail")); + assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("MissingParameter.EmptyPhoneNumberSet")); + assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("LimitExceeded.PhoneNumberCountLimit")); + assertEquals(GlobalErrorCodeConstants.BAD_REQUEST, codeMapping.apply("FailedOperation.FailResolvePacket")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_MONEY_NOT_ENOUGH, codeMapping.apply("FailedOperation.InsufficientBalanceInSmsPackage")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_MARKET_LIMIT_CONTROL, codeMapping.apply("FailedOperation.MarketingSendTimeConstraint")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_BLACK, codeMapping.apply("FailedOperation.PhoneNumberInBlacklist")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SIGN_INVALID, codeMapping.apply("FailedOperation.SignatureIncorrectOrUnapproved")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.MissingTemplateToModify")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_INVALID, codeMapping.apply("FailedOperation.TemplateIncorrectOrUnapproved")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_MOBILE_INVALID, codeMapping.apply("InvalidParameterValue.IncorrectPhoneNumber")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_APP_ID_INVALID, codeMapping.apply("InvalidParameterValue.SdkAppIdNotExist")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterLengthLimit")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_TEMPLATE_PARAM_ERROR, codeMapping.apply("InvalidParameterValue.TemplateParameterFormatError")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_DAY_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberDailyLimit")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberThirtySecondLimit")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_SEND_BUSINESS_LIMIT_CONTROL, codeMapping.apply("LimitExceeded.PhoneNumberOneHourLimit")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.RequestPermissionDeny")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.ForbidAddMarketingTemplates")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("FailedOperation.NotEnterpriseCertification")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_PERMISSION_DENY, codeMapping.apply("UnauthorizedOperation.IndividualUserMarketingSmsPermissionDeny")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_IP_DENY, codeMapping.apply("UnauthorizedOperation.RequestIpNotInWhitelist")); + assertEquals(SmsFrameworkErrorCodeConstants.SMS_ACCOUNT_INVALID, codeMapping.apply("AuthFailure.SecretIdNotFound")); + } + +} \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-social/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-social/pom.xml new file mode 100644 index 0000000..58bde1c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-social/pom.xml @@ -0,0 +1,56 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + jar + 4.0.0 + + yunxi-spring-boot-starter-biz-social + ${project.artifactId} + + + + com.yunxi.scm + yunxi-common + + + + org.springframework.boot + spring-boot-starter-aop + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + com.xingyuv + spring-boot-starter-justauth + + + cn.hutool + hutool-core + + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + + \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/config/YunxiSocialAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/config/YunxiSocialAutoConfiguration.java new file mode 100644 index 0000000..ef5566f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/config/YunxiSocialAutoConfiguration.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.social.config; + +import com.yunxi.scm.framework.social.core.YunxiAuthRequestFactory; +import com.xingyuv.http.HttpUtil; +import com.xingyuv.http.support.hutool.HutoolImpl; +import com.xingyuv.jushauth.cache.AuthStateCache; +import com.xingyuv.justauth.autoconfigure.JustAuthProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; + +/** + * 社交自动装配类 + * + * @author timfruit + * @date 2021-10-30 + */ +@Slf4j +@AutoConfiguration +@EnableConfigurationProperties(JustAuthProperties.class) +public class YunxiSocialAutoConfiguration { + + @Bean + @Primary + @ConditionalOnProperty(prefix = "justauth", value = "enabled", havingValue = "true", matchIfMissing = true) + public YunxiAuthRequestFactory yunxiAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) { + // 需要修改 HttpUtil 使用的实现,避免类报错 + HttpUtil.setHttp(new HutoolImpl()); + // 创建 YunxiAuthRequestFactory + return new YunxiAuthRequestFactory(properties, authStateCache); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/YunxiAuthRequestFactory.java b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/YunxiAuthRequestFactory.java new file mode 100644 index 0000000..24ca233 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/YunxiAuthRequestFactory.java @@ -0,0 +1,85 @@ +package com.yunxi.scm.framework.social.core; + +import cn.hutool.core.util.EnumUtil; +import cn.hutool.core.util.ReflectUtil; +import com.yunxi.scm.framework.social.core.enums.AuthExtendSource; +import com.yunxi.scm.framework.social.core.request.AuthWeChatMiniAppRequest; +import com.xingyuv.jushauth.cache.AuthStateCache; +import com.xingyuv.jushauth.config.AuthConfig; +import com.xingyuv.jushauth.config.AuthSource; +import com.xingyuv.jushauth.request.AuthRequest; +import com.xingyuv.justauth.AuthRequestFactory; +import com.xingyuv.justauth.autoconfigure.JustAuthProperties; + +import java.lang.reflect.Method; + +/** + * 第三方授权拓展 request 工厂类 + * 为使得拓展配置 {@link AuthConfig} 和默认配置齐平,所以自定义本工厂类 + * + * @author timfruit + * @date 2021-10-31 + */ +public class YunxiAuthRequestFactory extends AuthRequestFactory { + + protected JustAuthProperties properties; + protected AuthStateCache authStateCache; + + /** + * 由于父类 configureHttpConfig 方法是 private 修饰,所以获取后,进行反射调用 + */ + private final Method configureHttpConfigMethod = ReflectUtil.getMethod(AuthRequestFactory.class, + "configureHttpConfig", String.class, AuthConfig.class, JustAuthProperties.JustAuthHttpConfig.class); + + public YunxiAuthRequestFactory(JustAuthProperties properties, AuthStateCache authStateCache) { + super(properties, authStateCache); + this.properties = properties; + this.authStateCache = authStateCache; + } + + /** + * 返回 AuthRequest 对象 + * + * @param source {@link AuthSource} + * @return {@link AuthRequest} + */ + @Override + public AuthRequest get(String source) { + // 先尝试获取自定义扩展的 + AuthRequest authRequest = getExtendRequest(source); + // 找不到,使用默认拓展 + if (authRequest == null) { + authRequest = super.get(source); + } + return authRequest; + } + + protected AuthRequest getExtendRequest(String source) { + AuthExtendSource authExtendSource; + try { + authExtendSource = EnumUtil.fromString(AuthExtendSource.class, source.toUpperCase()); + } catch (IllegalArgumentException e) { + // 无自定义匹配 + return null; + } + + // 拓展配置和默认配置齐平,properties 放在一起 + AuthConfig config = properties.getType().get(authExtendSource.name()); + // 找不到对应关系,直接返回空 + if (config == null) { + return null; + } + // 反射调用,配置 http config + ReflectUtil.invoke(this, configureHttpConfigMethod, authExtendSource.name(), config, properties.getHttpConfig()); + + // 获得拓展的 Request + // noinspection SwitchStatementWithTooFewBranches + switch (authExtendSource) { + case WECHAT_MINI_APP: + return new AuthWeChatMiniAppRequest(config, authStateCache); + default: + return null; + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/enums/AuthExtendSource.java b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/enums/AuthExtendSource.java new file mode 100644 index 0000000..19c4849 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/enums/AuthExtendSource.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.framework.social.core.enums; + +import com.xingyuv.jushauth.config.AuthSource; +import com.xingyuv.jushauth.request.AuthDefaultRequest; + +/** + * 拓展 JustAuth 各 api 需要的 url, 用枚举类分平台类型管理 + * + * 默认配置 {@link com.xingyuv.jushauth.config.AuthDefaultSource} + * + * @author timfruit + */ +public enum AuthExtendSource implements AuthSource { + + /** + * 微信小程序授权登录 + */ + WECHAT_MINI_APP { + + @Override + public String authorize() { + // 参见 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html 文档 + throw new UnsupportedOperationException("不支持获取授权 url,请使用小程序内置函数 wx.login() 登录获取 code"); + } + + @Override + public String accessToken() { + // 参见 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 文档 + // 获取 openid, unionId , session_key 等字段 + return "https://api.weixin.qq.com/sns/jscode2session"; + } + + @Override + public String userInfo() { + // 参见 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 文档 + throw new UnsupportedOperationException("不支持获取用户信息 url,请使用小程序内置函数 wx.getUserProfile() 获取用户信息"); + } + + @Override + public Class getTargetClass() { + return null; + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/request/AuthWeChatMiniAppRequest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/request/AuthWeChatMiniAppRequest.java new file mode 100644 index 0000000..c65e699 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/java/com/yunxi/scm/framework/social/core/request/AuthWeChatMiniAppRequest.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.framework.social.core.request; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.social.core.enums.AuthExtendSource; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.xingyuv.jushauth.cache.AuthStateCache; +import com.xingyuv.jushauth.config.AuthConfig; +import com.xingyuv.jushauth.exception.AuthException; +import com.xingyuv.jushauth.model.AuthCallback; +import com.xingyuv.jushauth.model.AuthToken; +import com.xingyuv.jushauth.model.AuthUser; +import com.xingyuv.jushauth.request.AuthDefaultRequest; +import com.xingyuv.jushauth.utils.HttpUtils; +import com.xingyuv.jushauth.utils.UrlBuilder; +import lombok.Data; + +/** + * 微信小程序登陆 Request 请求 + * + * 由于 JustAuth 定位是面向 Web 为主的三方登录,所以微信小程序只能自己封装 + * + * @author timfruit + * @date 2021-10-29 + */ +public class AuthWeChatMiniAppRequest extends AuthDefaultRequest { + + public AuthWeChatMiniAppRequest(AuthConfig config, AuthStateCache authStateCache) { + super(config, AuthExtendSource.WECHAT_MINI_APP, authStateCache); + } + + @Override + protected AuthToken getAccessToken(AuthCallback authCallback) { + // 参见 https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 文档 + // 使用 code 获取对应的 openId、unionId 等字段 + String response = new HttpUtils(config.getHttpConfig()).get(accessTokenUrl(authCallback.getCode())).getBody(); + JSCode2SessionResponse accessTokenObject = JsonUtils.parseObject(response, JSCode2SessionResponse.class); + assert accessTokenObject != null; + checkResponse(accessTokenObject); + // 拼装结果 + return AuthToken.builder() + .openId(accessTokenObject.getOpenid()) + .unionId(accessTokenObject.getUnionId()) + .build(); + } + + @Override + protected AuthUser getUserInfo(AuthToken authToken) { + // 参见 https://developers.weixin.qq.com/miniprogram/dev/api/open-api/user-info/wx.getUserProfile.html 文档 + // 如果需要用户信息,需要在小程序调用函数后传给后端 + return AuthUser.builder() + .username("") + .nickname("") + .avatar("") + .uuid(authToken.getOpenId()) + .token(authToken) + .source(source.toString()) + .build(); + } + + /** + * 检查响应内容是否正确 + * + * @param response 请求响应内容 + */ + private void checkResponse(JSCode2SessionResponse response) { + if (response.getErrorCode() != 0) { + throw new AuthException(response.getErrorCode(), response.getErrorMsg()); + } + } + + @Override + protected String accessTokenUrl(String code) { + return UrlBuilder.fromBaseUrl(source.accessToken()) + .queryParam("appid", config.getClientId()) + .queryParam("secret", config.getClientSecret()) + .queryParam("js_code", code) + .queryParam("grant_type", "authorization_code") + .build(); + } + + @Data + @SuppressWarnings("SpellCheckingInspection") + private static class JSCode2SessionResponse { + + @JsonProperty("errcode") + private int errorCode; + @JsonProperty("errmsg") + private String errorMsg; + @JsonProperty("session_key") + private String sessionKey; + private String openid; + @JsonProperty("unionid") + private String unionId; + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..c617d6a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-social/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.social.config.YunxiSocialAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/pom.xml new file mode 100644 index 0000000..49d6f2b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/pom.xml @@ -0,0 +1,67 @@ + + + + yunxi-framework + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-biz-tenant + jar + + ${project.artifactId} + 多租户 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-job + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mq + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + + com.google.guava + guava + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/config/TenantProperties.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/config/TenantProperties.java new file mode 100644 index 0000000..8df9302 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/config/TenantProperties.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.framework.tenant.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Collections; +import java.util.Set; + +/** + * 多租户配置 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yunxi.tenant") +@Data +public class TenantProperties { + + /** + * 租户是否开启 + */ + private static final Boolean ENABLE_DEFAULT = true; + + /** + * 是否开启 + */ + private Boolean enable = ENABLE_DEFAULT; + + /** + * 需要忽略多租户的请求 + * + * 默认情况下,每个请求需要带上 tenant-id 的请求头。但是,部分请求是无需带上的,例如说短信回调、支付回调等 Open API! + */ + private Set ignoreUrls = Collections.emptySet(); + + /** + * 需要忽略多租户的表 + * + * 即默认所有表都开启多租户的功能,所以记得添加对应的 tenant_id 字段哟 + */ + private Set ignoreTables = Collections.emptySet(); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/config/YunxiTenantAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/config/YunxiTenantAutoConfiguration.java new file mode 100644 index 0000000..54d766a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/config/YunxiTenantAutoConfiguration.java @@ -0,0 +1,135 @@ +package com.yunxi.scm.framework.tenant.config; + +import cn.hutool.core.annotation.AnnotationUtil; +import com.yunxi.scm.framework.common.enums.WebFilterOrderEnum; +import com.yunxi.scm.framework.mybatis.core.util.MyBatisUtils; +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.framework.tenant.core.aop.TenantIgnoreAspect; +import com.yunxi.scm.framework.tenant.core.db.TenantDatabaseInterceptor; +import com.yunxi.scm.framework.tenant.core.job.TenantJob; +import com.yunxi.scm.framework.tenant.core.job.TenantJobHandlerDecorator; +import com.yunxi.scm.framework.tenant.core.mq.TenantRedisMessageInterceptor; +import com.yunxi.scm.framework.tenant.core.redis.TenantRedisCacheManager; +import com.yunxi.scm.framework.tenant.core.security.TenantSecurityWebFilter; +import com.yunxi.scm.framework.tenant.core.service.TenantFrameworkService; +import com.yunxi.scm.framework.tenant.core.service.TenantFrameworkServiceImpl; +import com.yunxi.scm.framework.tenant.core.web.TenantContextWebFilter; +import com.yunxi.scm.framework.web.config.WebProperties; +import com.yunxi.scm.framework.web.core.handler.GlobalExceptionHandler; +import com.yunxi.scm.module.system.api.tenant.TenantApi; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.Objects; + +@AutoConfiguration +@ConditionalOnProperty(prefix = "yunxi.tenant", value = "enable", matchIfMissing = true) // 允许使用 yunxi.tenant.enable=false 禁用多租户 +@EnableConfigurationProperties(TenantProperties.class) +public class YunxiTenantAutoConfiguration { + + @Bean + public TenantFrameworkService tenantFrameworkService(TenantApi tenantApi) { + return new TenantFrameworkServiceImpl(tenantApi); + } + + // ========== AOP ========== + + @Bean + public TenantIgnoreAspect tenantIgnoreAspect() { + return new TenantIgnoreAspect(); + } + + // ========== DB ========== + + @Bean + public TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties, + MybatisPlusInterceptor interceptor) { + TenantLineInnerInterceptor inner = new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties)); + // 添加到 interceptor 中 + // 需要加在首个,主要是为了在分页插件前面。这个是 MyBatis Plus 的规定 + MyBatisUtils.addInterceptor(interceptor, inner, 0); + return inner; + } + + // ========== WEB ========== + + @Bean + public FilterRegistrationBean tenantContextWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantContextWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER); + return registrationBean; + } + + // ========== Security ========== + + @Bean + public FilterRegistrationBean tenantSecurityWebFilter(TenantProperties tenantProperties, + WebProperties webProperties, + GlobalExceptionHandler globalExceptionHandler, + TenantFrameworkService tenantFrameworkService) { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TenantSecurityWebFilter(tenantProperties, webProperties, + globalExceptionHandler, tenantFrameworkService)); + registrationBean.setOrder(WebFilterOrderEnum.TENANT_SECURITY_FILTER); + return registrationBean; + } + + // ========== MQ ========== + + @Bean + public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { + return new TenantRedisMessageInterceptor(); + } + + // ========== Job ========== + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) { + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof JobHandler)) { + return bean; + } + // 有 TenantJob 注解的情况下,才会进行处理 + if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) { + return bean; + } + + // 使用 TenantJobHandlerDecorator 装饰 + return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean); + } + + }; + } + + // ========== Redis ========== + + @Bean + @Primary // 引入租户时,tenantRedisCacheManager 为主 Bean + public RedisCacheManager tenantRedisCacheManager(RedisTemplate redisTemplate, + RedisCacheConfiguration redisCacheConfiguration) { + // 创建 RedisCacheWriter 对象 + RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); + // 创建 TenantRedisCacheManager 对象 + return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/aop/TenantIgnore.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/aop/TenantIgnore.java new file mode 100644 index 0000000..d5ff668 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/aop/TenantIgnore.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.framework.tenant.core.aop; + +import java.lang.annotation.*; + +/** + * 忽略租户,标记指定方法不进行租户的自动过滤 + * + * 注意,只有 DB 的场景会过滤,其它场景暂时不过滤: + * 1、Redis 场景:因为是基于 Key 实现多租户的能力,所以忽略没有意义,不像 DB 是一个 column 实现的 + * 2、MQ 场景:有点难以抉择,目前可以通过 Consumer 手动在消费的方法上,添加 @TenantIgnore 进行忽略 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface TenantIgnore { +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/aop/TenantIgnoreAspect.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/aop/TenantIgnoreAspect.java new file mode 100644 index 0000000..127c826 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/aop/TenantIgnoreAspect.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.framework.tenant.core.aop; + +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.tenant.core.util.TenantUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +/** + * 忽略多租户的 Aspect,基于 {@link TenantIgnore} 注解实现,用于一些全局的逻辑。 + * 例如说,一个定时任务,读取所有数据,进行处理。 + * 又例如说,读取所有数据,进行缓存。 + * + * 整体逻辑的实现,和 {@link TenantUtils#executeIgnore(Runnable)} 需要保持一致 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class TenantIgnoreAspect { + + @Around("@annotation(tenantIgnore)") + public Object around(ProceedingJoinPoint joinPoint, TenantIgnore tenantIgnore) throws Throwable { + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setIgnore(true); + // 执行逻辑 + return joinPoint.proceed(); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/context/TenantContextHolder.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/context/TenantContextHolder.java new file mode 100644 index 0000000..aa2417d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/context/TenantContextHolder.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.framework.tenant.core.context; + +import com.yunxi.scm.framework.common.enums.DocumentEnum; +import com.alibaba.ttl.TransmittableThreadLocal; + +/** + * 多租户上下文 Holder + * + * @author 芋道源码 + */ +public class TenantContextHolder { + + /** + * 当前租户编号 + */ + private static final ThreadLocal TENANT_ID = new TransmittableThreadLocal<>(); + + /** + * 是否忽略租户 + */ + private static final ThreadLocal IGNORE = new TransmittableThreadLocal<>(); + + /** + * 获得租户编号。 + * + * @return 租户编号 + */ + public static Long getTenantId() { + return TENANT_ID.get(); + } + + /** + * 获得租户编号。如果不存在,则抛出 NullPointerException 异常 + * + * @return 租户编号 + */ + public static Long getRequiredTenantId() { + Long tenantId = getTenantId(); + if (tenantId == null) { + throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:" + + DocumentEnum.TENANT.getUrl()); + } + return tenantId; + } + + public static void setTenantId(Long tenantId) { + TENANT_ID.set(tenantId); + } + + public static void setIgnore(Boolean ignore) { + IGNORE.set(ignore); + } + + /** + * 当前是否忽略租户 + * + * @return 是否忽略 + */ + public static boolean isIgnore() { + return Boolean.TRUE.equals(IGNORE.get()); + } + + public static void clear() { + TENANT_ID.remove(); + IGNORE.remove(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/db/TenantBaseDO.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/db/TenantBaseDO.java new file mode 100644 index 0000000..ea6622e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/db/TenantBaseDO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.tenant.core.db; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 拓展多租户的 BaseDO 基类 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public abstract class TenantBaseDO extends BaseDO { + + /** + * 多租户编号 + */ + private Long tenantId; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/db/TenantDatabaseInterceptor.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/db/TenantDatabaseInterceptor.java new file mode 100644 index 0000000..c4ecdca --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/db/TenantDatabaseInterceptor.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.tenant.core.db; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.tenant.config.TenantProperties; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.LongValue; + +import java.util.HashSet; +import java.util.Set; + +/** + * 基于 MyBatis Plus 多租户的功能,实现 DB 层面的多租户的功能 + * + * @author 芋道源码 + */ +public class TenantDatabaseInterceptor implements TenantLineHandler { + + private final Set ignoreTables = new HashSet<>(); + + public TenantDatabaseInterceptor(TenantProperties properties) { + // 不同 DB 下,大小写的习惯不同,所以需要都添加进去 + properties.getIgnoreTables().forEach(table -> { + ignoreTables.add(table.toLowerCase()); + ignoreTables.add(table.toUpperCase()); + }); + // 在 OracleKeyGenerator 中,生成主键时,会查询这个表,查询这个表后,会自动拼接 TENANT_ID 导致报错 + ignoreTables.add("DUAL"); + } + + @Override + public Expression getTenantId() { + return new LongValue(TenantContextHolder.getRequiredTenantId()); + } + + @Override + public boolean ignoreTable(String tableName) { + return TenantContextHolder.isIgnore() // 情况一,全局忽略多租户 + || CollUtil.contains(ignoreTables, tableName); // 情况二,忽略多租户的表 + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/job/TenantJob.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/job/TenantJob.java new file mode 100644 index 0000000..1d11b73 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/job/TenantJob.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.framework.tenant.core.job; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 多租户 Job 注解 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface TenantJob { +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/job/TenantJobHandlerDecorator.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/job/TenantJobHandlerDecorator.java new file mode 100644 index 0000000..3b43ea5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/job/TenantJobHandlerDecorator.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.framework.tenant.core.job; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.tenant.core.service.TenantFrameworkService; +import lombok.AllArgsConstructor; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 多租户 JobHandler 装饰器 + * 任务执行时,会按照租户逐个执行 Job 的逻辑 + * + * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class TenantJobHandlerDecorator implements JobHandler { + + private final TenantFrameworkService tenantFrameworkService; + /** + * 被装饰的 Job + */ + private final JobHandler jobHandler; + + @Override + public final String execute(String param) throws Exception { + // 获得租户列表 + List tenantIds = tenantFrameworkService.getTenantIds(); + if (CollUtil.isEmpty(tenantIds)) { + return null; + } + + // 逐个租户,执行 Job + Map results = new ConcurrentHashMap<>(); + tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况 + try { + // 设置租户 + TenantContextHolder.setTenantId(tenantId); + // 执行 Job + String result = jobHandler.execute(param); + // 添加结果 + results.put(tenantId, result); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + TenantContextHolder.clear(); + } + }); + return JsonUtils.toJsonString(results); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/mq/TenantRedisMessageInterceptor.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/mq/TenantRedisMessageInterceptor.java new file mode 100644 index 0000000..0572393 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/mq/TenantRedisMessageInterceptor.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.framework.tenant.core.mq; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.mq.core.interceptor.RedisMessageInterceptor; +import com.yunxi.scm.framework.mq.core.message.AbstractRedisMessage; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; + +import static com.yunxi.scm.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * 多租户 {@link AbstractRedisMessage} 拦截器 + * + * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 + * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中 + * + * @author 芋道源码 + */ +public class TenantRedisMessageInterceptor implements RedisMessageInterceptor { + + @Override + public void sendMessageBefore(AbstractRedisMessage message) { + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId != null) { + message.addHeader(HEADER_TENANT_ID, tenantId.toString()); + } + } + + @Override + public void consumeMessageBefore(AbstractRedisMessage message) { + String tenantIdStr = message.getHeader(HEADER_TENANT_ID); + if (StrUtil.isNotEmpty(tenantIdStr)) { + TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr)); + } + } + + @Override + public void consumeMessageAfter(AbstractRedisMessage message) { + // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况 + TenantContextHolder.clear(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/redis/TenantRedisCacheManager.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/redis/TenantRedisCacheManager.java new file mode 100644 index 0000000..ecf5b9f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/redis/TenantRedisCacheManager.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.framework.tenant.core.redis; + +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.Cache; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; + +/** + * 多租户的 {@link RedisCacheManager} 实现类 + * + * 操作指定 name 的 {@link Cache} 时,自动拼接租户后缀,格式为 name + ":" + tenantId + 后缀 + * + * @author airhead + */ +@Slf4j +public class TenantRedisCacheManager extends RedisCacheManager { + + public TenantRedisCacheManager(RedisCacheWriter cacheWriter, + RedisCacheConfiguration defaultCacheConfiguration) { + super(cacheWriter, defaultCacheConfiguration); + } + + @Override + public Cache getCache(String name) { + // 如果开启多租户,则 name 拼接租户后缀 + if (!TenantContextHolder.isIgnore() + && TenantContextHolder.getTenantId() != null) { + name = name + ":" + TenantContextHolder.getTenantId(); + } + + // 继续基于父方法 + return super.getCache(name); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/security/TenantSecurityWebFilter.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/security/TenantSecurityWebFilter.java new file mode 100644 index 0000000..bfc93c4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/security/TenantSecurityWebFilter.java @@ -0,0 +1,117 @@ +package com.yunxi.scm.framework.tenant.core.security; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.framework.tenant.config.TenantProperties; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.tenant.core.service.TenantFrameworkService; +import com.yunxi.scm.framework.web.config.WebProperties; +import com.yunxi.scm.framework.web.core.filter.ApiRequestFilter; +import com.yunxi.scm.framework.web.core.handler.GlobalExceptionHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.AntPathMatcher; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Objects; + +/** + * 多租户 Security Web 过滤器 + * 1. 如果是登陆的用户,校验是否有权限访问该租户,避免越权问题。 + * 2. 如果请求未带租户的编号,检查是否是忽略的 URL,否则也不允许访问。 + * 3. 校验租户是合法,例如说被禁用、到期 + * + * @author 芋道源码 + */ +@Slf4j +public class TenantSecurityWebFilter extends ApiRequestFilter { + + private final TenantProperties tenantProperties; + + private final AntPathMatcher pathMatcher; + + private final GlobalExceptionHandler globalExceptionHandler; + private final TenantFrameworkService tenantFrameworkService; + + public TenantSecurityWebFilter(TenantProperties tenantProperties, + WebProperties webProperties, + GlobalExceptionHandler globalExceptionHandler, + TenantFrameworkService tenantFrameworkService) { + super(webProperties); + this.tenantProperties = tenantProperties; + this.pathMatcher = new AntPathMatcher(); + this.globalExceptionHandler = globalExceptionHandler; + this.tenantFrameworkService = tenantFrameworkService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + Long tenantId = TenantContextHolder.getTenantId(); + // 1. 登陆的用户,校验是否有权限访问该租户,避免越权问题。 + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + if (user != null) { + // 如果获取不到租户编号,则尝试使用登陆用户的租户编号 + if (tenantId == null) { + tenantId = user.getTenantId(); + TenantContextHolder.setTenantId(tenantId); + // 如果传递了租户编号,则进行比对租户编号,避免越权问题 + } else if (!Objects.equals(user.getTenantId(), TenantContextHolder.getTenantId())) { + log.error("[doFilterInternal][租户({}) User({}/{}) 越权访问租户({}) URL({}/{})]", + user.getTenantId(), user.getId(), user.getUserType(), + TenantContextHolder.getTenantId(), request.getRequestURI(), request.getMethod()); + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.FORBIDDEN.getCode(), + "您无权访问该租户的数据")); + return; + } + } + + // 如果非允许忽略租户的 URL,则校验租户是否合法 + if (!isIgnoreUrl(request)) { + // 2. 如果请求未带租户的编号,不允许访问。 + if (tenantId == null) { + log.error("[doFilterInternal][URL({}/{}) 未传递租户编号]", request.getRequestURI(), request.getMethod()); + ServletUtils.writeJSON(response, CommonResult.error(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), + "请求的租户标识未传递,请进行排查")); + return; + } + // 3. 校验租户是合法,例如说被禁用、到期 + try { + tenantFrameworkService.validTenant(tenantId); + } catch (Throwable ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } else { // 如果是允许忽略租户的 URL,若未传递租户编号,则默认忽略租户编号,避免报错 + if (tenantId == null) { + TenantContextHolder.setIgnore(true); + } + } + + // 继续过滤 + chain.doFilter(request, response); + } + + private boolean isIgnoreUrl(HttpServletRequest request) { + // 快速匹配,保证性能 + if (CollUtil.contains(tenantProperties.getIgnoreUrls(), request.getRequestURI())) { + return true; + } + // 逐个 Ant 路径匹配 + for (String url : tenantProperties.getIgnoreUrls()) { + if (pathMatcher.match(url, request.getRequestURI())) { + return true; + } + } + return false; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/service/TenantFrameworkService.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/service/TenantFrameworkService.java new file mode 100644 index 0000000..9d73a09 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/service/TenantFrameworkService.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.framework.tenant.core.service; + +import java.util.List; + +/** + * Tenant 框架 Service 接口,定义获取租户信息 + * + * @author 芋道源码 + */ +public interface TenantFrameworkService { + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIds(); + + /** + * 校验租户是否合法 + * + * @param id 租户编号 + */ + void validTenant(Long id); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/service/TenantFrameworkServiceImpl.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/service/TenantFrameworkServiceImpl.java new file mode 100644 index 0000000..ce5ff16 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/service/TenantFrameworkServiceImpl.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.framework.tenant.core.service; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.util.cache.CacheUtils; +import com.yunxi.scm.module.system.api.tenant.TenantApi; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; + +import java.time.Duration; +import java.util.List; + +/** + * Tenant 框架 Service 实现类 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class TenantFrameworkServiceImpl implements TenantFrameworkService { + + private static final ServiceException SERVICE_EXCEPTION_NULL = new ServiceException(); + + private final TenantApi tenantApi; + + /** + * 针对 {@link #getTenantIds()} 的缓存 + */ + private final LoadingCache> getTenantIdsCache = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader>() { + + @Override + public List load(Object key) { + return tenantApi.getTenantIdList(); + } + + }); + + /** + * 针对 {@link #validTenant(Long)} 的缓存 + */ + private final LoadingCache validTenantCache = CacheUtils.buildAsyncReloadingCache( + Duration.ofMinutes(1L), // 过期时间 1 分钟 + new CacheLoader() { + + @Override + public ServiceException load(Long id) { + try { + tenantApi.validateTenant(id); + return SERVICE_EXCEPTION_NULL; + } catch (ServiceException ex) { + return ex; + } + } + + }); + + @Override + @SneakyThrows + public List getTenantIds() { + return getTenantIdsCache.get(Boolean.TRUE); + } + + @Override + public void validTenant(Long id) { + ServiceException serviceException = validTenantCache.getUnchecked(id); + if (serviceException != SERVICE_EXCEPTION_NULL) { + throw serviceException; + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/util/TenantUtils.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/util/TenantUtils.java new file mode 100644 index 0000000..967fbab --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/util/TenantUtils.java @@ -0,0 +1,93 @@ +package com.yunxi.scm.framework.tenant.core.util; + +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; + +import java.util.Map; +import java.util.concurrent.Callable; + +import static com.yunxi.scm.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * 多租户 Util + * + * @author 芋道源码 + */ +public class TenantUtils { + + /** + * 使用指定租户,执行对应的逻辑 + * + * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 + * 当然,执行完成后,还是会恢复回去 + * + * @param tenantId 租户编号 + * @param runnable 逻辑 + */ + public static void execute(Long tenantId, Runnable runnable) { + Long oldTenantId = TenantContextHolder.getTenantId(); + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setTenantId(tenantId); + TenantContextHolder.setIgnore(false); + // 执行逻辑 + runnable.run(); + } finally { + TenantContextHolder.setTenantId(oldTenantId); + TenantContextHolder.setIgnore(oldIgnore); + } + } + + /** + * 使用指定租户,执行对应的逻辑 + * + * 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户 + * 当然,执行完成后,还是会恢复回去 + * + * @param tenantId 租户编号 + * @param callable 逻辑 + */ + public static V execute(Long tenantId, Callable callable) { + Long oldTenantId = TenantContextHolder.getTenantId(); + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setTenantId(tenantId); + TenantContextHolder.setIgnore(false); + // 执行逻辑 + return callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + TenantContextHolder.setTenantId(oldTenantId); + TenantContextHolder.setIgnore(oldIgnore); + } + } + + /** + * 忽略租户,执行对应的逻辑 + * + * @param runnable 逻辑 + */ + public static void executeIgnore(Runnable runnable) { + Boolean oldIgnore = TenantContextHolder.isIgnore(); + try { + TenantContextHolder.setIgnore(true); + // 执行逻辑 + runnable.run(); + } finally { + TenantContextHolder.setIgnore(oldIgnore); + } + } + + /** + * 将多租户编号,添加到 header 中 + * + * @param headers HTTP 请求 headers + * @param tenantId 租户编号 + */ + public static void addTenantHeader(Map headers, Long tenantId) { + if (tenantId != null) { + headers.put(HEADER_TENANT_ID, tenantId.toString()); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/web/TenantContextWebFilter.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/web/TenantContextWebFilter.java new file mode 100644 index 0000000..6122181 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/core/web/TenantContextWebFilter.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.framework.tenant.core.web; + +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 多租户 Context Web 过滤器 + * 将请求 Header 中的 tenant-id 解析出来,添加到 {@link TenantContextHolder} 中,这样后续的 DB 等操作,可以获得到租户编号。 + * + * @author 芋道源码 + */ +public class TenantContextWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + // 设置 + Long tenantId = WebFrameworkUtils.getTenantId(request); + if (tenantId != null) { + TenantContextHolder.setTenantId(tenantId); + } + try { + chain.doFilter(request, response); + } finally { + // 清理 + TenantContextHolder.clear(); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/package-info.java new file mode 100644 index 0000000..d4b4942 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/java/com/yunxi/scm/framework/tenant/package-info.java @@ -0,0 +1,17 @@ +/** + * 多租户,支持如下层面: + * 1. DB:基于 MyBatis Plus 多租户的功能实现。 + * 2. Redis:通过在 Redis Key 上拼接租户编号的方式,进行隔离。 + * 3. Web:请求 HTTP API 时,解析 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 4. Security:校验当前登陆的用户,是否越权访问其它租户的数据。 + * 5. Job:在 JobHandler 执行任务时,会按照每个租户,都独立并行执行一次。 + * 6. MQ:在 Producer 发送消息时,Header 带上 tenant-id 租户编号;在 Consumer 消费消息时,将 Header 的 tenant-id 租户编号,添加到租户上下文。 + * 7. Async:异步需要保证 ThreadLocal 的传递性,通过使用阿里开源的 TransmittableThreadLocal 实现。相关的改造点,可见: + * 1)Spring Async: + * {@link com.yunxi.scm.framework.quartz.config.YunxiAsyncAutoConfiguration#threadPoolTaskExecutorBeanPostProcessor()} + * 2)Spring Security: + * TransmittableThreadLocalSecurityContextHolderStrategy + * 和 YunxiSecurityAutoConfiguration#securityContextHolderMethodInvokingFactoryBean() 方法 + * + */ +package com.yunxi.scm.framework.tenant; diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..fd99945 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.tenant.config.YunxiTenantAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/test/java/com/yunxi/scm/framework/tenant/core/job/TenantJobTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/test/java/com/yunxi/scm/framework/tenant/core/job/TenantJobTest.java new file mode 100644 index 0000000..71f0c5a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/test/java/com/yunxi/scm/framework/tenant/core/job/TenantJobTest.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.framework.tenant.core.job; + +import com.yunxi.scm.framework.tenant.core.service.TenantFrameworkService; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * 验证 job 租户逻辑 + * {@link TenantJobHandlerDecorator} + * + * @author gaibu + */ +public class TenantJobTest extends BaseMockitoUnitTest { + + @Mock + TenantFrameworkService tenantFrameworkService; + + @Test + public void test() throws Exception { + // 准备测试租户 id + List tenantIds = Lists.newArrayList(1L, 2L, 3L); + // mock 数据 + Mockito.doReturn(tenantIds).when(tenantFrameworkService).getTenantIds(); + // 准备测试任务 + TestJob testJob = new TestJob(); + // 创建任务装饰器 + TenantJobHandlerDecorator tenantJobHandlerDecorator = new TenantJobHandlerDecorator(tenantFrameworkService, testJob); + + // 执行任务 + tenantJobHandlerDecorator.execute(null); + + // 断言返回值 + assertEquals(testJob.getTenantIds(), tenantIds); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/test/java/com/yunxi/scm/framework/tenant/core/job/TestJob.java b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/test/java/com/yunxi/scm/framework/tenant/core/job/TestJob.java new file mode 100644 index 0000000..c20eaea --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-tenant/src/test/java/com/yunxi/scm/framework/tenant/core/job/TestJob.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.framework.tenant.core.job; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +@Component +@TenantJob // 标记多租户 +public class TestJob implements JobHandler { + + private final List tenantIds = new CopyOnWriteArrayList<>(); + + @Override + public String execute(String param) throws Exception { + tenantIds.add(TenantContextHolder.getTenantId()); + return "success"; + } + + public List getTenantIds() { + CollUtil.sort(tenantIds, Long::compareTo); + return tenantIds; + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/pom.xml new file mode 100644 index 0000000..6ef19c0 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/pom.xml @@ -0,0 +1,45 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-biz-weixin + jar + + ${project.artifactId} + 微信拓展 + 1. 基于 weixin-java-mp 库,对接微信公众号平台。目前主要解决微信公众号的支付场景。 + 2. 基于 weixin-java-miniapp 库,对接微信小程序。目前主要解决微信小程序的一键登录场景。 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + + com.github.binarywang + wx-java-mp-spring-boot-starter + + + com.github.binarywang + wx-java-miniapp-spring-boot-starter + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/main/java/com/yunxi/scm/framework/weixin/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/main/java/com/yunxi/scm/framework/weixin/package-info.java new file mode 100644 index 0000000..4093d6f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/main/java/com/yunxi/scm/framework/weixin/package-info.java @@ -0,0 +1,7 @@ +/** + * 微信拓展 + * 1. 基于 weixin-java-mp 库,对接微信公众号平台。目前主要解决微信公众号的支付场景。 + * 2. 基于 weixin-java-miniapp 库,对接微信小程序。目前主要解决微信小程序的一键登录场景。 + */ +package com.yunxi.scm.framework.weixin; + diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/test-integration/java/com/yunxi/scm/framework/weixin/WxMpServiceTest.java b/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/test-integration/java/com/yunxi/scm/framework/weixin/WxMpServiceTest.java new file mode 100644 index 0000000..b7cf768 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/test-integration/java/com/yunxi/scm/framework/weixin/WxMpServiceTest.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.framework.weixin; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; + +import javax.annotation.Resource; + +@SpringBootTest(classes = WxMpServiceTest.Application.class) +public class WxMpServiceTest { + + @Resource + private WxMpService wxMpService; + + @Test + public void testGetAccessToken() throws WxErrorException { + String accessToken = wxMpService.getAccessToken(); + System.out.println(accessToken); + } + + @Test + public void testGet() throws WxErrorException { + String jsapiTicket = wxMpService.getJsapiTicket(); + System.out.println(jsapiTicket); + } + + @SpringBootApplication + public static class Application { + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml b/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml new file mode 100644 index 0000000..9b30060 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-biz-weixin/src/test-integration/resources/application.yml @@ -0,0 +1,11 @@ +--- #################### 微信公众号相关配置 #################### +wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 + mp: + # 公众号配置(必填) + app-id: wx041349c6f39b268b + secret: 5abee519483bc9f8cb37ce280e814bd0 + # 存储配置,解决 AccessToken 的跨节点的共享 +# config-storage: +# type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 +# key-prefix: wx # Redis Key 的前缀 TODO 芋艿:解决下 Redis key 管理的配置 +# http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-captcha/pom.xml new file mode 100644 index 0000000..d25f414 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-captcha/pom.xml @@ -0,0 +1,38 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-captcha + jar + + ${project.artifactId} + 验证码拓展 + 1. 基于 aj-captcha 实现滑块验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/ + + + + + com.xingyuv + spring-boot-starter-captcha-plus + + + + org.springframework.boot + spring-boot-starter + + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/config/YunxiCaptchaConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/config/YunxiCaptchaConfiguration.java new file mode 100644 index 0000000..92cc44c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/config/YunxiCaptchaConfiguration.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.framework.captcha.config; + +import com.yunxi.scm.framework.captcha.core.service.RedisCaptchaServiceImpl; +import com.xingyuv.captcha.properties.AjCaptchaProperties; +import com.xingyuv.captcha.service.CaptchaCacheService; +import com.xingyuv.captcha.service.impl.CaptchaServiceFactory; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import javax.annotation.Resource; + +@AutoConfiguration +public class YunxiCaptchaConfiguration { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Bean + public CaptchaCacheService captchaCacheService(AjCaptchaProperties config) { + // 缓存类型 redis/local/.... + CaptchaCacheService ret = CaptchaServiceFactory.getCache(config.getCacheType().name()); + if (ret instanceof RedisCaptchaServiceImpl) { + ((RedisCaptchaServiceImpl) ret).setStringRedisTemplate(stringRedisTemplate); + } + return ret; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/core/enums/CaptchaRedisKeyConstants.java b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/core/enums/CaptchaRedisKeyConstants.java new file mode 100644 index 0000000..6324bd9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/core/enums/CaptchaRedisKeyConstants.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.captcha.core.enums; + +/** + * 验证码 Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface CaptchaRedisKeyConstants { + + /** + * 验证码的请求限流 + * + * KEY 格式:AJ.CAPTCHA.REQ.LIMIT-%s-%s + * VALUE 数据类型:String // 例如说:验证失败 5 次,get 接口锁定 + * 过期时间:60 秒 + */ + String AJ_CAPTCHA_REQ_LIMIT = "AJ.CAPTCHA.REQ.LIMIT-%s-%s"; + + /** + * 验证码的坐标 + * + * KEY 格式:RUNNING:CAPTCHA:%s // AbstractCaptchaService.REDIS_CAPTCHA_KEY + * VALUE 数据类型:String // PointVO.class {"secretKey":"PP1w2Frr2KEejD2m","x":162,"y":5} + * 过期时间:120 秒 + */ + String AJ_CAPTCHA_RUNNING = "RUNNING:CAPTCHA:%s"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/core/service/RedisCaptchaServiceImpl.java b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/core/service/RedisCaptchaServiceImpl.java new file mode 100644 index 0000000..32223dc --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/core/service/RedisCaptchaServiceImpl.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.framework.captcha.core.service; + +import com.xingyuv.captcha.service.CaptchaCacheService; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis 实现验证码的存储 + * + * @author 星语 + */ +@NoArgsConstructor // 保证 aj-captcha 的 SPI 创建 +@AllArgsConstructor +public class RedisCaptchaServiceImpl implements CaptchaCacheService { + + @Resource // 保证 aj-captcha 的 SPI 创建时的注入 + private StringRedisTemplate stringRedisTemplate; + + @Override + public String type() { + return "redis"; + } + + public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) { + this.stringRedisTemplate = stringRedisTemplate; + } + + @Override + public void set(String key, String value, long expiresInSeconds) { + stringRedisTemplate.opsForValue().set(key, value, expiresInSeconds, TimeUnit.SECONDS); + } + + @Override + public boolean exists(String key) { + return Boolean.TRUE.equals(stringRedisTemplate.hasKey(key)); + } + + @Override + public void delete(String key) { + stringRedisTemplate.delete(key); + } + + @Override + public String get(String key) { + return stringRedisTemplate.opsForValue().get(key); + } + + @Override + public Long increment(String key, long val) { + return stringRedisTemplate.opsForValue().increment(key,val); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/package-info.java new file mode 100644 index 0000000..2c14be4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/java/com/yunxi/scm/framework/captcha/package-info.java @@ -0,0 +1,7 @@ +/** + * 验证码拓展 + * 1. 基于 aj-captcha 实现滑块验证码,文档:https://ajcaptcha.beliefteam.cn/captcha-doc/ + * + * @author 星语 + */ +package com.yunxi.scm.framework.captcha; diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaCacheService b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaCacheService new file mode 100644 index 0000000..b78d435 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/META-INF/services/com.xingyuv.captcha.service.CaptchaCacheService @@ -0,0 +1 @@ +com.yunxi.scm.framework.captcha.core.service.RedisCaptchaServiceImpl diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..2256bdf --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.captcha.config.YunxiCaptchaConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg1.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg1.png new file mode 100644 index 0000000..c481457 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg1.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg2.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg2.png new file mode 100644 index 0000000..bf8fb38 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg2.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg3.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg3.png new file mode 100644 index 0000000..f871d3d Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg3.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg4.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg4.png new file mode 100644 index 0000000..2e3d871 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg4.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg5.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg5.png new file mode 100644 index 0000000..fe383b7 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg5.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg6.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg6.png new file mode 100644 index 0000000..5024ceb Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg6.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg7.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg7.png new file mode 100644 index 0000000..efe76f8 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg7.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg8.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg8.png new file mode 100644 index 0000000..2727aa3 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg8.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg9.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg9.png new file mode 100644 index 0000000..4463aa2 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/original/bg9.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/1.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/1.png new file mode 100644 index 0000000..ef11324 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/1.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/10.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/10.png new file mode 100644 index 0000000..297e44c Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/10.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/11.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/11.png new file mode 100644 index 0000000..d9b1da8 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/11.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/12.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/12.png new file mode 100644 index 0000000..07e7313 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/12.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/13.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/13.png new file mode 100644 index 0000000..82c3dd9 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/13.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/14.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/14.png new file mode 100644 index 0000000..0b9a866 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/14.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/15.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/15.png new file mode 100644 index 0000000..86b0d1c Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/15.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/16.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/16.png new file mode 100644 index 0000000..e90a6e2 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/16.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/17.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/17.png new file mode 100644 index 0000000..a82cbc7 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/17.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/18.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/18.png new file mode 100644 index 0000000..d3f3cfd Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/18.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/19.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/19.png new file mode 100644 index 0000000..eb2855b Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/19.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/8.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/8.png new file mode 100644 index 0000000..3cb5ce1 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/8.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/9.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/9.png new file mode 100644 index 0000000..384d354 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/11/9.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/2.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/2.png new file mode 100644 index 0000000..baf3f06 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/2.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/3.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/3.png new file mode 100644 index 0000000..ccaf617 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/3.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/4.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/4.png new file mode 100644 index 0000000..7dab162 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/jigsaw/slidingBlock/4.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg1.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg1.png new file mode 100644 index 0000000..14e7345 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg1.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg10.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg10.png new file mode 100644 index 0000000..1ea1d6d Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg10.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg2.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg2.png new file mode 100644 index 0000000..0edb329 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg2.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg3.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg3.png new file mode 100644 index 0000000..9167996 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg3.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg4.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg4.png new file mode 100644 index 0000000..e8e8e6c Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg4.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg5.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg5.png new file mode 100644 index 0000000..66a3181 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg5.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg6.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg6.png new file mode 100644 index 0000000..9b0f5d8 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg6.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg7.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg7.png new file mode 100644 index 0000000..db41c74 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg7.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg8.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg8.png new file mode 100644 index 0000000..3496813 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg8.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg9.png b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg9.png new file mode 100644 index 0000000..4e7b477 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-captcha/src/main/resources/images/pic-click/bg9.png differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-desensitize/pom.xml new file mode 100644 index 0000000..6526ef3 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + yunxi-framework + com.yunxi.scm + ${revision} + + + yunxi-spring-boot-starter-desensitize + 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏 + + + + com.yunxi.scm + yunxi-common + + + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/annotation/DesensitizeBy.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/annotation/DesensitizeBy.java new file mode 100644 index 0000000..9c316e7 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/annotation/DesensitizeBy.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.framework.desensitize.core.base.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.handler.DesensitizationHandler; +import com.yunxi.scm.framework.desensitize.core.base.serializer.StringDesensitizeSerializer; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 顶级脱敏注解,自定义注解需要使用此注解 + * + * @author gaibu + */ +@Documented +@Target(ElementType.ANNOTATION_TYPE) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside // 此注解是其他所有 jackson 注解的元注解,打上了此注解的注解表明是 jackson 注解的一部分 +@JsonSerialize(using = StringDesensitizeSerializer.class) // 指定序列化器 +public @interface DesensitizeBy { + + /** + * 脱敏处理器 + */ + @SuppressWarnings("rawtypes") + Class handler(); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/handler/DesensitizationHandler.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/handler/DesensitizationHandler.java new file mode 100644 index 0000000..f8f6851 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/handler/DesensitizationHandler.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.desensitize.core.base.handler; + +import java.lang.annotation.Annotation; + +/** + * 脱敏处理器接口 + * + * @author gaibu + */ +public interface DesensitizationHandler { + + /** + * 脱敏 + * + * @param origin 原始字符串 + * @param annotation 注解信息 + * @return 脱敏后的字符串 + */ + String desensitize(String origin, T annotation); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java new file mode 100644 index 0000000..bce44e3 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/base/serializer/StringDesensitizeSerializer.java @@ -0,0 +1,92 @@ +package com.yunxi.scm.framework.desensitize.core.base.serializer; + +import cn.hutool.core.annotation.AnnotationUtil; +import cn.hutool.core.lang.Singleton; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.base.handler.DesensitizationHandler; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.ContextualSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import lombok.Getter; +import lombok.Setter; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +/** + * 脱敏序列化器 + * + * 实现 JSON 返回数据时,使用 {@link DesensitizationHandler} 对声明脱敏注解的字段,进行脱敏处理。 + * + * @author gaibu + */ +@SuppressWarnings("rawtypes") +public class StringDesensitizeSerializer extends StdSerializer implements ContextualSerializer { + + @Getter + @Setter + private DesensitizationHandler desensitizationHandler; + + protected StringDesensitizeSerializer() { + super(String.class); + } + + @Override + public JsonSerializer createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) { + DesensitizeBy annotation = beanProperty.getAnnotation(DesensitizeBy.class); + if (annotation == null) { + return this; + } + // 创建一个 StringDesensitizeSerializer 对象,使用 DesensitizeBy 对应的处理器 + StringDesensitizeSerializer serializer = new StringDesensitizeSerializer(); + serializer.setDesensitizationHandler(Singleton.get(annotation.handler())); + return serializer; + } + + @Override + @SuppressWarnings("unchecked") + public void serialize(String value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException { + if (StrUtil.isBlank(value)) { + gen.writeNull(); + return; + } + // 获取序列化字段 + Field field = getField(gen); + + // 自定义处理器 + DesensitizeBy[] annotations = AnnotationUtil.getCombinationAnnotations(field, DesensitizeBy.class); + if (ArrayUtil.isEmpty(annotations)) { + gen.writeString(value); + return; + } + for (Annotation annotation : field.getAnnotations()) { + if (AnnotationUtil.hasAnnotation(annotation.annotationType(), DesensitizeBy.class)) { + value = this.desensitizationHandler.desensitize(value, annotation); + gen.writeString(value); + return; + } + } + gen.writeString(value); + } + + /** + * 获取字段 + * + * @param generator JsonGenerator + * @return 字段 + */ + private Field getField(JsonGenerator generator) { + String currentName = generator.getOutputContext().getCurrentName(); + Object currentValue = generator.getCurrentValue(); + Class currentValueClass = currentValue.getClass(); + return ReflectUtil.getField(currentValueClass, currentName); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/package-info.java new file mode 100644 index 0000000..2a17f2c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 脱敏组件:支持 JSON 返回数据时,将邮箱、手机等字段进行脱敏 + */ +package com.yunxi.scm.framework.desensitize.core; diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/annotation/EmailDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/annotation/EmailDesensitize.java new file mode 100644 index 0000000..172848b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/annotation/EmailDesensitize.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.desensitize.core.regex.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.regex.handler.EmailDesensitizationHandler; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 邮箱脱敏注解 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = EmailDesensitizationHandler.class) +public @interface EmailDesensitize { + + /** + * 匹配的正则表达式 + */ + String regex() default "(^.)[^@]*(@.*$)"; + + /** + * 替换规则,邮箱; + * + * 比如:example@gmail.com 脱敏之后为 e****@gmail.com + */ + String replacer() default "$1****$2"; +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/annotation/RegexDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/annotation/RegexDesensitize.java new file mode 100644 index 0000000..eb3e542 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/annotation/RegexDesensitize.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.framework.desensitize.core.regex.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.regex.handler.DefaultRegexDesensitizationHandler; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 正则脱敏注解 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = DefaultRegexDesensitizationHandler.class) +public @interface RegexDesensitize { + + /** + * 匹配的正则表达式(默认匹配所有) + */ + String regex() default "^[\\s\\S]*$"; + + /** + * 替换规则,会将匹配到的字符串全部替换成 replacer + * + * 例如:regex=123; replacer=****** + * 原始字符串 123456789 + * 脱敏后字符串 ******456789 + */ + String replacer() default "******"; +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java new file mode 100644 index 0000000..818a920 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/AbstractRegexDesensitizationHandler.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.framework.desensitize.core.regex.handler; + +import com.yunxi.scm.framework.desensitize.core.base.handler.DesensitizationHandler; + +import java.lang.annotation.Annotation; + +/** + * 正则表达式脱敏处理器抽象类,已实现通用的方法 + * + * @author gaibu + */ +public abstract class AbstractRegexDesensitizationHandler + implements DesensitizationHandler { + + @Override + public String desensitize(String origin, T annotation) { + String regex = getRegex(annotation); + String replacer = getReplacer(annotation); + return origin.replaceAll(regex, replacer); + } + + /** + * 获取注解上的 regex 参数 + * + * @param annotation 注解信息 + * @return 正则表达式 + */ + abstract String getRegex(T annotation); + + /** + * 获取注解上的 replacer 参数 + * + * @param annotation 注解信息 + * @return 待替换的字符串 + */ + abstract String getReplacer(T annotation); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java new file mode 100644 index 0000000..01f3c99 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/DefaultRegexDesensitizationHandler.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.desensitize.core.regex.handler; + +import com.yunxi.scm.framework.desensitize.core.regex.annotation.RegexDesensitize; + +/** + * {@link RegexDesensitize} 的正则脱敏处理器 + * + * @author gaibu + */ +public class DefaultRegexDesensitizationHandler extends AbstractRegexDesensitizationHandler { + + @Override + String getRegex(RegexDesensitize annotation) { + return annotation.regex(); + } + + @Override + String getReplacer(RegexDesensitize annotation) { + return annotation.replacer(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java new file mode 100644 index 0000000..504d5b3 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/regex/handler/EmailDesensitizationHandler.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.framework.desensitize.core.regex.handler; + +import com.yunxi.scm.framework.desensitize.core.regex.annotation.EmailDesensitize; + +/** + * {@link EmailDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class EmailDesensitizationHandler extends AbstractRegexDesensitizationHandler { + + @Override + String getRegex(EmailDesensitize annotation) { + return annotation.regex(); + } + + @Override + String getReplacer(EmailDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/BankCardDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/BankCardDesensitize.java new file mode 100644 index 0000000..8d6afe6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/BankCardDesensitize.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.desensitize.core.slider.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.slider.handler.BankCardDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 银行卡号 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = BankCardDesensitization.class) +public @interface BankCardDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 6; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 2; + + /** + * 替换规则,银行卡号; 比如:9988002866797031 脱敏之后为 998800********31 + */ + String replacer() default "*"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java new file mode 100644 index 0000000..1b00452 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/CarLicenseDesensitize.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.desensitize.core.slider.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.slider.handler.CarLicenseDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 车牌号 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = CarLicenseDesensitization.class) +public @interface CarLicenseDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 3; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 1; + + /** + * 替换规则,车牌号;比如:粤A66666 脱敏之后为粤A6***6 + */ + String replacer() default "*"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java new file mode 100644 index 0000000..e940b04 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/ChineseNameDesensitize.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.desensitize.core.slider.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.slider.handler.ChineseNameDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 中文名 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = ChineseNameDesensitization.class) +public @interface ChineseNameDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 1; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 0; + + /** + * 替换规则,中文名;比如:刘子豪脱敏之后为刘** + */ + String replacer() default "*"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java new file mode 100644 index 0000000..d0a97dd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/FixedPhoneDesensitize.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.desensitize.core.slider.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.slider.handler.FixedPhoneDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 固定电话 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = FixedPhoneDesensitization.class) +public @interface FixedPhoneDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 4; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 2; + + /** + * 替换规则,固定电话;比如:01086551122 脱敏之后为 0108*****22 + */ + String replacer() default "*"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/IdCardDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/IdCardDesensitize.java new file mode 100644 index 0000000..da76175 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/IdCardDesensitize.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.desensitize.core.slider.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.slider.handler.IdCardDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 身份证 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = IdCardDesensitization.class) +public @interface IdCardDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 6; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 2; + + /** + * 替换规则,身份证号码;比如:530321199204074611 脱敏之后为 530321**********11 + */ + String replacer() default "*"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/MobileDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/MobileDesensitize.java new file mode 100644 index 0000000..5f11c92 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/MobileDesensitize.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.desensitize.core.slider.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.slider.handler.MobileDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 手机号 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = MobileDesensitization.class) +public @interface MobileDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 3; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 4; + + /** + * 替换规则,手机号;比如:13248765917 脱敏之后为 132****5917 + */ + String replacer() default "*"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/PasswordDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/PasswordDesensitize.java new file mode 100644 index 0000000..f61dfc5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/PasswordDesensitize.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.framework.desensitize.core.slider.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.slider.handler.PasswordDesensitization; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 密码 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = PasswordDesensitization.class) +public @interface PasswordDesensitize { + + /** + * 前缀保留长度 + */ + int prefixKeep() default 0; + + /** + * 后缀保留长度 + */ + int suffixKeep() default 0; + + /** + * 替换规则,密码; + * + * 比如:123456 脱敏之后为 ****** + */ + String replacer() default "*"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/SliderDesensitize.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/SliderDesensitize.java new file mode 100644 index 0000000..ce271a0 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/annotation/SliderDesensitize.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.desensitize.core.slider.annotation; + +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.slider.handler.DefaultDesensitizationHandler; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 滑动脱敏注解 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = DefaultDesensitizationHandler.class) +public @interface SliderDesensitize { + + /** + * 后缀保留长度 + */ + int suffixKeep() default 0; + + /** + * 替换规则,会将前缀后缀保留后,全部替换成 replacer + * + * 例如:prefixKeep = 1; suffixKeep = 2; replacer = "*"; + * 原始字符串 123456 + * 脱敏后 1***56 + */ + String replacer() default "*"; + + /** + * 前缀保留长度 + */ + int prefixKeep() default 0; +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java new file mode 100644 index 0000000..869bf61 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/AbstractSliderDesensitizationHandler.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.base.handler.DesensitizationHandler; + +import java.lang.annotation.Annotation; + +/** + * 滑动脱敏处理器抽象类,已实现通用的方法 + * + * @author gaibu + */ +public abstract class AbstractSliderDesensitizationHandler + implements DesensitizationHandler { + + @Override + public String desensitize(String origin, T annotation) { + int prefixKeep = getPrefixKeep(annotation); + int suffixKeep = getSuffixKeep(annotation); + String replacer = getReplacer(annotation); + int length = origin.length(); + + // 情况一:原始字符串长度小于等于保留长度,则原始字符串全部替换 + if (prefixKeep >= length || suffixKeep >= length) { + return buildReplacerByLength(replacer, length); + } + + // 情况二:原始字符串长度小于等于前后缀保留字符串长度,则原始字符串全部替换 + if ((prefixKeep + suffixKeep) >= length) { + return buildReplacerByLength(replacer, length); + } + + // 情况三:原始字符串长度大于前后缀保留字符串长度,则替换中间字符串 + int interval = length - prefixKeep - suffixKeep; + return origin.substring(0, prefixKeep) + + buildReplacerByLength(replacer, interval) + + origin.substring(prefixKeep + interval); + } + + /** + * 根据长度循环构建替换符 + * + * @param replacer 替换符 + * @param length 长度 + * @return 构建后的替换符 + */ + private String buildReplacerByLength(String replacer, int length) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < length; i++) { + builder.append(replacer); + } + return builder.toString(); + } + + /** + * 前缀保留长度 + * + * @param annotation 注解信息 + * @return 前缀保留长度 + */ + abstract Integer getPrefixKeep(T annotation); + + /** + * 后缀保留长度 + * + * @param annotation 注解信息 + * @return 后缀保留长度 + */ + abstract Integer getSuffixKeep(T annotation); + + /** + * 替换符 + * + * @param annotation 注解信息 + * @return 替换符 + */ + abstract String getReplacer(T annotation); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/BankCardDesensitization.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/BankCardDesensitization.java new file mode 100644 index 0000000..24d76fb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/BankCardDesensitization.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.slider.annotation.BankCardDesensitize; + +/** + * {@link BankCardDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class BankCardDesensitization extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(BankCardDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(BankCardDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(BankCardDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java new file mode 100644 index 0000000..f71b96a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/CarLicenseDesensitization.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.slider.annotation.CarLicenseDesensitize; + +/** + * {@link CarLicenseDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class CarLicenseDesensitization extends AbstractSliderDesensitizationHandler { + @Override + Integer getPrefixKeep(CarLicenseDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(CarLicenseDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(CarLicenseDesensitize annotation) { + return annotation.replacer(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java new file mode 100644 index 0000000..4d4ff23 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/ChineseNameDesensitization.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.slider.annotation.ChineseNameDesensitize; + +/** + * {@link ChineseNameDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class ChineseNameDesensitization extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(ChineseNameDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(ChineseNameDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(ChineseNameDesensitize annotation) { + return annotation.replacer(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java new file mode 100644 index 0000000..57b32c8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/DefaultDesensitizationHandler.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.slider.annotation.SliderDesensitize; + +/** + * {@link SliderDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class DefaultDesensitizationHandler extends AbstractSliderDesensitizationHandler { + @Override + Integer getPrefixKeep(SliderDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(SliderDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(SliderDesensitize annotation) { + return annotation.replacer(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java new file mode 100644 index 0000000..415fd0d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/FixedPhoneDesensitization.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize; + +/** + * {@link FixedPhoneDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class FixedPhoneDesensitization extends AbstractSliderDesensitizationHandler { + @Override + Integer getPrefixKeep(FixedPhoneDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(FixedPhoneDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(FixedPhoneDesensitize annotation) { + return annotation.replacer(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/IdCardDesensitization.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/IdCardDesensitization.java new file mode 100644 index 0000000..9aa70c6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/IdCardDesensitization.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.slider.annotation.IdCardDesensitize; + +/** + * {@link IdCardDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class IdCardDesensitization extends AbstractSliderDesensitizationHandler { + @Override + Integer getPrefixKeep(IdCardDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(IdCardDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(IdCardDesensitize annotation) { + return annotation.replacer(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/MobileDesensitization.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/MobileDesensitization.java new file mode 100644 index 0000000..d97e185 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/MobileDesensitization.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.slider.annotation.MobileDesensitize; + +/** + * {@link MobileDesensitize} 的脱敏处理器 + * + * @author gaibu + */ +public class MobileDesensitization extends AbstractSliderDesensitizationHandler { + + @Override + Integer getPrefixKeep(MobileDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(MobileDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(MobileDesensitize annotation) { + return annotation.replacer(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/PasswordDesensitization.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/PasswordDesensitization.java new file mode 100644 index 0000000..c6f2728 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/main/java/com/yunxi/scm/framework/desensitize/core/slider/handler/PasswordDesensitization.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.desensitize.core.slider.handler; + +import com.yunxi.scm.framework.desensitize.core.slider.annotation.PasswordDesensitize; + +/** + * {@link PasswordDesensitize} 的码脱敏处理器 + * + * @author gaibu + */ +public class PasswordDesensitization extends AbstractSliderDesensitizationHandler { + @Override + Integer getPrefixKeep(PasswordDesensitize annotation) { + return annotation.prefixKeep(); + } + + @Override + Integer getSuffixKeep(PasswordDesensitize annotation) { + return annotation.suffixKeep(); + } + + @Override + String getReplacer(PasswordDesensitize annotation) { + return annotation.replacer(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/DesensitizeTest.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/DesensitizeTest.java new file mode 100644 index 0000000..ca692ad --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/DesensitizeTest.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.framework.desensitize.core; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.desensitize.core.regex.annotation.EmailDesensitize; +import com.yunxi.scm.framework.desensitize.core.regex.annotation.RegexDesensitize; +import com.yunxi.scm.framework.desensitize.core.annotation.Address; +import com.yunxi.scm.framework.desensitize.core.slider.annotation.BankCardDesensitize; +import com.yunxi.scm.framework.desensitize.core.slider.annotation.CarLicenseDesensitize; +import com.yunxi.scm.framework.desensitize.core.slider.annotation.ChineseNameDesensitize; +import com.yunxi.scm.framework.desensitize.core.slider.annotation.FixedPhoneDesensitize; +import com.yunxi.scm.framework.desensitize.core.slider.annotation.IdCardDesensitize; +import com.yunxi.scm.framework.desensitize.core.slider.annotation.PasswordDesensitize; +import com.yunxi.scm.framework.desensitize.core.slider.annotation.MobileDesensitize; +import com.yunxi.scm.framework.desensitize.core.slider.annotation.SliderDesensitize; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import lombok.Data; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DesensitizeTest} 的单元测试 + */ +public class DesensitizeTest extends BaseMockitoUnitTest { + + @Test + public void test() { + // 准备参数 + DesensitizeDemo desensitizeDemo = new DesensitizeDemo(); + desensitizeDemo.setNickname("芋道源码"); + desensitizeDemo.setBankCard("9988002866797031"); + desensitizeDemo.setCarLicense("粤A66666"); + desensitizeDemo.setFixedPhone("01086551122"); + desensitizeDemo.setIdCard("530321199204074611"); + desensitizeDemo.setPassword("123456"); + desensitizeDemo.setPhoneNumber("13248765917"); + desensitizeDemo.setSlider1("ABCDEFG"); + desensitizeDemo.setSlider2("ABCDEFG"); + desensitizeDemo.setSlider3("ABCDEFG"); + desensitizeDemo.setEmail("1@email.com"); + desensitizeDemo.setRegex("你好,我是芋道源码"); + desensitizeDemo.setAddress("北京市海淀区上地十街10号"); + desensitizeDemo.setOrigin("芋道源码"); + + // 调用 + DesensitizeDemo d = JsonUtils.parseObject(JsonUtils.toJsonString(desensitizeDemo), DesensitizeDemo.class); + // 断言 + assertNotNull(d); + assertEquals("芋***", d.getNickname()); + assertEquals("998800********31", d.getBankCard()); + assertEquals("粤A6***6", d.getCarLicense()); + assertEquals("0108*****22", d.getFixedPhone()); + assertEquals("530321**********11", d.getIdCard()); + assertEquals("******", d.getPassword()); + assertEquals("132****5917", d.getPhoneNumber()); + assertEquals("#######", d.getSlider1()); + assertEquals("ABC*EFG", d.getSlider2()); + assertEquals("*******", d.getSlider3()); + assertEquals("1****@email.com", d.getEmail()); + assertEquals("你好,我是*", d.getRegex()); + assertEquals("北京市海淀区上地十街10号*", d.getAddress()); + assertEquals("芋道源码", d.getOrigin()); + } + + @Data + public static class DesensitizeDemo { + + @ChineseNameDesensitize + private String nickname; + @BankCardDesensitize + private String bankCard; + @CarLicenseDesensitize + private String carLicense; + @FixedPhoneDesensitize + private String fixedPhone; + @IdCardDesensitize + private String idCard; + @PasswordDesensitize + private String password; + @MobileDesensitize + private String phoneNumber; + @SliderDesensitize(prefixKeep = 6, suffixKeep = 1, replacer = "#") + private String slider1; + @SliderDesensitize(prefixKeep = 3, suffixKeep = 3) + private String slider2; + @SliderDesensitize(prefixKeep = 10) + private String slider3; + @EmailDesensitize + private String email; + @RegexDesensitize(regex = "芋道源码", replacer = "*") + private String regex; + @Address + private String address; + private String origin; + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/annotation/Address.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/annotation/Address.java new file mode 100644 index 0000000..85f7fc8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/annotation/Address.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.framework.desensitize.core.annotation; + +import com.yunxi.scm.framework.desensitize.core.DesensitizeTest; +import com.yunxi.scm.framework.desensitize.core.base.annotation.DesensitizeBy; +import com.yunxi.scm.framework.desensitize.core.handler.AddressHandler; +import com.fasterxml.jackson.annotation.JacksonAnnotationsInside; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 地址 + * + * 用于 {@link DesensitizeTest} 测试使用 + * + * @author gaibu + */ +@Documented +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@JacksonAnnotationsInside +@DesensitizeBy(handler = AddressHandler.class) +public @interface Address { + + String replacer() default "*"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/handler/AddressHandler.java b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/handler/AddressHandler.java new file mode 100644 index 0000000..b830852 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-desensitize/src/test/java/com/yunxi/scm/framework/desensitize/core/handler/AddressHandler.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.framework.desensitize.core.handler; + +import com.yunxi.scm.framework.desensitize.core.DesensitizeTest; +import com.yunxi.scm.framework.desensitize.core.base.handler.DesensitizationHandler; +import com.yunxi.scm.framework.desensitize.core.annotation.Address; + +/** + * {@link Address} 的脱敏处理器 + * + * 用于 {@link DesensitizeTest} 测试使用 + */ +public class AddressHandler implements DesensitizationHandler

{ + + @Override + public String desensitize(String origin, Address annotation) { + return origin + annotation.replacer(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-excel/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-excel/pom.xml new file mode 100644 index 0000000..c08ce79 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-excel/pom.xml @@ -0,0 +1,51 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-excel + jar + + ${project.artifactId} + Excel 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-dict + true + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + com.alibaba + easyexcel + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/annotations/DictFormat.java b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/annotations/DictFormat.java new file mode 100644 index 0000000..4c1438c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/annotations/DictFormat.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.framework.excel.core.annotations; + +import java.lang.annotation.*; + +/** + * 字典格式化 + * + * 实现将字典数据的值,格式化成字典数据的标签 + */ +@Target({ElementType.FIELD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface DictFormat { + + /** + * 例如说,SysDictTypeConstants、InfDictTypeConstants + * + * @return 字典类型 + */ + String value(); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/DictConvert.java b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/DictConvert.java new file mode 100644 index 0000000..902062b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/DictConvert.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.framework.excel.core.convert; + +import cn.hutool.core.convert.Convert; +import com.yunxi.scm.framework.dict.core.util.DictFrameworkUtils; +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.ReadCellData; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; +import lombok.extern.slf4j.Slf4j; + +/** + * Excel 数据字典转换器 + * + * @author 芋道源码 + */ +@Slf4j +public class DictConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public Object convertToJavaData(ReadCellData readCellData, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 使用字典解析 + String type = getType(contentProperty); + String label = readCellData.getStringValue(); + String value = DictFrameworkUtils.parseDictDataValue(type, label); + if (value == null) { + log.error("[convertToJavaData][type({}) 解析不掉 label({})]", type, label); + return null; + } + // 将 String 的 value 转换成对应的属性 + Class fieldClazz = contentProperty.getField().getType(); + return Convert.convert(fieldClazz, value); + } + + @Override + public WriteCellData convertToExcelData(Object object, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 空时,返回空 + if (object == null) { + return new WriteCellData<>(""); + } + + // 使用字典格式化 + String type = getType(contentProperty); + String value = String.valueOf(object); + String label = DictFrameworkUtils.getDictDataLabel(type, value); + if (label == null) { + log.error("[convertToExcelData][type({}) 转换不了 label({})]", type, value); + return new WriteCellData<>(""); + } + // 生成 Excel 小表格 + return new WriteCellData<>(label); + } + + private static String getType(ExcelContentProperty contentProperty) { + return contentProperty.getField().getAnnotation(DictFormat.class).value(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/JsonConvert.java b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/JsonConvert.java new file mode 100644 index 0000000..16d2060 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/JsonConvert.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.framework.excel.core.convert; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +/** + * Excel Json 转换器 + * + * @author 芋道源码 + */ +public class JsonConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public WriteCellData convertToExcelData(Object value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + // 生成 Excel 小表格 + return new WriteCellData<>(JsonUtils.toJsonString(value)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/MoneyConvert.java b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/MoneyConvert.java new file mode 100644 index 0000000..bd108fb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/convert/MoneyConvert.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.framework.excel.core.convert; + +import com.alibaba.excel.converters.Converter; +import com.alibaba.excel.enums.CellDataTypeEnum; +import com.alibaba.excel.metadata.GlobalConfiguration; +import com.alibaba.excel.metadata.data.WriteCellData; +import com.alibaba.excel.metadata.property.ExcelContentProperty; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金额转换器 + * + * 金额单位:分 + * + * @author 芋道源码 + */ +public class MoneyConvert implements Converter { + + @Override + public Class supportJavaTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public CellDataTypeEnum supportExcelTypeKey() { + throw new UnsupportedOperationException("暂不支持,也不需要"); + } + + @Override + public WriteCellData convertToExcelData(Integer value, ExcelContentProperty contentProperty, + GlobalConfiguration globalConfiguration) { + BigDecimal result = BigDecimal.valueOf(value) + .divide(new BigDecimal(100), 2, RoundingMode.HALF_UP); + return new WriteCellData<>(result.toString()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/util/ExcelUtils.java b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/util/ExcelUtils.java new file mode 100644 index 0000000..e8cc459 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/core/util/ExcelUtils.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.framework.excel.core.util; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.List; + +/** + * Excel 工具类 + * + * @author 芋道源码 + */ +public class ExcelUtils { + + /** + * 将列表以 Excel 响应给前端 + * + * @param response 响应 + * @param filename 文件名 + * @param sheetName Excel sheet 名 + * @param head Excel head 头 + * @param data 数据列表哦 + * @param 泛型,保证 head 和 data 类型的一致性 + * @throws IOException 写入失败的情况 + */ + public static void write(HttpServletResponse response, String filename, String sheetName, + Class head, List data) throws IOException { + // 输出 Excel + EasyExcel.write(response.getOutputStream(), head) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .sheet(sheetName).doWrite(data); + // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 + response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + response.setContentType("application/vnd.ms-excel;charset=UTF-8"); + } + + public static List read(MultipartFile file, Class head) throws IOException { + return EasyExcel.read(file.getInputStream(), head, null) + .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 + .doReadAllSync(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/package-info.java new file mode 100644 index 0000000..d4166ad --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-excel/src/main/java/com/yunxi/scm/framework/excel/package-info.java @@ -0,0 +1,4 @@ +/** + * 基于 EasyExcel 实现 Excel 相关的操作 + */ +package com.yunxi.scm.framework.excel; diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-file/pom.xml new file mode 100644 index 0000000..222aff2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/pom.xml @@ -0,0 +1,83 @@ + + + + yunxi-framework + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-file + + ${project.artifactId} + 文件客户端,支持多种存储器 + 1. file:本地磁盘 + 2. ftp:FTP 服务器 + 2. sftp:SFTP 服务器 + 4. db:数据库 + 5. s3:支持 S3 协议的云存储服务,例如说 MinIO、阿里云、华为云、腾讯云、七牛云等等 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter + + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.slf4j + slf4j-api + + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-core + + + + commons-net + commons-net + + + com.jcraft + jsch + + + + org.apache.tika + tika-core + + + + + io.minio + minio + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/config/YunxiFileAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/config/YunxiFileAutoConfiguration.java new file mode 100644 index 0000000..22b2446 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/config/YunxiFileAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.file.config; + +import com.yunxi.scm.framework.file.core.client.FileClientFactory; +import com.yunxi.scm.framework.file.core.client.FileClientFactoryImpl; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * 文件配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +public class YunxiFileAutoConfiguration { + + @Bean + public FileClientFactory fileClientFactory() { + return new FileClientFactoryImpl(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/AbstractFileClient.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/AbstractFileClient.java new file mode 100644 index 0000000..0be1a30 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/AbstractFileClient.java @@ -0,0 +1,69 @@ +package com.yunxi.scm.framework.file.core.client; + +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; + +/** + * 文件客户端的抽象类,提供模板方法,减少子类的冗余代码 + * + * @author 芋道源码 + */ +@Slf4j +public abstract class AbstractFileClient implements FileClient { + + /** + * 配置编号 + */ + private final Long id; + /** + * 文件配置 + */ + protected Config config; + + public AbstractFileClient(Long id, Config config) { + this.id = id; + this.config = config; + } + + /** + * 初始化 + */ + public final void init() { + doInit(); + log.info("[init][配置({}) 初始化完成]", config); + } + + /** + * 自定义初始化 + */ + protected abstract void doInit(); + + public final void refresh(Config config) { + // 判断是否更新 + if (config.equals(this.config)) { + return; + } + log.info("[refresh][配置({})发生变化,重新初始化]", config); + this.config = config; + // 初始化 + this.init(); + } + + @Override + public Long getId() { + return id; + } + + /** + * 格式化文件的 URL 访问地址 + * 使用场景:local、ftp、db,通过 FileController 的 getFile 来获取文件内容 + * + * @param domain 自定义域名 + * @param path 文件路径 + * @return URL 访问地址 + */ + protected String formatFileUrl(String domain, String path) { + return StrUtil.format("{}/admin-api/infra/file/{}/get/{}", domain, getId(), path); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClient.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClient.java new file mode 100644 index 0000000..4ad9d16 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClient.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.file.core.client; + +/** + * 文件客户端 + * + * @author 芋道源码 + */ +public interface FileClient { + + /** + * 获得客户端编号 + * + * @return 客户端编号 + */ + Long getId(); + + /** + * 上传文件 + * + * @param content 文件流 + * @param path 相对路径 + * @return 完整路径,即 HTTP 访问地址 + * @throws Exception 上传文件时,抛出 Exception 异常 + */ + String upload(byte[] content, String path, String type) throws Exception; + + /** + * 删除文件 + * + * @param path 相对路径 + * @throws Exception 删除文件时,抛出 Exception 异常 + */ + void delete(String path) throws Exception; + + /** + * 获得文件的内容 + * + * @param path 相对路径 + * @return 文件的内容 + */ + byte[] getContent(String path) throws Exception; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientConfig.java new file mode 100644 index 0000000..ceb1d0f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientConfig.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.framework.file.core.client; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +/** + * 文件客户端的配置 + * 不同实现的客户端,需要不同的配置,通过子类来定义 + * + * @author 芋道源码 + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) +// @JsonTypeInfo 注解的作用,Jackson 多态 +// 1. 序列化到时数据库时,增加 @class 属性。 +// 2. 反序列化到内存对象时,通过 @class 属性,可以创建出正确的类型 +public interface FileClientConfig { +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientFactory.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientFactory.java new file mode 100644 index 0000000..5484eab --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientFactory.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.framework.file.core.client; + +public interface FileClientFactory { + + /** + * 获得文件客户端 + * + * @param configId 配置编号 + * @return 文件客户端 + */ + FileClient getFileClient(Long configId); + + /** + * 创建文件客户端 + * + * @param configId 配置编号 + * @param storage 存储器的枚举 {@link com.yunxi.scm.framework.file.core.enums.FileStorageEnum} + * @param config 文件配置 + */ + void createOrUpdateFileClient(Long configId, Integer storage, Config config); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientFactoryImpl.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientFactoryImpl.java new file mode 100644 index 0000000..e8a2b4a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/FileClientFactoryImpl.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.framework.file.core.client; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReflectUtil; +import com.yunxi.scm.framework.file.core.enums.FileStorageEnum; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +/** + * 文件客户端的工厂实现类 + * + * @author 芋道源码 + */ +@Slf4j +public class FileClientFactoryImpl implements FileClientFactory { + + /** + * 文件客户端 Map + * key:配置编号 + */ + private final ConcurrentMap> clients = new ConcurrentHashMap<>(); + + @Override + public FileClient getFileClient(Long configId) { + AbstractFileClient client = clients.get(configId); + if (client == null) { + log.error("[getFileClient][配置编号({}) 找不到客户端]", configId); + } + return client; + } + + @Override + @SuppressWarnings("unchecked") + public void createOrUpdateFileClient(Long configId, Integer storage, Config config) { + AbstractFileClient client = (AbstractFileClient) clients.get(configId); + if (client == null) { + client = this.createFileClient(configId, storage, config); + client.init(); + clients.put(client.getId(), client); + } else { + client.refresh(config); + } + } + + @SuppressWarnings("unchecked") + private AbstractFileClient createFileClient( + Long configId, Integer storage, Config config) { + FileStorageEnum storageEnum = FileStorageEnum.getByStorage(storage); + Assert.notNull(storageEnum, String.format("文件配置(%s) 为空", storageEnum)); + // 创建客户端 + return (AbstractFileClient) ReflectUtil.newInstance(storageEnum.getClientClass(), configId, config); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileClient.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileClient.java new file mode 100644 index 0000000..842e6a6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileClient.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.framework.file.core.client.db; + +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.file.core.client.AbstractFileClient; + +/** + * 基于 DB 存储的文件客户端的配置类 + * + * @author 芋道源码 + */ +public class DBFileClient extends AbstractFileClient { + + private DBFileContentFrameworkDAO dao; + + public DBFileClient(Long id, DBFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + } + + @Override + public String upload(byte[] content, String path, String type) { + getDao().insert(getId(), path, content); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + getDao().delete(getId(), path); + } + + @Override + public byte[] getContent(String path) { + return getDao().selectContent(getId(), path); + } + + private DBFileContentFrameworkDAO getDao() { + // 延迟获取,因为 SpringUtil 初始化太慢 + if (dao == null) { + dao = SpringUtil.getBean(DBFileContentFrameworkDAO.class); + } + return dao; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileClientConfig.java new file mode 100644 index 0000000..45443f1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileClientConfig.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.framework.file.core.client.db; + +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; + +/** + * 基于 DB 存储的文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class DBFileClientConfig implements FileClientConfig { + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileContentFrameworkDAO.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileContentFrameworkDAO.java new file mode 100644 index 0000000..cb3b322 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/db/DBFileContentFrameworkDAO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.file.core.client.db; + +/** + * 文件内容 Framework DAO 接口 + * + * @author 芋道源码 + */ +public interface DBFileContentFrameworkDAO { + + /** + * 插入文件内容 + * + * @param configId 配置编号 + * @param path 路径 + * @param content 内容 + */ + void insert(Long configId, String path, byte[] content); + + /** + * 删除文件内容 + * + * @param configId 配置编号 + * @param path 路径 + */ + void delete(Long configId, String path); + + /** + * 获得文件内容 + * + * @param configId 配置编号 + * @param path 路径 + * @return 内容 + */ + byte[] selectContent(Long configId, String path); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClient.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClient.java new file mode 100644 index 0000000..d7d1e7d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClient.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.framework.file.core.client.ftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.ftp.Ftp; +import cn.hutool.extra.ftp.FtpException; +import cn.hutool.extra.ftp.FtpMode; +import com.yunxi.scm.framework.file.core.client.AbstractFileClient; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; + +/** + * Ftp 文件客户端 + * + * @author 芋道源码 + */ +public class FtpFileClient extends AbstractFileClient { + + private Ftp ftp; + + public FtpFileClient(Long id, FtpFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 把配置的 \ 替换成 /, 如果路径配置 \a\test, 替换成 /a/test, 替换方法已经处理 null 情况 + config.setBasePath(StrUtil.replace(config.getBasePath(), StrUtil.BACKSLASH, StrUtil.SLASH)); + // ftp的路径是 / 结尾 + if (!config.getBasePath().endsWith(StrUtil.SLASH)) { + config.setBasePath(config.getBasePath() + StrUtil.SLASH); + } + // 初始化 Ftp 对象 + this.ftp = new Ftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword(), + CharsetUtil.CHARSET_UTF_8, null, null, FtpMode.valueOf(config.getMode())); + } + + @Override + public String upload(byte[] content, String path, String type) { + // 执行写入 + String filePath = getFilePath(path); + String fileName = FileUtil.getName(filePath); + String dir = StrUtil.removeSuffix(filePath, fileName); + ftp.reconnectIfTimeout(); + boolean success = ftp.upload(dir, fileName, new ByteArrayInputStream(content)); + if (!success) { + throw new FtpException(StrUtil.format("上传文件到目标目录 ({}) 失败", filePath)); + } + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + ftp.reconnectIfTimeout(); + ftp.delFile(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + String fileName = FileUtil.getName(filePath); + String dir = StrUtil.removeSuffix(filePath, fileName); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ftp.reconnectIfTimeout(); + ftp.download(dir, fileName, out); + return out.toByteArray(); + } + + private String getFilePath(String path) { + return config.getBasePath() + path; + } + +} \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClientConfig.java new file mode 100644 index 0000000..d10bb90 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClientConfig.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.framework.file.core.client.ftp; + +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * Ftp 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class FtpFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + + /** + * 主机地址 + */ + @NotEmpty(message = "host 不能为空") + private String host; + /** + * 主机端口 + */ + @NotNull(message = "port 不能为空") + private Integer port; + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + private String username; + /** + * 密码 + */ + @NotEmpty(message = "密码不能为空") + private String password; + /** + * 连接模式 + * + * 使用 {@link cn.hutool.extra.ftp.FtpMode} 对应的字符串 + */ + @NotEmpty(message = "连接模式不能为空") + private String mode; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClient.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClient.java new file mode 100644 index 0000000..9a7763c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClient.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.framework.file.core.client.local; + +import cn.hutool.core.io.FileUtil; +import com.yunxi.scm.framework.file.core.client.AbstractFileClient; + +import java.io.File; + +/** + * 本地文件客户端 + * + * @author 芋道源码 + */ +public class LocalFileClient extends AbstractFileClient { + + public LocalFileClient(Long id, LocalFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 补全风格。例如说 Linux 是 /,Windows 是 \ + if (!config.getBasePath().endsWith(File.separator)) { + config.setBasePath(config.getBasePath() + File.separator); + } + } + + @Override + public String upload(byte[] content, String path, String type) { + // 执行写入 + String filePath = getFilePath(path); + FileUtil.writeBytes(content, filePath); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + FileUtil.del(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + return FileUtil.readBytes(filePath); + } + + private String getFilePath(String path) { + return config.getBasePath() + path; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClientConfig.java new file mode 100644 index 0000000..6c6b274 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClientConfig.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.framework.file.core.client.local; + +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; + +/** + * 本地文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class LocalFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClient.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClient.java new file mode 100644 index 0000000..4afcb93 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClient.java @@ -0,0 +1,120 @@ +package com.yunxi.scm.framework.file.core.client.s3; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.http.HttpUtil; +import com.yunxi.scm.framework.file.core.client.AbstractFileClient; +import io.minio.*; + +import java.io.ByteArrayInputStream; + +import static com.yunxi.scm.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_ALIYUN; +import static com.yunxi.scm.framework.file.core.client.s3.S3FileClientConfig.ENDPOINT_TENCENT; + +/** + * 基于 S3 协议的文件客户端,实现 MinIO、阿里云、腾讯云、七牛云、华为云等云服务 + *

+ * S3 协议的客户端,采用亚马逊提供的 software.amazon.awssdk.s3 库 + * + * @author 芋道源码 + */ +public class S3FileClient extends AbstractFileClient { + + private MinioClient client; + + public S3FileClient(Long id, S3FileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 补全 domain + if (StrUtil.isEmpty(config.getDomain())) { + config.setDomain(buildDomain()); + } + // 初始化客户端 + client = MinioClient.builder() + .endpoint(buildEndpointURL()) // Endpoint URL + .region(buildRegion()) // Region + .credentials(config.getAccessKey(), config.getAccessSecret()) // 认证密钥 + .build(); + } + + /** + * 基于 endpoint 构建调用云服务的 URL 地址 + * + * @return URI 地址 + */ + private String buildEndpointURL() { + // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO + if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { + return config.getEndpoint(); + } + return StrUtil.format("https://{}", config.getEndpoint()); + } + + /** + * 基于 bucket + endpoint 构建访问的 Domain 地址 + * + * @return Domain 地址 + */ + private String buildDomain() { + // 如果已经是 http 或者 https,则不进行拼接.主要适配 MinIO + if (HttpUtil.isHttp(config.getEndpoint()) || HttpUtil.isHttps(config.getEndpoint())) { + return StrUtil.format("{}/{}", config.getEndpoint(), config.getBucket()); + } + // 阿里云、腾讯云、华为云都适合。七牛云比较特殊,必须有自定义域名 + return StrUtil.format("https://{}.{}", config.getBucket(), config.getEndpoint()); + } + + /** + * 基于 bucket 构建 region 地区 + * + * @return region 地区 + */ + private String buildRegion() { + // 阿里云必须有 region,否则会报错 + if (config.getEndpoint().contains(ENDPOINT_ALIYUN)) { + return StrUtil.subBefore(config.getEndpoint(), '.', false) + .replaceAll("-internal", "")// 去除内网 Endpoint 的后缀 + .replaceAll("https://", ""); + } + // 腾讯云必须有 region,否则会报错 + if (config.getEndpoint().contains(ENDPOINT_TENCENT)) { + return StrUtil.subAfter(config.getEndpoint(), ".cos.", false) + .replaceAll("." + ENDPOINT_TENCENT, ""); // 去除 Endpoint + } + return null; + } + + @Override + public String upload(byte[] content, String path, String type) throws Exception { + // 执行上传 + client.putObject(PutObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .contentType(type) + .object(path) // 相对路径作为 key + .stream(new ByteArrayInputStream(content), content.length, -1) // 文件内容 + .build()); + // 拼接返回路径 + return config.getDomain() + "/" + path; + } + + @Override + public void delete(String path) throws Exception { + client.removeObject(RemoveObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .object(path) // 相对路径作为 key + .build()); + } + + @Override + public byte[] getContent(String path) throws Exception { + GetObjectResponse response = client.getObject(GetObjectArgs.builder() + .bucket(config.getBucket()) // bucket 必须传递 + .object(path) // 相对路径作为 key + .build()); + return IoUtil.readBytes(response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClientConfig.java new file mode 100644 index 0000000..4d513bd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClientConfig.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.framework.file.core.client.s3; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; + +/** + * S3 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class S3FileClientConfig implements FileClientConfig { + + public static final String ENDPOINT_QINIU = "qiniucs.com"; + public static final String ENDPOINT_ALIYUN = "aliyuncs.com"; + public static final String ENDPOINT_TENCENT = "myqcloud.com"; + + /** + * 节点地址 + * 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO 。例如说,http://127.0.0.1:9000 + * 2. 阿里云:https://help.aliyun.com/document_detail/31837.html + * 3. 腾讯云:https://cloud.tencent.com/document/product/436/6224 + * 4. 七牛云:https://developer.qiniu.com/kodo/4088/s3-access-domainname + * 5. 华为云:https://developer.huaweicloud.com/endpoint?OBS + */ + @NotNull(message = "endpoint 不能为空") + private String endpoint; + /** + * 自定义域名 + * 1. MinIO:通过 Nginx 配置 + * 2. 阿里云:https://help.aliyun.com/document_detail/31836.html + * 3. 腾讯云:https://cloud.tencent.com/document/product/436/11142 + * 4. 七牛云:https://developer.qiniu.com/kodo/8556/set-the-custom-source-domain-name + * 5. 华为云:https://support.huaweicloud.com/usermanual-obs/obs_03_0032.html + */ + @URL(message = "domain 必须是 URL 格式") + private String domain; + /** + * 存储 Bucket + */ + @NotNull(message = "bucket 不能为空") + private String bucket; + + /** + * 访问 Key + * 1. MinIO:https://www.iocoder.cn/Spring-Boot/MinIO + * 2. 阿里云:https://ram.console.aliyun.com/manage/ak + * 3. 腾讯云:https://console.cloud.tencent.com/cam/capi + * 4. 七牛云:https://portal.qiniu.com/user/key + * 5. 华为云:https://support.huaweicloud.com/qs-obs/obs_qs_0005.html + */ + @NotNull(message = "accessKey 不能为空") + private String accessKey; + /** + * 访问 Secret + */ + @NotNull(message = "accessSecret 不能为空") + private String accessSecret; + + @SuppressWarnings("RedundantIfStatement") + @AssertTrue(message = "domain 不能为空") + @JsonIgnore + public boolean isDomainValid() { + // 如果是七牛,必须带有 domain + if (StrUtil.contains(endpoint, ENDPOINT_QINIU) && StrUtil.isEmpty(domain)) { + return false; + } + return true; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClient.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClient.java new file mode 100644 index 0000000..264cd23 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClient.java @@ -0,0 +1,61 @@ +package com.yunxi.scm.framework.file.core.client.sftp; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.extra.ssh.Sftp; +import com.yunxi.scm.framework.common.util.io.FileUtils; +import com.yunxi.scm.framework.file.core.client.AbstractFileClient; + +import java.io.File; + +/** + * Sftp 文件客户端 + * + * @author 芋道源码 + */ +public class SftpFileClient extends AbstractFileClient { + + private Sftp sftp; + + public SftpFileClient(Long id, SftpFileClientConfig config) { + super(id, config); + } + + @Override + protected void doInit() { + // 补全风格。例如说 Linux 是 /,Windows 是 \ + if (!config.getBasePath().endsWith(File.separator)) { + config.setBasePath(config.getBasePath() + File.separator); + } + // 初始化 Ftp 对象 + this.sftp = new Sftp(config.getHost(), config.getPort(), config.getUsername(), config.getPassword()); + } + + @Override + public String upload(byte[] content, String path, String type) { + // 执行写入 + String filePath = getFilePath(path); + File file = FileUtils.createTempFile(content); + sftp.upload(filePath, file); + // 拼接返回路径 + return super.formatFileUrl(config.getDomain(), path); + } + + @Override + public void delete(String path) { + String filePath = getFilePath(path); + sftp.delFile(filePath); + } + + @Override + public byte[] getContent(String path) { + String filePath = getFilePath(path); + File destFile = FileUtils.createTempFile(); + sftp.download(filePath, destFile); + return FileUtil.readBytes(destFile); + } + + private String getFilePath(String path) { + return config.getBasePath() + path; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClientConfig.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClientConfig.java new file mode 100644 index 0000000..a5548a6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClientConfig.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.framework.file.core.client.sftp; + +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * Sftp 文件客户端的配置类 + * + * @author 芋道源码 + */ +@Data +public class SftpFileClientConfig implements FileClientConfig { + + /** + * 基础路径 + */ + @NotEmpty(message = "基础路径不能为空") + private String basePath; + + /** + * 自定义域名 + */ + @NotEmpty(message = "domain 不能为空") + @URL(message = "domain 必须是 URL 格式") + private String domain; + + /** + * 主机地址 + */ + @NotEmpty(message = "host 不能为空") + private String host; + /** + * 主机端口 + */ + @NotNull(message = "port 不能为空") + private Integer port; + /** + * 用户名 + */ + @NotEmpty(message = "用户名不能为空") + private String username; + /** + * 密码 + */ + @NotEmpty(message = "密码不能为空") + private String password; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/enums/FileStorageEnum.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/enums/FileStorageEnum.java new file mode 100644 index 0000000..d127475 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/enums/FileStorageEnum.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.framework.file.core.enums; + +import cn.hutool.core.util.ArrayUtil; +import com.yunxi.scm.framework.file.core.client.FileClient; +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import com.yunxi.scm.framework.file.core.client.db.DBFileClient; +import com.yunxi.scm.framework.file.core.client.db.DBFileClientConfig; +import com.yunxi.scm.framework.file.core.client.ftp.FtpFileClient; +import com.yunxi.scm.framework.file.core.client.ftp.FtpFileClientConfig; +import com.yunxi.scm.framework.file.core.client.local.LocalFileClient; +import com.yunxi.scm.framework.file.core.client.local.LocalFileClientConfig; +import com.yunxi.scm.framework.file.core.client.s3.S3FileClient; +import com.yunxi.scm.framework.file.core.client.s3.S3FileClientConfig; +import com.yunxi.scm.framework.file.core.client.sftp.SftpFileClient; +import com.yunxi.scm.framework.file.core.client.sftp.SftpFileClientConfig; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 文件存储器枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum FileStorageEnum { + + DB(1, DBFileClientConfig.class, DBFileClient.class), + + LOCAL(10, LocalFileClientConfig.class, LocalFileClient.class), + FTP(11, FtpFileClientConfig.class, FtpFileClient.class), + SFTP(12, SftpFileClientConfig.class, SftpFileClient.class), + + S3(20, S3FileClientConfig.class, S3FileClient.class), + ; + + /** + * 存储器 + */ + private final Integer storage; + + /** + * 配置类 + */ + private final Class configClass; + /** + * 客户端类 + */ + private final Class clientClass; + + public static FileStorageEnum getByStorage(Integer storage) { + return ArrayUtil.firstMatch(o -> o.getStorage().equals(storage), values()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/utils/FileTypeUtils.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/utils/FileTypeUtils.java new file mode 100644 index 0000000..2b0f7f8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/java/com/yunxi/scm/framework/file/core/utils/FileTypeUtils.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.framework.file.core.utils; + +import com.alibaba.ttl.TransmittableThreadLocal; +import lombok.SneakyThrows; +import org.apache.tika.Tika; + +/** + * 文件类型 Utils + * + * @author 芋道源码 + */ +public class FileTypeUtils { + + private static final ThreadLocal TIKA = TransmittableThreadLocal.withInitial(Tika::new); + + /** + * 获得文件的 mineType,对于doc,jar等文件会有误差 + * + * @param data 文件内容 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + @SneakyThrows + public static String getMineType(byte[] data) { + return TIKA.get().detect(data); + } + + /** + * 已知文件名,获取文件类型,在某些情况下比通过字节数组准确,例如使用jar文件时,通过名字更为准确 + * + * @param name 文件名 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + public static String getMineType(String name) { + return TIKA.get().detect(name); + } + + /** + * 在拥有文件和数据的情况下,最好使用此方法,最为准确 + * + * @param data 文件内容 + * @param name 文件名 + * @return mineType 无法识别时会返回“application/octet-stream” + */ + public static String getMineType(byte[] data, String name) { + return TIKA.get().detect(data, name); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..09c82eb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.file.config.YunxiFileAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/config/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/config/package-info.java new file mode 100644 index 0000000..d50ef65 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/config/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,避免 package 无法提交到 Git 仓库 + */ +package com.yunxi.scm.framework.file.config; diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClientTest.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClientTest.java new file mode 100644 index 0000000..eb9637c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/ftp/FtpFileClientTest.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.framework.file.core.client.ftp; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.extra.ftp.FtpMode; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class FtpFileClientTest { + + @Test + @Disabled + public void test() { + // 创建客户端 + FtpFileClientConfig config = new FtpFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath("/home/ftp"); + config.setHost("kanchai.club"); + config.setPort(221); + config.setUsername(""); + config.setPassword(""); + config.setMode(FtpMode.Passive.name()); + FtpFileClient client = new FtpFileClient(0L, config); + client.init(); + // 上传文件 + String path = IdUtil.fastSimpleUUID() + ".jpg"; + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String fullPath = client.upload(content, path, "image/jpeg"); + System.out.println("访问地址:" + fullPath); + if (false) { + byte[] bytes = client.getContent(path); + System.out.println("文件内容:" + bytes); + } + if (false) { + client.delete(path); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClientTest.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClientTest.java new file mode 100644 index 0000000..9aab9ce --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/local/LocalFileClientTest.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.framework.file.core.client.local; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class LocalFileClientTest { + + @Test + @Disabled + public void test() { + // 创建客户端 + LocalFileClientConfig config = new LocalFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath("/Users/yunai/file_test"); + LocalFileClient client = new LocalFileClient(0L, config); + client.init(); + // 上传文件 + String path = IdUtil.fastSimpleUUID() + ".jpg"; + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String fullPath = client.upload(content, path, "image/jpeg"); + System.out.println("访问地址:" + fullPath); + client.delete(path); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClientTest.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClientTest.java new file mode 100644 index 0000000..20c28bc --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/s3/S3FileClientTest.java @@ -0,0 +1,117 @@ +package com.yunxi.scm.framework.file.core.client.s3; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import javax.validation.Validation; + +public class S3FileClientTest { + + @Test + @Disabled // MinIO,如果要集成测试,可以注释本行 + public void testMinIO() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 + config.setAccessKey("admin"); + config.setAccessSecret("password"); + config.setBucket("yunxiyuanma"); + config.setDomain(null); + // 默认 9000 endpoint + config.setEndpoint("http://127.0.0.1:9000"); + + // 执行上传 + testExecuteUpload(config); + } + + @Test + @Disabled // 阿里云 OSS,如果要集成测试,可以注释本行 + public void testAliyun() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 + config.setAccessKey(System.getenv("ALIYUN_ACCESS_KEY")); + config.setAccessSecret(System.getenv("ALIYUN_SECRET_KEY")); + config.setBucket("yunai-aoteman"); + config.setDomain(null); // 如果有自定义域名,则可以设置。http://ali-oss.iocoder.cn + // 默认北京的 endpoint + config.setEndpoint("oss-cn-beijing.aliyuncs.com"); + + // 执行上传 + testExecuteUpload(config); + } + + @Test + @Disabled // 腾讯云 COS,如果要集成测试,可以注释本行 + public void testQCloud() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 + config.setAccessKey(System.getenv("QCLOUD_ACCESS_KEY")); + config.setAccessSecret(System.getenv("QCLOUD_SECRET_KEY")); + config.setBucket("aoteman-1255880240"); + config.setDomain(null); // 如果有自定义域名,则可以设置。http://tengxun-oss.iocoder.cn + // 默认上海的 endpoint + config.setEndpoint("cos.ap-shanghai.myqcloud.com"); + + // 执行上传 + testExecuteUpload(config); + } + + @Test + @Disabled // 七牛云存储,如果要集成测试,可以注释本行 + public void testQiniu() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 +// config.setAccessKey(System.getenv("QINIU_ACCESS_KEY")); +// config.setAccessSecret(System.getenv("QINIU_SECRET_KEY")); + config.setAccessKey("b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8"); + config.setAccessSecret("kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP"); + config.setBucket("ruoyi-vue-pro"); + config.setDomain("http://test.yunxi.iocoder.cn"); // 如果有自定义域名,则可以设置。http://static.yunxi.iocoder.cn + // 默认上海的 endpoint + config.setEndpoint("s3-cn-south-1.qiniucs.com"); + + // 执行上传 + testExecuteUpload(config); + } + + @Test + @Disabled // 华为云存储,如果要集成测试,可以注释本行 + public void testHuaweiCloud() throws Exception { + S3FileClientConfig config = new S3FileClientConfig(); + // 配置成你自己的 +// config.setAccessKey(System.getenv("HUAWEI_CLOUD_ACCESS_KEY")); +// config.setAccessSecret(System.getenv("HUAWEI_CLOUD_SECRET_KEY")); + config.setBucket("yunxi"); + config.setDomain(null); // 如果有自定义域名,则可以设置。 + // 默认上海的 endpoint + config.setEndpoint("obs.cn-east-3.myhuaweicloud.com"); + + // 执行上传 + testExecuteUpload(config); + } + + private void testExecuteUpload(S3FileClientConfig config) throws Exception { + // 校验配置 + ValidationUtils.validate(Validation.buildDefaultValidatorFactory().getValidator(), config); + // 创建 Client + S3FileClient client = new S3FileClient(0L, config); + client.init(); + // 上传文件 + String path = IdUtil.fastSimpleUUID() + ".jpg"; + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String fullPath = client.upload(content, path, "image/jpeg"); + System.out.println("访问地址:" + fullPath); + // 读取文件 + if (true) { + byte[] bytes = client.getContent(path); + System.out.println("文件内容:" + bytes.length); + } + // 删除文件 + if (false) { + client.delete(path); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClientTest.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClientTest.java new file mode 100644 index 0000000..5421b35 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/client/sftp/SftpFileClientTest.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.framework.file.core.client.sftp; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +public class SftpFileClientTest { + + @Test + @Disabled + public void test() { + // 创建客户端 + SftpFileClientConfig config = new SftpFileClientConfig(); + config.setDomain("http://127.0.0.1:48080"); + config.setBasePath("/home/ftp"); + config.setHost("kanchai.club"); + config.setPort(222); + config.setUsername(""); + config.setPassword(""); + SftpFileClient client = new SftpFileClient(0L, config); + client.init(); + // 上传文件 + String path = IdUtil.fastSimpleUUID() + ".jpg"; + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String fullPath = client.upload(content, path, "image/jpeg"); + System.out.println("访问地址:" + fullPath); + if (false) { + byte[] bytes = client.getContent(path); + System.out.println("文件内容:" + bytes); + } + if (false) { + client.delete(path); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/enums/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/enums/package-info.java new file mode 100644 index 0000000..4b41b52 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/java/com/yunxi/scm/framework/file/core/enums/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,避免 package 无法提交到 Git 仓库 + */ +package com.yunxi.scm.framework.file.core.enums; diff --git a/yunxi-framework/yunxi-spring-boot-starter-file/src/test/resources/file/erweima.jpg b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/resources/file/erweima.jpg new file mode 100644 index 0000000..1447283 Binary files /dev/null and b/yunxi-framework/yunxi-spring-boot-starter-file/src/test/resources/file/erweima.jpg differ diff --git a/yunxi-framework/yunxi-spring-boot-starter-flowable/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-flowable/pom.xml new file mode 100644 index 0000000..5b6e570 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-flowable/pom.xml @@ -0,0 +1,37 @@ + + + + yunxi-framework + com.yunxi.scm + ${revision} + + 4.0.0 + + yunxi-spring-boot-starter-flowable + + + + com.yunxi.scm + yunxi-common + + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + org.flowable + flowable-spring-boot-starter-process + + + org.flowable + flowable-spring-boot-starter-actuator + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/config/YunxiFlowableConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/config/YunxiFlowableConfiguration.java new file mode 100644 index 0000000..41308d1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/config/YunxiFlowableConfiguration.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.flowable.config; + +import com.yunxi.scm.framework.common.enums.WebFilterOrderEnum; +import com.yunxi.scm.framework.flowable.core.web.FlowableWebFilter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@AutoConfiguration +public class YunxiFlowableConfiguration { + + /** + * 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean + * + * 如果不创建,会导致项目启动时,Flowable 报错的问题 + */ + @Bean + public AsyncListenableTaskExecutor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); + executor.setMaxPoolSize(8); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("flowable-task-Executor-"); + executor.setAwaitTerminationSeconds(30); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAllowCoreThreadTimeOut(true); + executor.initialize(); + return executor; + } + + /** + * 配置 flowable Web 过滤器 + */ + @Bean + public FilterRegistrationBean flowableWebFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new FlowableWebFilter()); + registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER); + return registrationBean; + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/package-info.java new file mode 100644 index 0000000..0a19675 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.framework.flowable.core; diff --git a/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/util/FlowableUtils.java b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/util/FlowableUtils.java new file mode 100644 index 0000000..9f7526b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/util/FlowableUtils.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.framework.flowable.core.util; + +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.common.engine.impl.identity.Authentication; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Flowable 相关的工具方法 + * + * @author 芋道源码 + */ +public class FlowableUtils { + + // ========== User 相关的工具方法 ========== + + public static void setAuthenticatedUserId(Long userId) { + Authentication.setAuthenticatedUserId(String.valueOf(userId)); + } + + public static void clearAuthenticatedUserId() { + Authentication.setAuthenticatedUserId(null); + } + + // ========== BPMN 相关的工具方法 ========== + + /** + * 获得 BPMN 流程中,指定的元素们 + * + * @param model + * @param clazz 指定元素。例如说,{@link org.flowable.bpmn.model.UserTask}、{@link org.flowable.bpmn.model.Gateway} 等等 + * @return 元素们 + */ + public static List getBpmnModelElements(BpmnModel model, Class clazz) { + List result = new ArrayList<>(); + model.getProcesses().forEach(process -> { + process.getFlowElements().forEach(flowElement -> { + if (flowElement.getClass().isAssignableFrom(clazz)) { + result.add((T) flowElement); + } + }); + }); + return result; + } + + /** + * 比较 两个bpmnModel 是否相同 + * @param oldModel 老的bpmn model + * @param newModel 新的bpmn model + */ + public static boolean equals(BpmnModel oldModel, BpmnModel newModel) { + // 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较 + return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel)); + } + + /** + * 把 bpmnModel 转换成 byte[] + * @param model bpmnModel + */ + public static byte[] getBpmnBytes(BpmnModel model) { + if (model == null) { + return new byte[0]; + } + BpmnXMLConverter converter = new BpmnXMLConverter(); + return converter.convertToXML(model); + } + + // ========== Execution 相关的工具方法 ========== + + public static String formatCollectionVariable(String activityId) { + return activityId + "_assignees"; + } + + public static String formatCollectionElementVariable(String activityId) { + return activityId + "_assignee"; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/web/FlowableWebFilter.java b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/web/FlowableWebFilter.java new file mode 100644 index 0000000..2dcfa48 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/core/web/FlowableWebFilter.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.framework.flowable.core.web; + +import com.yunxi.scm.framework.flowable.core.util.FlowableUtils; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +/** + * flowable Web 过滤器,将 userId 设置到 {@link org.flowable.common.engine.impl.identity.Authentication} 中 + * + * @author jason + */ +public class FlowableWebFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + try { + // 设置工作流的用户 + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (userId != null) { + FlowableUtils.setAuthenticatedUserId(userId); + } + // 过滤 + chain.doFilter(request, response); + } finally { + // 清理 + FlowableUtils.clearAuthenticatedUserId(); + } + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/package-info.java new file mode 100644 index 0000000..576fb5e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/java/com/yunxi/scm/framework/flowable/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.framework.flowable; diff --git a/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..b25bc32 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.flowable.config.YunxiFlowableConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-job/pom.xml new file mode 100644 index 0000000..e001e31 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/pom.xml @@ -0,0 +1,41 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-job + jar + + ${project.artifactId} + 任务拓展 + 1. 定时任务,基于 Quartz 拓展 + 2. 异步任务,基于 Spring Async 拓展 + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-quartz + + + + + jakarta.validation + jakarta.validation-api + + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/config/YunxiAsyncAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/config/YunxiAsyncAutoConfiguration.java new file mode 100644 index 0000000..773e492 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/config/YunxiAsyncAutoConfiguration.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.quartz.config; + +import com.alibaba.ttl.TtlRunnable; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +/** + * 异步任务 Configuration + */ +@AutoConfiguration +@EnableAsync +public class YunxiAsyncAutoConfiguration { + + @Bean + public BeanPostProcessor threadPoolTaskExecutorBeanPostProcessor() { + return new BeanPostProcessor() { + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof ThreadPoolTaskExecutor)) { + return bean; + } + // 修改提交的任务,接入 TransmittableThreadLocal + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; + } + + }; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/config/YunxiQuartzAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/config/YunxiQuartzAutoConfiguration.java new file mode 100644 index 0000000..70015a8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/config/YunxiQuartzAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.quartz.config; + +import com.yunxi.scm.framework.quartz.core.scheduler.SchedulerManager; +import org.quartz.Scheduler; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * 定时任务 Configuration + */ +@AutoConfiguration +@EnableScheduling // 开启 Spring 自带的定时任务 +public class YunxiQuartzAutoConfiguration { + + @Bean + public SchedulerManager schedulerManager(Scheduler scheduler) { + return new SchedulerManager(scheduler); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/enums/JobDataKeyEnum.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/enums/JobDataKeyEnum.java new file mode 100644 index 0000000..2c10bc0 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/enums/JobDataKeyEnum.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.framework.quartz.core.enums; + +/** + * Quartz Job Data 的 key 枚举 + */ +public enum JobDataKeyEnum { + + JOB_ID, + JOB_HANDLER_NAME, + JOB_HANDLER_PARAM, + JOB_RETRY_COUNT, // 最大重试次数 + JOB_RETRY_INTERVAL, // 每次重试间隔 + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/handler/JobHandler.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/handler/JobHandler.java new file mode 100644 index 0000000..4ad74f8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/handler/JobHandler.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.framework.quartz.core.handler; + +/** + * 任务处理器 + * + * @author 芋道源码 + */ +public interface JobHandler { + + /** + * 执行任务 + * + * @param param 参数 + * @return 结果 + * @throws Exception 异常 + */ + String execute(String param) throws Exception; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/handler/JobHandlerInvoker.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/handler/JobHandlerInvoker.java new file mode 100644 index 0000000..dd14291 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/handler/JobHandlerInvoker.java @@ -0,0 +1,114 @@ +package com.yunxi.scm.framework.quartz.core.handler; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.thread.ThreadUtil; +import com.yunxi.scm.framework.quartz.core.enums.JobDataKeyEnum; +import com.yunxi.scm.framework.quartz.core.service.JobLogFrameworkService; +import lombok.extern.slf4j.Slf4j; +import org.quartz.DisallowConcurrentExecution; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.PersistJobDataAfterExecution; +import org.springframework.context.ApplicationContext; +import org.springframework.scheduling.quartz.QuartzJobBean; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; + +import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; + +/** + * 基础 Job 调用者,负责调用 {@link JobHandler#execute(String)} 执行任务 + * + * @author 芋道源码 + */ +@DisallowConcurrentExecution +@PersistJobDataAfterExecution +@Slf4j +public class JobHandlerInvoker extends QuartzJobBean { + + @Resource + private ApplicationContext applicationContext; + + @Resource + private JobLogFrameworkService jobLogFrameworkService; + + @Override + protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException { + // 第一步,获得 Job 数据 + Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name()); + String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name()); + String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name()); + int refireCount = executionContext.getRefireCount(); + int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0); + int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0); + + // 第二步,执行任务 + Long jobLogId = null; + LocalDateTime startTime = LocalDateTime.now(); + String data = null; + Throwable exception = null; + try { + // 记录 Job 日志(初始) + jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1); + // 执行任务 + data = this.executeInternal(jobHandlerName, jobHandlerParam); + } catch (Throwable ex) { + exception = ex; + } + + // 第三步,记录执行日志 + this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext); + + // 第四步,处理有异常的情况 + handleException(exception, refireCount, retryCount, retryInterval); + } + + private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception { + // 获得 JobHandler 对象 + JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class); + Assert.notNull(jobHandler, "JobHandler 不会为空"); + // 执行任务 + return jobHandler.execute(jobHandlerParam); + } + + private void updateJobLogResultAsync(Long jobLogId, LocalDateTime startTime, String data, Throwable exception, + JobExecutionContext executionContext) { + LocalDateTime endTime = LocalDateTime.now(); + // 处理是否成功 + boolean success = exception == null; + if (!success) { + data = getRootCauseMessage(exception); + } + // 更新日志 + try { + jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) LocalDateTimeUtil.between(startTime, endTime).toMillis(), success, data); + } catch (Exception ex) { + log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]", + executionContext.getJobDetail().getKey(), jobLogId, success, data); + } + } + + private void handleException(Throwable exception, + int refireCount, int retryCount, int retryInterval) throws JobExecutionException { + // 如果有异常,则进行重试 + if (exception == null) { + return; + } + // 情况一:如果到达重试上限,则直接抛出异常即可 + if (refireCount >= retryCount) { + throw new JobExecutionException(exception); + } + + // 情况二:如果未到达重试上限,则 sleep 一定间隔时间,然后重试 + // 这里使用 sleep 来实现,主要还是希望实现比较简单。因为,同一时间,不会存在大量失败的 Job。 + if (retryInterval > 0) { + ThreadUtil.sleep(retryInterval); + } + // 第二个参数,refireImmediately = true,表示立即重试 + throw new JobExecutionException(exception, true); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/scheduler/SchedulerManager.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/scheduler/SchedulerManager.java new file mode 100644 index 0000000..519cda9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/scheduler/SchedulerManager.java @@ -0,0 +1,130 @@ +package com.yunxi.scm.framework.quartz.core.scheduler; + +import com.yunxi.scm.framework.quartz.core.enums.JobDataKeyEnum; +import com.yunxi.scm.framework.quartz.core.handler.JobHandlerInvoker; +import org.quartz.*; + +/** + * {@link org.quartz.Scheduler} 的管理器,负责创建任务 + * + * 考虑到实现的简洁性,我们使用 jobHandlerName 作为唯一标识,即: + * 1. Job 的 {@link JobDetail#getKey()} + * 2. Trigger 的 {@link Trigger#getKey()} + * + * 另外,jobHandlerName 对应到 Spring Bean 的名字,直接调用 + * + * @author 芋道源码 + */ +public class SchedulerManager { + + private final Scheduler scheduler; + + public SchedulerManager(Scheduler scheduler) { + this.scheduler = scheduler; + } + + /** + * 添加 Job 到 Quartz 中 + * + * @param jobId 任务编号 + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @param cronExpression CRON 表达式 + * @param retryCount 重试次数 + * @param retryInterval 重试间隔 + * @throws SchedulerException 添加异常 + */ + public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) + throws SchedulerException { + // 创建 JobDetail 对象 + JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class) + .usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId) + .usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName) + .withIdentity(jobHandlerName).build(); + // 创建 Trigger 对象 + Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); + // 新增调度 + scheduler.scheduleJob(jobDetail, trigger); + } + + /** + * 更新 Job 到 Quartz + * + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @param cronExpression CRON 表达式 + * @param retryCount 重试次数 + * @param retryInterval 重试间隔 + * @throws SchedulerException 更新异常 + */ + public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) + throws SchedulerException { + // 创建新 Trigger 对象 + Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval); + // 修改调度 + scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger); + } + + /** + * 删除 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 删除异常 + */ + public void deleteJob(String jobHandlerName) throws SchedulerException { + scheduler.deleteJob(new JobKey(jobHandlerName)); + } + + /** + * 暂停 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 暂停异常 + */ + public void pauseJob(String jobHandlerName) throws SchedulerException { + scheduler.pauseJob(new JobKey(jobHandlerName)); + } + + /** + * 启动 Quartz 中的 Job + * + * @param jobHandlerName 任务处理器的名字 + * @throws SchedulerException 启动异常 + */ + public void resumeJob(String jobHandlerName) throws SchedulerException { + scheduler.resumeJob(new JobKey(jobHandlerName)); + scheduler.resumeTrigger(new TriggerKey(jobHandlerName)); + } + + /** + * 立即触发一次 Quartz 中的 Job + * + * @param jobId 任务编号 + * @param jobHandlerName 任务处理器的名字 + * @param jobHandlerParam 任务处理器的参数 + * @throws SchedulerException 触发异常 + */ + public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam) + throws SchedulerException { + JobDataMap data = new JobDataMap(); // 无需重试,所以不设置 retryCount 和 retryInterval + data.put(JobDataKeyEnum.JOB_ID.name(), jobId); + data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName); + data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam); + // 触发任务 + scheduler.triggerJob(new JobKey(jobHandlerName), data); + } + + private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression, + Integer retryCount, Integer retryInterval) { + return TriggerBuilder.newTrigger() + .withIdentity(jobHandlerName) + .withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)) + .usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam) + .usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount) + .usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval) + .build(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/service/JobLogFrameworkService.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/service/JobLogFrameworkService.java new file mode 100644 index 0000000..255179a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/service/JobLogFrameworkService.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.framework.quartz.core.service; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * Job 日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface JobLogFrameworkService { + + /** + * 创建 Job 日志 + * + * @param jobId 任务编号 + * @param beginTime 开始时间 + * @param jobHandlerName Job 处理器的名字 + * @param jobHandlerParam Job 处理器的参数 + * @param executeIndex 第几次执行 + * @return Job 日志的编号 + */ + Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId, + @NotNull(message = "开始时间") LocalDateTime beginTime, + @NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName, + String jobHandlerParam, + @NotNull(message = "第几次执行不能为空") Integer executeIndex); + + /** + * 更新 Job 日志的执行结果 + * + * @param logId 日志编号 + * @param endTime 结束时间。因为是异步,避免记录时间不准去 + * @param duration 运行时长,单位:毫秒 + * @param success 是否成功 + * @param result 成功数据 + */ + void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId, + @NotNull(message = "结束时间不能为空") LocalDateTime endTime, + @NotNull(message = "运行时长不能为空") Integer duration, + boolean success, String result); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/util/CronUtils.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/util/CronUtils.java new file mode 100644 index 0000000..c835a49 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/core/util/CronUtils.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.framework.quartz.core.util; + +import cn.hutool.core.date.LocalDateTimeUtil; +import org.quartz.CronExpression; + +import java.text.ParseException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Quartz Cron 表达式的工具类 + * + * @author 芋道源码 + */ +public class CronUtils { + + /** + * 校验 CRON 表达式是否有效 + * + * @param cronExpression CRON 表达式 + * @return 是否有效 + */ + public static boolean isValid(String cronExpression) { + return CronExpression.isValidExpression(cronExpression); + } + + /** + * 基于 CRON 表达式,获得下 n 个满足执行的时间 + * + * @param cronExpression CRON 表达式 + * @param n 数量 + * @return 满足条件的执行时间 + */ + public static List getNextTimes(String cronExpression, int n) { + // 获得 CronExpression 对象 + CronExpression cron; + try { + cron = new CronExpression(cronExpression); + } catch (ParseException e) { + throw new IllegalArgumentException(e.getMessage()); + } + // 从当前开始计算,n 个满足条件的 + Date now = new Date(); + List nextTimes = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + Date nextTime = cron.getNextValidTimeAfter(now); + nextTimes.add(LocalDateTimeUtil.of(nextTime)); + // 切换现在,为下一个触发时间; + now = nextTime; + } + return nextTimes; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/package-info.java new file mode 100644 index 0000000..e56e034 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/java/com/yunxi/scm/framework/quartz/package-info.java @@ -0,0 +1,7 @@ +/** + * 1. 定时任务,采用 Quartz 实现进程内的任务执行。 + * 考虑到高可用,使用 Quartz 自带的 MySQL 集群方案。 + * + * 2. 异步任务,采用 Spring Async 异步执行。 + */ +package com.yunxi.scm.framework.quartz; diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..b97a04f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.yunxi.scm.framework.quartz.config.YunxiQuartzAutoConfiguration +com.yunxi.scm.framework.quartz.config.YunxiAsyncAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md b/yunxi-framework/yunxi-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md new file mode 100644 index 0000000..03c64aa --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/《芋道 Spring Boot 定时任务入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md b/yunxi-framework/yunxi-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md new file mode 100644 index 0000000..996421f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-job/《芋道 Spring Boot 异步任务入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-monitor/pom.xml new file mode 100644 index 0000000..3b79de2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/pom.xml @@ -0,0 +1,73 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-monitor + jar + + ${project.artifactId} + 服务监控,提供链路追踪、日志服务、指标收集等等功能 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework + spring-web + provided + + + + jakarta.servlet + jakarta.servlet-api + provided + + + + + io.opentracing + opentracing-util + + + org.apache.skywalking + apm-toolkit-trace + + + org.apache.skywalking + apm-toolkit-logback-1.x + + + org.apache.skywalking + apm-toolkit-opentracing + + + + + io.micrometer + micrometer-registry-prometheus + + + + de.codecentric + spring-boot-admin-starter-client + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/TracerProperties.java b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/TracerProperties.java new file mode 100644 index 0000000..b0f200b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/TracerProperties.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.framework.tracer.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * BizTracer配置类 + * + * @author 麻薯 + */ +@ConfigurationProperties("yunxi.tracer") +@Data +public class TracerProperties { +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/YunxiMetricsAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/YunxiMetricsAutoConfiguration.java new file mode 100644 index 0000000..372f716 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/YunxiMetricsAutoConfiguration.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.framework.tracer.config; + +import io.micrometer.core.instrument.MeterRegistry; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; + +/** + * Metrics 配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnClass({MeterRegistryCustomizer.class}) +@ConditionalOnProperty(prefix = "yunxi.metrics", value = "enable", matchIfMissing = true) // 允许使用 yunxi.metrics.enable=false 禁用 Metrics +public class YunxiMetricsAutoConfiguration { + + @Bean + public MeterRegistryCustomizer metricsCommonTags( + @Value("${spring.application.name}") String applicationName) { + return registry -> registry.config().commonTags("application", applicationName); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/YunxiTracerAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/YunxiTracerAutoConfiguration.java new file mode 100644 index 0000000..47b45e6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/config/YunxiTracerAutoConfiguration.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.framework.tracer.config; + +import com.yunxi.scm.framework.common.enums.WebFilterOrderEnum; +import com.yunxi.scm.framework.tracer.core.aop.BizTraceAspect; +import com.yunxi.scm.framework.tracer.core.filter.TraceFilter; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; + +/** + * Tracer 配置类 + * + * @author mashu + */ +@AutoConfiguration +@ConditionalOnClass({BizTraceAspect.class}) +@EnableConfigurationProperties(TracerProperties.class) +@ConditionalOnProperty(prefix = "yunxi.tracer", value = "enable", matchIfMissing = true) +public class YunxiTracerAutoConfiguration { + + // TODO @芋艿:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk +// @Bean +// public TracerProperties bizTracerProperties() { +// return new TracerProperties(); +// } +// +// @Bean +// public BizTraceAspect bizTracingAop() { +// return new BizTraceAspect(tracer()); +// } +// +// @Bean +// public Tracer tracer() { +// // 创建 SkywalkingTracer 对象 +// SkywalkingTracer tracer = new SkywalkingTracer(); +// // 设置为 GlobalTracer 的追踪器 +// GlobalTracer.register(tracer); +// return tracer; +// } + + /** + * 创建 TraceFilter 过滤器,响应 header 设置 traceId + */ + @Bean + public FilterRegistrationBean traceFilter() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new TraceFilter()); + registrationBean.setOrder(WebFilterOrderEnum.TRACE_FILTER); + return registrationBean; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/annotation/BizTrace.java b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/annotation/BizTrace.java new file mode 100644 index 0000000..c780a91 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/annotation/BizTrace.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.framework.tracer.core.annotation; + +import java.lang.annotation.*; + +/** + * 打印业务编号 / 业务类型注解 + * + * 使用时,需要设置 SkyWalking OAP Server 的 application.yaml 配置文件,修改 SW_SEARCHABLE_TAG_KEYS 配置项, + * 增加 biz.type 和 biz.id 两值,然后重启 SkyWalking OAP Server 服务器。 + * + * @author 麻薯 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface BizTrace { + + /** + * 业务编号 tag 名 + */ + String ID_TAG = "biz.id"; + /** + * 业务类型 tag 名 + */ + String TYPE_TAG = "biz.type"; + + /** + * @return 操作名 + */ + String operationName() default ""; + + /** + * @return 业务编号 + */ + String id(); + + /** + * @return 业务类型 + */ + String type(); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/aop/BizTraceAspect.java b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/aop/BizTraceAspect.java new file mode 100644 index 0000000..a59103b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/aop/BizTraceAspect.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.framework.tracer.core.aop; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.tracer.core.annotation.BizTrace; +import com.yunxi.scm.framework.common.util.spring.SpringExpressionUtils; +import com.yunxi.scm.framework.tracer.core.util.TracerFrameworkUtils; +import io.opentracing.Span; +import io.opentracing.Tracer; +import io.opentracing.tag.Tags; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import java.util.Map; + +import static java.util.Arrays.asList; + +/** + * {@link BizTrace} 切面,记录业务链路 + * + * @author mashu + */ +@Aspect +@AllArgsConstructor +@Slf4j +public class BizTraceAspect { + + private static final String BIZ_OPERATION_NAME_PREFIX = "Biz/"; + + private final Tracer tracer; + + @Around(value = "@annotation(trace)") + public Object around(ProceedingJoinPoint joinPoint, BizTrace trace) throws Throwable { + // 创建 span + String operationName = getOperationName(joinPoint, trace); + Span span = tracer.buildSpan(operationName) + .withTag(Tags.COMPONENT.getKey(), "biz") + .start(); + try { + // 执行原有方法 + return joinPoint.proceed(); + } catch (Throwable throwable) { + TracerFrameworkUtils.onError(throwable, span); + throw throwable; + } finally { + // 设置 Span 的 biz 属性 + setBizTag(span, joinPoint, trace); + // 完成 Span + span.finish(); + } + } + + private String getOperationName(ProceedingJoinPoint joinPoint, BizTrace trace) { + // 自定义操作名 + if (StrUtil.isNotEmpty(trace.operationName())) { + return BIZ_OPERATION_NAME_PREFIX + trace.operationName(); + } + // 默认操作名,使用方法名 + return BIZ_OPERATION_NAME_PREFIX + + joinPoint.getSignature().getDeclaringType().getSimpleName() + + "/" + joinPoint.getSignature().getName(); + } + + private void setBizTag(Span span, ProceedingJoinPoint joinPoint, BizTrace trace) { + try { + Map result = SpringExpressionUtils.parseExpressions(joinPoint, asList(trace.type(), trace.id())); + span.setTag(BizTrace.TYPE_TAG, MapUtil.getStr(result, trace.type())); + span.setTag(BizTrace.ID_TAG, MapUtil.getStr(result, trace.id())); + } catch (Exception ex) { + log.error("[setBizTag][解析 bizType 与 bizId 发生异常]", ex); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/filter/TraceFilter.java b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/filter/TraceFilter.java new file mode 100644 index 0000000..70082e5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/filter/TraceFilter.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.framework.tracer.core.filter; + +import com.yunxi.scm.framework.common.util.monitor.TracerUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Trace 过滤器,打印 traceId 到 header 中返回 + * + * @author 芋道源码 + */ +public class TraceFilter extends OncePerRequestFilter { + + /** + * Header 名 - 链路追踪编号 + */ + private static final String HEADER_NAME_TRACE_ID = "trace-id"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws IOException, ServletException { + // 设置响应 traceId + response.addHeader(HEADER_NAME_TRACE_ID, TracerUtils.getTraceId()); + // 继续过滤 + chain.doFilter(request, response); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/util/TracerFrameworkUtils.java b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/util/TracerFrameworkUtils.java new file mode 100644 index 0000000..73d22dd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/core/util/TracerFrameworkUtils.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.framework.tracer.core.util; + +import io.opentracing.Span; +import io.opentracing.tag.Tags; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.Map; + +/** + * 链路追踪 Util + * + * @author 芋道源码 + */ +public class TracerFrameworkUtils { + + /** + * 将异常记录到 Span 中,参考自 com.aliyuncs.utils.TraceUtils + * + * @param throwable 异常 + * @param span Span + */ + public static void onError(Throwable throwable, Span span) { + Tags.ERROR.set(span, Boolean.TRUE); + if (throwable != null) { + span.log(errorLogs(throwable)); + } + } + + private static Map errorLogs(Throwable throwable) { + Map errorLogs = new HashMap(10); + errorLogs.put("event", Tags.ERROR.getKey()); + errorLogs.put("error.object", throwable); + errorLogs.put("error.kind", throwable.getClass().getName()); + String message = throwable.getCause() != null ? throwable.getCause().getMessage() : throwable.getMessage(); + if (message != null) { + errorLogs.put("message", message); + } + StringWriter sw = new StringWriter(); + throwable.printStackTrace(new PrintWriter(sw)); + errorLogs.put("stack", sw.toString()); + return errorLogs; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/package-info.java new file mode 100644 index 0000000..d632f72 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/java/com/yunxi/scm/framework/tracer/package-info.java @@ -0,0 +1,6 @@ +/** + * 使用 SkyWalking 组件,作为链路追踪、日志中心。 + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.tracer; diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..600ed25 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.yunxi.scm.framework.tracer.config.YunxiTracerAutoConfiguration +com.yunxi.scm.framework.tracer.config.YunxiMetricsAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 0000000..48fd213 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md new file mode 100644 index 0000000..2dfcbf6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 监控端点 Actuator 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md new file mode 100644 index 0000000..504425f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-monitor/《芋道 Spring Boot 链路追踪 SkyWalking 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-mq/pom.xml new file mode 100644 index 0000000..39da7c2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/pom.xml @@ -0,0 +1,26 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-mq + jar + + ${project.artifactId} + 消息队列,基于 Redis Pub/Sub 实现广播消费,基于 Stream 实现集群消费 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/config/YunxiMQAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/config/YunxiMQAutoConfiguration.java new file mode 100644 index 0000000..63ba372 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/config/YunxiMQAutoConfiguration.java @@ -0,0 +1,170 @@ +package com.yunxi.scm.framework.mq.config; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.system.SystemUtil; +import com.yunxi.scm.framework.common.enums.DocumentEnum; +import com.yunxi.scm.framework.mq.core.RedisMQTemplate; +import com.yunxi.scm.framework.mq.core.interceptor.RedisMessageInterceptor; +import com.yunxi.scm.framework.mq.core.pubsub.AbstractChannelMessageListener; +import com.yunxi.scm.framework.mq.core.stream.AbstractStreamMessageListener; +import com.yunxi.scm.framework.mq.job.RedisPendingMessageResendJob; +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainerX; +import org.springframework.data.redis.stream.StreamMessageListenerContainer; +import org.springframework.scheduling.annotation.EnableScheduling; + +import java.util.List; +import java.util.Properties; + +/** + * 消息队列配置类 + * + * @author 芋道源码 + */ +@Slf4j +@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息 +@AutoConfiguration(after = YunxiRedisAutoConfiguration.class) +public class YunxiMQAutoConfiguration { + + @Bean + public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate, + List interceptors) { + RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate); + // 添加拦截器 + interceptors.forEach(redisMQTemplate::addInterceptor); + return redisMQTemplate; + } + + // ========== 消费者相关 ========== + + /** + * 创建 Redis Pub/Sub 广播消费的容器 + */ + @Bean(initMethod = "start", destroyMethod = "stop") + @ConditionalOnBean(AbstractChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听 + @ConditionalOnProperty(prefix = "yunxi.mq.redis.pubsub", value = "enable", matchIfMissing = true) // 允许使用 yunxi.mq.redis.pubsub.enable=false 禁用多租户 + public RedisMessageListenerContainer redisMessageListenerContainer( + RedisMQTemplate redisMQTemplate, List> listeners) { + // 创建 RedisMessageListenerContainer 对象 + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + // 设置 RedisConnection 工厂。 + container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory()); + // 添加监听器 + listeners.forEach(listener -> { + listener.setRedisMQTemplate(redisMQTemplate); + container.addMessageListener(listener, new ChannelTopic(listener.getChannel())); + log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]", + listener.getChannel(), listener.getClass().getName()); + }); + return container; + } + + /** + * 创建 Redis Stream 重新消费的任务 + */ + @Bean + @ConditionalOnBean(AbstractStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 + @ConditionalOnProperty(prefix = "yunxi.mq.redis.stream", value = "enable", matchIfMissing = true) // 允许使用 yunxi.mq.redis.stream.enable=false 禁用多租户 + public RedisPendingMessageResendJob redisPendingMessageResendJob(List> listeners, + RedisMQTemplate redisTemplate, + @Value("${spring.application.name}") String groupName, + RedissonClient redissonClient) { + return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient); + } + + /** + * 创建 Redis Stream 集群消费的容器 + *

+ * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html + */ + @Bean(initMethod = "start", destroyMethod = "stop") + @ConditionalOnBean(AbstractStreamMessageListener.class) // 只有 AbstractStreamMessageListener 存在的时候,才需要注册 Redis pubsub 监听 + @ConditionalOnProperty(prefix = "yunxi.mq.redis.stream", value = "enable", matchIfMissing = true) // 允许使用 yunxi.mq.redis.stream.enable=false 禁用多租户 + public StreamMessageListenerContainer> redisStreamMessageListenerContainer( + RedisMQTemplate redisMQTemplate, List> listeners) { + RedisTemplate redisTemplate = redisMQTemplate.getRedisTemplate(); + checkRedisVersion(redisTemplate); + // 第一步,创建 StreamMessageListenerContainer 容器 + // 创建 options 配置 + StreamMessageListenerContainer.StreamMessageListenerContainerOptions> containerOptions = + StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder() + .batchSize(10) // 一次性最多拉取多少条消息 + .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化 + .build(); + // 创建 container 对象 + StreamMessageListenerContainer> container = +// StreamMessageListenerContainer.create(redisTemplate.getRequiredConnectionFactory(), containerOptions); + DefaultStreamMessageListenerContainerX.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions); + + // 第二步,注册监听器,消费对应的 Stream 主题 + String consumerName = buildConsumerName(); + listeners.parallelStream().forEach(listener -> { + log.info("[redisStreamMessageListenerContainer][开始注册 StreamKey({}) 对应的监听器({})]", + listener.getStreamKey(), listener.getClass().getName()); + // 创建 listener 对应的消费者分组 + try { + redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup()); + } catch (Exception ignore) { + } + // 设置 listener 对应的 redisTemplate + listener.setRedisMQTemplate(redisMQTemplate); + // 创建 Consumer 对象 + Consumer consumer = Consumer.from(listener.getGroup(), consumerName); + // 设置 Consumer 消费进度,以最小消费进度为准 + StreamOffset streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed()); + // 设置 Consumer 监听 + StreamMessageListenerContainer.StreamReadRequestBuilder builder = StreamMessageListenerContainer.StreamReadRequest + .builder(streamOffset).consumer(consumer) + .autoAcknowledge(false) // 不自动 ack + .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false + container.register(builder.build(), listener); + log.info("[redisStreamMessageListenerContainer][完成注册 StreamKey({}) 对应的监听器({})]", + listener.getStreamKey(), listener.getClass().getName()); + }); + return container; + } + + /** + * 构建消费者名字,使用本地 IP + 进程编号的方式。 + * 参考自 RocketMQ clientId 的实现 + * + * @return 消费者名字 + */ + private static String buildConsumerName() { + return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID()); + } + + /** + * 校验 Redis 版本号,是否满足最低的版本号要求! + */ + private static void checkRedisVersion(RedisTemplate redisTemplate) { + // 获得 Redis 版本 + Properties info = redisTemplate.execute((RedisCallback) RedisServerCommands::info); + String version = MapUtil.getStr(info, "redis_version"); + // 校验最低版本必须大于等于 5.0.0 + int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false)); + if (majorVersion < 5) { + throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" + + "请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl())); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/RedisMQTemplate.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/RedisMQTemplate.java new file mode 100644 index 0000000..762af47 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/RedisMQTemplate.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.framework.mq.core; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.mq.core.interceptor.RedisMessageInterceptor; +import com.yunxi.scm.framework.mq.core.message.AbstractRedisMessage; +import com.yunxi.scm.framework.mq.core.pubsub.AbstractChannelMessage; +import com.yunxi.scm.framework.mq.core.stream.AbstractStreamMessage; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.RedisTemplate; + +import java.util.ArrayList; +import java.util.List; + +/** + * Redis MQ 操作模板类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class RedisMQTemplate { + + @Getter + private final RedisTemplate redisTemplate; + /** + * 拦截器数组 + */ + @Getter + private final List interceptors = new ArrayList<>(); + + /** + * 发送 Redis 消息,基于 Redis pub/sub 实现 + * + * @param message 消息 + */ + public void send(T message) { + try { + sendMessageBefore(message); + // 发送消息 + redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message)); + } finally { + sendMessageAfter(message); + } + } + + /** + * 发送 Redis 消息,基于 Redis Stream 实现 + * + * @param message 消息 + * @return 消息记录的编号对象 + */ + public RecordId send(T message) { + try { + sendMessageBefore(message); + // 发送消息 + return redisTemplate.opsForStream().add(StreamRecords.newRecord() + .ofObject(JsonUtils.toJsonString(message)) // 设置内容 + .withStreamKey(message.getStreamKey())); // 设置 stream key + } finally { + sendMessageAfter(message); + } + } + + /** + * 添加拦截器 + * + * @param interceptor 拦截器 + */ + public void addInterceptor(RedisMessageInterceptor interceptor) { + interceptors.add(interceptor); + } + + private void sendMessageBefore(AbstractRedisMessage message) { + // 正序 + interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message)); + } + + private void sendMessageAfter(AbstractRedisMessage message) { + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).sendMessageAfter(message); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/interceptor/RedisMessageInterceptor.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/interceptor/RedisMessageInterceptor.java new file mode 100644 index 0000000..4a85c04 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/interceptor/RedisMessageInterceptor.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.framework.mq.core.interceptor; + +import com.yunxi.scm.framework.mq.core.message.AbstractRedisMessage; + +/** + * {@link AbstractRedisMessage} 消息拦截器 + * 通过拦截器,作为插件机制,实现拓展。 + * 例如说,多租户场景下的 MQ 消息处理 + * + * @author 芋道源码 + */ +public interface RedisMessageInterceptor { + + default void sendMessageBefore(AbstractRedisMessage message) { + } + + default void sendMessageAfter(AbstractRedisMessage message) { + } + + default void consumeMessageBefore(AbstractRedisMessage message) { + } + + default void consumeMessageAfter(AbstractRedisMessage message) { + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/message/AbstractRedisMessage.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/message/AbstractRedisMessage.java new file mode 100644 index 0000000..97785dc --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/message/AbstractRedisMessage.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.framework.mq.core.message; + +import lombok.Data; + +import java.util.HashMap; +import java.util.Map; + +/** + * Redis 消息抽象基类 + * + * @author 芋道源码 + */ +@Data +public abstract class AbstractRedisMessage { + + /** + * 头 + */ + private Map headers = new HashMap<>(); + + public String getHeader(String key) { + return headers.get(key); + } + + public void addHeader(String key, String value) { + headers.put(key, value); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/pubsub/AbstractChannelMessage.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/pubsub/AbstractChannelMessage.java new file mode 100644 index 0000000..f249e4b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/pubsub/AbstractChannelMessage.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.mq.core.pubsub; + +import com.yunxi.scm.framework.mq.core.message.AbstractRedisMessage; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Redis Channel Message 抽象类 + * + * @author 芋道源码 + */ +public abstract class AbstractChannelMessage extends AbstractRedisMessage { + + /** + * 获得 Redis Channel + * + * @return Channel + */ + @JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。 + public abstract String getChannel(); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/pubsub/AbstractChannelMessageListener.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/pubsub/AbstractChannelMessageListener.java new file mode 100644 index 0000000..0cb3ccd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/pubsub/AbstractChannelMessageListener.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.framework.mq.core.pubsub; + +import cn.hutool.core.util.TypeUtil; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.mq.core.RedisMQTemplate; +import com.yunxi.scm.framework.mq.core.interceptor.RedisMessageInterceptor; +import com.yunxi.scm.framework.mq.core.message.AbstractRedisMessage; +import lombok.Setter; +import lombok.SneakyThrows; +import org.springframework.data.redis.connection.Message; +import org.springframework.data.redis.connection.MessageListener; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * Redis Pub/Sub 监听器抽象类,用于实现广播消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractChannelMessageListener implements MessageListener { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + private final String channel; + /** + * RedisMQTemplate + */ + @Setter + private RedisMQTemplate redisMQTemplate; + + @SneakyThrows + protected AbstractChannelMessageListener() { + this.messageType = getMessageClass(); + this.channel = messageType.getDeclaredConstructor().newInstance().getChannel(); + } + + /** + * 获得 Sub 订阅的 Redis Channel 通道 + * + * @return channel + */ + public final String getChannel() { + return channel; + } + + @Override + public final void onMessage(Message message, byte[] bytes) { + T messageObj = JsonUtils.parseObject(message.getBody(), messageType); + try { + consumeMessageBefore(messageObj); + // 消费消息 + this.onMessage(messageObj); + } finally { + consumeMessageAfter(messageObj); + } + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Type type = TypeUtil.getTypeArgument(getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) type; + } + + private void consumeMessageBefore(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 正序 + interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); + } + + private void consumeMessageAfter(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).consumeMessageAfter(message); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/stream/AbstractStreamMessage.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/stream/AbstractStreamMessage.java new file mode 100644 index 0000000..232b6a2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/stream/AbstractStreamMessage.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.mq.core.stream; + +import com.yunxi.scm.framework.mq.core.message.AbstractRedisMessage; +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Redis Stream Message 抽象类 + * + * @author 芋道源码 + */ +public abstract class AbstractStreamMessage extends AbstractRedisMessage { + + /** + * 获得 Redis Stream Key + * + * @return Channel + */ + @JsonIgnore // 避免序列化 + public abstract String getStreamKey(); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/stream/AbstractStreamMessageListener.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/stream/AbstractStreamMessageListener.java new file mode 100644 index 0000000..1705165 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/core/stream/AbstractStreamMessageListener.java @@ -0,0 +1,113 @@ +package com.yunxi.scm.framework.mq.core.stream; + +import cn.hutool.core.util.TypeUtil; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.mq.core.RedisMQTemplate; +import com.yunxi.scm.framework.mq.core.interceptor.RedisMessageInterceptor; +import com.yunxi.scm.framework.mq.core.message.AbstractRedisMessage; +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.connection.stream.ObjectRecord; +import org.springframework.data.redis.stream.StreamListener; + +import java.lang.reflect.Type; +import java.util.List; + +/** + * Redis Stream 监听器抽象类,用于实现集群消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractStreamMessageListener + implements StreamListener> { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + @Getter + private final String streamKey; + + /** + * Redis 消费者分组,默认使用 spring.application.name 名字 + */ + @Value("${spring.application.name}") + @Getter + private String group; + /** + * RedisMQTemplate + */ + @Setter + private RedisMQTemplate redisMQTemplate; + + @SneakyThrows + protected AbstractStreamMessageListener() { + this.messageType = getMessageClass(); + this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey(); + } + + @Override + public void onMessage(ObjectRecord message) { + // 消费消息 + T messageObj = JsonUtils.parseObject(message.getValue(), messageType); + try { + consumeMessageBefore(messageObj); + // 消费消息 + this.onMessage(messageObj); + // ack 消息消费完成 + redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message); + // TODO 芋艿:需要额外考虑以下几个点: + // 1. 处理异常的情况 + // 2. 发送日志;以及事务的结合 + // 3. 消费日志;以及通用的幂等性 + // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638 + } finally { + consumeMessageAfter(messageObj); + } + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Type type = TypeUtil.getTypeArgument(getClass(), 0); + if (type == null) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) type; + } + + private void consumeMessageBefore(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 正序 + interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message)); + } + + private void consumeMessageAfter(AbstractRedisMessage message) { + assert redisMQTemplate != null; + List interceptors = redisMQTemplate.getInterceptors(); + // 倒序 + for (int i = interceptors.size() - 1; i >= 0; i--) { + interceptors.get(i).consumeMessageAfter(message); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/job/RedisPendingMessageResendJob.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/job/RedisPendingMessageResendJob.java new file mode 100644 index 0000000..dcf10ad --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/job/RedisPendingMessageResendJob.java @@ -0,0 +1,100 @@ +package com.yunxi.scm.framework.mq.job; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.mq.core.RedisMQTemplate; +import com.yunxi.scm.framework.mq.core.stream.AbstractStreamMessageListener; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.core.StreamOperations; +import org.springframework.scheduling.annotation.Scheduled; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 这个任务用于处理,crash 之后的消费者未消费完的消息 + */ +@Slf4j +@AllArgsConstructor +public class RedisPendingMessageResendJob { + + private static final String LOCK_KEY = "redis:pending:msg:lock"; + + /** + * 消息超时时间,默认 5 分钟 + * + * 1. 超时的消息才会被重新投递 + * 2. 由于定时任务 1 分钟一次,消息超时后不会被立即重投,极端情况下消息5分钟过期后,再等 1 分钟才会被扫瞄到 + */ + private static final int EXPIRE_TIME = 5 * 60; + + private final List> listeners; + private final RedisMQTemplate redisTemplate; + private final String groupName; + private final RedissonClient redissonClient; + + /** + * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题 + */ + @Scheduled(cron = "35 * * * * ?") + public void messageResend() { + RLock lock = redissonClient.getLock(LOCK_KEY); + // 尝试加锁 + if (lock.tryLock()) { + try { + execute(); + } catch (Exception ex) { + log.error("[messageResend][执行异常]", ex); + } finally { + lock.unlock(); + } + } + } + + /** + * 执行清理逻辑 + * + * @see 讨论 + */ + private void execute() { + StreamOperations ops = redisTemplate.getRedisTemplate().opsForStream(); + listeners.forEach(listener -> { + PendingMessagesSummary pendingMessagesSummary = Objects.requireNonNull(ops.pending(listener.getStreamKey(), groupName)); + // 每个消费者的 pending 队列消息数量 + Map pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer(); + pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> { + log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount); + // 每个消费者的 pending消息的详情信息 + PendingMessages pendingMessages = ops.pending(listener.getStreamKey(), Consumer.from(groupName, consumerName), Range.unbounded(), pendingMessageCount); + if (pendingMessages.isEmpty()) { + return; + } + pendingMessages.forEach(pendingMessage -> { + // 获取消息上一次传递到 consumer 的时间, + long lastDelivery = pendingMessage.getElapsedTimeSinceLastDelivery().getSeconds(); + if (lastDelivery < EXPIRE_TIME){ + return; + } + // 获取指定 id 的消息体 + List> records = ops.range(listener.getStreamKey(), + Range.of(Range.Bound.inclusive(pendingMessage.getIdAsString()), Range.Bound.inclusive(pendingMessage.getIdAsString()))); + if (CollUtil.isEmpty(records)) { + return; + } + // 重新投递消息 + redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord() + .ofObject(records.get(0).getValue()) // 设置内容 + .withStreamKey(listener.getStreamKey())); + // ack 消息消费完成 + redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, records.get(0)); + log.info("[processPendingMessage][消息({})重新投递成功]", records.get(0).getId()); + }); + }); + }); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/package-info.java new file mode 100644 index 0000000..aab53de --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/com/yunxi/scm/framework/mq/package-info.java @@ -0,0 +1,6 @@ +/** + * 消息队列,基于 Redis 提供: + * 1. 基于 Pub/Sub 实现广播消费 + * 2. 基于 Stream 实现集群消费 + */ +package com.yunxi.scm.framework.mq; diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java new file mode 100644 index 0000000..b4cf4c5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java @@ -0,0 +1,62 @@ +package org.springframework.data.redis.stream; + +import cn.hutool.core.util.ReflectUtil; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.stream.ByteRecord; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.Record; +import org.springframework.util.Assert; + +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +/** + * 拓展 DefaultStreamMessageListenerContainer 实现,解决 Spring Data Redis + Redisson 结合使用时,Redisson 在 Stream 获得不到数据时,返回 null 而不是空 List,导致 NPE 异常。 + * 对应 issue:https://github.com/spring-projects/spring-data-redis/issues/2147 和 https://github.com/redisson/redisson/issues/4006 + * 目前看下来 Spring Data Redis 不肯加 null 判断,Redisson 暂时也没改返回 null 到空 List 的打算,所以暂时只能自己改,哽咽! + * + * @author 芋道源码 + */ +public class DefaultStreamMessageListenerContainerX> extends DefaultStreamMessageListenerContainer { + + /** + * 参考 {@link StreamMessageListenerContainer#create(RedisConnectionFactory, StreamMessageListenerContainerOptions)} 的实现 + */ + public static > StreamMessageListenerContainer create(RedisConnectionFactory connectionFactory, StreamMessageListenerContainer.StreamMessageListenerContainerOptions options) { + Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!"); + Assert.notNull(options, "StreamMessageListenerContainerOptions must not be null!"); + return new DefaultStreamMessageListenerContainerX<>(connectionFactory, options); + } + + public DefaultStreamMessageListenerContainerX(RedisConnectionFactory connectionFactory, StreamMessageListenerContainerOptions containerOptions) { + super(connectionFactory, containerOptions); + } + + /** + * 参考 {@link DefaultStreamMessageListenerContainer#register(StreamReadRequest, StreamListener)} 的实现 + */ + @Override + public Subscription register(StreamReadRequest streamRequest, StreamListener listener) { + return this.doRegisterX(getReadTaskX(streamRequest, listener)); + } + + @SuppressWarnings("unchecked") + private StreamPollTask getReadTaskX(StreamReadRequest streamRequest, StreamListener listener) { + StreamPollTask task = ReflectUtil.invoke(this, "getReadTask", streamRequest, listener); + // 修改 readFunction 方法 + Function> readFunction = (Function>) ReflectUtil.getFieldValue(task, "readFunction"); + ReflectUtil.setFieldValue(task, "readFunction", (Function>) readOffset -> { + List records = readFunction.apply(readOffset); + //【重点】保证 records 不是空,避免 NPE 的问题!!! + return records != null ? records : Collections.emptyList(); + }); + return task; + } + + private Subscription doRegisterX(Task task) { + return ReflectUtil.invoke(this, "doRegister", task); + } + +} + diff --git a/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..dcf7caf --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.mq.config.YunxiMQAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-mybatis/pom.xml new file mode 100644 index 0000000..ad61e9c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/pom.xml @@ -0,0 +1,71 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-mybatis + jar + + ${project.artifactId} + 数据库连接池、多数据源、事务、MyBatis 拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + provided + + + + + com.mysql + mysql-connector-j + + + com.oracle.database.jdbc + ojdbc8 + + + org.postgresql + postgresql + + + com.microsoft.sqlserver + mssql-jdbc + + + com.dameng + DmJdbcDriver18 + + + com.alibaba + druid-spring-boot-starter + + + com.baomidou + mybatis-plus-boot-starter + + + com.baomidou + dynamic-datasource-spring-boot-starter + + + + com.github.yulichang + mybatis-plus-join-boot-starter + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/config/YunxiDataSourceAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/config/YunxiDataSourceAutoConfiguration.java new file mode 100644 index 0000000..3013f7a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/config/YunxiDataSourceAutoConfiguration.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.datasource.config; + +import com.yunxi.scm.framework.datasource.core.filter.DruidAdRemoveFilter; +import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +/** + * 数据库配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@EnableTransactionManagement(proxyTargetClass = true) // 启动事务管理 +@EnableConfigurationProperties(DruidStatProperties.class) +public class YunxiDataSourceAutoConfiguration { + + /** + * 创建 DruidAdRemoveFilter 过滤器,过滤 common.js 的广告 + */ + @Bean + @ConditionalOnProperty(name = "spring.datasource.druid.web-stat-filter.enabled", havingValue = "true") + public FilterRegistrationBean druidAdRemoveFilterFilter(DruidStatProperties properties) { + // 获取 druid web 监控页面的参数 + DruidStatProperties.StatViewServlet config = properties.getStatViewServlet(); + // 提取 common.js 的配置路径 + String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*"; + String commonJsPattern = pattern.replaceAll("\\*", "js/common.js"); + // 创建 DruidAdRemoveFilter Bean + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(new DruidAdRemoveFilter()); + registrationBean.addUrlPatterns(commonJsPattern); + return registrationBean; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/core/enums/DataSourceEnum.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/core/enums/DataSourceEnum.java new file mode 100644 index 0000000..7d3dba9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/core/enums/DataSourceEnum.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.framework.datasource.core.enums; + +/** + * 对应于多数据源中不同数据源配置 + * + * 通过在方法上,使用 {@link com.baomidou.dynamic.datasource.annotation.DS} 注解,设置使用的数据源。 + * 注意,默认是 {@link #MASTER} 数据源 + * + * 对应官方文档为 http://dynamic-datasource.com/guide/customize/Annotation.html + */ +public interface DataSourceEnum { + + /** + * 主库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Master} 注解 + */ + String MASTER = "master"; + /** + * 从库,推荐使用 {@link com.baomidou.dynamic.datasource.annotation.Slave} 注解 + */ + String SLAVE = "slave"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/core/filter/DruidAdRemoveFilter.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/core/filter/DruidAdRemoveFilter.java new file mode 100644 index 0000000..78b942a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/core/filter/DruidAdRemoveFilter.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.framework.datasource.core.filter; + +import com.alibaba.druid.util.Utils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Druid 底部广告过滤器 + * + * @author 芋道源码 + */ +public class DruidAdRemoveFilter extends OncePerRequestFilter { + + /** + * common.js 的路径 + */ + private static final String COMMON_JS_ILE_PATH = "support/http/resources/js/common.js"; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + chain.doFilter(request, response); + // 重置缓冲区,响应头不会被重置 + response.resetBuffer(); + // 获取 common.js + String text = Utils.readFromResource(COMMON_JS_ILE_PATH); + // 正则替换 banner, 除去底部的广告信息 + text = text.replaceAll("
", ""); + text = text.replaceAll("powered.*?shrek.wang", ""); + response.getWriter().write(text); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/package-info.java new file mode 100644 index 0000000..92fabe0 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/datasource/package-info.java @@ -0,0 +1,5 @@ +/** + * 数据库连接池,采用 Druid + * 多数据源,采用爆米花 + */ +package com.yunxi.scm.framework.datasource; diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/expression/AndExpressionX.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/expression/AndExpressionX.java new file mode 100644 index 0000000..beb332a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/expression/AndExpressionX.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.framework.expression; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.AndExpression; + +/** + * AndExpression 的扩展类(会在原有表达式两端加上括号) + */ +public class AndExpressionX extends AndExpression { + + public AndExpressionX() { + } + + public AndExpressionX(Expression leftExpression, Expression rightExpression) { + this.setLeftExpression(leftExpression); + this.setRightExpression(rightExpression); + } + + @Override + public String toString() { + return "(" + super.toString() + ")"; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/expression/OrExpressionX.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/expression/OrExpressionX.java new file mode 100644 index 0000000..376269f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/expression/OrExpressionX.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.framework.expression; + +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; + +/** + * OrExpression 的扩展类(会在原有表达式两端加上括号) + */ +public class OrExpressionX extends OrExpression { + + public OrExpressionX() { + } + + public OrExpressionX(Expression leftExpression, Expression rightExpression) { + this.setLeftExpression(leftExpression); + this.setRightExpression(rightExpression); + } + + @Override + public String toString() { + return "(" + super.toString() + ")"; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java new file mode 100644 index 0000000..ce9f2f0 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/config/IdTypeEnvironmentPostProcessor.java @@ -0,0 +1,108 @@ +package com.yunxi.scm.framework.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import com.yunxi.scm.framework.mybatis.core.enums.SqlConstants; +import com.yunxi.scm.framework.mybatis.core.util.JdbcUtils; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.annotation.IdType; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; + +import java.util.Set; + +/** + * 当 IdType 为 {@link IdType#NONE} 时,根据 PRIMARY 数据源所使用的数据库,自动设置 + * + * @author 芋道源码 + */ +@Slf4j +public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor { + + private static final String ID_TYPE_KEY = "mybatis-plus.global-config.db-config.id-type"; + + private static final String DATASOURCE_DYNAMIC_KEY = "spring.datasource.dynamic"; + + private static final String QUARTZ_JOB_STORE_DRIVER_KEY = "spring.quartz.properties.org.quartz.jobStore.driverDelegateClass"; + + private static final Set INPUT_ID_TYPES = SetUtils.asSet(DbType.ORACLE, DbType.ORACLE_12C, + DbType.POSTGRE_SQL, DbType.KINGBASE_ES, DbType.DB2, DbType.H2); + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + // 如果获取不到 DbType,则不进行处理 + DbType dbType = getDbType(environment); + if (dbType == null) { + return; + } + + // 设置 Quartz JobStore 对应的 Driver + // TODO 芋艿:暂时没有找到特别合适的地方,先放在这里 + setJobStoreDriverIfPresent(environment, dbType); + + // 初始化 SQL 静态变量 + SqlConstants.init(dbType); + + // 如果非 NONE,则不进行处理 + IdType idType = getIdType(environment); + if (idType != IdType.NONE) { + return; + } + // 情况一,用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 + if (INPUT_ID_TYPES.contains(dbType)) { + setIdType(environment, IdType.INPUT); + return; + } + // 情况二,自增 ID,适合 MySQL 等直接自增的数据库 + setIdType(environment, IdType.AUTO); + } + + public IdType getIdType(ConfigurableEnvironment environment) { + return environment.getProperty(ID_TYPE_KEY, IdType.class); + } + + public void setIdType(ConfigurableEnvironment environment, IdType idType) { + environment.getSystemProperties().put(ID_TYPE_KEY, idType); + log.info("[setIdType][修改 MyBatis Plus 的 idType 为({})]", idType); + } + + public void setJobStoreDriverIfPresent(ConfigurableEnvironment environment, DbType dbType) { + String driverClass = environment.getProperty(QUARTZ_JOB_STORE_DRIVER_KEY); + if (StrUtil.isNotEmpty(driverClass)) { + return; + } + // 根据 dbType 类型,获取对应的 driverClass + switch (dbType) { + case POSTGRE_SQL: + driverClass = "org.quartz.impl.jdbcjobstore.PostgreSQLDelegate"; + break; + case ORACLE: + case ORACLE_12C: + driverClass = "org.quartz.impl.jdbcjobstore.oracle.OracleDelegate"; + break; + case SQL_SERVER: + case SQL_SERVER2005: + driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate"; + break; + } + // 设置 driverClass 变量 + if (StrUtil.isNotEmpty(driverClass)) { + environment.getSystemProperties().put(QUARTZ_JOB_STORE_DRIVER_KEY, driverClass); + } + } + + public static DbType getDbType(ConfigurableEnvironment environment) { + String primary = environment.getProperty(DATASOURCE_DYNAMIC_KEY + "." + "primary"); + if (StrUtil.isEmpty(primary)) { + return null; + } + String url = environment.getProperty(DATASOURCE_DYNAMIC_KEY + ".datasource." + primary + ".url"); + if (StrUtil.isEmpty(url)) { + return null; + } + return JdbcUtils.getDbType(url); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/config/YunxiMybatisAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/config/YunxiMybatisAutoConfiguration.java new file mode 100644 index 0000000..2f4471e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/config/YunxiMybatisAutoConfiguration.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.framework.mybatis.config; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.mybatis.core.handler.DefaultDBFieldHandler; +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator; +import com.baomidou.mybatisplus.extension.incrementer.*; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.apache.ibatis.annotations.Mapper; +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * MyBaits 配置类 + * + * @author 芋道源码 + */ +@AutoConfiguration +@MapperScan(value = "${yunxi.info.base-package}", annotationClass = Mapper.class, + lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试 +public class YunxiMybatisAutoConfiguration { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); + mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件 + return mybatisPlusInterceptor; + } + + @Bean + public MetaObjectHandler defaultMetaObjectHandler(){ + return new DefaultDBFieldHandler(); // 自动填充参数类 + } + + @Bean + @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT") + public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) { + DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment); + if (dbType != null) { + switch (dbType) { + case POSTGRE_SQL: + return new PostgreKeyGenerator(); + case ORACLE: + case ORACLE_12C: + return new OracleKeyGenerator(); + case H2: + return new H2KeyGenerator(); + case KINGBASE_ES: + return new KingbaseKeyGenerator(); + case DM: + return new DmKeyGenerator(); + } + } + // 找不到合适的 IKeyGenerator 实现类 + throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/dataobject/BaseDO.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/dataobject/BaseDO.java new file mode 100644 index 0000000..cf21078 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/dataobject/BaseDO.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.framework.mybatis.core.dataobject; + +import com.baomidou.mybatisplus.annotation.FieldFill; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableLogic; +import lombok.Data; +import org.apache.ibatis.type.JdbcType; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 基础实体对象 + * + * @author 芋道源码 + */ +@Data +public abstract class BaseDO implements Serializable { + + /** + * 创建时间 + */ + @TableField(fill = FieldFill.INSERT) + private LocalDateTime createTime; + /** + * 最后更新时间 + */ + @TableField(fill = FieldFill.INSERT_UPDATE) + private LocalDateTime updateTime; + /** + * 创建者,目前使用 SysUser 的 id 编号 + * + * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 + */ + @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR) + private String creator; + /** + * 更新者,目前使用 SysUser 的 id 编号 + * + * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。 + */ + @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR) + private String updater; + /** + * 是否删除 + */ + @TableLogic + private Boolean deleted; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/enums/SqlConstants.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/enums/SqlConstants.java new file mode 100644 index 0000000..4815124 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/enums/SqlConstants.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.mybatis.core.enums; + +import com.baomidou.mybatisplus.annotation.DbType; + +/** + * SQL相关常量类 + * + * @author 芋道源码 + */ +public class SqlConstants { + + /** + * 数据库的类型 + */ + public static DbType DB_TYPE; + + public static void init(DbType dbType) { + DB_TYPE = dbType; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/handler/DefaultDBFieldHandler.java new file mode 100644 index 0000000..1ad73dd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.framework.mybatis.core.handler; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import org.apache.ibatis.reflection.MetaObject; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * 通用参数填充实现类 + * + * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值 + * + * @author hexiaowu + */ +public class DefaultDBFieldHandler implements MetaObjectHandler { + + @Override + public void insertFill(MetaObject metaObject) { + if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) { + BaseDO baseDO = (BaseDO) metaObject.getOriginalObject(); + + LocalDateTime current = LocalDateTime.now(); + // 创建时间为空,则以当前时间为插入时间 + if (Objects.isNull(baseDO.getCreateTime())) { + baseDO.setCreateTime(current); + } + // 更新时间为空,则以当前时间为更新时间 + if (Objects.isNull(baseDO.getUpdateTime())) { + baseDO.setUpdateTime(current); + } + + Long userId = WebFrameworkUtils.getLoginUserId(); + // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 + if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { + baseDO.setCreator(userId.toString()); + } + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 + if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) { + baseDO.setUpdater(userId.toString()); + } + } + } + + @Override + public void updateFill(MetaObject metaObject) { + // 更新时间为空,则以当前时间为更新时间 + Object modifyTime = getFieldValByName("updateTime", metaObject); + if (Objects.isNull(modifyTime)) { + setFieldValByName("updateTime", LocalDateTime.now(), metaObject); + } + + // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 + Object modifier = getFieldValByName("updater", metaObject); + Long userId = WebFrameworkUtils.getLoginUserId(); + if (Objects.nonNull(userId) && Objects.isNull(modifier)) { + setFieldValByName("updater", userId.toString(), metaObject); + } + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/mapper/BaseMapperX.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/mapper/BaseMapperX.java new file mode 100644 index 0000000..c749db5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/mapper/BaseMapperX.java @@ -0,0 +1,118 @@ +package com.yunxi.scm.framework.mybatis.core.mapper; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.util.MyBatisUtils; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import com.baomidou.mybatisplus.extension.toolkit.Db; +import org.apache.ibatis.annotations.Param; + +import java.util.Collection; +import java.util.List; + +/** + * 在 MyBatis Plus 的 BaseMapper 的基础上拓展,提供更多的能力 + */ +public interface BaseMapperX extends BaseMapper { + + default PageResult selectPage(PageParam pageParam, @Param("ew") Wrapper queryWrapper) { + // MyBatis Plus 查询 + IPage mpPage = MyBatisUtils.buildPage(pageParam); + selectPage(mpPage, queryWrapper); + // 转换返回 + return new PageResult<>(mpPage.getRecords(), mpPage.getTotal()); + } + + default T selectOne(String field, Object value) { + return selectOne(new QueryWrapper().eq(field, value)); + } + + default T selectOne(SFunction field, Object value) { + return selectOne(new LambdaQueryWrapper().eq(field, value)); + } + + default T selectOne(String field1, Object value1, String field2, Object value2) { + return selectOne(new QueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + default T selectOne(SFunction field1, Object value1, SFunction field2, Object value2) { + return selectOne(new LambdaQueryWrapper().eq(field1, value1).eq(field2, value2)); + } + + default Long selectCount() { + return selectCount(new QueryWrapper()); + } + + default Long selectCount(String field, Object value) { + return selectCount(new QueryWrapper().eq(field, value)); + } + + default Long selectCount(SFunction field, Object value) { + return selectCount(new LambdaQueryWrapper().eq(field, value)); + } + + default List selectList() { + return selectList(new QueryWrapper<>()); + } + + default List selectList(String field, Object value) { + return selectList(new QueryWrapper().eq(field, value)); + } + + default List selectList(SFunction field, Object value) { + return selectList(new LambdaQueryWrapper().eq(field, value)); + } + + default List selectList(String field, Collection values) { + return selectList(new QueryWrapper().in(field, values)); + } + + default List selectList(SFunction field, Collection values) { + return selectList(new LambdaQueryWrapper().in(field, values)); + } + + default List selectList(SFunction leField, SFunction geField, Object value) { + return selectList(new LambdaQueryWrapper().le(leField, value).ge(geField, value)); + } + + /** + * 批量插入,适合大量数据插入 + * + * @param entities 实体们 + */ + default void insertBatch(Collection entities) { + Db.saveBatch(entities); + } + + /** + * 批量插入,适合大量数据插入 + * + * @param entities 实体们 + * @param size 插入数量 Db.saveBatch 默认为 1000 + */ + default void insertBatch(Collection entities, int size) { + Db.saveBatch(entities, size); + } + + default void updateBatch(T update) { + update(update, new QueryWrapper<>()); + } + + default void updateBatch(Collection entities) { + Db.updateBatchById(entities); + } + + default void updateBatch(Collection entities, int size) { + Db.updateBatchById(entities, size); + } + + default void saveOrUpdateBatch(Collection collection) { + Db.saveOrUpdateBatch(collection); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/query/LambdaQueryWrapperX.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/query/LambdaQueryWrapperX.java new file mode 100644 index 0000000..ca7ae7e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/query/LambdaQueryWrapperX.java @@ -0,0 +1,135 @@ +package com.yunxi.scm.framework.mybatis.core.query; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.support.SFunction; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + *

+ * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class LambdaQueryWrapperX extends LambdaQueryWrapper { + + public LambdaQueryWrapperX likeIfPresent(SFunction column, String val) { + if (StringUtils.hasText(val)) { + return (LambdaQueryWrapperX) super.like(column, val); + } + return this; + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Collection values) { + if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { + return (LambdaQueryWrapperX) super.in(column, values); + } + return this; + } + + public LambdaQueryWrapperX inIfPresent(SFunction column, Object... values) { + if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { + return (LambdaQueryWrapperX) super.in(column, values); + } + return this; + } + + public LambdaQueryWrapperX eqIfPresent(SFunction column, Object val) { + if (ObjectUtil.isNotEmpty(val)) { + return (LambdaQueryWrapperX) super.eq(column, val); + } + return this; + } + + public LambdaQueryWrapperX neIfPresent(SFunction column, Object val) { + if (ObjectUtil.isNotEmpty(val)) { + return (LambdaQueryWrapperX) super.ne(column, val); + } + return this; + } + + public LambdaQueryWrapperX gtIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.gt(column, val); + } + return this; + } + + public LambdaQueryWrapperX geIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.ge(column, val); + } + return this; + } + + public LambdaQueryWrapperX ltIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.lt(column, val); + } + return this; + } + + public LambdaQueryWrapperX leIfPresent(SFunction column, Object val) { + if (val != null) { + return (LambdaQueryWrapperX) super.le(column, val); + } + return this; + } + + public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (LambdaQueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (LambdaQueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (LambdaQueryWrapperX) le(column, val2); + } + return this; + } + + public LambdaQueryWrapperX betweenIfPresent(SFunction column, Object[] values) { + Object val1 = ArrayUtils.get(values, 0); + Object val2 = ArrayUtils.get(values, 1); + return betweenIfPresent(column, val1, val2); + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public LambdaQueryWrapperX eq(boolean condition, SFunction column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public LambdaQueryWrapperX eq(SFunction column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public LambdaQueryWrapperX orderByDesc(SFunction column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public LambdaQueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public LambdaQueryWrapperX in(SFunction column, Collection coll) { + super.in(column, coll); + return this; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/query/QueryWrapperX.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/query/QueryWrapperX.java new file mode 100644 index 0000000..c60dbb1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/query/QueryWrapperX.java @@ -0,0 +1,166 @@ +package com.yunxi.scm.framework.mybatis.core.query; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.mybatis.core.enums.SqlConstants; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; +import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.util.Collection; + +/** + * 拓展 MyBatis Plus QueryWrapper 类,主要增加如下功能: + * + * 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。 + * + * @param 数据类型 + */ +public class QueryWrapperX extends QueryWrapper { + + public QueryWrapperX likeIfPresent(String column, String val) { + if (StringUtils.hasText(val)) { + return (QueryWrapperX) super.like(column, val); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Collection values) { + if (!CollectionUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX inIfPresent(String column, Object... values) { + if (!ArrayUtils.isEmpty(values)) { + return (QueryWrapperX) super.in(column, values); + } + return this; + } + + public QueryWrapperX eqIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.eq(column, val); + } + return this; + } + + public QueryWrapperX neIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ne(column, val); + } + return this; + } + + public QueryWrapperX gtIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.gt(column, val); + } + return this; + } + + public QueryWrapperX geIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.ge(column, val); + } + return this; + } + + public QueryWrapperX ltIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.lt(column, val); + } + return this; + } + + public QueryWrapperX leIfPresent(String column, Object val) { + if (val != null) { + return (QueryWrapperX) super.le(column, val); + } + return this; + } + + public QueryWrapperX betweenIfPresent(String column, Object val1, Object val2) { + if (val1 != null && val2 != null) { + return (QueryWrapperX) super.between(column, val1, val2); + } + if (val1 != null) { + return (QueryWrapperX) ge(column, val1); + } + if (val2 != null) { + return (QueryWrapperX) le(column, val2); + } + return this; + } + + public QueryWrapperX betweenIfPresent(String column, Object[] values) { + if (values!= null && values.length != 0 && values[0] != null && values[1] != null) { + return (QueryWrapperX) super.between(column, values[0], values[1]); + } + if (values!= null && values.length != 0 && values[0] != null) { + return (QueryWrapperX) ge(column, values[0]); + } + if (values!= null && values.length != 0 && values[1] != null) { + return (QueryWrapperX) le(column, values[1]); + } + return this; + } + + // ========== 重写父类方法,方便链式调用 ========== + + @Override + public QueryWrapperX eq(boolean condition, String column, Object val) { + super.eq(condition, column, val); + return this; + } + + @Override + public QueryWrapperX eq(String column, Object val) { + super.eq(column, val); + return this; + } + + @Override + public QueryWrapperX orderByDesc(String column) { + super.orderByDesc(true, column); + return this; + } + + @Override + public QueryWrapperX last(String lastSql) { + super.last(lastSql); + return this; + } + + @Override + public QueryWrapperX in(String column, Collection coll) { + super.in(column, coll); + return this; + } + + /** + * 设置只返回最后一条 + * + * TODO 芋艿:不是完美解,需要在思考下。如果使用多数据源,并且数据源是多种类型时,可能会存在问题:实现之返回一条的语法不同 + * + * @return this + */ + public QueryWrapperX limitN(int n) { + Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型"); + switch (SqlConstants.DB_TYPE) { + case ORACLE: + case ORACLE_12C: + super.eq("ROWNUM", n); + break; + case SQL_SERVER: + case SQL_SERVER2005: + super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条,所以只好使用 * 查询剩余字段 + break; + default: + super.last("LIMIT " + n); + } + return this; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/EncryptTypeHandler.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/EncryptTypeHandler.java new file mode 100644 index 0000000..ea029a9 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/EncryptTypeHandler.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.framework.mybatis.core.type; + +import cn.hutool.core.lang.Assert; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.symmetric.AES; +import cn.hutool.extra.spring.SpringUtil; +import org.apache.ibatis.type.BaseTypeHandler; +import org.apache.ibatis.type.JdbcType; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * 字段字段的 TypeHandler 实现类,基于 {@link cn.hutool.crypto.symmetric.AES} 实现 + * 可通过 jasypt.encryptor.password 配置项,设置密钥 + * + * @author 芋道源码 + */ +public class EncryptTypeHandler extends BaseTypeHandler { + + private static final String ENCRYPTOR_PROPERTY_NAME = "mybatis-plus.encryptor.password"; + + private static AES aes; + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, encrypt(parameter)); + } + + @Override + public String getNullableResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return decrypt(value); + } + + @Override + public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return decrypt(value); + } + + @Override + public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return decrypt(value); + } + + private static String decrypt(String value) { + if (value == null) { + return null; + } + return getEncryptor().decryptStr(value); + } + + public static String encrypt(String rawValue) { + if (rawValue == null) { + return null; + } + return getEncryptor().encryptBase64(rawValue); + } + + private static AES getEncryptor() { + if (aes != null) { + return aes; + } + // 构建 AES + String password = SpringUtil.getProperty(ENCRYPTOR_PROPERTY_NAME); + Assert.notEmpty(password, "配置项({}) 不能为空", ENCRYPTOR_PROPERTY_NAME); + aes = SecureUtil.aes(password.getBytes()); + return aes; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/IntegerListTypeHandler.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/IntegerListTypeHandler.java new file mode 100644 index 0000000..b4aab1c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/IntegerListTypeHandler.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.util.string.StrUtils; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * List 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author jason + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class IntegerListTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public List getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public List getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public List getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private List getResult(String value) { + if (value == null) { + return null; + } + return StrUtils.splitToInteger(value, COMMA); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/JsonLongSetTypeHandler.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/JsonLongSetTypeHandler.java new file mode 100644 index 0000000..6d4e5c1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/JsonLongSetTypeHandler.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.framework.mybatis.core.type; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import com.fasterxml.jackson.core.type.TypeReference; + +import java.util.Set; + +/** + * 参考 {@link com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler} 实现 + * 在我们将字符串反序列化为 Set 并且泛型为 Long 时,如果每个元素的数值太小,会被处理成 Integer 类型,导致可能存在隐性的 BUG。 + * + * 例如说哦,SysUserDO 的 postIds 属性 + * + * @author 芋道源码 + */ +public class JsonLongSetTypeHandler extends AbstractJsonTypeHandler { + + private static final TypeReference> TYPE_REFERENCE = new TypeReference>(){}; + + @Override + protected Object parse(String json) { + return JsonUtils.parseObject(json, TYPE_REFERENCE); + } + + @Override + protected String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/LongListTypeHandler.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/LongListTypeHandler.java new file mode 100644 index 0000000..5d38ea5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/LongListTypeHandler.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.util.string.StrUtils; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * List 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author 芋道源码 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class LongListTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { + // 设置占位符 + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public List getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public List getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public List getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private List getResult(String value) { + if (value == null) { + return null; + } + return StrUtils.splitToLong(value, COMMA); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/StringListTypeHandler.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/StringListTypeHandler.java new file mode 100644 index 0000000..f151a7b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/type/StringListTypeHandler.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.framework.mybatis.core.type; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import org.apache.ibatis.type.JdbcType; +import org.apache.ibatis.type.MappedJdbcTypes; +import org.apache.ibatis.type.MappedTypes; +import org.apache.ibatis.type.TypeHandler; + +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * List 的类型转换器实现类,对应数据库的 varchar 类型 + * + * @author 永不言败 + * @since 2022 3/23 12:50:15 + */ +@MappedJdbcTypes(JdbcType.VARCHAR) +@MappedTypes(List.class) +public class StringListTypeHandler implements TypeHandler> { + + private static final String COMMA = ","; + + @Override + public void setParameter(PreparedStatement ps, int i, List strings, JdbcType jdbcType) throws SQLException { + // 设置占位符 + ps.setString(i, CollUtil.join(strings, COMMA)); + } + + @Override + public List getResult(ResultSet rs, String columnName) throws SQLException { + String value = rs.getString(columnName); + return getResult(value); + } + + @Override + public List getResult(ResultSet rs, int columnIndex) throws SQLException { + String value = rs.getString(columnIndex); + return getResult(value); + } + + @Override + public List getResult(CallableStatement cs, int columnIndex) throws SQLException { + String value = cs.getString(columnIndex); + return getResult(value); + } + + private List getResult(String value) { + if (value == null) { + return null; + } + return StrUtil.splitTrim(value, COMMA); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/util/JdbcUtils.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/util/JdbcUtils.java new file mode 100644 index 0000000..e124813 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/util/JdbcUtils.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.framework.mybatis.core.util; + +import com.baomidou.mybatisplus.annotation.DbType; + +import java.sql.Connection; +import java.sql.DriverManager; + +/** + * JDBC 工具类 + * + * @author 芋道源码 + */ +public class JdbcUtils { + + /** + * 判断连接是否正确 + * + * @param url 数据源连接 + * @param username 账号 + * @param password 密码 + * @return 是否正确 + */ + public static boolean isConnectionOK(String url, String username, String password) { + try (Connection ignored = DriverManager.getConnection(url, username, password)) { + return true; + } catch (Exception ex) { + return false; + } + } + + /** + * 获得 URL 对应的 DB 类型 + * + * @param url URL + * @return DB 类型 + */ + public static DbType getDbType(String url) { + String name = com.alibaba.druid.util.JdbcUtils.getDbType(url, null); + return DbType.getDbType(name); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/util/MyBatisUtils.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/util/MyBatisUtils.java new file mode 100644 index 0000000..778549b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/core/util/MyBatisUtils.java @@ -0,0 +1,88 @@ +package com.yunxi.scm.framework.mybatis.core.util; + +import cn.hutool.core.collection.CollectionUtil; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.SortingField; +import com.baomidou.mybatisplus.core.metadata.OrderItem; +import com.baomidou.mybatisplus.core.toolkit.StringPool; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import net.sf.jsqlparser.expression.Alias; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.schema.Table; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * MyBatis 工具类 + */ +public class MyBatisUtils { + + private static final String MYSQL_ESCAPE_CHARACTER = "`"; + + public static Page buildPage(PageParam pageParam) { + return buildPage(pageParam, null); + } + + public static Page buildPage(PageParam pageParam, Collection sortingFields) { + // 页码 + 数量 + Page page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); + // 排序字段 + if (!CollectionUtil.isEmpty(sortingFields)) { + page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? + OrderItem.asc(sortingField.getField()) : OrderItem.desc(sortingField.getField())) + .collect(Collectors.toList())); + } + return page; + } + + /** + * 将拦截器添加到链中 + * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 + * + * @param interceptor 链 + * @param inner 拦截器 + * @param index 位置 + */ + public static void addInterceptor(MybatisPlusInterceptor interceptor, InnerInterceptor inner, int index) { + List inners = new ArrayList<>(interceptor.getInterceptors()); + inners.add(index, inner); + interceptor.setInterceptors(inners); + } + + /** + * 获得 Table 对应的表名 + * + * 兼容 MySQL 转义表名 `t_xxx` + * + * @param table 表 + * @return 去除转移字符后的表名 + */ + public static String getTableName(Table table) { + String tableName = table.getName(); + if (tableName.startsWith(MYSQL_ESCAPE_CHARACTER) && tableName.endsWith(MYSQL_ESCAPE_CHARACTER)) { + tableName = tableName.substring(1, tableName.length() - 1); + } + return tableName; + } + + /** + * 构建 Column 对象 + * + * @param tableName 表名 + * @param tableAlias 别名 + * @param column 字段名 + * @return Column 对象 + */ + public static Column buildColumn(String tableName, Alias tableAlias, String column) { + if (tableAlias != null) { + tableName = tableAlias.getName(); + } + return new Column(tableName + StringPool.DOT + column); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/package-info.java new file mode 100644 index 0000000..041e913 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/mybatis/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 MyBatis Plus 提升使用 MyBatis 的开发效率 + */ +package com.yunxi.scm.framework.mybatis; diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/package-info.java new file mode 100644 index 0000000..686a299 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/java/com/yunxi/scm/framework/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.framework; diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..d000d44 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.env.EnvironmentPostProcessor=\ + com.yunxi.scm.framework.mybatis.config.IdTypeEnvironmentPostProcessor diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..368d98b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.yunxi.scm.framework.datasource.config.YunxiDataSourceAutoConfiguration +com.yunxi.scm.framework.mybatis.config.YunxiMybatisAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md new file mode 100644 index 0000000..a0bb0d5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot MyBatis 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md b/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md new file mode 100644 index 0000000..4aa3a89 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot 多数据源(读写分离)入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md b/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md new file mode 100644 index 0000000..18fde27 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-mybatis/《芋道 Spring Boot 数据库连接池入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-protection/pom.xml new file mode 100644 index 0000000..d16552f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/pom.xml @@ -0,0 +1,37 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-protection + jar + + ${project.artifactId} + 服务保证,提供分布式锁、幂等、限流、熔断等等功能 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + com.baomidou + lock4j-redisson-spring-boot-starter + + + + io.github.resilience4j + resilience4j-spring-boot2 + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/config/YunxiIdempotentConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/config/YunxiIdempotentConfiguration.java new file mode 100644 index 0000000..9614ddd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/config/YunxiIdempotentConfiguration.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.framework.idempotent.config; + +import com.yunxi.scm.framework.idempotent.core.aop.IdempotentAspect; +import com.yunxi.scm.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import com.yunxi.scm.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; +import com.yunxi.scm.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import com.yunxi.scm.framework.idempotent.core.redis.IdempotentRedisDAO; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.List; + +@AutoConfiguration(after = YunxiRedisAutoConfiguration.class) +public class YunxiIdempotentConfiguration { + + @Bean + public IdempotentAspect idempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { + return new IdempotentAspect(keyResolvers, idempotentRedisDAO); + } + + @Bean + public IdempotentRedisDAO idempotentRedisDAO(StringRedisTemplate stringRedisTemplate) { + return new IdempotentRedisDAO(stringRedisTemplate); + } + + // ========== 各种 IdempotentKeyResolver Bean ========== + + @Bean + public DefaultIdempotentKeyResolver defaultIdempotentKeyResolver() { + return new DefaultIdempotentKeyResolver(); + } + + @Bean + public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { + return new ExpressionIdempotentKeyResolver(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/annotation/Idempotent.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/annotation/Idempotent.java new file mode 100644 index 0000000..eae5fac --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/annotation/Idempotent.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.framework.idempotent.core.annotation; + +import com.yunxi.scm.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; +import com.yunxi.scm.framework.idempotent.core.keyresolver.IdempotentKeyResolver; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * 幂等注解 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface Idempotent { + + /** + * 幂等的超时时间,默认为 1 秒 + * + * 注意,如果执行时间超过它,请求还是会进来 + */ + int timeout() default 1; + /** + * 时间单位,默认为 SECONDS 秒 + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 提示信息,正在执行中的提示 + */ + String message() default "重复请求,请稍后重试"; + + /** + * 使用的 Key 解析器 + */ + Class keyResolver() default DefaultIdempotentKeyResolver.class; + /** + * 使用的 Key 参数 + */ + String keyArg() default ""; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/aop/IdempotentAspect.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/aop/IdempotentAspect.java new file mode 100644 index 0000000..e4438a5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/aop/IdempotentAspect.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.framework.idempotent.core.aop; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.idempotent.core.annotation.Idempotent; +import com.yunxi.scm.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import com.yunxi.scm.framework.idempotent.core.redis.IdempotentRedisDAO; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Map; + +/** + * 拦截声明了 {@link Idempotent} 注解的方法,实现幂等操作 + * + * @author 芋道源码 + */ +@Aspect +@Slf4j +public class IdempotentAspect { + + /** + * IdempotentKeyResolver 集合 + */ + private final Map, IdempotentKeyResolver> keyResolvers; + + private final IdempotentRedisDAO idempotentRedisDAO; + + public IdempotentAspect(List keyResolvers, IdempotentRedisDAO idempotentRedisDAO) { + this.keyResolvers = CollectionUtils.convertMap(keyResolvers, IdempotentKeyResolver::getClass); + this.idempotentRedisDAO = idempotentRedisDAO; + } + + @Before("@annotation(idempotent)") + public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) { + // 获得 IdempotentKeyResolver + IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); + Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); + // 解析 Key + String key = keyResolver.resolver(joinPoint, idempotent); + + // 锁定 Key。 + boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); + // 锁定失败,抛出异常 + if (!success) { + log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); + throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java new file mode 100644 index 0000000..8ac806c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/IdempotentKeyResolver.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.framework.idempotent.core.keyresolver; + +import com.yunxi.scm.framework.idempotent.core.annotation.Idempotent; +import org.aspectj.lang.JoinPoint; + +/** + * 幂等 Key 解析器接口 + * + * @author 芋道源码 + */ +public interface IdempotentKeyResolver { + + /** + * 解析一个 Key + * + * @param idempotent 幂等注解 + * @param joinPoint AOP 切面 + * @return Key + */ + String resolver(JoinPoint joinPoint, Idempotent idempotent); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java new file mode 100644 index 0000000..d96c6d1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import com.yunxi.scm.framework.idempotent.core.annotation.Idempotent; +import com.yunxi.scm.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; + +/** + * 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key + * + * 为了避免 Key 过长,使用 MD5 进行“压缩” + * + * @author 芋道源码 + */ +public class DefaultIdempotentKeyResolver implements IdempotentKeyResolver { + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + String methodName = joinPoint.getSignature().toString(); + String argsStr = StrUtil.join(",", joinPoint.getArgs()); + return SecureUtil.md5(methodName + argsStr); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java new file mode 100644 index 0000000..bed0563 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.framework.idempotent.core.keyresolver.impl; + +import cn.hutool.core.util.ArrayUtil; +import com.yunxi.scm.framework.idempotent.core.annotation.Idempotent; +import com.yunxi.scm.framework.idempotent.core.keyresolver.IdempotentKeyResolver; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * 基于 Spring EL 表达式, + * + * @author 芋道源码 + */ +public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { + + private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + private final ExpressionParser expressionParser = new SpelExpressionParser(); + + @Override + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { + // 获得被拦截方法参数名列表 + Method method = getMethod(joinPoint); + Object[] args = joinPoint.getArgs(); + String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); + // 准备 Spring EL 表达式解析的上下文 + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + if (ArrayUtil.isNotEmpty(parameterNames)) { + for (int i = 0; i < parameterNames.length; i++) { + evaluationContext.setVariable(parameterNames[i], args[i]); + } + } + + // 解析参数 + Expression expression = expressionParser.parseExpression(idempotent.keyArg()); + return expression.getValue(evaluationContext, String.class); + } + + private static Method getMethod(JoinPoint point) { + // 处理,声明在类上的情况 + MethodSignature signature = (MethodSignature) point.getSignature(); + Method method = signature.getMethod(); + if (!method.getDeclaringClass().isInterface()) { + return method; + } + + // 处理,声明在接口上的情况 + try { + return point.getTarget().getClass().getDeclaredMethod( + point.getSignature().getName(), method.getParameterTypes()); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/redis/IdempotentRedisDAO.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/redis/IdempotentRedisDAO.java new file mode 100644 index 0000000..e6319d1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/core/redis/IdempotentRedisDAO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.idempotent.core.redis; + +import lombok.AllArgsConstructor; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.concurrent.TimeUnit; + +/** + * 幂等 Redis DAO + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class IdempotentRedisDAO { + + /** + * 幂等操作 + * + * KEY 格式:idempotent:%s // 参数为 uuid + * VALUE 格式:String + * 过期时间:不固定 + */ + private static final String IDEMPOTENT = "idempotent:%s"; + + private final StringRedisTemplate redisTemplate; + + public Boolean setIfAbsent(String key, long timeout, TimeUnit timeUnit) { + String redisKey = formatKey(key); + return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); + } + + private static String formatKey(String key) { + return String.format(IDEMPOTENT, key); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/package-info.java new file mode 100644 index 0000000..a6df253 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/idempotent/package-info.java @@ -0,0 +1,12 @@ +/** + * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现 + * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。 + * + * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。 + * + * 和 it4alla/idempotent 组件的差异点,主要体现在两点: + * 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力 + * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。 + * 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。 + */ +package com.yunxi.scm.framework.idempotent; diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/config/YunxiLock4jConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/config/YunxiLock4jConfiguration.java new file mode 100644 index 0000000..d695266 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/config/YunxiLock4jConfiguration.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.framework.lock4j.config; + +import com.yunxi.scm.framework.lock4j.core.DefaultLockFailureStrategy; +import com.baomidou.lock.spring.boot.autoconfigure.LockAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration(before = LockAutoConfiguration.class) +public class YunxiLock4jConfiguration { + + @Bean + public DefaultLockFailureStrategy lockFailureStrategy() { + return new DefaultLockFailureStrategy(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/core/DefaultLockFailureStrategy.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/core/DefaultLockFailureStrategy.java new file mode 100644 index 0000000..34bb6c1 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/core/DefaultLockFailureStrategy.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.lock4j.core; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.baomidou.lock.LockFailureStrategy; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.Method; + +/** + * 自定义获取锁失败策略,抛出 {@link ServiceException} 异常 + */ +@Slf4j +public class DefaultLockFailureStrategy implements LockFailureStrategy { + + @Override + public void onLockFailure(String key, Method method, Object[] arguments) { + log.debug("[onLockFailure][线程:{} 获取锁失败,key:{} 获取失败:{} ]", Thread.currentThread().getName(), key, arguments); + throw new ServiceException(GlobalErrorCodeConstants.LOCKED); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/core/Lock4jRedisKeyConstants.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/core/Lock4jRedisKeyConstants.java new file mode 100644 index 0000000..7872f73 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/core/Lock4jRedisKeyConstants.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.framework.lock4j.core; + +/** + * Lock4j Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface Lock4jRedisKeyConstants { + + /** + * 分布式锁 + * + * KEY 格式:lock4j:%s // 参数来自 DefaultLockKeyBuilder 类 + * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 + * 过期时间:不固定 + */ + String LOCK4J = "lock4j:%s"; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/package-info.java new file mode 100644 index 0000000..a76b337 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/lock4j/package-info.java @@ -0,0 +1,4 @@ +/** + * 分布式锁组件,使用 https://gitee.com/baomidou/lock4j 开源项目 + */ +package com.yunxi.scm.framework.lock4j; diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/resilience4j/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/resilience4j/package-info.java new file mode 100644 index 0000000..db498db --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/resilience4j/package-info.java @@ -0,0 +1,9 @@ +/** + * 使用 Resilience4j 组件,实现服务保障,包括: + * 1. 熔断器 + * 2. 限流器 + * 3. 舱壁隔离 + * 4. 重试 + * 5. 限时器 + */ +package com.yunxi.scm.framework.resilience4j; diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md new file mode 100644 index 0000000..b4cd896 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/java/com/yunxi/scm/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..94e4aa7 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-protection/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.yunxi.scm.framework.idempotent.config.YunxiIdempotentConfiguration +com.yunxi.scm.framework.lock4j.config.YunxiLock4jConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-redis/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-redis/pom.xml new file mode 100644 index 0000000..7cd7a57 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-redis/pom.xml @@ -0,0 +1,47 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-redis + jar + + ${project.artifactId} + Redis 封装拓展 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.redisson + redisson-spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-cache + + + + io.netty + netty-all + + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/config/YunxiCacheAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/config/YunxiCacheAutoConfiguration.java new file mode 100644 index 0000000..3f03e7e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/config/YunxiCacheAutoConfiguration.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.framework.redis.config; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.redis.core.TimeoutRedisCacheManager; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.cache.CacheProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializationContext; + +import java.util.Objects; + +import static com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration.buildRedisSerializer; + +/** + * Cache 配置类,基于 Redis 实现 + */ +@AutoConfiguration +@EnableConfigurationProperties({CacheProperties.class}) +@EnableCaching +public class YunxiCacheAutoConfiguration { + + /** + * RedisCacheConfiguration Bean + *

+ * 参考 org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration 的 createConfiguration 方法 + */ + @Bean + @Primary + public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); + // 设置使用 : 单冒号,而不是双 :: 冒号,避免 Redis Desktop Manager 多余空格 + // 详细可见 https://blog.csdn.net/chuixue24/article/details/103928965 博客 + config = config.computePrefixWith(cacheName -> cacheName + StrUtil.COLON); + // 设置使用 JSON 序列化方式 + config = config.serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(buildRedisSerializer())); + + // 设置 CacheProperties.Redis 的属性 + CacheProperties.Redis redisProperties = cacheProperties.getRedis(); + if (redisProperties.getTimeToLive() != null) { + config = config.entryTtl(redisProperties.getTimeToLive()); + } + if (redisProperties.getKeyPrefix() != null) { + config = config.prefixCacheNameWith(redisProperties.getKeyPrefix()); + } + if (!redisProperties.isCacheNullValues()) { + config = config.disableCachingNullValues(); + } + if (!redisProperties.isUseKeyPrefix()) { + config = config.disableKeyPrefix(); + } + return config; + } + + @Bean + public RedisCacheManager redisCacheManager(RedisTemplate redisTemplate, + RedisCacheConfiguration redisCacheConfiguration) { + // 创建 RedisCacheWriter 对象 + RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); + RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory); + // 创建 TenantRedisCacheManager 对象 + return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/config/YunxiRedisAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/config/YunxiRedisAutoConfiguration.java new file mode 100644 index 0000000..37aa942 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/config/YunxiRedisAutoConfiguration.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.framework.redis.config; + +import cn.hutool.core.util.ReflectUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.RedisSerializer; + +/** + * Redis 配置类 + */ +@AutoConfiguration +public class YunxiRedisAutoConfiguration { + + /** + * 创建 RedisTemplate Bean,使用 JSON 序列化方式 + */ + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + // 创建 RedisTemplate 对象 + RedisTemplate template = new RedisTemplate<>(); + // 设置 RedisConnection 工厂。😈 它就是实现多种 Java Redis 客户端接入的秘密工厂。感兴趣的胖友,可以自己去撸下。 + template.setConnectionFactory(factory); + // 使用 String 序列化方式,序列化 KEY 。 + template.setKeySerializer(RedisSerializer.string()); + template.setHashKeySerializer(RedisSerializer.string()); + // 使用 JSON 序列化方式(库是 Jackson ),序列化 VALUE 。 + template.setValueSerializer(buildRedisSerializer()); + template.setHashValueSerializer(buildRedisSerializer()); + return template; + } + + public static RedisSerializer buildRedisSerializer() { + RedisSerializer json = RedisSerializer.json(); + // 解决 LocalDateTime 的序列化 + ObjectMapper objectMapper = (ObjectMapper) ReflectUtil.getFieldValue(json, "mapper"); + objectMapper.registerModules(new JavaTimeModule()); + return json; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/core/TimeoutRedisCacheManager.java b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/core/TimeoutRedisCacheManager.java new file mode 100644 index 0000000..396f2b7 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/core/TimeoutRedisCacheManager.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.framework.redis.core; + +import cn.hutool.core.util.StrUtil; +import org.springframework.boot.convert.DurationStyle; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.redis.cache.RedisCache; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +/** + * 支持自定义过期时间的 {@link RedisCacheManager} 实现类 + * + * 在 {@link Cacheable#cacheNames()} 格式为 "key#ttl" 时,# 后面的 ttl 为过期时间,单位为秒 + * + * @author 芋道源码 + */ +public class TimeoutRedisCacheManager extends RedisCacheManager { + + private static final String SPLIT = "#"; + + public TimeoutRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) { + super(cacheWriter, defaultCacheConfiguration); + } + + @Override + protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) { + if (StrUtil.isEmpty(name)) { + return super.createRedisCache(name, cacheConfig); + } + // 如果使用 # 分隔,大小不为 2,则说明不使用自定义过期时间 + String[] names = StrUtil.splitToArray(name, SPLIT); + if (names.length != 2) { + return super.createRedisCache(name, cacheConfig); + } + + // 核心:通过修改 cacheConfig 的过期时间,实现自定义过期时间 + if (cacheConfig != null) { + // 移除 # 后面的 : 以及后面的内容,避免影响解析 + names[1] = StrUtil.subBefore(names[1], StrUtil.COLON, false); + // 解析时间 + Duration duration = DurationStyle.detectAndParse(names[1], ChronoUnit.SECONDS); + cacheConfig = cacheConfig.entryTtl(duration); + } + return super.createRedisCache(names[0], cacheConfig); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/package-info.java new file mode 100644 index 0000000..24fa802 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/java/com/yunxi/scm/framework/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * 采用 Spring Data Redis 操作 Redis,底层使用 Redisson 作为客户端 + */ +package com.yunxi.scm.framework.redis; diff --git a/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..8fdb1aa --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-redis/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration +com.yunxi.scm.framework.redis.config.YunxiCacheAutoConfiguration \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md new file mode 100644 index 0000000..9d74d69 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-redis/《芋道 Spring Boot Cache 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md new file mode 100644 index 0000000..91bf118 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-redis/《芋道 Spring Boot Redis 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-security/pom.xml new file mode 100644 index 0000000..875d8c8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/pom.xml @@ -0,0 +1,61 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-security + jar + + ${project.artifactId} + 用户的认证、权限的校验 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.google.guava + guava + + + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/AuthorizeRequestsCustomizer.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/AuthorizeRequestsCustomizer.java new file mode 100644 index 0000000..366e66a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/AuthorizeRequestsCustomizer.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.security.config; + +import com.yunxi.scm.framework.web.config.WebProperties; +import org.springframework.core.Ordered; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +import javax.annotation.Resource; + +/** + * 自定义的 URL 的安全配置 + * 目的:每个 Maven Module 可以自定义规则! + * + * @author 芋道源码 + */ +public abstract class AuthorizeRequestsCustomizer + implements Customizer.ExpressionInterceptUrlRegistry>, Ordered { + + @Resource + private WebProperties webProperties; + + protected String buildAdminApi(String url) { + return webProperties.getAdminApi().getPrefix() + url; + } + + protected String buildAppApi(String url) { + return webProperties.getAppApi().getPrefix() + url; + } + + @Override + public int getOrder() { + return 0; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/SecurityProperties.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/SecurityProperties.java new file mode 100644 index 0000000..a86d54b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/SecurityProperties.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.framework.security.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.List; + +@ConfigurationProperties(prefix = "yunxi.security") +@Validated +@Data +public class SecurityProperties { + + /** + * HTTP 请求时,访问令牌的请求 Header + */ + @NotEmpty(message = "Token Header 不能为空") + private String tokenHeader = "Authorization"; + + /** + * mock 模式的开关 + */ + @NotNull(message = "mock 模式的开关不能为空") + private Boolean mockEnable = false; + /** + * mock 模式的密钥 + * 一定要配置密钥,保证安全性 + */ + @NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。 + private String mockSecret = "test"; + + /** + * 免登录的 URL 列表 + */ + private List permitAllUrls = Collections.emptyList(); + + /** + * PasswordEncoder 加密复杂度,越高开销越大 + */ + private Integer passwordEncoderLength = 4; +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/YunxiSecurityAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/YunxiSecurityAutoConfiguration.java new file mode 100644 index 0000000..77f15bc --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/YunxiSecurityAutoConfiguration.java @@ -0,0 +1,102 @@ +package com.yunxi.scm.framework.security.config; + +import com.yunxi.scm.framework.security.core.aop.PreAuthenticatedAspect; +import com.yunxi.scm.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; +import com.yunxi.scm.framework.security.core.filter.TokenAuthenticationFilter; +import com.yunxi.scm.framework.security.core.handler.AccessDeniedHandlerImpl; +import com.yunxi.scm.framework.security.core.handler.AuthenticationEntryPointImpl; +import com.yunxi.scm.framework.security.core.service.SecurityFrameworkService; +import com.yunxi.scm.framework.security.core.service.SecurityFrameworkServiceImpl; +import com.yunxi.scm.framework.web.core.handler.GlobalExceptionHandler; +import com.yunxi.scm.module.system.api.oauth2.OAuth2TokenApi; +import com.yunxi.scm.module.system.api.permission.PermissionApi; +import org.springframework.beans.factory.config.MethodInvokingFactoryBean; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.AccessDeniedHandler; + +import javax.annotation.Resource; + +/** + * Spring Security 自动配置类,主要用于相关组件的配置 + * + * 注意,不能和 {@link YunxiWebSecurityConfigurerAdapter} 用一个,原因是会导致初始化报错。 + * 参见 https://stackoverflow.com/questions/53847050/spring-boot-delegatebuilder-cannot-be-null-on-autowiring-authenticationmanager 文档。 + * + * @author 芋道源码 + */ +@AutoConfiguration +@EnableConfigurationProperties(SecurityProperties.class) +public class YunxiSecurityAutoConfiguration { + + @Resource + private SecurityProperties securityProperties; + + /** + * 处理用户未登录拦截的切面的 Bean + */ + @Bean + public PreAuthenticatedAspect preAuthenticatedAspect() { + return new PreAuthenticatedAspect(); + } + + /** + * 认证失败处理类 Bean + */ + @Bean + public AuthenticationEntryPoint authenticationEntryPoint() { + return new AuthenticationEntryPointImpl(); + } + + /** + * 权限不够处理器 Bean + */ + @Bean + public AccessDeniedHandler accessDeniedHandler() { + return new AccessDeniedHandlerImpl(); + } + + /** + * Spring Security 加密器 + * 考虑到安全性,这里采用 BCryptPasswordEncoder 加密器 + * + * @see Password Encoding with Spring Security + */ + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(securityProperties.getPasswordEncoderLength()); + } + + /** + * Token 认证过滤器 Bean + */ + @Bean + public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler, + OAuth2TokenApi oauth2TokenApi) { + return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi); + } + + @Bean("ss") // 使用 Spring Security 的缩写,方便使用 + public SecurityFrameworkService securityFrameworkService(PermissionApi permissionApi) { + return new SecurityFrameworkServiceImpl(permissionApi); + } + + /** + * 声明调用 {@link SecurityContextHolder#setStrategyName(String)} 方法, + * 设置使用 {@link TransmittableThreadLocalSecurityContextHolderStrategy} 作为 Security 的上下文策略 + */ + @Bean + public MethodInvokingFactoryBean securityContextHolderMethodInvokingFactoryBean() { + MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); + methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class); + methodInvokingFactoryBean.setTargetMethod("setStrategyName"); + methodInvokingFactoryBean.setArguments(TransmittableThreadLocalSecurityContextHolderStrategy.class.getName()); + return methodInvokingFactoryBean; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/YunxiWebSecurityConfigurerAdapter.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/YunxiWebSecurityConfigurerAdapter.java new file mode 100644 index 0000000..ab4edcf --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/config/YunxiWebSecurityConfigurerAdapter.java @@ -0,0 +1,188 @@ +package com.yunxi.scm.framework.security.config; + +import com.yunxi.scm.framework.security.core.filter.TokenAuthenticationFilter; +import com.yunxi.scm.framework.web.config.WebProperties; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 自定义的 Spring Security 配置适配器实现 + * + * @author 芋道源码 + */ +@AutoConfiguration +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class YunxiWebSecurityConfigurerAdapter { + + @Resource + private WebProperties webProperties; + @Resource + private SecurityProperties securityProperties; + + /** + * 认证失败处理类 Bean + */ + @Resource + private AuthenticationEntryPoint authenticationEntryPoint; + /** + * 权限不够处理器 Bean + */ + @Resource + private AccessDeniedHandler accessDeniedHandler; + /** + * Token 认证过滤器 Bean + */ + @Resource + private TokenAuthenticationFilter authenticationTokenFilter; + + /** + * 自定义的权限映射 Bean 们 + * + * @see #filterChain(HttpSecurity) + */ + @Resource + private List authorizeRequestsCustomizers; + + @Resource + private ApplicationContext applicationContext; + + /** + * 由于 Spring Security 创建 AuthenticationManager 对象时,没声明 @Bean 注解,导致无法被注入 + * 通过覆写父类的该方法,添加 @Bean 注解,解决该问题 + */ + @Bean + public AuthenticationManager authenticationManagerBean(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + /** + * 配置 URL 的安全配置 + * + * anyRequest | 匹配所有请求路径 + * access | SpringEl表达式结果为true时可以访问 + * anonymous | 匿名可以访问 + * denyAll | 用户不能访问 + * fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录) + * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 + * hasAnyRole | 如果有参数,参数表示角色,则其中任何一个角色可以访问 + * hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 + * hasIpAddress | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 + * hasRole | 如果有参数,参数表示角色,则其角色可以访问 + * permitAll | 用户可以任意访问 + * rememberMe | 允许通过remember-me登录的用户访问 + * authenticated | 用户登录后可访问 + */ + @Bean + protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { + // 登出 + httpSecurity + // 开启跨域 + .cors().and() + // CSRF 禁用,因为不使用 Session + .csrf().disable() + // 基于 token 机制,所以不需要 Session + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() + .headers().frameOptions().disable().and() + // 一堆自定义的 Spring Security 处理器 + .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint) + .accessDeniedHandler(accessDeniedHandler); + // 登录、登录暂时不使用 Spring Security 的拓展点,主要考虑一方面拓展多用户、多种登录方式相对复杂,一方面用户的学习成本较高 + + // 获得 @PermitAll 带来的 URL 列表,免登录 + Multimap permitAllUrls = getPermitAllUrlsFromAnnotations(); + // 设置每个请求的权限 + httpSecurity + // ①:全局共享规则 + .authorizeRequests() + // 1.1 静态资源,可匿名访问 + .antMatchers(HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll() + // 1.2 设置 @PermitAll 无需认证 + .antMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() + .antMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() + // 1.3 基于 yunxi.security.permit-all-urls 无需认证 + .antMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() + // 1.4 设置 App API 无需认证 + .antMatchers(buildAppApi("/**")).permitAll() + // 1.5 验证码captcha 允许匿名访问 + .antMatchers("/captcha/get", "/captcha/check").permitAll() + // 1.6 webSocket 允许匿名访问 + .antMatchers("/websocket/message").permitAll() + // ②:每个项目的自定义规则 + .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 + authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) + // ③:兜底规则,必须认证 + .authorizeRequests() + .anyRequest().authenticated() + ; + + // 添加 Token Filter + httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + return httpSecurity.build(); + } + + private String buildAppApi(String url) { + return webProperties.getAppApi().getPrefix() + url; + } + + private Multimap getPermitAllUrlsFromAnnotations() { + Multimap result = HashMultimap.create(); + // 获得接口对应的 HandlerMethod 集合 + RequestMappingHandlerMapping requestMappingHandlerMapping = (RequestMappingHandlerMapping) + applicationContext.getBean("requestMappingHandlerMapping"); + Map handlerMethodMap = requestMappingHandlerMapping.getHandlerMethods(); + // 获得有 @PermitAll 注解的接口 + for (Map.Entry entry : handlerMethodMap.entrySet()) { + HandlerMethod handlerMethod = entry.getValue(); + if (!handlerMethod.hasMethodAnnotation(PermitAll.class)) { + continue; + } + if (entry.getKey().getPatternsCondition() == null) { + continue; + } + Set urls = entry.getKey().getPatternsCondition().getPatterns(); + // 根据请求方法,添加到 result 结果 + entry.getKey().getMethodsCondition().getMethods().forEach(requestMethod -> { + switch (requestMethod) { + case GET: + result.putAll(HttpMethod.GET, urls); + break; + case POST: + result.putAll(HttpMethod.POST, urls); + break; + case PUT: + result.putAll(HttpMethod.PUT, urls); + break; + case DELETE: + result.putAll(HttpMethod.DELETE, urls); + break; + } + }); + } + return result; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/LoginUser.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/LoginUser.java new file mode 100644 index 0000000..66aaf50 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/LoginUser.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.framework.security.core; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.Data; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 登录用户信息 + * + * @author 芋道源码 + */ +@Data +public class LoginUser { + + /** + * 用户编号 + */ + private Long id; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围 + */ + private List scopes; + + // ========== 上下文 ========== + /** + * 上下文字段,不进行持久化 + * + * 1. 用于基于 LoginUser 维度的临时缓存 + */ + @JsonIgnore + private Map context; + + public void setContext(String key, Object value) { + if (context == null) { + context = new HashMap<>(); + } + context.put(key, value); + } + + public T getContext(String key, Class type) { + return MapUtil.get(context, key, type); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/annotations/PreAuthenticated.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/annotations/PreAuthenticated.java new file mode 100644 index 0000000..d17f6a7 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/annotations/PreAuthenticated.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.framework.security.core.annotations; + +import java.lang.annotation.*; + +/** + * 声明用户需要登录 + * + * 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解,原因是不通过时,抛出的是认证不通过,而不是未登录 + * + * @author 芋道源码 + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +public @interface PreAuthenticated { +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/aop/PreAuthenticatedAspect.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/aop/PreAuthenticatedAspect.java new file mode 100644 index 0000000..052c09f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/aop/PreAuthenticatedAspect.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.framework.security.core.aop; + +import com.yunxi.scm.framework.security.core.annotations.PreAuthenticated; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; + +@Aspect +@Slf4j +public class PreAuthenticatedAspect { + + @Around("@annotation(preAuthenticated)") + public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable { + if (SecurityFrameworkUtils.getLoginUser() == null) { + throw exception(UNAUTHORIZED); + } + return joinPoint.proceed(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java new file mode 100644 index 0000000..fe1b793 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/context/TransmittableThreadLocalSecurityContextHolderStrategy.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.framework.security.core.context; + +import com.alibaba.ttl.TransmittableThreadLocal; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.util.Assert; + +/** + * 基于 TransmittableThreadLocal 实现的 Security Context 持有者策略 + * 目的是,避免 @Async 等异步执行时,原生 ThreadLocal 的丢失问题 + * + * @author 芋道源码 + */ +public class TransmittableThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy { + + /** + * 使用 TransmittableThreadLocal 作为上下文 + */ + private static final ThreadLocal CONTEXT_HOLDER = new TransmittableThreadLocal<>(); + + @Override + public void clearContext() { + CONTEXT_HOLDER.remove(); + } + + @Override + public SecurityContext getContext() { + SecurityContext ctx = CONTEXT_HOLDER.get(); + if (ctx == null) { + ctx = createEmptyContext(); + CONTEXT_HOLDER.set(ctx); + } + return ctx; + } + + @Override + public void setContext(SecurityContext context) { + Assert.notNull(context, "Only non-null SecurityContext instances are permitted"); + CONTEXT_HOLDER.set(context); + } + + @Override + public SecurityContext createEmptyContext() { + return new SecurityContextImpl(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/filter/TokenAuthenticationFilter.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/filter/TokenAuthenticationFilter.java new file mode 100644 index 0000000..10a20f6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/filter/TokenAuthenticationFilter.java @@ -0,0 +1,113 @@ +package com.yunxi.scm.framework.security.core.filter; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.security.config.SecurityProperties; +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.framework.web.core.handler.GlobalExceptionHandler; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import com.yunxi.scm.module.system.api.oauth2.OAuth2TokenApi; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Token 过滤器,验证 token 的有效性 + * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class TokenAuthenticationFilter extends OncePerRequestFilter { + + private final SecurityProperties securityProperties; + + private final GlobalExceptionHandler globalExceptionHandler; + + private final OAuth2TokenApi oauth2TokenApi; + + @Override + @SuppressWarnings("NullableProblems") + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) + throws ServletException, IOException { + String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotEmpty(token)) { + Integer userType = WebFrameworkUtils.getLoginUserType(request); + try { + // 1.1 基于 token 构建登录用户 + LoginUser loginUser = buildLoginUserByToken(token, userType); + // 1.2 模拟 Login 功能,方便日常开发调试 + if (loginUser == null) { + loginUser = mockLoginUser(request, token, userType); + } + + // 2. 设置当前用户 + if (loginUser != null) { + SecurityFrameworkUtils.setLoginUser(loginUser, request); + } + } catch (Throwable ex) { + CommonResult result = globalExceptionHandler.allExceptionHandler(request, ex); + ServletUtils.writeJSON(response, result); + return; + } + } + + // 继续过滤链 + chain.doFilter(request, response); + } + + private LoginUser buildLoginUserByToken(String token, Integer userType) { + try { + OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); + if (accessToken == null) { + return null; + } + // 用户类型不匹配,无权限 + if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) { + throw new AccessDeniedException("错误的用户类型"); + } + // 构建登录用户 + return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()); + } catch (ServiceException serviceException) { + // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可 + return null; + } + } + + /** + * 模拟登录用户,方便日常开发调试 + * + * 注意,在线上环境下,一定要关闭该功能!!! + * + * @param request 请求 + * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号 + * @param userType 用户类型 + * @return 模拟的 LoginUser + */ + private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) { + if (!securityProperties.getMockEnable()) { + return null; + } + // 必须以 mockSecret 开头 + if (!token.startsWith(securityProperties.getMockSecret())) { + return null; + } + // 构建模拟用户 + Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length())); + return new LoginUser().setId(userId).setUserType(userType) + .setTenantId(WebFrameworkUtils.getTenantId(request)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/handler/AccessDeniedHandlerImpl.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/handler/AccessDeniedHandlerImpl.java new file mode 100644 index 0000000..356e6a7 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/handler/AccessDeniedHandlerImpl.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.security.core.handler; + +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.access.ExceptionTranslationFilter; +import org.springframework.stereotype.Component; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.FORBIDDEN; +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; + +/** + * 访问一个需要认证的 URL 资源,已经认证(登录)但是没有权限的情况下,返回 {@link GlobalErrorCodeConstants#FORBIDDEN} 错误码。 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#handleAccessDeniedException(HttpServletRequest, HttpServletResponse, FilterChain, AccessDeniedException)} 方法,调用当前类 + * + * @author 芋道源码 + */ +@Slf4j +@SuppressWarnings("JavadocReference") +public class AccessDeniedHandlerImpl implements AccessDeniedHandler { + + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) + throws IOException, ServletException { + // 打印 warn 的原因是,不定期合并 warn,看看有没恶意破坏 + log.warn("[commence][访问 URL({}) 时,用户({}) 权限不够]", request.getRequestURI(), + SecurityFrameworkUtils.getLoginUserId(), e); + // 返回 403 + ServletUtils.writeJSON(response, CommonResult.error(FORBIDDEN)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/handler/AuthenticationEntryPointImpl.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/handler/AuthenticationEntryPointImpl.java new file mode 100644 index 0000000..5aca640 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/handler/AuthenticationEntryPointImpl.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.framework.security.core.handler; + +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.access.ExceptionTranslationFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED; + +/** + * 访问一个需要认证的 URL 资源,但是此时自己尚未认证(登录)的情况下,返回 {@link GlobalErrorCodeConstants#UNAUTHORIZED} 错误码,从而使前端重定向到登录页 + * + * 补充:Spring Security 通过 {@link ExceptionTranslationFilter#sendStartAuthentication(HttpServletRequest, HttpServletResponse, FilterChain, AuthenticationException)} 方法,调用当前类 + * + * @author ruoyi + */ +@Slf4j +@SuppressWarnings("JavadocReference") // 忽略文档引用报错 +public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) { + log.debug("[commence][访问 URL({}) 时,没有登录]", request.getRequestURI(), e); + // 返回 401 + ServletUtils.writeJSON(response, CommonResult.error(UNAUTHORIZED)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/service/SecurityFrameworkService.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/service/SecurityFrameworkService.java new file mode 100644 index 0000000..fff78ba --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/service/SecurityFrameworkService.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.framework.security.core.service; + +/** + * Security 框架 Service 接口,定义权限相关的校验操作 + * + * @author 芋道源码 + */ +public interface SecurityFrameworkService { + + /** + * 判断是否有权限 + * + * @param permission 权限 + * @return 是否 + */ + boolean hasPermission(String permission); + + /** + * 判断是否有权限,任一一个即可 + * + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(String... permissions); + + /** + * 判断是否有角色 + * + * 注意,角色使用的是 SysRoleDO 的 code 标识 + * + * @param role 角色 + * @return 是否 + */ + boolean hasRole(String role); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(String... roles); + + /** + * 判断是否有授权 + * + * @param scope 授权 + * @return 是否 + */ + boolean hasScope(String scope); + + /** + * 判断是否有授权范围,任一一个即可 + * + * @param scope 授权范围数组 + * @return 是否 + */ + boolean hasAnyScopes(String... scope); +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/service/SecurityFrameworkServiceImpl.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/service/SecurityFrameworkServiceImpl.java new file mode 100644 index 0000000..b8793eb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/service/SecurityFrameworkServiceImpl.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.framework.security.core.service; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.module.system.api.permission.PermissionApi; +import lombok.AllArgsConstructor; + +import java.util.Arrays; + +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 默认的 {@link SecurityFrameworkService} 实现类 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class SecurityFrameworkServiceImpl implements SecurityFrameworkService { + + private final PermissionApi permissionApi; + + @Override + public boolean hasPermission(String permission) { + return hasAnyPermissions(permission); + } + + @Override + public boolean hasAnyPermissions(String... permissions) { + return permissionApi.hasAnyPermissions(getLoginUserId(), permissions); + } + + @Override + public boolean hasRole(String role) { + return hasAnyRoles(role); + } + + @Override + public boolean hasAnyRoles(String... roles) { + return permissionApi.hasAnyRoles(getLoginUserId(), roles); + } + + @Override + public boolean hasScope(String scope) { + return hasAnyScopes(scope); + } + + @Override + public boolean hasAnyScopes(String... scope) { + LoginUser user = SecurityFrameworkUtils.getLoginUser(); + if (user == null) { + return false; + } + return CollUtil.containsAny(user.getScopes(), Arrays.asList(scope)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/util/SecurityFrameworkUtils.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/util/SecurityFrameworkUtils.java new file mode 100644 index 0000000..77967b4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/core/util/SecurityFrameworkUtils.java @@ -0,0 +1,109 @@ +package com.yunxi.scm.framework.security.core.util; + +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import org.springframework.lang.Nullable; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; + +/** + * 安全服务工具类 + * + * @author 芋道源码 + */ +public class SecurityFrameworkUtils { + + public static final String AUTHORIZATION_BEARER = "Bearer"; + + private SecurityFrameworkUtils() {} + + /** + * 从请求中,获得认证 Token + * + * @param request 请求 + * @param header 认证 Token 对应的 Header 名字 + * @return 认证 Token + */ + public static String obtainAuthorization(HttpServletRequest request, String header) { + String authorization = request.getHeader(header); + if (!StringUtils.hasText(authorization)) { + return null; + } + int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); + if (index == -1) { // 未找到 + return null; + } + return authorization.substring(index + 7).trim(); + } + + /** + * 获得当前认证信息 + * + * @return 认证信息 + */ + public static Authentication getAuthentication() { + SecurityContext context = SecurityContextHolder.getContext(); + if (context == null) { + return null; + } + return context.getAuthentication(); + } + + /** + * 获取当前用户 + * + * @return 当前用户 + */ + @Nullable + public static LoginUser getLoginUser() { + Authentication authentication = getAuthentication(); + if (authentication == null) { + return null; + } + return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null; + } + + /** + * 获得当前用户的编号,从上下文中 + * + * @return 用户编号 + */ + @Nullable + public static Long getLoginUserId() { + LoginUser loginUser = getLoginUser(); + return loginUser != null ? loginUser.getId() : null; + } + + /** + * 设置当前用户 + * + * @param loginUser 登录用户 + * @param request 请求 + */ + public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) { + // 创建 Authentication,并设置到上下文 + Authentication authentication = buildAuthentication(loginUser, request); + SecurityContextHolder.getContext().setAuthentication(authentication); + + // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; + // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 + WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); + WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); + } + + private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { + // 创建 UsernamePasswordAuthenticationToken 对象 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken( + loginUser, null, Collections.emptyList()); + authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + return authenticationToken; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/package-info.java new file mode 100644 index 0000000..51cd96e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/java/com/yunxi/scm/framework/security/package-info.java @@ -0,0 +1,7 @@ +/** + * 基于 Spring Security 框架 + * 实现安全认证功能 + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.security; diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..d7bbcc5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.yunxi.scm.framework.security.config.YunxiSecurityAutoConfiguration +com.yunxi.scm.framework.security.config.YunxiWebSecurityConfigurerAdapter \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md new file mode 100644 index 0000000..7d98b2b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-security/《芋道 Spring Boot 安全框架 Spring Security 入门》.md @@ -0,0 +1,2 @@ +* 芋道 Spring Security 入门: +* Spring Security 基本概念: diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-test/pom.xml new file mode 100644 index 0000000..b9d8221 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/pom.xml @@ -0,0 +1,60 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-test + jar + + ${project.artifactId} + 测试组件,用于单元测试、集成测试 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + org.mockito + mockito-inline + + + org.springframework.boot + spring-boot-starter-test + + + + com.h2database + h2 + + + + com.github.fppt + jedis-mock + + + + uk.co.jemos.podam + podam + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/config/RedisTestConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/config/RedisTestConfiguration.java new file mode 100644 index 0000000..a7a0a0b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/config/RedisTestConfiguration.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.framework.test.config; + +import com.github.fppt.jedismock.RedisServer; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import java.io.IOException; + +/** + * Redis 测试 Configuration,主要实现内嵌 Redis 的启动 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@Lazy(false) // 禁止延迟加载 +@EnableConfigurationProperties(RedisProperties.class) +public class RedisTestConfiguration { + + /** + * 创建模拟的 Redis Server 服务器 + */ + @Bean + public RedisServer redisServer(RedisProperties properties) throws IOException { + RedisServer redisServer = new RedisServer(properties.getPort()); + // 一次执行多个单元测试时,貌似创建多个 spring 容器,导致不进行 stop。这样,就导致端口被占用,无法启动。。。 + try { + redisServer.start(); + } catch (Exception ignore) {} + return redisServer; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/config/SqlInitializationTestConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/config/SqlInitializationTestConfiguration.java new file mode 100644 index 0000000..4abc501 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/config/SqlInitializationTestConfiguration.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.framework.test.config; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializer; +import org.springframework.boot.sql.init.AbstractScriptDatabaseInitializer; +import org.springframework.boot.sql.init.DatabaseInitializationSettings; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import javax.sql.DataSource; + +/** + * SQL 初始化的测试 Configuration + * + * 为什么不使用 org.springframework.boot.autoconfigure.sql.init.DataSourceInitializationConfiguration 呢? + * 因为我们在单元测试会使用 spring.main.lazy-initialization 为 true,开启延迟加载。此时,会导致 DataSourceInitializationConfiguration 初始化 + * 不过呢,当前类的实现代码,基本是复制 DataSourceInitializationConfiguration 的哈! + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(AbstractScriptDatabaseInitializer.class) +@ConditionalOnSingleCandidate(DataSource.class) +@ConditionalOnClass(name = "org.springframework.jdbc.datasource.init.DatabasePopulator") +@Lazy(value = false) // 禁止延迟加载 +@EnableConfigurationProperties(SqlInitializationProperties.class) +public class SqlInitializationTestConfiguration { + + @Bean + public DataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, + SqlInitializationProperties initializationProperties) { + DatabaseInitializationSettings settings = createFrom(initializationProperties); + return new DataSourceScriptDatabaseInitializer(dataSource, settings); + } + + static DatabaseInitializationSettings createFrom(SqlInitializationProperties properties) { + DatabaseInitializationSettings settings = new DatabaseInitializationSettings(); + settings.setSchemaLocations(properties.getSchemaLocations()); + settings.setDataLocations(properties.getDataLocations()); + settings.setContinueOnError(properties.isContinueOnError()); + settings.setSeparator(properties.getSeparator()); + settings.setEncoding(properties.getEncoding()); + settings.setMode(properties.getMode()); + return settings; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseDbAndRedisUnitTest.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseDbAndRedisUnitTest.java new file mode 100644 index 0000000..2de16d3 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseDbAndRedisUnitTest.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.framework.test.core.ut; + +import com.yunxi.scm.framework.datasource.config.YunxiDataSourceAutoConfiguration; +import com.yunxi.scm.framework.mybatis.config.YunxiMybatisAutoConfiguration; +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import com.yunxi.scm.framework.test.config.RedisTestConfiguration; +import com.yunxi.scm.framework.test.config.SqlInitializationTestConfiguration; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +/** + * 依赖内存 DB + Redis 的单元测试 + * + * 相比 {@link BaseDbUnitTest} 来说,额外增加了内存 Redis + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB +public class BaseDbAndRedisUnitTest { + + @Import({ + // DB 配置类 + YunxiDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类 + SqlInitializationTestConfiguration.class, // SQL 初始化 + // MyBatis 配置类 + YunxiMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer +// RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YunxiRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseDbUnitTest.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseDbUnitTest.java new file mode 100644 index 0000000..7d30132 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseDbUnitTest.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.framework.test.core.ut; + +import com.yunxi.scm.framework.datasource.config.YunxiDataSourceAutoConfiguration; +import com.yunxi.scm.framework.mybatis.config.YunxiMybatisAutoConfiguration; +import com.yunxi.scm.framework.test.config.SqlInitializationTestConfiguration; +import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import com.github.yulichang.autoconfigure.MybatisPlusJoinAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; + +/** + * 依赖内存 DB 的单元测试 + * + * 注意,Service 层同样适用。对于 Service 层的单元测试,我们针对自己模块的 Mapper 走的是 H2 内存数据库,针对别的模块的 Service 走的是 Mock 方法 + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +@Sql(scripts = "/sql/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD) // 每个单元测试结束后,清理 DB +public class BaseDbUnitTest { + + @Import({ + // DB 配置类 + YunxiDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + DruidDataSourceAutoConfigure.class, // Druid 自动配置类 + SqlInitializationTestConfiguration.class, // SQL 初始化 + // MyBatis 配置类 + YunxiMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseMockitoUnitTest.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseMockitoUnitTest.java new file mode 100644 index 0000000..5272f07 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseMockitoUnitTest.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.framework.test.core.ut; + +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * 纯 Mockito 的单元测试 + * + * @author 芋道源码 + */ +@ExtendWith(MockitoExtension.class) +public class BaseMockitoUnitTest { +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseRedisUnitTest.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseRedisUnitTest.java new file mode 100644 index 0000000..d42314d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/BaseRedisUnitTest.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.framework.test.core.ut; + +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import com.yunxi.scm.framework.test.config.RedisTestConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +/** + * 依赖内存 Redis 的单元测试 + * + * 相比 {@link BaseDbUnitTest} 来说,从内存 DB 改成了内存 Redis + * + * @author 芋道源码 + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisUnitTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 application-unit-test 配置文件 +public class BaseRedisUnitTest { + + @Import({ + // Redis 配置类 + RedisTestConfiguration.class, // Redis 测试配置类,用于启动 RedisServer + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YunxiRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/package-info.java new file mode 100644 index 0000000..aab1d74 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/ut/package-info.java @@ -0,0 +1,4 @@ +/** + * 提供单元测试 Unit Test 的基类 + */ +package com.yunxi.scm.framework.test.core.ut; diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/util/AssertUtils.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/util/AssertUtils.java new file mode 100644 index 0000000..b9ebe1f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/util/AssertUtils.java @@ -0,0 +1,101 @@ +package com.yunxi.scm.framework.test.core.util; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ReflectUtil; +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * 单元测试,assert 断言工具类 + * + * @author 芋道源码 + */ +public class AssertUtils { + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + */ + public static void assertPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + Arrays.stream(expectedFields).forEach(expectedField -> { + // 忽略 jacoco 自动生成的 $jacocoData 属性的情况 + if (expectedField.isSynthetic()) { + return; + } + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return; + } + // 比对 + Assertions.assertEquals( + ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField), + String.format("Field(%s) 不匹配", expectedField.getName()) + ); + }); + } + + /** + * 比对两个对象的属性是否一致 + * + * 注意,如果 expected 存在的属性,actual 不存在的时候,会进行忽略 + * + * @param expected 期望对象 + * @param actual 实际对象 + * @param ignoreFields 忽略的属性数组 + * @return 是否一致 + */ + public static boolean isPojoEquals(Object expected, Object actual, String... ignoreFields) { + Field[] expectedFields = ReflectUtil.getFields(expected.getClass()); + return Arrays.stream(expectedFields).allMatch(expectedField -> { + // 如果是忽略的属性,则不进行比对 + if (ArrayUtil.contains(ignoreFields, expectedField.getName())) { + return true; + } + // 忽略不存在的属性 + Field actualField = ReflectUtil.getField(actual.getClass(), expectedField.getName()); + if (actualField == null) { + return true; + } + return Objects.equals(ReflectUtil.getFieldValue(expected, expectedField), + ReflectUtil.getFieldValue(actual, actualField)); + }); + } + + /** + * 执行方法,校验抛出的 Service 是否符合条件 + * + * @param executable 业务异常 + * @param errorCode 错误码对象 + * @param messageParams 消息参数 + */ + public static void assertServiceException(Executable executable, ErrorCode errorCode, Object... messageParams) { + // 调用方法 + ServiceException serviceException = assertThrows(ServiceException.class, executable); + // 校验错误码 + Assertions.assertEquals(errorCode.getCode(), serviceException.getCode(), "错误码不匹配"); + String message = ServiceExceptionUtil.doFormat(errorCode.getCode(), errorCode.getMsg(), messageParams); + Assertions.assertEquals(message, serviceException.getMessage(), "错误提示不匹配"); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/util/RandomUtils.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/util/RandomUtils.java new file mode 100644 index 0000000..c9bd6f2 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/core/util/RandomUtils.java @@ -0,0 +1,140 @@ +package com.yunxi.scm.framework.test.core.util; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import uk.co.jemos.podam.api.PodamFactory; +import uk.co.jemos.podam.api.PodamFactoryImpl; +import uk.co.jemos.podam.common.AttributeStrategy; + +import javax.validation.constraints.Email; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 随机工具类 + * + * @author 芋道源码 + */ +public class RandomUtils { + + private static final int RANDOM_STRING_LENGTH = 10; + + private static final int TINYINT_MAX = 127; + + private static final int RANDOM_DATE_MAX = 30; + + private static final int RANDOM_COLLECTION_LENGTH = 5; + + private static final PodamFactory PODAM_FACTORY = new PodamFactoryImpl(); + + static { + // 字符串 + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(String.class, + (dataProviderStrategy, attributeMetadata, map) -> randomString()); + // Integer + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Integer.class, (dataProviderStrategy, attributeMetadata, map) -> { + // 如果是 status 的字段,返回 0 或 1 + if ("status".equals(attributeMetadata.getAttributeName())) { + return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); + } + // 如果是 type、status 结尾的字段,返回 tinyint 范围 + if (StrUtil.endWithAnyIgnoreCase(attributeMetadata.getAttributeName(), + "type", "status", "category", "scope", "result")) { + return RandomUtil.randomInt(0, TINYINT_MAX + 1); + } + return RandomUtil.randomInt(); + }); + // LocalDateTime + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class, + (dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime()); + // Boolean + PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(Boolean.class, (dataProviderStrategy, attributeMetadata, map) -> { + // 如果是 deleted 的字段,返回非删除 + if ("deleted".equals(attributeMetadata.getAttributeName())) { + return false; + } + return RandomUtil.randomBoolean(); + }); + } + + public static String randomString() { + return RandomUtil.randomString(RANDOM_STRING_LENGTH); + } + + public static Long randomLongId() { + return RandomUtil.randomLong(0, Long.MAX_VALUE); + } + + public static Integer randomInteger() { + return RandomUtil.randomInt(0, Integer.MAX_VALUE); + } + + public static Date randomDate() { + return RandomUtil.randomDay(0, RANDOM_DATE_MAX); + } + + public static LocalDateTime randomLocalDateTime() { + // 设置 Nano 为零的原因,避免 MySQL、H2 存储不到时间戳 + return LocalDateTimeUtil.of(randomDate()).withNano(0); + } + + public static Short randomShort() { + return (short) RandomUtil.randomInt(0, Short.MAX_VALUE); + } + + public static Set randomSet(Class clazz) { + return Stream.iterate(0, i -> i).limit(RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH)) + .map(i -> randomPojo(clazz)).collect(Collectors.toSet()); + } + + public static Integer randomCommonStatus() { + return RandomUtil.randomEle(CommonStatusEnum.values()).getStatus(); + } + + public static String randomEmail() { + return randomString() + "@qq.com"; + } + + public static String randomURL() { + return "https://www.iocoder.cn/" + randomString(); + } + + @SafeVarargs + public static T randomPojo(Class clazz, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static T randomPojo(Class clazz, Type type, Consumer... consumers) { + T pojo = PODAM_FACTORY.manufacturePojo(clazz, type); + // 非空时,回调逻辑。通过它,可以实现 Pojo 的进一步处理 + if (ArrayUtil.isNotEmpty(consumers)) { + Arrays.stream(consumers).forEach(consumer -> consumer.accept(pojo)); + } + return pojo; + } + + @SafeVarargs + public static List randomPojoList(Class clazz, Consumer... consumers) { + int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH); + return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) + .collect(Collectors.toList()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/package-info.java new file mode 100644 index 0000000..497907a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/src/main/java/com/yunxi/scm/framework/test/package-info.java @@ -0,0 +1,4 @@ +/** + * 测试组件,用于单元测试、集成测试等等 + */ +package com.yunxi.scm.framework.test; diff --git a/yunxi-framework/yunxi-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md new file mode 100644 index 0000000..1706bf7 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-test/《芋道 Spring Boot 单元测试 Test 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-web/pom.xml new file mode 100644 index 0000000..d70cf12 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/pom.xml @@ -0,0 +1,73 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-web + jar + + ${project.artifactId} + Web 框架,全局异常、API 日志等 + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + com.github.xiaoymin + knife4j-openapi3-spring-boot-starter + + + org.springdoc + springdoc-openapi-ui + + + + org.springframework.security + spring-security-core + provided + + + + + com.yunxi.scm + yunxi-module-infra-api + ${revision} + + + + + io.github.resilience4j + resilience4j-ratelimiter + provided + + + + + org.jsoup + jsoup + + + + + diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/config/YunxiApiLogAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/config/YunxiApiLogAutoConfiguration.java new file mode 100644 index 0000000..72d931e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/config/YunxiApiLogAutoConfiguration.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.framework.apilog.config; + +import com.yunxi.scm.framework.apilog.core.filter.ApiAccessLogFilter; +import com.yunxi.scm.framework.apilog.core.service.ApiAccessLogFrameworkService; +import com.yunxi.scm.framework.apilog.core.service.ApiAccessLogFrameworkServiceImpl; +import com.yunxi.scm.framework.apilog.core.service.ApiErrorLogFrameworkService; +import com.yunxi.scm.framework.apilog.core.service.ApiErrorLogFrameworkServiceImpl; +import com.yunxi.scm.framework.common.enums.WebFilterOrderEnum; +import com.yunxi.scm.framework.web.config.WebProperties; +import com.yunxi.scm.framework.web.config.YunxiWebAutoConfiguration; +import com.yunxi.scm.module.infra.api.logger.ApiAccessLogApi; +import com.yunxi.scm.module.infra.api.logger.ApiErrorLogApi; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; + +import javax.servlet.Filter; + +@AutoConfiguration(after = YunxiWebAutoConfiguration.class) +public class YunxiApiLogAutoConfiguration { + + @Bean + public ApiAccessLogFrameworkService apiAccessLogFrameworkService(ApiAccessLogApi apiAccessLogApi) { + return new ApiAccessLogFrameworkServiceImpl(apiAccessLogApi); + } + + @Bean + public ApiErrorLogFrameworkService apiErrorLogFrameworkService(ApiErrorLogApi apiErrorLogApi) { + return new ApiErrorLogFrameworkServiceImpl(apiErrorLogApi); + } + + /** + * 创建 ApiAccessLogFilter Bean,记录 API 请求日志 + */ + @Bean + @ConditionalOnProperty(prefix = "yunxi.access-log", value = "enable", matchIfMissing = true) // 允许使用 yunxi.access-log.enable=false 禁用访问日志 + public FilterRegistrationBean apiAccessLogFilter(WebProperties webProperties, + @Value("${spring.application.name}") String applicationName, + ApiAccessLogFrameworkService apiAccessLogFrameworkService) { + ApiAccessLogFilter filter = new ApiAccessLogFilter(webProperties, applicationName, apiAccessLogFrameworkService); + return createFilterBean(filter, WebFilterOrderEnum.API_ACCESS_LOG_FILTER); + } + + private static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/filter/ApiAccessLogFilter.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/filter/ApiAccessLogFilter.java new file mode 100644 index 0000000..fe112eb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/filter/ApiAccessLogFilter.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.framework.apilog.core.filter; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.apilog.core.service.ApiAccessLog; +import com.yunxi.scm.framework.apilog.core.service.ApiAccessLogFrameworkService; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.monitor.TracerUtils; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.web.config.WebProperties; +import com.yunxi.scm.framework.web.core.filter.ApiRequestFilter; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import lombok.extern.slf4j.Slf4j; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; + +/** + * API 访问日志 Filter + * + * @author 芋道源码 + */ +@Slf4j +public class ApiAccessLogFilter extends ApiRequestFilter { + + private final String applicationName; + + private final ApiAccessLogFrameworkService apiAccessLogFrameworkService; + + public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogFrameworkService apiAccessLogFrameworkService) { + super(webProperties); + this.applicationName = applicationName; + this.apiAccessLogFrameworkService = apiAccessLogFrameworkService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + // 获得开始时间 + LocalDateTime beginTime = LocalDateTime.now(); + // 提前获得参数,避免 XssFilter 过滤处理 + Map queryString = ServletUtils.getParamMap(request); + String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null; + + try { + // 继续过滤器 + filterChain.doFilter(request, response); + // 正常执行,记录日志 + createApiAccessLog(request, beginTime, queryString, requestBody, null); + } catch (Exception ex) { + // 异常执行,记录日志 + createApiAccessLog(request, beginTime, queryString, requestBody, ex); + throw ex; + } + } + + private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + ApiAccessLog accessLog = new ApiAccessLog(); + try { + this.buildApiAccessLogDTO(accessLog, request, beginTime, queryString, requestBody, ex); + apiAccessLogFrameworkService.createApiAccessLog(accessLog); + } catch (Throwable th) { + log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th); + } + } + + private void buildApiAccessLogDTO(ApiAccessLog accessLog, HttpServletRequest request, LocalDateTime beginTime, + Map queryString, String requestBody, Exception ex) { + // 处理用户信息 + accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + accessLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置访问结果 + CommonResult result = WebFrameworkUtils.getCommonResult(request); + if (result != null) { + accessLog.setResultCode(result.getCode()); + accessLog.setResultMsg(result.getMsg()); + } else if (ex != null) { + accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR.getCode()); + accessLog.setResultMsg(ExceptionUtil.getRootCauseMessage(ex)); + } else { + accessLog.setResultCode(0); + accessLog.setResultMsg(""); + } + // 设置其它字段 + accessLog.setTraceId(TracerUtils.getTraceId()); + accessLog.setApplicationName(applicationName); + accessLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder().put("query", queryString).put("body", requestBody).build(); + accessLog.setRequestParams(toJsonString(requestParams)); + accessLog.setRequestMethod(request.getMethod()); + accessLog.setUserAgent(ServletUtils.getUserAgent(request)); + accessLog.setUserIp(ServletUtils.getClientIP(request)); + // 持续时间 + accessLog.setBeginTime(beginTime); + accessLog.setEndTime(LocalDateTime.now()); + accessLog.setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLog.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLog.java new file mode 100644 index 0000000..03b92d8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLog.java @@ -0,0 +1,85 @@ +package com.yunxi.scm.framework.apilog.core.service; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 访问日志 + * + * @author 芋道源码 + */ +@Data +public class ApiAccessLog { + + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 开始请求时间 + */ + @NotNull(message = "开始请求时间不能为空") + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLogFrameworkService.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLogFrameworkService.java new file mode 100644 index 0000000..8e58ada --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLogFrameworkService.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.framework.apilog.core.service; + +/** + * API 访问日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogFrameworkService { + + /** + * 创建 API 访问日志 + * + * @param apiAccessLog API 访问日志 + */ + void createApiAccessLog(ApiAccessLog apiAccessLog); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java new file mode 100644 index 0000000..8b8273b --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiAccessLogFrameworkServiceImpl.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.apilog.core.service; + +import cn.hutool.core.bean.BeanUtil; +import com.yunxi.scm.module.infra.api.logger.ApiAccessLogApi; +import com.yunxi.scm.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; + +/** + * API 访问日志 Framework Service 实现类 + * + * 基于 {@link ApiAccessLogApi} 服务,记录访问日志 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class ApiAccessLogFrameworkServiceImpl implements ApiAccessLogFrameworkService { + + private final ApiAccessLogApi apiAccessLogApi; + + @Override + @Async + public void createApiAccessLog(ApiAccessLog apiAccessLog) { + ApiAccessLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiAccessLog, ApiAccessLogCreateReqDTO.class); + apiAccessLogApi.createApiAccessLog(reqDTO); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLog.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLog.java new file mode 100644 index 0000000..6a72eed --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLog.java @@ -0,0 +1,107 @@ +package com.yunxi.scm.framework.apilog.core.service; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 错误日志 + * + * @author 芋道源码 + */ +@Data +public class ApiErrorLog { + + /** + * 链路编号 + */ + private String traceId; + /** + * 账号编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 异常时间 + */ + @NotNull(message = "异常时间不能为空") + private LocalDateTime exceptionTime; + /** + * 异常名 + */ + @NotNull(message = "异常名不能为空") + private String exceptionName; + /** + * 异常发生的类全名 + */ + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + /** + * 异常发生的类文件 + */ + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + /** + * 异常发生的方法名 + */ + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + */ + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + /** + * 异常的栈轨迹异常的栈轨迹 + */ + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + /** + * 异常导致的根消息 + */ + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + /** + * 异常导致的消息 + */ + @NotNull(message = "异常导致的消息不能为空") + private String exceptionMessage; + + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLogFrameworkService.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLogFrameworkService.java new file mode 100644 index 0000000..42a2b17 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLogFrameworkService.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.framework.apilog.core.service; + +/** + * API 错误日志 Framework Service 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogFrameworkService { + + /** + * 创建 API 错误日志 + * + * @param apiErrorLog API 错误日志 + */ + void createApiErrorLog(ApiErrorLog apiErrorLog); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java new file mode 100644 index 0000000..6125ab8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/core/service/ApiErrorLogFrameworkServiceImpl.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.framework.apilog.core.service; + +import cn.hutool.core.bean.BeanUtil; +import com.yunxi.scm.module.infra.api.logger.ApiErrorLogApi; +import com.yunxi.scm.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Async; + +/** + * API 错误日志 Framework Service 实现类 + * + * 基于 {@link ApiErrorLogApi} 服务,记录错误日志 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public class ApiErrorLogFrameworkServiceImpl implements ApiErrorLogFrameworkService { + + private final ApiErrorLogApi apiErrorLogApi; + + @Override + @Async + public void createApiErrorLog(ApiErrorLog apiErrorLog) { + ApiErrorLogCreateReqDTO reqDTO = BeanUtil.copyProperties(apiErrorLog, ApiErrorLogCreateReqDTO.class); + apiErrorLogApi.createApiErrorLog(reqDTO); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/package-info.java new file mode 100644 index 0000000..8d2da9c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/apilog/package-info.java @@ -0,0 +1,8 @@ +/** + * API 日志:包含两类 + * 1. API 访问日志:记录用户访问 API 的访问日志,定期归档历史日志。 + * 2. 异常日志:记录用户访问 API 的系统异常,方便日常排查问题与告警。 + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.apilog; diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/config/YunxiJacksonAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/config/YunxiJacksonAutoConfiguration.java new file mode 100644 index 0000000..acb1317 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/config/YunxiJacksonAutoConfiguration.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.framework.jackson.config; + +import com.yunxi.scm.framework.jackson.core.databind.NumberSerializer; +import com.yunxi.scm.framework.jackson.core.databind.LocalDateTimeDeserializer; +import com.yunxi.scm.framework.jackson.core.databind.LocalDateTimeSerializer; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.annotation.Bean; + +import java.time.LocalDateTime; + +@AutoConfiguration +@Slf4j +public class YunxiJacksonAutoConfiguration { + + @Bean + public BeanPostProcessor objectMapperBeanPostProcessor() { + return new BeanPostProcessor() { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (!(bean instanceof ObjectMapper)) { + return bean; + } + ObjectMapper objectMapper = (ObjectMapper) bean; + SimpleModule simpleModule = new SimpleModule(); + /* + * 1. 新增Long类型序列化规则,数值超过2^53-1,在JS会出现精度丢失问题,因此Long自动序列化为字符串类型 + * 2. 新增LocalDateTime序列化、反序列化规则 + */ + simpleModule + .addSerializer(Long.class, NumberSerializer.INSTANCE) + .addSerializer(Long.TYPE, NumberSerializer.INSTANCE) + .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.INSTANCE) + .addDeserializer(LocalDateTime.class, LocalDateTimeDeserializer.INSTANCE); + + objectMapper.registerModules(simpleModule); + + JsonUtils.init(objectMapper); + log.info("初始化 jackson 自动配置"); + return bean; + } + }; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalDateTimeDeserializer.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalDateTimeDeserializer.java new file mode 100644 index 0000000..73c419c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalDateTimeDeserializer.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.framework.jackson.core.databind; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +import java.io.IOException; +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * LocalDateTime反序列化规则 + *

+ * 会将毫秒级时间戳反序列化为LocalDateTime + */ +public class LocalDateTimeDeserializer extends JsonDeserializer { + + public static final LocalDateTimeDeserializer INSTANCE = new LocalDateTimeDeserializer(); + + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return LocalDateTime.ofInstant(Instant.ofEpochMilli(p.getValueAsLong()), ZoneId.systemDefault()); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalDateTimeSerializer.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalDateTimeSerializer.java new file mode 100644 index 0000000..2f5e671 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalDateTimeSerializer.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.framework.jackson.core.databind; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.ZoneId; + +/** + * LocalDateTime序列化规则 + *

+ * 会将LocalDateTime序列化为毫秒级时间戳 + */ +public class LocalDateTimeSerializer extends JsonSerializer { + + public static final LocalDateTimeSerializer INSTANCE = new LocalDateTimeSerializer(); + + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli()); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalTimeJson.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalTimeJson.java new file mode 100644 index 0000000..6c45ce6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/LocalTimeJson.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.framework.jackson.core.databind; + +import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer; + +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_HOUR_MINUTE_SECOND; + +public class LocalTimeJson { + + public static final LocalTimeSerializer SERIALIZER = new LocalTimeSerializer(DateTimeFormatter + .ofPattern(FORMAT_HOUR_MINUTE_SECOND) + .withZone(ZoneId.systemDefault())); + + public static final LocalTimeDeserializer DESERIALIZABLE = new LocalTimeDeserializer(DateTimeFormatter + .ofPattern(FORMAT_HOUR_MINUTE_SECOND) + .withZone(ZoneId.systemDefault())); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/NumberSerializer.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/NumberSerializer.java new file mode 100644 index 0000000..f17bcbd --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/databind/NumberSerializer.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.framework.jackson.core.databind; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; + +import java.io.IOException; + +/** + * Long 序列化规则 + * + * 会将超长 long 值转换为 string,解决前端 JavaScript 最大安全整数是 2^53-1 的问题 + * + * @author 星语 + */ +@JacksonStdImpl +public class NumberSerializer extends com.fasterxml.jackson.databind.ser.std.NumberSerializer { + + private static final long MAX_SAFE_INTEGER = 9007199254740991L; + private static final long MIN_SAFE_INTEGER = -9007199254740991L; + + public static final NumberSerializer INSTANCE = new NumberSerializer(Number.class); + + public NumberSerializer(Class rawType) { + super(rawType); + } + + @Override + public void serialize(Number value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + // 超出范围 序列化位字符串 + if (value.longValue() > MIN_SAFE_INTEGER && value.longValue() < MAX_SAFE_INTEGER) { + super.serialize(value, gen, serializers); + } else { + gen.writeString(value.toString()); + } + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/package-info.java new file mode 100644 index 0000000..6eefaba --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/jackson/core/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.framework.jackson.core; diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/package-info.java new file mode 100644 index 0000000..04034fe --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/package-info.java @@ -0,0 +1,4 @@ +/** + * Web 框架,全局异常、API 日志等 + */ +package com.yunxi.scm.framework; diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/config/SwaggerProperties.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/config/SwaggerProperties.java new file mode 100644 index 0000000..ee3defb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/config/SwaggerProperties.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.framework.swagger.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import javax.validation.constraints.NotEmpty; + +/** + * Swagger 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties("yunxi.swagger") +@Data +public class SwaggerProperties { + + /** + * 标题 + */ + @NotEmpty(message = "标题不能为空") + private String title; + /** + * 描述 + */ + @NotEmpty(message = "描述不能为空") + private String description; + /** + * 作者 + */ + @NotEmpty(message = "作者不能为空") + private String author; + /** + * 版本 + */ + @NotEmpty(message = "版本不能为空") + private String version; + /** + * url + */ + @NotEmpty(message = "扫描的 package 不能为空") + private String url; + /** + * email + */ + @NotEmpty(message = "扫描的 email 不能为空") + private String email; + + /** + * license + */ + @NotEmpty(message = "扫描的 license 不能为空") + private String license; + + /** + * license-url + */ + @NotEmpty(message = "扫描的 license-url 不能为空") + private String licenseUrl; + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/config/YunxiSwaggerAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/config/YunxiSwaggerAutoConfiguration.java new file mode 100644 index 0000000..69a82b4 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/config/YunxiSwaggerAutoConfiguration.java @@ -0,0 +1,155 @@ +package com.yunxi.scm.framework.swagger.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.media.IntegerSchema; +import io.swagger.v3.oas.models.media.StringSchema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springdoc.core.*; +import org.springdoc.core.customizers.OpenApiBuilderCustomizer; +import org.springdoc.core.customizers.ServerBaseUrlCustomizer; +import org.springdoc.core.providers.JavadocProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpHeaders; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static com.yunxi.scm.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; + +/** + * Swagger 自动配置类,基于 OpenAPI + Springdoc 实现。 + * + * 友情提示: + * 1. Springdoc 文档地址:仓库 + * 2. Swagger 规范,于 2015 更名为 OpenAPI 规范,本质是一个东西 + * + * @author 芋道源码 + */ +@AutoConfiguration +@ConditionalOnClass({OpenAPI.class}) +@EnableConfigurationProperties(SwaggerProperties.class) +@ConditionalOnProperty(prefix = "springdoc.api-docs", name = "enabled", havingValue = "true", matchIfMissing = true) // 设置为 false 时,禁用 +public class YunxiSwaggerAutoConfiguration { + + // ========== 全局 OpenAPI 配置 ========== + + @Bean + public OpenAPI createApi(SwaggerProperties properties) { + Map securitySchemas = buildSecuritySchemes(); + OpenAPI openAPI = new OpenAPI() + // 接口信息 + .info(buildInfo(properties)) + // 接口安全配置 + .components(new Components().securitySchemes(securitySchemas)) + .addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)); + securitySchemas.keySet().forEach(key -> openAPI.addSecurityItem(new SecurityRequirement().addList(key))); + return openAPI; + } + + /** + * API 摘要信息 + */ + private Info buildInfo(SwaggerProperties properties) { + return new Info() + .title(properties.getTitle()) + .description(properties.getDescription()) + .version(properties.getVersion()) + .contact(new Contact().name(properties.getAuthor()).url(properties.getUrl()).email(properties.getEmail())) + .license(new License().name(properties.getLicense()).url(properties.getLicenseUrl())); + } + + /** + * 安全模式,这里配置通过请求头 Authorization 传递 token 参数 + */ + private Map buildSecuritySchemes() { + Map securitySchemes = new HashMap<>(); + SecurityScheme securityScheme = new SecurityScheme() + .type(SecurityScheme.Type.APIKEY) // 类型 + .name(HttpHeaders.AUTHORIZATION) // 请求头的 name + .in(SecurityScheme.In.HEADER); // token 所在位置 + securitySchemes.put(HttpHeaders.AUTHORIZATION, securityScheme); + return securitySchemes; + } + + /** + * 自定义 OpenAPI 处理器 + */ + @Bean + public OpenAPIService openApiBuilder(Optional openAPI, + SecurityService securityParser, + SpringDocConfigProperties springDocConfigProperties, + PropertyResolverUtils propertyResolverUtils, + Optional> openApiBuilderCustomizers, + Optional> serverBaseUrlCustomizers, + Optional javadocProvider) { + + return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, + propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider); + } + + // ========== 分组 OpenAPI 配置 ========== + + /** + * 所有模块的 API 分组 + */ + @Bean + public GroupedOpenApi allGroupedOpenApi() { + return buildGroupedOpenApi("all", ""); + } + + public static GroupedOpenApi buildGroupedOpenApi(String group) { + return buildGroupedOpenApi(group, group); + } + + public static GroupedOpenApi buildGroupedOpenApi(String group, String path) { + return GroupedOpenApi.builder() + .group(group) + .pathsToMatch("/admin-api/" + path + "/**", "/app-api/" + path + "/**") + .addOperationCustomizer((operation, handlerMethod) -> operation + .addParametersItem(buildTenantHeaderParameter()) + .addParametersItem(buildSecurityHeaderParameter())) + .build(); + } + + /** + * 构建 Tenant 租户编号请求头参数 + * + * @return 多租户参数 + */ + private static Parameter buildTenantHeaderParameter() { + return new Parameter() + .name(HEADER_TENANT_ID) // header 名 + .description("租户编号") // 描述 + .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header + .schema(new IntegerSchema()._default(1L).name(HEADER_TENANT_ID).description("租户编号")); // 默认:使用租户编号为 1 + } + + /** + * 构建 Authorization 认证请求头参数 + * + * 解决 Knife4j Authorize 未生效,请求header里未包含参数 + * + * @return 认证参数 + */ + private static Parameter buildSecurityHeaderParameter() { + return new Parameter() + .name(HttpHeaders.AUTHORIZATION) // header 名 + .description("认证 Token") // 描述 + .in(String.valueOf(SecurityScheme.In.HEADER)) // 请求 header + .schema(new StringSchema()._default("Bearer test1").name(HEADER_TENANT_ID).description("认证 Token")); // 默认:使用用户编号为 1 + } + +} + diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/package-info.java new file mode 100644 index 0000000..4d2a6be --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/swagger/package-info.java @@ -0,0 +1,6 @@ +/** + * 基于 Swagger + Knife4j 实现 API 接口文档 + * + * @author 芋道源码 + */ +package com.yunxi.scm.framework.swagger; diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/config/WebProperties.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/config/WebProperties.java new file mode 100644 index 0000000..6616ca3 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/config/WebProperties.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.framework.web.config; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@ConfigurationProperties(prefix = "yunxi.web") +@Validated +@Data +public class WebProperties { + + @NotNull(message = "APP API 不能为空") + private Api appApi = new Api("/app-api", "**.controller.app.**"); + @NotNull(message = "Admin API 不能为空") + private Api adminApi = new Api("/admin-api", "**.controller.admin.**"); + + @NotNull(message = "Admin UI 不能为空") + private Ui adminUi; + + @Data + @AllArgsConstructor + @NoArgsConstructor + @Valid + public static class Api { + + /** + * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀 + * + * + * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题 + * 这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。 + * + * @see YunxiWebAutoConfiguration#configurePathMatch(PathMatchConfigurer) + */ + @NotEmpty(message = "API 前缀不能为空") + private String prefix; + + /** + * Controller 所在包的 Ant 路径规则 + * + * 主要目的是,给该 Controller 设置指定的 {@link #prefix} + */ + @NotEmpty(message = "Controller 所在包不能为空") + private String controller; + + } + + @Data + @Valid + public static class Ui { + + /** + * 访问地址 + */ + private String url; + + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/config/YunxiWebAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/config/YunxiWebAutoConfiguration.java new file mode 100644 index 0000000..9a816a0 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/config/YunxiWebAutoConfiguration.java @@ -0,0 +1,128 @@ +package com.yunxi.scm.framework.web.config; + +import com.yunxi.scm.framework.apilog.core.service.ApiErrorLogFrameworkService; +import com.yunxi.scm.framework.common.enums.WebFilterOrderEnum; +import com.yunxi.scm.framework.web.core.filter.CacheRequestBodyFilter; +import com.yunxi.scm.framework.web.core.filter.DemoFilter; +import com.yunxi.scm.framework.web.core.handler.GlobalExceptionHandler; +import com.yunxi.scm.framework.web.core.handler.GlobalResponseBodyHandler; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.annotation.Resource; +import javax.servlet.Filter; + +@AutoConfiguration +@EnableConfigurationProperties(WebProperties.class) +public class YunxiWebAutoConfiguration implements WebMvcConfigurer { + + @Resource + private WebProperties webProperties; + /** + * 应用名 + */ + @Value("${spring.application.name}") + private String applicationName; + + @Override + public void configurePathMatch(PathMatchConfigurer configurer) { + configurePathMatch(configurer, webProperties.getAdminApi()); + configurePathMatch(configurer, webProperties.getAppApi()); + } + + /** + * 设置 API 前缀,仅仅匹配 controller 包下的 + * + * @param configurer 配置 + * @param api API 配置 + */ + private void configurePathMatch(PathMatchConfigurer configurer, WebProperties.Api api) { + AntPathMatcher antPathMatcher = new AntPathMatcher("."); + configurer.addPathPrefix(api.getPrefix(), clazz -> clazz.isAnnotationPresent(RestController.class) + && antPathMatcher.match(api.getController(), clazz.getPackage().getName())); // 仅仅匹配 controller 包 + } + + @Bean + public GlobalExceptionHandler globalExceptionHandler(ApiErrorLogFrameworkService ApiErrorLogFrameworkService) { + return new GlobalExceptionHandler(applicationName, ApiErrorLogFrameworkService); + } + + @Bean + public GlobalResponseBodyHandler globalResponseBodyHandler() { + return new GlobalResponseBodyHandler(); + } + + @Bean + @SuppressWarnings("InstantiationOfUtilityClass") + public WebFrameworkUtils webFrameworkUtils(WebProperties webProperties) { + // 由于 WebFrameworkUtils 需要使用到 webProperties 属性,所以注册为一个 Bean + return new WebFrameworkUtils(webProperties); + } + + // ========== Filter 相关 ========== + + /** + * 创建 CorsFilter Bean,解决跨域问题 + */ + @Bean + public FilterRegistrationBean corsFilterBean() { + // 创建 CorsConfiguration 对象 + CorsConfiguration config = new CorsConfiguration(); + config.setAllowCredentials(true); + config.addAllowedOriginPattern("*"); // 设置访问源地址 + config.addAllowedHeader("*"); // 设置访问源请求头 + config.addAllowedMethod("*"); // 设置访问源请求方法 + // 创建 UrlBasedCorsConfigurationSource 对象 + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); // 对接口配置跨域设置 + return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER); + } + + /** + * 创建 RequestBodyCacheFilter Bean,可重复读取请求内容 + */ + @Bean + public FilterRegistrationBean requestBodyCacheFilter() { + return createFilterBean(new CacheRequestBodyFilter(), WebFilterOrderEnum.REQUEST_BODY_CACHE_FILTER); + } + + /** + * 创建 DemoFilter Bean,演示模式 + */ + @Bean + @ConditionalOnProperty(value = "yunxi.demo", havingValue = "true") + public FilterRegistrationBean demoFilter() { + return createFilterBean(new DemoFilter(), WebFilterOrderEnum.DEMO_FILTER); + } + + public static FilterRegistrationBean createFilterBean(T filter, Integer order) { + FilterRegistrationBean bean = new FilterRegistrationBean<>(filter); + bean.setOrder(order); + return bean; + } + + /** + * 创建 RestTemplate 实例 + * + * @param restTemplateBuilder {@link RestTemplateAutoConfiguration#restTemplateBuilder} + */ + @Bean + public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) { + return restTemplateBuilder.build(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/ApiRequestFilter.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/ApiRequestFilter.java new file mode 100644 index 0000000..d46677d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/ApiRequestFilter.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.web.config.WebProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.http.HttpServletRequest; + +/** + * 过滤 /admin-api、/app-api 等 API 请求的过滤器 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +public abstract class ApiRequestFilter extends OncePerRequestFilter { + + protected final WebProperties webProperties; + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 只过滤 API 请求的地址 + return !StrUtil.startWithAny(request.getRequestURI(), webProperties.getAdminApi().getPrefix(), + webProperties.getAppApi().getPrefix()); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/CacheRequestBodyFilter.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/CacheRequestBodyFilter.java new file mode 100644 index 0000000..f7eabda --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/CacheRequestBodyFilter.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.framework.web.core.filter; + +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Request Body 缓存 Filter,实现它的可重复读取 + * + * @author 芋道源码 + */ +public class CacheRequestBodyFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new CacheRequestBodyWrapper(request), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 只处理 json 请求内容 + return !ServletUtils.isJsonRequest(request); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/CacheRequestBodyWrapper.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/CacheRequestBodyWrapper.java new file mode 100644 index 0000000..90b7a9d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/CacheRequestBodyWrapper.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.framework.web.core.filter; + +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; + +import javax.servlet.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * Request Body 缓存 Wrapper + * + * @author 芋道源码 + */ +public class CacheRequestBodyWrapper extends HttpServletRequestWrapper { + + /** + * 缓存的内容 + */ + private final byte[] body; + + public CacheRequestBodyWrapper(HttpServletRequest request) { + super(request); + body = ServletUtils.getBodyBytes(request); + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); + // 返回 ServletInputStream + return new ServletInputStream() { + + @Override + public int read() { + return inputStream.read(); + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) {} + + @Override + public int available() { + return body.length; + } + + }; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/DemoFilter.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/DemoFilter.java new file mode 100644 index 0000000..df94346 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/filter/DemoFilter.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.framework.web.core.filter; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.DEMO_DENY; + +/** + * 演示 Filter,禁止用户发起写操作,避免影响测试数据 + * + * @author 芋道源码 + */ +public class DemoFilter extends OncePerRequestFilter { + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + String method = request.getMethod(); + return !StrUtil.equalsAnyIgnoreCase(method, "POST", "PUT", "DELETE") // 写操作时,不进行过滤率 + || WebFrameworkUtils.getLoginUserId(request) == null; // 非登录用户时,不进行过滤 + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { + // 直接返回 DEMO_DENY 的结果。即,请求不继续 + ServletUtils.writeJSON(response, CommonResult.error(DEMO_DENY)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/handler/GlobalExceptionHandler.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..3f8228e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/handler/GlobalExceptionHandler.java @@ -0,0 +1,324 @@ +package com.yunxi.scm.framework.web.core.handler; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.apilog.core.service.ApiErrorLog; +import com.yunxi.scm.framework.apilog.core.service.ApiErrorLogFrameworkService; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.common.util.monitor.TracerUtils; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import io.github.resilience4j.ratelimiter.RequestNotPermitted; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.util.Assert; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +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.servlet.NoHandlerFoundException; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.ValidationException; +import java.time.LocalDateTime; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.*; + +/** + * 全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号 + * + * @author 芋道源码 + */ +@RestControllerAdvice +@AllArgsConstructor +@Slf4j +public class GlobalExceptionHandler { + + private final String applicationName; + + private final ApiErrorLogFrameworkService apiErrorLogFrameworkService; + + /** + * 处理所有异常,主要是提供给 Filter 使用 + * 因为 Filter 不走 SpringMVC 的流程,但是我们又需要兜底处理异常,所以这里提供一个全量的异常处理过程,保持逻辑统一。 + * + * @param request 请求 + * @param ex 异常 + * @return 通用返回 + */ + public CommonResult allExceptionHandler(HttpServletRequest request, Throwable ex) { + if (ex instanceof MissingServletRequestParameterException) { + return missingServletRequestParameterExceptionHandler((MissingServletRequestParameterException) ex); + } + if (ex instanceof MethodArgumentTypeMismatchException) { + return methodArgumentTypeMismatchExceptionHandler((MethodArgumentTypeMismatchException) ex); + } + if (ex instanceof MethodArgumentNotValidException) { + return methodArgumentNotValidExceptionExceptionHandler((MethodArgumentNotValidException) ex); + } + if (ex instanceof BindException) { + return bindExceptionHandler((BindException) ex); + } + if (ex instanceof ConstraintViolationException) { + return constraintViolationExceptionHandler((ConstraintViolationException) ex); + } + if (ex instanceof ValidationException) { + return validationException((ValidationException) ex); + } + if (ex instanceof NoHandlerFoundException) { + return noHandlerFoundExceptionHandler(request, (NoHandlerFoundException) ex); + } + if (ex instanceof HttpRequestMethodNotSupportedException) { + return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); + } + if (ex instanceof RequestNotPermitted) { + return requestNotPermittedExceptionHandler(request, (RequestNotPermitted) ex); + } + if (ex instanceof ServiceException) { + return serviceExceptionHandler((ServiceException) ex); + } + if (ex instanceof AccessDeniedException) { + return accessDeniedExceptionHandler(request, (AccessDeniedException) ex); + } + return defaultExceptionHandler(request, ex); + } + + /** + * 处理 SpringMVC 请求参数缺失 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数,结果并未传递 xx 参数 + */ + @ExceptionHandler(value = MissingServletRequestParameterException.class) + public CommonResult missingServletRequestParameterExceptionHandler(MissingServletRequestParameterException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数缺失:%s", ex.getParameterName())); + } + + /** + * 处理 SpringMVC 请求参数类型错误 + * + * 例如说,接口上设置了 @RequestParam("xx") 参数为 Integer,结果传递 xx 参数类型为 String + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public CommonResult methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { + log.warn("[missingServletRequestParameterExceptionHandler]", ex); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", ex.getMessage())); + } + + /** + * 处理 SpringMVC 参数校验不正确 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public CommonResult methodArgumentNotValidExceptionExceptionHandler(MethodArgumentNotValidException ex) { + log.warn("[methodArgumentNotValidExceptionExceptionHandler]", ex); + FieldError fieldError = ex.getBindingResult().getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 SpringMVC 参数绑定不正确,本质上也是通过 Validator 校验 + */ + @ExceptionHandler(BindException.class) + public CommonResult bindExceptionHandler(BindException ex) { + log.warn("[handleBindException]", ex); + FieldError fieldError = ex.getFieldError(); + assert fieldError != null; // 断言,避免告警 + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", fieldError.getDefaultMessage())); + } + + /** + * 处理 Validator 校验不通过产生的异常 + */ + @ExceptionHandler(value = ConstraintViolationException.class) + public CommonResult constraintViolationExceptionHandler(ConstraintViolationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + ConstraintViolation constraintViolation = ex.getConstraintViolations().iterator().next(); + return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数不正确:%s", constraintViolation.getMessage())); + } + + /** + * 处理 Dubbo Consumer 本地参数校验时,抛出的 ValidationException 异常 + */ + @ExceptionHandler(value = ValidationException.class) + public CommonResult validationException(ValidationException ex) { + log.warn("[constraintViolationExceptionHandler]", ex); + // 无法拼接明细的错误信息,因为 Dubbo Consumer 抛出 ValidationException 异常时,是直接的字符串信息,且人类不可读 + return CommonResult.error(BAD_REQUEST); + } + + /** + * 处理 SpringMVC 请求地址不存在 + * + * 注意,它需要设置如下两个配置项: + * 1. spring.mvc.throw-exception-if-no-handler-found 为 true + * 2. spring.mvc.static-path-pattern 为 /statics/** + */ + @ExceptionHandler(NoHandlerFoundException.class) + public CommonResult noHandlerFoundExceptionHandler(HttpServletRequest req, NoHandlerFoundException ex) { + log.warn("[noHandlerFoundExceptionHandler]", ex); + return CommonResult.error(NOT_FOUND.getCode(), String.format("请求地址不存在:%s", ex.getRequestURL())); + } + + /** + * 处理 SpringMVC 请求方法不正确 + * + * 例如说,A 接口的方法为 GET 方式,结果请求方法为 POST 方式,导致不匹配 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public CommonResult httpRequestMethodNotSupportedExceptionHandler(HttpRequestMethodNotSupportedException ex) { + log.warn("[httpRequestMethodNotSupportedExceptionHandler]", ex); + return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); + } + + /** + * 处理 Resilience4j 限流抛出的异常 + */ + @ExceptionHandler(value = RequestNotPermitted.class) + public CommonResult requestNotPermittedExceptionHandler(HttpServletRequest req, RequestNotPermitted ex) { + log.warn("[requestNotPermittedExceptionHandler][url({}) 访问过于频繁]", req.getRequestURL(), ex); + return CommonResult.error(TOO_MANY_REQUESTS); + } + + /** + * 处理 Spring Security 权限不足的异常 + * + * 来源是,使用 @PreAuthorize 注解,AOP 进行权限拦截 + */ + @ExceptionHandler(value = AccessDeniedException.class) + public CommonResult accessDeniedExceptionHandler(HttpServletRequest req, AccessDeniedException ex) { + log.warn("[accessDeniedExceptionHandler][userId({}) 无法访问 url({})]", WebFrameworkUtils.getLoginUserId(req), + req.getRequestURL(), ex); + return CommonResult.error(FORBIDDEN); + } + + /** + * 处理业务异常 ServiceException + * + * 例如说,商品库存不足,用户手机号已存在。 + */ + @ExceptionHandler(value = ServiceException.class) + public CommonResult serviceExceptionHandler(ServiceException ex) { + log.info("[serviceExceptionHandler]", ex); + return CommonResult.error(ex.getCode(), ex.getMessage()); + } + + /** + * 处理系统异常,兜底处理所有的一切 + */ + @ExceptionHandler(value = Exception.class) + public CommonResult defaultExceptionHandler(HttpServletRequest req, Throwable ex) { + // 情况一:处理表不存在的异常 + CommonResult tableNotExistsResult = handleTableNotExists(ex); + if (tableNotExistsResult != null) { + return tableNotExistsResult; + } + + // 情况二:处理异常 + log.error("[defaultExceptionHandler]", ex); + // 插入异常日志 + this.createExceptionLog(req, ex); + // 返回 ERROR CommonResult + return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg()); + } + + private void createExceptionLog(HttpServletRequest req, Throwable e) { + // 插入错误日志 + ApiErrorLog errorLog = new ApiErrorLog(); + try { + // 初始化 errorLog + initExceptionLog(errorLog, req, e); + // 执行插入 errorLog + apiErrorLogFrameworkService.createApiErrorLog(errorLog); + } catch (Throwable th) { + log.error("[createExceptionLog][url({}) log({}) 发生异常]", req.getRequestURI(), JsonUtils.toJsonString(errorLog), th); + } + } + + private void initExceptionLog(ApiErrorLog errorLog, HttpServletRequest request, Throwable e) { + // 处理用户信息 + errorLog.setUserId(WebFrameworkUtils.getLoginUserId(request)); + errorLog.setUserType(WebFrameworkUtils.getLoginUserType(request)); + // 设置异常字段 + errorLog.setExceptionName(e.getClass().getName()); + errorLog.setExceptionMessage(ExceptionUtil.getMessage(e)); + errorLog.setExceptionRootCauseMessage(ExceptionUtil.getRootCauseMessage(e)); + errorLog.setExceptionStackTrace(ExceptionUtils.getStackTrace(e)); + StackTraceElement[] stackTraceElements = e.getStackTrace(); + Assert.notEmpty(stackTraceElements, "异常 stackTraceElements 不能为空"); + StackTraceElement stackTraceElement = stackTraceElements[0]; + errorLog.setExceptionClassName(stackTraceElement.getClassName()); + errorLog.setExceptionFileName(stackTraceElement.getFileName()); + errorLog.setExceptionMethodName(stackTraceElement.getMethodName()); + errorLog.setExceptionLineNumber(stackTraceElement.getLineNumber()); + // 设置其它字段 + errorLog.setTraceId(TracerUtils.getTraceId()); + errorLog.setApplicationName(applicationName); + errorLog.setRequestUrl(request.getRequestURI()); + Map requestParams = MapUtil.builder() + .put("query", ServletUtils.getParamMap(request)) + .put("body", ServletUtils.getBody(request)).build(); + errorLog.setRequestParams(JsonUtils.toJsonString(requestParams)); + errorLog.setRequestMethod(request.getMethod()); + errorLog.setUserAgent(ServletUtils.getUserAgent(request)); + errorLog.setUserIp(ServletUtils.getClientIP(request)); + errorLog.setExceptionTime(LocalDateTime.now()); + } + + /** + * 处理 Table 不存在的异常情况 + * + * @param ex 异常 + * @return 如果是 Table 不存在的异常,则返回对应的 CommonResult + */ + private CommonResult handleTableNotExists(Throwable ex) { + String message = ExceptionUtil.getRootCauseMessage(ex); + if (!message.contains("doesn't exist")) { + return null; + } + // 1. 数据报表 + if (message.contains("report_")) { + log.error("[报表模块 yunxi-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[报表模块 yunxi-module-report - 表结构未导入][参考 https://doc.iocoder.cn/report/ 开启]"); + } + // 2. 工作流 + if (message.contains("bpm_")) { + log.error("[工作流模块 yunxi-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[工作流模块 yunxi-module-bpm - 表结构未导入][参考 https://doc.iocoder.cn/bpm/ 开启]"); + } + // 3. 微信公众号 + if (message.contains("mp_")) { + log.error("[微信公众号 yunxi-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[微信公众号 yunxi-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + // 4. 商城系统 + if (StrUtil.containsAny(message, "product_", "promotion_", "trade_")) { + log.error("[商城系统 yunxi-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[商城系统 yunxi-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + } + // 5. 支付平台 + if (message.contains("pay_")) { + log.error("[支付模块 yunxi-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[支付模块 yunxi-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + } + return null; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/handler/GlobalResponseBodyHandler.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/handler/GlobalResponseBodyHandler.java new file mode 100644 index 0000000..e8b8503 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/handler/GlobalResponseBodyHandler.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.framework.web.core.handler; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +/** + * 全局响应结果(ResponseBody)处理器 + * + * 不同于在网上看到的很多文章,会选择自动将 Controller 返回结果包上 {@link CommonResult}, + * 在 onemall 中,是 Controller 在返回时,主动自己包上 {@link CommonResult}。 + * 原因是,GlobalResponseBodyHandler 本质上是 AOP,它不应该改变 Controller 返回的数据结构 + * + * 目前,GlobalResponseBodyHandler 的主要作用是,记录 Controller 的返回结果, + * 方便 {@link com.yunxi.scm.framework.apilog.core.filter.ApiAccessLogFilter} 记录访问日志 + */ +@ControllerAdvice +public class GlobalResponseBodyHandler implements ResponseBodyAdvice { + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public boolean supports(MethodParameter returnType, Class converterType) { + if (returnType.getMethod() == null) { + return false; + } + // 只拦截返回结果为 CommonResult 类型 + return returnType.getMethod().getReturnType() == CommonResult.class; + } + + @Override + @SuppressWarnings("NullableProblems") // 避免 IDEA 警告 + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, + ServerHttpRequest request, ServerHttpResponse response) { + // 记录 Controller 结果 + WebFrameworkUtils.setCommonResult(((ServletServerHttpRequest) request).getServletRequest(), (CommonResult) body); + return body; + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/util/WebFrameworkUtils.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/util/WebFrameworkUtils.java new file mode 100644 index 0000000..2c1eeeb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/core/util/WebFrameworkUtils.java @@ -0,0 +1,128 @@ +package com.yunxi.scm.framework.web.core.util; + +import cn.hutool.core.util.NumberUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.web.config.WebProperties; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletRequest; + +/** + * 专属于 web 包的工具类 + * + * @author 芋道源码 + */ +public class WebFrameworkUtils { + + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_ID = "login_user_id"; + private static final String REQUEST_ATTRIBUTE_LOGIN_USER_TYPE = "login_user_type"; + + private static final String REQUEST_ATTRIBUTE_COMMON_RESULT = "common_result"; + + public static final String HEADER_TENANT_ID = "tenant-id"; + + private static WebProperties properties; + + public WebFrameworkUtils(WebProperties webProperties) { + WebFrameworkUtils.properties = webProperties; + } + + /** + * 获得租户编号,从 header 中 + * 考虑到其它 framework 组件也会使用到租户编号,所以不得不放在 WebFrameworkUtils 统一提供 + * + * @param request 请求 + * @return 租户编号 + */ + public static Long getTenantId(HttpServletRequest request) { + String tenantId = request.getHeader(HEADER_TENANT_ID); + return NumberUtil.isNumber(tenantId) ? Long.valueOf(tenantId) : null; + } + + public static void setLoginUserId(ServletRequest request, Long userId) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID, userId); + } + + /** + * 设置用户类型 + * + * @param request 请求 + * @param userType 用户类型 + */ + public static void setLoginUserType(ServletRequest request, Integer userType) { + request.setAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE, userType); + } + + /** + * 获得当前用户的编号,从请求中 + * 注意:该方法仅限于 framework 框架使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Long getLoginUserId(HttpServletRequest request) { + if (request == null) { + return null; + } + return (Long) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_ID); + } + + /** + * 获得当前用户的类型 + * 注意:该方法仅限于 web 相关的 framework 组件使用!!! + * + * @param request 请求 + * @return 用户编号 + */ + public static Integer getLoginUserType(HttpServletRequest request) { + if (request == null) { + return null; + } + // 1. 优先,从 Attribute 中获取 + Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); + if (userType != null) { + return userType; + } + // 2. 其次,基于 URL 前缀的约定 + if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) { + return UserTypeEnum.ADMIN.getValue(); + } + if (request.getRequestURI().startsWith(properties.getAppApi().getPrefix())) { + return UserTypeEnum.MEMBER.getValue(); + } + return null; + } + + public static Integer getLoginUserType() { + HttpServletRequest request = getRequest(); + return getLoginUserType(request); + } + + public static Long getLoginUserId() { + HttpServletRequest request = getRequest(); + return getLoginUserId(request); + } + + public static void setCommonResult(ServletRequest request, CommonResult result) { + request.setAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT, result); + } + + public static CommonResult getCommonResult(ServletRequest request) { + return (CommonResult) request.getAttribute(REQUEST_ATTRIBUTE_COMMON_RESULT); + } + + public static HttpServletRequest getRequest() { + RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (!(requestAttributes instanceof ServletRequestAttributes)) { + return null; + } + ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes; + return servletRequestAttributes.getRequest(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/package-info.java new file mode 100644 index 0000000..6576686 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * 针对 SpringMVC 的基础封装 + */ +package com.yunxi.scm.framework.web; diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/config/XssProperties.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/config/XssProperties.java new file mode 100644 index 0000000..c0ff689 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/config/XssProperties.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.framework.xss.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import java.util.Collections; +import java.util.List; + +/** + * Xss 配置属性 + * + * @author 芋道源码 + */ +@ConfigurationProperties(prefix = "yunxi.xss") +@Validated +@Data +public class XssProperties { + + /** + * 是否开启,默认为 true + */ + private boolean enable = true; + /** + * 需要排除的 URL,默认为空 + */ + private List excludeUrls = Collections.emptyList(); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/config/YunxiXssAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/config/YunxiXssAutoConfiguration.java new file mode 100644 index 0000000..7686562 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/config/YunxiXssAutoConfiguration.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.framework.xss.config; + +import com.yunxi.scm.framework.common.enums.WebFilterOrderEnum; +import com.yunxi.scm.framework.xss.core.clean.JsoupXssCleaner; +import com.yunxi.scm.framework.xss.core.clean.XssCleaner; +import com.yunxi.scm.framework.xss.core.filter.XssFilter; +import com.yunxi.scm.framework.xss.core.json.XssStringJsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import static com.yunxi.scm.framework.web.config.YunxiWebAutoConfiguration.createFilterBean; + +@AutoConfiguration +@EnableConfigurationProperties(XssProperties.class) +public class YunxiXssAutoConfiguration implements WebMvcConfigurer { + + /** + * Xss 清理者 + * + * @return XssCleaner + */ + @Bean + @ConditionalOnMissingBean(XssCleaner.class) + public XssCleaner xssCleaner() { + return new JsoupXssCleaner(); + } + + /** + * 注册 Jackson 的序列化器,用于处理 json 类型参数的 xss 过滤 + * + * @return Jackson2ObjectMapperBuilderCustomizer + */ + @Bean + @ConditionalOnMissingBean(name = "xssJacksonCustomizer") + @ConditionalOnBean(ObjectMapper.class) + @ConditionalOnProperty(value = "yunxi.xss.enable", havingValue = "true") + public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssCleaner xssCleaner) { + // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理 + return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(xssCleaner)); + } + + /** + * 创建 XssFilter Bean,解决 Xss 安全问题 + */ + @Bean + @ConditionalOnBean(XssCleaner.class) + public FilterRegistrationBean xssFilter(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) { + return createFilterBean(new XssFilter(properties, pathMatcher, xssCleaner), WebFilterOrderEnum.XSS_FILTER); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/clean/JsoupXssCleaner.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/clean/JsoupXssCleaner.java new file mode 100644 index 0000000..b9d4649 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/clean/JsoupXssCleaner.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.framework.xss.core.clean; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Safelist; + +/** + * 基于 JSONP 实现 XSS 过滤字符串 + */ +public class JsoupXssCleaner implements XssCleaner { + + private final Safelist safelist; + + /** + * 用于在 src 属性使用相对路径时,强制转换为绝对路径。 为空时不处理,值应为绝对路径的前缀(包含协议部分) + */ + private final String baseUri; + + /** + * 无参构造,默认使用 {@link JsoupXssCleaner#buildSafelist} 方法构建一个安全列表 + */ + public JsoupXssCleaner() { + this.safelist = buildSafelist(); + this.baseUri = ""; + } + + /** + * 构建一个 Xss 清理的 Safelist 规则。 + * 基于 Safelist#relaxed() 的基础上: + * 1. 扩展支持了 style 和 class 属性 + * 2. a 标签额外支持了 target 属性 + * 3. img 标签额外支持了 data 协议,便于支持 base64 + * + * @return Safelist + */ + private Safelist buildSafelist() { + // 使用 jsoup 提供的默认的 + Safelist relaxedSafelist = Safelist.relaxed(); + // 富文本编辑时一些样式是使用 style 来进行实现的 + // 比如红色字体 style="color:red;", 所以需要给所有标签添加 style 属性 + // 注意:style 属性会有注入风险 + relaxedSafelist.addAttributes(":all", "style", "class"); + // 保留 a 标签的 target 属性 + relaxedSafelist.addAttributes("a", "target"); + // 支持img 为base64 + relaxedSafelist.addProtocols("img", "src", "data"); + + // 保留相对路径, 保留相对路径时,必须提供对应的 baseUri 属性,否则依然会被删除 + // WHITELIST.preserveRelativeLinks(false); + + // 移除 a 标签和 img 标签的一些协议限制,这会导致 xss 防注入失效,如 + // 虽然可以重写 WhiteList#isSafeAttribute 来处理,但是有隐患,所以暂时不支持相对路径 + // WHITELIST.removeProtocols("a", "href", "ftp", "http", "https", "mailto"); + // WHITELIST.removeProtocols("img", "src", "http", "https"); + return relaxedSafelist; + } + + @Override + public String clean(String html) { + return Jsoup.clean(html, baseUri, safelist, new Document.OutputSettings().prettyPrint(false)); + } + +} + diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/clean/XssCleaner.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/clean/XssCleaner.java new file mode 100644 index 0000000..34c874e --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/clean/XssCleaner.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.framework.xss.core.clean; + +/** + * 对 html 文本中的有 Xss 风险的数据进行清理 + */ +public interface XssCleaner { + + /** + * 清理有 Xss 风险的文本 + * + * @param html 原 html + * @return 清理后的 html + */ + String clean(String html); + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/filter/XssFilter.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/filter/XssFilter.java new file mode 100644 index 0000000..4702387 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/filter/XssFilter.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.framework.xss.core.filter; + +import com.yunxi.scm.framework.xss.config.XssProperties; +import com.yunxi.scm.framework.xss.core.clean.XssCleaner; +import lombok.AllArgsConstructor; +import org.springframework.util.PathMatcher; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Xss 过滤器 + * + * @author 芋道源码 + */ +@AllArgsConstructor +public class XssFilter extends OncePerRequestFilter { + + /** + * 属性 + */ + private final XssProperties properties; + /** + * 路径匹配器 + */ + private final PathMatcher pathMatcher; + + private final XssCleaner xssCleaner; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws IOException, ServletException { + filterChain.doFilter(new XssRequestWrapper(request, xssCleaner), response); + } + + @Override + protected boolean shouldNotFilter(HttpServletRequest request) { + // 如果关闭,则不过滤 + if (!properties.isEnable()) { + return true; + } + + // 如果匹配到无需过滤,则不过滤 + String uri = request.getRequestURI(); + return properties.getExcludeUrls().stream().anyMatch(excludeUrl -> pathMatcher.match(excludeUrl, uri)); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/filter/XssRequestWrapper.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/filter/XssRequestWrapper.java new file mode 100644 index 0000000..92ae987 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/filter/XssRequestWrapper.java @@ -0,0 +1,92 @@ +package com.yunxi.scm.framework.xss.core.filter; + +import com.yunxi.scm.framework.xss.core.clean.XssCleaner; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Xss 请求 Wrapper + * + * @author 芋道源码 + */ +public class XssRequestWrapper extends HttpServletRequestWrapper { + + private final XssCleaner xssCleaner; + + public XssRequestWrapper(HttpServletRequest request, XssCleaner xssCleaner) { + super(request); + this.xssCleaner = xssCleaner; + } + + // ============================ parameter ============================ + @Override + public Map getParameterMap() { + Map map = new LinkedHashMap<>(); + Map parameters = super.getParameterMap(); + for (Map.Entry entry : parameters.entrySet()) { + String[] values = entry.getValue(); + for (int i = 0; i < values.length; i++) { + values[i] = xssCleaner.clean(values[i]); + } + map.put(entry.getKey(), values); + } + return map; + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (values == null) { + return null; + } + int count = values.length; + String[] encodedValues = new String[count]; + for (int i = 0; i < count; i++) { + encodedValues[i] = xssCleaner.clean(values[i]); + } + return encodedValues; + } + + @Override + public String getParameter(String name) { + String value = super.getParameter(name); + if (value == null) { + return null; + } + return xssCleaner.clean(value); + } + + // ============================ attribute ============================ + @Override + public Object getAttribute(String name) { + Object value = super.getAttribute(name); + if (value instanceof String) { + return xssCleaner.clean((String) value); + } + return value; + } + + // ============================ header ============================ + @Override + public String getHeader(String name) { + String value = super.getHeader(name); + if (value == null) { + return null; + } + return xssCleaner.clean(value); + } + + // ============================ queryString ============================ + @Override + public String getQueryString() { + String value = super.getQueryString(); + if (value == null) { + return null; + } + return xssCleaner.clean(value); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/json/XssStringJsonDeserializer.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/json/XssStringJsonDeserializer.java new file mode 100644 index 0000000..75596f6 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/core/json/XssStringJsonDeserializer.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.framework.xss.core.json; + +import com.yunxi.scm.framework.xss.core.clean.XssCleaner; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.std.StringDeserializer; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; + +/** + * XSS 过滤 jackson 反序列化器。 + * 在反序列化的过程中,会对字符串进行 XSS 过滤。 + * + * @author Hccake + */ +@Slf4j +@AllArgsConstructor +public class XssStringJsonDeserializer extends StringDeserializer { + + private final XssCleaner xssCleaner; + + @Override + public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p.hasToken(JsonToken.VALUE_STRING)) { + return xssCleaner.clean(p.getText()); + } + JsonToken t = p.currentToken(); + // [databind#381] + if (t == JsonToken.START_ARRAY) { + return _deserializeFromArray(p, ctxt); + } + // need to gracefully handle byte[] data, as base64 + if (t == JsonToken.VALUE_EMBEDDED_OBJECT) { + Object ob = p.getEmbeddedObject(); + if (ob == null) { + return null; + } + if (ob instanceof byte[]) { + return ctxt.getBase64Variant().encode((byte[]) ob, false); + } + // otherwise, try conversion using toString()... + return ob.toString(); + } + // 29-Jun-2020, tatu: New! "Scalar from Object" (mostly for XML) + if (t == JsonToken.START_OBJECT) { + return ctxt.extractScalarFromObject(p, this, _valueClass); + } + + if (t.isScalarValue()) { + String text = p.getValueAsString(); + return xssCleaner.clean(text); + } + return (String) ctxt.handleUnexpectedToken(_valueClass, p); + } +} + diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/package-info.java new file mode 100644 index 0000000..609b727 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/java/com/yunxi/scm/framework/xss/package-info.java @@ -0,0 +1,6 @@ +/** + * 针对 XSS 的基础封装 + * + * XSS 说明:https://tech.meituan.com/2018/09/27/fe-security.html + */ +package com.yunxi.scm.framework.xss; diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..3c7ef7c --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,6 @@ +com.yunxi.scm.framework.apilog.config.YunxiApiLogAutoConfiguration +com.yunxi.scm.framework.jackson.config.YunxiJacksonAutoConfiguration +com.github.xiaoymin.knife4j.spring.configuration.Knife4jAutoConfiguration +com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration +com.yunxi.scm.framework.web.config.YunxiWebAutoConfiguration +com.yunxi.scm.framework.xss.config.YunxiXssAutoConfiguration diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md new file mode 100644 index 0000000..1ab1fa8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/《芋道 Spring Boot API 接口文档 Swagger 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md b/yunxi-framework/yunxi-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md new file mode 100644 index 0000000..2c5a3e8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-web/《芋道 Spring Boot SpringMVC 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/pom.xml b/yunxi-framework/yunxi-spring-boot-starter-websocket/pom.xml new file mode 100644 index 0000000..ff586b8 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/pom.xml @@ -0,0 +1,37 @@ + + + + com.yunxi.scm + yunxi-framework + ${revision} + + 4.0.0 + yunxi-spring-boot-starter-websocket + jar + + ${project.artifactId} + WebSocket + https://github.com/YunaiV/ruoyi-vue-pro + + + + + + com.yunxi.scm + yunxi-common + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-websocket + + + + \ No newline at end of file diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/WebSocketHandlerConfig.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/WebSocketHandlerConfig.java new file mode 100644 index 0000000..d6336ff --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/WebSocketHandlerConfig.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.framework.websocket.config; + +import com.yunxi.scm.framework.websocket.core.UserHandshakeInterceptor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.web.socket.server.HandshakeInterceptor; + +@EnableConfigurationProperties(WebSocketProperties.class) +public class WebSocketHandlerConfig { + @Bean + public HandshakeInterceptor handshakeInterceptor() { + return new UserHandshakeInterceptor(); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/WebSocketProperties.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/WebSocketProperties.java new file mode 100644 index 0000000..09b92ef --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/WebSocketProperties.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.framework.websocket.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * WebSocket 配置项 + * + * @author xingyu4j + */ +@ConfigurationProperties("yunxi.websocket") +@Data +@Validated +public class WebSocketProperties { + + /** + * 路径 + */ + private String path = ""; + /** + * 默认最多允许同时在线用户数 + */ + private int maxOnlineCount = 0; + /** + * 是否保存session + */ + private boolean sessionMap = true; +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/YunxiWebSocketAutoConfiguration.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/YunxiWebSocketAutoConfiguration.java new file mode 100644 index 0000000..d3e3171 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/config/YunxiWebSocketAutoConfiguration.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.framework.websocket.config; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.List; + +/** + * WebSocket 自动配置 + * + * @author xingyu4j + */ +@AutoConfiguration +// 允许使用 yunxi.websocket.enable=false 禁用websocket +@ConditionalOnProperty(prefix = "yunxi.websocket", value = "enable", matchIfMissing = true) +@EnableConfigurationProperties(WebSocketProperties.class) +public class YunxiWebSocketAutoConfiguration { + @Bean + @ConditionalOnMissingBean + public WebSocketConfigurer webSocketConfigurer(List handshakeInterceptor, + WebSocketHandler webSocketHandler, + WebSocketProperties webSocketProperties) { + + return registry -> registry + .addHandler(webSocketHandler, webSocketProperties.getPath()) + .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0])); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/UserHandshakeInterceptor.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/UserHandshakeInterceptor.java new file mode 100644 index 0000000..694e0e5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/UserHandshakeInterceptor.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.framework.websocket.core; + +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.server.HandshakeInterceptor; + +import java.util.Map; + +public class UserHandshakeInterceptor implements HandshakeInterceptor { + @Override + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception { + LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); + attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser); + return true; + } + + @Override + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { + + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketKeyDefine.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketKeyDefine.java new file mode 100644 index 0000000..ffb952a --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketKeyDefine.java @@ -0,0 +1,9 @@ +package com.yunxi.scm.framework.websocket.core; + + +import lombok.Data; + +@Data +public class WebSocketKeyDefine { + public static final String LOGIN_USER ="LOGIN_USER"; +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketMessageDO.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketMessageDO.java new file mode 100644 index 0000000..78cfe6d --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketMessageDO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.framework.websocket.core; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +@Data +@Accessors(chain = true) +public class WebSocketMessageDO { + /** + * 接收消息的seesion + */ + private List seesionKeyList; + /** + * 发送消息 + */ + private String msgText; + + public static WebSocketMessageDO build(List seesionKeyList, String msgText) { + return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketSessionHandler.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketSessionHandler.java new file mode 100644 index 0000000..7dfebe5 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketSessionHandler.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.framework.websocket.core; + +import org.springframework.web.socket.WebSocketSession; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public final class WebSocketSessionHandler { + private WebSocketSessionHandler() { + } + + private static final Map SESSION_MAP = new ConcurrentHashMap<>(); + + public static void addSession(Object sessionKey, WebSocketSession session) { + SESSION_MAP.put(sessionKey.toString(), session); + } + + public static void removeSession(Object sessionKey) { + SESSION_MAP.remove(sessionKey.toString()); + } + + public static WebSocketSession getSession(Object sessionKey) { + return SESSION_MAP.get(sessionKey.toString()); + } + + public static Collection getSessions() { + return SESSION_MAP.values(); + } + + public static Set getSessionKeys() { + return SESSION_MAP.keySet(); + } + +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketUtils.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketUtils.java new file mode 100644 index 0000000..75e95f3 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/WebSocketUtils.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.framework.websocket.core; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; + +import java.io.IOException; + +@Slf4j +public class WebSocketUtils { + public static boolean sendMessage(WebSocketSession seesion, String message) { + if (seesion == null) { + log.error("seesion 不存在"); + return false; + } + if (seesion.isOpen()) { + try { + seesion.sendMessage(new TextMessage(message)); + } catch (IOException e) { + log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e); + return false; + } + } + return true; + } + + public static boolean sendMessage(Object sessionKey, String message) { + WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey); + return sendMessage(session, message); + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/YunxiWebSocketHandlerDecorator.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/YunxiWebSocketHandlerDecorator.java new file mode 100644 index 0000000..2c3fd7f --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/core/YunxiWebSocketHandlerDecorator.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.framework.websocket.core; + +import com.yunxi.scm.framework.security.core.LoginUser; +import org.springframework.web.socket.CloseStatus; +import org.springframework.web.socket.WebSocketHandler; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.WebSocketHandlerDecorator; + +public class YunxiWebSocketHandlerDecorator extends WebSocketHandlerDecorator { + public YunxiWebSocketHandlerDecorator(WebSocketHandler delegate) { + super(delegate); + } + + /** + * websocket 连接时执行的动作 + * @param session websocket session 对象 + * @throws Exception 异常对象 + */ + @Override + public void afterConnectionEstablished(final WebSocketSession session) throws Exception { + Object sessionKey = sessionKeyGen(session); + WebSocketSessionHandler.addSession(sessionKey, session); + } + + /** + * websocket 关闭连接时执行的动作 + * @param session websocket session 对象 + * @param closeStatus 关闭状态对象 + * @throws Exception 异常对象 + */ + @Override + public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception { + Object sessionKey = sessionKeyGen(session); + WebSocketSessionHandler.removeSession(sessionKey); + } + + public Object sessionKeyGen(WebSocketSession webSocketSession) { + + Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER); + + if (obj instanceof LoginUser) { + LoginUser loginUser = (LoginUser) obj; + // userId 作为唯一区分 + return String.valueOf(loginUser.getId()); + } + + return null; + } +} diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/package-info.java b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/package-info.java new file mode 100644 index 0000000..e5c38cb --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/java/com/yunxi/scm/framework/websocket/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.framework.websocket; diff --git a/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..2646368 --- /dev/null +++ b/yunxi-framework/yunxi-spring-boot-starter-websocket/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.yunxi.scm.framework.websocket.config.YunxiWebSocketAutoConfiguration \ No newline at end of file diff --git a/yunxi-module-bpm/pom.xml b/yunxi-module-bpm/pom.xml new file mode 100644 index 0000000..98a85a2 --- /dev/null +++ b/yunxi-module-bpm/pom.xml @@ -0,0 +1,27 @@ + + + + yunxi + com.yunxi.scm + ${revision} + + 4.0.0 + + yunxi-module-bpm-api + yunxi-module-bpm-biz + + yunxi-module-bpm + pom + + ${project.artifactId} + + bpm 包下,业务流程管理(Business Process Management),我们放工作流的功能。 + 例如说:流程定义、表单配置、审核中心(我的申请、我的待办、我的已办)等等 + bpm 解释:https://baike.baidu.com/item/BPM/1933 + + 工作流基于 Flowable 6 实现,分成流程定义、流程表单、流程实例、流程任务等功能模块。 + + + diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/pom.xml b/yunxi-module-bpm/yunxi-module-bpm-api/pom.xml new file mode 100644 index 0000000..428467a --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/pom.xml @@ -0,0 +1,33 @@ + + + + com.yunxi.scm + yunxi-module-bpm + ${revision} + + 4.0.0 + yunxi-module-bpm-api + jar + + ${project.artifactId} + + bpm 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/package-info.java new file mode 100644 index 0000000..6b97e52 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/package-info.java @@ -0,0 +1,4 @@ +/** + * bpm API 包,定义暴露给其它模块的 API + */ +package com.yunxi.scm.module.bpm.api; diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/task/BpmProcessInstanceApi.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/task/BpmProcessInstanceApi.java new file mode 100644 index 0000000..a96f2c1 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/task/BpmProcessInstanceApi.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.bpm.api.task; + +import com.yunxi.scm.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; + +import javax.validation.Valid; + +/** + * 流程实例 Api 接口 + * + * @author 芋道源码 + */ +public interface BpmProcessInstanceApi { + + /** + * 创建流程实例(提供给内部) + * + * @param userId 用户编号 + * @param reqDTO 创建信息 + * @return 实例的编号 + */ + String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java new file mode 100644 index 0000000..d3c76f5 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/api/task/dto/BpmProcessInstanceCreateReqDTO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.bpm.api.task.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.Map; + +/** + * 流程实例的创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class BpmProcessInstanceCreateReqDTO { + + /** + * 流程定义的标识 + */ + @NotEmpty(message = "流程定义的标识不能为空") + private String processDefinitionKey; + /** + * 变量实例 + */ + private Map variables; + + /** + * 业务的唯一标识 + * + * 例如说,请假申请的编号。通过它,可以查询到对应的实例 + */ + @NotEmpty(message = "业务的唯一标识") + private String businessKey; +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/DictTypeConstants.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/DictTypeConstants.java new file mode 100644 index 0000000..4e1a798 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/DictTypeConstants.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.module.bpm.enums; + +/** + * BPM 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String TASK_ASSIGN_RULE_TYPE = "bpm_task_assign_rule_type"; // 任务分配规则类型 + String TASK_ASSIGN_SCRIPT = "bpm_task_assign_script"; // 任务分配自定义脚本 + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/ErrorCodeConstants.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..76e06b2 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/ErrorCodeConstants.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.module.bpm.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Bpm 错误码枚举类 + * + * bpm 系统,使用 1-009-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 通用流程处理 模块 1009000000 ========== + ErrorCode HIGHLIGHT_IMG_ERROR = new ErrorCode(1009000002, "获取高亮流程图异常"); + + // ========== OA 流程模块 1009001000 ========== + ErrorCode OA_LEAVE_NOT_EXISTS = new ErrorCode(1009001001, "请假申请不存在"); + ErrorCode OA_PM_POST_NOT_EXISTS = new ErrorCode(1009001002, "项目经理岗位未设置"); + ErrorCode OA_DEPART_PM_POST_NOT_EXISTS = new ErrorCode(1009001009, "部门的项目经理不存在"); + ErrorCode OA_BM_POST_NOT_EXISTS = new ErrorCode(1009001004, "部门经理岗位未设置"); + ErrorCode OA_DEPART_BM_POST_NOT_EXISTS = new ErrorCode(1009001005, "部门的部门经理不存在"); + ErrorCode OA_HR_POST_NOT_EXISTS = new ErrorCode(1009001006, "HR岗位未设置"); + ErrorCode OA_DAY_LEAVE_ERROR = new ErrorCode(1009001007, "请假天数必须>=1"); + + // ========== 流程模型 1009002000 ========== + ErrorCode MODEL_KEY_EXISTS = new ErrorCode(1009002000, "已经存在流程标识为【{}】的流程"); + ErrorCode MODEL_NOT_EXISTS = new ErrorCode(1009002001, "流程模型不存在"); + ErrorCode MODEL_KEY_VALID = new ErrorCode(1009002002, "流程标识格式不正确,需要以字母或下划线开头,后接任意字母、数字、中划线、下划线、句点!"); + ErrorCode MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG = new ErrorCode(1009002003, "部署流程失败,原因:流程表单未配置,请点击【修改流程】按钮进行配置"); + ErrorCode MODEL_DEPLOY_FAIL_TASK_ASSIGN_RULE_NOT_CONFIG = new ErrorCode(1009002004, "部署流程失败," + + "原因:用户任务({})未配置分配规则,请点击【修改流程】按钮进行配置"); + ErrorCode MODEL_DEPLOY_FAIL_TASK_INFO_EQUALS = new ErrorCode(1009003005, "流程定义部署失败,原因:信息未发生变化"); + + // ========== 流程定义 1009003000 ========== + ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1009003000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图"); + ErrorCode PROCESS_DEFINITION_NAME_NOT_MATCH = new ErrorCode(1009003001, "流程定义的名字期望是({}),当前是({}),请修改 BPMN 流程图"); + ErrorCode PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1009003002, "流程定义不存在"); + ErrorCode PROCESS_DEFINITION_IS_SUSPENDED = new ErrorCode(1009003003, "流程定义处于挂起状态"); + ErrorCode PROCESS_DEFINITION_BPMN_MODEL_NOT_EXISTS = new ErrorCode(1009003004, "流程定义的模型不存在"); + + // ========== 流程实例 1009004000 ========== + ErrorCode PROCESS_INSTANCE_NOT_EXISTS = new ErrorCode(1009004000, "流程实例不存在"); + ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS = new ErrorCode(1009004001, "流程取消失败,流程不处于运行中"); + ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF = new ErrorCode(1009004002, "流程取消失败,该流程不是你发起的"); + + // ========== 流程任务 1009005000 ========== + ErrorCode TASK_COMPLETE_FAIL_NOT_EXISTS = new ErrorCode(1009005000, "审批任务失败,原因:该任务不处于未审批"); + ErrorCode TASK_COMPLETE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1009005001, "审批任务失败,原因:该任务的审批人不是你"); + + // ========== 流程任务分配规则 1009006000 ========== + ErrorCode TASK_ASSIGN_RULE_EXISTS = new ErrorCode(1009006000, "流程({}) 的任务({}) 已经存在分配规则"); + ErrorCode TASK_ASSIGN_RULE_NOT_EXISTS = new ErrorCode(1009006001, "流程任务分配规则不存在"); + ErrorCode TASK_UPDATE_FAIL_NOT_MODEL = new ErrorCode(1009006002, "只有流程模型的任务分配规则,才允许被修改"); + ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1009006003, "操作失败,原因:找不到任务的审批人!"); + ErrorCode TASK_ASSIGN_SCRIPT_NOT_EXISTS = new ErrorCode(1009006004, "操作失败,原因:任务分配脚本({}) 不存在"); + + // ========== 动态表单模块 1009010000 ========== + ErrorCode FORM_NOT_EXISTS = new ErrorCode(1009010000, "动态表单不存在"); + ErrorCode FORM_FIELD_REPEAT = new ErrorCode(1009010001, "表单项({}) 和 ({}) 使用了相同的字段名({})"); + + // ========== 用户组模块 1009011000 ========== + ErrorCode USER_GROUP_NOT_EXISTS = new ErrorCode(1009011000, "用户组不存在"); + ErrorCode USER_GROUP_IS_DISABLE = new ErrorCode(1009011001, "名字为【{}】的用户组已被禁用"); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmModelFormTypeEnum.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmModelFormTypeEnum.java new file mode 100644 index 0000000..bf3757b --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmModelFormTypeEnum.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.bpm.enums.definition; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * BPM 模型的表单类型的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmModelFormTypeEnum { + + NORMAL(10, "流程表单"), // 对应 BpmFormDO + CUSTOM(20, "业务表单") // 业务自己定义的表单,自己进行数据的存储 + ; + + private final Integer type; + private final String desc; +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmTaskAssignRuleTypeEnum.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmTaskAssignRuleTypeEnum.java new file mode 100644 index 0000000..f570cf8 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmTaskAssignRuleTypeEnum.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.bpm.enums.definition; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * BPM 任务分配规则的类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmTaskAssignRuleTypeEnum { + + ROLE(10, "角色"), + DEPT_MEMBER(20, "部门的成员"), // 包括负责人 + DEPT_LEADER(21, "部门的负责人"), + POST(22, "岗位"), + USER(30, "用户"), + USER_GROUP(40, "用户组"), + SCRIPT(50, "自定义脚本"), // 例如说,发起人所在部门的领导、发起人所在部门的领导的领导 + ; + + /** + * 类型 + */ + private final Integer type; + /** + * 描述 + */ + private final String desc; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmTaskRuleScriptEnum.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmTaskRuleScriptEnum.java new file mode 100644 index 0000000..87ae6b1 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/definition/BpmTaskRuleScriptEnum.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.bpm.enums.definition; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * BPM 任务规则的脚本枚举 + * 目前暂时通过 TODO 芋艿:硬编码,未来可以考虑 Groovy 动态脚本的方式 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmTaskRuleScriptEnum { + + START_USER(10L, "流程发起人"), + + LEADER_X1(20L, "流程发起人的一级领导"), + LEADER_X2(21L, "流程发起人的二级领导"); + + /** + * 脚本编号 + */ + private final Long id; + /** + * 脚本描述 + */ + private final String desc; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/message/BpmMessageEnum.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/message/BpmMessageEnum.java new file mode 100644 index 0000000..e94c0ca --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/message/BpmMessageEnum.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.bpm.enums.message; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * Bpm 消息的枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum BpmMessageEnum { + + PROCESS_INSTANCE_APPROVE("bpm_process_instance_approve"), // 流程任务被审批通过时,发送给申请人 + PROCESS_INSTANCE_REJECT("bpm_process_instance_reject"), // 流程任务被审批不通过时,发送给申请人 + TASK_ASSIGNED("bpm_task_assigned"); // 任务被分配时,发送给审批人 + + /** + * 短信模板的标识 + * + * 关联 SmsTemplateDO 的 code 属性 + */ + private final String smsTemplateCode; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceDeleteReasonEnum.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceDeleteReasonEnum.java new file mode 100644 index 0000000..804f573 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceDeleteReasonEnum.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.bpm.enums.task; + +import cn.hutool.core.util.StrUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 流程实例的删除原因 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmProcessInstanceDeleteReasonEnum { + + REJECT_TASK("不通过任务,原因:{}"), // 修改文案时,需要注意 isRejectReason 方法 + CANCEL_TASK("主动取消任务,原因:{}"), + + // ========== 流程任务的独有原因 ========== + MULTI_TASK_END("系统自动取消,原因:多任务审批已经满足条件,无需审批该任务"), // 多实例满足 condition 而结束时,其它任务实例任务会被取消,对应的删除原因是 MI_END + + ; + + private final String reason; + + /** + * 格式化理由 + * + * @param args 参数 + * @return 理由 + */ + public String format(Object... args) { + return StrUtil.format(reason, args); + } + + // ========== 逻辑 ========== + + public static boolean isRejectReason(String reason) { + return StrUtil.startWith(reason, "不通过任务,原因:"); + } + + /** + * 将 Flowable 的删除原因,翻译成对应的中文原因 + * + * @param reason 原始原因 + * @return 原因 + */ + public static String translateReason(String reason) { + if (StrUtil.isEmpty(reason)) { + return reason; + } + switch (reason) { + case "MI_END": return MULTI_TASK_END.getReason(); + default: return reason; + } + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceResultEnum.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceResultEnum.java new file mode 100644 index 0000000..526acfa --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceResultEnum.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.bpm.enums.task; + +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 流程实例的结果 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum BpmProcessInstanceResultEnum { + + PROCESS(1, "处理中"), + APPROVE(2, "通过"), + REJECT(3, "不通过"), + CANCEL(4, "已取消"), + + // ========== 流程任务独有的状态 ========== + + BACK(5, "退回/驳回"); + + /** + * 结果 + * + * 如果新增时,注意 {@link #isEndResult(Integer)} 是否需要变更 + */ + private final Integer result; + /** + * 描述 + */ + private final String desc; + + /** + * 判断该结果是否已经处于 End 最终结果 + * + * 主要用于一些结果更新的逻辑,如果已经是最终结果,就不再进行更新 + * + * @param result 结果 + * @return 是否 + */ + public static boolean isEndResult(Integer result) { + return ObjectUtils.equalsAny(result, APPROVE.getResult(), REJECT.getResult(), CANCEL.getResult(), BACK.getResult()); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java new file mode 100644 index 0000000..9e24960 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-api/src/main/java/com/yunxi/scm/module/bpm/enums/task/BpmProcessInstanceStatusEnum.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.bpm.enums.task; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 流程实例的状态 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum BpmProcessInstanceStatusEnum { + + RUNNING(1, "进行中"), + FINISH(2, "已完成"); + + /** + * 状态 + */ + private final Integer status; + /** + * 描述 + */ + private final String desc; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/pom.xml b/yunxi-module-bpm/yunxi-module-bpm-biz/pom.xml new file mode 100644 index 0000000..5e4f95f --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/pom.xml @@ -0,0 +1,72 @@ + + + + com.yunxi.scm + yunxi-module-bpm + ${revision} + + 4.0.0 + yunxi-module-bpm-biz + + ${project.artifactId} + + bpm 包下,业务流程管理(Business Process Management),我们放工作流的功能,基于 Flowable 6 版本实现。 + 例如说:流程定义、表单配置、审核中心(我的申请、我的待办、我的已办)等等 + + + + com.yunxi.scm + yunxi-module-bpm-api + ${revision} + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-data-permission + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-tenant + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + + + + com.yunxi.scm + yunxi-spring-boot-starter-flowable + + + diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/api/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/api/package-info.java new file mode 100644 index 0000000..75b655a --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/api/package-info.java @@ -0,0 +1,4 @@ +/** + * bpm API 实现类,定义暴露给其它模块的 API + */ +package com.yunxi.scm.module.bpm.api; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/api/task/BpmProcessInstanceApiImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/api/task/BpmProcessInstanceApiImpl.java new file mode 100644 index 0000000..3355138 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/api/task/BpmProcessInstanceApiImpl.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.bpm.api.task; + +import com.yunxi.scm.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import com.yunxi.scm.module.bpm.service.task.BpmProcessInstanceService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Valid; + +/** + * Flowable 流程实例 Api 实现类 + * + * @author 芋道源码 + * @author jason + */ +@Service +@Validated +public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @Override + public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO) { + return processInstanceService.createProcessInstance(userId, reqDTO); + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmFormController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmFormController.java new file mode 100644 index 0000000..4f2691c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmFormController.java @@ -0,0 +1,79 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition; + +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.*; +import com.yunxi.scm.module.bpm.convert.definition.BpmFormConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.module.bpm.service.definition.BpmFormService; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 动态表单") +@RestController +@RequestMapping("/bpm/form") +@Validated +public class BpmFormController { + + @Resource + private BpmFormService formService; + + @PostMapping("/create") + @Operation(summary = "创建动态表单") + @PreAuthorize("@ss.hasPermission('bpm:form:create')") + public CommonResult createForm(@Valid @RequestBody BpmFormCreateReqVO createReqVO) { + return success(formService.createForm(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新动态表单") + @PreAuthorize("@ss.hasPermission('bpm:form:update')") + public CommonResult updateForm(@Valid @RequestBody BpmFormUpdateReqVO updateReqVO) { + formService.updateForm(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除动态表单") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:form:delete')") + public CommonResult deleteForm(@RequestParam("id") Long id) { + formService.deleteForm(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得动态表单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:form:query')") + public CommonResult getForm(@RequestParam("id") Long id) { + BpmFormDO form = formService.getForm(id); + return success(BpmFormConvert.INSTANCE.convert(form)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获得动态表单的精简列表", description = "用于表单下拉框") + public CommonResult> getSimpleForms() { + List list = formService.getFormList(); + return success(BpmFormConvert.INSTANCE.convertList2(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得动态表单分页") + @PreAuthorize("@ss.hasPermission('bpm:form:query')") + public CommonResult> getFormPage(@Valid BpmFormPageReqVO pageVO) { + PageResult pageResult = formService.getFormPage(pageVO); + return success(BpmFormConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmModelController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmModelController.java new file mode 100644 index 0000000..4c6a1b5 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmModelController.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.io.IoUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.model.*; +import com.yunxi.scm.module.bpm.convert.definition.BpmModelConvert; +import com.yunxi.scm.module.bpm.service.definition.BpmModelService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import java.io.IOException; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 流程模型") +@RestController +@RequestMapping("/bpm/model") +@Validated +public class BpmModelController { + + @Resource + private BpmModelService modelService; + + @GetMapping("/page") + @Operation(summary = "获得模型分页") + public CommonResult> getModelPage(BpmModelPageReqVO pageVO) { + return success(modelService.getModelPage(pageVO)); + } + + @GetMapping("/get") + @Operation(summary = "获得模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:model:query')") + public CommonResult getModel(@RequestParam("id") String id) { + BpmModelRespVO model = modelService.getModel(id); + return success(model); + } + + @PostMapping("/create") + @Operation(summary = "新建模型") + @PreAuthorize("@ss.hasPermission('bpm:model:create')") + public CommonResult createModel(@Valid @RequestBody BpmModelCreateReqVO createRetVO) { + return success(modelService.createModel(createRetVO, null)); + } + + @PutMapping("/update") + @Operation(summary = "修改模型") + @PreAuthorize("@ss.hasPermission('bpm:model:update')") + public CommonResult updateModel(@Valid @RequestBody BpmModelUpdateReqVO modelVO) { + modelService.updateModel(modelVO); + return success(true); + } + + @PostMapping("/import") + @Operation(summary = "导入模型") + @PreAuthorize("@ss.hasPermission('bpm:model:import')") + public CommonResult importModel(@Valid BpmModeImportReqVO importReqVO) throws IOException { + BpmModelCreateReqVO createReqVO = BpmModelConvert.INSTANCE.convert(importReqVO); + // 读取文件 + String bpmnXml = IoUtils.readUtf8(importReqVO.getBpmnFile().getInputStream(), false); + return success(modelService.createModel(createReqVO, bpmnXml)); + } + + @PostMapping("/deploy") + @Operation(summary = "部署模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:model:deploy')") + public CommonResult deployModel(@RequestParam("id") String id) { + modelService.deployModel(id); + return success(true); + } + + @PutMapping("/update-state") + @Operation(summary = "修改模型的状态", description = "实际更新的部署的流程定义的状态") + @PreAuthorize("@ss.hasPermission('bpm:model:update')") + public CommonResult updateModelState(@Valid @RequestBody BpmModelUpdateStateReqVO reqVO) { + modelService.updateModelState(reqVO.getId(), reqVO.getState()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除模型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:model:delete')") + public CommonResult deleteModel(@RequestParam("id") String id) { + modelService.deleteModel(id); + return success(true); + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java new file mode 100644 index 0000000..0b61914 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionListReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageItemRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import com.yunxi.scm.module.bpm.service.definition.BpmProcessDefinitionService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 流程定义") +@RestController +@RequestMapping("/bpm/process-definition") +@Validated +public class BpmProcessDefinitionController { + + @Resource + private BpmProcessDefinitionService bpmDefinitionService; + + @GetMapping("/page") + @Operation(summary = "获得流程定义分页") + @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')") + public CommonResult> getProcessDefinitionPage( + BpmProcessDefinitionPageReqVO pageReqVO) { + return success(bpmDefinitionService.getProcessDefinitionPage(pageReqVO)); + } + + @GetMapping ("/list") + @Operation(summary = "获得流程定义列表") + @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')") + public CommonResult> getProcessDefinitionList( + BpmProcessDefinitionListReqVO listReqVO) { + return success(bpmDefinitionService.getProcessDefinitionList(listReqVO)); + } + + @GetMapping ("/get-bpmn-xml") + @Operation(summary = "获得流程定义的 BPMN XML") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:process-definition:query')") + public CommonResult getProcessDefinitionBpmnXML(@RequestParam("id") String id) { + String bpmnXML = bpmDefinitionService.getProcessDefinitionBpmnXML(id); + return success(bpmnXML); + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmTaskAssignRuleController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmTaskAssignRuleController.java new file mode 100644 index 0000000..502f731 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmTaskAssignRuleController.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleUpdateReqVO; +import com.yunxi.scm.module.bpm.service.definition.BpmTaskAssignRuleService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 任务分配规则") +@RestController +@RequestMapping("/bpm/task-assign-rule") +@Validated +public class BpmTaskAssignRuleController { + + @Resource + private BpmTaskAssignRuleService taskAssignRuleService; + + @GetMapping("/list") + @Operation(summary = "获得任务分配规则列表") + @Parameters({ + @Parameter(name = "modelId", description = "模型编号", example = "1024"), + @Parameter(name = "processDefinitionId", description = "流程定义的编号", example = "2048") + }) + @PreAuthorize("@ss.hasPermission('bpm:task-assign-rule:query')") + public CommonResult> getTaskAssignRuleList( + @RequestParam(value = "modelId", required = false) String modelId, + @RequestParam(value = "processDefinitionId", required = false) String processDefinitionId) { + return success(taskAssignRuleService.getTaskAssignRuleList(modelId, processDefinitionId)); + } + + @PostMapping("/create") + @Operation(summary = "创建任务分配规则") + @PreAuthorize("@ss.hasPermission('bpm:task-assign-rule:create')") + public CommonResult createTaskAssignRule(@Valid @RequestBody BpmTaskAssignRuleCreateReqVO reqVO) { + return success(taskAssignRuleService.createTaskAssignRule(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新任务分配规则") + @PreAuthorize("@ss.hasPermission('bpm:task-assign-rule:update')") + public CommonResult updateTaskAssignRule(@Valid @RequestBody BpmTaskAssignRuleUpdateReqVO reqVO) { + taskAssignRuleService.updateTaskAssignRule(reqVO); + return success(true); + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmUserGroupController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmUserGroupController.java new file mode 100644 index 0000000..7f15328 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/BpmUserGroupController.java @@ -0,0 +1,85 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition; + +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupUpdateReqVO; +import com.yunxi.scm.module.bpm.convert.definition.BpmUserGroupConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import com.yunxi.scm.module.bpm.service.definition.BpmUserGroupService; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 用户组") +@RestController +@RequestMapping("/bpm/user-group") +@Validated +public class BpmUserGroupController { + + @Resource + private BpmUserGroupService userGroupService; + + @PostMapping("/create") + @Operation(summary = "创建用户组") + @PreAuthorize("@ss.hasPermission('bpm:user-group:create')") + public CommonResult createUserGroup(@Valid @RequestBody BpmUserGroupCreateReqVO createReqVO) { + return success(userGroupService.createUserGroup(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新用户组") + @PreAuthorize("@ss.hasPermission('bpm:user-group:update')") + public CommonResult updateUserGroup(@Valid @RequestBody BpmUserGroupUpdateReqVO updateReqVO) { + userGroupService.updateUserGroup(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户组") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:user-group:delete')") + public CommonResult deleteUserGroup(@RequestParam("id") Long id) { + userGroupService.deleteUserGroup(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得用户组") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('bpm:user-group:query')") + public CommonResult getUserGroup(@RequestParam("id") Long id) { + BpmUserGroupDO userGroup = userGroupService.getUserGroup(id); + return success(BpmUserGroupConvert.INSTANCE.convert(userGroup)); + } + + @GetMapping("/page") + @Operation(summary = "获得用户组分页") + @PreAuthorize("@ss.hasPermission('bpm:user-group:query')") + public CommonResult> getUserGroupPage(@Valid BpmUserGroupPageReqVO pageVO) { + PageResult pageResult = userGroupService.getUserGroupPage(pageVO); + return success(BpmUserGroupConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取用户组精简信息列表", description = "只包含被开启的用户组,主要用于前端的下拉选项") + public CommonResult> getSimpleUserGroups() { + // 获用户门列表,只要开启状态的 + List list = userGroupService.getUserGroupListByStatus(CommonStatusEnum.ENABLE.getStatus()); + // 排序后,返回给前端 + return success(BpmUserGroupConvert.INSTANCE.convertList2(list)); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormBaseVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormBaseVO.java new file mode 100644 index 0000000..d23ba1f --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormBaseVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.form; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +/** +* 动态表单 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class BpmFormBaseVO { + + @Schema(description = "表单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "表单名称不能为空") + private String name; + + @Schema(description = "表单状态-参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "表单状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "我是备注") + private String remark; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormCreateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormCreateReqVO.java new file mode 100644 index 0000000..7ecb910 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormCreateReqVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.form; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 动态表单创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmFormCreateReqVO extends BpmFormBaseVO { + + @Schema(description = "表单的配置-JSON 字符串", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单的配置不能为空") + private String conf; + + @Schema(description = "表单项的数组-JSON 字符串的数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单项的数组不能为空") + private List fields; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormPageReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormPageReqVO.java new file mode 100644 index 0000000..772866f --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormPageReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.form; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 动态表单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmFormPageReqVO extends PageParam { + + @Schema(description = "表单名称", example = "芋道") + private String name; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormRespVO.java new file mode 100644 index 0000000..dc3aa77 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormRespVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.form; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 动态表单 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmFormRespVO extends BpmFormBaseVO { + + @Schema(description = "表单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "表单的配置-JSON 字符串", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单的配置不能为空") + private String conf; + + @Schema(description = "表单项的数组-JSON 字符串的数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单项的数组不能为空") + private List fields; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormSimpleRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormSimpleRespVO.java new file mode 100644 index 0000000..981327c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormSimpleRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.form; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 流程表单精简 Response VO") +@Data +public class BpmFormSimpleRespVO { + + @Schema(description = "表单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "表单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormUpdateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormUpdateReqVO.java new file mode 100644 index 0000000..f7b8975 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/form/BpmFormUpdateReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.form; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; +import java.util.List; + +@Schema(description = "管理后台 - 动态表单更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmFormUpdateReqVO extends BpmFormBaseVO { + + @Schema(description = "表单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "表单编号不能为空") + private Long id; + + @Schema(description = "表单的配置-JSON 字符串", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单的配置不能为空") + private String conf; + + @Schema(description = "表单项的数组-JSON 字符串的数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "表单项的数组不能为空") + private List fields; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupBaseVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupBaseVO.java new file mode 100644 index 0000000..955b87f --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupBaseVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Set; + +/** +* 用户组 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class BpmUserGroupBaseVO { + + @Schema(description = "组名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "组名不能为空") + private String name; + + @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + @NotNull(message = "描述不能为空") + private String description; + + @Schema(description = "成员编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotNull(message = "成员编号数组不能为空") + private Set memberUserIds; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupCreateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupCreateReqVO.java new file mode 100644 index 0000000..bf8c1c2 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupCreateReqVO.java @@ -0,0 +1,11 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.group; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "管理后台 - 用户组创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmUserGroupCreateReqVO extends BpmUserGroupBaseVO { + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupPageReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupPageReqVO.java new file mode 100644 index 0000000..07a66e5 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupPageReqVO.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.group; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户组分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmUserGroupPageReqVO extends PageParam { + + @Schema(description = "组名", example = "芋道") + private String name; + + @Schema(description = "状态", example = "1") + private Integer status; + + @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java new file mode 100644 index 0000000..40405be --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户组 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmUserGroupRespVO extends BpmUserGroupBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSimpleRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSimpleRespVO.java new file mode 100644 index 0000000..7450011 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSimpleRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 用户组精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class BpmUserGroupSimpleRespVO { + + @Schema(description = "用户组编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户组名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupUpdateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupUpdateReqVO.java new file mode 100644 index 0000000..0fa1a79 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/group/BpmUserGroupUpdateReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.group; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 用户组更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmUserGroupUpdateReqVO extends BpmUserGroupBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java new file mode 100644 index 0000000..cbee0b1 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModeImportReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 流程模型的导入 Request VO 相比流程模型的新建来说,只是多了一个 bpmnFile 文件") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmModeImportReqVO extends BpmModelCreateReqVO { + + @Schema(description = "BPMN 文件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "BPMN 文件不能为空") + private MultipartFile bpmnFile; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelBaseVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelBaseVO.java new file mode 100644 index 0000000..8ade826 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelBaseVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.model; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** +* 流程模型 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class BpmModelBaseVO { + + @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_yunxi") + @NotEmpty(message = "流程标识不能为空") + private String key; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotEmpty(message = "流程名称不能为空") + private String name; + + @Schema(description = "流程描述", example = "我是描述") + private String description; + + @Schema(description = "流程分类-参见 bpm_model_category 数据字典", example = "1") + @NotEmpty(message = "流程分类不能为空") + private String category; + + @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1") + private Integer formType; + @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024") + private Long formId; + @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", + example = "/bpm/oa/leave/create") + private String formCustomCreatePath; + @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", + example = "/bpm/oa/leave/view") + private String formCustomViewPath; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelCreateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelCreateReqVO.java new file mode 100644 index 0000000..2baa13d --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelCreateReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 流程模型的创建 Request VO") +@Data +public class BpmModelCreateReqVO { + + @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_yunxi") + @NotEmpty(message = "流程标识不能为空") + private String key; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotEmpty(message = "流程名称不能为空") + private String name; + + @Schema(description = "流程描述", example = "我是描述") + private String description; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelPageItemRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelPageItemRespVO.java new file mode 100644 index 0000000..98604e8 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelPageItemRespVO.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 流程模型的分页的每一项 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmModelPageItemRespVO extends BpmModelBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "表单名字", example = "请假表单") + private String formName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + /** + * 最新部署的流程定义 + */ + private ProcessDefinition processDefinition; + + @Schema(description = "流程定义") + @Data + public static class ProcessDefinition { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer version; + + @Schema(description = "部署时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime deploymentTime; + + @Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer suspensionState; + + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelPageReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelPageReqVO.java new file mode 100644 index 0000000..ba74a00 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelPageReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.model; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + + +@Schema(description = "管理后台 - 流程模型分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmModelPageReqVO extends PageParam { + + @Schema(description = "标识-精准匹配", example = "process1641042089407") + private String key; + + @Schema(description = "名字-模糊匹配", example = "芋道") + private String name; + + @Schema(description = "流程分类-参见 bpm_model_category 数据字典", example = "1") + private String category; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java new file mode 100644 index 0000000..004e67d --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 流程模型的创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmModelRespVO extends BpmModelBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED) + private String bpmnXml; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateReqVO.java new file mode 100644 index 0000000..c027216 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateReqVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 流程模型的更新 Request VO") +@Data +public class BpmModelUpdateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "编号不能为空") + private String id; + + @Schema(description = "流程名称", example = "芋道") + private String name; + + @Schema(description = "流程描述", example = "我是描述") + private String description; + + @Schema(description = "流程分类-参见 bpm_model_category 数据字典", example = "1") + private String category; + + @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED) + private String bpmnXml; + + @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1") + private Integer formType; + @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024") + private Long formId; + @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", + example = "/bpm/oa/leave/create") + private String formCustomCreatePath; + @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", + example = "/bpm/oa/leave/view") + private String formCustomViewPath; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateStateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateStateReqVO.java new file mode 100644 index 0000000..5a7ec62 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/model/BpmModelUpdateStateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 流程模型更新状态 Request VO") +@Data +public class BpmModelUpdateStateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private String id; + + @Schema(description = "状态-见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer state; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionListReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionListReqVO.java new file mode 100644 index 0000000..7141b29 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionListReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.process; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 流程定义列表 Request VO") +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class BpmProcessDefinitionListReqVO extends PageParam { + + @Schema(description = "中断状态-参见 SuspensionState 枚举", example = "1") + private Integer suspensionState; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageItemRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageItemRespVO.java new file mode 100644 index 0000000..46948bb --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageItemRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.process; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 流程定义的分页的每一项 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmProcessDefinitionPageItemRespVO extends BpmProcessDefinitionRespVO { + + @Schema(description = "表单名字", example = "请假表单") + private String formName; + + @Schema(description = "部署时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime deploymentTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageReqVO.java new file mode 100644 index 0000000..cb8c41c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionPageReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.process; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 流程定义分页 Request VO") +@Data +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class BpmProcessDefinitionPageReqVO extends PageParam { + + @Schema(description = "标识-精准匹配", example = "process1641042089407") + private String key; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java new file mode 100644 index 0000000..4a8f754 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.process; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Schema(description = "管理后台 - 流程定义 Response VO") +@Data +public class BpmProcessDefinitionRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "版本", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer version; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotEmpty(message = "流程名称不能为空") + private String name; + + @Schema(description = "流程描述", example = "我是描述") + private String description; + + @Schema(description = "流程分类-参见 bpm_model_category 数据字典", example = "1") + @NotEmpty(message = "流程分类不能为空") + private String category; + + @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1") + private Integer formType; + @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024") + private Long formId; + @Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) + private String formConf; + @Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) + private List formFields; + @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", + example = "/bpm/oa/leave/create") + private String formCustomCreatePath; + @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", + example = "/bpm/oa/leave/view") + private String formCustomViewPath; + + @Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer suspensionState; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleBaseVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleBaseVO.java new file mode 100644 index 0000000..be3324c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleBaseVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Set; + +/** + * 流程任务分配规则 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class BpmTaskAssignRuleBaseVO { + + @Schema(description = "规则类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "bpm_task_assign_rule_type") + @NotNull(message = "规则类型不能为空") + private Integer type; + + @Schema(description = "规则值数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3") + @NotNull(message = "规则值数组不能为空") + private Set options; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleCreateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleCreateReqVO.java new file mode 100644 index 0000000..da6cc1e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleCreateReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 流程任务分配规则的创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmTaskAssignRuleCreateReqVO extends BpmTaskAssignRuleBaseVO { + + @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "流程模型的编号不能为空") + private String modelId; + + @Schema(description = "流程任务定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotEmpty(message = "流程任务定义的编号不能为空") + private String taskDefinitionKey; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleRespVO.java new file mode 100644 index 0000000..c0b0f6c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleRespVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 流程任务分配规则的 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmTaskAssignRuleRespVO extends BpmTaskAssignRuleBaseVO { + + @Schema(description = "任务分配规则的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private String modelId; + + @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + private String processDefinitionId; + + @Schema(description = "流程任务定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private String taskDefinitionKey; + @Schema(description = "流程任务定义的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "关注芋道") + private String taskDefinitionName; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleUpdateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleUpdateReqVO.java new file mode 100644 index 0000000..f17455c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 流程任务分配规则的更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmTaskAssignRuleUpdateReqVO extends BpmTaskAssignRuleBaseVO { + + @Schema(description = "任务分配规则的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "任务分配规则的编号不能为空") + private Long id; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/BpmOALeaveController.http b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/BpmOALeaveController.http new file mode 100644 index 0000000..96bbf96 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/BpmOALeaveController.http @@ -0,0 +1,12 @@ +### 请求 /bpm/oa/leave/create 接口 => 成功 +POST {{baseUrl}}/bpm/oa/leave/create +Content-Type: application/json +tenant-id: 1 +Authorization: Bearer {{token}} + +{ + "startTime": "2022-03-01", + "endTime": "2022-03-05", + "type": 1, + "reason": "我要请假啦啦啦!" +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/BpmOALeaveController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/BpmOALeaveController.java new file mode 100644 index 0000000..befb256 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/BpmOALeaveController.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.bpm.controller.admin.oa; + +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeavePageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeaveRespVO; +import com.yunxi.scm.module.bpm.convert.oa.BpmOALeaveConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.oa.BpmOALeaveDO; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.bpm.service.oa.BpmOALeaveService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * OA 请假申请 Controller,用于演示自己存储数据,接入工作流的例子 + * + * @author jason + * @author 芋道源码 + */ +@Tag(name = "管理后台 - OA 请假申请") +@RestController +@RequestMapping("/bpm/oa/leave") +@Validated +public class BpmOALeaveController { + + @Resource + private BpmOALeaveService leaveService; + + @PostMapping("/create") + @PreAuthorize("@ss.hasPermission('bpm:oa-leave:create')") + @Operation(summary = "创建请求申请") + public CommonResult createLeave(@Valid @RequestBody BpmOALeaveCreateReqVO createReqVO) { + return success(leaveService.createLeave(getLoginUserId(), createReqVO)); + } + + @GetMapping("/get") + @PreAuthorize("@ss.hasPermission('bpm:oa-leave:query')") + @Operation(summary = "获得请假申请") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getLeave(@RequestParam("id") Long id) { + BpmOALeaveDO leave = leaveService.getLeave(id); + return success(BpmOALeaveConvert.INSTANCE.convert(leave)); + } + + @GetMapping("/page") + @PreAuthorize("@ss.hasPermission('bpm:oa-leave:query')") + @Operation(summary = "获得请假申请分页") + public CommonResult> getLeavePage(@Valid BpmOALeavePageReqVO pageVO) { + PageResult pageResult = leaveService.getLeavePage(getLoginUserId(), pageVO); + return success(BpmOALeaveConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/package-info.java new file mode 100644 index 0000000..f874003 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/package-info.java @@ -0,0 +1,5 @@ +/** + * OA 示例,用于演示外部业务接入 BPM 工作流的示例 + * 一般的接入方式,只需要调用 接口,后续 Admin 用户在管理后台的【待办事务】进行审批 + */ +package com.yunxi.scm.module.bpm.controller.admin.oa; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveBaseVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveBaseVO.java new file mode 100644 index 0000000..2658266 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveBaseVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.bpm.controller.admin.oa.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; +import javax.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* 请假申请 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class BpmOALeaveBaseVO { + + @Schema(description = "请假的开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开始时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime startTime; + @Schema(description = "请假的结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "结束时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endTime; + + @Schema(description = "请假类型-参见 bpm_oa_type 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "阅读芋道源码") + private String reason; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveCreateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveCreateReqVO.java new file mode 100644 index 0000000..c2a079d --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.oa.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.AssertTrue; + +@Schema(description = "管理后台 - 请假申请创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmOALeaveCreateReqVO extends BpmOALeaveBaseVO { + + @AssertTrue(message = "结束时间,需要在开始时间之后") + public boolean isEndTimeValid() { + return !getEndTime().isBefore(getStartTime()); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeavePageReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeavePageReqVO.java new file mode 100644 index 0000000..af4b685 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeavePageReqVO.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.bpm.controller.admin.oa.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; +import com.yunxi.scm.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 请假申请分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmOALeavePageReqVO extends PageParam { + + @Schema(description = "状态-参见 bpm_process_instance_result 枚举", example = "1") + private Integer result; + + @Schema(description = "请假类型-参见 bpm_oa_type", example = "1") + private Integer type; + + @Schema(description = "原因-模糊匹配", example = "阅读芋道源码") + private String reason; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "申请时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveRespVO.java new file mode 100644 index 0000000..8d2555a --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/oa/vo/BpmOALeaveRespVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.bpm.controller.admin.oa.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 请假申请 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmOALeaveRespVO extends BpmOALeaveBaseVO { + + @Schema(description = "请假表单主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "状态-参见 bpm_process_instance_result 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer result; + + @Schema(description = "申请时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "申请时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime createTime; + + @Schema(description = "流程id") + private String processInstanceId; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmActivityController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmActivityController.java new file mode 100644 index 0000000..b45f4fc --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmActivityController.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.bpm.controller.admin.task; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO; +import com.yunxi.scm.module.bpm.service.task.BpmActivityService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 流程活动实例") +@RestController +@RequestMapping("/bpm/activity") +@Validated +public class BpmActivityController { + + @Resource + private BpmActivityService activityService; + + @GetMapping("/list") + @Operation(summary = "生成指定流程实例的高亮流程图", + description = "只高亮进行中的任务。不过要注意,该接口暂时没用,通过前端的 ProcessViewer.vue 界面的 highlightDiagram 方法生成") + @Parameter(name = "processInstanceId", description = "流程实例的编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:task:query')") + public CommonResult> getActivityList( + @RequestParam("processInstanceId") String processInstanceId) { + return success(activityService.getActivityListByProcessInstanceId(processInstanceId)); + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmProcessInstanceController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmProcessInstanceController.java new file mode 100644 index 0000000..030e05a --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmProcessInstanceController.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.bpm.controller.admin.task; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.instance.*; +import com.yunxi.scm.module.bpm.service.task.BpmProcessInstanceService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请” +@RestController +@RequestMapping("/bpm/process-instance") +@Validated +public class BpmProcessInstanceController { + + @Resource + private BpmProcessInstanceService processInstanceService; + + @GetMapping("/my-page") + @Operation(summary = "获得我的实例分页列表", description = "在【我的流程】菜单中,进行调用") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + public CommonResult> getMyProcessInstancePage( + @Valid BpmProcessInstanceMyPageReqVO pageReqVO) { + return success(processInstanceService.getMyProcessInstancePage(getLoginUserId(), pageReqVO)); + } + + @PostMapping("/create") + @Operation(summary = "新建流程实例") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + public CommonResult createProcessInstance(@Valid @RequestBody BpmProcessInstanceCreateReqVO createReqVO) { + return success(processInstanceService.createProcessInstance(getLoginUserId(), createReqVO)); + } + + @GetMapping("/get") + @Operation(summary = "获得指定流程实例", description = "在【流程详细】界面中,进行调用") + @Parameter(name = "id", description = "流程实例的编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + public CommonResult getProcessInstance(@RequestParam("id") String id) { + return success(processInstanceService.getProcessInstanceVO(id)); + } + + @DeleteMapping("/cancel") + @Operation(summary = "取消流程实例", description = "撤回发起的流程") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')") + public CommonResult cancelProcessInstance(@Valid @RequestBody BpmProcessInstanceCancelReqVO cancelReqVO) { + processInstanceService.cancelProcessInstance(getLoginUserId(), cancelReqVO); + return success(true); + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmTaskController.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmTaskController.java new file mode 100644 index 0000000..ea0eff5 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/BpmTaskController.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.bpm.controller.admin.task; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.task.*; +import com.yunxi.scm.module.bpm.service.task.BpmTaskService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.web.core.util.WebFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 流程任务实例") +@RestController +@RequestMapping("/bpm/task") +@Validated +public class BpmTaskController { + + @Resource + private BpmTaskService taskService; + + @GetMapping("todo-page") + @Operation(summary = "获取 Todo 待办任务分页") + @PreAuthorize("@ss.hasPermission('bpm:task:query')") + public CommonResult> getTodoTaskPage(@Valid BpmTaskTodoPageReqVO pageVO) { + return success(taskService.getTodoTaskPage(getLoginUserId(), pageVO)); + } + + @GetMapping("done-page") + @Operation(summary = "获取 Done 已办任务分页") + @PreAuthorize("@ss.hasPermission('bpm:task:query')") + public CommonResult> getDoneTaskPage(@Valid BpmTaskDonePageReqVO pageVO) { + return success(taskService.getDoneTaskPage(getLoginUserId(), pageVO)); + } + + @GetMapping("/list-by-process-instance-id") + @Operation(summary = "获得指定流程实例的任务列表", description = "包括完成的、未完成的") + @Parameter(name = "processInstanceId", description = "流程实例的编号", required = true) + @PreAuthorize("@ss.hasPermission('bpm:task:query')") + public CommonResult> getTaskListByProcessInstanceId( + @RequestParam("processInstanceId") String processInstanceId) { + return success(taskService.getTaskListByProcessInstanceId(processInstanceId)); + } + + @PutMapping("/approve") + @Operation(summary = "通过任务") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult approveTask(@Valid @RequestBody BpmTaskApproveReqVO reqVO) { + taskService.approveTask(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/reject") + @Operation(summary = "不通过任务") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult rejectTask(@Valid @RequestBody BpmTaskRejectReqVO reqVO) { + taskService.rejectTask(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/update-assignee") + @Operation(summary = "更新任务的负责人", description = "用于【流程详情】的【转派】按钮") + @PreAuthorize("@ss.hasPermission('bpm:task:update')") + public CommonResult updateTaskAssignee(@Valid @RequestBody BpmTaskUpdateAssigneeReqVO reqVO) { + taskService.updateTaskAssignee(getLoginUserId(), reqVO); + return success(true); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/activity/BpmActivityRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/activity/BpmActivityRespVO.java new file mode 100644 index 0000000..af9377d --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/activity/BpmActivityRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 流程活动的 Response VO") +@Data +public class BpmActivityRespVO { + + @Schema(description = "流程活动的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String key; + @Schema(description = "流程活动的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartEvent") + private String type; + + @Schema(description = "流程活动的开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + @Schema(description = "流程活动的结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "关联的流程任务的编号-关联的流程任务,只有 UserTask 等类型才有", example = "2048") + private String taskId; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCancelReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCancelReqVO.java new file mode 100644 index 0000000..7f2595e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCancelReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 流程实例的取消 Request VO") +@Data +public class BpmProcessInstanceCancelReqVO { + + @Schema(description = "流程实例的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "流程实例的编号不能为空") + private String id; + + @Schema(description = "取消原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "不请假了!") + @NotEmpty(message = "取消原因不能为空") + private String reason; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java new file mode 100644 index 0000000..81fde50 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.Map; + +@Schema(description = "管理后台 - 流程实例的创建 Request VO") +@Data +public class BpmProcessInstanceCreateReqVO { + + @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "流程定义编号不能为空") + private String processDefinitionId; + + @Schema(description = "变量实例") + private Map variables; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceMyPageReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceMyPageReqVO.java new file mode 100644 index 0000000..9331953 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceMyPageReqVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.instance; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 流程实例的分页 Item Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmProcessInstanceMyPageReqVO extends PageParam { + + @Schema(description = "流程名称", example = "芋道") + private String name; + + @Schema(description = "流程定义的编号", example = "2048") + private String processDefinitionId; + + @Schema(description = "流程实例的状态-参见 bpm_process_instance_status", example = "1") + private Integer status; + + @Schema(description = "流程实例的结果-参见 bpm_process_instance_result", example = "2") + private Integer result; + + @Schema(description = "流程分类-参见 bpm_model_category 数据字典", example = "1") + private String category; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstancePageItemRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstancePageItemRespVO.java new file mode 100644 index 0000000..166c207 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstancePageItemRespVO.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 流程实例的分页 Item Response VO") +@Data +public class BpmProcessInstancePageItemRespVO { + + @Schema(description = "流程实例的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private String processDefinitionId; + + @Schema(description = "流程分类-参见 bpm_model_category 数据字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String category; + + @Schema(description = "流程实例的状态-参见 bpm_process_instance_status", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "流程实例的结果-参见 bpm_process_instance_result", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer result; + + @Schema(description = "提交时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + /** + * 当前任务 + */ + private List tasks; + + @Schema(description = "流程任务") + @Data + public static class Task { + + @Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java new file mode 100644 index 0000000..7a18489 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java @@ -0,0 +1,94 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.instance; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - 流程实例的 Response VO") +@Data +public class BpmProcessInstanceRespVO { + + @Schema(description = "流程实例的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "流程分类-参见 bpm_model_category 数据字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String category; + + @Schema(description = "流程实例的状态-参见 bpm_process_instance_status", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "流程实例的结果-参见 bpm_process_instance_result", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer result; + + @Schema(description = "提交时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "提交的表单值", requiredMode = Schema.RequiredMode.REQUIRED) + private Map formVariables; + + @Schema(description = "业务的唯一标识-例如说,请假申请的编号", example = "1") + private String businessKey; + + /** + * 发起流程的用户 + */ + private User startUser; + + /** + * 流程定义 + */ + private ProcessDefinition processDefinition; + + @Schema(description = "用户信息") + @Data + public static class User { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long deptId; + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") + private String deptName; + + } + + @Schema(description = "流程定义信息") + @Data + public static class ProcessDefinition { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1") + private Integer formType; + @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024") + private Long formId; + @Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) + private String formConf; + @Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) + private List formFields; + @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", + example = "/bpm/oa/leave/create") + private String formCustomCreatePath; + @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", + example = "/bpm/oa/leave/view") + private String formCustomViewPath; + + @Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED) + private String bpmnXml; + + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java new file mode 100644 index 0000000..857329c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 通过流程任务的 Request VO") +@Data +public class BpmTaskApproveReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!") + @NotEmpty(message = "审批意见不能为空") + private String reason; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskDonePageItemRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskDonePageItemRespVO.java new file mode 100644 index 0000000..89485e6 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskDonePageItemRespVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 流程任务的 Done 已完成的分页项 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmTaskDonePageItemRespVO extends BpmTaskTodoPageItemRespVO { + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + @Schema(description = "持续时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Long durationInMillis; + + @Schema(description = "任务结果-参见 bpm_process_instance_result", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer result; + @Schema(description = "审批建议", requiredMode = Schema.RequiredMode.REQUIRED, example = "不请假了!") + private String reason; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskDonePageReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskDonePageReqVO.java new file mode 100644 index 0000000..464686b --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskDonePageReqVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.task; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 流程任务的 Done 已办的分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmTaskDonePageReqVO extends PageParam { + + @Schema(description = "流程任务名", example = "芋道") + private String name; + + @Schema(description = "开始的创建收间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime beginCreateTime; + + @Schema(description = "结束的创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endCreateTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java new file mode 100644 index 0000000..cd56b75 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskRejectReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 不通过流程任务的 Request VO") +@Data +public class BpmTaskRejectReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "审批意见", requiredMode = Schema.RequiredMode.REQUIRED, example = "不错不错!") + @NotEmpty(message = "审批意见不能为空") + private String reason; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java new file mode 100644 index 0000000..68c12eb --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 流程任务的 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmTaskRespVO extends BpmTaskDonePageItemRespVO { + + @Schema(description = "任务定义的标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "user-001") + private String definitionKey; + + /** + * 审核的用户信息 + */ + private User assigneeUser; + + @Schema(description = "用户信息") + @Data + public static class User { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long deptId; + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") + private String deptName; + + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskTodoPageItemRespVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskTodoPageItemRespVO.java new file mode 100644 index 0000000..48a11fc --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskTodoPageItemRespVO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 流程任务的 Running 进行中的分页项 Response VO") +@Data +public class BpmTaskTodoPageItemRespVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "任务名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "接收时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime claimTime; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "激活状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer suspensionState; + + /** + * 所属流程实例 + */ + private ProcessInstance processInstance; + + @Data + @Schema(description = "流程实例") + public static class ProcessInstance { + + @Schema(description = "流程实例编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String id; + + @Schema(description = "流程实例名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "发起人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long startUserId; + + @Schema(description = "发起人的用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String startUserNickname; + + @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private String processDefinitionId; + + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskTodoPageReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskTodoPageReqVO.java new file mode 100644 index 0000000..5b77c8e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskTodoPageReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.task; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 流程任务的 TODO 待办的分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmTaskTodoPageReqVO extends PageParam { + + @Schema(description = "流程任务名", example = "芋道") + private String name; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskUpdateAssigneeReqVO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskUpdateAssigneeReqVO.java new file mode 100644 index 0000000..c0d62fe --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/admin/task/vo/task/BpmTaskUpdateAssigneeReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.bpm.controller.admin.task.vo.task; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 流程任务的更新负责人的 Request VO") +@Data +public class BpmTaskUpdateAssigneeReqVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "任务编号不能为空") + private String id; + + @Schema(description = "新审批人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "新审批人的用户编号不能为空") + private Long assigneeUserId; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/app/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/app/package-info.java new file mode 100644 index 0000000..a5e20c4 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.bpm.controller.app; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/package-info.java new file mode 100644 index 0000000..d2bc746 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yunxi-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yunxi-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.yunxi.scm.module.bpm.controller; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmFormConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmFormConvert.java new file mode 100644 index 0000000..aa890fa --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmFormConvert.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.bpm.convert.definition; + +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormSimpleRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormUpdateReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 动态表单 Convert + * + * @author 芋艿 + */ +@Mapper +public interface BpmFormConvert { + + BpmFormConvert INSTANCE = Mappers.getMapper(BpmFormConvert.class); + + BpmFormDO convert(BpmFormCreateReqVO bean); + + BpmFormDO convert(BpmFormUpdateReqVO bean); + + BpmFormRespVO convert(BpmFormDO bean); + + List convertList2(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmModelConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmModelConvert.java new file mode 100644 index 0000000..6336c8d --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmModelConvert.java @@ -0,0 +1,141 @@ +package com.yunxi.scm.module.bpm.convert.definition; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.model.*; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.Model; +import org.flowable.engine.repository.ProcessDefinition; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 流程模型 Convert + * + * @author yunlongn + */ +@Mapper +public interface BpmModelConvert { + + BpmModelConvert INSTANCE = Mappers.getMapper(BpmModelConvert.class); + + default List convertList(List list, Map formMap, + Map deploymentMap, + Map processDefinitionMap) { + return CollectionUtils.convertList(list, model -> { + BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class); + BpmFormDO form = metaInfo != null ? formMap.get(metaInfo.getFormId()) : null; + Deployment deployment = model.getDeploymentId() != null ? deploymentMap.get(model.getDeploymentId()) : null; + ProcessDefinition processDefinition = model.getDeploymentId() != null ? processDefinitionMap.get(model.getDeploymentId()) : null; + return convert(model, form, deployment, processDefinition); + }); + } + + default BpmModelPageItemRespVO convert(Model model, BpmFormDO form, Deployment deployment, ProcessDefinition processDefinition) { + BpmModelPageItemRespVO modelRespVO = new BpmModelPageItemRespVO(); + modelRespVO.setId(model.getId()); + modelRespVO.setCreateTime(DateUtils.of(model.getCreateTime())); + // 通用 copy + copyTo(model, modelRespVO); + // Form + if (form != null) { + modelRespVO.setFormId(form.getId()); + modelRespVO.setFormName(form.getName()); + } + // ProcessDefinition + modelRespVO.setProcessDefinition(this.convert(processDefinition)); + if (modelRespVO.getProcessDefinition() != null) { + modelRespVO.getProcessDefinition().setSuspensionState(processDefinition.isSuspended() ? + SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode()); + modelRespVO.getProcessDefinition().setDeploymentTime(DateUtils.of(deployment.getDeploymentTime())); + } + return modelRespVO; + } + + default BpmModelRespVO convert(Model model) { + BpmModelRespVO modelRespVO = new BpmModelRespVO(); + modelRespVO.setId(model.getId()); + modelRespVO.setCreateTime(DateUtils.of(model.getCreateTime())); + // 通用 copy + copyTo(model, modelRespVO); + return modelRespVO; + } + + default void copyTo(Model model, BpmModelBaseVO to) { + to.setName(model.getName()); + to.setKey(model.getKey()); + to.setCategory(model.getCategory()); + // metaInfo + BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class); + copyTo(metaInfo, to); + } + + BpmModelCreateReqVO convert(BpmModeImportReqVO bean); + + default BpmProcessDefinitionCreateReqDTO convert2(Model model, BpmFormDO form) { + BpmProcessDefinitionCreateReqDTO createReqDTO = new BpmProcessDefinitionCreateReqDTO(); + createReqDTO.setModelId(model.getId()); + createReqDTO.setName(model.getName()); + createReqDTO.setKey(model.getKey()); + createReqDTO.setCategory(model.getCategory()); + BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class); + // metaInfo + copyTo(metaInfo, createReqDTO); + // form + if (form != null) { + createReqDTO.setFormConf(form.getConf()); + createReqDTO.setFormFields(form.getFields()); + } + return createReqDTO; + } + + void copyTo(BpmModelMetaInfoRespDTO from, @MappingTarget BpmProcessDefinitionCreateReqDTO to); + + void copyTo(BpmModelMetaInfoRespDTO from, @MappingTarget BpmModelBaseVO to); + + BpmModelPageItemRespVO.ProcessDefinition convert(ProcessDefinition bean); + + default void copy(Model model, BpmModelCreateReqVO bean) { + model.setName(bean.getName()); + model.setKey(bean.getKey()); + model.setMetaInfo(buildMetaInfoStr(null, bean.getDescription(), null, null, + null, null)); + } + + default void copy(Model model, BpmModelUpdateReqVO bean) { + model.setName(bean.getName()); + model.setCategory(bean.getCategory()); + model.setMetaInfo(buildMetaInfoStr(JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class), + bean.getDescription(), bean.getFormType(), bean.getFormId(), + bean.getFormCustomCreatePath(), bean.getFormCustomViewPath())); + } + + default String buildMetaInfoStr(BpmModelMetaInfoRespDTO metaInfo, String description, Integer formType, + Long formId, String formCustomCreatePath, String formCustomViewPath) { + if (metaInfo == null) { + metaInfo = new BpmModelMetaInfoRespDTO(); + } + // 只有非空,才进行设置,避免更新时的覆盖 + if (StrUtil.isNotEmpty(description)) { + metaInfo.setDescription(description); + } + if (Objects.nonNull(formType)) { + metaInfo.setFormType(formType); + metaInfo.setFormId(formId); + metaInfo.setFormCustomCreatePath(formCustomCreatePath); + metaInfo.setFormCustomViewPath(formCustomViewPath); + } + return JsonUtils.toJsonString(metaInfo); + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmProcessDefinitionConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmProcessDefinitionConvert.java new file mode 100644 index 0000000..06d5d5f --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmProcessDefinitionConvert.java @@ -0,0 +1,84 @@ +package com.yunxi.scm.module.bpm.convert.definition; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageItemRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.ProcessDefinition; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +/** + * Bpm 流程定义的 Convert + * + * @author yunlong.li + */ +@Mapper +public interface BpmProcessDefinitionConvert { + + BpmProcessDefinitionConvert INSTANCE = Mappers.getMapper(BpmProcessDefinitionConvert.class); + + BpmProcessDefinitionPageItemRespVO convert(ProcessDefinition bean); + + BpmProcessDefinitionExtDO convert2(BpmProcessDefinitionCreateReqDTO bean); + + default List convertList(List list, Map deploymentMap, + Map processDefinitionDOMap, Map formMap) { + return CollectionUtils.convertList(list, definition -> { + Deployment deployment = definition.getDeploymentId() != null ? deploymentMap.get(definition.getDeploymentId()) : null; + BpmProcessDefinitionExtDO definitionDO = processDefinitionDOMap.get(definition.getId()); + BpmFormDO form = definitionDO != null ? formMap.get(definitionDO.getFormId()) : null; + return convert(definition, deployment, definitionDO, form); + }); + } + + default List convertList3(List list, + Map processDefinitionDOMap) { + return CollectionUtils.convertList(list, processDefinition -> { + BpmProcessDefinitionRespVO respVO = convert3(processDefinition); + BpmProcessDefinitionExtDO processDefinitionExtDO = processDefinitionDOMap.get(processDefinition.getId()); + // 复制通用属性 + copyTo(processDefinitionExtDO, respVO); + return respVO; + }); + } + + @Mapping(source = "suspended", target = "suspensionState", qualifiedByName = "convertSuspendedToSuspensionState") + BpmProcessDefinitionRespVO convert3(ProcessDefinition bean); + + @Named("convertSuspendedToSuspensionState") + default Integer convertSuspendedToSuspensionState(boolean suspended) { + return suspended ? SuspensionState.SUSPENDED.getStateCode() : + SuspensionState.ACTIVE.getStateCode(); + } + + default BpmProcessDefinitionPageItemRespVO convert(ProcessDefinition bean, Deployment deployment, + BpmProcessDefinitionExtDO processDefinitionExtDO, BpmFormDO form) { + BpmProcessDefinitionPageItemRespVO respVO = convert(bean); + respVO.setSuspensionState(bean.isSuspended() ? SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode()); + if (deployment != null) { + respVO.setDeploymentTime(LocalDateTimeUtil.of(deployment.getDeploymentTime())); + } + if (form != null) { + respVO.setFormName(form.getName()); + } + // 复制通用属性 + copyTo(processDefinitionExtDO, respVO); + return respVO; + } + + @Mapping(source = "from.id", target = "to.id", ignore = true) + void copyTo(BpmProcessDefinitionExtDO from, @MappingTarget BpmProcessDefinitionRespVO to); +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmTaskAssignRuleConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmTaskAssignRuleConvert.java new file mode 100644 index 0000000..cc61ed7 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmTaskAssignRuleConvert.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.bpm.convert.definition; + +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleUpdateReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO; +import org.flowable.bpmn.model.UserTask; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface BpmTaskAssignRuleConvert { + BpmTaskAssignRuleConvert INSTANCE = Mappers.getMapper(BpmTaskAssignRuleConvert.class); + + default List convertList(List tasks, List rules) { + Map ruleMap = CollectionUtils.convertMap(rules, BpmTaskAssignRuleDO::getTaskDefinitionKey); + // 以 UserTask 为主维度,原因是:流程图编辑后,一些规则实际就没用了。 + return CollectionUtils.convertList(tasks, task -> { + BpmTaskAssignRuleRespVO respVO = convert(ruleMap.get(task.getId())); + if (respVO == null) { + respVO = new BpmTaskAssignRuleRespVO(); + respVO.setTaskDefinitionKey(task.getId()); + } + respVO.setTaskDefinitionName(task.getName()); + return respVO; + }); + } + + BpmTaskAssignRuleRespVO convert(BpmTaskAssignRuleDO bean); + + BpmTaskAssignRuleDO convert(BpmTaskAssignRuleCreateReqVO bean); + + BpmTaskAssignRuleDO convert(BpmTaskAssignRuleUpdateReqVO bean); + + List convertList2(List list); +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmUserGroupConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmUserGroupConvert.java new file mode 100644 index 0000000..af6b815 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/definition/BpmUserGroupConvert.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.bpm.convert.definition; + +import java.util.*; + +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupUpdateReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import com.yunxi.scm.framework.common.pojo.PageResult; + +import org.mapstruct.Mapper; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +/** + * 用户组 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface BpmUserGroupConvert { + + BpmUserGroupConvert INSTANCE = Mappers.getMapper(BpmUserGroupConvert.class); + + BpmUserGroupDO convert(BpmUserGroupCreateReqVO bean); + + BpmUserGroupDO convert(BpmUserGroupUpdateReqVO bean); + + BpmUserGroupRespVO convert(BpmUserGroupDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + @Named("convertList2") + List convertList2(List list); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/message/BpmMessageConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/message/BpmMessageConvert.java new file mode 100644 index 0000000..0589c28 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/message/BpmMessageConvert.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.bpm.convert.message; + +import com.yunxi.scm.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.Map; + +@Mapper +public interface BpmMessageConvert { + + BpmMessageConvert INSTANCE = Mappers.getMapper(BpmMessageConvert.class); + + @Mapping(target = "mobile", ignore = true) + @Mapping(source = "userId", target = "userId") + @Mapping(source = "templateCode", target = "templateCode") + @Mapping(source = "templateParams", target = "templateParams") + SmsSendSingleToUserReqDTO convert(Long userId, String templateCode, Map templateParams); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/oa/BpmOALeaveConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/oa/BpmOALeaveConvert.java new file mode 100644 index 0000000..fe71576 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/oa/BpmOALeaveConvert.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.bpm.convert.oa; + +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeaveRespVO; +import com.yunxi.scm.module.bpm.dal.dataobject.oa.BpmOALeaveDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 请假申请 Convert + * + * @author 芋艿 + */ +@Mapper +public interface BpmOALeaveConvert { + + BpmOALeaveConvert INSTANCE = Mappers.getMapper(BpmOALeaveConvert.class); + + BpmOALeaveDO convert(BpmOALeaveCreateReqVO bean); + + BpmOALeaveRespVO convert(BpmOALeaveDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/package-info.java new file mode 100644 index 0000000..f14113b --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package com.yunxi.scm.module.bpm.convert; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmActivityConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmActivityConvert.java new file mode 100644 index 0000000..9f8f38e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmActivityConvert.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.bpm.convert.task; + +import com.yunxi.scm.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO; +import org.flowable.engine.history.HistoricActivityInstance; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * BPM 活动 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface BpmActivityConvert { + + BpmActivityConvert INSTANCE = Mappers.getMapper(BpmActivityConvert.class); + + List convertList(List list); + + @Mappings({ + @Mapping(source = "activityId", target = "key"), + @Mapping(source = "activityType", target = "type") + }) + BpmActivityRespVO convert(HistoricActivityInstance bean); +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmProcessInstanceConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmProcessInstanceConvert.java new file mode 100644 index 0000000..7b3bb01 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmProcessInstanceConvert.java @@ -0,0 +1,114 @@ +package com.yunxi.scm.module.bpm.convert.task; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.number.NumberUtils; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageItemRespVO; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; +import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO; +import com.yunxi.scm.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEvent; +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +/** + * 流程实例 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface BpmProcessInstanceConvert { + + BpmProcessInstanceConvert INSTANCE = Mappers.getMapper(BpmProcessInstanceConvert.class); + + default PageResult convertPage(PageResult page, + Map> taskMap) { + List list = convertList(page.getList()); + list.forEach(respVO -> respVO.setTasks(convertList2(taskMap.get(respVO.getId())))); + return new PageResult<>(list, page.getTotal()); + } + + List convertList(List list); + + @Mapping(source = "processInstanceId", target = "id") + BpmProcessInstancePageItemRespVO convert(BpmProcessInstanceExtDO bean); + + List convertList2(List tasks); + + default BpmProcessInstanceRespVO convert2(HistoricProcessInstance processInstance, BpmProcessInstanceExtDO processInstanceExt, + ProcessDefinition processDefinition, BpmProcessDefinitionExtDO processDefinitionExt, + String bpmnXml, AdminUserRespDTO startUser, DeptRespDTO dept) { + BpmProcessInstanceRespVO respVO = convert2(processInstance); + copyTo(processInstanceExt, respVO); + // definition + respVO.setProcessDefinition(convert2(processDefinition)); + copyTo(processDefinitionExt, respVO.getProcessDefinition()); + respVO.getProcessDefinition().setBpmnXml(bpmnXml); + // user + if (startUser != null) { + respVO.setStartUser(convert2(startUser)); + if (dept != null) { + respVO.getStartUser().setDeptName(dept.getName()); + } + } + return respVO; + } + + BpmProcessInstanceRespVO convert2(HistoricProcessInstance bean); + + @Mapping(source = "from.id", target = "to.id", ignore = true) + void copyTo(BpmProcessInstanceExtDO from, @MappingTarget BpmProcessInstanceRespVO to); + + BpmProcessInstanceRespVO.ProcessDefinition convert2(ProcessDefinition bean); + + @Mapping(source = "from.id", target = "to.id", ignore = true) + void copyTo(BpmProcessDefinitionExtDO from, @MappingTarget BpmProcessInstanceRespVO.ProcessDefinition to); + + BpmProcessInstanceRespVO.User convert2(AdminUserRespDTO bean); + + default BpmProcessInstanceResultEvent convert(Object source, HistoricProcessInstance instance, Integer result) { + BpmProcessInstanceResultEvent event = new BpmProcessInstanceResultEvent(source); + event.setId(instance.getId()); + event.setProcessDefinitionKey(instance.getProcessDefinitionKey()); + event.setBusinessKey(instance.getBusinessKey()); + event.setResult(result); + return event; + } + + default BpmProcessInstanceResultEvent convert(Object source, ProcessInstance instance, Integer result) { + BpmProcessInstanceResultEvent event = new BpmProcessInstanceResultEvent(source); + event.setId(instance.getId()); + event.setProcessDefinitionKey(instance.getProcessDefinitionKey()); + event.setBusinessKey(instance.getBusinessKey()); + event.setResult(result); + return event; + } + + default BpmMessageSendWhenProcessInstanceApproveReqDTO convert2ApprovedReq(ProcessInstance instance){ + return new BpmMessageSendWhenProcessInstanceApproveReqDTO() + .setStartUserId(NumberUtils.parseLong(instance.getStartUserId())) + .setProcessInstanceId(instance.getId()) + .setProcessInstanceName(instance.getName()); + } + + default BpmMessageSendWhenProcessInstanceRejectReqDTO convert2RejectReq(ProcessInstance instance, String reason) { + return new BpmMessageSendWhenProcessInstanceRejectReqDTO() + .setProcessInstanceName(instance.getName()) + .setProcessInstanceId(instance.getId()) + .setReason(reason) + .setStartUserId(NumberUtils.parseLong(instance.getStartUserId())); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmTaskConvert.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmTaskConvert.java new file mode 100644 index 0000000..91833dd --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/BpmTaskConvert.java @@ -0,0 +1,144 @@ +package com.yunxi.scm.module.bpm.convert.task; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.number.NumberUtils; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.task.BpmTaskDonePageItemRespVO; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.task.BpmTaskTodoPageItemRespVO; +import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmTaskExtDO; +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.mapstruct.*; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +/** + * Bpm 任务 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface BpmTaskConvert { + + BpmTaskConvert INSTANCE = Mappers.getMapper(BpmTaskConvert.class); + + default List convertList1(List tasks, + Map processInstanceMap, + Map userMap) { + return CollectionUtils.convertList(tasks, task -> { + BpmTaskTodoPageItemRespVO respVO = convert1(task); + ProcessInstance processInstance = processInstanceMap.get(task.getProcessInstanceId()); + if (processInstance != null) { + AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); + respVO.setProcessInstance(convert(processInstance, startUser)); + } + return respVO; + }); + } + + @Mapping(source = "suspended", target = "suspensionState", qualifiedByName = "convertSuspendedToSuspensionState") + @Mapping(target = "claimTime", expression = "java(bean.getClaimTime()==null?null: LocalDateTime.ofInstant(bean.getClaimTime().toInstant(),ZoneId.systemDefault()))") + @Mapping(target = "createTime", expression = "java(bean.getCreateTime()==null?null:LocalDateTime.ofInstant(bean.getCreateTime().toInstant(),ZoneId.systemDefault()))") + BpmTaskTodoPageItemRespVO convert1(Task bean); + + @Named("convertSuspendedToSuspensionState") + default Integer convertSuspendedToSuspensionState(boolean suspended) { + return suspended ? SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode(); + } + + default List convertList2(List tasks, + Map bpmTaskExtDOMap, Map historicProcessInstanceMap, + Map userMap) { + return CollectionUtils.convertList(tasks, task -> { + BpmTaskDonePageItemRespVO respVO = convert2(task); + BpmTaskExtDO taskExtDO = bpmTaskExtDOMap.get(task.getId()); + copyTo(taskExtDO, respVO); + HistoricProcessInstance processInstance = historicProcessInstanceMap.get(task.getProcessInstanceId()); + if (processInstance != null) { + AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); + respVO.setProcessInstance(convert(processInstance, startUser)); + } + return respVO; + }); + } + + BpmTaskDonePageItemRespVO convert2(HistoricTaskInstance bean); + + @Mappings({ + @Mapping(source = "processInstance.id", target = "id"), + @Mapping(source = "processInstance.name", target = "name"), + @Mapping(source = "processInstance.startUserId", target = "startUserId"), + @Mapping(source = "processInstance.processDefinitionId", target = "processDefinitionId"), + @Mapping(source = "startUser.nickname", target = "startUserNickname") + }) + BpmTaskTodoPageItemRespVO.ProcessInstance convert(ProcessInstance processInstance, AdminUserRespDTO startUser); + + default List convertList3(List tasks, + Map bpmTaskExtDOMap, HistoricProcessInstance processInstance, + Map userMap, Map deptMap) { + return CollectionUtils.convertList(tasks, task -> { + BpmTaskRespVO respVO = convert3(task); + BpmTaskExtDO taskExtDO = bpmTaskExtDOMap.get(task.getId()); + copyTo(taskExtDO, respVO); + if (processInstance != null) { + AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId())); + respVO.setProcessInstance(convert(processInstance, startUser)); + } + AdminUserRespDTO assignUser = userMap.get(NumberUtils.parseLong(task.getAssignee())); + if (assignUser != null) { + respVO.setAssigneeUser(convert3(assignUser)); + DeptRespDTO dept = deptMap.get(assignUser.getDeptId()); + if (dept != null) { + respVO.getAssigneeUser().setDeptName(dept.getName()); + } + } + return respVO; + }); + } + + @Mapping(source = "taskDefinitionKey", target = "definitionKey") + @Mapping(target = "createTime", expression = "java(bean.getCreateTime() == null ? null : LocalDateTime.ofInstant(bean.getCreateTime().toInstant(), ZoneId.systemDefault()))") + @Mapping(target = "endTime", expression = "java(bean.getEndTime() == null ? null : LocalDateTime.ofInstant(bean.getEndTime().toInstant(), ZoneId.systemDefault()))") + BpmTaskRespVO convert3(HistoricTaskInstance bean); + + BpmTaskRespVO.User convert3(AdminUserRespDTO bean); + + @Mapping(target = "id", ignore = true) + void copyTo(BpmTaskExtDO from, @MappingTarget BpmTaskDonePageItemRespVO to); + + @Mappings({@Mapping(source = "processInstance.id", target = "id"), + @Mapping(source = "processInstance.name", target = "name"), + @Mapping(source = "processInstance.startUserId", target = "startUserId"), + @Mapping(source = "processInstance.processDefinitionId", target = "processDefinitionId"), + @Mapping(source = "startUser.nickname", target = "startUserNickname")}) + BpmTaskTodoPageItemRespVO.ProcessInstance convert(HistoricProcessInstance processInstance, + AdminUserRespDTO startUser); + + default BpmTaskExtDO convert2TaskExt(Task task) { + BpmTaskExtDO taskExtDO = new BpmTaskExtDO().setTaskId(task.getId()) + .setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())).setName(task.getName()) + .setProcessDefinitionId(task.getProcessDefinitionId()).setProcessInstanceId(task.getProcessInstanceId()); + taskExtDO.setCreateTime(LocalDateTimeUtil.of(task.getCreateTime())); + return taskExtDO; + } + + default BpmMessageSendWhenTaskCreatedReqDTO convert(ProcessInstance processInstance, AdminUserRespDTO startUser, + Task task) { + BpmMessageSendWhenTaskCreatedReqDTO reqDTO = new BpmMessageSendWhenTaskCreatedReqDTO(); + reqDTO.setProcessInstanceId(processInstance.getProcessInstanceId()) + .setProcessInstanceName(processInstance.getName()).setStartUserId(startUser.getId()) + .setStartUserNickname(startUser.getNickname()).setTaskId(task.getId()).setTaskName(task.getName()) + .setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())); + return reqDTO; + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/package-info.java new file mode 100644 index 0000000..bce1043 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/task/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.bpm.convert.task; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..19fbece --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmFormDO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmFormDO.java new file mode 100644 index 0000000..69eb6f1 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmFormDO.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.bpm.dal.dataobject.definition; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * 工作流的表单定义 + * 用于工作流的申请表单,需要动态配置的场景 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_form", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmFormDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 表单名 + */ + private String name; + /** + * 状态 + */ + private Integer status; + /** + * 表单的配置 + */ + private String conf; + /** + * 表单项的数组 + * + * 目前直接将 https://github.com/JakHuang/form-generator 生成的 JSON 串,直接保存 + * 定义:https://github.com/JakHuang/form-generator/issues/46 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List fields; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmProcessDefinitionExtDO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmProcessDefinitionExtDO.java new file mode 100644 index 0000000..9109675 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmProcessDefinitionExtDO.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.bpm.dal.dataobject.definition; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.bpm.enums.definition.BpmModelFormTypeEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * Bpm 流程定义的拓展表 + * 主要解决 Activiti {@link ProcessDefinition} 不支持拓展字段,所以新建拓展表 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_process_definition_ext", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmProcessDefinitionExtDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 流程定义的编号 + * + * 关联 ProcessDefinition 的 id 属性 + */ + private String processDefinitionId; + /** + * 流程模型的编号 + * + * 关联 Model 的 id 属性 + */ + private String modelId; + /** + * 描述 + */ + private String description; + + /** + * 表单类型 + * + * 关联 {@link BpmModelFormTypeEnum} + */ + private Integer formType; + /** + * 动态表单编号 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + * + * 关联 {@link BpmFormDO#getId()} + */ + private Long formId; + /** + * 表单的配置 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + * + * 冗余 {@link BpmFormDO#getConf()} + */ + private String formConf; + /** + * 表单项的数组 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + * + * 冗余 {@link BpmFormDO#getFields()} ()} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List formFields; + /** + * 自定义表单的提交路径,使用 Vue 的路由地址 + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomCreatePath; + /** + * 自定义表单的查看路径,使用 Vue 的路由地址 + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomViewPath; + + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmTaskAssignRuleDO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmTaskAssignRuleDO.java new file mode 100644 index 0000000..91e04e2 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmTaskAssignRuleDO.java @@ -0,0 +1,83 @@ +package com.yunxi.scm.module.bpm.dal.dataobject.definition; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum; +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskRuleScriptEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Set; + +/** + * Bpm 任务分配的规则表,用于自定义配置每个任务的负责人、候选人的分配规则。 + * 也就是说,废弃 BPMN 原本的 UserTask 设置的 assignee、candidateUsers 等配置,而是通过使用该规则进行计算对应的负责人。 + * + * 1. 默认情况下,{@link #processDefinitionId} 为 {@link #PROCESS_DEFINITION_ID_NULL} 值,表示贵改则与流程模型关联 + * 2. 在流程模型部署后,会将他的所有规则记录,复制出一份新部署出来的流程定义,通过设置 {@link #processDefinitionId} 为新的流程定义的编号进行关联 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_task_assign_rule", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmTaskAssignRuleDO extends BaseDO { + + /** + * {@link #processDefinitionId} 空串,用于标识属于流程模型,而不属于流程定义 + */ + public static final String PROCESS_DEFINITION_ID_NULL = ""; + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 流程模型编号 + * + * 关联 Model 的 id 属性 + */ + private String modelId; + /** + * 流程定义编号 + * + * 关联 ProcessDefinition 的 id 属性 + */ + private String processDefinitionId; + /** + * 流程任务的定义 Key + * + * 关联 Task 的 taskDefinitionKey 属性 + */ + private String taskDefinitionKey; + + /** + * 规则类型 + * + * 枚举 {@link BpmTaskAssignRuleTypeEnum} + */ + @TableField("`type`") + private Integer type; + /** + * 规则值数组,一般关联指定表的编号 + * 根据 type 不同,对应的值是不同的: + * + * 1. {@link BpmTaskAssignRuleTypeEnum#ROLE} 时:角色编号 + * 2. {@link BpmTaskAssignRuleTypeEnum#DEPT_MEMBER} 时:部门编号 + * 3. {@link BpmTaskAssignRuleTypeEnum#DEPT_LEADER} 时:部门编号 + * 4. {@link BpmTaskAssignRuleTypeEnum#USER} 时:用户编号 + * 5. {@link BpmTaskAssignRuleTypeEnum#USER_GROUP} 时:用户组编号 + * 6. {@link BpmTaskAssignRuleTypeEnum#SCRIPT} 时:脚本编号,目前通过 {@link BpmTaskRuleScriptEnum#getId()} 标识 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set options; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmTaskMessageRuleDO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmTaskMessageRuleDO.java new file mode 100644 index 0000000..deacb9d --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmTaskMessageRuleDO.java @@ -0,0 +1,5 @@ +package com.yunxi.scm.module.bpm.dal.dataobject.definition; + +// TODO 芋艿:先埋个坑。任务消息的配置规则。说白了,就是不同的 +public class BpmTaskMessageRuleDO { +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmUserGroupDO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmUserGroupDO.java new file mode 100644 index 0000000..6a9673a --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/definition/BpmUserGroupDO.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.bpm.dal.dataobject.definition; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Set; + +/** + * Bpm 用户组 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_user_group", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmUserGroupDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + /** + * 组名 + */ + private String name; + /** + * 描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 成员用户编号数组 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set memberUserIds; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java new file mode 100644 index 0000000..72d5e38 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/oa/BpmOALeaveDO.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.module.bpm.dal.dataobject.oa; + +import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceResultEnum; +import lombok.*; +import java.time.LocalDateTime; +import com.baomidou.mybatisplus.annotation.*; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; + +/** + * OA 请假申请 DO + * + * {@link #day} 请假天数,目前先简单做。一般是分成请假上午和下午,可以是 1 整天,可以是 0.5 半天 + * + * @author jason + * @author 芋道源码 + */ +@TableName("bpm_oa_leave") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BpmOALeaveDO extends BaseDO { + + /** + * 请假表单主键 + */ + @TableId + private Long id; + /** + * 申请人的用户编号 + * + * 关联 AdminUserDO 的 id 属性 + */ + private Long userId; + /** + * 请假类型 + */ + @TableField("`type`") + private String type; + /** + * 原因 + */ + private String reason; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 结束时间 + */ + private LocalDateTime endTime; + /** + * 请假天数 + */ + private Long day; + /** + * 请假的结果 + * + * 枚举 {@link BpmProcessInstanceResultEnum} + * 考虑到简单,所以直接复用了 BpmProcessInstanceResultEnum 枚举,也可以自己定义一个枚举哈 + */ + private Integer result; + + /** + * 对应的流程编号 + * + * 关联 ProcessInstance 的 id 属性 + */ + private String processInstanceId; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java new file mode 100644 index 0000000..84cf6a8 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/task/BpmProcessInstanceExtDO.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.bpm.dal.dataobject.task; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceResultEnum; +import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceStatusEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * Bpm 流程实例的拓展表 + * 主要解决 Activiti ProcessInstance 和 HistoricProcessInstance 不支持拓展字段,所以新建拓展表 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_process_instance_ext", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmProcessInstanceExtDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + /** + * 发起流程的用户编号 + * + * 冗余 HistoricProcessInstance 的 startUserId 属性 + */ + private Long startUserId; + /** + * 流程实例的名字 + * + * 冗余 ProcessInstance 的 name 属性,用于筛选 + */ + private String name; + /** + * 流程实例的编号 + * + * 关联 ProcessInstance 的 id 属性 + */ + private String processInstanceId; + /** + * 流程定义的编号 + * + * 关联 ProcessDefinition 的 id 属性 + */ + private String processDefinitionId; + /** + * 流程分类 + * + * 冗余 ProcessDefinition 的 category 属性 + * 数据字典 bpm_model_category + */ + private String category; + /** + * 流程实例的状态 + * + * 枚举 {@link BpmProcessInstanceStatusEnum} + */ + private Integer status; + /** + * 流程实例的结果 + * + * 枚举 {@link BpmProcessInstanceResultEnum} + */ + private Integer result; + /** + * 结束时间 + * + * 冗余 HistoricProcessInstance 的 endTime 属性 + */ + private LocalDateTime endTime; + + /** + * 提交的表单值 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map formVariables; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/task/BpmTaskExtDO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/task/BpmTaskExtDO.java new file mode 100644 index 0000000..5dba50c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/dataobject/task/BpmTaskExtDO.java @@ -0,0 +1,85 @@ +package com.yunxi.scm.module.bpm.dal.dataobject.task; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceResultEnum; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +/** + * Bpm 流程任务的拓展表 + * 主要解决 Flowable Task 和 HistoricTaskInstance 不支持拓展字段,所以新建拓展表 + * + * @author 芋道源码 + */ +@TableName(value = "bpm_task_ext", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmTaskExtDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + + /** + * 任务的审批人 + * + * 冗余 Task 的 assignee 属性 + */ + private Long assigneeUserId; + /** + * 任务的名字 + * + * 冗余 Task 的 name 属性,为了筛选 + */ + private String name; + /** + * 任务的编号 + * + * 关联 Task 的 id 属性 + */ + private String taskId; +// /** +// * 任务的标识 +// * +// * 关联 {@link Task#getTaskDefinitionKey()} +// */ +// private String definitionKey; + /** + * 任务的结果 + * + * 枚举 {@link BpmProcessInstanceResultEnum} + */ + private Integer result; + /** + * 审批建议 + */ + private String reason; + /** + * 任务的结束时间 + * + * 冗余 HistoricTaskInstance 的 endTime 属性 + */ + private LocalDateTime endTime; + + /** + * 流程实例的编号 + * + * 关联 ProcessInstance 的 id 属性 + */ + private String processInstanceId; + /** + * 流程定义的编号 + * + * 关联 ProcessDefinition 的 id 属性 + */ + private String processDefinitionId; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmFormMapper.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmFormMapper.java new file mode 100644 index 0000000..7a3bc08 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmFormMapper.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.bpm.dal.mysql.definition; + + +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.QueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +/** + * 动态表单 Mapper + * + * @author 风里雾里 + */ +@Mapper +public interface BpmFormMapper extends BaseMapperX { + + default PageResult selectPage(BpmFormPageReqVO reqVO) { + return selectPage(reqVO, new QueryWrapperX() + .likeIfPresent("name", reqVO.getName()) + .orderByDesc("id")); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmProcessDefinitionExtMapper.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmProcessDefinitionExtMapper.java new file mode 100644 index 0000000..05f8667 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmProcessDefinitionExtMapper.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.bpm.dal.mysql.definition; + +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface BpmProcessDefinitionExtMapper extends BaseMapperX { + + default List selectListByProcessDefinitionIds(Collection processDefinitionIds) { + return selectList("process_definition_id", processDefinitionIds); + } + + default BpmProcessDefinitionExtDO selectByProcessDefinitionId(String processDefinitionId) { + return selectOne("process_definition_id", processDefinitionId); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmTaskAssignRuleMapper.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmTaskAssignRuleMapper.java new file mode 100644 index 0000000..4070c32 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmTaskAssignRuleMapper.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.bpm.dal.mysql.definition; + +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.QueryWrapperX; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.lang.Nullable; + +import java.util.List; + +@Mapper +public interface BpmTaskAssignRuleMapper extends BaseMapperX { + + default List selectListByProcessDefinitionId(String processDefinitionId, + @Nullable String taskDefinitionKey) { + return selectList(new QueryWrapperX() + .eq("process_definition_id", processDefinitionId) + .eqIfPresent("task_definition_key", taskDefinitionKey)); + } + + default List selectListByModelId(String modelId) { + return selectList(new QueryWrapperX() + .eq("model_id", modelId) + .eq("process_definition_id", BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL)); + } + + default BpmTaskAssignRuleDO selectListByModelIdAndTaskDefinitionKey(String modelId, + String taskDefinitionKey) { + return selectOne(new QueryWrapperX() + .eq("model_id", modelId) + .eq("process_definition_id", BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL) + .eq("task_definition_key", taskDefinitionKey)); + } + + + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmUserGroupMapper.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmUserGroupMapper.java new file mode 100644 index 0000000..3acbc11 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/definition/BpmUserGroupMapper.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.bpm.dal.mysql.definition; + +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 用户组 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface BpmUserGroupMapper extends BaseMapperX { + + default PageResult selectPage(BpmUserGroupPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(BpmUserGroupDO::getName, reqVO.getName()) + .eqIfPresent(BpmUserGroupDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BpmUserGroupDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BpmUserGroupDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(BpmUserGroupDO::getStatus, status); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/oa/BpmOALeaveMapper.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/oa/BpmOALeaveMapper.java new file mode 100644 index 0000000..adb2c4e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/oa/BpmOALeaveMapper.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.bpm.dal.mysql.oa; + +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeavePageReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.oa.BpmOALeaveDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +/** + * 请假申请 Mapper + * + * @author jason + * @author 芋道源码 + */ +@Mapper +public interface BpmOALeaveMapper extends BaseMapperX { + + default PageResult selectPage(Long userId, BpmOALeavePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(BpmOALeaveDO::getUserId, userId) + .eqIfPresent(BpmOALeaveDO::getResult, reqVO.getResult()) + .eqIfPresent(BpmOALeaveDO::getType, reqVO.getType()) + .likeIfPresent(BpmOALeaveDO::getReason, reqVO.getReason()) + .betweenIfPresent(BpmOALeaveDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BpmOALeaveDO::getId)); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/task/BpmProcessInstanceExtMapper.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/task/BpmProcessInstanceExtMapper.java new file mode 100644 index 0000000..74d7f8f --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/task/BpmProcessInstanceExtMapper.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.bpm.dal.mysql.task; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceMyPageReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface BpmProcessInstanceExtMapper extends BaseMapperX { + + default PageResult selectPage(Long userId, BpmProcessInstanceMyPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(BpmProcessInstanceExtDO::getStartUserId, userId) + .likeIfPresent(BpmProcessInstanceExtDO::getName, reqVO.getName()) + .eqIfPresent(BpmProcessInstanceExtDO::getProcessDefinitionId, reqVO.getProcessDefinitionId()) + .eqIfPresent(BpmProcessInstanceExtDO::getCategory, reqVO.getCategory()) + .eqIfPresent(BpmProcessInstanceExtDO::getStatus, reqVO.getStatus()) + .eqIfPresent(BpmProcessInstanceExtDO::getResult, reqVO.getResult()) + .betweenIfPresent(BpmProcessInstanceExtDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BpmProcessInstanceExtDO::getId)); + } + + default BpmProcessInstanceExtDO selectByProcessInstanceId(String processInstanceId) { + return selectOne(BpmProcessInstanceExtDO::getProcessInstanceId, processInstanceId); + } + + default void updateByProcessInstanceId(BpmProcessInstanceExtDO updateObj) { + update(updateObj, new LambdaQueryWrapperX() + .eq(BpmProcessInstanceExtDO::getProcessInstanceId, updateObj.getProcessInstanceId())); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/task/BpmTaskExtMapper.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/task/BpmTaskExtMapper.java new file mode 100644 index 0000000..1c1893e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/dal/mysql/task/BpmTaskExtMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.bpm.dal.mysql.task; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmTaskExtDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface BpmTaskExtMapper extends BaseMapperX { + + default void updateByTaskId(BpmTaskExtDO entity) { + update(entity, new LambdaQueryWrapper().eq(BpmTaskExtDO::getTaskId, entity.getTaskId())); + } + + default List selectListByTaskIds(Collection taskIds) { + return selectList(BpmTaskExtDO::getTaskId, taskIds); + } + + default BpmTaskExtDO selectByTaskId(String taskId) { + return selectOne(BpmTaskExtDO::getTaskId, taskId); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/config/BpmCommonConfiguration.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/config/BpmCommonConfiguration.java new file mode 100644 index 0000000..a6515d5 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/config/BpmCommonConfiguration.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.bpm.framework.bpm.config; + +import com.yunxi.scm.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * BPM 通用的 Configuration 配置类,提供给 Activiti 和 Flowable + */ +@Configuration(proxyBeanMethods = false) +public class BpmCommonConfiguration { + + @Bean + public BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher(ApplicationEventPublisher publisher) { + return new BpmProcessInstanceResultEventPublisher(publisher); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/config/BpmSecurityConfiguration.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/config/BpmSecurityConfiguration.java new file mode 100644 index 0000000..1cecf6b --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/config/BpmSecurityConfiguration.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.bpm.framework.bpm.config; + +import com.yunxi.scm.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * @author kemengkai + * @create 2022-05-07 08:15 + */ +@Configuration("bpmSecurityConfiguration") +public class BpmSecurityConfiguration { + + @Bean("bpmAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // 任务回退接口 + registry.antMatchers(buildAdminApi("/bpm/task/back")).permitAll(); + } + + }; + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEvent.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEvent.java new file mode 100644 index 0000000..b2444c7 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEvent.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.bpm.framework.bpm.core.event; + +import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO; +import lombok.Data; +import org.springframework.context.ApplicationEvent; + +import javax.validation.constraints.NotNull; + +/** + * 流程实例的结果发生变化的 Event + * 定位:由于额外增加了 {@link BpmProcessInstanceExtDO#getResult()} 结果,所以增加该事件 + * + * @author 芋道源码 + */ +@SuppressWarnings("ALL") +@Data +public class BpmProcessInstanceResultEvent extends ApplicationEvent { + + /** + * 流程实例的编号 + */ + @NotNull(message = "流程实例的编号不能为空") + private String id; + /** + * 流程实例的 key + */ + @NotNull(message = "流程实例的 key 不能为空") + private String processDefinitionKey; + /** + * 流程实例的结果 + */ + @NotNull(message = "流程实例的结果不能为空") + private Integer result; + /** + * 流程实例对应的业务标识 + * 例如说,请假 + */ + private String businessKey; + + public BpmProcessInstanceResultEvent(Object source) { + super(source); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventListener.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventListener.java new file mode 100644 index 0000000..89329e9 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventListener.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.bpm.framework.bpm.core.event; + +import cn.hutool.core.util.StrUtil; +import org.springframework.context.ApplicationListener; + +/** + * {@link BpmProcessInstanceResultEvent} 的监听器 + * + * @author 芋道源码 + */ +public abstract class BpmProcessInstanceResultEventListener + implements ApplicationListener { + + @Override + public final void onApplicationEvent(BpmProcessInstanceResultEvent event) { + if (!StrUtil.equals(event.getProcessDefinitionKey(), getProcessDefinitionKey())) { + return; + } + onEvent(event); + } + + /** + * @return 返回监听的流程定义 Key + */ + protected abstract String getProcessDefinitionKey(); + + /** + * 处理事件 + * + * @param event 事件 + */ + protected abstract void onEvent(BpmProcessInstanceResultEvent event); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventPublisher.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventPublisher.java new file mode 100644 index 0000000..d15ada6 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/BpmProcessInstanceResultEventPublisher.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.bpm.framework.bpm.core.event; + +import lombok.AllArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.validation.annotation.Validated; + +import javax.validation.Valid; + +/** + * {@link BpmProcessInstanceResultEvent} 的生产者 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Validated +public class BpmProcessInstanceResultEventPublisher { + + private final ApplicationEventPublisher publisher; + + public void sendProcessInstanceResultEvent(@Valid BpmProcessInstanceResultEvent event) { + publisher.publishEvent(event); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/package-info.java new file mode 100644 index 0000000..4b558ca --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/event/package-info.java @@ -0,0 +1,6 @@ +/** + * 自定义 Event 实现,提供方便业务接入的 Listener! + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.bpm.framework.bpm.core.event; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/package-info.java new file mode 100644 index 0000000..626ac2e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.bpm.framework.bpm.core; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/package-info.java new file mode 100644 index 0000000..7980ef2 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/bpm/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供给 Activiti 和 Flowable 的通用封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.bpm.framework.bpm; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java new file mode 100644 index 0000000..7286741 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/config/BpmFlowableConfiguration.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.bpm.framework.flowable.config; + +import cn.hutool.core.collection.ListUtil; +import com.yunxi.scm.module.bpm.framework.flowable.core.behavior.BpmActivityBehaviorFactory; +import com.yunxi.scm.module.bpm.service.definition.BpmTaskAssignRuleService; +import org.flowable.common.engine.api.delegate.event.FlowableEventListener; +import org.flowable.spring.SpringProcessEngineConfiguration; +import org.flowable.spring.boot.EngineConfigurationConfigurer; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * BPM 模块的 Flowable 配置类 + * + * @author jason + */ +@Configuration(proxyBeanMethods = false) +public class BpmFlowableConfiguration { + + /** + * BPM 模块的 ProcessEngineConfigurationConfigurer 实现类: + * + * 1. 设置各种监听器 + * 2. 设置自定义的 ActivityBehaviorFactory 实现 + */ + @Bean + public EngineConfigurationConfigurer bpmProcessEngineConfigurationConfigurer( + ObjectProvider listeners, + BpmActivityBehaviorFactory bpmActivityBehaviorFactory) { + return configuration -> { + // 注册监听器,例如说 BpmActivityEventListener + configuration.setEventListeners(ListUtil.toList(listeners.iterator())); + // 设置 ActivityBehaviorFactory 实现类,用于流程任务的审核人的自定义 + configuration.setActivityBehaviorFactory(bpmActivityBehaviorFactory); + }; + } + + @Bean + public BpmActivityBehaviorFactory bpmActivityBehaviorFactory(BpmTaskAssignRuleService taskRuleService) { + BpmActivityBehaviorFactory bpmActivityBehaviorFactory = new BpmActivityBehaviorFactory(); + bpmActivityBehaviorFactory.setBpmTaskRuleService(taskRuleService); + return bpmActivityBehaviorFactory; + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmActivityBehaviorFactory.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmActivityBehaviorFactory.java new file mode 100644 index 0000000..39eedde --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmActivityBehaviorFactory.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior; + +import com.yunxi.scm.module.bpm.service.definition.BpmTaskAssignRuleService; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Setter; +import lombok.ToString; +import org.flowable.bpmn.model.Activity; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; +import org.flowable.engine.impl.bpmn.behavior.UserTaskActivityBehavior; +import org.flowable.engine.impl.bpmn.parser.factory.DefaultActivityBehaviorFactory; + +/** + * 自定义的 ActivityBehaviorFactory 实现类,目的如下: + * 1. 自定义 {@link #createUserTaskActivityBehavior(UserTask)}:实现自定义的流程任务的 assignee 负责人的分配 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BpmActivityBehaviorFactory extends DefaultActivityBehaviorFactory { + + @Setter + private BpmTaskAssignRuleService bpmTaskRuleService; + + @Override + public UserTaskActivityBehavior createUserTaskActivityBehavior(UserTask userTask) { + return new BpmUserTaskActivityBehavior(userTask) + .setBpmTaskRuleService(bpmTaskRuleService); + } + + @Override + public ParallelMultiInstanceBehavior createParallelMultiInstanceBehavior(Activity activity, + AbstractBpmnActivityBehavior innerActivityBehavior) { + return new BpmParallelMultiInstanceBehavior(activity, innerActivityBehavior) + .setBpmTaskRuleService(bpmTaskRuleService); + } + + // TODO @ke:SequentialMultiInstanceBehavior 这个抽空也可以看看 + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java new file mode 100644 index 0000000..982980d --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior; + +import com.yunxi.scm.framework.flowable.core.util.FlowableUtils; +import com.yunxi.scm.module.bpm.service.definition.BpmTaskAssignRuleService; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.Activity; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; +import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; + +import java.util.Set; + +/** + * 自定义的【并行】的【多个】流程任务的 assignee 负责人的分配 + * 第一步,基于分配规则,计算出分配任务的【多个】候选人们。 + * 第二步,将【多个】任务候选人们,设置到 DelegateExecution 的 collectionVariable 变量中,以便 BpmUserTaskActivityBehavior 使用它 + * + * @author kemengkai + * @date 2022-04-21 16:57 + */ +@Slf4j +public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehavior { + + @Setter + private BpmTaskAssignRuleService bpmTaskRuleService; + + public BpmParallelMultiInstanceBehavior(Activity activity, + AbstractBpmnActivityBehavior innerActivityBehavior) { + super(activity, innerActivityBehavior); + } + + /** + * 重写该方法,主要实现两个功能: + * 1. 忽略原有的 collectionVariable、collectionElementVariable 表达式,而是采用自己定义的 + * 2. 获得任务的处理人,并设置到 collectionVariable 中,用于 BpmUserTaskActivityBehavior 从中可以获取任务的处理人 + * + * 注意,多个任务实例,每个任务实例对应一个处理人,所以返回的数量就是任务处理人的数量 + * + * @param execution 执行任务 + * @return 数量 + */ + @Override + protected int resolveNrOfInstances(DelegateExecution execution) { + // 第一步,设置 collectionVariable 和 CollectionVariable + // 从 execution.getVariable() 读取所有任务处理人的 key + super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 + super.collectionVariable = FlowableUtils.formatCollectionVariable(execution.getCurrentActivityId()); + // 从 execution.getVariable() 读取当前所有任务处理的人的 key + super.collectionElementVariable = FlowableUtils.formatCollectionElementVariable(execution.getCurrentActivityId()); + + // 第二步,获取任务的所有处理人 + Set assigneeUserIds = bpmTaskRuleService.calculateTaskCandidateUsers(execution); + execution.setVariable(super.collectionVariable, assigneeUserIds); + return assigneeUserIds.size(); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java new file mode 100644 index 0000000..edcde78 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/BpmUserTaskActivityBehavior.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.module.bpm.service.definition.BpmTaskAssignRuleService; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.UserTask; +import org.flowable.common.engine.impl.el.ExpressionManager; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.impl.bpmn.behavior.UserTaskActivityBehavior; +import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl; +import org.flowable.engine.impl.util.TaskHelper; +import org.flowable.task.service.TaskService; +import org.flowable.task.service.impl.persistence.entity.TaskEntity; + +import java.util.List; +import java.util.Set; + +/** + * 自定义的【单个】流程任务的 assignee 负责人的分配 + * 第一步,基于分配规则,计算出分配任务的【单个】候选人。如果找不到,则直接报业务异常,不继续执行后续的流程; + * 第二步,随机选择一个候选人,则选择作为 assignee 负责人。 + * + * @author 芋道源码 + */ +@Slf4j +public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior { + + @Setter + private BpmTaskAssignRuleService bpmTaskRuleService; + + public BpmUserTaskActivityBehavior(UserTask userTask) { + super(userTask); + } + + @Override + protected void handleAssignments(TaskService taskService, String assignee, String owner, + List candidateUsers, List candidateGroups, TaskEntity task, ExpressionManager expressionManager, + DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) { + // 第一步,获得任务的候选用户 + Long assigneeUserId = calculateTaskCandidateUsers(execution); + Assert.notNull(assigneeUserId, "任务处理人不能为空"); + // 第二步,设置作为负责人 + TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId)); + } + + private Long calculateTaskCandidateUsers(DelegateExecution execution) { + // 情况一,如果是多实例的任务,例如说会签、或签等情况,则从 Variable 中获取。它的任务处理人在 BpmParallelMultiInstanceBehavior 中已经被分配了 + if (super.multiInstanceActivityBehavior != null) { + return execution.getVariable(super.multiInstanceActivityBehavior.getCollectionElementVariable(), Long.class); + } + + // 情况二,如果非多实例的任务,则计算任务处理人 + // 第一步,先计算可处理该任务的处理人们 + Set candidateUserIds = bpmTaskRuleService.calculateTaskCandidateUsers(execution); + // 第二步,后随机选择一个任务的处理人 + // 疑问:为什么一定要选择一个任务处理人? + // 解答:项目对 bpm 的任务是责任到人,所以每个任务有且仅有一个处理人。 + // 如果希望一个任务可以同时被多个人处理,可以考虑使用 BpmParallelMultiInstanceBehavior 实现的会签 or 或签。 + int index = RandomUtil.randomInt(candidateUserIds.size()); + return CollUtil.get(candidateUserIds, index); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/BpmTaskAssignScript.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/BpmTaskAssignScript.java new file mode 100644 index 0000000..4eb9d7e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/BpmTaskAssignScript.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script; + +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskRuleScriptEnum; +import org.flowable.engine.delegate.DelegateExecution; + +import java.util.Set; + +/** + * Bpm 任务分配的自定义 Script 脚本 + * 使用场景: + * 1. 设置审批人为发起人 + * 2. 设置审批人为发起人的 Leader + * 3. 甚至审批人为发起人的 Leader 的 Leader + * + * @author 芋道源码 + */ +public interface BpmTaskAssignScript { + + /** + * 基于执行任务,获得任务的候选用户们 + * + * @param execution 执行任务 + * @return 候选人用户的编号数组 + */ + Set calculateTaskCandidateUsers(DelegateExecution execution); + + /** + * 获得枚举值 + * + * @return 枚举值 + */ + BpmTaskRuleScriptEnum getEnum(); +} + diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderAbstractScript.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderAbstractScript.java new file mode 100644 index 0000000..fa9ff2e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderAbstractScript.java @@ -0,0 +1,70 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.impl; + +import com.yunxi.scm.framework.common.util.number.NumberUtils; +import com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript; +import com.yunxi.scm.module.bpm.service.task.BpmProcessInstanceService; +import com.yunxi.scm.module.system.api.dept.DeptApi; +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.api.user.AdminUserApi; +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.util.Assert; + +import javax.annotation.Resource; +import java.util.Set; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static java.util.Collections.emptySet; + +/** + * 分配给发起人的 Leader 审批的 Script 实现类 + * 目前 Leader 的定义是, + * + * @author 芋道源码 + */ +public abstract class BpmTaskAssignLeaderAbstractScript implements BpmTaskAssignScript { + + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + @Resource + @Lazy // 解决循环依赖 + private BpmProcessInstanceService bpmProcessInstanceService; + + protected Set calculateTaskCandidateUsers(DelegateExecution execution, int level) { + Assert.isTrue(level > 0, "level 必须大于 0"); + // 获得发起人 + ProcessInstance processInstance = bpmProcessInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId()); + // 获得对应 leve 的部门 + DeptRespDTO dept = null; + for (int i = 0; i < level; i++) { + // 获得 level 对应的部门 + if (dept == null) { + dept = getStartUserDept(startUserId); + if (dept == null) { // 找不到发起人的部门,所以无法使用该规则 + return emptySet(); + } + } else { + DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()); + if (parentDept == null) { // 找不到父级部门,所以只好结束寻找。原因是:例如说,级别比较高的人,所在部门层级比较少 + break; + } + dept = parentDept; + } + } + return dept.getLeaderUserId() != null ? asSet(dept.getLeaderUserId()) : emptySet(); + } + + private DeptRespDTO getStartUserDept(Long startUserId) { + AdminUserRespDTO startUser = adminUserApi.getUser(startUserId); + if (startUser.getDeptId() == null) { // 找不到部门,所以无法使用该规则 + return null; + } + return deptApi.getDept(startUser.getDeptId()); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX1Script.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX1Script.java new file mode 100644 index 0000000..90dc5ee --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX1Script.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.impl; + +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskRuleScriptEnum; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +import java.util.Set; + +/** + * 分配给发起人的一级 Leader 审批的 Script 实现类 + * + * @author 芋道源码 + */ +@Component +public class BpmTaskAssignLeaderX1Script extends BpmTaskAssignLeaderAbstractScript { + + @Override + public Set calculateTaskCandidateUsers(DelegateExecution execution) { + return calculateTaskCandidateUsers(execution, 1); + } + + @Override + public BpmTaskRuleScriptEnum getEnum() { + return BpmTaskRuleScriptEnum.LEADER_X1; + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX2Script.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX2Script.java new file mode 100644 index 0000000..9ac2fca --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX2Script.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.impl; + +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskRuleScriptEnum; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +import java.util.Set; + +/** + * 分配给发起人的二级 Leader 审批的 Script 实现类 + * + * @author 芋道源码 + */ +@Component +public class BpmTaskAssignLeaderX2Script extends BpmTaskAssignLeaderAbstractScript { + + @Override + public Set calculateTaskCandidateUsers(DelegateExecution execution) { + return calculateTaskCandidateUsers(execution, 2); + } + + @Override + public BpmTaskRuleScriptEnum getEnum() { + return BpmTaskRuleScriptEnum.LEADER_X2; + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignStartUserScript.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignStartUserScript.java new file mode 100644 index 0000000..d328ba2 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignStartUserScript.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.impl; + +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import com.yunxi.scm.framework.common.util.number.NumberUtils; +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskRuleScriptEnum; +import com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript; +import com.yunxi.scm.module.bpm.service.task.BpmProcessInstanceService; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Set; + +/** + * 分配给发起人审批的 Script 实现类 + * + * @author 芋道源码 + */ +@Component +public class BpmTaskAssignStartUserScript implements BpmTaskAssignScript { + + @Resource + @Lazy // 解决循环依赖 + private BpmProcessInstanceService bpmProcessInstanceService; + + @Override + public Set calculateTaskCandidateUsers(DelegateExecution execution) { + ProcessInstance processInstance = bpmProcessInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId()); + return SetUtils.asSet(startUserId); + } + + @Override + public BpmTaskRuleScriptEnum getEnum() { + return BpmTaskRuleScriptEnum.START_USER; + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java new file mode 100644 index 0000000..3beec18 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.listener; + +import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO; +import com.yunxi.scm.module.bpm.service.task.BpmProcessInstanceService; +import com.google.common.collect.ImmutableSet; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; +import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener; +import org.flowable.engine.delegate.event.FlowableCancelledEvent; +import org.flowable.engine.runtime.ProcessInstance; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Set; + +/** + * 监听 {@link ProcessInstance} 的开始与完成,创建与更新对应的 {@link BpmProcessInstanceExtDO} 记录 + * + * @author jason + */ +@Component +public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener { + + @Resource + @Lazy + private BpmProcessInstanceService processInstanceService; + + public static final Set PROCESS_INSTANCE_EVENTS = ImmutableSet.builder() + .add(FlowableEngineEventType.PROCESS_CREATED) + .add(FlowableEngineEventType.PROCESS_CANCELLED) + .add(FlowableEngineEventType.PROCESS_COMPLETED) + .build(); + + public BpmProcessInstanceEventListener(){ + super(PROCESS_INSTANCE_EVENTS); + } + + @Override + protected void processCreated(FlowableEngineEntityEvent event) { + processInstanceService.createProcessInstanceExt((ProcessInstance)event.getEntity()); + } + + @Override + protected void processCancelled(FlowableCancelledEvent event) { + processInstanceService.updateProcessInstanceExtCancel(event); + } + + @Override + protected void processCompleted(FlowableEngineEntityEvent event) { + processInstanceService.updateProcessInstanceExtComplete((ProcessInstance)event.getEntity()); + } +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java new file mode 100644 index 0000000..0e43f8f --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.listener; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmTaskExtDO; +import com.yunxi.scm.module.bpm.service.task.BpmActivityService; +import com.yunxi.scm.module.bpm.service.task.BpmTaskService; +import com.google.common.collect.ImmutableSet; +import lombok.extern.slf4j.Slf4j; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent; +import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType; +import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener; +import org.flowable.engine.delegate.event.FlowableActivityCancelledEvent; +import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.task.api.Task; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Set; + +/** + * 监听 {@link org.flowable.task.api.Task} 的开始与完成,创建与更新对应的 {@link BpmTaskExtDO} 记录 + * + * @author jason + */ +@Component +@Slf4j +public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { + + @Resource + @Lazy // 解决循环依赖 + private BpmTaskService taskService; + + @Resource + @Lazy // 解决循环依赖 + private BpmActivityService activityService; + + public static final Set TASK_EVENTS = ImmutableSet.builder() + .add(FlowableEngineEventType.TASK_CREATED) + .add(FlowableEngineEventType.TASK_ASSIGNED) + .add(FlowableEngineEventType.TASK_COMPLETED) + .add(FlowableEngineEventType.ACTIVITY_CANCELLED) + .build(); + + public BpmTaskEventListener(){ + super(TASK_EVENTS); + } + + @Override + protected void taskCreated(FlowableEngineEntityEvent event) { + taskService.createTaskExt((Task) event.getEntity()); + } + + @Override + protected void taskCompleted(FlowableEngineEntityEvent event) { + taskService.updateTaskExtComplete((Task)event.getEntity()); + } + + @Override + protected void taskAssigned(FlowableEngineEntityEvent event) { + taskService.updateTaskExtAssign((Task)event.getEntity()); + } + + @Override + protected void activityCancelled(FlowableActivityCancelledEvent event) { + List activityList = activityService.getHistoricActivityListByExecutionId(event.getExecutionId()); + if (CollUtil.isEmpty(activityList)) { + log.error("[activityCancelled][使用 executionId({}) 查找不到对应的活动实例]", event.getExecutionId()); + return; + } + // 遍历处理 + activityList.forEach(activity -> { + if (StrUtil.isEmpty(activity.getTaskId())) { + return; + } + taskService.updateTaskExtCancel(activity.getTaskId()); + }); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/package-info.java new file mode 100644 index 0000000..baf432c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 bpm 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.bpm.framework; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/web/config/BpmWebConfiguration.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/web/config/BpmWebConfiguration.java new file mode 100644 index 0000000..294b95a --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/web/config/BpmWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.bpm.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * bpm 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class BpmWebConfiguration { + + /** + * bpm 模块的 API 分组 + */ + @Bean + public GroupedOpenApi bpmGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("bpm"); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/web/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/web/package-info.java new file mode 100644 index 0000000..6db00de --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * bpm 模块的 web 配置 + */ +package com.yunxi.scm.module.bpm.framework.web; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/package-info.java new file mode 100644 index 0000000..33307e0 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/package-info.java @@ -0,0 +1,12 @@ +/** + * bpm 包下,业务流程管理(Business Process Management),我们放工作流的功能,基于 Flowable 6 版本实现。 + * 例如说:流程定义、表单配置、审核中心(我的申请、我的待办、我的已办)等等 + * + * bpm 解释:https://baike.baidu.com/item/BPM/1933 + * + * 1. Controller URL:以 /bpm/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 bpm_ 开头,方便在数据库中区分 + * + * 注意,由于 Bpm 模块下,容易和其它模块重名,所以类名都加载 Bpm 的前缀~ + */ +package com.yunxi.scm.module.bpm; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmFormService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmFormService.java new file mode 100644 index 0000000..da47a78 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmFormService.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormUpdateReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; + + +/** + * 动态表单 Service 接口 + * + * @author @风里雾里 + */ +public interface BpmFormService { + + /** + * 创建动态表单 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createForm(@Valid BpmFormCreateReqVO createReqVO); + + /** + * 更新动态表单 + * + * @param updateReqVO 更新信息 + */ + void updateForm(@Valid BpmFormUpdateReqVO updateReqVO); + + /** + * 删除动态表单 + * + * @param id 编号 + */ + void deleteForm(Long id); + + /** + * 获得动态表单 + * + * @param id 编号 + * @return 动态表单 + */ + BpmFormDO getForm(Long id); + + /** + * 获得动态表单列表 + * + * @return 动态表单列表 + */ + List getFormList(); + + /** + * 获得动态表单列表 + * + * @param ids 编号 + * @return 动态表单列表 + */ + List getFormList(Collection ids); + + /** + * 获得动态表单 Map + * + * @param ids 编号 + * @return 动态表单 Map + */ + default Map getFormMap(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyMap(); + } + return CollectionUtils.convertMap(this.getFormList(ids), BpmFormDO::getId); + } + + /** + * 获得动态表单分页 + * + * @param pageReqVO 分页查询 + * @return 动态表单分页 + */ + PageResult getFormPage(BpmFormPageReqVO pageReqVO); + + /** + * 校验流程表单已配置 + * + * @param configStr configStr 字段 + * @return 流程表单 + */ + BpmFormDO checkFormConfig(String configStr); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmFormServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmFormServiceImpl.java new file mode 100644 index 0000000..ba4bbbb --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmFormServiceImpl.java @@ -0,0 +1,132 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormUpdateReqVO; +import com.yunxi.scm.module.bpm.convert.definition.BpmFormConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.module.bpm.dal.mysql.definition.BpmFormMapper; +import com.yunxi.scm.module.bpm.enums.ErrorCodeConstants; +import com.yunxi.scm.module.bpm.enums.definition.BpmModelFormTypeEnum; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmFormFieldRespDTO; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.*; + +/** + * 动态表单 Service 实现类 + * + * @author 风里雾里 + */ +@Service +@Validated +public class BpmFormServiceImpl implements BpmFormService { + + @Resource + private BpmFormMapper formMapper; + + @Override + public Long createForm(BpmFormCreateReqVO createReqVO) { + this.checkFields(createReqVO.getFields()); + // 插入 + BpmFormDO form = BpmFormConvert.INSTANCE.convert(createReqVO); + formMapper.insert(form); + // 返回 + return form.getId(); + } + + @Override + public void updateForm(BpmFormUpdateReqVO updateReqVO) { + this.checkFields(updateReqVO.getFields()); + // 校验存在 + this.validateFormExists(updateReqVO.getId()); + // 更新 + BpmFormDO updateObj = BpmFormConvert.INSTANCE.convert(updateReqVO); + formMapper.updateById(updateObj); + } + + @Override + public void deleteForm(Long id) { + // 校验存在 + this.validateFormExists(id); + // 删除 + formMapper.deleteById(id); + } + + private void validateFormExists(Long id) { + if (formMapper.selectById(id) == null) { + throw exception(ErrorCodeConstants.FORM_NOT_EXISTS); + } + } + + @Override + public BpmFormDO getForm(Long id) { + return formMapper.selectById(id); + } + + @Override + public List getFormList() { + return formMapper.selectList(); + } + + @Override + public List getFormList(Collection ids) { + return formMapper.selectBatchIds(ids); + } + + @Override + public PageResult getFormPage(BpmFormPageReqVO pageReqVO) { + return formMapper.selectPage(pageReqVO); + } + + + @Override + public BpmFormDO checkFormConfig(String configStr) { + BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(configStr, BpmModelMetaInfoRespDTO.class); + if (metaInfo == null || metaInfo.getFormType() == null) { + throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG); + } + // 校验表单存在 + if (Objects.equals(metaInfo.getFormType(), BpmModelFormTypeEnum.NORMAL.getType())) { + BpmFormDO form = getForm(metaInfo.getFormId()); + if (form == null) { + throw exception(FORM_NOT_EXISTS); + } + return form; + } + return null; + } + + /** + * 校验 Field,避免 field 重复 + * + * @param fields field 数组 + */ + private void checkFields(List fields) { + if (true) { // TODO 芋艿:兼容 Vue3 工作流:因为采用了新的表单设计器,所以暂时不校验 + return; + } + Map fieldMap = new HashMap<>(); // key 是 vModel,value 是 label + for (String field : fields) { + BpmFormFieldRespDTO fieldDTO = JsonUtils.parseObject(field, BpmFormFieldRespDTO.class); + Assert.notNull(fieldDTO); + String oldLabel = fieldMap.put(fieldDTO.getVModel(), fieldDTO.getLabel()); + // 如果不存在,则直接返回 + if (oldLabel == null) { + continue; + } + // 如果存在,则报错 + throw exception(ErrorCodeConstants.FORM_FIELD_REPEAT, oldLabel, fieldDTO.getLabel(), fieldDTO.getVModel()); + } + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmModelService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmModelService.java new file mode 100644 index 0000000..f50d657 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmModelService.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.model.*; +import org.flowable.bpmn.model.BpmnModel; + +import javax.validation.Valid; + +/** + * Flowable流程模型接口 + * + * @author yunlongn + */ +public interface BpmModelService { + /** + * 获得流程模型分页 + * + * @param pageVO 分页查询 + * @return 流程模型分页 + */ + PageResult getModelPage(BpmModelPageReqVO pageVO); + + /** + * 创建流程模型 + * + * @param modelVO 创建信息 + * @param bpmnXml BPMN XML + * @return 创建的流程模型的编号 + */ + String createModel(@Valid BpmModelCreateReqVO modelVO, String bpmnXml); + + /** + * 获得流程模块 + * + * @param id 编号 + * @return 流程模型 + */ + BpmModelRespVO getModel(String id); + + /** + * 修改流程模型 + * + * @param updateReqVO 更新信息 + */ + void updateModel(@Valid BpmModelUpdateReqVO updateReqVO); + + /** + * 将流程模型,部署成一个流程定义 + * + * @param id 编号 + */ + void deployModel(String id); + + /** + * 删除模型 + * + * @param id 编号 + */ + void deleteModel(String id); + + /** + * 修改模型的状态,实际更新的部署的流程定义的状态 + * + * @param id 编号 + * @param state 状态 + */ + void updateModelState(String id, Integer state); + + /** + * 获得流程模型编号对应的 BPMN Model + * + * @param id 流程模型编号 + * @return BPMN Model + */ + BpmnModel getBpmnModel(String id); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmModelServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmModelServiceImpl.java new file mode 100644 index 0000000..1a8aa2e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmModelServiceImpl.java @@ -0,0 +1,287 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.common.util.object.PageUtils; +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.model.*; +import com.yunxi.scm.module.bpm.convert.definition.BpmModelConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.module.bpm.enums.definition.BpmModelFormTypeEnum; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.common.engine.impl.util.io.BytesStreamSource; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.Model; +import org.flowable.engine.repository.ModelQuery; +import org.flowable.engine.repository.ProcessDefinition; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.ObjectUtils; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.*; + +/** + * Flowable流程模型实现 + * 主要进行 Flowable {@link Model} 的维护 + * + * @author yunlongn + * @author 芋道源码 + * @author jason + */ +@Service +@Validated +@Slf4j +public class BpmModelServiceImpl implements BpmModelService { + + @Resource + private RepositoryService repositoryService; + @Resource + private BpmProcessDefinitionService processDefinitionService; + @Resource + private BpmFormService bpmFormService; + @Resource + private BpmTaskAssignRuleService taskAssignRuleService; + + @Override + public PageResult getModelPage(BpmModelPageReqVO pageVO) { + ModelQuery modelQuery = repositoryService.createModelQuery(); + if (StrUtil.isNotBlank(pageVO.getKey())) { + modelQuery.modelKey(pageVO.getKey()); + } + if (StrUtil.isNotBlank(pageVO.getName())) { + modelQuery.modelNameLike("%" + pageVO.getName() + "%"); // 模糊匹配 + } + if (StrUtil.isNotBlank(pageVO.getCategory())) { + modelQuery.modelCategory(pageVO.getCategory()); + } + // 执行查询 + List models = modelQuery.orderByCreateTime().desc() + .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + + // 获得 Form Map + Set formIds = CollectionUtils.convertSet(models, model -> { + BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class); + return metaInfo != null ? metaInfo.getFormId() : null; + }); + Map formMap = bpmFormService.getFormMap(formIds); + + // 获得 Deployment Map + Set deploymentIds = new HashSet<>(); + models.forEach(model -> CollectionUtils.addIfNotNull(deploymentIds, model.getDeploymentId())); + Map deploymentMap = processDefinitionService.getDeploymentMap(deploymentIds); + // 获得 ProcessDefinition Map + List processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds); + Map processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId); + + // 拼接结果 + long modelCount = modelQuery.count(); + return new PageResult<>(BpmModelConvert.INSTANCE.convertList(models, formMap, deploymentMap, processDefinitionMap), modelCount); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public String createModel(@Valid BpmModelCreateReqVO createReqVO, String bpmnXml) { + checkKeyNCName(createReqVO.getKey()); + // 校验流程标识已经存在 + Model keyModel = getModelByKey(createReqVO.getKey()); + if (keyModel != null) { + throw exception(MODEL_KEY_EXISTS, createReqVO.getKey()); + } + + // 创建流程定义 + Model model = repositoryService.newModel(); + BpmModelConvert.INSTANCE.copy(model, createReqVO); + // 保存流程定义 + repositoryService.saveModel(model); + // 保存 BPMN XML + saveModelBpmnXml(model, bpmnXml); + return model.getId(); + } + + private Model getModelByKey(String key) { + return repositoryService.createModelQuery().modelKey(key).singleResult(); + } + + @Override + public BpmModelRespVO getModel(String id) { + Model model = repositoryService.getModel(id); + if (model == null) { + return null; + } + BpmModelRespVO modelRespVO = BpmModelConvert.INSTANCE.convert(model); + // 拼接 bpmn XML + byte[] bpmnBytes = repositoryService.getModelEditorSource(id); + modelRespVO.setBpmnXml(StrUtil.utf8Str(bpmnBytes)); + return modelRespVO; + } + + @Override + @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务 + public void updateModel(@Valid BpmModelUpdateReqVO updateReqVO) { + // 校验流程模型存在 + Model model = repositoryService.getModel(updateReqVO.getId()); + if (model == null) { + throw exception(MODEL_NOT_EXISTS); + } + + // 修改流程定义 + BpmModelConvert.INSTANCE.copy(model, updateReqVO); + // 更新模型 + repositoryService.saveModel(model); + // 更新 BPMN XML + saveModelBpmnXml(model, updateReqVO.getBpmnXml()); + } + + @Override + @Transactional(rollbackFor = Exception.class) // 因为进行多个操作,所以开启事务 + public void deployModel(String id) { + // 1.1 校验流程模型存在 + Model model = repositoryService.getModel(id); + if (ObjectUtils.isEmpty(model)) { + throw exception(MODEL_NOT_EXISTS); + } + // 1.2 校验流程图 + // TODO 芋艿:校验流程图的有效性;例如说,是否有开始的元素,是否有结束的元素; + byte[] bpmnBytes = repositoryService.getModelEditorSource(model.getId()); + if (bpmnBytes == null) { + throw exception(MODEL_NOT_EXISTS); + } + // 1.3 校验表单已配 + BpmFormDO form = checkFormConfig(model.getMetaInfo()); + // 1.4 校验任务分配规则已配置 + taskAssignRuleService.checkTaskAssignRuleAllConfig(id); + + // 1.5 校验模型是否发生修改。如果未修改,则不允许创建 + BpmProcessDefinitionCreateReqDTO definitionCreateReqDTO = BpmModelConvert.INSTANCE.convert2(model, form).setBpmnBytes(bpmnBytes); + if (processDefinitionService.isProcessDefinitionEquals(definitionCreateReqDTO)) { // 流程定义的信息相等 + ProcessDefinition oldProcessDefinition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId()); + if (oldProcessDefinition != null && taskAssignRuleService.isTaskAssignRulesEquals(model.getId(), oldProcessDefinition.getId())) { + throw exception(MODEL_DEPLOY_FAIL_TASK_INFO_EQUALS); + } + } + + // 2.1 创建流程定义 + String definitionId = processDefinitionService.createProcessDefinition(definitionCreateReqDTO); + + // 2.2 将老的流程定义进行挂起。也就是说,只有最新部署的流程定义,才可以发起任务。 + updateProcessDefinitionSuspended(model.getDeploymentId()); + + // 2.3 更新 model 的 deploymentId,进行关联 + ProcessDefinition definition = processDefinitionService.getProcessDefinition(definitionId); + model.setDeploymentId(definition.getDeploymentId()); + repositoryService.saveModel(model); + + // 2.4 复制任务分配规则 + taskAssignRuleService.copyTaskAssignRules(id, definition.getId()); + } + + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteModel(String id) { + // 校验流程模型存在 + Model model = repositoryService.getModel(id); + if (model == null) { + throw exception(MODEL_NOT_EXISTS); + } + // 执行删除 + repositoryService.deleteModel(id); + // 禁用流程定义 + updateProcessDefinitionSuspended(model.getDeploymentId()); + } + + @Override + public void updateModelState(String id, Integer state) { + // 校验流程模型存在 + Model model = repositoryService.getModel(id); + if (model == null) { + throw exception(MODEL_NOT_EXISTS); + } + // 校验流程定义存在 + ProcessDefinition definition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId()); + if (definition == null) { + throw exception(PROCESS_DEFINITION_NOT_EXISTS); + } + + // 更新状态 + processDefinitionService.updateProcessDefinitionState(definition.getId(), state); + } + + @Override + public BpmnModel getBpmnModel(String id) { + byte[] bpmnBytes = repositoryService.getModelEditorSource(id); + if (ArrayUtil.isEmpty(bpmnBytes)) { + return null; + } + BpmnXMLConverter converter = new BpmnXMLConverter(); + return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true); + } + + private void checkKeyNCName(String key) { + if (!ValidationUtils.isXmlNCName(key)) { + throw exception(MODEL_KEY_VALID); + } + } + + /** + * 校验流程表单已配置 + * + * @param metaInfoStr 流程模型 metaInfo 字段 + * @return 流程表单 + */ + private BpmFormDO checkFormConfig(String metaInfoStr) { + BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(metaInfoStr, BpmModelMetaInfoRespDTO.class); + if (metaInfo == null || metaInfo.getFormType() == null) { + throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG); + } + // 校验表单存在 + if (Objects.equals(metaInfo.getFormType(), BpmModelFormTypeEnum.NORMAL.getType())) { + BpmFormDO form = bpmFormService.getForm(metaInfo.getFormId()); + if (form == null) { + throw exception(FORM_NOT_EXISTS); + } + return form; + } + return null; + } + + private void saveModelBpmnXml(Model model, String bpmnXml) { + if (StrUtil.isEmpty(bpmnXml)) { + return; + } + repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml)); + } + + /** + * 挂起 deploymentId 对应的流程定义。 这里一个deploymentId 只关联一个流程定义 + * @param deploymentId 流程发布Id. + */ + private void updateProcessDefinitionSuspended(String deploymentId) { + if (StrUtil.isEmpty(deploymentId)) { + return; + } + ProcessDefinition oldDefinition = processDefinitionService.getProcessDefinitionByDeploymentId(deploymentId); + if (oldDefinition == null) { + return; + } + processDefinitionService.updateProcessDefinitionState(oldDefinition.getId(), SuspensionState.SUSPENDED.getStateCode()); + } + + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmProcessDefinitionService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmProcessDefinitionService.java new file mode 100644 index 0000000..6d9ed66 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmProcessDefinitionService.java @@ -0,0 +1,159 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionListReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageItemRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.ProcessDefinition; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.Set; +/** + * Flowable流程定义接口 + * + * @author yunlong.li + * @author ZJQ + * @author 芋道源码 + */ +public interface BpmProcessDefinitionService { + + /** + * 获得流程定义分页 + * + * @param pageReqVO 分页入参 + * @return 流程定义 Page + */ + PageResult getProcessDefinitionPage(BpmProcessDefinitionPageReqVO pageReqVO); + + /** + * 获得流程定义列表 + * + * @param listReqVO 列表入参 + * @return 流程定义列表 + */ + List getProcessDefinitionList(BpmProcessDefinitionListReqVO listReqVO); + + /** + * 创建流程定义 + * + * @param createReqDTO 创建信息 + * @return 流程编号 + */ + String createProcessDefinition(@Valid BpmProcessDefinitionCreateReqDTO createReqDTO); + + /** + * 更新流程定义状态 + * + * @param id 流程定义的编号 + * @param state 状态 + */ + void updateProcessDefinitionState(String id, Integer state); + + /** + * 获得流程定义对应的 BPMN XML + * + * @param id 流程定义编号 + * @return BPMN XML + */ + String getProcessDefinitionBpmnXML(String id); + + /** + * 获得需要创建的流程定义,是否和当前激活的流程定义相等 + * + * @param createReqDTO 创建信息 + * @return 是否相等 + */ + boolean isProcessDefinitionEquals(@Valid BpmProcessDefinitionCreateReqDTO createReqDTO); + + /** + * 获得编号对应的 BpmProcessDefinitionExtDO + * + * @param id 编号 + * @return 流程定义拓展 + */ + BpmProcessDefinitionExtDO getProcessDefinitionExt(String id); + + /** + * 获得编号对应的 ProcessDefinition + * + * @param id 编号 + * @return 流程定义 + */ + ProcessDefinition getProcessDefinition(String id); + + /** + * 获得编号对应的 ProcessDefinition + * + * 相比 {@link #getProcessDefinition(String)} 方法,category 的取值是正确 + * + * @param id 编号 + * @return 流程定义 + */ + ProcessDefinition getProcessDefinition2(String id); + + /** + * 获得 deploymentId 对应的 ProcessDefinition + * + * @param deploymentId 部署编号 + * @return 流程定义 + */ + ProcessDefinition getProcessDefinitionByDeploymentId(String deploymentId); + + /** + * 获得 deploymentIds 对应的 ProcessDefinition 数组 + * + * @param deploymentIds 部署编号的数组 + * @return 流程定义的数组 + */ + List getProcessDefinitionListByDeploymentIds(Set deploymentIds); + + /** + * 获得流程定义标识对应的激活的流程定义 + * + * @param key 流程定义的标识 + * @return 流程定义 + */ + ProcessDefinition getActiveProcessDefinition(String key); + + /** + * 获得 ids 对应的 Deployment Map + * + * @param ids 部署编号的数组 + * @return 流程部署 Map + */ + default Map getDeploymentMap(Set ids) { + return CollectionUtils.convertMap(getDeployments(ids), Deployment::getId); + } + + /** + * 获得 ids 对应的 Deployment 数组 + * + * @param ids 部署编号的数组 + * @return 流程部署的数组 + */ + List getDeployments(Set ids); + + /** + * 获得 id 对应的 Deployment + * + * @param id 部署编号 + * @return 流程部署 + */ + Deployment getDeployment(String id); + + /** + * 获得 Bpmn 模型 + * + * @param processDefinitionId 流程定义的编号 + * @return Bpmn 模型 + */ + BpmnModel getBpmnModel(String processDefinitionId); +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java new file mode 100644 index 0000000..05efefd --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java @@ -0,0 +1,286 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.object.PageUtils; +import com.yunxi.scm.framework.flowable.core.util.FlowableUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionListReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageItemRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import com.yunxi.scm.module.bpm.convert.definition.BpmProcessDefinitionConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; +import com.yunxi.scm.module.bpm.dal.mysql.definition.BpmProcessDefinitionExtMapper; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmProcessDefinitionCreateReqDTO; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.converter.BpmnXMLConverter; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.impl.db.SuspensionState; +import org.flowable.common.engine.impl.util.io.BytesStreamSource; +import org.flowable.engine.RepositoryService; +import org.flowable.engine.repository.Deployment; +import org.flowable.engine.repository.ProcessDefinition; +import org.flowable.engine.repository.ProcessDefinitionQuery; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.*; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_KEY_NOT_MATCH; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NAME_NOT_MATCH; +import static java.util.Collections.emptyList; + +/** + * 流程定义实现 + * 主要进行 Flowable {@link ProcessDefinition} 和 {@link Deployment} 的维护 + * + * @author yunlongn + * @author ZJQ + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionService { + + private static final String BPMN_FILE_SUFFIX = ".bpmn"; + + @Resource + private RepositoryService repositoryService; + + @Resource + private BpmProcessDefinitionExtMapper processDefinitionMapper; + + @Resource + private BpmFormService formService; + + @Override + public ProcessDefinition getProcessDefinition(String id) { + return repositoryService.getProcessDefinition(id); + } + + @Override + public ProcessDefinition getProcessDefinition2(String id) { + return repositoryService.createProcessDefinitionQuery().processDefinitionId(id).singleResult(); + } + + @Override + public ProcessDefinition getProcessDefinitionByDeploymentId(String deploymentId) { + if (StrUtil.isEmpty(deploymentId)) { + return null; + } + return repositoryService.createProcessDefinitionQuery().deploymentId(deploymentId).singleResult(); + } + + @Override + public List getProcessDefinitionListByDeploymentIds(Set deploymentIds) { + if (CollUtil.isEmpty(deploymentIds)) { + return emptyList(); + } + return repositoryService.createProcessDefinitionQuery().deploymentIds(deploymentIds).list(); + } + + @Override + public ProcessDefinition getActiveProcessDefinition(String key) { + return repositoryService.createProcessDefinitionQuery().processDefinitionKey(key).active().singleResult(); + } + + @Override + public List getDeployments(Set ids) { + if (CollUtil.isEmpty(ids)) { + return emptyList(); + } + List list = new ArrayList<>(ids.size()); + for (String id : ids) { + addIfNotNull(list, getDeployment(id)); + } + return list; + } + + @Override + public Deployment getDeployment(String id) { + if (StrUtil.isEmpty(id)) { + return null; + } + return repositoryService.createDeploymentQuery().deploymentId(id).singleResult(); + } + + @Override + public BpmnModel getBpmnModel(String processDefinitionId) { + return repositoryService.getBpmnModel(processDefinitionId); + } + + @Override + public String createProcessDefinition(@Valid BpmProcessDefinitionCreateReqDTO createReqDTO) { + // 创建 Deployment 部署 + Deployment deploy = repositoryService.createDeployment() + .key(createReqDTO.getKey()).name(createReqDTO.getName()).category(createReqDTO.getCategory()) + .addBytes(createReqDTO.getKey() + BPMN_FILE_SUFFIX, createReqDTO.getBpmnBytes()) + .deploy(); + + // 设置 ProcessDefinition 的 category 分类 + ProcessDefinition definition = repositoryService.createProcessDefinitionQuery() + .deploymentId(deploy.getId()).singleResult(); + repositoryService.setProcessDefinitionCategory(definition.getId(), createReqDTO.getCategory()); + // 注意 1,ProcessDefinition 的 key 和 name 是通过 BPMN 中的 的 id 和 name 决定 + // 注意 2,目前该项目的设计上,需要保证 Model、Deployment、ProcessDefinition 使用相同的 key,保证关联性。 + // 否则,会导致 ProcessDefinition 的分页无法查询到。 + if (!Objects.equals(definition.getKey(), createReqDTO.getKey())) { + throw exception(PROCESS_DEFINITION_KEY_NOT_MATCH, createReqDTO.getKey(), definition.getKey()); + } + if (!Objects.equals(definition.getName(), createReqDTO.getName())) { + throw exception(PROCESS_DEFINITION_NAME_NOT_MATCH, createReqDTO.getName(), definition.getName()); + } + + // 插入拓展表 + BpmProcessDefinitionExtDO definitionDO = BpmProcessDefinitionConvert.INSTANCE.convert2(createReqDTO) + .setProcessDefinitionId(definition.getId()); + processDefinitionMapper.insert(definitionDO); + return definition.getId(); + } + + @Override + public void updateProcessDefinitionState(String id, Integer state) { + // 激活 + if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), state)) { + repositoryService.activateProcessDefinitionById(id, false, null); + return; + } + // 挂起 + if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), state)) { + // suspendProcessInstances = false,进行中的任务,不进行挂起。 + // 原因:只要新的流程不允许发起即可,老流程继续可以执行。 + repositoryService.suspendProcessDefinitionById(id, false, null); + return; + } + log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state); + } + + @Override + public String getProcessDefinitionBpmnXML(String id) { + BpmnModel bpmnModel = repositoryService.getBpmnModel(id); + if (bpmnModel == null) { + return null; + } + BpmnXMLConverter converter = new BpmnXMLConverter(); + return StrUtil.utf8Str(converter.convertToXML(bpmnModel)); + } + + @Override + public boolean isProcessDefinitionEquals(@Valid BpmProcessDefinitionCreateReqDTO createReqDTO) { + // 校验 name、description 是否更新 + ProcessDefinition oldProcessDefinition = getActiveProcessDefinition(createReqDTO.getKey()); + if (oldProcessDefinition == null) { + return false; + } + BpmProcessDefinitionExtDO oldProcessDefinitionExt = getProcessDefinitionExt(oldProcessDefinition.getId()); + if (!StrUtil.equals(createReqDTO.getName(), oldProcessDefinition.getName()) + || !StrUtil.equals(createReqDTO.getDescription(), oldProcessDefinitionExt.getDescription()) + || !StrUtil.equals(createReqDTO.getCategory(), oldProcessDefinition.getCategory())) { + return false; + } + // 校验 form 信息是否更新 + if (!ObjectUtil.equal(createReqDTO.getFormType(), oldProcessDefinitionExt.getFormType()) + || !ObjectUtil.equal(createReqDTO.getFormId(), oldProcessDefinitionExt.getFormId()) + || !ObjectUtil.equal(createReqDTO.getFormConf(), oldProcessDefinitionExt.getFormConf()) + || !ObjectUtil.equal(createReqDTO.getFormFields(), oldProcessDefinitionExt.getFormFields()) + || !ObjectUtil.equal(createReqDTO.getFormCustomCreatePath(), oldProcessDefinitionExt.getFormCustomCreatePath()) + || !ObjectUtil.equal(createReqDTO.getFormCustomViewPath(), oldProcessDefinitionExt.getFormCustomViewPath())) { + return false; + } + // 校验 BPMN XML 信息 + BpmnModel newModel = buildBpmnModel(createReqDTO.getBpmnBytes()); + BpmnModel oldModel = getBpmnModel(oldProcessDefinition.getId()); + // 对比字节变化 + if (!FlowableUtils.equals(oldModel, newModel)) { + return false; + } + // 最终发现都一致,则返回 true + return true; + } + + /** + * 构建对应的 BPMN Model + * + * @param bpmnBytes 原始的 BPMN XML 字节数组 + * @return BPMN Model + */ + private BpmnModel buildBpmnModel(byte[] bpmnBytes) { + // 转换成 BpmnModel 对象 + BpmnXMLConverter converter = new BpmnXMLConverter(); + return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true); + } + + @Override + public BpmProcessDefinitionExtDO getProcessDefinitionExt(String id) { + return processDefinitionMapper.selectByProcessDefinitionId(id); + } + + @Override + public List getProcessDefinitionList(BpmProcessDefinitionListReqVO listReqVO) { + // 拼接查询条件 + ProcessDefinitionQuery definitionQuery = repositoryService.createProcessDefinitionQuery(); + if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), listReqVO.getSuspensionState())) { + definitionQuery.suspended(); + } else if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), listReqVO.getSuspensionState())) { + definitionQuery.active(); + } + // 执行查询 + List processDefinitions = definitionQuery.list(); + if (CollUtil.isEmpty(processDefinitions)) { + return Collections.emptyList(); + } + + // 获得 BpmProcessDefinitionDO Map + List processDefinitionDOs = processDefinitionMapper.selectListByProcessDefinitionIds( + convertList(processDefinitions, ProcessDefinition::getId)); + Map processDefinitionDOMap = convertMap(processDefinitionDOs, + BpmProcessDefinitionExtDO::getProcessDefinitionId); + // 执行查询,并返回 + return BpmProcessDefinitionConvert.INSTANCE.convertList3(processDefinitions, processDefinitionDOMap); + } + + @Override + public PageResult getProcessDefinitionPage(BpmProcessDefinitionPageReqVO pageVO) { + ProcessDefinitionQuery definitionQuery = repositoryService.createProcessDefinitionQuery(); + if (StrUtil.isNotBlank(pageVO.getKey())) { + definitionQuery.processDefinitionKey(pageVO.getKey()); + } + + // 执行查询 + List processDefinitions = definitionQuery.orderByProcessDefinitionVersion().desc() + .listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + + if (CollUtil.isEmpty(processDefinitions)) { + return new PageResult<>(emptyList(), definitionQuery.count()); + } + // 获得 Deployment Map + Set deploymentIds = new HashSet<>(); + processDefinitions.forEach(definition -> addIfNotNull(deploymentIds, definition.getDeploymentId())); + Map deploymentMap = getDeploymentMap(deploymentIds); + + // 获得 BpmProcessDefinitionDO Map + List processDefinitionDOs = processDefinitionMapper.selectListByProcessDefinitionIds( + convertList(processDefinitions, ProcessDefinition::getId)); + Map processDefinitionDOMap = convertMap(processDefinitionDOs, + BpmProcessDefinitionExtDO::getProcessDefinitionId); + + // 获得 Form Map + Set formIds = convertSet(processDefinitionDOs, BpmProcessDefinitionExtDO::getFormId); + Map formMap = formService.getFormMap(formIds); + + // 拼接结果 + long definitionCount = definitionQuery.count(); + return new PageResult<>(BpmProcessDefinitionConvert.INSTANCE.convertList(processDefinitions, deploymentMap, + processDefinitionDOMap, formMap), definitionCount); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleService.java new file mode 100644 index 0000000..5ad4403 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleService.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleUpdateReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.lang.Nullable; + +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +/** + * BPM 任务分配规则 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmTaskAssignRuleService { + + /** + * 获得流程定义的任务分配规则数组 + * + * @param processDefinitionId 流程定义的编号 + * @param taskDefinitionKey 流程任务定义的 Key。允许空 + * @return 任务规则数组 + */ + List getTaskAssignRuleListByProcessDefinitionId(String processDefinitionId, + @Nullable String taskDefinitionKey); + + /** + * 获得流程模型的任务规则数组 + * + * @param modelId 流程模型的编号 + * @return 任务规则数组 + */ + List getTaskAssignRuleListByModelId(String modelId); + + /** + * 获得流程定义的任务分配规则数组 + * + * @param modelId 流程模型的编号 + * @param processDefinitionId 流程定义的编号 + * @return 任务规则数组 + */ + List getTaskAssignRuleList(String modelId, String processDefinitionId); + + /** + * 创建任务分配规则 + * + * @param reqVO 创建信息 + * @return 规则编号 + */ + Long createTaskAssignRule(@Valid BpmTaskAssignRuleCreateReqVO reqVO); + + /** + * 更新任务分配规则 + * + * @param reqVO 创建信息 + */ + void updateTaskAssignRule(@Valid BpmTaskAssignRuleUpdateReqVO reqVO); + + /** + * 判断指定流程模型和流程定义的分配规则是否相等 + * + * @param modelId 流程模型编号 + * @param processDefinitionId 流程定义编号 + * @return 是否相等 + */ + boolean isTaskAssignRulesEquals(String modelId, String processDefinitionId); + + /** + * 将流程流程模型的任务分配规则,复制一份给流程定义 + * 目的:每次流程模型部署时,都会生成一个新的流程定义,此时考虑到每次部署的流程不可变性,所以需要复制一份给该流程定义 + * + * @param fromModelId 流程模型编号 + * @param toProcessDefinitionId 流程定义编号 + */ + void copyTaskAssignRules(String fromModelId, String toProcessDefinitionId); + + /** + * 校验流程模型的任务分配规则全部都配置了 + * 目的:如果有规则未配置,会导致流程任务找不到负责人,进而流程无法进行下去! + * + * @param id 流程模型编号 + */ + void checkTaskAssignRuleAllConfig(String id); + + /** + * 计算当前执行任务的处理人 + * + * @param execution 执行任务 + * @return 处理人的编号数组 + */ + Set calculateTaskCandidateUsers(DelegateExecution execution); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java new file mode 100644 index 0000000..9946273 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleServiceImpl.java @@ -0,0 +1,344 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.framework.flowable.core.util.FlowableUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleUpdateReqVO; +import com.yunxi.scm.module.bpm.convert.definition.BpmTaskAssignRuleConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import com.yunxi.scm.module.bpm.dal.mysql.definition.BpmTaskAssignRuleMapper; +import com.yunxi.scm.module.bpm.enums.DictTypeConstants; +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum; +import com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript; +import com.yunxi.scm.module.system.api.dept.DeptApi; +import com.yunxi.scm.module.system.api.dept.PostApi; +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.api.dict.DictDataApi; +import com.yunxi.scm.module.system.api.permission.PermissionApi; +import com.yunxi.scm.module.system.api.permission.RoleApi; +import com.yunxi.scm.module.system.api.user.AdminUserApi; +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.UserTask; +import org.flowable.common.engine.api.FlowableException; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.*; + +import static cn.hutool.core.text.CharSequenceUtil.format; +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.*; + +/** + * BPM 任务分配规则 Service 实现类 + */ +@Service +@Validated +@Slf4j +public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService { + + @Resource + private BpmTaskAssignRuleMapper taskRuleMapper; + @Resource + @Lazy // 解决循环依赖 + private BpmModelService modelService; + @Resource + @Lazy // 解决循环依赖 + private BpmProcessDefinitionService processDefinitionService; + @Resource + private BpmUserGroupService userGroupService; + @Resource + private RoleApi roleApi; + @Resource + private DeptApi deptApi; + @Resource + private PostApi postApi; + @Resource + private AdminUserApi adminUserApi; + @Resource + private DictDataApi dictDataApi; + @Resource + private PermissionApi permissionApi; + /** + * 任务分配脚本 + */ + private Map scriptMap = Collections.emptyMap(); + + @Resource + public void setScripts(List scripts) { + this.scriptMap = convertMap(scripts, script -> script.getEnum().getId()); + } + + @Override + public List getTaskAssignRuleListByProcessDefinitionId(String processDefinitionId, + String taskDefinitionKey) { + return taskRuleMapper.selectListByProcessDefinitionId(processDefinitionId, taskDefinitionKey); + } + + @Override + public List getTaskAssignRuleListByModelId(String modelId) { + return taskRuleMapper.selectListByModelId(modelId); + } + + @Override + public List getTaskAssignRuleList(String modelId, String processDefinitionId) { + // 获得规则 + List rules = Collections.emptyList(); + BpmnModel model = null; + if (StrUtil.isNotEmpty(modelId)) { + rules = getTaskAssignRuleListByModelId(modelId); + model = modelService.getBpmnModel(modelId); + } else if (StrUtil.isNotEmpty(processDefinitionId)) { + rules = getTaskAssignRuleListByProcessDefinitionId(processDefinitionId, null); + model = processDefinitionService.getBpmnModel(processDefinitionId); + } + if (model == null) { + return Collections.emptyList(); + } + // 获得用户任务,只有用户任务才可以设置分配规则 + List userTasks = FlowableUtils.getBpmnModelElements(model, UserTask.class); + if (CollUtil.isEmpty(userTasks)) { + return Collections.emptyList(); + } + // 转换数据 + return BpmTaskAssignRuleConvert.INSTANCE.convertList(userTasks, rules); + } + + @Override + public Long createTaskAssignRule(@Valid BpmTaskAssignRuleCreateReqVO reqVO) { + // 校验参数 + validTaskAssignRuleOptions(reqVO.getType(), reqVO.getOptions()); + // 校验是否已经配置 + BpmTaskAssignRuleDO existRule = + taskRuleMapper.selectListByModelIdAndTaskDefinitionKey(reqVO.getModelId(), reqVO.getTaskDefinitionKey()); + if (existRule != null) { + throw exception(TASK_ASSIGN_RULE_EXISTS, reqVO.getModelId(), reqVO.getTaskDefinitionKey()); + } + + // 存储 + BpmTaskAssignRuleDO rule = BpmTaskAssignRuleConvert.INSTANCE.convert(reqVO) + .setProcessDefinitionId(BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL); // 只有流程模型,才允许新建 + taskRuleMapper.insert(rule); + return rule.getId(); + } + + @Override + public void updateTaskAssignRule(@Valid BpmTaskAssignRuleUpdateReqVO reqVO) { + // 校验参数 + validTaskAssignRuleOptions(reqVO.getType(), reqVO.getOptions()); + // 校验是否存在 + BpmTaskAssignRuleDO existRule = taskRuleMapper.selectById(reqVO.getId()); + if (existRule == null) { + throw exception(TASK_ASSIGN_RULE_NOT_EXISTS); + } + // 只允许修改流程模型的规则 + if (!Objects.equals(BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL, existRule.getProcessDefinitionId())) { + throw exception(TASK_UPDATE_FAIL_NOT_MODEL); + } + + // 执行更新 + taskRuleMapper.updateById(BpmTaskAssignRuleConvert.INSTANCE.convert(reqVO)); + } + + @Override + public boolean isTaskAssignRulesEquals(String modelId, String processDefinitionId) { + // 调用 VO 接口的原因是,过滤掉流程模型不需要的规则,保持和 copyTaskAssignRules 方法的一致性 + List modelRules = getTaskAssignRuleList(modelId, null); + List processInstanceRules = getTaskAssignRuleList(null, processDefinitionId); + if (modelRules.size() != processInstanceRules.size()) { + return false; + } + + // 遍历,匹配对应的规则 + Map processInstanceRuleMap = + CollectionUtils.convertMap(processInstanceRules, BpmTaskAssignRuleRespVO::getTaskDefinitionKey); + for (BpmTaskAssignRuleRespVO modelRule : modelRules) { + BpmTaskAssignRuleRespVO processInstanceRule = processInstanceRuleMap.get(modelRule.getTaskDefinitionKey()); + if (processInstanceRule == null) { + return false; + } + if (!ObjectUtil.equals(modelRule.getType(), processInstanceRule.getType()) || !ObjectUtil.equal( + modelRule.getOptions(), processInstanceRule.getOptions())) { + return false; + } + } + return true; + } + + @Override + public void copyTaskAssignRules(String fromModelId, String toProcessDefinitionId) { + List rules = getTaskAssignRuleList(fromModelId, null); + if (CollUtil.isEmpty(rules)) { + return; + } + // 开始复制 + List newRules = BpmTaskAssignRuleConvert.INSTANCE.convertList2(rules); + newRules.forEach(rule -> rule.setProcessDefinitionId(toProcessDefinitionId).setId(null).setCreateTime(null) + .setUpdateTime(null)); + taskRuleMapper.insertBatch(newRules); + } + + @Override + public void checkTaskAssignRuleAllConfig(String id) { + // 一个用户任务都没配置,所以无需配置规则 + List taskAssignRules = getTaskAssignRuleList(id, null); + if (CollUtil.isEmpty(taskAssignRules)) { + return; + } + // 校验未配置规则的任务 + taskAssignRules.forEach(rule -> { + if (CollUtil.isEmpty(rule.getOptions())) { + throw exception(MODEL_DEPLOY_FAIL_TASK_ASSIGN_RULE_NOT_CONFIG, rule.getTaskDefinitionName()); + } + }); + } + + private void validTaskAssignRuleOptions(Integer type, Set options) { + if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.ROLE.getType())) { + roleApi.validRoleList(options); + } else if (ObjectUtils.equalsAny(type, BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType(), + BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType())) { + deptApi.validateDeptList(options); + } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.POST.getType())) { + postApi.validPostList(options); + } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.USER.getType())) { + adminUserApi.validateUserList(options); + } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.USER_GROUP.getType())) { + userGroupService.validUserGroups(options); + } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.SCRIPT.getType())) { + dictDataApi.validateDictDataList(DictTypeConstants.TASK_ASSIGN_SCRIPT, + CollectionUtils.convertSet(options, String::valueOf)); + } else { + throw new IllegalArgumentException(format("未知的规则类型({})", type)); + } + } + + @Override + @DataPermission(enable = false) // 忽略数据权限,不然分配会存在问题 + public Set calculateTaskCandidateUsers(DelegateExecution execution) { + BpmTaskAssignRuleDO rule = getTaskRule(execution); + return calculateTaskCandidateUsers(execution, rule); + } + + @VisibleForTesting + BpmTaskAssignRuleDO getTaskRule(DelegateExecution execution) { + List taskRules = getTaskAssignRuleListByProcessDefinitionId( + execution.getProcessDefinitionId(), execution.getCurrentActivityId()); + if (CollUtil.isEmpty(taskRules)) { + throw new FlowableException(format("流程任务({}/{}/{}) 找不到符合的任务规则", + execution.getId(), execution.getProcessDefinitionId(), execution.getCurrentActivityId())); + } + if (taskRules.size() > 1) { + throw new FlowableException(format("流程任务({}/{}/{}) 找到过多任务规则({})", + execution.getId(), execution.getProcessDefinitionId(), execution.getCurrentActivityId())); + } + return taskRules.get(0); + } + + @VisibleForTesting + Set calculateTaskCandidateUsers(DelegateExecution execution, BpmTaskAssignRuleDO rule) { + Set assigneeUserIds = null; + if (Objects.equals(BpmTaskAssignRuleTypeEnum.ROLE.getType(), rule.getType())) { + assigneeUserIds = calculateTaskCandidateUsersByRole(rule); + } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType(), rule.getType())) { + assigneeUserIds = calculateTaskCandidateUsersByDeptMember(rule); + } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType(), rule.getType())) { + assigneeUserIds = calculateTaskCandidateUsersByDeptLeader(rule); + } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.POST.getType(), rule.getType())) { + assigneeUserIds = calculateTaskCandidateUsersByPost(rule); + } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.USER.getType(), rule.getType())) { + assigneeUserIds = calculateTaskCandidateUsersByUser(rule); + } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.USER_GROUP.getType(), rule.getType())) { + assigneeUserIds = calculateTaskCandidateUsersByUserGroup(rule); + } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.SCRIPT.getType(), rule.getType())) { + assigneeUserIds = calculateTaskCandidateUsersByScript(execution, rule); + } + + // 移除被禁用的用户 + removeDisableUsers(assigneeUserIds); + // 如果候选人为空,抛出异常 + if (CollUtil.isEmpty(assigneeUserIds)) { + log.error("[calculateTaskCandidateUsers][流程任务({}/{}/{}) 任务规则({}) 找不到候选人]", execution.getId(), + execution.getProcessDefinitionId(), execution.getCurrentActivityId(), toJsonString(rule)); + throw exception(TASK_CREATE_FAIL_NO_CANDIDATE_USER); + } + return assigneeUserIds; + } + + private Set calculateTaskCandidateUsersByRole(BpmTaskAssignRuleDO rule) { + return permissionApi.getUserRoleIdListByRoleIds(rule.getOptions()); + } + + private Set calculateTaskCandidateUsersByDeptMember(BpmTaskAssignRuleDO rule) { + List users = adminUserApi.getUserListByDeptIds(rule.getOptions()); + return convertSet(users, AdminUserRespDTO::getId); + } + + private Set calculateTaskCandidateUsersByDeptLeader(BpmTaskAssignRuleDO rule) { + List depts = deptApi.getDeptList(rule.getOptions()); + return convertSet(depts, DeptRespDTO::getLeaderUserId); + } + + private Set calculateTaskCandidateUsersByPost(BpmTaskAssignRuleDO rule) { + List users = adminUserApi.getUserListByPostIds(rule.getOptions()); + return convertSet(users, AdminUserRespDTO::getId); + } + + private Set calculateTaskCandidateUsersByUser(BpmTaskAssignRuleDO rule) { + return rule.getOptions(); + } + + private Set calculateTaskCandidateUsersByUserGroup(BpmTaskAssignRuleDO rule) { + List userGroups = userGroupService.getUserGroupList(rule.getOptions()); + Set userIds = new HashSet<>(); + userGroups.forEach(group -> userIds.addAll(group.getMemberUserIds())); + return userIds; + } + + private Set calculateTaskCandidateUsersByScript(DelegateExecution execution, BpmTaskAssignRuleDO rule) { + // 获得对应的脚本 + List scripts = new ArrayList<>(rule.getOptions().size()); + rule.getOptions().forEach(id -> { + BpmTaskAssignScript script = scriptMap.get(id); + if (script == null) { + throw exception(TASK_ASSIGN_SCRIPT_NOT_EXISTS, id); + } + scripts.add(script); + }); + // 逐个计算任务 + Set userIds = new HashSet<>(); + scripts.forEach(script -> CollUtil.addAll(userIds, script.calculateTaskCandidateUsers(execution))); + return userIds; + } + + @VisibleForTesting + void removeDisableUsers(Set assigneeUserIds) { + if (CollUtil.isEmpty(assigneeUserIds)) { + return; + } + Map userMap = adminUserApi.getUserMap(assigneeUserIds); + assigneeUserIds.removeIf(id -> { + AdminUserRespDTO user = userMap.get(id); + return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus()); + }); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupService.java new file mode 100644 index 0000000..07515c2 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupService.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import java.util.*; +import javax.validation.*; + +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupUpdateReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import com.yunxi.scm.framework.common.pojo.PageResult; + +/** + * 用户组 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmUserGroupService { + + /** + * 创建用户组 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createUserGroup(@Valid BpmUserGroupCreateReqVO createReqVO); + + /** + * 更新用户组 + * + * @param updateReqVO 更新信息 + */ + void updateUserGroup(@Valid BpmUserGroupUpdateReqVO updateReqVO); + + /** + * 删除用户组 + * + * @param id 编号 + */ + void deleteUserGroup(Long id); + + /** + * 获得用户组 + * + * @param id 编号 + * @return 用户组 + */ + BpmUserGroupDO getUserGroup(Long id); + + /** + * 获得用户组列表 + * + * @param ids 编号 + * @return 用户组列表 + */ + List getUserGroupList(Collection ids); + + /** + * 获得指定状态的用户组列表 + * + * @param status 状态 + * @return 用户组列表 + */ + List getUserGroupListByStatus(Integer status); + + /** + * 获得用户组分页 + * + * @param pageReqVO 分页查询 + * @return 用户组分页 + */ + PageResult getUserGroupPage(BpmUserGroupPageReqVO pageReqVO); + + /** + * 校验用户组们是否有效。如下情况,视为无效: + * 1. 用户组编号不存在 + * 2. 用户组被禁用 + * + * @param ids 用户组编号数组 + */ + void validUserGroups(Set ids); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupServiceImpl.java new file mode 100644 index 0000000..4789e30 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupServiceImpl.java @@ -0,0 +1,111 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupUpdateReqVO; +import com.yunxi.scm.module.bpm.convert.definition.BpmUserGroupConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import com.yunxi.scm.module.bpm.dal.mysql.definition.BpmUserGroupMapper; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.*; + +/** + * 用户组 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class BpmUserGroupServiceImpl implements BpmUserGroupService { + + @Resource + private BpmUserGroupMapper userGroupMapper; + + @Override + public Long createUserGroup(BpmUserGroupCreateReqVO createReqVO) { + // 插入 + BpmUserGroupDO userGroup = BpmUserGroupConvert.INSTANCE.convert(createReqVO); + userGroupMapper.insert(userGroup); + // 返回 + return userGroup.getId(); + } + + @Override + public void updateUserGroup(BpmUserGroupUpdateReqVO updateReqVO) { + // 校验存在 + this.validateUserGroupExists(updateReqVO.getId()); + // 更新 + BpmUserGroupDO updateObj = BpmUserGroupConvert.INSTANCE.convert(updateReqVO); + userGroupMapper.updateById(updateObj); + } + + @Override + public void deleteUserGroup(Long id) { + // 校验存在 + this.validateUserGroupExists(id); + // 删除 + userGroupMapper.deleteById(id); + } + + private void validateUserGroupExists(Long id) { + if (userGroupMapper.selectById(id) == null) { + throw ServiceExceptionUtil.exception(USER_GROUP_NOT_EXISTS); + } + } + + @Override + public BpmUserGroupDO getUserGroup(Long id) { + return userGroupMapper.selectById(id); + } + + @Override + public List getUserGroupList(Collection ids) { + return userGroupMapper.selectBatchIds(ids); + } + + + @Override + public List getUserGroupListByStatus(Integer status) { + return userGroupMapper.selectListByStatus(status); + } + + @Override + public PageResult getUserGroupPage(BpmUserGroupPageReqVO pageReqVO) { + return userGroupMapper.selectPage(pageReqVO); + } + + @Override + public void validUserGroups(Set ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得用户组信息 + List userGroups = userGroupMapper.selectBatchIds(ids); + Map userGroupMap = CollectionUtils.convertMap(userGroups, BpmUserGroupDO::getId); + // 校验 + ids.forEach(id -> { + BpmUserGroupDO userGroup = userGroupMap.get(id); + if (userGroup == null) { + throw ServiceExceptionUtil.exception(USER_GROUP_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(userGroup.getStatus())) { + throw exception(USER_GROUP_IS_DISABLE, userGroup.getName()); + } + }); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmFormFieldRespDTO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmFormFieldRespDTO.java new file mode 100644 index 0000000..38e9563 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmFormFieldRespDTO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.bpm.service.definition.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * Bpm 表单的 Field 表单项 Response DTO + * 字段的定义,可见 https://github.com/JakHuang/form-generator/issues/46 文档 + * + * @author 芋道源码 + */ +@Data +public class BpmFormFieldRespDTO { + + /** + * 表单标题 + */ + private String label; + /** + * 表单字段的属性名,可自定义 + */ + @JsonProperty(value = "vModel") + private String vModel; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmModelMetaInfoRespDTO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmModelMetaInfoRespDTO.java new file mode 100644 index 0000000..94aa2e2 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmModelMetaInfoRespDTO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.bpm.service.definition.dto; + +import com.yunxi.scm.module.bpm.enums.definition.BpmModelFormTypeEnum; +import lombok.Data; + +/** + * BPM 流程 MetaInfo Response DTO + * 主要用于 { Model#setMetaInfo(String)} 的存储 + * + * @author 芋道源码 + */ +@Data +public class BpmModelMetaInfoRespDTO { + + /** + * 流程描述 + */ + private String description; + /** + * 表单类型 + */ + private Integer formType; + /** + * 表单编号 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + */ + private Long formId; + /** + * 自定义表单的提交路径,使用 Vue 的路由地址 + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomCreatePath; + /** + * 自定义表单的查看路径,使用 Vue 的路由地址 + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomViewPath; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmProcessDefinitionCreateReqDTO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmProcessDefinitionCreateReqDTO.java new file mode 100644 index 0000000..8445b49 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/definition/dto/BpmProcessDefinitionCreateReqDTO.java @@ -0,0 +1,104 @@ +package com.yunxi.scm.module.bpm.service.definition.dto; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.bpm.enums.definition.BpmModelFormTypeEnum; +import lombok.Data; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Objects; + +/** + * 流程定义创建 Request DTO + */ +@Data +public class BpmProcessDefinitionCreateReqDTO { + + // ========== 模型相关 ========== + + /** + * 流程模型的编号 + */ + @NotEmpty(message = "流程模型编号不能为空") + private String modelId; + /** + * 流程标识 + */ + @NotEmpty(message = "流程标识不能为空") + private String key; + /** + * 流程名称 + */ + @NotEmpty(message = "流程名称不能为空") + private String name; + /** + * 流程描述 + */ + private String description; + /** + * 流程分类 + * 参见 bpm_model_category 数据字典 + */ + @NotEmpty(message = "流程分类不能为空") + private String category; + /** + * BPMN XML + */ + @NotEmpty(message = "BPMN XML 不能为空") + private byte[] bpmnBytes; + + // ========== 表单相关 ========== + + /** + * 表单类型 + */ + @NotNull(message = "表单类型不能为空") + private Integer formType; + /** + * 动态表单编号 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + */ + private Long formId; + /** + * 表单的配置 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + */ + private String formConf; + /** + * 表单项的数组 + * 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL} 时 + */ + private List formFields; + /** + * 自定义表单的提交路径,使用 Vue 的路由地址 + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomCreatePath; + /** + * 自定义表单的查看路径,使用 Vue 的路由地址 + * 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时 + */ + private String formCustomViewPath; + + @AssertTrue(message = "流程表单信息不全") + public boolean isNormalFormTypeValid() { + // 如果非业务表单,则直接通过 + if (!Objects.equals(formType, BpmModelFormTypeEnum.NORMAL.getType())) { + return true; + } + return formId != null && StrUtil.isNotEmpty(formConf) && CollUtil.isNotEmpty(formFields); + } + + @AssertTrue(message = "业务表单信息不全") + public boolean isNormalCustomTypeValid() { + // 如果非业务表单,则直接通过 + if (!Objects.equals(formType, BpmModelFormTypeEnum.CUSTOM.getType())) { + return true; + } + return StrUtil.isNotEmpty(formCustomCreatePath) && StrUtil.isNotEmpty(formCustomViewPath); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/BpmMessageService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/BpmMessageService.java new file mode 100644 index 0000000..a997928 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/BpmMessageService.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.bpm.service.message; + +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; + +import javax.validation.Valid; + +/** + * BPM 消息 Service 接口 + * + * TODO 芋艿:未来支持消息的可配置;不同的流程,在什么场景下,需要发送什么消息,消息的内容是什么; + * + * @author 芋道源码 + */ +public interface BpmMessageService { + + /** + * 发送流程实例被通过的消息 + * + * @param reqDTO 发送信息 + */ + void sendMessageWhenProcessInstanceApprove(@Valid BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO); + + /** + * 发送流程实例被不通过的消息 + * + * @param reqDTO 发送信息 + */ + void sendMessageWhenProcessInstanceReject(@Valid BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO); + + /** + * 发送任务被分配的消息 + * + * @param reqDTO 发送信息 + */ + void sendMessageWhenTaskAssigned(@Valid BpmMessageSendWhenTaskCreatedReqDTO reqDTO); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/BpmMessageServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/BpmMessageServiceImpl.java new file mode 100644 index 0000000..b5e49b1 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/BpmMessageServiceImpl.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.module.bpm.service.message; + +import com.yunxi.scm.framework.web.config.WebProperties; +import com.yunxi.scm.module.bpm.convert.message.BpmMessageConvert; +import com.yunxi.scm.module.bpm.enums.message.BpmMessageEnum; +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO; +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO; +import com.yunxi.scm.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; +import com.yunxi.scm.module.system.api.sms.SmsSendApi; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +/** + * BPM 消息 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class BpmMessageServiceImpl implements BpmMessageService { + + @Resource + private SmsSendApi smsSendApi; + + @Resource + private WebProperties webProperties; + + @Override + public void sendMessageWhenProcessInstanceApprove(BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO) { + Map templateParams = new HashMap<>(); + templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); + templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); + smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(), + BpmMessageEnum.PROCESS_INSTANCE_APPROVE.getSmsTemplateCode(), templateParams)); + } + + @Override + public void sendMessageWhenProcessInstanceReject(BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO) { + Map templateParams = new HashMap<>(); + templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); + templateParams.put("reason", reqDTO.getReason()); + templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); + smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getStartUserId(), + BpmMessageEnum.PROCESS_INSTANCE_REJECT.getSmsTemplateCode(), templateParams)); + } + + @Override + public void sendMessageWhenTaskAssigned(BpmMessageSendWhenTaskCreatedReqDTO reqDTO) { + Map templateParams = new HashMap<>(); + templateParams.put("processInstanceName", reqDTO.getProcessInstanceName()); + templateParams.put("taskName", reqDTO.getTaskName()); + templateParams.put("startUserNickname", reqDTO.getStartUserNickname()); + templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId())); + smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(), + BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams)); + } + + private String getProcessInstanceDetailUrl(String taskId) { + return webProperties.getAdminUi().getUrl() + "/bpm/process-instance/detail?id=" + taskId; + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceApproveReqDTO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceApproveReqDTO.java new file mode 100644 index 0000000..c2aacb5 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceApproveReqDTO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.bpm.service.message.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * BPM 发送流程实例被通过 Request DTO + */ +@Data +public class BpmMessageSendWhenProcessInstanceApproveReqDTO { + + /** + * 流程实例的编号 + */ + @NotEmpty(message = "流程实例的编号不能为空") + private String processInstanceId; + /** + * 流程实例的名字 + */ + @NotEmpty(message = "流程实例的名字不能为空") + private String processInstanceName; + @NotNull(message = "发起人的用户编号") + private Long startUserId; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceRejectReqDTO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceRejectReqDTO.java new file mode 100644 index 0000000..5780666 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenProcessInstanceRejectReqDTO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.bpm.service.message.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * BPM 发送流程实例被不通过 Request DTO + */ +@Data +public class BpmMessageSendWhenProcessInstanceRejectReqDTO { + + /** + * 流程实例的编号 + */ + @NotEmpty(message = "流程实例的编号不能为空") + private String processInstanceId; + /** + * 流程实例的名字 + */ + @NotEmpty(message = "流程实例的名字不能为空") + private String processInstanceName; + @NotNull(message = "发起人的用户编号") + private Long startUserId; + + /** + * 不通过理由 + */ + @NotEmpty(message = "不通过理由不能为空") + private String reason; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenTaskCreatedReqDTO.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenTaskCreatedReqDTO.java new file mode 100644 index 0000000..3fe7f29 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/message/dto/BpmMessageSendWhenTaskCreatedReqDTO.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.bpm.service.message.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * BPM 发送任务被分配 Request DTO + */ +@Data +public class BpmMessageSendWhenTaskCreatedReqDTO { + + /** + * 流程实例的编号 + */ + @NotEmpty(message = "流程实例的编号不能为空") + private String processInstanceId; + /** + * 流程实例的名字 + */ + @NotEmpty(message = "流程实例的名字不能为空") + private String processInstanceName; + @NotNull(message = "发起人的用户编号") + private Long startUserId; + @NotEmpty(message = "发起人的昵称") + private String startUserNickname; + + /** + * 流程任务的编号 + */ + @NotEmpty(message = "流程任务的编号不能为空") + private String taskId; + /** + * 流程任务的名字 + */ + @NotEmpty(message = "流程任务的名字不能为空") + private String taskName; + + /** + * 审批人的用户编号 + */ + @NotNull(message = "审批人的用户编号不能为空") + private Long assigneeUserId; + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/BpmOALeaveService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/BpmOALeaveService.java new file mode 100644 index 0000000..1f442a8 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/BpmOALeaveService.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.bpm.service.oa; + + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeavePageReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.oa.BpmOALeaveDO; + +import javax.validation.Valid; + +/** + * 请假申请 Service 接口 + * + * @author jason + * @author 芋道源码 + */ +public interface BpmOALeaveService { + + /** + * 创建请假申请 + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createLeave(Long userId, @Valid BpmOALeaveCreateReqVO createReqVO); + + /** + * 更新请假申请的状态 + * + * @param id 编号 + * @param result 结果 + */ + void updateLeaveResult(Long id, Integer result); + + /** + * 获得请假申请 + * + * @param id 编号 + * @return 请假申请 + */ + BpmOALeaveDO getLeave(Long id); + + /** + * 获得请假申请分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页查询 + * @return 请假申请分页 + */ + PageResult getLeavePage(Long userId, BpmOALeavePageReqVO pageReqVO); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/BpmOALeaveServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/BpmOALeaveServiceImpl.java new file mode 100644 index 0000000..a451505 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/BpmOALeaveServiceImpl.java @@ -0,0 +1,88 @@ +package com.yunxi.scm.module.bpm.service.oa; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.bpm.api.task.BpmProcessInstanceApi; +import com.yunxi.scm.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.oa.vo.BpmOALeavePageReqVO; +import com.yunxi.scm.module.bpm.convert.oa.BpmOALeaveConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.oa.BpmOALeaveDO; +import com.yunxi.scm.module.bpm.dal.mysql.oa.BpmOALeaveMapper; +import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceResultEnum; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.OA_LEAVE_NOT_EXISTS; + +/** + * OA 请假申请 Service 实现类 + * + * @author jason + * @author 芋道源码 + */ +@Service +@Validated +public class BpmOALeaveServiceImpl implements BpmOALeaveService { + + /** + * OA 请假对应的流程定义 KEY + */ + public static final String PROCESS_KEY = "oa_leave"; + + @Resource + private BpmOALeaveMapper leaveMapper; + + @Resource + private BpmProcessInstanceApi processInstanceApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createLeave(Long userId, BpmOALeaveCreateReqVO createReqVO) { + // 插入 OA 请假单 + long day = LocalDateTimeUtil.between(createReqVO.getStartTime(), createReqVO.getEndTime()).toDays(); + BpmOALeaveDO leave = BpmOALeaveConvert.INSTANCE.convert(createReqVO).setUserId(userId).setDay(day) + .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult()); + leaveMapper.insert(leave); + + // 发起 BPM 流程 + Map processInstanceVariables = new HashMap<>(); + processInstanceVariables.put("day", day); + String processInstanceId = processInstanceApi.createProcessInstance(userId, + new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(PROCESS_KEY) + .setVariables(processInstanceVariables).setBusinessKey(String.valueOf(leave.getId()))); + + // 将工作流的编号,更新到 OA 请假单中 + leaveMapper.updateById(new BpmOALeaveDO().setId(leave.getId()).setProcessInstanceId(processInstanceId)); + return leave.getId(); + } + + @Override + public void updateLeaveResult(Long id, Integer result) { + validateLeaveExists(id); + leaveMapper.updateById(new BpmOALeaveDO().setId(id).setResult(result)); + } + + private void validateLeaveExists(Long id) { + if (leaveMapper.selectById(id) == null) { + throw exception(OA_LEAVE_NOT_EXISTS); + } + } + + @Override + public BpmOALeaveDO getLeave(Long id) { + return leaveMapper.selectById(id); + } + + @Override + public PageResult getLeavePage(Long userId, BpmOALeavePageReqVO pageReqVO) { + return leaveMapper.selectPage(userId, pageReqVO); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/listener/BpmOALeaveResultListener.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/listener/BpmOALeaveResultListener.java new file mode 100644 index 0000000..31ef65e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/oa/listener/BpmOALeaveResultListener.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.bpm.service.oa.listener; + +import com.yunxi.scm.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEvent; +import com.yunxi.scm.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventListener; +import com.yunxi.scm.module.bpm.service.oa.BpmOALeaveService; +import com.yunxi.scm.module.bpm.service.oa.BpmOALeaveServiceImpl; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * OA 请假单的结果的监听器实现类 + * + * @author 芋道源码 + */ +@Component +public class BpmOALeaveResultListener extends BpmProcessInstanceResultEventListener { + + @Resource + private BpmOALeaveService leaveService; + + @Override + protected String getProcessDefinitionKey() { + return BpmOALeaveServiceImpl.PROCESS_KEY; + } + + @Override + protected void onEvent(BpmProcessInstanceResultEvent event) { + leaveService.updateLeaveResult(Long.parseLong(event.getBusinessKey()), event.getResult()); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmActivityService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmActivityService.java new file mode 100644 index 0000000..55c794a --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmActivityService.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.bpm.service.task; + +import com.yunxi.scm.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO; +import org.flowable.engine.history.HistoricActivityInstance; + +import java.util.List; + +/** + * BPM 活动实例 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmActivityService { + + /** + * 获得指定流程实例的活动实例列表 + * + * @param processInstanceId 流程实例的编号 + * @return 活动实例列表 + */ + List getActivityListByProcessInstanceId(String processInstanceId); + + /** + * 获得执行编号对应的活动实例 + * + * @param executionId 执行编号 + * @return 活动实例 + */ + List getHistoricActivityListByExecutionId(String executionId); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmActivityServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmActivityServiceImpl.java new file mode 100644 index 0000000..409823d --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmActivityServiceImpl.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.bpm.service.task; + +import com.yunxi.scm.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO; +import com.yunxi.scm.module.bpm.convert.task.BpmActivityConvert; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.HistoryService; +import org.flowable.engine.history.HistoricActivityInstance; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + + +/** + * BPM 活动实例 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +@Validated +public class BpmActivityServiceImpl implements BpmActivityService { + + @Resource + private HistoryService historyService; + + @Override + public List getActivityListByProcessInstanceId(String processInstanceId) { + List activityList = historyService.createHistoricActivityInstanceQuery() + .processInstanceId(processInstanceId).list(); + return BpmActivityConvert.INSTANCE.convertList(activityList); + } + + @Override + public List getHistoricActivityListByExecutionId(String executionId) { + return historyService.createHistoricActivityInstanceQuery().executionId(executionId).list(); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmProcessInstanceService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmProcessInstanceService.java new file mode 100644 index 0000000..1fae57c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmProcessInstanceService.java @@ -0,0 +1,147 @@ +package com.yunxi.scm.module.bpm.service.task; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.instance.*; +import org.flowable.engine.delegate.event.FlowableCancelledEvent; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 流程实例 Service 接口 + * + * @author 芋道源码 + */ +public interface BpmProcessInstanceService { + + /** + * 获得流程实例 + * + * @param id 流程实例的编号 + * @return 流程实例 + */ + ProcessInstance getProcessInstance(String id); + + /** + * 获得流程实例列表 + * + * @param ids 流程实例的编号集合 + * @return 流程实例列表 + */ + List getProcessInstances(Set ids); + + /** + * 获得流程实例 Map + * + * @param ids 流程实例的编号集合 + * @return 流程实例列表 Map + */ + default Map getProcessInstanceMap(Set ids) { + return CollectionUtils.convertMap(getProcessInstances(ids), ProcessInstance::getProcessInstanceId); + } + + /** + * 获得流程实例的分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页请求 + * @return 流程实例的分页 + */ + PageResult getMyProcessInstancePage(Long userId, + @Valid BpmProcessInstanceMyPageReqVO pageReqVO); + /** + * 创建流程实例(提供给前端) + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 实例的编号 + */ + String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO); + + /** + * 创建流程实例(提供给内部) + * + * @param userId 用户编号 + * @param createReqDTO 创建信息 + * @return 实例的编号 + */ + String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO); + + /** + * 获得流程实例 VO 信息 + * + * @param id 流程实例的编号 + * @return 流程实例 + */ + BpmProcessInstanceRespVO getProcessInstanceVO(String id); + + /** + * 取消流程实例 + * + * @param userId 用户编号 + * @param cancelReqVO 取消信息 + */ + void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO); + + /** + * 获得历史的流程实例 + * + * @param id 流程实例的编号 + * @return 历史的流程实例 + */ + HistoricProcessInstance getHistoricProcessInstance(String id); + + /** + * 获得历史的流程实例列表 + * + * @param ids 流程实例的编号集合 + * @return 历史的流程实例列表 + */ + List getHistoricProcessInstances(Set ids); + + /** + * 获得历史的流程实例 Map + * + * @param ids 流程实例的编号集合 + * @return 历史的流程实例列表 Map + */ + default Map getHistoricProcessInstanceMap(Set ids) { + return CollectionUtils.convertMap(getHistoricProcessInstances(ids), HistoricProcessInstance::getId); + } + + /** + * 创建 ProcessInstance 拓展记录 + * + * @param instance 流程任务 + */ + void createProcessInstanceExt(ProcessInstance instance); + + /** + * 更新 ProcessInstance 拓展记录为取消 + * + * @param event 流程取消事件 + */ + void updateProcessInstanceExtCancel(FlowableCancelledEvent event); + + /** + * 更新 ProcessInstance 拓展记录为完成 + * + * @param instance 流程任务 + */ + void updateProcessInstanceExtComplete(ProcessInstance instance); + + /** + * 更新 ProcessInstance 拓展记录为不通过 + * + * @param id 流程编号 + * @param reason 理由。例如说,审批不通过时,需要传递该值 + */ + void updateProcessInstanceExtReject(String id, String reason); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmProcessInstanceServiceImpl.java new file mode 100644 index 0000000..feb790a --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.StrUtil; import com.yunxi.scm.framework.common.pojo.PageResult; import com.yunxi.scm.framework.common.util.number.NumberUtils; import com.yunxi.scm.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import com.yunxi.scm.module.bpm.controller.admin.task.vo.instance.*; import com.yunxi.scm.module.bpm.convert.task.BpmProcessInstanceConvert; import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmProcessDefinitionExtDO; import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmProcessInstanceExtDO; import com.yunxi.scm.module.bpm.dal.mysql.task.BpmProcessInstanceExtMapper; import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum; import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceResultEnum; import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceStatusEnum; import com.yunxi.scm.module.bpm.framework.bpm.core.event.BpmProcessInstanceResultEventPublisher; import com.yunxi.scm.module.bpm.service.definition.BpmProcessDefinitionService; import com.yunxi.scm.module.bpm.service.message.BpmMessageService; import com.yunxi.scm.module.system.api.dept.DeptApi; import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; import com.yunxi.scm.module.system.api.user.AdminUserApi; import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; import org.flowable.engine.delegate.event.FlowableCancelledEvent; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; import javax.validation.Valid; import java.time.LocalDateTime; import java.util.*; import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.*; /** * 流程实例 Service 实现类 * * ProcessDefinition & ProcessInstance & Execution & Task 的关系: * 1. * * HistoricProcessInstance & ProcessInstance 的关系: * 1. * * 简单来说,前者 = 历史 + 运行中的流程实例,后者仅是运行中的流程实例 * * @author 芋道源码 */ @Service @Validated @Slf4j public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService { @Resource private RuntimeService runtimeService; @Resource private BpmProcessInstanceExtMapper processInstanceExtMapper; @Resource @Lazy // 解决循环依赖 private BpmTaskService taskService; @Resource private BpmProcessDefinitionService processDefinitionService; @Resource private HistoryService historyService; @Resource private AdminUserApi adminUserApi; @Resource private DeptApi deptApi; @Resource private BpmProcessInstanceResultEventPublisher processInstanceResultEventPublisher; @Resource private BpmMessageService messageService; @Override public ProcessInstance getProcessInstance(String id) { return runtimeService.createProcessInstanceQuery().processInstanceId(id).singleResult(); } @Override public List getProcessInstances(Set ids) { return runtimeService.createProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public PageResult getMyProcessInstancePage(Long userId, BpmProcessInstanceMyPageReqVO pageReqVO) { // 通过 BpmProcessInstanceExtDO 表,先查询到对应的分页 PageResult pageResult = processInstanceExtMapper.selectPage(userId, pageReqVO); if (CollUtil.isEmpty(pageResult.getList())) { return new PageResult<>(pageResult.getTotal()); } // 获得流程 Task Map List processInstanceIds = convertList(pageResult.getList(), BpmProcessInstanceExtDO::getProcessInstanceId); Map> taskMap = taskService.getTaskMapByProcessInstanceIds(processInstanceIds); // 转换返回 return BpmProcessInstanceConvert.INSTANCE.convertPage(pageResult, taskMap); } @Override @Transactional(rollbackFor = Exception.class) public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqVO createReqVO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition(createReqVO.getProcessDefinitionId()); // 发起流程 return createProcessInstance0(userId, definition, createReqVO.getVariables(), null); } @Override public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO createReqDTO) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getActiveProcessDefinition(createReqDTO.getProcessDefinitionKey()); // 发起流程 return createProcessInstance0(userId, definition, createReqDTO.getVariables(), createReqDTO.getBusinessKey()); } @Override public BpmProcessInstanceRespVO getProcessInstanceVO(String id) { // 获得流程实例 HistoricProcessInstance processInstance = getHistoricProcessInstance(id); if (processInstance == null) { return null; } BpmProcessInstanceExtDO processInstanceExt = processInstanceExtMapper.selectByProcessInstanceId(id); Assert.notNull(processInstanceExt, "流程实例拓展({}) 不存在", id); // 获得流程定义 ProcessDefinition processDefinition = processDefinitionService .getProcessDefinition(processInstance.getProcessDefinitionId()); Assert.notNull(processDefinition, "流程定义({}) 不存在", processInstance.getProcessDefinitionId()); BpmProcessDefinitionExtDO processDefinitionExt = processDefinitionService.getProcessDefinitionExt( processInstance.getProcessDefinitionId()); Assert.notNull(processDefinitionExt, "流程定义拓展({}) 不存在", id); String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId()); // 获得 User AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())); DeptRespDTO dept = null; if (startUser != null) { dept = deptApi.getDept(startUser.getDeptId()); } // 拼接结果 return BpmProcessInstanceConvert.INSTANCE.convert2(processInstance, processInstanceExt, processDefinition, processDefinitionExt, bpmnXml, startUser, dept); } @Override public void cancelProcessInstance(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 校验流程实例存在 ProcessInstance instance = getProcessInstance(cancelReqVO.getId()); if (instance == null) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS); } // 只能取消自己的 if (!Objects.equals(instance.getStartUserId(), String.valueOf(userId))) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF); } // 通过删除流程实例,实现流程实例的取消, // 删除流程实例,正则执行任务 ACT_RU_TASK. 任务会被删除。通过历史表查询 deleteProcessInstance(cancelReqVO.getId(), BpmProcessInstanceDeleteReasonEnum.CANCEL_TASK.format(cancelReqVO.getReason())); } /** * 获得历史的流程实例 * * @param id 流程实例的编号 * @return 历史的流程实例 */ @Override public HistoricProcessInstance getHistoricProcessInstance(String id) { return historyService.createHistoricProcessInstanceQuery().processInstanceId(id).singleResult(); } @Override public List getHistoricProcessInstances(Set ids) { return historyService.createHistoricProcessInstanceQuery().processInstanceIds(ids).list(); } @Override public void createProcessInstanceExt(ProcessInstance instance) { // 获得流程定义 ProcessDefinition definition = processDefinitionService.getProcessDefinition2(instance.getProcessDefinitionId()); // 插入 BpmProcessInstanceExtDO 对象 BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() .setProcessInstanceId(instance.getId()) .setProcessDefinitionId(definition.getId()) .setName(instance.getProcessDefinitionName()) .setStartUserId(Long.valueOf(instance.getStartUserId())) .setCategory(definition.getCategory()) .setStatus(BpmProcessInstanceStatusEnum.RUNNING.getStatus()) .setResult(BpmProcessInstanceResultEnum.PROCESS.getResult()); processInstanceExtMapper.insert(instanceExtDO); } @Override public void updateProcessInstanceExtCancel(FlowableCancelledEvent event) { // 判断是否为 Reject 不通过。如果是,则不进行更新. // 因为,updateProcessInstanceExtReject 方法,已经进行更新了 if (BpmProcessInstanceDeleteReasonEnum.isRejectReason((String)event.getCause())) { return; } // 需要主动查询,因为 instance 只有 id 属性 // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance HistoricProcessInstance processInstance = getHistoricProcessInstance(event.getProcessInstanceId()); // 更新拓展表 BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() .setProcessInstanceId(event.getProcessInstanceId()) .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置 .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) .setResult(BpmProcessInstanceResultEnum.CANCEL.getResult()); processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); // 发送流程实例的状态事件 processInstanceResultEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); } @Override public void updateProcessInstanceExtComplete(ProcessInstance instance) { // 需要主动查询,因为 instance 只有 id 属性 // 另外,此时如果去查询 ProcessInstance 的话,字段是不全的,所以去查询了 HistoricProcessInstance HistoricProcessInstance processInstance = getHistoricProcessInstance(instance.getId()); // 更新拓展表 BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO() .setProcessInstanceId(instance.getProcessInstanceId()) .setEndTime(LocalDateTime.now()) // 由于 ProcessInstance 里没有办法拿到 endTime,所以这里设置 .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()); // 如果正常完全,说明审批通过 processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); // 发送流程被通过的消息 messageService.sendMessageWhenProcessInstanceApprove(BpmProcessInstanceConvert.INSTANCE.convert2ApprovedReq(instance)); // 发送流程实例的状态事件 processInstanceResultEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); } @Override @Transactional(rollbackFor = Exception.class) public void updateProcessInstanceExtReject(String id, String reason) { // 需要主动查询,因为 instance 只有 id 属性 ProcessInstance processInstance = getProcessInstance(id); // 删除流程实例,以实现驳回任务时,取消整个审批流程 deleteProcessInstance(id, StrUtil.format(BpmProcessInstanceDeleteReasonEnum.REJECT_TASK.format(reason))); // 更新 status + result // 注意,不能和上面的逻辑更换位置。因为 deleteProcessInstance 会触发流程的取消,进而调用 updateProcessInstanceExtCancel 方法, // 设置 result 为 BpmProcessInstanceStatusEnum.CANCEL,显然和 result 不一定是一致的 BpmProcessInstanceExtDO instanceExtDO = new BpmProcessInstanceExtDO().setProcessInstanceId(id) .setStatus(BpmProcessInstanceStatusEnum.FINISH.getStatus()) .setResult(BpmProcessInstanceResultEnum.REJECT.getResult()); processInstanceExtMapper.updateByProcessInstanceId(instanceExtDO); // 发送流程被不通过的消息 messageService.sendMessageWhenProcessInstanceReject(BpmProcessInstanceConvert.INSTANCE.convert2RejectReq(processInstance, reason)); // 发送流程实例的状态事件 processInstanceResultEventPublisher.sendProcessInstanceResultEvent( BpmProcessInstanceConvert.INSTANCE.convert(this, processInstance, instanceExtDO.getResult())); } private void deleteProcessInstance(String id, String reason) { runtimeService.deleteProcessInstance(id, reason); } private String createProcessInstance0(Long userId, ProcessDefinition definition, Map variables, String businessKey) { // 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); } if (definition.isSuspended()) { throw exception(PROCESS_DEFINITION_IS_SUSPENDED); } // 创建流程实例 ProcessInstance instance = runtimeService.createProcessInstanceBuilder() .processDefinitionId(definition.getId()) .businessKey(businessKey) .name(definition.getName().trim()) .variables(variables) .start(); // 设置流程名字 runtimeService.setProcessInstanceName(instance.getId(), definition.getName()); // 补全流程实例的拓展表 processInstanceExtMapper.updateByProcessInstanceId(new BpmProcessInstanceExtDO().setProcessInstanceId(instance.getId()) .setFormVariables(variables)); return instance.getId(); } } \ No newline at end of file diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmTaskService.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmTaskService.java new file mode 100644 index 0000000..d58481e --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmTaskService.java @@ -0,0 +1,131 @@ +package com.yunxi.scm.module.bpm.service.task; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.task.*; +import org.flowable.task.api.Task; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +/** + * 流程任务实例 Service 接口 + * + * @author jason + * @author 芋道源码 + */ +public interface BpmTaskService { + + /** + * 获得待办的流程任务分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页请求 + * + * @return 流程任务分页 + */ + PageResult getTodoTaskPage(Long userId, BpmTaskTodoPageReqVO pageReqVO); + + /** + * 获得已办的流程任务分页 + * + * @param userId 用户编号 + * @param pageReqVO 分页请求 + * + * @return 流程任务分页 + */ + PageResult getDoneTaskPage(Long userId, BpmTaskDonePageReqVO pageReqVO); + + /** + * 获得流程任务 Map + * + * @param processInstanceIds 流程实例的编号数组 + * + * @return 流程任务 Map + */ + default Map> getTaskMapByProcessInstanceIds(List processInstanceIds) { + return CollectionUtils.convertMultiMap(getTasksByProcessInstanceIds(processInstanceIds), + Task::getProcessInstanceId); + } + + /** + * 获得流程任务列表 + * + * @param processInstanceIds 流程实例的编号数组 + * + * @return 流程任务列表 + */ + List getTasksByProcessInstanceIds(List processInstanceIds); + + /** + * 获得指令流程实例的流程任务列表,包括所有状态的 + * + * @param processInstanceId 流程实例的编号 + * + * @return 流程任务列表 + */ + List getTaskListByProcessInstanceId(String processInstanceId); + + /** + * 通过任务 + * + * @param userId 用户编号 + * @param reqVO 通过请求 + */ + void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO); + + /** + * 不通过任务 + * + * @param userId 用户编号 + * @param reqVO 不通过请求 + */ + void rejectTask(Long userId, @Valid BpmTaskRejectReqVO reqVO); + + /** + * 将流程任务分配给指定用户 + * + * @param userId 用户编号 + * @param reqVO 分配请求 + */ + void updateTaskAssignee(Long userId, BpmTaskUpdateAssigneeReqVO reqVO); + + /** + * 将流程任务分配给指定用户 + * + * @param id 流程任务编号 + * @param userId 用户编号 + */ + void updateTaskAssignee(String id, Long userId); + + /** + * 创建 Task 拓展记录 + * + * @param task 任务实体 + */ + void createTaskExt(Task task); + + /** + * 更新 Task 拓展记录为完成 + * + * @param task 任务实体 + */ + void updateTaskExtComplete(Task task); + + /** + * 更新 Task 拓展记录为已取消 + * + * @param taskId 任务的编号 + */ + void updateTaskExtCancel(String taskId); + + /** + * 更新 Task 拓展记录,并发送通知 + * + * @param task 任务实体 + */ + void updateTaskExtAssign(Task task); + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmTaskServiceImpl.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmTaskServiceImpl.java new file mode 100644 index 0000000..dc58071 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/BpmTaskServiceImpl.java @@ -0,0 +1,319 @@ +package com.yunxi.scm.module.bpm.service.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.common.util.number.NumberUtils; +import com.yunxi.scm.framework.common.util.object.PageUtils; +import com.yunxi.scm.module.bpm.controller.admin.task.vo.task.*; +import com.yunxi.scm.module.bpm.convert.task.BpmTaskConvert; +import com.yunxi.scm.module.bpm.dal.dataobject.task.BpmTaskExtDO; +import com.yunxi.scm.module.bpm.dal.mysql.task.BpmTaskExtMapper; +import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceDeleteReasonEnum; +import com.yunxi.scm.module.bpm.enums.task.BpmProcessInstanceResultEnum; +import com.yunxi.scm.module.bpm.service.message.BpmMessageService; +import com.yunxi.scm.module.system.api.dept.DeptApi; +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.api.user.AdminUserApi; +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.HistoryService; +import org.flowable.engine.TaskService; +import org.flowable.engine.history.HistoricProcessInstance; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskQuery; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.flowable.task.api.history.HistoricTaskInstanceQuery; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.*; + +/** + * 流程任务实例 Service 实现类 + * + * @author 芋道源码 + * @author jason + */ +@Slf4j +@Service +public class BpmTaskServiceImpl implements BpmTaskService { + + @Resource + private TaskService taskService; + @Resource + private HistoryService historyService; + + @Resource + private BpmProcessInstanceService processInstanceService; + @Resource + private AdminUserApi adminUserApi; + @Resource + private DeptApi deptApi; + @Resource + private BpmTaskExtMapper taskExtMapper; + @Resource + private BpmMessageService messageService; + + @Override + public PageResult getTodoTaskPage(Long userId, BpmTaskTodoPageReqVO pageVO) { + // 查询待办任务 + TaskQuery taskQuery = taskService.createTaskQuery().taskAssignee(String.valueOf(userId)) // 分配给自己 + .orderByTaskCreateTime().desc(); // 创建时间倒序 + if (StrUtil.isNotBlank(pageVO.getName())) { + taskQuery.taskNameLike("%" + pageVO.getName() + "%"); + } + if (ArrayUtil.get(pageVO.getCreateTime(), 0) != null) { + taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); + } + if (ArrayUtil.get(pageVO.getCreateTime(), 1) != null) { + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); + } + // 执行查询 + List tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + if (CollUtil.isEmpty(tasks)) { + return PageResult.empty(taskQuery.count()); + } + + // 获得 ProcessInstance Map + Map processInstanceMap = + processInstanceService.getProcessInstanceMap(convertSet(tasks, Task::getProcessInstanceId)); + // 获得 User Map + Map userMap = adminUserApi.getUserMap( + convertSet(processInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); + // 拼接结果 + return new PageResult<>(BpmTaskConvert.INSTANCE.convertList1(tasks, processInstanceMap, userMap), + taskQuery.count()); + } + + @Override + public PageResult getDoneTaskPage(Long userId, BpmTaskDonePageReqVO pageVO) { + // 查询已办任务 + HistoricTaskInstanceQuery taskQuery = historyService.createHistoricTaskInstanceQuery().finished() // 已完成 + .taskAssignee(String.valueOf(userId)) // 分配给自己 + .orderByHistoricTaskInstanceEndTime().desc(); // 审批时间倒序 + if (StrUtil.isNotBlank(pageVO.getName())) { + taskQuery.taskNameLike("%" + pageVO.getName() + "%"); + } + if (pageVO.getBeginCreateTime() != null) { + taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getBeginCreateTime())); + } + if (pageVO.getEndCreateTime() != null) { + taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getEndCreateTime())); + } + // 执行查询 + List tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + if (CollUtil.isEmpty(tasks)) { + return PageResult.empty(taskQuery.count()); + } + + // 获得 TaskExtDO Map + List bpmTaskExtDOs = + taskExtMapper.selectListByTaskIds(convertSet(tasks, HistoricTaskInstance::getId)); + Map bpmTaskExtDOMap = convertMap(bpmTaskExtDOs, BpmTaskExtDO::getTaskId); + // 获得 ProcessInstance Map + Map historicProcessInstanceMap = + processInstanceService.getHistoricProcessInstanceMap( + convertSet(tasks, HistoricTaskInstance::getProcessInstanceId)); + // 获得 User Map + Map userMap = adminUserApi.getUserMap( + convertSet(historicProcessInstanceMap.values(), instance -> Long.valueOf(instance.getStartUserId()))); + // 拼接结果 + return new PageResult<>( + BpmTaskConvert.INSTANCE.convertList2(tasks, bpmTaskExtDOMap, historicProcessInstanceMap, userMap), + taskQuery.count()); + } + + @Override + public List getTasksByProcessInstanceIds(List processInstanceIds) { + if (CollUtil.isEmpty(processInstanceIds)) { + return Collections.emptyList(); + } + return taskService.createTaskQuery().processInstanceIdIn(processInstanceIds).list(); + } + + @Override + public List getTaskListByProcessInstanceId(String processInstanceId) { + // 获得任务列表 + List tasks = historyService.createHistoricTaskInstanceQuery() + .processInstanceId(processInstanceId) + .orderByHistoricTaskInstanceStartTime().desc() // 创建时间倒序 + .list(); + if (CollUtil.isEmpty(tasks)) { + return Collections.emptyList(); + } + + // 获得 TaskExtDO Map + List bpmTaskExtDOs = taskExtMapper.selectListByTaskIds(convertSet(tasks, HistoricTaskInstance::getId)); + Map bpmTaskExtDOMap = convertMap(bpmTaskExtDOs, BpmTaskExtDO::getTaskId); + // 获得 ProcessInstance Map + HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(processInstanceId); + // 获得 User Map + Set userIds = convertSet(tasks, task -> NumberUtils.parseLong(task.getAssignee())); + userIds.add(NumberUtils.parseLong(processInstance.getStartUserId())); + Map userMap = adminUserApi.getUserMap(userIds); + // 获得 Dept Map + Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + + // 拼接数据 + return BpmTaskConvert.INSTANCE.convertList3(tasks, bpmTaskExtDOMap, processInstance, userMap, deptMap); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO) { + // 校验任务存在 + Task task = checkTask(userId, reqVO.getId()); + // 校验流程实例存在 + ProcessInstance instance = processInstanceService.getProcessInstance(task.getProcessInstanceId()); + if (instance == null) { + throw exception(PROCESS_INSTANCE_NOT_EXISTS); + } + + // 完成任务,审批通过 + taskService.complete(task.getId(), instance.getProcessVariables()); + + // 更新任务拓展表为通过 + taskExtMapper.updateByTaskId( + new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()) + .setReason(reqVO.getReason())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void rejectTask(Long userId, @Valid BpmTaskRejectReqVO reqVO) { + Task task = checkTask(userId, reqVO.getId()); + // 校验流程实例存在 + ProcessInstance instance = processInstanceService.getProcessInstance(task.getProcessInstanceId()); + if (instance == null) { + throw exception(PROCESS_INSTANCE_NOT_EXISTS); + } + + // 更新流程实例为不通过 + processInstanceService.updateProcessInstanceExtReject(instance.getProcessInstanceId(), reqVO.getReason()); + + // 更新任务拓展表为不通过 + taskExtMapper.updateByTaskId( + new BpmTaskExtDO().setTaskId(task.getId()).setResult(BpmProcessInstanceResultEnum.REJECT.getResult()) + .setEndTime(LocalDateTime.now()).setReason(reqVO.getReason())); + } + + @Override + public void updateTaskAssignee(Long userId, BpmTaskUpdateAssigneeReqVO reqVO) { + // 校验任务存在 + Task task = checkTask(userId, reqVO.getId()); + // 更新负责人 + updateTaskAssignee(task.getId(), reqVO.getAssigneeUserId()); + } + + @Override + public void updateTaskAssignee(String id, Long userId) { + taskService.setAssignee(id, String.valueOf(userId)); + } + + /** + * 校验任务是否存在, 并且是否是分配给自己的任务 + * + * @param userId 用户 id + * @param taskId task id + */ + private Task checkTask(Long userId, String taskId) { + Task task = getTask(taskId); + if (task == null) { + throw exception(TASK_COMPLETE_FAIL_NOT_EXISTS); + } + if (!Objects.equals(userId, NumberUtils.parseLong(task.getAssignee()))) { + throw exception(TASK_COMPLETE_FAIL_ASSIGN_NOT_SELF); + } + return task; + } + + @Override + public void createTaskExt(Task task) { + BpmTaskExtDO taskExtDO = + BpmTaskConvert.INSTANCE.convert2TaskExt(task).setResult(BpmProcessInstanceResultEnum.PROCESS.getResult()); + taskExtMapper.insert(taskExtDO); + } + + @Override + public void updateTaskExtComplete(Task task) { + BpmTaskExtDO taskExtDO = BpmTaskConvert.INSTANCE.convert2TaskExt(task) + .setResult(BpmProcessInstanceResultEnum.APPROVE.getResult()) // 不设置也问题不大,因为 Complete 一般是审核通过,已经设置 + .setEndTime(LocalDateTime.now()); + taskExtMapper.updateByTaskId(taskExtDO); + } + + @Override + public void updateTaskExtCancel(String taskId) { + // 需要在事务提交后,才进行查询。不然查询不到历史的原因 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + // 可能只是活动,不是任务,所以查询不到 + HistoricTaskInstance task = getHistoricTask(taskId); + if (task == null) { + return; + } + + // 如果任务拓展表已经是完成的状态,则跳过 + BpmTaskExtDO taskExt = taskExtMapper.selectByTaskId(taskId); + if (taskExt == null) { + log.error("[updateTaskExtCancel][taskId({}) 查找不到对应的记录,可能存在问题]", taskId); + return; + } + // 如果已经是最终的结果,则跳过 + if (BpmProcessInstanceResultEnum.isEndResult(taskExt.getResult())) { + log.error("[updateTaskExtCancel][taskId({}) 处于结果({}),无需进行更新]", taskId, taskExt.getResult()); + return; + } + + // 更新任务 + taskExtMapper.updateById(new BpmTaskExtDO().setId(taskExt.getId()).setResult(BpmProcessInstanceResultEnum.CANCEL.getResult()) + .setEndTime(LocalDateTime.now()).setReason(BpmProcessInstanceDeleteReasonEnum.translateReason(task.getDeleteReason()))); + } + + }); + } + + @Override + public void updateTaskExtAssign(Task task) { + BpmTaskExtDO taskExtDO = + new BpmTaskExtDO().setAssigneeUserId(NumberUtils.parseLong(task.getAssignee())).setTaskId(task.getId()); + taskExtMapper.updateByTaskId(taskExtDO); + // 发送通知。在事务提交时,批量执行操作,所以直接查询会无法查询到 ProcessInstance,所以这里是通过监听事务的提交来实现。 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + ProcessInstance processInstance = + processInstanceService.getProcessInstance(task.getProcessInstanceId()); + AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())); + messageService.sendMessageWhenTaskAssigned( + BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task)); + } + }); + } + + private Task getTask(String id) { + return taskService.createTaskQuery().taskId(id).singleResult(); + } + + private HistoricTaskInstance getHistoricTask(String id) { + return historyService.createHistoricTaskInstanceQuery().taskId(id).singleResult(); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/package-info.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/package-info.java new file mode 100644 index 0000000..546fd4c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/main/java/com/yunxi/scm/module/bpm/service/task/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.bpm.service.task; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX2ScriptTest.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX2ScriptTest.java new file mode 100644 index 0000000..4575163 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX2ScriptTest.java @@ -0,0 +1,104 @@ +package com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.impl; + +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.bpm.service.task.BpmProcessInstanceService; +import com.yunxi.scm.module.system.api.dept.DeptApi; +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.api.user.AdminUserApi; +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.engine.delegate.DelegateExecution; +import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.Set; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +public class BpmTaskAssignLeaderX2ScriptTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskAssignLeaderX2Script script; + + @Mock + private AdminUserApi adminUserApi; + @Mock + private DeptApi deptApi; + @Mock + private BpmProcessInstanceService bpmProcessInstanceService; + + @Test + public void testCalculateTaskCandidateUsers_noDept() { + // 准备参数 + DelegateExecution execution = mockDelegateExecution(1L); + // mock 方法(startUser) + AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); + when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); + // mock 方法(getStartUserDept)没有部门 + when(deptApi.getDept(eq(10L))).thenReturn(null); + + // 调用 + Set result = script.calculateTaskCandidateUsers(execution); + // 断言 + assertEquals(0, result.size()); + } + + @Test + public void testCalculateTaskCandidateUsers_noParentDept() { + // 准备参数 + DelegateExecution execution = mockDelegateExecution(1L); + // mock 方法(startUser) + AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); + when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); + DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L) + .setLeaderUserId(20L)); + // mock 方法(getDept) + when(deptApi.getDept(eq(10L))).thenReturn(startUserDept); + when(deptApi.getDept(eq(100L))).thenReturn(null); + + // 调用 + Set result = script.calculateTaskCandidateUsers(execution); + // 断言 + assertEquals(asSet(20L), result); + } + + @Test + public void testCalculateTaskCandidateUsers_existParentDept() { + // 准备参数 + DelegateExecution execution = mockDelegateExecution(1L); + // mock 方法(startUser) + AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); + when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); + DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L) + .setLeaderUserId(20L)); + when(deptApi.getDept(eq(10L))).thenReturn(startUserDept); + // mock 方法(父 dept) + DeptRespDTO parentDept = randomPojo(DeptRespDTO.class, o -> o.setId(100L).setParentId(1000L) + .setLeaderUserId(200L)); + when(deptApi.getDept(eq(100L))).thenReturn(parentDept); + + // 调用 + Set result = script.calculateTaskCandidateUsers(execution); + // 断言 + assertEquals(asSet(200L), result); + } + + @SuppressWarnings("SameParameterValue") + private DelegateExecution mockDelegateExecution(Long startUserId) { + ExecutionEntityImpl execution = new ExecutionEntityImpl(); + execution.setProcessInstanceId(randomString()); + // mock 返回 startUserId + ExecutionEntityImpl processInstance = new ExecutionEntityImpl(); + processInstance.setStartUserId(String.valueOf(startUserId)); + when(bpmProcessInstanceService.getProcessInstance(eq(execution.getProcessInstanceId()))) + .thenReturn(processInstance); + return execution; + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmFormServiceTest.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmFormServiceTest.java new file mode 100644 index 0000000..7567324 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmFormServiceTest.java @@ -0,0 +1,145 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.form.BpmFormUpdateReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmFormDO; +import com.yunxi.scm.module.bpm.dal.mysql.definition.BpmFormMapper; +import com.yunxi.scm.module.bpm.service.definition.dto.BpmFormFieldRespDTO; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.FORM_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link BpmFormServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(BpmFormServiceImpl.class) +public class BpmFormServiceTest extends BaseDbUnitTest { + + @Resource + private BpmFormServiceImpl formService; + + @Resource + private BpmFormMapper formMapper; + + @Test + public void testCreateForm_success() { + // 准备参数 + BpmFormCreateReqVO reqVO = randomPojo(BpmFormCreateReqVO.class, o -> { + o.setConf("{}"); + o.setFields(randomFields()); + }); + + // 调用 + Long formId = formService.createForm(reqVO); + // 断言 + assertNotNull(formId); + // 校验记录的属性是否正确 + BpmFormDO form = formMapper.selectById(formId); + assertPojoEquals(reqVO, form); + } + + @Test + public void testUpdateForm_success() { + // mock 数据 + BpmFormDO dbForm = randomPojo(BpmFormDO.class, o -> { + o.setConf("{}"); + o.setFields(randomFields()); + }); + formMapper.insert(dbForm);// @Sql: 先插入出一条存在的数据 + // 准备参数 + BpmFormUpdateReqVO reqVO = randomPojo(BpmFormUpdateReqVO.class, o -> { + o.setId(dbForm.getId()); // 设置更新的 ID + o.setConf("{'yunxi': 'yuanma'}"); + o.setFields(randomFields()); + }); + + // 调用 + formService.updateForm(reqVO); + // 校验是否更新正确 + BpmFormDO form = formMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, form); + } + + @Test + public void testUpdateForm_notExists() { + // 准备参数 + BpmFormUpdateReqVO reqVO = randomPojo(BpmFormUpdateReqVO.class, o -> { + o.setConf("{'yunxi': 'yuanma'}"); + o.setFields(randomFields()); + }); + + // 调用, 并断言异常 + assertServiceException(() -> formService.updateForm(reqVO), FORM_NOT_EXISTS); + } + + @Test + public void testDeleteForm_success() { + // mock 数据 + BpmFormDO dbForm = randomPojo(BpmFormDO.class); + formMapper.insert(dbForm);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbForm.getId(); + + // 调用 + formService.deleteForm(id); + // 校验数据不存在了 + assertNull(formMapper.selectById(id)); + } + + @Test + public void testDeleteForm_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> formService.deleteForm(id), FORM_NOT_EXISTS); + } + + @Test + public void testGetFormPage() { + // mock 数据 + BpmFormDO dbForm = randomPojo(BpmFormDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + }); + formMapper.insert(dbForm); + // 测试 name 不匹配 + formMapper.insert(cloneIgnoreId(dbForm, o -> o.setName("源码"))); + // 准备参数 + BpmFormPageReqVO reqVO = new BpmFormPageReqVO(); + reqVO.setName("芋道"); + + // 调用 + PageResult pageResult = formService.getFormPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbForm, pageResult.getList().get(0)); + } + + private List randomFields() { + int size = RandomUtil.randomInt(1, 3); + return Stream.iterate(0, i -> i).limit(size) + .map(i -> JsonUtils.toJsonString(randomPojo(BpmFormFieldRespDTO.class))) + .collect(Collectors.toList()); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleServiceImplTest.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleServiceImplTest.java new file mode 100644 index 0000000..eeb6054 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmTaskAssignRuleServiceImplTest.java @@ -0,0 +1,227 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum; +import com.yunxi.scm.module.bpm.enums.definition.BpmTaskRuleScriptEnum; +import com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript; +import com.yunxi.scm.module.bpm.framework.flowable.core.behavior.script.impl.BpmTaskAssignStartUserScript; +import com.yunxi.scm.module.system.api.dept.DeptApi; +import com.yunxi.scm.module.system.api.dept.PostApi; +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.api.dict.DictDataApi; +import com.yunxi.scm.module.system.api.permission.PermissionApi; +import com.yunxi.scm.module.system.api.permission.RoleApi; +import com.yunxi.scm.module.system.api.user.AdminUserApi; +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import org.flowable.engine.delegate.DelegateExecution; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Collections.singleton; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link BpmTaskAssignRuleService} 的单元测试 + * + * @author 芋道源码 + */ +@Import({BpmTaskAssignRuleServiceImpl.class, BpmTaskAssignStartUserScript.class}) // Import 引入 BpmTaskAssignStartUserScript 目的是保证不报错 +public class BpmTaskAssignRuleServiceImplTest extends BaseDbUnitTest { + + @Resource + private BpmTaskAssignRuleServiceImpl bpmTaskRuleService; + + @MockBean + private BpmUserGroupService userGroupService; + @MockBean + private DeptApi deptApi; + @MockBean + private AdminUserApi adminUserApi; + @MockBean + private PermissionApi permissionApi; + @MockBean + private RoleApi roleApi; + @MockBean + private PostApi postApi; + @MockBean + private DictDataApi dictDataApi; + + @Test + public void testCalculateTaskCandidateUsers_Role() { + // 准备参数 + BpmTaskAssignRuleDO rule = new BpmTaskAssignRuleDO().setOptions(asSet(1L, 2L)) + .setType(BpmTaskAssignRuleTypeEnum.ROLE.getType()); + // mock 方法 + when(permissionApi.getUserRoleIdListByRoleIds(eq(rule.getOptions()))) + .thenReturn(asSet(11L, 22L)); + mockGetUserMap(asSet(11L, 22L)); + + // 调用 + Set results = bpmTaskRuleService.calculateTaskCandidateUsers(null, rule); + // 断言 + assertEquals(asSet(11L, 22L), results); + } + + @Test + public void testCalculateTaskCandidateUsers_DeptMember() { + // 准备参数 + BpmTaskAssignRuleDO rule = new BpmTaskAssignRuleDO().setOptions(asSet(1L, 2L)) + .setType(BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType()); + // mock 方法 + List users = CollectionUtils.convertList(asSet(11L, 22L), + id -> new AdminUserRespDTO().setId(id)); + when(adminUserApi.getUserListByDeptIds(eq(rule.getOptions()))).thenReturn(users); + mockGetUserMap(asSet(11L, 22L)); + + // 调用 + Set results = bpmTaskRuleService.calculateTaskCandidateUsers(null, rule); + // 断言 + assertEquals(asSet(11L, 22L), results); + } + + @Test + public void testCalculateTaskCandidateUsers_DeptLeader() { + // 准备参数 + BpmTaskAssignRuleDO rule = new BpmTaskAssignRuleDO().setOptions(asSet(1L, 2L)) + .setType(BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType()); + // mock 方法 + DeptRespDTO dept1 = randomPojo(DeptRespDTO.class, o -> o.setLeaderUserId(11L)); + DeptRespDTO dept2 = randomPojo(DeptRespDTO.class, o -> o.setLeaderUserId(22L)); + when(deptApi.getDeptList(eq(rule.getOptions()))).thenReturn(Arrays.asList(dept1, dept2)); + mockGetUserMap(asSet(11L, 22L)); + + // 调用 + Set results = bpmTaskRuleService.calculateTaskCandidateUsers(null, rule); + // 断言 + assertEquals(asSet(11L, 22L), results); + } + + @Test + public void testCalculateTaskCandidateUsers_Post() { + // 准备参数 + BpmTaskAssignRuleDO rule = new BpmTaskAssignRuleDO().setOptions(asSet(1L, 2L)) + .setType(BpmTaskAssignRuleTypeEnum.POST.getType()); + // mock 方法 + List users = CollectionUtils.convertList(asSet(11L, 22L), + id -> new AdminUserRespDTO().setId(id)); + when(adminUserApi.getUserListByPostIds(eq(rule.getOptions()))).thenReturn(users); + mockGetUserMap(asSet(11L, 22L)); + + // 调用 + Set results = bpmTaskRuleService.calculateTaskCandidateUsers(null, rule); + // 断言 + assertEquals(asSet(11L, 22L), results); + } + + @Test + public void testCalculateTaskCandidateUsers_User() { + // 准备参数 + BpmTaskAssignRuleDO rule = new BpmTaskAssignRuleDO().setOptions(asSet(1L, 2L)) + .setType(BpmTaskAssignRuleTypeEnum.USER.getType()); + // mock 方法 + mockGetUserMap(asSet(1L, 2L)); + + // 调用 + Set results = bpmTaskRuleService.calculateTaskCandidateUsers(null, rule); + // 断言 + assertEquals(asSet(1L, 2L), results); + } + + @Test + public void testCalculateTaskCandidateUsers_UserGroup() { + // 准备参数 + BpmTaskAssignRuleDO rule = new BpmTaskAssignRuleDO().setOptions(asSet(1L, 2L)) + .setType(BpmTaskAssignRuleTypeEnum.USER_GROUP.getType()); + // mock 方法 + BpmUserGroupDO userGroup1 = randomPojo(BpmUserGroupDO.class, o -> o.setMemberUserIds(asSet(11L, 12L))); + BpmUserGroupDO userGroup2 = randomPojo(BpmUserGroupDO.class, o -> o.setMemberUserIds(asSet(21L, 22L))); + when(userGroupService.getUserGroupList(eq(rule.getOptions()))).thenReturn(Arrays.asList(userGroup1, userGroup2)); + mockGetUserMap(asSet(11L, 12L, 21L, 22L)); + + // 调用 + Set results = bpmTaskRuleService.calculateTaskCandidateUsers(null, rule); + // 断言 + assertEquals(asSet(11L, 12L, 21L, 22L), results); + } + + @Test + public void testCalculateTaskCandidateUsers_Script() { + // 准备参数 + BpmTaskAssignRuleDO rule = new BpmTaskAssignRuleDO().setOptions(asSet(20L, 21L)) + .setType(BpmTaskAssignRuleTypeEnum.SCRIPT.getType()); + // mock 方法 + BpmTaskAssignScript script1 = new BpmTaskAssignScript() { + + @Override + public Set calculateTaskCandidateUsers(DelegateExecution task) { + return singleton(11L); + } + + @Override + public BpmTaskRuleScriptEnum getEnum() { + return BpmTaskRuleScriptEnum.LEADER_X1; + } + }; + BpmTaskAssignScript script2 = new BpmTaskAssignScript() { + + @Override + public Set calculateTaskCandidateUsers(DelegateExecution task) { + return singleton(22L); + } + + @Override + public BpmTaskRuleScriptEnum getEnum() { + return BpmTaskRuleScriptEnum.LEADER_X2; + } + }; + bpmTaskRuleService.setScripts(Arrays.asList(script1, script2)); + mockGetUserMap(asSet(11L, 22L)); + + // 调用 + Set results = bpmTaskRuleService.calculateTaskCandidateUsers(null, rule); + // 断言 + assertEquals(asSet(11L, 22L), results); + } + + @Test + public void testRemoveDisableUsers() { + // 准备参数. 1L 可以找到;2L 是禁用的;3L 找不到 + Set assigneeUserIds = asSet(1L, 2L, 3L); + // mock 方法 + AdminUserRespDTO user1 = randomPojo(AdminUserRespDTO.class, o -> o.setId(1L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + AdminUserRespDTO user2 = randomPojo(AdminUserRespDTO.class, o -> o.setId(2L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + Map userMap = MapUtil.builder(user1.getId(), user1) + .put(user2.getId(), user2).build(); + when(adminUserApi.getUserMap(eq(assigneeUserIds))).thenReturn(userMap); + + // 调用 + bpmTaskRuleService.removeDisableUsers(assigneeUserIds); + // 断言 + assertEquals(asSet(1L), assigneeUserIds); + } + + private void mockGetUserMap(Set assigneeUserIds) { + Map userMap = CollectionUtils.convertMap(assigneeUserIds, id -> id, + id -> new AdminUserRespDTO().setId(id).setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(adminUserApi.getUserMap(eq(assigneeUserIds))).thenReturn(userMap); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupServiceTest.java b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupServiceTest.java new file mode 100644 index 0000000..b0ac469 --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/java/com/yunxi/scm/module/bpm/service/definition/BpmUserGroupServiceTest.java @@ -0,0 +1,131 @@ +package com.yunxi.scm.module.bpm.service.definition; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.framework.test.core.util.AssertUtils; +import com.yunxi.scm.framework.test.core.util.RandomUtils; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupCreateReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO; +import com.yunxi.scm.module.bpm.controller.admin.definition.vo.group.BpmUserGroupUpdateReqVO; +import com.yunxi.scm.module.bpm.dal.dataobject.definition.BpmUserGroupDO; +import com.yunxi.scm.module.bpm.dal.mysql.definition.BpmUserGroupMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.module.bpm.enums.ErrorCodeConstants.USER_GROUP_NOT_EXISTS; + +/** + * {@link BpmUserGroupServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(BpmUserGroupServiceImpl.class) +public class BpmUserGroupServiceTest extends BaseDbUnitTest { + + @Resource + private BpmUserGroupServiceImpl userGroupService; + + @Resource + private BpmUserGroupMapper userGroupMapper; + + @Test + public void testCreateUserGroup_success() { + // 准备参数 + BpmUserGroupCreateReqVO reqVO = RandomUtils.randomPojo(BpmUserGroupCreateReqVO.class); + + // 调用 + Long userGroupId = userGroupService.createUserGroup(reqVO); + // 断言 + Assertions.assertNotNull(userGroupId); + // 校验记录的属性是否正确 + BpmUserGroupDO userGroup = userGroupMapper.selectById(userGroupId); + AssertUtils.assertPojoEquals(reqVO, userGroup); + } + + @Test + public void testUpdateUserGroup_success() { + // mock 数据 + BpmUserGroupDO dbUserGroup = RandomUtils.randomPojo(BpmUserGroupDO.class); + userGroupMapper.insert(dbUserGroup);// @Sql: 先插入出一条存在的数据 + // 准备参数 + BpmUserGroupUpdateReqVO reqVO = RandomUtils.randomPojo(BpmUserGroupUpdateReqVO.class, o -> { + o.setId(dbUserGroup.getId()); // 设置更新的 ID + }); + + // 调用 + userGroupService.updateUserGroup(reqVO); + // 校验是否更新正确 + BpmUserGroupDO userGroup = userGroupMapper.selectById(reqVO.getId()); // 获取最新的 + AssertUtils.assertPojoEquals(reqVO, userGroup); + } + + @Test + public void testUpdateUserGroup_notExists() { + // 准备参数 + BpmUserGroupUpdateReqVO reqVO = RandomUtils.randomPojo(BpmUserGroupUpdateReqVO.class); + + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> userGroupService.updateUserGroup(reqVO), USER_GROUP_NOT_EXISTS); + } + + @Test + public void testDeleteUserGroup_success() { + // mock 数据 + BpmUserGroupDO dbUserGroup = RandomUtils.randomPojo(BpmUserGroupDO.class); + userGroupMapper.insert(dbUserGroup);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbUserGroup.getId(); + + // 调用 + userGroupService.deleteUserGroup(id); + // 校验数据不存在了 + Assertions.assertNull(userGroupMapper.selectById(id)); + } + + @Test + public void testDeleteUserGroup_notExists() { + // 准备参数 + Long id = RandomUtils.randomLongId(); + + // 调用, 并断言异常 + AssertUtils.assertServiceException(() -> userGroupService.deleteUserGroup(id), USER_GROUP_NOT_EXISTS); + } + + @Test + public void testGetUserGroupPage() { + // mock 数据 + BpmUserGroupDO dbUserGroup = RandomUtils.randomPojo(BpmUserGroupDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + userGroupMapper.insert(dbUserGroup); + // 测试 name 不匹配 + userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setName("芋道"))); + // 测试 status 不匹配 + userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + userGroupMapper.insert(cloneIgnoreId(dbUserGroup, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + BpmUserGroupPageReqVO reqVO = new BpmUserGroupPageReqVO(); + reqVO.setName("源码"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2021, 11, 10),buildTime(2021, 11, 12)})); + + // 调用 + PageResult pageResult = userGroupService.getUserGroupPage(reqVO); + // 断言 + Assertions.assertEquals(1, pageResult.getTotal()); + Assertions.assertEquals(1, pageResult.getList().size()); + AssertUtils.assertPojoEquals(dbUserGroup, pageResult.getList().get(0)); + } + +} diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..ed1209c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,44 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + +mybatis-plus: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + type-aliases-package: ${yunxi.info.base-package}.module.*.dal.dataobject + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm.module diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/logback.xml b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/sql/clean.sql b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..6e42d3c --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,2 @@ +DELETE FROM "bpm_form"; +DELETE FROM "bpm_user_group"; diff --git a/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..20a939b --- /dev/null +++ b/yunxi-module-bpm/yunxi-module-bpm-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS "bpm_user_group" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "description" varchar(255) NOT NULL, + "status" tinyint NOT NULL, + "member_user_ids" varchar(255) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '用户组'; + +CREATE TABLE IF NOT EXISTS "bpm_form" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "status" tinyint NOT NULL, + "fields" varchar(255) NOT NULL, + "conf" varchar(255) NOT NULL, + "remark" varchar(255), + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '动态表单'; diff --git a/yunxi-module-infra/pom.xml b/yunxi-module-infra/pom.xml new file mode 100644 index 0000000..76df4be --- /dev/null +++ b/yunxi-module-infra/pom.xml @@ -0,0 +1,25 @@ + + + + com.yunxi.scm + yunxi + ${revision} + + 4.0.0 + + yunxi-module-infra-api + yunxi-module-infra-biz + + yunxi-module-infra + pom + + ${project.artifactId} + + infra 模块,主要提供两块能力: + 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + + + diff --git a/yunxi-module-infra/yunxi-module-infra-api/pom.xml b/yunxi-module-infra/yunxi-module-infra-api/pom.xml new file mode 100644 index 0000000..6553063 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/pom.xml @@ -0,0 +1,33 @@ + + + + com.yunxi.scm + yunxi-module-infra + ${revision} + + 4.0.0 + yunxi-module-infra-api + jar + + ${project.artifactId} + + infra 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/file/FileApi.java b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/file/FileApi.java new file mode 100644 index 0000000..cea1907 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/file/FileApi.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.infra.api.file; + +/** + * 文件 API 接口 + * + * @author 芋道源码 + */ +public interface FileApi { + + /** + * 保存文件,并返回文件的访问路径 + * + * @param content 文件内容 + * @return 文件路径 + */ + default String createFile(byte[] content) { + return createFile(null, null, content); + } + + /** + * 保存文件,并返回文件的访问路径 + * + * @param path 文件路径 + * @param content 文件内容 + * @return 文件路径 + */ + default String createFile(String path, byte[] content) { + return createFile(null, path, content); + } + + /** + * 保存文件,并返回文件的访问路径 + * + * @param name 文件名称 + * @param path 文件路径 + * @param content 文件内容 + * @return 文件路径 + */ + String createFile(String name, String path, byte[] content); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiAccessLogApi.java b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiAccessLogApi.java new file mode 100644 index 0000000..a25bd64 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiAccessLogApi.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.infra.api.logger; + +import com.yunxi.scm.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * API 访问日志的 API 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogApi { + + /** + * 创建 API 访问日志 + * + * @param createDTO 创建信息 + */ + void createApiAccessLog(@Valid ApiAccessLogCreateReqDTO createDTO); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiErrorLogApi.java b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiErrorLogApi.java new file mode 100644 index 0000000..c0a7f90 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiErrorLogApi.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.infra.api.logger; + +import com.yunxi.scm.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * API 错误日志的 API 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogApi { + + /** + * 创建 API 错误日志 + * + * @param createDTO 创建信息 + */ + void createApiErrorLog(@Valid ApiErrorLogCreateReqDTO createDTO); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java new file mode 100644 index 0000000..b4fe7e7 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/dto/ApiAccessLogCreateReqDTO.java @@ -0,0 +1,85 @@ +package com.yunxi.scm.module.infra.api.logger.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 访问日志 + * + * @author 芋道源码 + */ +@Data +public class ApiAccessLogCreateReqDTO { + + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 开始请求时间 + */ + @NotNull(message = "开始请求时间不能为空") + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + @NotNull(message = "结束请求时间不能为空") + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + /** + * 结果码 + */ + @NotNull(message = "错误码不能为空") + private Integer resultCode; + /** + * 结果提示 + */ + private String resultMsg; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/dto/ApiErrorLogCreateReqDTO.java b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/dto/ApiErrorLogCreateReqDTO.java new file mode 100644 index 0000000..517e609 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/logger/dto/ApiErrorLogCreateReqDTO.java @@ -0,0 +1,107 @@ +package com.yunxi.scm.module.infra.api.logger.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +/** + * API 错误日志 + * + * @author 芋道源码 + */ +@Data +public class ApiErrorLogCreateReqDTO { + + /** + * 链路编号 + */ + private String traceId; + /** + * 账号编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + + /** + * 请求方法名 + */ + @NotNull(message = "http 请求方法不能为空") + private String requestMethod; + /** + * 访问地址 + */ + @NotNull(message = "访问地址不能为空") + private String requestUrl; + /** + * 请求参数 + */ + @NotNull(message = "请求参数不能为空") + private String requestParams; + /** + * 用户 IP + */ + @NotNull(message = "ip 不能为空") + private String userIp; + /** + * 浏览器 UA + */ + @NotNull(message = "User-Agent 不能为空") + private String userAgent; + + /** + * 异常时间 + */ + @NotNull(message = "异常时间不能为空") + private LocalDateTime exceptionTime; + /** + * 异常名 + */ + @NotNull(message = "异常名不能为空") + private String exceptionName; + /** + * 异常发生的类全名 + */ + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + /** + * 异常发生的类文件 + */ + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + /** + * 异常发生的方法名 + */ + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + */ + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + /** + * 异常的栈轨迹异常的栈轨迹 + */ + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + /** + * 异常导致的根消息 + */ + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + /** + * 异常导致的消息 + */ + @NotNull(message = "异常导致的消息不能为空") + private String exceptionMessage; + + +} diff --git a/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/package-info.java b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/package-info.java new file mode 100644 index 0000000..0d48833 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/api/package-info.java @@ -0,0 +1,4 @@ +/** + * infra API 包,定义暴露给其它模块的 API + */ +package com.yunxi.scm.module.infra.api; diff --git a/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/enums/DictTypeConstants.java b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/enums/DictTypeConstants.java new file mode 100644 index 0000000..11108a1 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/enums/DictTypeConstants.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.enums; + +/** + * Infra 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String REDIS_TIMEOUT_TYPE = "infra_redis_timeout_type"; // Redis 超时类型 + + String JOB_STATUS = "infra_job_status"; // 定时任务状态的枚举 + String JOB_LOG_STATUS = "infra_job_log_status"; // 定时任务日志状态的枚举 + + String API_ERROR_LOG_PROCESS_STATUS = "infra_api_error_log_process_status"; // API 错误日志的处理状态的枚举 + + String CONFIG_TYPE = "infra_config_type"; // 参数配置类型 + String BOOLEAN_STRING = "infra_boolean_string"; // Boolean 是否类型 + +} diff --git a/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/enums/ErrorCodeConstants.java b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..c505079 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-api/src/main/java/com/yunxi/scm/module/infra/enums/ErrorCodeConstants.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.infra.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Infra 错误码枚举类 + * + * infra 系统,使用 1-001-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 参数配置 1001000000 ========== + ErrorCode CONFIG_NOT_EXISTS = new ErrorCode(1001000001, "参数配置不存在"); + ErrorCode CONFIG_KEY_DUPLICATE = new ErrorCode(1001000002, "参数配置 key 重复"); + ErrorCode CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE = new ErrorCode(1001000003, "不能删除类型为系统内置的参数配置"); + ErrorCode CONFIG_GET_VALUE_ERROR_IF_VISIBLE = new ErrorCode(1001000004, "获取参数配置失败,原因:不允许获取不可见配置"); + + // ========== 定时任务 1001001000 ========== + ErrorCode JOB_NOT_EXISTS = new ErrorCode(1001001000, "定时任务不存在"); + ErrorCode JOB_HANDLER_EXISTS = new ErrorCode(1001001001, "定时任务的处理器已经存在"); + ErrorCode JOB_CHANGE_STATUS_INVALID = new ErrorCode(1001001002, "只允许修改为开启或者关闭状态"); + ErrorCode JOB_CHANGE_STATUS_EQUALS = new ErrorCode(1001001003, "定时任务已经处于该状态,无需修改"); + ErrorCode JOB_UPDATE_ONLY_NORMAL_STATUS = new ErrorCode(1001001004, "只有开启状态的任务,才可以修改"); + ErrorCode JOB_CRON_EXPRESSION_VALID = new ErrorCode(1001001005, "CRON 表达式不正确"); + + // ========== API 错误日志 1001002000 ========== + ErrorCode API_ERROR_LOG_NOT_FOUND = new ErrorCode(1001002000, "API 错误日志不存在"); + ErrorCode API_ERROR_LOG_PROCESSED = new ErrorCode(1001002001, "API 错误日志已处理"); + + // ========= 文件相关 1001003000================= + ErrorCode FILE_PATH_EXISTS = new ErrorCode(1001003000, "文件路径已存在"); + ErrorCode FILE_NOT_EXISTS = new ErrorCode(1001003001, "文件不存在"); + ErrorCode FILE_IS_EMPTY = new ErrorCode(1001003002, "文件为空"); + + // ========== 代码生成器 1001004000 ========== + ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1003001000, "表定义已经存在"); + ErrorCode CODEGEN_IMPORT_TABLE_NULL = new ErrorCode(1003001001, "导入的表不存在"); + ErrorCode CODEGEN_IMPORT_COLUMNS_NULL = new ErrorCode(1003001002, "导入的字段不存在"); + ErrorCode CODEGEN_TABLE_NOT_EXISTS = new ErrorCode(1003001004, "表定义不存在"); + ErrorCode CODEGEN_COLUMN_NOT_EXISTS = new ErrorCode(1003001005, "字段义不存在"); + ErrorCode CODEGEN_SYNC_COLUMNS_NULL = new ErrorCode(1003001006, "同步的字段不存在"); + ErrorCode CODEGEN_SYNC_NONE_CHANGE = new ErrorCode(1003001007, "同步失败,不存在改变"); + ErrorCode CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL = new ErrorCode(1003001008, "数据库的表注释未填写"); + ErrorCode CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL = new ErrorCode(1003001009, "数据库的表字段({})注释未填写"); + + // ========== 字典类型(测试)1001005000 ========== + ErrorCode TEST_DEMO_NOT_EXISTS = new ErrorCode(1001005000, "测试示例不存在"); + + // ========== 文件配置 1001006000 ========== + ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1001006000, "文件配置不存在"); + ErrorCode FILE_CONFIG_DELETE_FAIL_MASTER = new ErrorCode(1001006001, "该文件配置不允许删除,原因:它是主配置,删除会导致无法上传文件"); + + // ========== 数据源配置 1001007000 ========== + ErrorCode DATA_SOURCE_CONFIG_NOT_EXISTS = new ErrorCode(1001007000, "数据源配置不存在"); + ErrorCode DATA_SOURCE_CONFIG_NOT_OK = new ErrorCode(1001007001, "数据源配置不正确,无法进行连接"); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/pom.xml b/yunxi-module-infra/yunxi-module-infra-biz/pom.xml new file mode 100644 index 0000000..4582077 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/pom.xml @@ -0,0 +1,122 @@ + + + + com.yunxi.scm + yunxi-module-infra + ${revision} + + 4.0.0 + yunxi-module-infra-biz + jar + + ${project.artifactId} + + infra 模块,主要提供两块能力: + 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + + + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + com.yunxi.scm + yunxi-module-infra-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + com.baomidou + mybatis-plus-generator + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + + + com.yunxi.scm + yunxi-spring-boot-starter-job + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mq + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + + + + org.apache.velocity + velocity-engine-core + + + + cn.smallbun.screw + screw-core + + + + + com.yunxi.scm + yunxi-spring-boot-starter-monitor + + + + de.codecentric + spring-boot-admin-starter-server + + + + + com.yunxi.scm + yunxi-spring-boot-starter-file + + + + + org.springframework.boot + spring-boot-starter-websocket + + + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/file/FileApiImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/file/FileApiImpl.java new file mode 100644 index 0000000..d49ee7a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/file/FileApiImpl.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.infra.api.file; + +import com.yunxi.scm.module.infra.service.file.FileService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 文件 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class FileApiImpl implements FileApi { + + @Resource + private FileService fileService; + + @Override + public String createFile(String name, String path, byte[] content) { + return fileService.createFile(name, path, content); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiAccessLogApiImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiAccessLogApiImpl.java new file mode 100644 index 0000000..d6c37c4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiAccessLogApiImpl.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.infra.api.logger; + +import com.yunxi.scm.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import com.yunxi.scm.module.infra.service.logger.ApiAccessLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * API 访问日志的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiAccessLogApiImpl implements ApiAccessLogApi { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + apiAccessLogService.createApiAccessLog(createDTO); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiErrorLogApiImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiErrorLogApiImpl.java new file mode 100644 index 0000000..89d17d9 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/logger/ApiErrorLogApiImpl.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.infra.api.logger; + +import com.yunxi.scm.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.yunxi.scm.module.infra.service.logger.ApiErrorLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * API 访问日志的 API 接口 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiErrorLogApiImpl implements ApiErrorLogApi { + + @Resource + private ApiErrorLogService apiErrorLogService; + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + apiErrorLogService.createApiErrorLog(createDTO); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/package-info.java new file mode 100644 index 0000000..7abf151 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/api/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.infra.api; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/CodegenController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/CodegenController.java new file mode 100644 index 0000000..bb8322d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/CodegenController.java @@ -0,0 +1,141 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.ZipUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import com.yunxi.scm.module.infra.convert.codegen.CodegenConvert; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.yunxi.scm.module.infra.service.codegen.CodegenService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 代码生成器") +@RestController +@RequestMapping("/infra/codegen") +@Validated +public class CodegenController { + + @Resource + private CodegenService codegenService; + + @GetMapping("/db/table/list") + @Operation(summary = "获得数据库自带的表定义列表", description = "会过滤掉已经导入 Codegen 的表") + @Parameters({ + @Parameter(name = "dataSourceConfigId", description = "数据源配置的编号", required = true, example = "1"), + @Parameter(name = "name", description = "表名,模糊匹配", example = "yunxi"), + @Parameter(name = "comment", description = "描述,模糊匹配", example = "芋道") + }) + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult> getDatabaseTableList( + @RequestParam(value = "dataSourceConfigId") Long dataSourceConfigId, + @RequestParam(value = "name", required = false) String name, + @RequestParam(value = "comment", required = false) String comment) { + return success(codegenService.getDatabaseTableList(dataSourceConfigId, name, comment)); + } + + @GetMapping("/table/page") + @Operation(summary = "获得表定义分页") + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult> getCodeGenTablePage(@Valid CodegenTablePageReqVO pageReqVO) { + PageResult pageResult = codegenService.getCodegenTablePage(pageReqVO); + return success(CodegenConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/detail") + @Operation(summary = "获得表和字段的明细") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:query')") + public CommonResult getCodegenDetail(@RequestParam("tableId") Long tableId) { + CodegenTableDO table = codegenService.getCodegenTablePage(tableId); + List columns = codegenService.getCodegenColumnListByTableId(tableId); + // 拼装返回 + return success(CodegenConvert.INSTANCE.convert(table, columns)); + } + + @Operation(summary = "基于数据库的表结构,创建代码生成器的表和字段定义") + @PostMapping("/create-list") + @PreAuthorize("@ss.hasPermission('infra:codegen:create')") + public CommonResult> createCodegenList(@Valid @RequestBody CodegenCreateListReqVO reqVO) { + return success(codegenService.createCodegenList(getLoginUserId(), reqVO)); + } + + @Operation(summary = "更新数据库的表和字段定义") + @PutMapping("/update") + @PreAuthorize("@ss.hasPermission('infra:codegen:update')") + public CommonResult updateCodegen(@Valid @RequestBody CodegenUpdateReqVO updateReqVO) { + codegenService.updateCodegen(updateReqVO); + return success(true); + } + + @Operation(summary = "基于数据库的表结构,同步数据库的表和字段定义") + @PutMapping("/sync-from-db") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:update')") + public CommonResult syncCodegenFromDB(@RequestParam("tableId") Long tableId) { + codegenService.syncCodegenFromDB(tableId); + return success(true); + } + + @Operation(summary = "删除数据库的表和字段定义") + @DeleteMapping("/delete") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:delete')") + public CommonResult deleteCodegen(@RequestParam("tableId") Long tableId) { + codegenService.deleteCodegen(tableId); + return success(true); + } + + @Operation(summary = "预览生成代码") + @GetMapping("/preview") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:preview')") + public CommonResult> previewCodegen(@RequestParam("tableId") Long tableId) { + Map codes = codegenService.generationCodes(tableId); + return success(CodegenConvert.INSTANCE.convert(codes)); + } + + @Operation(summary = "下载生成代码") + @GetMapping("/download") + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:codegen:download')") + public void downloadCodegen(@RequestParam("tableId") Long tableId, + HttpServletResponse response) throws IOException { + // 生成代码 + Map codes = codegenService.generationCodes(tableId); + // 构建 zip 包 + String[] paths = codes.keySet().toArray(new String[0]); + ByteArrayInputStream[] ins = codes.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipUtil.zip(outputStream, paths, ins); + // 输出 + ServletUtils.writeAttachment(response, "codegen.zip", outputStream.toByteArray()); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java new file mode 100644 index 0000000..7eeab99 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenCreateListReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 基于数据库的表结构,创建代码生成器的表和字段定义 Request VO") +@Data +public class CodegenCreateListReqVO { + + @Schema(description = "数据源配置的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数据源配置的编号不能为空") + private Long dataSourceConfigId; + + @Schema(description = "表名数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1, 2, 3]") + @NotNull(message = "表名数组不能为空") + private List tableNames; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java new file mode 100644 index 0000000..e27b036 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenDetailRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo; + +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 代码生成表和字段的明细 Response VO") +@Data +public class CodegenDetailRespVO { + + @Schema(description = "表定义") + private CodegenTableRespVO table; + + @Schema(description = "字段定义") + private List columns; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java new file mode 100644 index 0000000..344b75c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenPreviewRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 代码生成预览 Response VO,注意,每个文件都是一个该对象") +@Data +public class CodegenPreviewRespVO { + + @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "java/com.yunxi.scm/adminserver/modules/system/controller/test/SysTestDemoController.java") + private String filePath; + + @Schema(description = "代码", requiredMode = Schema.RequiredMode.REQUIRED, example = "Hello World") + private String code; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java new file mode 100644 index 0000000..bf34126 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.column.CodegenColumnBaseVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.CodegenTableBaseVO; +import com.yunxi.scm.module.infra.enums.codegen.CodegenSceneEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 代码生成表和字段的修改 Request VO") +@Data +public class CodegenUpdateReqVO { + + @Valid // 校验内嵌的字段 + @NotNull(message = "表定义不能为空") + private Table table; + + @Valid // 校验内嵌的字段 + @NotNull(message = "字段定义不能为空") + private List columns; + + @Schema(description = "更新表定义") + @Data + @EqualsAndHashCode(callSuper = true) + @ToString(callSuper = true) + @Valid + public static class Table extends CodegenTableBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @AssertTrue(message = "上级菜单不能为空,请前往 [修改生成配置 -> 生成信息] 界面,设置“上级菜单”字段") + public boolean isParentMenuIdValid() { + // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的 + return ObjectUtil.notEqual(getScene(), CodegenSceneEnum.ADMIN.getScene()) + || getParentMenuId() != null; + } + + } + + @Schema(description = "更新表定义") + @Data + @EqualsAndHashCode(callSuper = true) + @ToString(callSuper = true) + public static class Column extends CodegenColumnBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/column/CodegenColumnBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/column/CodegenColumnBaseVO.java new file mode 100644 index 0000000..5db5597 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/column/CodegenColumnBaseVO.java @@ -0,0 +1,85 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo.column; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 代码生成字段定义 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class CodegenColumnBaseVO { + + @Schema(description = "表编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "表编号不能为空") + private Long tableId; + + @Schema(description = "字段名", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_age") + @NotNull(message = "字段名不能为空") + private String columnName; + + @Schema(description = "字段类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "int(11)") + @NotNull(message = "字段类型不能为空") + private String dataType; + + @Schema(description = "字段描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "年龄") + @NotNull(message = "字段描述不能为空") + private String columnComment; + + @Schema(description = "是否允许为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否允许为空不能为空") + private Boolean nullable; + + @Schema(description = "是否主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否主键不能为空") + private Boolean primaryKey; + + @Schema(description = "是否自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否自增不能为空") + private String autoIncrement; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "排序不能为空") + private Integer ordinalPosition; + + @Schema(description = "Java 属性类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "userAge") + @NotNull(message = "Java 属性类型不能为空") + private String javaType; + + @Schema(description = "Java 属性名", requiredMode = Schema.RequiredMode.REQUIRED, example = "Integer") + @NotNull(message = "Java 属性名不能为空") + private String javaField; + + @Schema(description = "字典类型", example = "sys_gender") + private String dictType; + + @Schema(description = "数据示例", example = "1024") + private String example; + + @Schema(description = "是否为 Create 创建操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否为 Create 创建操作的字段不能为空") + private Boolean createOperation; + + @Schema(description = "是否为 Update 更新操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否为 Update 更新操作的字段不能为空") + private Boolean updateOperation; + + @Schema(description = "是否为 List 查询操作的字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否为 List 查询操作的字段不能为空") + private Boolean listOperation; + + @Schema(description = "List 查询操作的条件类型,参见 CodegenColumnListConditionEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "LIKE") + @NotNull(message = "List 查询操作的条件类型不能为空") + private String listOperationCondition; + + @Schema(description = "是否为 List 查询操作的返回字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否为 List 查询操作的返回字段不能为空") + private Boolean listOperationResult; + + @Schema(description = "显示类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "input") + @NotNull(message = "显示类型不能为空") + private String htmlType; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java new file mode 100644 index 0000000..91a23d2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/column/CodegenColumnRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo.column; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 代码生成字段定义 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CodegenColumnRespVO extends CodegenColumnBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTableBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTableBaseVO.java new file mode 100644 index 0000000..07dd520 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTableBaseVO.java @@ -0,0 +1,61 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo.table; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 代码生成 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class CodegenTableBaseVO { + + @Schema(description = "生成场景,参见 CodegenSceneEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "导入类型不能为空") + private Integer scene; + + @Schema(description = "表名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi") + @NotNull(message = "表名称不能为空") + private String tableName; + + @Schema(description = "表描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "表描述不能为空") + private String tableComment; + + @Schema(description = "备注", example = "我是备注") + private String remark; + + @Schema(description = "模块名", requiredMode = Schema.RequiredMode.REQUIRED, example = "system") + @NotNull(message = "模块名不能为空") + private String moduleName; + + @Schema(description = "业务名", requiredMode = Schema.RequiredMode.REQUIRED, example = "codegen") + @NotNull(message = "业务名不能为空") + private String businessName; + + @Schema(description = "类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "CodegenTable") + @NotNull(message = "类名称不能为空") + private String className; + + @Schema(description = "类描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "代码生成器的表定义") + @NotNull(message = "类描述不能为空") + private String classComment; + + @Schema(description = "作者", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + @NotNull(message = "作者不能为空") + private String author; + + @Schema(description = "模板类型,参见 CodegenTemplateTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模板类型不能为空") + private Integer templateType; + + @Schema(description = "前端类型,参见 CodegenFrontTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + @NotNull(message = "前端类型不能为空") + private Integer frontType; + + @Schema(description = "父菜单编号", example = "1024") + private Long parentMenuId; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java new file mode 100644 index 0000000..0dd796a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTablePageReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo.table; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 表定义分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CodegenTablePageReqVO extends PageParam { + + @Schema(description = "表名称,模糊匹配", example = "yunxi") + private String tableName; + + @Schema(description = "表描述,模糊匹配", example = "芋道") + private String tableComment; + + @Schema(description = "实体,模糊匹配", example = "Yunxi") + private String className; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java new file mode 100644 index 0000000..8776d47 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/CodegenTableRespVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo.table; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 代码生成表定义 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CodegenTableRespVO extends CodegenTableBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer dataSourceConfigId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java new file mode 100644 index 0000000..f53d25b --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/codegen/vo/table/DatabaseTableRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.infra.controller.admin.codegen.vo.table; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 数据库的表定义 Response VO") +@Data +public class DatabaseTableRespVO { + + @Schema(description = "表名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yuanma") + private String name; + + @Schema(description = "表描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String comment; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/ConfigController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/ConfigController.java new file mode 100644 index 0000000..8e6f924 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/ConfigController.java @@ -0,0 +1,105 @@ +package com.yunxi.scm.module.infra.controller.admin.config; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.infra.controller.admin.config.vo.*; +import com.yunxi.scm.module.infra.convert.config.ConfigConvert; +import com.yunxi.scm.module.infra.dal.dataobject.config.ConfigDO; +import com.yunxi.scm.module.infra.enums.ErrorCodeConstants; +import com.yunxi.scm.module.infra.service.config.ConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 参数配置") +@RestController +@RequestMapping("/infra/config") +@Validated +public class ConfigController { + + @Resource + private ConfigService configService; + + @PostMapping("/create") + @Operation(summary = "创建参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:create')") + public CommonResult createConfig(@Valid @RequestBody ConfigCreateReqVO reqVO) { + return success(configService.createConfig(reqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:update')") + public CommonResult updateConfig(@Valid @RequestBody ConfigUpdateReqVO reqVO) { + configService.updateConfig(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除参数配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:config:delete')") + public CommonResult deleteConfig(@RequestParam("id") Long id) { + configService.deleteConfig(id); + return success(true); + } + + @GetMapping(value = "/get") + @Operation(summary = "获得参数配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:config:query')") + public CommonResult getConfig(@RequestParam("id") Long id) { + return success(ConfigConvert.INSTANCE.convert(configService.getConfig(id))); + } + + @GetMapping(value = "/get-value-by-key") + @Operation(summary = "根据参数键名查询参数值", description = "不可见的配置,不允许返回给前端") + @Parameter(name = "key", description = "参数键", required = true, example = "yunai.biz.username") + public CommonResult getConfigKey(@RequestParam("key") String key) { + ConfigDO config = configService.getConfigByKey(key); + if (config == null) { + return success(null); + } + if (!config.getVisible()) { + throw exception(ErrorCodeConstants.CONFIG_GET_VALUE_ERROR_IF_VISIBLE); + } + return success(config.getValue()); + } + + @GetMapping("/page") + @Operation(summary = "获取参数配置分页") + @PreAuthorize("@ss.hasPermission('infra:config:query')") + public CommonResult> getConfigPage(@Valid ConfigPageReqVO reqVO) { + PageResult page = configService.getConfigPage(reqVO); + return success(ConfigConvert.INSTANCE.convertPage(page)); + } + + @GetMapping("/export") + @Operation(summary = "导出参数配置") + @PreAuthorize("@ss.hasPermission('infra:config:export')") + @OperateLog(type = EXPORT) + public void exportConfig(@Valid ConfigExportReqVO reqVO, + HttpServletResponse response) throws IOException { + List list = configService.getConfigList(reqVO); + // 拼接数据 + List datas = ConfigConvert.INSTANCE.convertList(list); + // 输出 + ExcelUtils.write(response, "参数配置.xls", "数据", ConfigExcelVO.class, datas); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigBaseVO.java new file mode 100644 index 0000000..5262a1f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigBaseVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.infra.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 参数配置 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class ConfigBaseVO { + + @Schema(description = "参数分组", requiredMode = Schema.RequiredMode.REQUIRED, example = "biz") + @NotEmpty(message = "参数分组不能为空") + @Size(max = 50, message = "参数名称不能超过50个字符") + private String category; + + @Schema(description = "参数名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "数据库名") + @NotBlank(message = "参数名称不能为空") + @Size(max = 100, message = "参数名称不能超过100个字符") + private String name; + + @Schema(description = "参数键值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotBlank(message = "参数键值不能为空") + @Size(max = 500, message = "参数键值长度不能超过500个字符") + private String value; + + @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否可见不能为空") + private Boolean visible; + + @Schema(description = "备注", example = "备注一下很帅气!") + private String remark; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigCreateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigCreateReqVO.java new file mode 100644 index 0000000..26a76b4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 参数配置创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ConfigCreateReqVO extends ConfigBaseVO { + + @Schema(description = "参数键名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunai.db.username") + @NotBlank(message = "参数键名长度不能为空") + @Size(max = 100, message = "参数键名长度不能超过100个字符") + private String key; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigExcelVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigExcelVO.java new file mode 100644 index 0000000..3da6a45 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigExcelVO.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.infra.controller.admin.config.vo; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 参数配置 Excel 导出响应 VO + */ +@Data +public class ConfigExcelVO { + + @ExcelProperty("参数配置序号") + private Long id; + + @ExcelProperty("参数键名") + private String configKey; + + @ExcelProperty("参数分类") + private String category; + + @ExcelProperty("参数名称") + private String name; + + @ExcelProperty("参数键值") + private String value; + + @ExcelProperty(value = "参数类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CONFIG_TYPE) + private Integer type; + + @ExcelProperty(value = "是否可见", converter = DictConvert.class) + @DictFormat(DictTypeConstants.BOOLEAN_STRING) + private Boolean visible; + + @ExcelProperty("备注") + private String remark; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigExportReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigExportReqVO.java new file mode 100644 index 0000000..bcd0e8c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigExportReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.infra.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 参数配置导出 Request VO") +@Data +public class ConfigExportReqVO { + + @Schema(description = "参数名称", example = "模糊匹配") + private String name; + + @Schema(description = "参数键名,模糊匹配", example = "yunai.db.username") + private String key; + + @Schema(description = "参数类型,参见 SysConfigTypeEnum 枚举", example = "1") + private Integer type; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigPageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigPageReqVO.java new file mode 100644 index 0000000..0385338 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigPageReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.infra.controller.admin.config.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 参数配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ConfigPageReqVO extends PageParam { + + @Schema(description = "数据源名称,模糊匹配", example = "名称") + private String name; + + @Schema(description = "参数键名,模糊匹配", example = "yunai.db.username") + private String key; + + @Schema(description = "参数类型,参见 SysConfigTypeEnum 枚举", example = "1") + private Integer type; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigRespVO.java new file mode 100644 index 0000000..0f63cbf --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigRespVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.infra.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 参数配置信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class ConfigRespVO extends ConfigBaseVO { + + @Schema(description = "参数配置序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "参数键名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunai.db.username") + @NotBlank(message = "参数键名长度不能为空") + @Size(max = 100, message = "参数键名长度不能超过100个字符") + private String key; + + @Schema(description = "参数类型,参见 SysConfigTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigUpdateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigUpdateReqVO.java new file mode 100644 index 0000000..650189a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/config/vo/ConfigUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.controller.admin.config.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 参数配置创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ConfigUpdateReqVO extends ConfigBaseVO { + + @Schema(description = "参数配置序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "参数配置编号不能为空") + private Long id; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/DataSourceConfigController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/DataSourceConfigController.java new file mode 100644 index 0000000..b394ba1 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/DataSourceConfigController.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.module.infra.controller.admin.db; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigRespVO; +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigUpdateReqVO; +import com.yunxi.scm.module.infra.convert.db.DataSourceConfigConvert; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.yunxi.scm.module.infra.service.db.DataSourceConfigService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 数据源配置") +@RestController +@RequestMapping("/infra/data-source-config") +@Validated +public class DataSourceConfigController { + + @Resource + private DataSourceConfigService dataSourceConfigService; + + @PostMapping("/create") + @Operation(summary = "创建数据源配置") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:create')") + public CommonResult createDataSourceConfig(@Valid @RequestBody DataSourceConfigCreateReqVO createReqVO) { + return success(dataSourceConfigService.createDataSourceConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新数据源配置") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:update')") + public CommonResult updateDataSourceConfig(@Valid @RequestBody DataSourceConfigUpdateReqVO updateReqVO) { + dataSourceConfigService.updateDataSourceConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除数据源配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:data-source-config:delete')") + public CommonResult deleteDataSourceConfig(@RequestParam("id") Long id) { + dataSourceConfigService.deleteDataSourceConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得数据源配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:query')") + public CommonResult getDataSourceConfig(@RequestParam("id") Long id) { + DataSourceConfigDO dataSourceConfig = dataSourceConfigService.getDataSourceConfig(id); + return success(DataSourceConfigConvert.INSTANCE.convert(dataSourceConfig)); + } + + @GetMapping("/list") + @Operation(summary = "获得数据源配置列表") + @PreAuthorize("@ss.hasPermission('infra:data-source-config:query')") + public CommonResult> getDataSourceConfigList() { + List list = dataSourceConfigService.getDataSourceConfigList(); + return success(DataSourceConfigConvert.INSTANCE.convertList(list)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/DatabaseDocController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/DatabaseDocController.java new file mode 100644 index 0000000..6e4f1c1 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/DatabaseDocController.java @@ -0,0 +1,154 @@ +package com.yunxi.scm.module.infra.controller.admin.db; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import cn.smallbun.screw.core.Configuration; +import cn.smallbun.screw.core.engine.EngineConfig; +import cn.smallbun.screw.core.engine.EngineFileType; +import cn.smallbun.screw.core.engine.EngineTemplateType; +import cn.smallbun.screw.core.execute.DocumentationExecute; +import cn.smallbun.screw.core.process.ProcessConfig; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +@Tag(name = "管理后台 - 数据库文档") +@RestController +@RequestMapping("/infra/db-doc") +public class DatabaseDocController { + + @Resource + private DynamicDataSourceProperties dynamicDataSourceProperties; + + private static final String FILE_OUTPUT_DIR = System.getProperty("java.io.tmpdir") + File.separator + + "db-doc"; + private static final String DOC_FILE_NAME = "数据库文档"; + private static final String DOC_VERSION = "1.0.0"; + private static final String DOC_DESCRIPTION = "文档描述"; + + @GetMapping("/export-html") + @Operation(summary = "导出 html 格式的数据文档") + @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true") + public void exportHtml(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.HTML, deleteFile, response); + } + + @GetMapping("/export-word") + @Operation(summary = "导出 word 格式的数据文档") + @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true") + public void exportWord(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.WORD, deleteFile, response); + } + + @GetMapping("/export-markdown") + @Operation(summary = "导出 markdown 格式的数据文档") + @Parameter(name = "deleteFile", description = "是否删除在服务器本地生成的数据库文档", example = "true") + public void exportMarkdown(@RequestParam(defaultValue = "true") Boolean deleteFile, + HttpServletResponse response) throws IOException { + doExportFile(EngineFileType.MD, deleteFile, response); + } + + private void doExportFile(EngineFileType fileOutputType, Boolean deleteFile, + HttpServletResponse response) throws IOException { + String docFileName = DOC_FILE_NAME + "_" + IdUtil.fastSimpleUUID(); + String filePath = doExportFile(fileOutputType, docFileName); + String downloadFileName = DOC_FILE_NAME + fileOutputType.getFileSuffix(); //下载后的文件名 + try { + // 读取,返回 + ServletUtils.writeAttachment(response, downloadFileName, FileUtil.readBytes(filePath)); + } finally { + handleDeleteFile(deleteFile, filePath); + } + } + + /** + * 输出文件,返回文件路径 + * + * @param fileOutputType 文件类型 + * @param fileName 文件名, 无需 ".docx" 等文件后缀 + * @return 生成的文件所在路径 + */ + private String doExportFile(EngineFileType fileOutputType, String fileName) { + try (HikariDataSource dataSource = buildDataSource()) { + // 创建 screw 的配置 + Configuration config = Configuration.builder() + .version(DOC_VERSION) // 版本 + .description(DOC_DESCRIPTION) // 描述 + .dataSource(dataSource) // 数据源 + .engineConfig(buildEngineConfig(fileOutputType, fileName)) // 引擎配置 + .produceConfig(buildProcessConfig()) // 处理配置 + .build(); + + // 执行 screw,生成数据库文档 + new DocumentationExecute(config).execute(); + + return FILE_OUTPUT_DIR + File.separator + fileName + fileOutputType.getFileSuffix(); + } + } + + private void handleDeleteFile(Boolean deleteFile, String filePath) { + if (!deleteFile) { + return; + } + FileUtil.del(filePath); + } + + /** + * 创建数据源 + */ + // TODO 芋艿:screw 暂时不支持 druid,尴尬 + private HikariDataSource buildDataSource() { + // 获得 DataSource 数据源,目前只支持首个 + String primary = dynamicDataSourceProperties.getPrimary(); + DataSourceProperty dataSourceProperty = dynamicDataSourceProperties.getDatasource().get(primary); + // 创建 HikariConfig 配置类 + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(dataSourceProperty.getUrl()); + hikariConfig.setUsername(dataSourceProperty.getUsername()); + hikariConfig.setPassword(dataSourceProperty.getPassword()); + hikariConfig.addDataSourceProperty("useInformationSchema", "true"); // 设置可以获取 tables remarks 信息 + // 创建数据源 + return new HikariDataSource(hikariConfig); + } + + /** + * 创建 screw 的引擎配置 + */ + private static EngineConfig buildEngineConfig(EngineFileType fileOutputType, String docFileName) { + return EngineConfig.builder() + .fileOutputDir(FILE_OUTPUT_DIR) // 生成文件路径 + .openOutputDir(false) // 打开目录 + .fileType(fileOutputType) // 文件类型 + .produceType(EngineTemplateType.velocity) // 文件类型 + .fileName(docFileName) // 自定义文件名称 + .build(); + } + + /** + * 创建 screw 的处理配置,一般可忽略 + * 指定生成逻辑、当存在指定表、指定表前缀、指定表后缀时,将生成指定表,其余表不生成、并跳过忽略表配置 + */ + private static ProcessConfig buildProcessConfig() { + return ProcessConfig.builder() + .ignoreTablePrefix(Arrays.asList("QRTZ_", "ACT_")) // 忽略表前缀 + .build(); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigBaseVO.java new file mode 100644 index 0000000..cabda50 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigBaseVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.infra.controller.admin.db.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +/** +* 数据源配置 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DataSourceConfigBaseVO { + + @Schema(description = "数据源名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") + @NotNull(message = "数据源名称不能为空") + private String name; + + @Schema(description = "数据源连接", requiredMode = Schema.RequiredMode.REQUIRED, example = "jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro") + @NotNull(message = "数据源连接不能为空") + private String url; + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "root") + @NotNull(message = "用户名不能为空") + private String username; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigCreateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigCreateReqVO.java new file mode 100644 index 0000000..b884c04 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigCreateReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.infra.controller.admin.db.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 数据源配置创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DataSourceConfigCreateReqVO extends DataSourceConfigBaseVO { + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotNull(message = "密码不能为空") + private String password; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java new file mode 100644 index 0000000..a12f70c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.controller.admin.db.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 数据源配置 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DataSourceConfigRespVO extends DataSourceConfigBaseVO { + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigUpdateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigUpdateReqVO.java new file mode 100644 index 0000000..095715c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/db/vo/DataSourceConfigUpdateReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.infra.controller.admin.db.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 数据源配置更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DataSourceConfigUpdateReqVO extends DataSourceConfigBaseVO { + + @Schema(description = "主键编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "主键编号不能为空") + private Long id; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotNull(message = "密码不能为空") + private String password; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileConfigController.http b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileConfigController.http new file mode 100644 index 0000000..9424f73 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileConfigController.http @@ -0,0 +1,45 @@ +### 请求 /infra/file-config/create 接口 => 成功 +POST {{baseUrl}}/infra/file-config/create +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "name": "S3 - 七牛云", + "remark": "", + "storage": 20, + "config": { + "accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8", + "accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP", + "bucket": "ruoyi-vue-pro", + "endpoint": "s3-cn-south-1.qiniucs.com", + "domain": "http://test.yunxi.iocoder.cn", + "region": "oss-cn-beijing" + } +} + +### 请求 /infra/file-config/update 接口 => 成功 +PUT {{baseUrl}}/infra/file-config/update +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} + +{ + "id": 2, + "name": "S3 - 七牛云", + "remark": "", + "config": { + "accessKey": "b7yvuhBSAGjmtPhMFcn9iMOxUOY_I06cA_p0ZUx8", + "accessSecret": "kXM1l5ia1RvSX3QaOEcwI3RLz3Y2rmNszWonKZtP", + "bucket": "ruoyi-vue-pro", + "endpoint": "s3-cn-south-1.qiniucs.com", + "domain": "http://test.yunxi.iocoder.cn", + "region": "oss-cn-beijing" + } +} + +### 请求 /infra/file-config/test 接口 => 成功 +GET {{baseUrl}}/infra/file-config/test?id=2 +Content-Type: application/json +tenant-id: {{adminTenentId}} +Authorization: Bearer {{token}} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileConfigController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileConfigController.java new file mode 100644 index 0000000..c150358 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileConfigController.java @@ -0,0 +1,89 @@ +package com.yunxi.scm.module.infra.controller.admin.file; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigRespVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import com.yunxi.scm.module.infra.convert.file.FileConfigConvert; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileConfigDO; +import com.yunxi.scm.module.infra.service.file.FileConfigService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 文件配置") +@RestController +@RequestMapping("/infra/file-config") +@Validated +public class FileConfigController { + + @Resource + private FileConfigService fileConfigService; + + @PostMapping("/create") + @Operation(summary = "创建文件配置") + @PreAuthorize("@ss.hasPermission('infra:file-config:create')") + public CommonResult createFileConfig(@Valid @RequestBody FileConfigCreateReqVO createReqVO) { + return success(fileConfigService.createFileConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新文件配置") + @PreAuthorize("@ss.hasPermission('infra:file-config:update')") + public CommonResult updateFileConfig(@Valid @RequestBody FileConfigUpdateReqVO updateReqVO) { + fileConfigService.updateFileConfig(updateReqVO); + return success(true); + } + + @PutMapping("/update-master") + @Operation(summary = "更新文件配置为 Master") + @PreAuthorize("@ss.hasPermission('infra:file-config:update')") + public CommonResult updateFileConfigMaster(@RequestParam("id") Long id) { + fileConfigService.updateFileConfigMaster(id); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除文件配置") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:file-config:delete')") + public CommonResult deleteFileConfig(@RequestParam("id") Long id) { + fileConfigService.deleteFileConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得文件配置") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult getFileConfig(@RequestParam("id") Long id) { + FileConfigDO fileConfig = fileConfigService.getFileConfig(id); + return success(FileConfigConvert.INSTANCE.convert(fileConfig)); + } + + @GetMapping("/page") + @Operation(summary = "获得文件配置分页") + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult> getFileConfigPage(@Valid FileConfigPageReqVO pageVO) { + PageResult pageResult = fileConfigService.getFileConfigPage(pageVO); + return success(FileConfigConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/test") + @Operation(summary = "测试文件配置是否正确") + @PreAuthorize("@ss.hasPermission('infra:file-config:query')") + public CommonResult testFileConfig(@RequestParam("id") Long id) throws Exception { + String url = fileConfigService.testFileConfig(id); + return success(url); + } +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileController.java new file mode 100644 index 0000000..db4a84e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/FileController.java @@ -0,0 +1,92 @@ +package com.yunxi.scm.module.infra.controller.admin.file; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.file.FileRespVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.file.FileUploadReqVO; +import com.yunxi.scm.module.infra.convert.file.FileConvert; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileDO; +import com.yunxi.scm.module.infra.service.file.FileService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 文件存储") +@RestController +@RequestMapping("/infra/file") +@Validated +@Slf4j +public class FileController { + + @Resource + private FileService fileService; + + @PostMapping("/upload") + @Operation(summary = "上传文件") + @OperateLog(logArgs = false) // 上传文件,没有记录操作日志的必要 + public CommonResult uploadFile(FileUploadReqVO uploadReqVO) throws Exception { + MultipartFile file = uploadReqVO.getFile(); + String path = uploadReqVO.getPath(); + return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除文件") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:file:delete')") + public CommonResult deleteFile(@RequestParam("id") Long id) throws Exception { + fileService.deleteFile(id); + return success(true); + } + + @GetMapping("/{configId}/get/**") + @PermitAll + @Operation(summary = "下载文件") + @Parameter(name = "configId", description = "配置编号", required = true) + public void getFileContent(HttpServletRequest request, + HttpServletResponse response, + @PathVariable("configId") Long configId) throws Exception { + // 获取请求的路径 + String path = StrUtil.subAfter(request.getRequestURI(), "/get/", false); + if (StrUtil.isEmpty(path)) { + throw new IllegalArgumentException("结尾的 path 路径必须传递"); + } + + // 读取内容 + byte[] content = fileService.getFileContent(configId, path); + if (content == null) { + log.warn("[getFileContent][configId({}) path({}) 文件不存在]", configId, path); + response.setStatus(HttpStatus.NOT_FOUND.value()); + return; + } + ServletUtils.writeAttachment(response, path, content); + } + + @GetMapping("/page") + @Operation(summary = "获得文件分页") + @PreAuthorize("@ss.hasPermission('infra:file:query')") + public CommonResult> getFilePage(@Valid FilePageReqVO pageVO) { + PageResult pageResult = fileService.getFilePage(pageVO); + return success(FileConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigBaseVO.java new file mode 100644 index 0000000..4940594 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigBaseVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.infra.controller.admin.file.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 文件配置 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class FileConfigBaseVO { + + @Schema(description = "配置名", requiredMode = Schema.RequiredMode.REQUIRED, example = "S3 - 阿里云") + @NotNull(message = "配置名不能为空") + private String name; + + @Schema(description = "备注", example = "我是备注") + private String remark; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigCreateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigCreateReqVO.java new file mode 100644 index 0000000..e231d25 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigCreateReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.infra.controller.admin.file.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 文件配置创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FileConfigCreateReqVO extends FileConfigBaseVO { + + @Schema(description = "存储器,参见 FileStorageEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "存储器不能为空") + private Integer storage; + + @Schema(description = "存储配置,配置是动态参数,所以使用 Map 接收", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "存储配置不能为空") + private Map config; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java new file mode 100644 index 0000000..9968f2b --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigPageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.infra.controller.admin.file.vo.config; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 文件配置分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FileConfigPageReqVO extends PageParam { + + @Schema(description = "配置名", example = "S3 - 阿里云") + private String name; + + @Schema(description = "存储器", example = "1") + private Integer storage; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java new file mode 100644 index 0000000..c0d6b36 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigRespVO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.infra.controller.admin.file.vo.config; + +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 文件配置 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FileConfigRespVO extends FileConfigBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "存储器,参见 FileStorageEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "存储器不能为空") + private Integer storage; + + @Schema(description = "是否为主配置", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否为主配置不能为空") + private Boolean master; + + @Schema(description = "存储配置", requiredMode = Schema.RequiredMode.REQUIRED) + private FileClientConfig config; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigUpdateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigUpdateReqVO.java new file mode 100644 index 0000000..51bae14 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/config/FileConfigUpdateReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.infra.controller.admin.file.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 文件配置更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FileConfigUpdateReqVO extends FileConfigBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "存储配置,配置是动态参数,所以使用 Map 接收", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "存储配置不能为空") + private Map config; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FilePageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FilePageReqVO.java new file mode 100644 index 0000000..ec98f0e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FilePageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.infra.controller.admin.file.vo.file; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 文件分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class FilePageReqVO extends PageParam { + + @Schema(description = "文件路径,模糊匹配", example = "yunxi") + private String path; + + @Schema(description = "文件类型,模糊匹配", example = "jpg") + private String type; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FileRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FileRespVO.java new file mode 100644 index 0000000..569c4b2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FileRespVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.infra.controller.admin.file.vo.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 文件 Response VO,不返回 content 字段,太大") +@Data +public class FileRespVO { + + @Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "11") + private Long configId; + + @Schema(description = "文件路径", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi.jpg") + private String path; + + @Schema(description = "原文件名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi.jpg") + private String name; + + @Schema(description = "文件 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yunxi.jpg") + private String url; + + @Schema(description = "文件MIME类型", example = "application/octet-stream") + private String type; + + @Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer size; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java new file mode 100644 index 0000000..56fdfb7 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.controller.admin.file.vo.file; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 上传文件 Request VO") +@Data +public class FileUploadReqVO { + + @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件附件不能为空") + private MultipartFile file; + + @Schema(description = "文件附件", example = "yunxiyuanma.png") + private String path; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/JobController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/JobController.java new file mode 100644 index 0000000..bff1951 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/JobController.java @@ -0,0 +1,145 @@ +package com.yunxi.scm.module.infra.controller.admin.job; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.framework.quartz.core.util.CronUtils; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.*; +import com.yunxi.scm.module.infra.convert.job.JobConvert; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobDO; +import com.yunxi.scm.module.infra.service.job.JobService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import org.quartz.SchedulerException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 定时任务") +@RestController +@RequestMapping("/infra/job") +@Validated +public class JobController { + + @Resource + private JobService jobService; + + @PostMapping("/create") + @Operation(summary = "创建定时任务") + @PreAuthorize("@ss.hasPermission('infra:job:create')") + public CommonResult createJob(@Valid @RequestBody JobCreateReqVO createReqVO) + throws SchedulerException { + return success(jobService.createJob(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新定时任务") + @PreAuthorize("@ss.hasPermission('infra:job:update')") + public CommonResult updateJob(@Valid @RequestBody JobUpdateReqVO updateReqVO) + throws SchedulerException { + jobService.updateJob(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新定时任务的状态") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "status", description = "状态", required = true, example = "1"), + }) + @PreAuthorize("@ss.hasPermission('infra:job:update')") + public CommonResult updateJobStatus(@RequestParam(value = "id") Long id, @RequestParam("status") Integer status) + throws SchedulerException { + jobService.updateJobStatus(id, status); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除定时任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:job:delete')") + public CommonResult deleteJob(@RequestParam("id") Long id) + throws SchedulerException { + jobService.deleteJob(id); + return success(true); + } + + @PutMapping("/trigger") + @Operation(summary = "触发定时任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:job:trigger')") + public CommonResult triggerJob(@RequestParam("id") Long id) throws SchedulerException { + jobService.triggerJob(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得定时任务") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult getJob(@RequestParam("id") Long id) { + JobDO job = jobService.getJob(id); + return success(JobConvert.INSTANCE.convert(job)); + } + + @GetMapping("/list") + @Operation(summary = "获得定时任务列表") + @Parameter(name = "ids", description = "编号列表", required = true) + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobList(@RequestParam("ids") Collection ids) { + List list = jobService.getJobList(ids); + return success(JobConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得定时任务分页") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobPage(@Valid JobPageReqVO pageVO) { + PageResult pageResult = jobService.getJobPage(pageVO); + return success(JobConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出定时任务 Excel") + @PreAuthorize("@ss.hasPermission('infra:job:export')") + @OperateLog(type = EXPORT) + public void exportJobExcel(@Valid JobExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = jobService.getJobList(exportReqVO); + // 导出 Excel + List datas = JobConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "定时任务.xls", "数据", JobExcelVO.class, datas); + } + + @GetMapping("/get_next_times") + @Operation(summary = "获得定时任务的下 n 次执行时间") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "count", description = "数量", example = "5") + }) + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobNextTimes(@RequestParam("id") Long id, + @RequestParam(value = "count", required = false, defaultValue = "5") Integer count) { + JobDO job = jobService.getJob(id); + if (job == null) { + return success(Collections.emptyList()); + } + return success(CronUtils.getNextTimes(job.getCronExpression(), count)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/JobLogController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/JobLogController.java new file mode 100644 index 0000000..3176b69 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/JobLogController.java @@ -0,0 +1,81 @@ +package com.yunxi.scm.module.infra.controller.admin.job; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogExcelVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogRespVO; +import com.yunxi.scm.module.infra.convert.job.JobLogConvert; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobLogDO; +import com.yunxi.scm.module.infra.service.job.JobLogService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 定时任务日志") +@RestController +@RequestMapping("/infra/job-log") +@Validated +public class JobLogController { + + @Resource + private JobLogService jobLogService; + + @GetMapping("/get") + @Operation(summary = "获得定时任务日志") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult getJobLog(@RequestParam("id") Long id) { + JobLogDO jobLog = jobLogService.getJobLog(id); + return success(JobLogConvert.INSTANCE.convert(jobLog)); + } + + @GetMapping("/list") + @Operation(summary = "获得定时任务日志列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobLogList(@RequestParam("ids") Collection ids) { + List list = jobLogService.getJobLogList(ids); + return success(JobLogConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得定时任务日志分页") + @PreAuthorize("@ss.hasPermission('infra:job:query')") + public CommonResult> getJobLogPage(@Valid JobLogPageReqVO pageVO) { + PageResult pageResult = jobLogService.getJobLogPage(pageVO); + return success(JobLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出定时任务日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:job:export')") + @OperateLog(type = EXPORT) + public void exportJobLogExcel(@Valid JobLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = jobLogService.getJobLogList(exportReqVO); + // 导出 Excel + List datas = JobLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "任务日志.xls", "数据", JobLogExcelVO.class, datas); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobBaseVO.java new file mode 100644 index 0000000..1af4a5c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobBaseVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.job; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 定时任务 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class JobBaseVO { + + @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试任务") + @NotNull(message = "任务名称不能为空") + private String name; + + @Schema(description = "处理器的参数", example = "yunxi") + private String handlerParam; + + @Schema(description = "CRON 表达式", requiredMode = Schema.RequiredMode.REQUIRED, example = "0/10 * * * * ? *") + @NotNull(message = "CRON 表达式不能为空") + private String cronExpression; + + @Schema(description = "重试次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + @NotNull(message = "重试次数不能为空") + private Integer retryCount; + + @Schema(description = "重试间隔", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + @NotNull(message = "重试间隔不能为空") + private Integer retryInterval; + + @Schema(description = "监控超时时间", example = "1000") + private Integer monitorTimeout; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobCreateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobCreateReqVO.java new file mode 100644 index 0000000..07a0bb3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.job; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 定时任务创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobCreateReqVO extends JobBaseVO { + + @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") + @NotNull(message = "处理器的名字不能为空") + private String handlerName; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobExcelVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobExcelVO.java new file mode 100644 index 0000000..b55ada3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobExcelVO.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.job; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 定时任务 Excel VO + * + * @author 芋道源码 + */ +@Data +public class JobExcelVO { + + @ExcelProperty("任务编号") + private Long id; + + @ExcelProperty("任务名称") + private String name; + + @ExcelProperty(value = "任务状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.JOB_STATUS) + private Integer status; + + @ExcelProperty("处理器的名字") + private String handlerName; + + @ExcelProperty("处理器的参数") + private String handlerParam; + + @ExcelProperty("CRON 表达式") + private String cronExpression; + + @ExcelProperty("最后一次执行的开始时间") + private LocalDateTime executeBeginTime; + + @ExcelProperty("最后一次执行的结束时间") + private LocalDateTime executeEndTime; + + @ExcelProperty("上一次触发时间") + private LocalDateTime firePrevTime; + + @ExcelProperty("下一次触发时间") + private LocalDateTime fireNextTime; + + @ExcelProperty("监控超时时间") + private Integer monitorTimeout; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobExportReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobExportReqVO.java new file mode 100644 index 0000000..dc711bc --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobExportReqVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.job; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 定时任务 Excel 导出 Request VO-参数和 JobPageReqVO 是一致的") +@Data +public class JobExportReqVO { + + @Schema(description = "任务名称-模糊匹配", example = "测试任务") + private String name; + + @Schema(description = "任务状态-参见 JobStatusEnum 枚举", example = "1") + private Integer status; + + @Schema(description = "处理器的名字-模糊匹配", example = "UserSessionTimeoutJob") + private String handlerName; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobPageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobPageReqVO.java new file mode 100644 index 0000000..b26bd5a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobPageReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.job; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 定时任务分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobPageReqVO extends PageParam { + + @Schema(description = "任务名称,模糊匹配", example = "测试任务") + private String name; + + @Schema(description = "任务状态,参见 JobStatusEnum 枚举", example = "1") + private Integer status; + + @Schema(description = "处理器的名字,模糊匹配", example = "sysUserSessionTimeoutJob") + private String handlerName; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobRespVO.java new file mode 100644 index 0000000..6a2c645 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobRespVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.job; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 定时任务 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobRespVO extends JobBaseVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") + @NotNull(message = "处理器的名字不能为空") + private String handlerName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobUpdateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobUpdateReqVO.java new file mode 100644 index 0000000..05dbceb --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/job/JobUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.job; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 定时任务更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobUpdateReqVO extends JobBaseVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "任务编号不能为空") + private Long id; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogBaseVO.java new file mode 100644 index 0000000..638ac0e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogBaseVO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* 定时任务日志 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class JobLogBaseVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "任务编号不能为空") + private Long jobId; + + @Schema(description = "处理器的名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "sysUserSessionTimeoutJob") + @NotNull(message = "处理器的名字不能为空") + private String handlerName; + + @Schema(description = "处理器的参数", example = "yunxi") + private String handlerParam; + + @Schema(description = "第几次执行", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "第几次执行不能为空") + private Integer executeIndex; + + @Schema(description = "开始执行时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开始执行时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime beginTime; + + @Schema(description = "结束执行时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endTime; + + @Schema(description = "执行时长", example = "123") + private Integer duration; + + @Schema(description = "任务状态,参见 JobLogStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "任务状态不能为空") + private Integer status; + + @Schema(description = "结果数据", example = "执行成功") + private String result; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogExcelVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogExcelVO.java new file mode 100644 index 0000000..a6a1d37 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogExcelVO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.log; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 定时任务 Excel VO + * + * @author 芋艿 + */ +@Data +public class JobLogExcelVO { + + @ExcelProperty("日志编号") + private Long id; + + @ExcelProperty("任务编号") + private Long jobId; + + @ExcelProperty("处理器的名字") + private String handlerName; + + @ExcelProperty("处理器的参数") + private String handlerParam; + + @ExcelProperty("第几次执行") + private Integer executeIndex; + + @ExcelProperty("开始执行时间") + private LocalDateTime beginTime; + + @ExcelProperty("结束执行时间") + private LocalDateTime endTime; + + @ExcelProperty("执行时长") + private Integer duration; + + @ExcelProperty(value = "任务状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.JOB_STATUS) + private Integer status; + + @ExcelProperty("结果数据") + private String result; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogExportReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogExportReqVO.java new file mode 100644 index 0000000..2f9363f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogExportReqVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 定时任务 Excel 导出 Request VO,参数和 JobLogPageReqVO 是一致的") +@Data +public class JobLogExportReqVO { + + @Schema(description = "任务编号", example = "10") + private Long jobId; + + @Schema(description = "处理器的名字,模糊匹配") + private String handlerName; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "开始执行时间") + private LocalDateTime beginTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "结束执行时间") + private LocalDateTime endTime; + + @Schema(description = "任务状态,参见 JobLogStatusEnum 枚举") + private Integer status; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java new file mode 100644 index 0000000..752b028 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogPageReqVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.log; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 定时任务日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobLogPageReqVO extends PageParam { + + @Schema(description = "任务编号", example = "10") + private Long jobId; + + @Schema(description = "处理器的名字,模糊匹配") + private String handlerName; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "开始执行时间") + private LocalDateTime beginTime; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "结束执行时间") + private LocalDateTime endTime; + + @Schema(description = "任务状态,参见 JobLogStatusEnum 枚举") + private Integer status; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogRespVO.java new file mode 100644 index 0000000..fc9a19b --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/job/vo/log/JobLogRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.infra.controller.admin.job.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 定时任务日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class JobLogRespVO extends JobLogBaseVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/ApiAccessLogController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/ApiAccessLogController.java new file mode 100644 index 0000000..ba41ffe --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/ApiAccessLogController.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.infra.controller.admin.logger; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExcelVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogRespVO; +import com.yunxi.scm.module.infra.convert.logger.ApiAccessLogConvert; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import com.yunxi.scm.module.infra.service.logger.ApiAccessLogService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - API 访问日志") +@RestController +@RequestMapping("/infra/api-access-log") +@Validated +public class ApiAccessLogController { + + @Resource + private ApiAccessLogService apiAccessLogService; + + @GetMapping("/page") + @Operation(summary = "获得API 访问日志分页") + @PreAuthorize("@ss.hasPermission('infra:api-access-log:query')") + public CommonResult> getApiAccessLogPage(@Valid ApiAccessLogPageReqVO pageVO) { + PageResult pageResult = apiAccessLogService.getApiAccessLogPage(pageVO); + return success(ApiAccessLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出API 访问日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:api-access-log:export')") + @OperateLog(type = EXPORT) + public void exportApiAccessLogExcel(@Valid ApiAccessLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = apiAccessLogService.getApiAccessLogList(exportReqVO); + // 导出 Excel + List datas = ApiAccessLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "API 访问日志.xls", "数据", ApiAccessLogExcelVO.class, datas); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/ApiErrorLogController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/ApiErrorLogController.java new file mode 100644 index 0000000..0c4f4b4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/ApiErrorLogController.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.infra.controller.admin.logger; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExcelVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogRespVO; +import com.yunxi.scm.module.infra.convert.logger.ApiErrorLogConvert; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import com.yunxi.scm.module.infra.service.logger.ApiErrorLogService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - API 错误日志") +@RestController +@RequestMapping("/infra/api-error-log") +@Validated +public class ApiErrorLogController { + + @Resource + private ApiErrorLogService apiErrorLogService; + + @PutMapping("/update-status") + @Operation(summary = "更新 API 错误日志的状态") + @Parameters({ + @Parameter(name = "id", description = "编号", required = true, example = "1024"), + @Parameter(name = "processStatus", description = "处理状态", required = true, example = "1") + }) + @PreAuthorize("@ss.hasPermission('infra:api-error-log:update-status')") + public CommonResult updateApiErrorLogProcess(@RequestParam("id") Long id, + @RequestParam("processStatus") Integer processStatus) { + apiErrorLogService.updateApiErrorLogProcess(id, processStatus, getLoginUserId()); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得 API 错误日志分页") + @PreAuthorize("@ss.hasPermission('infra:api-error-log:query')") + public CommonResult> getApiErrorLogPage(@Valid ApiErrorLogPageReqVO pageVO) { + PageResult pageResult = apiErrorLogService.getApiErrorLogPage(pageVO); + return success(ApiErrorLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出 API 错误日志 Excel") + @PreAuthorize("@ss.hasPermission('infra:api-error-log:export')") + @OperateLog(type = EXPORT) + public void exportApiErrorLogExcel(@Valid ApiErrorLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = apiErrorLogService.getApiErrorLogList(exportReqVO); + // 导出 Excel + List datas = ApiErrorLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "API 错误日志.xls", "数据", ApiErrorLogExcelVO.class, datas); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogBaseVO.java new file mode 100644 index 0000000..f35e2db --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogBaseVO.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* API 访问日志 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ApiAccessLogBaseVO { + + @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "66600cb6-7852-11eb-9439-0242ac130002") + @NotNull(message = "链路追踪编号不能为空") + private String traceId; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") + @NotNull(message = "应用名不能为空") + private String applicationName; + + @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") + @NotNull(message = "请求方法名不能为空") + private String requestMethod; + + @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xxx/yyy") + @NotNull(message = "请求地址不能为空") + private String requestUrl; + + @Schema(description = "请求参数") + private String requestParams; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotNull(message = "用户 IP不能为空") + private String userIp; + + @Schema(description = "浏览器 UA", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") + @NotNull(message = "浏览器 UA不能为空") + private String userAgent; + + @Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开始请求时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime beginTime; + + @Schema(description = "结束请求时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "结束请求时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endTime; + + @Schema(description = "执行时长", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "执行时长不能为空") + private Integer duration; + + @Schema(description = "结果码", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "结果码不能为空") + private Integer resultCode; + + @Schema(description = "结果提示", example = "芋道源码,牛逼!") + private String resultMsg; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExcelVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExcelVO.java new file mode 100644 index 0000000..7f81dfd --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExcelVO.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * API 访问日志 Excel VO + * + * @author 芋道源码 + */ +@Data +public class ApiAccessLogExcelVO { + + @ExcelProperty("日志主键") + private Long id; + + @ExcelProperty("链路追踪编号") + private String traceId; + + @ExcelProperty("用户编号") + private Long userId; + + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_TYPE) + private Integer userType; + + @ExcelProperty("应用名") + private String applicationName; + + @ExcelProperty("请求方法名") + private String requestMethod; + + @ExcelProperty("请求地址") + private String requestUrl; + + @ExcelProperty("请求参数") + private String requestParams; + + @ExcelProperty("用户 IP") + private String userIp; + + @ExcelProperty("浏览器 UA") + private String userAgent; + + @ExcelProperty("开始请求时间") + private LocalDateTime beginTime; + + @ExcelProperty("结束请求时间") + private LocalDateTime endTime; + + @ExcelProperty("执行时长") + private Integer duration; + + @ExcelProperty("结果码") + private Integer resultCode; + + @ExcelProperty("结果提示") + private String resultMsg; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExportReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExportReqVO.java new file mode 100644 index 0000000..4fcfb64 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogExportReqVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - API 访问日志 Excel 导出 Request VO,参数和 ApiAccessLogPageReqVO 是一致的") +@Data +public class ApiAccessLogExportReqVO { + + @Schema(description = "用户编号", example = "666") + private Long userId; + + @Schema(description = "用户类型", example = "2") + private Integer userType; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "请求地址,模糊匹配", example = "/xxx/yyy") + private String requestUrl; + + @Schema(description = "开始时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] beginTime; + + @Schema(description = "执行时长,大于等于,单位:毫秒", example = "100") + private Integer duration; + + @Schema(description = "结果码", example = "0") + private Integer resultCode; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java new file mode 100644 index 0000000..f04d8eb --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogPageReqVO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - API 访问日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiAccessLogPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "666") + private Long userId; + + @Schema(description = "用户类型", example = "2") + private Integer userType; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "请求地址,模糊匹配", example = "/xxx/yyy") + private String requestUrl; + + @Schema(description = "开始时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] beginTime; + + @Schema(description = "执行时长,大于等于,单位:毫秒", example = "100") + private Integer duration; + + @Schema(description = "结果码", example = "0") + private Integer resultCode; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java new file mode 100644 index 0000000..291e52a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - API 访问日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiAccessLogRespVO extends ApiAccessLogBaseVO { + + @Schema(description = "日志主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogBaseVO.java new file mode 100644 index 0000000..f4d2dd4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogBaseVO.java @@ -0,0 +1,95 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* API 错误日志 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ApiErrorLogBaseVO { + + @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "66600cb6-7852-11eb-9439-0242ac130002") + @NotNull(message = "链路追踪编号不能为空") + private String traceId; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + @NotNull(message = "用户编号不能为空") + private Integer userId; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") + @NotNull(message = "应用名不能为空") + private String applicationName; + + @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") + @NotNull(message = "请求方法名不能为空") + private String requestMethod; + + @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xx/yy") + @NotNull(message = "请求地址不能为空") + private String requestUrl; + + @Schema(description = "请求参数", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "请求参数不能为空") + private String requestParams; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotNull(message = "用户 IP不能为空") + private String userIp; + + @Schema(description = "浏览器 UA", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") + @NotNull(message = "浏览器 UA不能为空") + private String userAgent; + + @Schema(description = "异常发生时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常发生时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime exceptionTime; + + @Schema(description = "异常名", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常名不能为空") + private String exceptionName; + + @Schema(description = "异常导致的消息", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常导致的消息不能为空") + private String exceptionMessage; + + @Schema(description = "异常导致的根消息", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常导致的根消息不能为空") + private String exceptionRootCauseMessage; + + @Schema(description = "异常的栈轨迹", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常的栈轨迹不能为空") + private String exceptionStackTrace; + + @Schema(description = "异常发生的类全名", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常发生的类全名不能为空") + private String exceptionClassName; + + @Schema(description = "异常发生的类文件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常发生的类文件不能为空") + private String exceptionFileName; + + @Schema(description = "异常发生的方法名", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常发生的方法名不能为空") + private String exceptionMethodName; + + @Schema(description = "异常发生的方法所在行", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "异常发生的方法所在行不能为空") + private Integer exceptionLineNumber; + + @Schema(description = "处理状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "处理状态不能为空") + private Integer processStatus; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExcelVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExcelVO.java new file mode 100644 index 0000000..b91bbe8 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExcelVO.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.infra.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * API 错误日志 Excel VO + * + * @author 芋道源码 + */ +@Data +public class ApiErrorLogExcelVO { + + @ExcelProperty("编号") + private Integer id; + + @ExcelProperty("链路追踪编号") + private String traceId; + + @ExcelProperty("用户编号") + private Integer userId; + + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(com.yunxi.scm.module.system.enums.DictTypeConstants.USER_TYPE) + private Integer userType; + + @ExcelProperty("应用名") + private String applicationName; + + @ExcelProperty("请求方法名") + private String requestMethod; + + @ExcelProperty("请求地址") + private String requestUrl; + + @ExcelProperty("请求参数") + private String requestParams; + + @ExcelProperty("用户 IP") + private String userIp; + + @ExcelProperty("浏览器 UA") + private String userAgent; + + @ExcelProperty("异常发生时间") + private LocalDateTime exceptionTime; + + @ExcelProperty("异常名") + private String exceptionName; + + @ExcelProperty("异常导致的消息") + private String exceptionMessage; + + @ExcelProperty("异常导致的根消息") + private String exceptionRootCauseMessage; + + @ExcelProperty("异常的栈轨迹") + private String exceptionStackTrace; + + @ExcelProperty("异常发生的类全名") + private String exceptionClassName; + + @ExcelProperty("异常发生的类文件") + private String exceptionFileName; + + @ExcelProperty("异常发生的方法名") + private String exceptionMethodName; + + @ExcelProperty("异常发生的方法所在行") + private Integer exceptionLineNumber; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @ExcelProperty(value = "处理状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.API_ERROR_LOG_PROCESS_STATUS) + private Integer processStatus; + + @ExcelProperty("处理时间") + private LocalDateTime processTime; + + @ExcelProperty("处理用户编号") + private Integer processUserId; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExportReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExportReqVO.java new file mode 100644 index 0000000..74000c3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogExportReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - API 错误日志 Excel 导出 Request VO,参数和 ApiErrorLogPageReqVO 是一致的") +@Data +public class ApiErrorLogExportReqVO { + + @Schema(description = "用户编号", example = "666") + private Long userId; + + @Schema(description = "用户类型", example = "1") + private Integer userType; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "请求地址", example = "/xx/yy") + private String requestUrl; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "异常发生时间") + private LocalDateTime[] exceptionTime; + + @Schema(description = "处理状态", example = "0") + private Integer processStatus; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java new file mode 100644 index 0000000..e4192c7 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogPageReqVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - API 错误日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiErrorLogPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "666") + private Long userId; + + @Schema(description = "用户类型", example = "1") + private Integer userType; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "请求地址", example = "/xx/yy") + private String requestUrl; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "异常发生时间") + private LocalDateTime[] exceptionTime; + + @Schema(description = "处理状态", example = "0") + private Integer processStatus; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java new file mode 100644 index 0000000..c6773c2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - API 错误日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ApiErrorLogRespVO extends ApiErrorLogBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "处理时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime processTime; + + @Schema(description = "处理用户编号", example = "233") + private Integer processUserId; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/RedisController.http b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/RedisController.http new file mode 100644 index 0000000..8a0e70f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/RedisController.http @@ -0,0 +1,4 @@ +### 请求 /infra/redis/get-monitor-info 接口 => 成功 +GET {{baseUrl}}/infra/redis/get-monitor-info +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/RedisController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/RedisController.java new file mode 100644 index 0000000..c5e3d0d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/RedisController.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.infra.controller.admin.redis; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; +import com.yunxi.scm.module.infra.convert.redis.RedisConvert; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.data.redis.connection.RedisServerCommands; +import org.springframework.data.redis.core.RedisCallback; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Properties; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - Redis 监控") +@RestController +@RequestMapping("/infra/redis") +public class RedisController { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @GetMapping("/get-monitor-info") + @Operation(summary = "获得 Redis 监控信息") + @PreAuthorize("@ss.hasPermission('infra:redis:get-monitor-info')") + public CommonResult getRedisMonitorInfo() { + // 获得 Redis 统计信息 + Properties info = stringRedisTemplate.execute((RedisCallback) RedisServerCommands::info); + Long dbSize = stringRedisTemplate.execute(RedisServerCommands::dbSize); + Properties commandStats = stringRedisTemplate.execute(( + RedisCallback) connection -> connection.info("commandstats")); + assert commandStats != null; // 断言,避免警告 + // 拼接结果返回 + return success(RedisConvert.INSTANCE.build(info, dbSize, commandStats)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java new file mode 100644 index 0000000..f430e0e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/redis/vo/RedisMonitorRespVO.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.infra.controller.admin.redis.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Properties; + +@Schema(description = "管理后台 - Redis 监控信息 Response VO") +@Data +@Builder +@AllArgsConstructor +public class RedisMonitorRespVO { + + @Schema(description = "Redis info 指令结果,具体字段,查看 Redis 文档", requiredMode = Schema.RequiredMode.REQUIRED) + private Properties info; + + @Schema(description = "Redis key 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long dbSize; + + @Schema(description = "CommandStat 数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List commandStats; + + @Schema(description = "Redis 命令统计结果") + @Data + @Builder + @AllArgsConstructor + public static class CommandStat { + + @Schema(description = "Redis 命令", requiredMode = Schema.RequiredMode.REQUIRED, example = "get") + private String command; + + @Schema(description = "调用次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long calls; + + @Schema(description = "消耗 CPU 秒数", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private Long usec; + + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/TestDemoController.http b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/TestDemoController.http new file mode 100644 index 0000000..ed65d0b --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/TestDemoController.http @@ -0,0 +1,19 @@ +### 请求 /infra/test-demo/get 接口 => 成功 +GET {{baseUrl}}/infra/test-demo/get?id=106 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /infra/test-demo/update 接口 => 成功 +PUT {{baseUrl}}/infra/test-demo/update +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} +Content-Type: application/json + + +{ + "id": 106, + "name": "测试", + "status": "0", + "type": 1, + "category": 1 +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/TestDemoController.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/TestDemoController.java new file mode 100644 index 0000000..8926aae --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/TestDemoController.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.module.infra.controller.admin.test; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.infra.controller.admin.test.vo.*; +import com.yunxi.scm.module.infra.convert.test.TestDemoConvert; +import com.yunxi.scm.module.infra.dal.dataobject.test.TestDemoDO; +import com.yunxi.scm.module.infra.service.test.TestDemoService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 字典类型") +@RestController +@RequestMapping("/infra/test-demo") +@Validated +public class TestDemoController { + + @Resource + private TestDemoService testDemoService; + + @PostMapping("/create") + @Operation(summary = "创建字典类型") + @PreAuthorize("@ss.hasPermission('infra:test-demo:create')") + public CommonResult createTestDemo(@Valid @RequestBody TestDemoCreateReqVO createReqVO) { + return success(testDemoService.createTestDemo(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新字典类型") + @PreAuthorize("@ss.hasPermission('infra:test-demo:update')") + public CommonResult updateTestDemo(@Valid @RequestBody TestDemoUpdateReqVO updateReqVO) { + testDemoService.updateTestDemo(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除字典类型") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('infra:test-demo:delete')") + public CommonResult deleteTestDemo(@RequestParam("id") Long id) { + testDemoService.deleteTestDemo(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得字典类型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('infra:test-demo:query')") + public CommonResult getTestDemo(@RequestParam("id") Long id) { + TestDemoDO testDemo = testDemoService.getTestDemo(id); + return success(TestDemoConvert.INSTANCE.convert(testDemo)); + } + + @GetMapping("/list") + @Operation(summary = "获得字典类型列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('infra:test-demo:query')") + public CommonResult> getTestDemoList(@RequestParam("ids") Collection ids) { + List list = testDemoService.getTestDemoList(ids); + return success(TestDemoConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得字典类型分页") + @PreAuthorize("@ss.hasPermission('infra:test-demo:query')") public CommonResult> getTestDemoPage(@Valid TestDemoPageReqVO pageVO) { + PageResult pageResult = testDemoService.getTestDemoPage(pageVO); + return success(TestDemoConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出字典类型 Excel") + @PreAuthorize("@ss.hasPermission('infra:test-demo:export')") @OperateLog(type = EXPORT) + public void exportTestDemoExcel(@Valid TestDemoExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = testDemoService.getTestDemoList(exportReqVO); + // 导出 Excel + List datas = TestDemoConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "字典类型.xls", "数据", TestDemoExcelVO.class, datas); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoBaseVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoBaseVO.java new file mode 100644 index 0000000..8330f76 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoBaseVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.infra.controller.admin.test.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +/** +* 字典类型 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class TestDemoBaseVO { + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "名字不能为空") + private String name; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "类型不能为空") + private Integer type; + + @Schema(description = "分类", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "分类不能为空") + private Integer category; + + @Schema(description = "备注") + private String remark; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoCreateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoCreateReqVO.java new file mode 100644 index 0000000..2e95e77 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoCreateReqVO.java @@ -0,0 +1,11 @@ +package com.yunxi.scm.module.infra.controller.admin.test.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "管理后台 - 字典类型创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TestDemoCreateReqVO extends TestDemoBaseVO { + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoExcelVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoExcelVO.java new file mode 100644 index 0000000..3223ca8 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoExcelVO.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.infra.controller.admin.test.vo; + +import lombok.*; + +import java.time.LocalDateTime; + +import com.alibaba.excel.annotation.ExcelProperty; + +/** + * 字典类型 Excel VO + * + * @author 芋道源码 + */ +@Data +public class TestDemoExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("名字") + private String name; + + @ExcelProperty("状态") + private Integer status; + + @ExcelProperty("类型") + private Integer type; + + @ExcelProperty("分类") + private Integer category; + + @ExcelProperty("备注") + private String remark; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoExportReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoExportReqVO.java new file mode 100644 index 0000000..3771485 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoExportReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.infra.controller.admin.test.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; +import org.springframework.format.annotation.DateTimeFormat; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 字典类型 Excel 导出 Request VO,参数和 TestDemoPageReqVO 是一致的") +@Data +public class TestDemoExportReqVO { + + @Schema(description = "名字") + private String name; + + @Schema(description = "状态") + private Integer status; + + @Schema(description = "类型") + private Integer type; + + @Schema(description = "分类") + private Integer category; + + @Schema(description = "备注") + private String remark; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoPageReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoPageReqVO.java new file mode 100644 index 0000000..bf83235 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoPageReqVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.infra.controller.admin.test.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; +import com.yunxi.scm.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 字典类型分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TestDemoPageReqVO extends PageParam { + + @Schema(description = "名字") + private String name; + + @Schema(description = "状态") + private Integer status; + + @Schema(description = "类型") + private Integer type; + + @Schema(description = "分类") + private Integer category; + + @Schema(description = "备注") + private String remark; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoRespVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoRespVO.java new file mode 100644 index 0000000..92a03e0 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.infra.controller.admin.test.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 字典类型 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TestDemoRespVO extends TestDemoBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoUpdateReqVO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoUpdateReqVO.java new file mode 100644 index 0000000..b8ef1b3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/admin/test/vo/TestDemoUpdateReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.infra.controller.admin.test.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 字典类型更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TestDemoUpdateReqVO extends TestDemoBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/app/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/app/package-info.java new file mode 100644 index 0000000..49fee8d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.infra.controller.app; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/package-info.java new file mode 100644 index 0000000..74c6073 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yunxi-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yunxi-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.yunxi.scm.module.infra.controller; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/codegen/CodegenConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/codegen/CodegenConvert.java new file mode 100644 index 0000000..05b4ba7 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/codegen/CodegenConvert.java @@ -0,0 +1,93 @@ +package com.yunxi.scm.module.infra.convert.codegen; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenDetailRespVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenPreviewRespVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.column.CodegenColumnRespVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.CodegenTableRespVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import org.apache.ibatis.type.JdbcType; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Mapper +public interface CodegenConvert { + + CodegenConvert INSTANCE = Mappers.getMapper(CodegenConvert.class); + + // ========== TableInfo 相关 ========== + + @Mappings({ + @Mapping(source = "name", target = "tableName"), + @Mapping(source = "comment", target = "tableComment"), + }) + CodegenTableDO convert(TableInfo bean); + + List convertList(List list); + + @Mappings({ + @Mapping(source = "name", target = "columnName"), + @Mapping(source = "metaInfo.jdbcType", target = "dataType", qualifiedByName = "getDataType"), + @Mapping(source = "comment", target = "columnComment"), + @Mapping(source = "metaInfo.nullable", target = "nullable"), + @Mapping(source = "keyFlag", target = "primaryKey"), + @Mapping(source = "keyIdentityFlag", target = "autoIncrement"), + @Mapping(source = "columnType.type", target = "javaType"), + @Mapping(source = "propertyName", target = "javaField"), + }) + CodegenColumnDO convert(TableField bean); + + @Named("getDataType") + default String getDataType(JdbcType jdbcType) { + return jdbcType.name(); + } + + // ========== CodegenTableDO 相关 ========== + +// List convertList02(List list); + + CodegenTableRespVO convert(CodegenTableDO bean); + + PageResult convertPage(PageResult page); + + // ========== CodegenTableDO 相关 ========== + + List convertList02(List list); + + CodegenTableDO convert(CodegenUpdateReqVO.Table bean); + + List convertList03(List columns); + + List convertList04(List list); + + // ========== 其它 ========== + + default CodegenDetailRespVO convert(CodegenTableDO table, List columns) { + CodegenDetailRespVO respVO = new CodegenDetailRespVO(); + respVO.setTable(convert(table)); + respVO.setColumns(convertList02(columns)); + return respVO; + } + + default List convert(Map codes) { + return codes.entrySet().stream().map(entry -> { + CodegenPreviewRespVO respVO = new CodegenPreviewRespVO(); + respVO.setFilePath(entry.getKey()); + respVO.setCode(entry.getValue()); + return respVO; + }).collect(Collectors.toList()); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/config/ConfigConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/config/ConfigConvert.java new file mode 100644 index 0000000..8bf81dc --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/config/ConfigConvert.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.infra.convert.config; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigExcelVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigRespVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.config.ConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface ConfigConvert { + + ConfigConvert INSTANCE = Mappers.getMapper(ConfigConvert.class); + + PageResult convertPage(PageResult page); + + @Mapping(source = "configKey", target = "key") + ConfigRespVO convert(ConfigDO bean); + + @Mapping(source = "key", target = "configKey") + ConfigDO convert(ConfigCreateReqVO bean); + + ConfigDO convert(ConfigUpdateReqVO bean); + + @Mapping(source = "configKey", target = "key") + List convertList(List list); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/db/DataSourceConfigConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/db/DataSourceConfigConvert.java new file mode 100644 index 0000000..89593c4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/db/DataSourceConfigConvert.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.infra.convert.db; + +import java.util.*; + +import com.yunxi.scm.framework.common.pojo.PageResult; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import com.yunxi.scm.module.infra.controller.admin.db.vo.*; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; + +/** + * 数据源配置 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface DataSourceConfigConvert { + + DataSourceConfigConvert INSTANCE = Mappers.getMapper(DataSourceConfigConvert.class); + + DataSourceConfigDO convert(DataSourceConfigCreateReqVO bean); + + DataSourceConfigDO convert(DataSourceConfigUpdateReqVO bean); + + DataSourceConfigRespVO convert(DataSourceConfigDO bean); + + List convertList(List list); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/file/FileConfigConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/file/FileConfigConvert.java new file mode 100644 index 0000000..c8e33c9 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/file/FileConfigConvert.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.infra.convert.file; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigRespVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 文件配置 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface FileConfigConvert { + + FileConfigConvert INSTANCE = Mappers.getMapper(FileConfigConvert.class); + + @Mapping(target = "config", ignore = true) + FileConfigDO convert(FileConfigCreateReqVO bean); + + @Mapping(target = "config", ignore = true) + FileConfigDO convert(FileConfigUpdateReqVO bean); + + FileConfigRespVO convert(FileConfigDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/file/FileConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/file/FileConvert.java new file mode 100644 index 0000000..c5aff3c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/file/FileConvert.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.infra.convert.file; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.file.vo.file.FileRespVO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface FileConvert { + + FileConvert INSTANCE = Mappers.getMapper(FileConvert.class); + + FileRespVO convert(FileDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/job/JobConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/job/JobConvert.java new file mode 100644 index 0000000..f1e98a5 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/job/JobConvert.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.infra.convert.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobExcelVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobRespVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 定时任务 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface JobConvert { + + JobConvert INSTANCE = Mappers.getMapper(JobConvert.class); + + JobDO convert(JobCreateReqVO bean); + + JobDO convert(JobUpdateReqVO bean); + + JobRespVO convert(JobDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/job/JobLogConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/job/JobLogConvert.java new file mode 100644 index 0000000..9dfb8a3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/job/JobLogConvert.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.infra.convert.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogExcelVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogRespVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 定时任务日志 Convert + * + * @author 芋艿 + */ +@Mapper +public interface JobLogConvert { + + JobLogConvert INSTANCE = Mappers.getMapper(JobLogConvert.class); + + JobLogRespVO convert(JobLogDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/logger/ApiAccessLogConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/logger/ApiAccessLogConvert.java new file mode 100644 index 0000000..c85faa4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/logger/ApiAccessLogConvert.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.infra.convert.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExcelVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogRespVO; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * API 访问日志 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ApiAccessLogConvert { + + ApiAccessLogConvert INSTANCE = Mappers.getMapper(ApiAccessLogConvert.class); + + ApiAccessLogRespVO convert(ApiAccessLogDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + ApiAccessLogDO convert(ApiAccessLogCreateReqDTO bean); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/logger/ApiErrorLogConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/logger/ApiErrorLogConvert.java new file mode 100644 index 0000000..ea2fda4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/logger/ApiErrorLogConvert.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.infra.convert.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExcelVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogRespVO; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * API 错误日志 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ApiErrorLogConvert { + + ApiErrorLogConvert INSTANCE = Mappers.getMapper(ApiErrorLogConvert.class); + + ApiErrorLogRespVO convert(ApiErrorLogDO bean); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + ApiErrorLogDO convert(ApiErrorLogCreateReqDTO bean); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/package-info.java new file mode 100644 index 0000000..a051fac --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package com.yunxi.scm.module.infra.convert; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/redis/RedisConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/redis/RedisConvert.java new file mode 100644 index 0000000..092c367 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/redis/RedisConvert.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.infra.convert.redis; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.infra.controller.admin.redis.vo.RedisMonitorRespVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.Properties; + +@Mapper +public interface RedisConvert { + + RedisConvert INSTANCE = Mappers.getMapper(RedisConvert.class); + + default RedisMonitorRespVO build(Properties info, Long dbSize, Properties commandStats) { + RedisMonitorRespVO respVO = RedisMonitorRespVO.builder().info(info).dbSize(dbSize) + .commandStats(new ArrayList<>(commandStats.size())).build(); + commandStats.forEach((key, value) -> { + respVO.getCommandStats().add(RedisMonitorRespVO.CommandStat.builder() + .command(StrUtil.subAfter((String) key, "cmdstat_", false)) + .calls(Long.valueOf(StrUtil.subBetween((String) value, "calls=", ","))) + .usec(Long.valueOf(StrUtil.subBetween((String) value, "usec=", ","))) + .build()); + }); + return respVO; + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/test/TestDemoConvert.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/test/TestDemoConvert.java new file mode 100644 index 0000000..135dce9 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/test/TestDemoConvert.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.infra.convert.test; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoExcelVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoRespVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.test.TestDemoDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 字典类型 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface TestDemoConvert { + + TestDemoConvert INSTANCE = Mappers.getMapper(TestDemoConvert.class); + + TestDemoDO convert(TestDemoCreateReqVO bean); + + TestDemoDO convert(TestDemoUpdateReqVO bean); + + TestDemoRespVO convert(TestDemoDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..19fbece --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/codegen/CodegenColumnDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/codegen/CodegenColumnDO.java new file mode 100644 index 0000000..9daf92f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/codegen/CodegenColumnDO.java @@ -0,0 +1,142 @@ +package com.yunxi.scm.module.infra.dal.dataobject.codegen; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; +import com.yunxi.scm.module.infra.enums.codegen.CodegenColumnListConditionEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 代码生成 column 字段定义 + * + * @author 芋道源码 + */ +@TableName(value = "infra_codegen_column", autoResultMap = true) +@KeySequence("infra_codegen_column_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class CodegenColumnDO extends BaseDO { + + /** + * ID 编号 + */ + @TableId + private Long id; + /** + * 表编号 + *

+ * 关联 {@link CodegenTableDO#getId()} + */ + private Long tableId; + + // ========== 表相关字段 ========== + + /** + * 字段名 + * + * 关联 {@link TableField#getName()} + */ + private String columnName; + /** + * 数据库字段类型 + * + * 关联 {@link TableField.MetaInfo#getJdbcType()} + */ + private String dataType; + /** + * 字段描述 + * + * 关联 {@link TableField#getComment()} + */ + private String columnComment; + /** + * 是否允许为空 + * + * 关联 {@link TableField.MetaInfo#isNullable()} + */ + private Boolean nullable; + /** + * 是否主键 + * + * 关联 {@link TableField#isKeyFlag()} + */ + private Boolean primaryKey; + /** + * 是否自增 + * + * 关联 {@link TableField#isKeyIdentityFlag()} + */ + private Boolean autoIncrement; + /** + * 排序 + */ + private Integer ordinalPosition; + + // ========== Java 相关字段 ========== + + /** + * Java 属性类型 + * + * 例如说 String、Boolean 等等 + * + * 关联 {@link TableField#getColumnType()} + */ + private String javaType; + /** + * Java 属性名 + * + * 关联 {@link TableField#getPropertyName()} + */ + private String javaField; + /** + * 字典类型 + *

+ * 关联 DictTypeDO 的 type 属性 + */ + private String dictType; + /** + * 数据示例,主要用于生成 Swagger 注解的 example 字段 + */ + private String example; + + // ========== CRUD 相关字段 ========== + + /** + * 是否为 Create 创建操作的字段 + */ + private Boolean createOperation; + /** + * 是否为 Update 更新操作的字段 + */ + private Boolean updateOperation; + /** + * 是否为 List 查询操作的字段 + */ + private Boolean listOperation; + /** + * List 查询操作的条件类型 + *

+ * 枚举 {@link CodegenColumnListConditionEnum} + */ + private String listOperationCondition; + /** + * 是否为 List 查询操作的返回字段 + */ + private Boolean listOperationResult; + + // ========== UI 相关字段 ========== + + /** + * 显示类型 + *

+ * 枚举 {@link CodegenColumnHtmlTypeEnum} + */ + private String htmlType; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/codegen/CodegenTableDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/codegen/CodegenTableDO.java new file mode 100644 index 0000000..3438389 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/codegen/CodegenTableDO.java @@ -0,0 +1,119 @@ +package com.yunxi.scm.module.infra.dal.dataobject.codegen; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.yunxi.scm.module.infra.enums.codegen.CodegenFrontTypeEnum; +import com.yunxi.scm.module.infra.enums.codegen.CodegenSceneEnum; +import com.yunxi.scm.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 代码生成 table 表定义 + * + * @author 芋道源码 + */ +@TableName(value = "infra_codegen_table", autoResultMap = true) +@KeySequence("infra_codegen_table_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class CodegenTableDO extends BaseDO { + + /** + * ID 编号 + */ + @TableId + private Long id; + + /** + * 数据源编号 + * + * 关联 {@link DataSourceConfigDO#getId()} + */ + private Long dataSourceConfigId; + /** + * 生成场景 + * + * 枚举 {@link CodegenSceneEnum} + */ + private Integer scene; + + // ========== 表相关字段 ========== + + /** + * 表名称 + * + * 关联 {@link TableInfo#getName()} + */ + private String tableName; + /** + * 表描述 + * + * 关联 {@link TableInfo#getComment()} + */ + private String tableComment; + /** + * 备注 + */ + private String remark; + + // ========== 类相关字段 ========== + + /** + * 模块名,即一级目录 + * + * 例如说,system、infra、tool 等等 + */ + private String moduleName; + /** + * 业务名,即二级目录 + * + * 例如说,user、permission、dict 等等 + */ + private String businessName; + /** + * 类名称(首字母大写) + * + * 例如说,SysUser、SysMenu、SysDictData 等等 + */ + private String className; + /** + * 类描述 + */ + private String classComment; + /** + * 作者 + */ + private String author; + + // ========== 生成相关字段 ========== + + /** + * 模板类型 + * + * 枚举 {@link CodegenTemplateTypeEnum} + */ + private Integer templateType; + /** + * 代码生成的前端类型 + * + * 枚举 {@link CodegenFrontTypeEnum} + */ + private Integer frontType; + + // ========== 菜单相关字段 ========== + + /** + * 父菜单编号 + * + * 关联 MenuDO 的 id 属性 + */ + private Long parentMenuId; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/config/ConfigDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/config/ConfigDO.java new file mode 100644 index 0000000..9db2288 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/config/ConfigDO.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.module.infra.dal.dataobject.config; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.infra.enums.config.ConfigTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 参数配置表 + * + * @author 芋道源码 + */ +@TableName("infra_config") +@KeySequence("infra_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ConfigDO extends BaseDO { + + /** + * 参数主键 + */ + @TableId + private Long id; + /** + * 参数分类 + */ + private String category; + /** + * 参数名称 + */ + private String name; + /** + * 参数键名 + * + * 支持多 DB 类型时,无法直接使用 key + @TableField("config_key") 来实现转换,原因是 "config_key" AS key 而存在报错 + */ + private String configKey; + /** + * 参数键值 + */ + private String value; + /** + * 参数类型 + * + * 枚举 {@link ConfigTypeEnum} + */ + private Integer type; + /** + * 是否可见 + * + * 不可见的参数,一般是敏感参数,前端不可获取 + */ + private Boolean visible; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/db/DataSourceConfigDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/db/DataSourceConfigDO.java new file mode 100644 index 0000000..f6bdcd6 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/db/DataSourceConfigDO.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.infra.dal.dataobject.db; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.EncryptTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 数据源配置 + * + * @author 芋道源码 + */ +@TableName(value = "infra_data_source_config", autoResultMap = true) +@KeySequence("infra_data_source_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DataSourceConfigDO extends BaseDO { + + /** + * 主键编号 - Master 数据源 + */ + public static final Long ID_MASTER = 0L; + + /** + * 主键编号 + */ + private Long id; + /** + * 连接名 + */ + private String name; + + /** + * 数据源连接 + */ + private String url; + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + @TableField(typeHandler = EncryptTypeHandler.class) + private String password; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileConfigDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileConfigDO.java new file mode 100644 index 0000000..4d2b695 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileConfigDO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.infra.dal.dataobject.file; + +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import com.yunxi.scm.framework.file.core.enums.FileStorageEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +/** + * 文件配置表 + * + * @author 芋道源码 + */ +@TableName(value = "infra_file_config", autoResultMap = true) +@KeySequence("infra_file_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileConfigDO extends BaseDO { + + /** + * 配置编号,数据库自增 + */ + private Long id; + /** + * 配置名 + */ + private String name; + /** + * 存储器 + * + * 枚举 {@link FileStorageEnum} + */ + private Integer storage; + /** + * 备注 + */ + private String remark; + /** + * 是否为主配置 + * + * 由于我们可以配置多个文件配置,默认情况下,使用主配置进行文件的上传 + */ + private Boolean master; + + /** + * 支付渠道配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private FileClientConfig config; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileContentDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileContentDO.java new file mode 100644 index 0000000..0b88ce2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileContentDO.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.infra.dal.dataobject.file; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 文件内容表 + * + * 专门用于存储 {@link com.yunxi.scm.framework.file.core.client.db.DBFileClient} 的文件内容 + * + * @author 芋道源码 + */ +@TableName("infra_file_content") +@KeySequence("infra_file_content_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileContentDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId(type = IdType.INPUT) + private String id; + /** + * 配置编号 + * + * 关联 {@link FileConfigDO#getId()} + */ + private Long configId; + /** + * 路径,即文件名 + */ + private String path; + /** + * 文件内容 + */ + private byte[] content; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileDO.java new file mode 100644 index 0000000..adc50f4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/file/FileDO.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.infra.dal.dataobject.file; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 文件表 + * 每次文件上传,都会记录一条记录到该表中 + * + * @author 芋道源码 + */ +@TableName("infra_file") +@KeySequence("infra_file_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FileDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + private Long id; + /** + * 配置编号 + * + * 关联 {@link FileConfigDO#getId()} + */ + private Long configId; + /** + * 原文件名 + */ + private String name; + /** + * 路径,即文件名 + */ + private String path; + /** + * 访问地址 + */ + private String url; + /** + * 文件的 MIME 类型,例如 "application/octet-stream" + */ + private String type; + /** + * 文件大小 + */ + private Integer size; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/job/JobDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/job/JobDO.java new file mode 100644 index 0000000..01ce33d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/job/JobDO.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.infra.dal.dataobject.job; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.infra.enums.job.JobStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 定时任务 DO + * + * @author 芋道源码 + */ +@TableName("infra_job") +@KeySequence("infra_job_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JobDO extends BaseDO { + + /** + * 任务编号 + */ + @TableId + private Long id; + /** + * 任务名称 + */ + private String name; + /** + * 任务状态 + * + * 枚举 {@link JobStatusEnum} + */ + private Integer status; + /** + * 处理器的名字 + */ + private String handlerName; + /** + * 处理器的参数 + */ + private String handlerParam; + /** + * CRON 表达式 + */ + private String cronExpression; + + // ========== 重试相关字段 ========== + /** + * 重试次数 + * 如果不重试,则设置为 0 + */ + private Integer retryCount; + /** + * 重试间隔,单位:毫秒 + * 如果没有间隔,则设置为 0 + */ + private Integer retryInterval; + + // ========== 监控相关字段 ========== + /** + * 监控超时时间,单位:毫秒 + * 为空时,表示不监控 + * + * 注意,这里的超时的目的,不是进行任务的取消,而是告警任务的执行时间过长 + */ + private Integer monitorTimeout; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/job/JobLogDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/job/JobLogDO.java new file mode 100644 index 0000000..7e4cea6 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/job/JobLogDO.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.infra.dal.dataobject.job; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.module.infra.enums.job.JobLogStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 定时任务的执行日志 + * + * @author 芋道源码 + */ +@TableName("infra_job_log") +@KeySequence("infra_job_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class JobLogDO extends BaseDO { + + /** + * 日志编号 + */ + private Long id; + /** + * 任务编号 + * + * 关联 {@link JobDO#getId()} + */ + private Long jobId; + /** + * 处理器的名字 + * + * 冗余字段 {@link JobDO#getHandlerName()} + */ + private String handlerName; + /** + * 处理器的参数 + * + * 冗余字段 {@link JobDO#getHandlerParam()} + */ + private String handlerParam; + /** + * 第几次执行 + * + * 用于区分是不是重试执行。如果是重试执行,则 index 大于 1 + */ + private Integer executeIndex; + + /** + * 开始执行时间 + */ + private LocalDateTime beginTime; + /** + * 结束执行时间 + */ + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 状态 + * + * 枚举 {@link JobLogStatusEnum} + */ + private Integer status; + /** + * 结果数据 + * + * 成功时,使用 {@link JobHandler#execute(String)} 的结果 + * 失败时,使用 {@link JobHandler#execute(String)} 的异常堆栈 + */ + private String result; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/logger/ApiAccessLogDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/logger/ApiAccessLogDO.java new file mode 100644 index 0000000..d0bd5f8 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/logger/ApiAccessLogDO.java @@ -0,0 +1,109 @@ +package com.yunxi.scm.module.infra.dal.dataobject.logger; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * API 访问日志 + * + * @author 芋道源码 + */ +@TableName("infra_api_access_log") +@KeySequence(value = "infra_api_access_log_seq") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ApiAccessLogDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 应用名 + * + * 目前读取 `spring.application.name` 配置项 + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 执行相关字段 ========== + + /** + * 开始请求时间 + */ + private LocalDateTime beginTime; + /** + * 结束请求时间 + */ + private LocalDateTime endTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/logger/ApiErrorLogDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/logger/ApiErrorLogDO.java new file mode 100644 index 0000000..1dbca3a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/logger/ApiErrorLogDO.java @@ -0,0 +1,156 @@ +package com.yunxi.scm.module.infra.dal.dataobject.logger; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * API 异常数据 + * + * @author 芋道源码 + */ +@TableName("infra_api_error_log") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +@KeySequence(value = "infra_api_error_log_seq") +public class ApiErrorLogDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 应用名 + * + * 目前读取 spring.application.name + */ + private String applicationName; + + // ========== 请求相关字段 ========== + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 访问地址 + */ + private String requestUrl; + /** + * 请求参数 + * + * query: Query String + * body: Quest Body + */ + private String requestParams; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + // ========== 异常相关字段 ========== + + /** + * 异常发生时间 + */ + private LocalDateTime exceptionTime; + /** + * 异常名 + * + * {@link Throwable#getClass()} 的类全名 + */ + private String exceptionName; + /** + * 异常导致的消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getMessage(Throwable)} + */ + private String exceptionMessage; + /** + * 异常导致的根消息 + * + * {@link cn.hutool.core.exceptions.ExceptionUtil#getRootCauseMessage(Throwable)} + */ + private String exceptionRootCauseMessage; + /** + * 异常的栈轨迹 + * + * {@link org.apache.commons.lang3.exception.ExceptionUtils#getStackTrace(Throwable)} + */ + private String exceptionStackTrace; + /** + * 异常发生的类全名 + * + * {@link StackTraceElement#getClassName()} + */ + private String exceptionClassName; + /** + * 异常发生的类文件 + * + * {@link StackTraceElement#getFileName()} + */ + private String exceptionFileName; + /** + * 异常发生的方法名 + * + * {@link StackTraceElement#getMethodName()} + */ + private String exceptionMethodName; + /** + * 异常发生的方法所在行 + * + * {@link StackTraceElement#getLineNumber()} + */ + private Integer exceptionLineNumber; + + // ========== 处理相关字段 ========== + + /** + * 处理状态 + * + * 枚举 {@link ApiErrorLogProcessStatusEnum} + */ + private Integer processStatus; + /** + * 处理时间 + */ + private LocalDateTime processTime; + /** + * 处理用户编号 + * + * 关联 com.yunxi.scm.adminserver.modules.system.dal.dataobject.user.SysUserDO.SysUserDO#getId() + */ + private Long processUserId; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/test/TestDemoDO.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/test/TestDemoDO.java new file mode 100644 index 0000000..2d0a034 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/dataobject/test/TestDemoDO.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.infra.dal.dataobject.test; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 字典类型 DO + * + * @author 芋道源码 + */ +@TableName("infra_test_demo") +@KeySequence("infra_test_demo_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TestDemoDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 名字 + */ + private String name; + /** + * 状态 + */ + private Integer status; + /** + * 类型 + */ + private Integer type; + /** + * 分类 + */ + private Integer category; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/codegen/CodegenColumnMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/codegen/CodegenColumnMapper.java new file mode 100644 index 0000000..e7bb1c9 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/codegen/CodegenColumnMapper.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.infra.dal.mysql.codegen; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface CodegenColumnMapper extends BaseMapperX { + + default List selectListByTableId(Long tableId) { + return selectList(new LambdaQueryWrapperX() + .eq(CodegenColumnDO::getTableId, tableId) + .orderByAsc(CodegenColumnDO::getId)); + } + + default void deleteListByTableId(Long tableId) { + delete(new LambdaQueryWrapperX() + .eq(CodegenColumnDO::getTableId, tableId)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/codegen/CodegenTableMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/codegen/CodegenTableMapper.java new file mode 100644 index 0000000..fab5624 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/codegen/CodegenTableMapper.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.infra.dal.mysql.codegen; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenTableDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface CodegenTableMapper extends BaseMapperX { + + default CodegenTableDO selectByTableNameAndDataSourceConfigId(String tableName, Long dataSourceConfigId) { + return selectOne(CodegenTableDO::getTableName, tableName, + CodegenTableDO::getDataSourceConfigId, dataSourceConfigId); + } + + default PageResult selectPage(CodegenTablePageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(CodegenTableDO::getTableName, pageReqVO.getTableName()) + .likeIfPresent(CodegenTableDO::getTableComment, pageReqVO.getTableComment()) + .likeIfPresent(CodegenTableDO::getClassName, pageReqVO.getClassName()) + .betweenIfPresent(CodegenTableDO::getCreateTime, pageReqVO.getCreateTime())); + } + + default List selectListByDataSourceConfigId(Long dataSourceConfigId) { + return selectList(CodegenTableDO::getDataSourceConfigId, dataSourceConfigId); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/config/ConfigMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/config/ConfigMapper.java new file mode 100644 index 0000000..fb84d7d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/config/ConfigMapper.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.infra.dal.mysql.config; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.config.ConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ConfigMapper extends BaseMapperX { + + default ConfigDO selectByKey(String key) { + return selectOne(ConfigDO::getConfigKey, key); + } + + default PageResult selectPage(ConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(ConfigDO::getName, reqVO.getName()) + .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey()) + .eqIfPresent(ConfigDO::getType, reqVO.getType()) + .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime())); + } + + default List selectList(ConfigExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(ConfigDO::getName, reqVO.getName()) + .likeIfPresent(ConfigDO::getConfigKey, reqVO.getKey()) + .eqIfPresent(ConfigDO::getType, reqVO.getType()) + .betweenIfPresent(ConfigDO::getCreateTime, reqVO.getCreateTime())); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/db/DataSourceConfigMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/db/DataSourceConfigMapper.java new file mode 100644 index 0000000..6ec9303 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/db/DataSourceConfigMapper.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.infra.dal.mysql.db; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 数据源配置 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface DataSourceConfigMapper extends BaseMapperX { +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileConfigMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileConfigMapper.java new file mode 100644 index 0000000..bf6b969 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileConfigMapper.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.infra.dal.mysql.file; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileConfigDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; + +@Mapper +public interface FileConfigMapper extends BaseMapperX { + + default PageResult selectPage(FileConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(FileConfigDO::getName, reqVO.getName()) + .eqIfPresent(FileConfigDO::getStorage, reqVO.getStorage()) + .betweenIfPresent(FileConfigDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(FileConfigDO::getId)); + } + + @Select("SELECT COUNT(*) FROM infra_file_config WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileContentDAOImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileContentDAOImpl.java new file mode 100644 index 0000000..eb4a7e5 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileContentDAOImpl.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.infra.dal.mysql.file; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.file.core.client.db.DBFileContentFrameworkDAO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileContentDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Optional; + +@Repository +public class FileContentDAOImpl implements DBFileContentFrameworkDAO { + + @Resource + private FileContentMapper fileContentMapper; + + @Override + public void insert(Long configId, String path, byte[] content) { + FileContentDO entity = new FileContentDO().setConfigId(configId) + .setPath(path).setContent(content); + fileContentMapper.insert(entity); + } + + @Override + public void delete(Long configId, String path) { + fileContentMapper.delete(buildQuery(configId, path)); + } + + @Override + public byte[] selectContent(Long configId, String path) { + List list = fileContentMapper.selectList( + buildQuery(configId, path).select(FileContentDO::getContent).orderByDesc(FileContentDO::getId)); + return Optional.ofNullable(CollUtil.getFirst(list)) + .map(FileContentDO::getContent) + .orElse(null); + } + + private LambdaQueryWrapper buildQuery(Long configId, String path) { + return new LambdaQueryWrapper() + .eq(FileContentDO::getConfigId, configId) + .eq(FileContentDO::getPath, path); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileContentMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileContentMapper.java new file mode 100644 index 0000000..77e7570 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileContentMapper.java @@ -0,0 +1,9 @@ +package com.yunxi.scm.module.infra.dal.mysql.file; + +import com.yunxi.scm.module.infra.dal.dataobject.file.FileContentDO; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface FileContentMapper extends BaseMapper { +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileMapper.java new file mode 100644 index 0000000..c29d6ba --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/file/FileMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.infra.dal.mysql.file; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 文件操作 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface FileMapper extends BaseMapperX { + + default PageResult selectPage(FilePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(FileDO::getPath, reqVO.getPath()) + .likeIfPresent(FileDO::getType, reqVO.getType()) + .betweenIfPresent(FileDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(FileDO::getId)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/job/JobLogMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/job/JobLogMapper.java new file mode 100644 index 0000000..b36fbe3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/job/JobLogMapper.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.infra.dal.mysql.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 任务日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface JobLogMapper extends BaseMapperX { + + default PageResult selectPage(JobLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(JobLogDO::getJobId, reqVO.getJobId()) + .likeIfPresent(JobLogDO::getHandlerName, reqVO.getHandlerName()) + .geIfPresent(JobLogDO::getBeginTime, reqVO.getBeginTime()) + .leIfPresent(JobLogDO::getEndTime, reqVO.getEndTime()) + .eqIfPresent(JobLogDO::getStatus, reqVO.getStatus()) + .orderByDesc(JobLogDO::getId) // ID 倒序 + ); + } + + default List selectList(JobLogExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(JobLogDO::getJobId, reqVO.getJobId()) + .likeIfPresent(JobLogDO::getHandlerName, reqVO.getHandlerName()) + .geIfPresent(JobLogDO::getBeginTime, reqVO.getBeginTime()) + .leIfPresent(JobLogDO::getEndTime, reqVO.getEndTime()) + .eqIfPresent(JobLogDO::getStatus, reqVO.getStatus()) + .orderByDesc(JobLogDO::getId) // ID 倒序 + ); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/job/JobMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/job/JobMapper.java new file mode 100644 index 0000000..0b4722e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/job/JobMapper.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.infra.dal.mysql.job; + +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 定时任务 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface JobMapper extends BaseMapperX { + + default JobDO selectByHandlerName(String handlerName) { + return selectOne(JobDO::getHandlerName, handlerName); + } + + default PageResult selectPage(JobPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(JobDO::getName, reqVO.getName()) + .eqIfPresent(JobDO::getStatus, reqVO.getStatus()) + .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName()) + ); + } + + default List selectList(JobExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(JobDO::getName, reqVO.getName()) + .eqIfPresent(JobDO::getStatus, reqVO.getStatus()) + .likeIfPresent(JobDO::getHandlerName, reqVO.getHandlerName()) + ); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/logger/ApiAccessLogMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/logger/ApiAccessLogMapper.java new file mode 100644 index 0000000..cc5c61b --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/logger/ApiAccessLogMapper.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.infra.dal.mysql.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * API 访问日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface ApiAccessLogMapper extends BaseMapperX { + + default PageResult selectPage(ApiAccessLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ApiAccessLogDO::getUserId, reqVO.getUserId()) + .eqIfPresent(ApiAccessLogDO::getUserType, reqVO.getUserType()) + .eqIfPresent(ApiAccessLogDO::getApplicationName, reqVO.getApplicationName()) + .likeIfPresent(ApiAccessLogDO::getRequestUrl, reqVO.getRequestUrl()) + .betweenIfPresent(ApiAccessLogDO::getBeginTime, reqVO.getBeginTime()) + .geIfPresent(ApiAccessLogDO::getDuration, reqVO.getDuration()) + .eqIfPresent(ApiAccessLogDO::getResultCode, reqVO.getResultCode()) + .orderByDesc(ApiAccessLogDO::getId) + ); + } + + default List selectList(ApiAccessLogExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(ApiAccessLogDO::getUserId, reqVO.getUserId()) + .eqIfPresent(ApiAccessLogDO::getUserType, reqVO.getUserType()) + .eqIfPresent(ApiAccessLogDO::getApplicationName, reqVO.getApplicationName()) + .likeIfPresent(ApiAccessLogDO::getRequestUrl, reqVO.getRequestUrl()) + .betweenIfPresent(ApiAccessLogDO::getBeginTime, reqVO.getBeginTime()) + .geIfPresent(ApiAccessLogDO::getDuration, reqVO.getDuration()) + .eqIfPresent(ApiAccessLogDO::getResultCode, reqVO.getResultCode()) + .orderByDesc(ApiAccessLogDO::getId) + ); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/logger/ApiErrorLogMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/logger/ApiErrorLogMapper.java new file mode 100644 index 0000000..4c10a94 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/logger/ApiErrorLogMapper.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.infra.dal.mysql.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * API 错误日志 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface ApiErrorLogMapper extends BaseMapperX { + + default PageResult selectPage(ApiErrorLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ApiErrorLogDO::getUserId, reqVO.getUserId()) + .eqIfPresent(ApiErrorLogDO::getUserType, reqVO.getUserType()) + .eqIfPresent(ApiErrorLogDO::getApplicationName, reqVO.getApplicationName()) + .likeIfPresent(ApiErrorLogDO::getRequestUrl, reqVO.getRequestUrl()) + .betweenIfPresent(ApiErrorLogDO::getExceptionTime, reqVO.getExceptionTime()) + .eqIfPresent(ApiErrorLogDO::getProcessStatus, reqVO.getProcessStatus()) + .orderByDesc(ApiErrorLogDO::getId) + ); + } + + default List selectList(ApiErrorLogExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(ApiErrorLogDO::getUserId, reqVO.getUserId()) + .eqIfPresent(ApiErrorLogDO::getUserType, reqVO.getUserType()) + .eqIfPresent(ApiErrorLogDO::getApplicationName, reqVO.getApplicationName()) + .likeIfPresent(ApiErrorLogDO::getRequestUrl, reqVO.getRequestUrl()) + .betweenIfPresent(ApiErrorLogDO::getExceptionTime, reqVO.getExceptionTime()) + .eqIfPresent(ApiErrorLogDO::getProcessStatus, reqVO.getProcessStatus()) + .orderByDesc(ApiErrorLogDO::getId) + ); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/test/TestDemoMapper.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/test/TestDemoMapper.java new file mode 100644 index 0000000..bf5d0ff --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/dal/mysql/test/TestDemoMapper.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.infra.dal.mysql.test; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.test.TestDemoDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 字典类型 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface TestDemoMapper extends BaseMapperX { + + default PageResult selectPage(TestDemoPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TestDemoDO::getName, reqVO.getName()) + .eqIfPresent(TestDemoDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TestDemoDO::getType, reqVO.getType()) + .eqIfPresent(TestDemoDO::getCategory, reqVO.getCategory()) + .eqIfPresent(TestDemoDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(TestDemoDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TestDemoDO::getId)); + } + + default List selectList(TestDemoExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(TestDemoDO::getName, reqVO.getName()) + .eqIfPresent(TestDemoDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TestDemoDO::getType, reqVO.getType()) + .eqIfPresent(TestDemoDO::getCategory, reqVO.getCategory()) + .eqIfPresent(TestDemoDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(TestDemoDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TestDemoDO::getId)); + } + + List selectList2(); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java new file mode 100644 index 0000000..a654060 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成器的字段 HTML 展示枚举 + */ +@AllArgsConstructor +@Getter +public enum CodegenColumnHtmlTypeEnum { + + INPUT("input"), // 文本框 + TEXTAREA("textarea"), // 文本域 + SELECT("select"), // 下拉框 + RADIO("radio"), // 单选框 + CHECKBOX("checkbox"), // 复选框 + DATETIME("datetime"), // 日期控件 + UPLOAD_IMAGE("upload_image"), // 上传图片 + UPLOAD_FILE("upload_file"), // 上传文件 + EDITOR("editor"), // 富文本控件 + ; + + /** + * 条件 + */ + private final String type; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenColumnListConditionEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenColumnListConditionEnum.java new file mode 100644 index 0000000..460c1c3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenColumnListConditionEnum.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成器的字段过滤条件枚举 + */ +@AllArgsConstructor +@Getter +public enum CodegenColumnListConditionEnum { + + EQ("="), + NE("!="), + GT(">"), + GTE(">="), + LT("<"), + LTE("<="), + LIKE("LIKE"), + BETWEEN("BETWEEN"); + + /** + * 条件 + */ + private final String condition; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenFrontTypeEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenFrontTypeEnum.java new file mode 100644 index 0000000..f89986f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenFrontTypeEnum.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成的前端类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenFrontTypeEnum { + + VUE2(10), // Vue2 Element UI 标准模版 + VUE3(20), // Vue3 Element Plus 标准模版 + VUE3_SCHEMA(21), // Vue3 Element Plus Schema 模版 + VUE3_VBEN(30), // Vue3 VBEN 模版 + ; + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenSceneEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenSceneEnum.java new file mode 100644 index 0000000..a045675 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenSceneEnum.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import static cn.hutool.core.util.ArrayUtil.*; + +/** + * 代码生成的场景枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenSceneEnum { + + ADMIN(1, "管理后台", "admin", ""), + APP(2, "用户 APP", "app", "App"); + + /** + * 场景 + */ + private final Integer scene; + /** + * 场景名 + */ + private final String name; + /** + * 基础包名 + */ + private final String basePackage; + /** + * Controller 和 VO 类的前缀 + */ + private final String prefixClass; + + public static CodegenSceneEnum valueOf(Integer scene) { + return firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene), values()); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenTemplateTypeEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenTemplateTypeEnum.java new file mode 100644 index 0000000..7f34b9e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/codegen/CodegenTemplateTypeEnum.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.infra.enums.codegen; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 代码生成模板类型 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CodegenTemplateTypeEnum { + + CRUD(1), // 单表(增删改查) + TREE(2), // 树表(增删改查) + ; + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/config/ConfigTypeEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/config/ConfigTypeEnum.java new file mode 100644 index 0000000..77f8758 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/config/ConfigTypeEnum.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.infra.enums.config; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum ConfigTypeEnum { + + /** + * 系统配置 + */ + SYSTEM(1), + /** + * 自定义配置 + */ + CUSTOM(2); + + private final Integer type; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/job/JobLogStatusEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/job/JobLogStatusEnum.java new file mode 100644 index 0000000..b1248f1 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/job/JobLogStatusEnum.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.infra.enums.job; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 任务日志的状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum JobLogStatusEnum { + + RUNNING(0), // 运行中 + SUCCESS(1), // 成功 + FAILURE(2); // 失败 + + /** + * 状态 + */ + private final Integer status; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/job/JobStatusEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/job/JobStatusEnum.java new file mode 100644 index 0000000..3f1bd10 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/job/JobStatusEnum.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.infra.enums.job; + +import com.google.common.collect.Sets; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.quartz.impl.jdbcjobstore.Constants; + +import java.util.Collections; +import java.util.Set; + +/** + * 任务状态的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum JobStatusEnum { + + /** + * 初始化中 + */ + INIT(0, Collections.emptySet()), + /** + * 开启 + */ + NORMAL(1, Sets.newHashSet(Constants.STATE_WAITING, Constants.STATE_ACQUIRED, Constants.STATE_BLOCKED)), + /** + * 暂停 + */ + STOP(2, Sets.newHashSet(Constants.STATE_PAUSED, Constants.STATE_PAUSED_BLOCKED)); + + /** + * 状态 + */ + private final Integer status; + /** + * 对应的 Quartz 触发器的状态集合 + */ + private final Set quartzStates; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java new file mode 100644 index 0000000..4ec672f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/logger/ApiErrorLogProcessStatusEnum.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.infra.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * API 异常数据的处理状态 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum ApiErrorLogProcessStatusEnum { + + INIT(0, "未处理"), + DONE(1, "已处理"), + IGNORE(2, "已忽略"); + + /** + * 状态 + */ + private final Integer status; + /** + * 资源类型名 + */ + private final String name; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/package-info.java new file mode 100644 index 0000000..59acf44 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/enums/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.infra.enums; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/config/CodegenConfiguration.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/config/CodegenConfiguration.java new file mode 100644 index 0000000..f782366 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/config/CodegenConfiguration.java @@ -0,0 +1,9 @@ +package com.yunxi.scm.module.infra.framework.codegen.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(CodegenProperties.class) +public class CodegenConfiguration { +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/config/CodegenProperties.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/config/CodegenProperties.java new file mode 100644 index 0000000..790f2ea --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/config/CodegenProperties.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.infra.framework.codegen.config; + +import com.yunxi.scm.module.infra.enums.codegen.CodegenFrontTypeEnum; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Collection; + +@ConfigurationProperties(prefix = "yunxi.codegen") +@Validated +@Data +public class CodegenProperties { + + /** + * 生成的 Java 代码的基础包 + */ + @NotNull(message = "Java 代码的基础包不能为空") + private String basePackage; + + /** + * 数据库名数组 + */ + @NotEmpty(message = "数据库不能为空") + private Collection dbSchemas; + + /** + * 代码生成的前端类型(默认) + * + * 枚举 {@link CodegenFrontTypeEnum#getType()} + */ + @NotNull(message = "代码生成的前端类型不能为空") + private Integer frontType; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/package-info.java new file mode 100644 index 0000000..e2ce454 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/codegen/package-info.java @@ -0,0 +1,4 @@ +/** + * 代码生成器 + */ +package com.yunxi.scm.module.infra.framework.codegen; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/config/AdminServerConfiguration.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/config/AdminServerConfiguration.java new file mode 100644 index 0000000..ec5aac8 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/config/AdminServerConfiguration.java @@ -0,0 +1,9 @@ +package com.yunxi.scm.module.infra.framework.monitor.config; + +import de.codecentric.boot.admin.server.config.EnableAdminServer; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableAdminServer +public class AdminServerConfiguration { +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/package-info.java new file mode 100644 index 0000000..c4c7324 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/package-info.java @@ -0,0 +1,4 @@ +/** + * 使用 Spring Boot Admin 实现简单的监控平台 + */ +package com.yunxi.scm.module.infra.framework.monitor; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md new file mode 100644 index 0000000..775c4d1 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/monitor/《芋道 Spring Boot 监控工具 Admin 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/package-info.java new file mode 100644 index 0000000..3f57c7f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 infra 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.infra.framework; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/security/config/SecurityConfiguration.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000..f412498 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.infra.framework.security.config; + +import com.yunxi.scm.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * Infra 模块的 Security 配置 + */ +@Configuration(proxyBeanMethods = false, value = "infraSecurityConfiguration") +public class SecurityConfiguration { + + @Value("${spring.boot.admin.context-path:''}") + private String adminSeverContextPath; + + @Bean("infraAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + // Swagger 接口文档 + registry.antMatchers("/v3/api-docs/**").permitAll() + .antMatchers("/swagger-ui.html").permitAll() + .antMatchers("/swagger-ui/**").permitAll() + .antMatchers("/swagger-resources/**").anonymous() + .antMatchers("/webjars/**").anonymous() + .antMatchers("/*/api-docs").anonymous(); + // 积木报表 + registry.antMatchers("/jmreport/**").permitAll(); + // Spring Boot Actuator 的安全配置 + registry.antMatchers("/actuator").anonymous() + .antMatchers("/actuator/**").anonymous(); + // Druid 监控 + registry.antMatchers("/druid/**").anonymous(); + // Spring Boot Admin Server 的安全配置 + registry.antMatchers(adminSeverContextPath).anonymous() + .antMatchers(adminSeverContextPath + "/**").anonymous(); + // 文件读取 + registry.antMatchers(buildAdminApi("/infra/file/*/get/**")).permitAll(); + } + + }; + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/security/core/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/security/core/package-info.java new file mode 100644 index 0000000..7c8cd03 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.infra.framework.security.core; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/web/config/InfraWebConfiguration.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/web/config/InfraWebConfiguration.java new file mode 100644 index 0000000..7264ec3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/web/config/InfraWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.infra.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * infra 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class InfraWebConfiguration { + + /** + * infra 模块的 API 分组 + */ + @Bean + public GroupedOpenApi infraGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("infra"); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/web/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/web/package-info.java new file mode 100644 index 0000000..0aaaa04 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * infra 模块的 web 配置 + */ +package com.yunxi.scm.module.infra.framework.web; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/consumer/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/consumer/package-info.java new file mode 100644 index 0000000..c99dc7f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/consumer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消费者 + */ +package com.yunxi.scm.module.infra.mq.consumer; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/message/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/message/package-info.java new file mode 100644 index 0000000..43f6599 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/message/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的消息 + */ +package com.yunxi.scm.module.infra.mq.message; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/producer/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/producer/package-info.java new file mode 100644 index 0000000..171e326 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/mq/producer/package-info.java @@ -0,0 +1,4 @@ +/** + * 消息队列的生产者 + */ +package com.yunxi.scm.module.infra.mq.producer; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/package-info.java new file mode 100644 index 0000000..2fe2edc --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/package-info.java @@ -0,0 +1,9 @@ +/** + * infra 模块,主要提供两块能力: + * 1. 我们放基础设施的运维与管理,支撑上层的通用与核心业务。 例如说:定时任务的管理、服务器的信息等等 + * 2. 研发工具,提升研发效率与质量。 例如说:代码生成器、接口文档等等 + * + * 1. Controller URL:以 /infra/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 infra_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.infra; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/CodegenService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/CodegenService.java new file mode 100644 index 0000000..471a8fe --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/CodegenService.java @@ -0,0 +1,94 @@ +package com.yunxi.scm.module.infra.service.codegen; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenTableDO; + +import java.util.List; +import java.util.Map; + +/** + * 代码生成 Service 接口 + * + * @author 芋道源码 + */ +public interface CodegenService { + + /** + * 基于数据库的表结构,创建代码生成器的表定义 + * + * @param userId 用户编号 + * @param reqVO 表信息 + * @return 创建的表定义的编号数组 + */ + List createCodegenList(Long userId, CodegenCreateListReqVO reqVO); + + /** + * 更新数据库的表和字段定义 + * + * @param updateReqVO 更新信息 + */ + void updateCodegen(CodegenUpdateReqVO updateReqVO); + + /** + * 基于数据库的表结构,同步数据库的表和字段定义 + * + * @param tableId 表编号 + */ + void syncCodegenFromDB(Long tableId); + + /** + * 删除数据库的表和字段定义 + * + * @param tableId 数据编号 + */ + void deleteCodegen(Long tableId); + + /** + * 获得表定义分页 + * + * @param pageReqVO 分页条件 + * @return 表定义分页 + */ + PageResult getCodegenTablePage(CodegenTablePageReqVO pageReqVO); + + /** + * 获得表定义 + * + * @param id 表编号 + * @return 表定义 + */ + CodegenTableDO getCodegenTablePage(Long id); + + /** + * 获得指定表的字段定义数组 + * + * @param tableId 表编号 + * @return 字段定义数组 + */ + List getCodegenColumnListByTableId(Long tableId); + + /** + * 执行指定表的代码生成 + * + * @param tableId 表编号 + * @return 生成结果。key 为文件路径,value 为对应的代码内容 + */ + Map generationCodes(Long tableId); + + /** + * 获得数据库自带的表定义列表 + * + * + * @param dataSourceConfigId 数据源的配置编号 + * @param name 表名称 + * @param comment 表描述 + * @return 表定义列表 + */ + List getDatabaseTableList(Long dataSourceConfigId, String name, String comment); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/CodegenServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/CodegenServiceImpl.java new file mode 100644 index 0000000..66ac81c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/CodegenServiceImpl.java @@ -0,0 +1,253 @@ +package com.yunxi.scm.module.infra.service.codegen; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO; +import com.yunxi.scm.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO; +import com.yunxi.scm.module.infra.convert.codegen.CodegenConvert; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.yunxi.scm.module.infra.dal.mysql.codegen.CodegenColumnMapper; +import com.yunxi.scm.module.infra.dal.mysql.codegen.CodegenTableMapper; +import com.yunxi.scm.module.infra.enums.codegen.CodegenSceneEnum; +import com.yunxi.scm.module.infra.framework.codegen.config.CodegenProperties; +import com.yunxi.scm.module.infra.service.codegen.inner.CodegenBuilder; +import com.yunxi.scm.module.infra.service.codegen.inner.CodegenEngine; +import com.yunxi.scm.module.infra.service.db.DatabaseTableService; +import com.yunxi.scm.module.system.api.user.AdminUserApi; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiPredicate; +import java.util.stream.Collectors; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.*; + +/** + * 代码生成 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class CodegenServiceImpl implements CodegenService { + + @Resource + private DatabaseTableService databaseTableService; + + @Resource + private CodegenTableMapper codegenTableMapper; + @Resource + private CodegenColumnMapper codegenColumnMapper; + + @Resource + private AdminUserApi userApi; + + @Resource + private CodegenBuilder codegenBuilder; + @Resource + private CodegenEngine codegenEngine; + + @Resource + private CodegenProperties codegenProperties; + + @Override + @Transactional(rollbackFor = Exception.class) + public List createCodegenList(Long userId, CodegenCreateListReqVO reqVO) { + List ids = new ArrayList<>(reqVO.getTableNames().size()); + // 遍历添加。虽然效率会低一点,但是没必要做成完全批量,因为不会这么大量 + reqVO.getTableNames().forEach(tableName -> ids.add(createCodegen(userId, reqVO.getDataSourceConfigId(), tableName))); + return ids; + } + + public Long createCodegen(Long userId, Long dataSourceConfigId, String tableName) { + // 从数据库中,获得数据库表结构 + TableInfo tableInfo = databaseTableService.getTable(dataSourceConfigId, tableName); + // 导入 + return createCodegen0(userId, dataSourceConfigId, tableInfo); + } + + private Long createCodegen0(Long userId, Long dataSourceConfigId, TableInfo tableInfo) { + // 校验导入的表和字段非空 + validateTableInfo(tableInfo); + // 校验是否已经存在 + if (codegenTableMapper.selectByTableNameAndDataSourceConfigId(tableInfo.getName(), + dataSourceConfigId) != null) { + throw exception(CODEGEN_TABLE_EXISTS); + } + + // 构建 CodegenTableDO 对象,插入到 DB 中 + CodegenTableDO table = codegenBuilder.buildTable(tableInfo); + table.setDataSourceConfigId(dataSourceConfigId); + table.setScene(CodegenSceneEnum.ADMIN.getScene()); // 默认配置下,使用管理后台的模板 + table.setFrontType(codegenProperties.getFrontType()); + table.setAuthor(userApi.getUser(userId).getNickname()); + codegenTableMapper.insert(table); + + // 构建 CodegenColumnDO 数组,插入到 DB 中 + List columns = codegenBuilder.buildColumns(table.getId(), tableInfo.getFields()); + // 如果没有主键,则使用第一个字段作为主键 + if (!tableInfo.isHavePrimaryKey()) { + columns.get(0).setPrimaryKey(true); + } + codegenColumnMapper.insertBatch(columns); + return table.getId(); + } + + private void validateTableInfo(TableInfo tableInfo) { + if (tableInfo == null) { + throw exception(CODEGEN_IMPORT_TABLE_NULL); + } + if (StrUtil.isEmpty(tableInfo.getComment())) { + throw exception(CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL); + } + if (CollUtil.isEmpty(tableInfo.getFields())) { + throw exception(CODEGEN_IMPORT_COLUMNS_NULL); + } + tableInfo.getFields().forEach(field -> { + if (StrUtil.isEmpty(field.getComment())) { + throw exception(CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL, field.getName()); + } + }); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCodegen(CodegenUpdateReqVO updateReqVO) { + // 校验是否已经存在 + if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + + // 更新 table 表定义 + CodegenTableDO updateTableObj = CodegenConvert.INSTANCE.convert(updateReqVO.getTable()); + codegenTableMapper.updateById(updateTableObj); + // 更新 column 字段定义 + List updateColumnObjs = CodegenConvert.INSTANCE.convertList03(updateReqVO.getColumns()); + updateColumnObjs.forEach(updateColumnObj -> codegenColumnMapper.updateById(updateColumnObj)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncCodegenFromDB(Long tableId) { + // 校验是否已经存在 + CodegenTableDO table = codegenTableMapper.selectById(tableId); + if (table == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + // 从数据库中,获得数据库表结构 + TableInfo tableInfo = databaseTableService.getTable(table.getDataSourceConfigId(), table.getTableName()); + // 执行同步 + syncCodegen0(tableId, tableInfo); + } + + private void syncCodegen0(Long tableId, TableInfo tableInfo) { + // 校验导入的表和字段非空 + validateTableInfo(tableInfo); + List tableFields = tableInfo.getFields(); + + // 构建 CodegenColumnDO 数组,只同步新增的字段 + List codegenColumns = codegenColumnMapper.selectListByTableId(tableId); + Set codegenColumnNames = CollectionUtils.convertSet(codegenColumns, CodegenColumnDO::getColumnName); + + //计算需要修改的字段,插入时重新插入,删除时将原来的删除 + BiPredicate pr = + (tableField, codegenColumn) -> tableField.getMetaInfo().getJdbcType().name().equals(codegenColumn.getDataType()) + && tableField.getMetaInfo().isNullable() == codegenColumn.getNullable() + && tableField.isKeyFlag() == codegenColumn.getPrimaryKey() + && tableField.getComment().equals(codegenColumn.getColumnComment()); + Map codegenColumnDOMap = CollectionUtils.convertMap(codegenColumns, CodegenColumnDO::getColumnName); + //需要修改的字段 + Set modifyFieldNames = tableFields.stream() + .filter(tableField -> codegenColumnDOMap.get(tableField.getColumnName()) != null + && !pr.test(tableField, codegenColumnDOMap.get(tableField.getColumnName()))) + .map(TableField::getColumnName) + .collect(Collectors.toSet()); + // 计算需要删除的字段 + Set tableFieldNames = CollectionUtils.convertSet(tableFields, TableField::getName); + Set deleteColumnIds = codegenColumns.stream() + .filter(column -> (!tableFieldNames.contains(column.getColumnName())) || modifyFieldNames.contains(column.getColumnName())) + .map(CodegenColumnDO::getId).collect(Collectors.toSet()); + // 移除已经存在的字段 + tableFields.removeIf(column -> codegenColumnNames.contains(column.getColumnName()) && (!modifyFieldNames.contains(column.getColumnName()))); + if (CollUtil.isEmpty(tableFields) && CollUtil.isEmpty(deleteColumnIds)) { + throw exception(CODEGEN_SYNC_NONE_CHANGE); + } + + // 插入新增的字段 + List columns = codegenBuilder.buildColumns(tableId, tableFields); + codegenColumnMapper.insertBatch(columns); + // 删除不存在的字段 + if (CollUtil.isNotEmpty(deleteColumnIds)) { + codegenColumnMapper.deleteBatchIds(deleteColumnIds); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteCodegen(Long tableId) { + // 校验是否已经存在 + if (codegenTableMapper.selectById(tableId) == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + + // 删除 table 表定义 + codegenTableMapper.deleteById(tableId); + // 删除 column 字段定义 + codegenColumnMapper.deleteListByTableId(tableId); + } + + @Override + public PageResult getCodegenTablePage(CodegenTablePageReqVO pageReqVO) { + return codegenTableMapper.selectPage(pageReqVO); + } + + @Override + public CodegenTableDO getCodegenTablePage(Long id) { + return codegenTableMapper.selectById(id); + } + + @Override + public List getCodegenColumnListByTableId(Long tableId) { + return codegenColumnMapper.selectListByTableId(tableId); + } + + @Override + public Map generationCodes(Long tableId) { + // 校验是否已经存在 + CodegenTableDO table = codegenTableMapper.selectById(tableId); + if (table == null) { + throw exception(CODEGEN_TABLE_NOT_EXISTS); + } + List columns = codegenColumnMapper.selectListByTableId(tableId); + if (CollUtil.isEmpty(columns)) { + throw exception(CODEGEN_COLUMN_NOT_EXISTS); + } + + // 执行生成 + return codegenEngine.execute(table, columns); + } + + @Override + public List getDatabaseTableList(Long dataSourceConfigId, String name, String comment) { + List tables = databaseTableService.getTableList(dataSourceConfigId, name, comment); + // 移除已经生成的表 + // 移除在 Codegen 中,已经存在的 + Set existsTables = CollectionUtils.convertSet( + codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId), CodegenTableDO::getTableName); + tables.removeIf(table -> existsTables.contains(table.getName())); + return CodegenConvert.INSTANCE.convertList04(tables); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/inner/CodegenBuilder.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/inner/CodegenBuilder.java new file mode 100644 index 0000000..836f10f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/inner/CodegenBuilder.java @@ -0,0 +1,213 @@ +package com.yunxi.scm.module.infra.service.codegen.inner; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.infra.convert.codegen.CodegenConvert; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.yunxi.scm.module.infra.enums.codegen.CodegenColumnHtmlTypeEnum; +import com.yunxi.scm.module.infra.enums.codegen.CodegenColumnListConditionEnum; +import com.yunxi.scm.module.infra.enums.codegen.CodegenTemplateTypeEnum; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.google.common.collect.Sets; +import org.springframework.stereotype.Component; + +import java.util.*; + +import static cn.hutool.core.text.CharSequenceUtil.*; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.hutool.core.util.RandomUtil.randomInt; + +/** + * 代码生成器的 Builder,负责: + * 1. 将数据库的表 {@link TableInfo} 定义,构建成 {@link CodegenTableDO} + * 2. 将数据库的列 {@link TableField} 构定义,建成 {@link CodegenColumnDO} + */ +@Component +public class CodegenBuilder { + + /** + * 字段名与 {@link CodegenColumnListConditionEnum} 的默认映射 + * 注意,字段的匹配以后缀的方式 + */ + private static final Map COLUMN_LIST_OPERATION_CONDITION_MAPPINGS = + MapUtil.builder() + .put("name", CodegenColumnListConditionEnum.LIKE) + .put("time", CodegenColumnListConditionEnum.BETWEEN) + .put("date", CodegenColumnListConditionEnum.BETWEEN) + .build(); + + /** + * 字段名与 {@link CodegenColumnHtmlTypeEnum} 的默认映射 + * 注意,字段的匹配以后缀的方式 + */ + private static final Map COLUMN_HTML_TYPE_MAPPINGS = + MapUtil.builder() + .put("status", CodegenColumnHtmlTypeEnum.RADIO) + .put("sex", CodegenColumnHtmlTypeEnum.RADIO) + .put("type", CodegenColumnHtmlTypeEnum.SELECT) + .put("image", CodegenColumnHtmlTypeEnum.UPLOAD_IMAGE) + .put("file", CodegenColumnHtmlTypeEnum.UPLOAD_FILE) + .put("content", CodegenColumnHtmlTypeEnum.EDITOR) + .put("description", CodegenColumnHtmlTypeEnum.EDITOR) + .put("demo", CodegenColumnHtmlTypeEnum.EDITOR) + .put("time", CodegenColumnHtmlTypeEnum.DATETIME) + .put("date", CodegenColumnHtmlTypeEnum.DATETIME) + .build(); + + /** + * 多租户编号的字段名 + */ + public static final String TENANT_ID_FIELD = "tenantId"; + /** + * {@link com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO} 的字段 + */ + public static final Set BASE_DO_FIELDS = new HashSet<>(); + /** + * 新增操作,不需要传递的字段 + */ + private static final Set CREATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); + /** + * 修改操作,不需要传递的字段 + */ + private static final Set UPDATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet(); + /** + * 列表操作的条件,不需要传递的字段 + */ + private static final Set LIST_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); + /** + * 列表操作的结果,不需要返回的字段 + */ + private static final Set LIST_OPERATION_RESULT_EXCLUDE_COLUMN = Sets.newHashSet(); + + static { + Arrays.stream(ReflectUtil.getFields(BaseDO.class)).forEach(field -> BASE_DO_FIELDS.add(field.getName())); + BASE_DO_FIELDS.add(TENANT_ID_FIELD); + // 处理 OPERATION 相关的字段 + CREATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是可能需要传递的 + LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是需要返回的 + } + + public CodegenTableDO buildTable(TableInfo tableInfo) { + CodegenTableDO table = CodegenConvert.INSTANCE.convert(tableInfo); + initTableDefault(table); + return table; + } + + /** + * 初始化 Table 表的默认字段 + * + * @param table 表定义 + */ + private void initTableDefault(CodegenTableDO table) { + // 以 system_dept 举例子。moduleName 为 system、businessName 为 dept、className 为 Dept + // 如果希望以 System 前缀,则可以手动在【代码生成 - 修改生成配置 - 基本信息】,将实体类名称改为 SystemDept 即可 + String tableName = table.getTableName().toLowerCase(); + // 第一步,_ 前缀的前面,作为 module 名字;第二步,moduleName 必须小写; + table.setModuleName(subBefore(tableName, '_', false).toLowerCase()); + // 第一步,第一个 _ 前缀的后面,作为 module 名字; 第二步,可能存在多个 _ 的情况,转换成驼峰; 第三步,businessName 必须小写; + table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase()); + // 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名 + table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false)))); + // 去除结尾的表,作为类描述 + table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表")); + table.setTemplateType(CodegenTemplateTypeEnum.CRUD.getType()); + } + + public List buildColumns(Long tableId, List tableFields) { + List columns = CodegenConvert.INSTANCE.convertList(tableFields); + int index = 1; + for (CodegenColumnDO column : columns) { + column.setTableId(tableId); + column.setOrdinalPosition(index++); + // 初始化 Column 列的默认字段 + processColumnOperation(column); // 处理 CRUD 相关的字段的默认值 + processColumnUI(column); // 处理 UI 相关的字段的默认值 + processColumnExample(column); // 处理字段的 swagger example 示例 + } + return columns; + } + + private void processColumnOperation(CodegenColumnDO column) { + // 处理 createOperation 字段 + column.setCreateOperation(!CREATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + && !column.getPrimaryKey()); // 对于主键,创建时无需传递 + // 处理 updateOperation 字段 + column.setUpdateOperation(!UPDATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + || column.getPrimaryKey()); // 对于主键,更新时需要传递 + // 处理 listOperation 字段 + column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) + && !column.getPrimaryKey()); // 对于主键,列表过滤不需要传递 + // 处理 listOperationCondition 字段 + COLUMN_LIST_OPERATION_CONDITION_MAPPINGS.entrySet().stream() + .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) + .findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition())); + if (column.getListOperationCondition() == null) { + column.setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition()); + } + // 处理 listOperationResult 字段 + column.setListOperationResult(!LIST_OPERATION_RESULT_EXCLUDE_COLUMN.contains(column.getJavaField())); + } + + private void processColumnUI(CodegenColumnDO column) { + // 基于后缀进行匹配 + COLUMN_HTML_TYPE_MAPPINGS.entrySet().stream() + .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) + .findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType())); + // 如果是 Boolean 类型时,设置为 radio 类型. + // 其它类型,因为字段名可以相对保障,所以不进行处理。例如说 date 对应 datetime 类型. + if (Boolean.class.getSimpleName().equals(column.getJavaType())) { + column.setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType()); + } + // 兜底,设置默认为 input 类型 + if (column.getHtmlType() == null) { + column.setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType()); + } + } + + /** + * 处理字段的 swagger example 示例 + * + * @param column 字段 + */ + private void processColumnExample(CodegenColumnDO column) { + // id、price、count 等可能是整数的后缀 + if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "id", "price", "count")) { + column.setExample(String.valueOf(randomInt(1, Short.MAX_VALUE))); + return; + } + // name + if (StrUtil.endWithIgnoreCase(column.getJavaField(), "name")) { + column.setExample(randomEle(new String[]{"张三", "李四", "王五", "赵六", "芋艿"})); + return; + } + // status + if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "status", "type")) { + column.setExample(randomEle(new String[]{"1", "2"})); + return; + } + // url + if (StrUtil.endWithIgnoreCase(column.getColumnName(), "url")) { + column.setExample("https://www.iocoder.cn"); + return; + } + // reason + if (StrUtil.endWithIgnoreCase(column.getColumnName(), "reason")) { + column.setExample(randomEle(new String[]{"不喜欢", "不对", "不好", "不香"})); + return; + } + // description、memo、remark + if (StrUtil.endWithAnyIgnoreCase(column.getColumnName(), "description", "memo", "remark")) { + column.setExample(randomEle(new String[]{"你猜", "随便", "你说的对"})); + return; + } + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/inner/CodegenEngine.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/inner/CodegenEngine.java new file mode 100644 index 0000000..04c0832 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/codegen/inner/CodegenEngine.java @@ -0,0 +1,301 @@ +package com.yunxi.scm.module.infra.service.codegen.inner; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.template.TemplateConfig; +import cn.hutool.extra.template.TemplateEngine; +import cn.hutool.extra.template.engine.velocity.VelocityEngine; +import com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.yunxi.scm.module.infra.enums.codegen.CodegenFrontTypeEnum; +import com.yunxi.scm.module.infra.enums.codegen.CodegenSceneEnum; +import com.yunxi.scm.module.infra.framework.codegen.config.CodegenProperties; +import com.google.common.collect.ImmutableTable; +import com.google.common.collect.Maps; +import com.google.common.collect.Table; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static cn.hutool.core.map.MapUtil.getStr; +import static cn.hutool.core.text.CharSequenceUtil.*; + +/** + * 代码生成的引擎,用于具体生成代码 + * 目前基于 {@link org.apache.velocity.app.Velocity} 模板引擎实现 + * + * 考虑到 Java 模板引擎的框架非常多,Freemarker、Velocity、Thymeleaf 等等,所以我们采用 hutool 封装的 {@link cn.hutool.extra.template.Template} 抽象 + * + * @author 芋道源码 + */ +@Component +public class CodegenEngine { + + /** + * 后端的模板配置 + * + * key:模板在 resources 的地址 + * value:生成的路径 + */ + private static final Map SERVER_TEMPLATES = MapUtil.builder(new LinkedHashMap<>()) // 有序 + // Java module-biz Main + .put(javaTemplatePath("controller/vo/baseVO"), javaModuleImplVOFilePath("BaseVO")) + .put(javaTemplatePath("controller/vo/createReqVO"), javaModuleImplVOFilePath("CreateReqVO")) + .put(javaTemplatePath("controller/vo/pageReqVO"), javaModuleImplVOFilePath("PageReqVO")) + .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO")) + .put(javaTemplatePath("controller/vo/updateReqVO"), javaModuleImplVOFilePath("UpdateReqVO")) + .put(javaTemplatePath("controller/vo/exportReqVO"), javaModuleImplVOFilePath("ExportReqVO")) + .put(javaTemplatePath("controller/vo/excelVO"), javaModuleImplVOFilePath("ExcelVO")) + .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath()) + .put(javaTemplatePath("convert/convert"), + javaModuleImplMainFilePath("convert/${table.businessName}/${table.className}Convert")) + .put(javaTemplatePath("dal/do"), + javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO")) + .put(javaTemplatePath("dal/mapper"), + javaModuleImplMainFilePath("dal/mysql/${table.businessName}/${table.className}Mapper")) + .put(javaTemplatePath("dal/mapper.xml"), mapperXmlFilePath()) + .put(javaTemplatePath("service/serviceImpl"), + javaModuleImplMainFilePath("service/${table.businessName}/${table.className}ServiceImpl")) + .put(javaTemplatePath("service/service"), + javaModuleImplMainFilePath("service/${table.businessName}/${table.className}Service")) + // Java module-biz Test + .put(javaTemplatePath("test/serviceTest"), + javaModuleImplTestFilePath("service/${table.businessName}/${table.className}ServiceImplTest")) + // Java module-api Main + .put(javaTemplatePath("enums/errorcode"), javaModuleApiMainFilePath("enums/ErrorCodeConstants_手动操作")) + // SQL + .put("codegen/sql/sql.vm", "sql/sql.sql") + .put("codegen/sql/h2.vm", "sql/h2.sql") + .build(); + + /** + * 后端的配置模版 + * + * key1:UI 模版的类型 {@link CodegenFrontTypeEnum#getType()} + * key2:模板在 resources 的地址 + * value:生成的路径 + */ + private static final Table FRONT_TEMPLATES = ImmutableTable.builder() + // Vue2 标准模版 + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/index.vue"), + vueFilePath("views/${table.moduleName}/${classNameVar}/index.vue")) + .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("api/api.js"), + vueFilePath("api/${table.moduleName}/${classNameVar}.js")) + // Vue3 标准模版 + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${classNameVar}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${classNameVar}/${simpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${classNameVar}/index.ts")) + // Vue3 Schema 模版 + .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"), + vue3FilePath("views/${table.moduleName}/${classNameVar}/${classNameVar}.data.ts")) + .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${classNameVar}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${classNameVar}/${simpleClassName}Form.vue")) + .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${classNameVar}/index.ts")) + // Vue3 vben 模版 + .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"), + vue3FilePath("views/${table.moduleName}/${classNameVar}/${classNameVar}.data.ts")) + .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/index.vue"), + vue3FilePath("views/${table.moduleName}/${classNameVar}/index.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/form.vue"), + vue3FilePath("views/${table.moduleName}/${classNameVar}/${simpleClassName}Modal.vue")) + .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("api/api.ts"), + vue3FilePath("api/${table.moduleName}/${classNameVar}/index.ts")) + .build(); + + @Resource + private CodegenProperties codegenProperties; + + /** + * 模板引擎,由 hutool 实现 + */ + private final TemplateEngine templateEngine; + /** + * 全局通用变量映射 + */ + private final Map globalBindingMap = new HashMap<>(); + + public CodegenEngine() { + // 初始化 TemplateEngine 属性 + TemplateConfig config = new TemplateConfig(); + config.setResourceMode(TemplateConfig.ResourceMode.CLASSPATH); + this.templateEngine = new VelocityEngine(config); + } + + @PostConstruct + private void initGlobalBindingMap() { + // 全局配置 + globalBindingMap.put("basePackage", codegenProperties.getBasePackage()); + globalBindingMap.put("baseFrameworkPackage", codegenProperties.getBasePackage() + + '.' + "framework"); // 用于后续获取测试类的 package 地址 + // 全局 Java Bean + globalBindingMap.put("CommonResultClassName", CommonResult.class.getName()); + globalBindingMap.put("PageResultClassName", PageResult.class.getName()); + // VO 类,独有字段 + globalBindingMap.put("PageParamClassName", PageParam.class.getName()); + globalBindingMap.put("DictFormatClassName", DictFormat.class.getName()); + // DO 类,独有字段 + globalBindingMap.put("BaseDOClassName", BaseDO.class.getName()); + globalBindingMap.put("baseDOFields", CodegenBuilder.BASE_DO_FIELDS); + globalBindingMap.put("QueryWrapperClassName", LambdaQueryWrapperX.class.getName()); + globalBindingMap.put("BaseMapperClassName", BaseMapperX.class.getName()); + // Util 工具类 + globalBindingMap.put("ServiceExceptionUtilClassName", ServiceExceptionUtil.class.getName()); + globalBindingMap.put("DateUtilsClassName", DateUtils.class.getName()); + globalBindingMap.put("ExcelUtilsClassName", ExcelUtils.class.getName()); + globalBindingMap.put("LocalDateTimeUtilsClassName", LocalDateTimeUtils.class.getName()); + globalBindingMap.put("ObjectUtilsClassName", ObjectUtils.class.getName()); + globalBindingMap.put("DictConvertClassName", DictConvert.class.getName()); + globalBindingMap.put("OperateLogClassName", OperateLog.class.getName()); + globalBindingMap.put("OperateTypeEnumClassName", OperateTypeEnum.class.getName()); + } + + public Map execute(CodegenTableDO table, List columns) { + // 创建 bindingMap + Map bindingMap = new HashMap<>(globalBindingMap); + bindingMap.put("table", table); + bindingMap.put("columns", columns); + bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); // 主键字段 + bindingMap.put("sceneEnum", CodegenSceneEnum.valueOf(table.getScene())); + + // className 相关 + // 去掉指定前缀,将 TestDictType 转换成 DictType. 因为在 create 等方法后,不需要带上 Test 前缀 + String simpleClassName = removePrefix(table.getClassName(), upperFirst(table.getModuleName())); + bindingMap.put("simpleClassName", simpleClassName); + bindingMap.put("simpleClassName_underlineCase", toUnderlineCase(simpleClassName)); // 将 DictType 转换成 dict_type + bindingMap.put("classNameVar", lowerFirst(simpleClassName)); // 将 DictType 转换成 dictType,用于变量 + // 将 DictType 转换成 dict-type + String simpleClassNameStrikeCase = toSymbolCase(simpleClassName, '-'); + bindingMap.put("simpleClassName_strikeCase", simpleClassNameStrikeCase); + // permission 前缀 + bindingMap.put("permissionPrefix", table.getModuleName() + ":" + simpleClassNameStrikeCase); + + // 执行生成 + Map templates = getTemplates(table.getFrontType()); + Map result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序 + templates.forEach((vmPath, filePath) -> { + filePath = formatFilePath(filePath, bindingMap); + String content = templateEngine.getTemplate(vmPath).render(bindingMap); + // 去除字段后面多余的 , 逗号 + content = content.replaceAll(",\n}", "\n}").replaceAll(",\n }", "\n }"); + result.put(filePath, content); + }); + return result; + } + + private Map getTemplates(Integer frontType) { + Map templates = new LinkedHashMap<>(); + templates.putAll(SERVER_TEMPLATES); + templates.putAll(FRONT_TEMPLATES.row(frontType)); + return templates; + } + + private String formatFilePath(String filePath, Map bindingMap) { + filePath = StrUtil.replace(filePath, "${basePackage}", + getStr(bindingMap, "basePackage").replaceAll("\\.", "/")); + filePath = StrUtil.replace(filePath, "${classNameVar}", + getStr(bindingMap, "classNameVar")); + filePath = StrUtil.replace(filePath, "${simpleClassName}", + getStr(bindingMap, "simpleClassName")); + // sceneEnum 包含的字段 + CodegenSceneEnum sceneEnum = (CodegenSceneEnum) bindingMap.get("sceneEnum"); + filePath = StrUtil.replace(filePath, "${sceneEnum.prefixClass}", sceneEnum.getPrefixClass()); + filePath = StrUtil.replace(filePath, "${sceneEnum.basePackage}", sceneEnum.getBasePackage()); + // table 包含的字段 + CodegenTableDO table = (CodegenTableDO) bindingMap.get("table"); + filePath = StrUtil.replace(filePath, "${table.moduleName}", table.getModuleName()); + filePath = StrUtil.replace(filePath, "${table.businessName}", table.getBusinessName()); + filePath = StrUtil.replace(filePath, "${table.className}", table.getClassName()); + return filePath; + } + + private static String javaTemplatePath(String path) { + return "codegen/java/" + path + ".vm"; + } + + private static String javaModuleImplVOFilePath(String path) { + return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + + "vo/${sceneEnum.prefixClass}${table.className}" + path, "biz", "main"); + } + + private static String javaModuleImplControllerFilePath() { + return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + + "${sceneEnum.prefixClass}${table.className}Controller", "biz", "main"); + } + + private static String javaModuleImplMainFilePath(String path) { + return javaModuleFilePath(path, "biz", "main"); + } + + private static String javaModuleApiMainFilePath(String path) { + return javaModuleFilePath(path, "api", "main"); + } + + private static String javaModuleImplTestFilePath(String path) { + return javaModuleFilePath(path, "biz", "test"); + } + + private static String javaModuleFilePath(String path, String module, String src) { + return "yunxi-module-${table.moduleName}/" + // 顶级模块 + "yunxi-module-${table.moduleName}-" + module + "/" + // 子模块 + "src/" + src + "/java/${basePackage}/module/${table.moduleName}/" + path + ".java"; + } + + private static String mapperXmlFilePath() { + return "yunxi-module-${table.moduleName}/" + // 顶级模块 + "yunxi-module-${table.moduleName}-biz/" + // 子模块 + "src/main/resources/mapper/${table.businessName}/${table.className}Mapper.xml"; + } + + private static String vueTemplatePath(String path) { + return "codegen/vue/" + path + ".vm"; + } + + private static String vueFilePath(String path) { + return "yunxi-ui-${sceneEnum.basePackage}/" + // 顶级目录 + "src/" + path; + } + + private static String vue3TemplatePath(String path) { + return "codegen/vue3/" + path + ".vm"; + } + + private static String vue3FilePath(String path) { + return "yunxi-ui-${sceneEnum.basePackage}-vue3/" + // 顶级目录 + "src/" + path; + } + + private static String vue3SchemaTemplatePath(String path) { + return "codegen/vue3_schema/" + path + ".vm"; + } + + private static String vue3VbenTemplatePath(String path) { + return "codegen/vue3_vben/" + path + ".vm"; + } +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/config/ConfigService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/config/ConfigService.java new file mode 100644 index 0000000..2456859 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/config/ConfigService.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.infra.service.config; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.config.ConfigDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 参数配置 Service 接口 + * + * @author 芋道源码 + */ +public interface ConfigService { + + /** + * 创建参数配置 + * + * @param reqVO 创建信息 + * @return 配置编号 + */ + Long createConfig(@Valid ConfigCreateReqVO reqVO); + + /** + * 更新参数配置 + * + * @param reqVO 更新信息 + */ + void updateConfig(@Valid ConfigUpdateReqVO reqVO); + + /** + * 删除参数配置 + * + * @param id 配置编号 + */ + void deleteConfig(Long id); + + /** + * 获得参数配置 + * + * @param id 配置编号 + * @return 参数配置 + */ + ConfigDO getConfig(Long id); + + /** + * 根据参数键,获得参数配置 + * + * @param key 配置键 + * @return 参数配置 + */ + ConfigDO getConfigByKey(String key); + + /** + * 获得参数配置分页列表 + * + * @param reqVO 分页条件 + * @return 分页列表 + */ + PageResult getConfigPage(@Valid ConfigPageReqVO reqVO); + + /** + * 获得参数配置列表 + * + * @param reqVO 列表 + * @return 列表 + */ + List getConfigList(@Valid ConfigExportReqVO reqVO); + + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/config/ConfigServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/config/ConfigServiceImpl.java new file mode 100644 index 0000000..745db3e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/config/ConfigServiceImpl.java @@ -0,0 +1,123 @@ +package com.yunxi.scm.module.infra.service.config; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigUpdateReqVO; +import com.yunxi.scm.module.infra.convert.config.ConfigConvert; +import com.yunxi.scm.module.infra.dal.dataobject.config.ConfigDO; +import com.yunxi.scm.module.infra.dal.mysql.config.ConfigMapper; +import com.yunxi.scm.module.infra.enums.config.ConfigTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.*; + +/** + * 参数配置 Service 实现类 + */ +@Service +@Slf4j +@Validated +public class ConfigServiceImpl implements ConfigService { + + @Resource + private ConfigMapper configMapper; + + @Override + public Long createConfig(ConfigCreateReqVO reqVO) { + // 校验正确性 + validateConfigForCreateOrUpdate(null, reqVO.getKey()); + // 插入参数配置 + ConfigDO config = ConfigConvert.INSTANCE.convert(reqVO); + config.setType(ConfigTypeEnum.CUSTOM.getType()); + configMapper.insert(config); + return config.getId(); + } + + @Override + public void updateConfig(ConfigUpdateReqVO reqVO) { + // 校验正确性 + validateConfigForCreateOrUpdate(reqVO.getId(), null); // 不允许更新 key + // 更新参数配置 + ConfigDO updateObj = ConfigConvert.INSTANCE.convert(reqVO); + configMapper.updateById(updateObj); + } + + @Override + public void deleteConfig(Long id) { + // 校验配置存在 + ConfigDO config = validateConfigExists(id); + // 内置配置,不允许删除 + if (ConfigTypeEnum.SYSTEM.getType().equals(config.getType())) { + throw exception(CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE); + } + // 删除 + configMapper.deleteById(id); + } + + @Override + public ConfigDO getConfig(Long id) { + return configMapper.selectById(id); + } + + @Override + public ConfigDO getConfigByKey(String key) { + return configMapper.selectByKey(key); + } + + @Override + public PageResult getConfigPage(ConfigPageReqVO reqVO) { + return configMapper.selectPage(reqVO); + } + + @Override + public List getConfigList(ConfigExportReqVO reqVO) { + return configMapper.selectList(reqVO); + } + + private void validateConfigForCreateOrUpdate(Long id, String key) { + // 校验自己存在 + validateConfigExists(id); + // 校验参数配置 key 的唯一性 + if (StrUtil.isNotEmpty(key)) { + validateConfigKeyUnique(id, key); + } + } + + @VisibleForTesting + public ConfigDO validateConfigExists(Long id) { + if (id == null) { + return null; + } + ConfigDO config = configMapper.selectById(id); + if (config == null) { + throw exception(CONFIG_NOT_EXISTS); + } + return config; + } + + @VisibleForTesting + public void validateConfigKeyUnique(Long id, String key) { + ConfigDO config = configMapper.selectByKey(key); + if (config == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的参数配置 + if (id == null) { + throw exception(CONFIG_KEY_DUPLICATE); + } + if (!config.getId().equals(id)) { + throw exception(CONFIG_KEY_DUPLICATE); + } + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigService.java new file mode 100644 index 0000000..1a8b33e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigService.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.infra.service.db; + +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 数据源配置 Service 接口 + * + * @author 芋道源码 + */ +public interface DataSourceConfigService { + + /** + * 创建数据源配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDataSourceConfig(@Valid DataSourceConfigCreateReqVO createReqVO); + + /** + * 更新数据源配置 + * + * @param updateReqVO 更新信息 + */ + void updateDataSourceConfig(@Valid DataSourceConfigUpdateReqVO updateReqVO); + + /** + * 删除数据源配置 + * + * @param id 编号 + */ + void deleteDataSourceConfig(Long id); + + /** + * 获得数据源配置 + * + * @param id 编号 + * @return 数据源配置 + */ + DataSourceConfigDO getDataSourceConfig(Long id); + + /** + * 获得数据源配置列表 + * + * @return 数据源配置列表 + */ + List getDataSourceConfigList(); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigServiceImpl.java new file mode 100644 index 0000000..f7f4bf7 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigServiceImpl.java @@ -0,0 +1,107 @@ +package com.yunxi.scm.module.infra.service.db; + +import com.yunxi.scm.framework.mybatis.core.util.JdbcUtils; +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigUpdateReqVO; +import com.yunxi.scm.module.infra.convert.db.DataSourceConfigConvert; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.yunxi.scm.module.infra.dal.mysql.db.DataSourceConfigMapper; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_EXISTS; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_OK; + +/** + * 数据源配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class DataSourceConfigServiceImpl implements DataSourceConfigService { + + @Resource + private DataSourceConfigMapper dataSourceConfigMapper; + + @Resource + private DynamicDataSourceProperties dynamicDataSourceProperties; + + @Override + public Long createDataSourceConfig(DataSourceConfigCreateReqVO createReqVO) { + DataSourceConfigDO dataSourceConfig = DataSourceConfigConvert.INSTANCE.convert(createReqVO); + validateConnectionOK(dataSourceConfig); + + // 插入 + dataSourceConfigMapper.insert(dataSourceConfig); + // 返回 + return dataSourceConfig.getId(); + } + + @Override + public void updateDataSourceConfig(DataSourceConfigUpdateReqVO updateReqVO) { + // 校验存在 + validateDataSourceConfigExists(updateReqVO.getId()); + DataSourceConfigDO updateObj = DataSourceConfigConvert.INSTANCE.convert(updateReqVO); + validateConnectionOK(updateObj); + + // 更新 + dataSourceConfigMapper.updateById(updateObj); + } + + @Override + public void deleteDataSourceConfig(Long id) { + // 校验存在 + validateDataSourceConfigExists(id); + // 删除 + dataSourceConfigMapper.deleteById(id); + } + + private void validateDataSourceConfigExists(Long id) { + if (dataSourceConfigMapper.selectById(id) == null) { + throw exception(DATA_SOURCE_CONFIG_NOT_EXISTS); + } + } + + @Override + public DataSourceConfigDO getDataSourceConfig(Long id) { + // 如果 id 为 0,默认为 master 的数据源 + if (Objects.equals(id, DataSourceConfigDO.ID_MASTER)) { + return buildMasterDataSourceConfig(); + } + // 从 DB 中读取 + return dataSourceConfigMapper.selectById(id); + } + + @Override + public List getDataSourceConfigList() { + List result = dataSourceConfigMapper.selectList(); + // 补充 master 数据源 + result.add(0, buildMasterDataSourceConfig()); + return result; + } + + private void validateConnectionOK(DataSourceConfigDO config) { + boolean success = JdbcUtils.isConnectionOK(config.getUrl(), config.getUsername(), config.getPassword()); + if (!success) { + throw exception(DATA_SOURCE_CONFIG_NOT_OK); + } + } + + private DataSourceConfigDO buildMasterDataSourceConfig() { + String primary = dynamicDataSourceProperties.getPrimary(); + DataSourceProperty dataSourceProperty = dynamicDataSourceProperties.getDatasource().get(primary); + return new DataSourceConfigDO().setId(DataSourceConfigDO.ID_MASTER).setName(primary) + .setUrl(dataSourceProperty.getUrl()) + .setUsername(dataSourceProperty.getUsername()) + .setPassword(dataSourceProperty.getPassword()); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DatabaseTableService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DatabaseTableService.java new file mode 100644 index 0000000..e84b1e2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DatabaseTableService.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.infra.service.db; + +import com.baomidou.mybatisplus.generator.config.po.TableInfo; + +import java.util.List; + +/** + * 数据库表 Service + * + * @author 芋道源码 + */ +public interface DatabaseTableService { + + /** + * 获得表列表,基于表名称 + 表描述进行模糊匹配 + * + * @param dataSourceConfigId 数据源配置的编号 + * @param nameLike 表名称,模糊匹配 + * @param commentLike 表描述,模糊匹配 + * @return 表列表 + */ + List getTableList(Long dataSourceConfigId, String nameLike, String commentLike); + + /** + * 获得指定表名 + * + * @param dataSourceConfigId 数据源配置的编号 + * @param tableName 表名称 + * @return 表 + */ + TableInfo getTable(Long dataSourceConfigId, String tableName); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DatabaseTableServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DatabaseTableServiceImpl.java new file mode 100644 index 0000000..4edd074 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/db/DatabaseTableServiceImpl.java @@ -0,0 +1,69 @@ +package com.yunxi.scm.module.infra.service.db; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.GlobalConfig; +import com.baomidou.mybatisplus.generator.config.StrategyConfig; +import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.baomidou.mybatisplus.generator.config.rules.DateType; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 数据库表 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class DatabaseTableServiceImpl implements DatabaseTableService { + + @Resource + private DataSourceConfigService dataSourceConfigService; + + @Override + public List getTableList(Long dataSourceConfigId, String nameLike, String commentLike) { + List tables = getTableList0(dataSourceConfigId, null); + return tables.stream().filter(tableInfo -> (StrUtil.isEmpty(nameLike) || tableInfo.getName().contains(nameLike)) + && (StrUtil.isEmpty(commentLike) || tableInfo.getComment().contains(commentLike))) + .collect(Collectors.toList()); + } + + @Override + public TableInfo getTable(Long dataSourceConfigId, String name) { + return CollUtil.getFirst(getTableList0(dataSourceConfigId, name)); + } + + private List getTableList0(Long dataSourceConfigId, String name) { + // 获得数据源配置 + DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId); + Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId); + + // 使用 MyBatis Plus Generator 解析表结构 + DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(), + config.getPassword()).build(); + StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder(); + if (StrUtil.isNotEmpty(name)) { + strategyConfig.addInclude(name); + } else { + // 移除工作流和定时任务前缀的表名 // TODO 未来做成可配置 + strategyConfig.addExclude("ACT_[\\S\\s]+|QRTZ_[\\S\\s]+|FLW_[\\S\\s]+"); + } + + GlobalConfig globalConfig = new GlobalConfig.Builder().dateType(DateType.TIME_PACK).build(); // 只使用 LocalDateTime 类型,不使用 LocalDate + ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, strategyConfig.build(), + null, globalConfig, null); + // 按照名字排序 + List tables = builder.getTableInfoList(); + tables.sort(Comparator.comparing(TableInfo::getName)); + return tables; + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileConfigService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileConfigService.java new file mode 100644 index 0000000..50335e3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileConfigService.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.module.infra.service.file; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.file.core.client.FileClient; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileConfigDO; + +import javax.validation.Valid; + +/** + * 文件配置 Service 接口 + * + * @author 芋道源码 + */ +public interface FileConfigService { + + /** + * 创建文件配置 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createFileConfig(@Valid FileConfigCreateReqVO createReqVO); + + /** + * 更新文件配置 + * + * @param updateReqVO 更新信息 + */ + void updateFileConfig(@Valid FileConfigUpdateReqVO updateReqVO); + + /** + * 更新文件配置为 Master + * + * @param id 编号 + */ + void updateFileConfigMaster(Long id); + + /** + * 删除文件配置 + * + * @param id 编号 + */ + void deleteFileConfig(Long id); + + /** + * 获得文件配置 + * + * @param id 编号 + * @return 文件配置 + */ + FileConfigDO getFileConfig(Long id); + + /** + * 获得文件配置分页 + * + * @param pageReqVO 分页查询 + * @return 文件配置分页 + */ + PageResult getFileConfigPage(FileConfigPageReqVO pageReqVO); + + /** + * 测试文件配置是否正确,通过上传文件 + * + * @param id 编号 + * @return 文件 URL + */ + String testFileConfig(Long id) throws Exception; + + /** + * 获得指定编号的文件客户端 + * + * @param id 配置编号 + * @return 文件客户端 + */ + FileClient getFileClient(Long id); + + /** + * 获得 Master 文件客户端 + * + * @return 文件客户端 + */ + FileClient getMasterFileClient(); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileConfigServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileConfigServiceImpl.java new file mode 100644 index 0000000..ae077fe --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileConfigServiceImpl.java @@ -0,0 +1,203 @@ +package com.yunxi.scm.module.infra.service.file; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.IdUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import com.yunxi.scm.framework.file.core.client.FileClient; +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import com.yunxi.scm.framework.file.core.client.FileClientFactory; +import com.yunxi.scm.framework.file.core.enums.FileStorageEnum; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import com.yunxi.scm.module.infra.convert.file.FileConfigConvert; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileConfigDO; +import com.yunxi.scm.module.infra.dal.mysql.file.FileConfigMapper; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import javax.validation.Validator; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS; + +/** + * 文件配置 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class FileConfigServiceImpl implements FileConfigService { + + @Resource + private FileClientFactory fileClientFactory; + + /** + * 文件配置的缓存 + */ + @Getter + private List fileConfigCache; + /** + * Master FileClient 对象,有且仅有一个,即 {@link FileConfigDO#getMaster()} 对应的 + */ + @Getter + private FileClient masterFileClient; + + @Resource + private FileConfigMapper fileConfigMapper; + + @Resource + private Validator validator; + + @PostConstruct + public void initLocalCache() { + // 第一步:查询数据 + List configs = fileConfigMapper.selectList(); + log.info("[initLocalCache][缓存文件配置,数量为:{}]", configs.size()); + + // 第二步:构建缓存:创建或更新文件 Client + configs.forEach(config -> { + fileClientFactory.createOrUpdateFileClient(config.getId(), config.getStorage(), config.getConfig()); + // 如果是 master,进行设置 + if (Boolean.TRUE.equals(config.getMaster())) { + masterFileClient = fileClientFactory.getFileClient(config.getId()); + } + }); + this.fileConfigCache = configs; + } + + /** + * 通过定时任务轮询,刷新缓存 + * + * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 + */ + @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) + public void refreshLocalCache() { + // 情况一:如果缓存里没有数据,则直接刷新缓存 + if (CollUtil.isEmpty(fileConfigCache)) { + initLocalCache(); + return; + } + + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 + LocalDateTime maxTime = CollectionUtils.getMaxValue(fileConfigCache, FileConfigDO::getUpdateTime); + if (fileConfigMapper.selectCountByUpdateTimeGt(maxTime) > 0) { + initLocalCache(); + } + } + + @Override + public Long createFileConfig(FileConfigCreateReqVO createReqVO) { + // 插入 + FileConfigDO fileConfig = FileConfigConvert.INSTANCE.convert(createReqVO) + .setConfig(parseClientConfig(createReqVO.getStorage(), createReqVO.getConfig())) + .setMaster(false); // 默认非 master + fileConfigMapper.insert(fileConfig); + + // 刷新缓存 + initLocalCache(); + return fileConfig.getId(); + } + + @Override + public void updateFileConfig(FileConfigUpdateReqVO updateReqVO) { + // 校验存在 + FileConfigDO config = validateFileConfigExists(updateReqVO.getId()); + // 更新 + FileConfigDO updateObj = FileConfigConvert.INSTANCE.convert(updateReqVO) + .setConfig(parseClientConfig(config.getStorage(), updateReqVO.getConfig())); + fileConfigMapper.updateById(updateObj); + + // 刷新缓存 + initLocalCache(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateFileConfigMaster(Long id) { + // 校验存在 + validateFileConfigExists(id); + // 更新其它为非 master + fileConfigMapper.updateBatch(new FileConfigDO().setMaster(false)); + // 更新 + fileConfigMapper.updateById(new FileConfigDO().setId(id).setMaster(true)); + + // 刷新缓存 + initLocalCache(); + } + + private FileClientConfig parseClientConfig(Integer storage, Map config) { + // 获取配置类 + Class configClass = FileStorageEnum.getByStorage(storage) + .getConfigClass(); + FileClientConfig clientConfig = JsonUtils.parseObject2(JsonUtils.toJsonString(config), configClass); + // 参数校验 + ValidationUtils.validate(validator, clientConfig); + // 设置参数 + return clientConfig; + } + + @Override + public void deleteFileConfig(Long id) { + // 校验存在 + FileConfigDO config = validateFileConfigExists(id); + if (Boolean.TRUE.equals(config.getMaster())) { + throw exception(FILE_CONFIG_DELETE_FAIL_MASTER); + } + // 删除 + fileConfigMapper.deleteById(id); + + // 刷新缓存 + initLocalCache(); + } + + private FileConfigDO validateFileConfigExists(Long id) { + FileConfigDO config = fileConfigMapper.selectById(id); + if (config == null) { + throw exception(FILE_CONFIG_NOT_EXISTS); + } + return config; + } + + @Override + public FileConfigDO getFileConfig(Long id) { + return fileConfigMapper.selectById(id); + } + + @Override + public PageResult getFileConfigPage(FileConfigPageReqVO pageReqVO) { + return fileConfigMapper.selectPage(pageReqVO); + } + + @Override + public String testFileConfig(Long id) throws Exception { + // 校验存在 + validateFileConfigExists(id); + // 上传文件 + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + return fileClientFactory.getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg"); + } + + @Override + public FileClient getFileClient(Long id) { + return fileClientFactory.getFileClient(id); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileService.java new file mode 100644 index 0000000..168b8aa --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileService.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.infra.service.file; + +import com.yunxi.scm.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileDO; + +/** + * 文件 Service 接口 + * + * @author 芋道源码 + */ +public interface FileService { + + /** + * 获得文件分页 + * + * @param pageReqVO 分页查询 + * @return 文件分页 + */ + PageResult getFilePage(FilePageReqVO pageReqVO); + + /** + * 保存文件,并返回文件的访问路径 + * + * @param name 文件名称 + * @param path 文件路径 + * @param content 文件内容 + * @return 文件路径 + */ + String createFile(String name, String path, byte[] content); + + /** + * 删除文件 + * + * @param id 编号 + */ + void deleteFile(Long id) throws Exception; + + /** + * 获得文件内容 + * + * @param configId 配置编号 + * @param path 文件路径 + * @return 文件内容 + */ + byte[] getFileContent(Long configId, String path) throws Exception; + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileServiceImpl.java new file mode 100644 index 0000000..6fc9eda --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/file/FileServiceImpl.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.infra.service.file; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.io.FileUtils; +import com.yunxi.scm.framework.file.core.client.FileClient; +import com.yunxi.scm.framework.file.core.utils.FileTypeUtils; +import com.yunxi.scm.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileDO; +import com.yunxi.scm.module.infra.dal.mysql.file.FileMapper; +import lombok.SneakyThrows; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; + +/** + * 文件 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class FileServiceImpl implements FileService { + + @Resource + private FileConfigService fileConfigService; + + @Resource + private FileMapper fileMapper; + + @Override + public PageResult getFilePage(FilePageReqVO pageReqVO) { + return fileMapper.selectPage(pageReqVO); + } + + @Override + @SneakyThrows + public String createFile(String name, String path, byte[] content) { + // 计算默认的 path 名 + String type = FileTypeUtils.getMineType(content, name); + if (StrUtil.isEmpty(path)) { + path = FileUtils.generatePath(content, name); + } + // 如果 name 为空,则使用 path 填充 + if (StrUtil.isEmpty(name)) { + name = path; + } + + // 上传到文件存储器 + FileClient client = fileConfigService.getMasterFileClient(); + Assert.notNull(client, "客户端(master) 不能为空"); + String url = client.upload(content, path, type); + + // 保存到数据库 + FileDO file = new FileDO(); + file.setConfigId(client.getId()); + file.setName(name); + file.setPath(path); + file.setUrl(url); + file.setType(type); + file.setSize(content.length); + fileMapper.insert(file); + return url; + } + + @Override + public void deleteFile(Long id) throws Exception { + // 校验存在 + FileDO file = validateFileExists(id); + + // 从文件存储器中删除 + FileClient client = fileConfigService.getFileClient(file.getConfigId()); + Assert.notNull(client, "客户端({}) 不能为空", file.getConfigId()); + client.delete(file.getPath()); + + // 删除记录 + fileMapper.deleteById(id); + } + + private FileDO validateFileExists(Long id) { + FileDO fileDO = fileMapper.selectById(id); + if (fileDO == null) { + throw exception(FILE_NOT_EXISTS); + } + return fileDO; + } + + @Override + public byte[] getFileContent(Long configId, String path) throws Exception { + FileClient client = fileConfigService.getFileClient(configId); + Assert.notNull(client, "客户端({}) 不能为空", configId); + return client.getContent(path); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobLogService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobLogService.java new file mode 100644 index 0000000..fc1303c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobLogService.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.module.infra.service.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.quartz.core.service.JobLogFrameworkService; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobLogDO; + +import java.util.Collection; +import java.util.List; + +/** + * Job 日志 Service 接口 + * + * @author 芋道源码 + */ +public interface JobLogService extends JobLogFrameworkService { + + /** + * 获得定时任务 + * + * @param id 编号 + * @return 定时任务 + */ + JobLogDO getJobLog(Long id); + + /** + * 获得定时任务列表 + * + * @param ids 编号 + * @return 定时任务列表 + */ + List getJobLogList(Collection ids); + + /** + * 获得定时任务分页 + * + * @param pageReqVO 分页查询 + * @return 定时任务分页 + */ + PageResult getJobLogPage(JobLogPageReqVO pageReqVO); + + /** + * 获得定时任务列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 定时任务分页 + */ + List getJobLogList(JobLogExportReqVO exportReqVO); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobLogServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobLogServiceImpl.java new file mode 100644 index 0000000..67d4575 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobLogServiceImpl.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.module.infra.service.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobLogDO; +import com.yunxi.scm.module.infra.dal.mysql.job.JobLogMapper; +import com.yunxi.scm.module.infra.enums.job.JobLogStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +/** + * Job 日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class JobLogServiceImpl implements JobLogService { + + @Resource + private JobLogMapper jobLogMapper; + + @Override + public Long createJobLog(Long jobId, LocalDateTime beginTime, String jobHandlerName, String jobHandlerParam, Integer executeIndex) { + JobLogDO log = JobLogDO.builder().jobId(jobId).handlerName(jobHandlerName).handlerParam(jobHandlerParam).executeIndex(executeIndex) + .beginTime(beginTime).status(JobLogStatusEnum.RUNNING.getStatus()).build(); + jobLogMapper.insert(log); + return log.getId(); + } + + @Override + @Async + public void updateJobLogResultAsync(Long logId, LocalDateTime endTime, Integer duration, boolean success, String result) { + try { + JobLogDO updateObj = JobLogDO.builder().id(logId).endTime(endTime).duration(duration) + .status(success ? JobLogStatusEnum.SUCCESS.getStatus() : JobLogStatusEnum.FAILURE.getStatus()).result(result).build(); + jobLogMapper.updateById(updateObj); + } catch (Exception ex) { + log.error("[updateJobLogResultAsync][logId({}) endTime({}) duration({}) success({}) result({})]", + logId, endTime, duration, success, result); + } + } + + @Override + public JobLogDO getJobLog(Long id) { + return jobLogMapper.selectById(id); + } + + @Override + public List getJobLogList(Collection ids) { + return jobLogMapper.selectBatchIds(ids); + } + + @Override + public PageResult getJobLogPage(JobLogPageReqVO pageReqVO) { + return jobLogMapper.selectPage(pageReqVO); + } + + @Override + public List getJobLogList(JobLogExportReqVO exportReqVO) { + return jobLogMapper.selectList(exportReqVO); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobService.java new file mode 100644 index 0000000..b4f4616 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobService.java @@ -0,0 +1,91 @@ +package com.yunxi.scm.module.infra.service.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobDO; +import org.quartz.SchedulerException; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 定时任务 Service 接口 + * + * @author 芋道源码 + */ +public interface JobService { + + /** + * 创建定时任务 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createJob(@Valid JobCreateReqVO createReqVO) throws SchedulerException; + + /** + * 更新定时任务 + * + * @param updateReqVO 更新信息 + */ + void updateJob(@Valid JobUpdateReqVO updateReqVO) throws SchedulerException; + + /** + * 更新定时任务的状态 + * + * @param id 任务编号 + * @param status 状态 + */ + void updateJobStatus(Long id, Integer status) throws SchedulerException; + + /** + * 触发定时任务 + * + * @param id 任务编号 + */ + void triggerJob(Long id) throws SchedulerException; + + /** + * 删除定时任务 + * + * @param id 编号 + */ + void deleteJob(Long id) throws SchedulerException; + + /** + * 获得定时任务 + * + * @param id 编号 + * @return 定时任务 + */ + JobDO getJob(Long id); + + /** + * 获得定时任务列表 + * + * @param ids 编号 + * @return 定时任务列表 + */ + List getJobList(Collection ids); + + /** + * 获得定时任务分页 + * + * @param pageReqVO 分页查询 + * @return 定时任务分页 + */ + PageResult getJobPage(JobPageReqVO pageReqVO); + + /** + * 获得定时任务列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 定时任务分页 + */ + List getJobList(JobExportReqVO exportReqVO); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobServiceImpl.java new file mode 100644 index 0000000..5d247c8 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/job/JobServiceImpl.java @@ -0,0 +1,173 @@ +package com.yunxi.scm.module.infra.service.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.quartz.core.scheduler.SchedulerManager; +import com.yunxi.scm.framework.quartz.core.util.CronUtils; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobUpdateReqVO; +import com.yunxi.scm.module.infra.convert.job.JobConvert; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobDO; +import com.yunxi.scm.module.infra.dal.mysql.job.JobMapper; +import com.yunxi.scm.module.infra.enums.job.JobStatusEnum; +import org.quartz.SchedulerException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.*; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.containsAny; + +/** + * 定时任务 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class JobServiceImpl implements JobService { + + @Resource + private JobMapper jobMapper; + + @Resource + private SchedulerManager schedulerManager; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createJob(JobCreateReqVO createReqVO) throws SchedulerException { + validateCronExpression(createReqVO.getCronExpression()); + // 校验唯一性 + if (jobMapper.selectByHandlerName(createReqVO.getHandlerName()) != null) { + throw exception(JOB_HANDLER_EXISTS); + } + // 插入 + JobDO job = JobConvert.INSTANCE.convert(createReqVO); + job.setStatus(JobStatusEnum.INIT.getStatus()); + fillJobMonitorTimeoutEmpty(job); + jobMapper.insert(job); + + // 添加 Job 到 Quartz 中 + schedulerManager.addJob(job.getId(), job.getHandlerName(), job.getHandlerParam(), job.getCronExpression(), + createReqVO.getRetryCount(), createReqVO.getRetryInterval()); + // 更新 + JobDO updateObj = JobDO.builder().id(job.getId()).status(JobStatusEnum.NORMAL.getStatus()).build(); + jobMapper.updateById(updateObj); + + // 返回 + return job.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateJob(JobUpdateReqVO updateReqVO) throws SchedulerException { + validateCronExpression(updateReqVO.getCronExpression()); + // 校验存在 + JobDO job = validateJobExists(updateReqVO.getId()); + // 只有开启状态,才可以修改.原因是,如果出暂停状态,修改 Quartz Job 时,会导致任务又开始执行 + if (!job.getStatus().equals(JobStatusEnum.NORMAL.getStatus())) { + throw exception(JOB_UPDATE_ONLY_NORMAL_STATUS); + } + // 更新 + JobDO updateObj = JobConvert.INSTANCE.convert(updateReqVO); + fillJobMonitorTimeoutEmpty(updateObj); + jobMapper.updateById(updateObj); + + // 更新 Job 到 Quartz 中 + schedulerManager.updateJob(job.getHandlerName(), updateReqVO.getHandlerParam(), updateReqVO.getCronExpression(), + updateReqVO.getRetryCount(), updateReqVO.getRetryInterval()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateJobStatus(Long id, Integer status) throws SchedulerException { + // 校验 status + if (!containsAny(status, JobStatusEnum.NORMAL.getStatus(), JobStatusEnum.STOP.getStatus())) { + throw exception(JOB_CHANGE_STATUS_INVALID); + } + // 校验存在 + JobDO job = validateJobExists(id); + // 校验是否已经为当前状态 + if (job.getStatus().equals(status)) { + throw exception(JOB_CHANGE_STATUS_EQUALS); + } + // 更新 Job 状态 + JobDO updateObj = JobDO.builder().id(id).status(status).build(); + jobMapper.updateById(updateObj); + + // 更新状态 Job 到 Quartz 中 + if (JobStatusEnum.NORMAL.getStatus().equals(status)) { // 开启 + schedulerManager.resumeJob(job.getHandlerName()); + } else { // 暂停 + schedulerManager.pauseJob(job.getHandlerName()); + } + } + + @Override + public void triggerJob(Long id) throws SchedulerException { + // 校验存在 + JobDO job = validateJobExists(id); + + // 触发 Quartz 中的 Job + schedulerManager.triggerJob(job.getId(), job.getHandlerName(), job.getHandlerParam()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteJob(Long id) throws SchedulerException { + // 校验存在 + JobDO job = validateJobExists(id); + // 更新 + jobMapper.deleteById(id); + + // 删除 Job 到 Quartz 中 + schedulerManager.deleteJob(job.getHandlerName()); + } + + private JobDO validateJobExists(Long id) { + JobDO job = jobMapper.selectById(id); + if (job == null) { + throw exception(JOB_NOT_EXISTS); + } + return job; + } + + private void validateCronExpression(String cronExpression) { + if (!CronUtils.isValid(cronExpression)) { + throw exception(JOB_CRON_EXPRESSION_VALID); + } + } + + @Override + public JobDO getJob(Long id) { + return jobMapper.selectById(id); + } + + @Override + public List getJobList(Collection ids) { + return jobMapper.selectBatchIds(ids); + } + + @Override + public PageResult getJobPage(JobPageReqVO pageReqVO) { + return jobMapper.selectPage(pageReqVO); + } + + @Override + public List getJobList(JobExportReqVO exportReqVO) { + return jobMapper.selectList(exportReqVO); + } + + private static void fillJobMonitorTimeoutEmpty(JobDO job) { + if (job.getMonitorTimeout() == null) { + job.setMonitorTimeout(0); + } + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogService.java new file mode 100644 index 0000000..0e7af60 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogService.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.infra.service.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiAccessLogDO; + +import java.util.List; + +/** + * API 访问日志 Service 接口 + * + * @author 芋道源码 + */ +public interface ApiAccessLogService { + + /** + * 创建 API 访问日志 + * + * @param createReqDTO API 访问日志 + */ + void createApiAccessLog(ApiAccessLogCreateReqDTO createReqDTO); + + /** + * 获得 API 访问日志分页 + * + * @param pageReqVO 分页查询 + * @return API 访问日志分页 + */ + PageResult getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO); + + /** + * 获得 API 访问日志列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return API 访问日志分页 + */ + List getApiAccessLogList(ApiAccessLogExportReqVO exportReqVO); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogServiceImpl.java new file mode 100644 index 0000000..8837db1 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogServiceImpl.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.infra.service.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.yunxi.scm.module.infra.convert.logger.ApiAccessLogConvert; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import com.yunxi.scm.module.infra.dal.mysql.logger.ApiAccessLogMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +/** + * API 访问日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiAccessLogServiceImpl implements ApiAccessLogService { + + @Resource + private ApiAccessLogMapper apiAccessLogMapper; + + @Override + public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { + ApiAccessLogDO apiAccessLog = ApiAccessLogConvert.INSTANCE.convert(createDTO); + apiAccessLogMapper.insert(apiAccessLog); + } + + @Override + public PageResult getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO) { + return apiAccessLogMapper.selectPage(pageReqVO); + } + + @Override + public List getApiAccessLogList(ApiAccessLogExportReqVO exportReqVO) { + return apiAccessLogMapper.selectList(exportReqVO); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogService.java new file mode 100644 index 0000000..05b288b --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogService.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.infra.service.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiErrorLogDO; + +import java.util.List; + +/** + * API 错误日志 Service 接口 + * + * @author 芋道源码 + */ +public interface ApiErrorLogService { + + /** + * 创建 API 错误日志 + * + * @param createReqDTO API 错误日志 + */ + void createApiErrorLog(ApiErrorLogCreateReqDTO createReqDTO); + + /** + * 获得 API 错误日志分页 + * + * @param pageReqVO 分页查询 + * @return API 错误日志分页 + */ + PageResult getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO); + + /** + * 获得 API 错误日志列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return API 错误日志分页 + */ + List getApiErrorLogList(ApiErrorLogExportReqVO exportReqVO); + + /** + * 更新 API 错误日志已处理 + * + * @param id API 日志编号 + * @param processStatus 处理结果 + * @param processUserId 处理人 + */ + void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogServiceImpl.java new file mode 100644 index 0000000..cc41851 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.infra.service.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.yunxi.scm.module.infra.convert.logger.ApiErrorLogConvert; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import com.yunxi.scm.module.infra.dal.mysql.logger.ApiErrorLogMapper; +import com.yunxi.scm.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_NOT_FOUND; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_PROCESSED; + +/** + * API 错误日志 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ApiErrorLogServiceImpl implements ApiErrorLogService { + + @Resource + private ApiErrorLogMapper apiErrorLogMapper; + + @Override + public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { + ApiErrorLogDO apiErrorLog = ApiErrorLogConvert.INSTANCE.convert(createDTO) + .setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); + apiErrorLogMapper.insert(apiErrorLog); + } + + @Override + public PageResult getApiErrorLogPage(ApiErrorLogPageReqVO pageReqVO) { + return apiErrorLogMapper.selectPage(pageReqVO); + } + + @Override + public List getApiErrorLogList(ApiErrorLogExportReqVO exportReqVO) { + return apiErrorLogMapper.selectList(exportReqVO); + } + + @Override + public void updateApiErrorLogProcess(Long id, Integer processStatus, Long processUserId) { + ApiErrorLogDO errorLog = apiErrorLogMapper.selectById(id); + if (errorLog == null) { + throw exception(API_ERROR_LOG_NOT_FOUND); + } + if (!ApiErrorLogProcessStatusEnum.INIT.getStatus().equals(errorLog.getProcessStatus())) { + throw exception(API_ERROR_LOG_PROCESSED); + } + // 标记处理 + apiErrorLogMapper.updateById(ApiErrorLogDO.builder().id(id).processStatus(processStatus) + .processUserId(processUserId).processTime(LocalDateTime.now()).build()); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/test/TestDemoService.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/test/TestDemoService.java new file mode 100644 index 0000000..ecc38e2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/test/TestDemoService.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.infra.service.test; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.test.TestDemoDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 字典类型 Service 接口 + * + * @author 芋道源码 + */ +public interface TestDemoService { + + /** + * 创建字典类型 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTestDemo(@Valid TestDemoCreateReqVO createReqVO); + + /** + * 更新字典类型 + * + * @param updateReqVO 更新信息 + */ + void updateTestDemo(@Valid TestDemoUpdateReqVO updateReqVO); + + /** + * 删除字典类型 + * + * @param id 编号 + */ + void deleteTestDemo(Long id); + + /** + * 获得字典类型 + * + * @param id 编号 + * @return 字典类型 + */ + TestDemoDO getTestDemo(Long id); + + /** + * 获得字典类型列表 + * + * @param ids 编号 + * @return 字典类型列表 + */ + List getTestDemoList(Collection ids); + + /** + * 获得字典类型分页 + * + * @param pageReqVO 分页查询 + * @return 字典类型分页 + */ + PageResult getTestDemoPage(TestDemoPageReqVO pageReqVO); + + /** + * 获得字典类型列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 字典类型列表 + */ + List getTestDemoList(TestDemoExportReqVO exportReqVO); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/test/TestDemoServiceImpl.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/test/TestDemoServiceImpl.java new file mode 100644 index 0000000..eb1d1c7 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/service/test/TestDemoServiceImpl.java @@ -0,0 +1,91 @@ +package com.yunxi.scm.module.infra.service.test; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO; +import com.yunxi.scm.module.infra.convert.test.TestDemoConvert; +import com.yunxi.scm.module.infra.dal.dataobject.test.TestDemoDO; +import com.yunxi.scm.module.infra.dal.mysql.test.TestDemoMapper; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.TEST_DEMO_NOT_EXISTS; + +/** + * 字典类型 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class TestDemoServiceImpl implements TestDemoService { + + @Resource + private TestDemoMapper testDemoMapper; + + @Override + public Long createTestDemo(TestDemoCreateReqVO createReqVO) { + // 插入 + TestDemoDO testDemo = TestDemoConvert.INSTANCE.convert(createReqVO); + testDemoMapper.insert(testDemo); + // 返回 + return testDemo.getId(); + } + + @Override + @CacheEvict(value = "test", key = "#updateReqVO.id") + public void updateTestDemo(TestDemoUpdateReqVO updateReqVO) { + // 校验存在 + validateTestDemoExists(updateReqVO.getId()); + // 更新 + TestDemoDO updateObj = TestDemoConvert.INSTANCE.convert(updateReqVO); + testDemoMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = "test", key = "#id") + public void deleteTestDemo(Long id) { + // 校验存在 + validateTestDemoExists(id); + // 删除 + testDemoMapper.deleteById(id); + } + + private void validateTestDemoExists(Long id) { + if (testDemoMapper.selectById(id) == null) { + throw exception(TEST_DEMO_NOT_EXISTS); + } + } + + @Override + @Cacheable(cacheNames = "test", key = "#id") + public TestDemoDO getTestDemo(Long id) { + return testDemoMapper.selectById(id); + } + + @Override + public List getTestDemoList(Collection ids) { + return testDemoMapper.selectBatchIds(ids); + } + + @Override + public PageResult getTestDemoPage(TestDemoPageReqVO pageReqVO) { +// testDemoMapper.selectList2(); + return testDemoMapper.selectPage(pageReqVO); + } + + @Override + public List getTestDemoList(TestDemoExportReqVO exportReqVO) { + return testDemoMapper.selectList(exportReqVO); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/SemaphoreUtils.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/SemaphoreUtils.java new file mode 100644 index 0000000..b8f4823 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/SemaphoreUtils.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.infra.websocket; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.Semaphore; + +/** + * 信号量相关处理 + * + */ +@Slf4j +public class SemaphoreUtils { + + /** + * 获取信号量 + * + * @param semaphore + * @return + */ + public static boolean tryAcquire(Semaphore semaphore) { + boolean flag = false; + + try { + flag = semaphore.tryAcquire(); + } catch (Exception e) { + log.error("获取信号量异常", e); + } + + return flag; + } + + /** + * 释放信号量 + * + * @param semaphore + */ + public static void release(Semaphore semaphore) { + + try { + semaphore.release(); + } catch (Exception e) { + log.error("释放信号量异常", e); + } + } +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketConfig.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketConfig.java new file mode 100644 index 0000000..a5812fd --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketConfig.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.infra.websocket; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + +/** + * websocket 配置 + */ +@Configuration +public class WebSocketConfig { + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketServer.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketServer.java new file mode 100644 index 0000000..f91b828 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketServer.java @@ -0,0 +1,86 @@ +package com.yunxi.scm.module.infra.websocket; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.websocket.*; +import javax.websocket.server.ServerEndpoint; +import java.util.concurrent.Semaphore; + +/** + * websocket 消息处理 + */ +@Component +@ServerEndpoint("/websocket/message") +@Slf4j +public class WebSocketServer { + + /** + * 默认最多允许同时在线用户数100 + */ + public static int socketMaxOnlineCount = 100; + + private static final Semaphore SOCKET_SEMAPHORE = new Semaphore(socketMaxOnlineCount); + + /** + * 连接建立成功调用的方法 + */ + @OnOpen + public void onOpen(Session session) throws Exception { + // 尝试获取信号量 + boolean semaphoreFlag = SemaphoreUtils.tryAcquire(SOCKET_SEMAPHORE); + if (!semaphoreFlag) { + // 未获取到信号量 + log.error("当前在线人数超过限制数:{}", socketMaxOnlineCount); + WebSocketUsers.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount); + session.close(); + } else { + String userId = WebSocketUsers.getParam("userId", session); + if (userId != null) { + // 添加用户 + WebSocketUsers.addSession(userId, session); + log.info("用户【userId={}】建立连接,当前连接用户总数:{}", userId, WebSocketUsers.getUsers().size()); + WebSocketUsers.sendMessage(session, "接收内容:连接成功"); + } else { + WebSocketUsers.sendMessage(session, "接收内容:连接失败"); + } + } + } + + /** + * 连接关闭时处理 + */ + @OnClose + public void onClose(Session session) { + log.info("用户【sessionId={}】关闭连接!", session.getId()); + // 移除用户 + WebSocketUsers.removeSession(session); + // 获取到信号量则需释放 + SemaphoreUtils.release(SOCKET_SEMAPHORE); + } + + /** + * 抛出异常时处理 + */ + @OnError + public void onError(Session session, Throwable exception) throws Exception { + if (session.isOpen()) { + // 关闭连接 + session.close(); + } + String sessionId = session.getId(); + log.info("用户【sessionId={}】连接异常!异常信息:{}", sessionId, exception); + // 移出用户 + WebSocketUsers.removeSession(session); + // 获取到信号量则需释放 + SemaphoreUtils.release(SOCKET_SEMAPHORE); + } + + /** + * 收到客户端消息时调用的方法 + */ + @OnMessage + public void onMessage(Session session, String message) { + WebSocketUsers.sendMessage(session, "接收内容:" + message); + } +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketUsers.java b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketUsers.java new file mode 100644 index 0000000..b9f4a8a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/java/com/yunxi/scm/module/infra/websocket/WebSocketUsers.java @@ -0,0 +1,178 @@ +package com.yunxi.scm.module.infra.websocket; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; +import lombok.extern.slf4j.Slf4j; +import org.bouncycastle.util.Strings; + +import javax.validation.constraints.NotNull; +import javax.websocket.Session; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * websocket 客户端用户 + */ +@Slf4j +public class WebSocketUsers { + + /** + * 用户集 + * TODO 需要登录用户的session? + */ + private static final Map SESSION_MAP = new ConcurrentHashMap<>(); + + /** + * 存储用户 + * + * @param userId 唯一键 + * @param session 用户信息 + */ + public static void addSession(String userId, Session session) { + SESSION_MAP.put(userId, session); + } + + /** + * 移除用户 + * + * @param session 用户信息 + * @return 移除结果 + */ + public static boolean removeSession(Session session) { + String key = null; + boolean flag = SESSION_MAP.containsValue(session); + if (flag) { + Set> entries = SESSION_MAP.entrySet(); + for (Map.Entry entry : entries) { + Session value = entry.getValue(); + if (value.equals(session)) { + key = entry.getKey(); + break; + } + } + } else { + return true; + } + return removeSession(key); + } + + /** + * 移出用户 + * + * @param userId 用户id + */ + public static boolean removeSession(String userId) { + log.info("用户【userId={}】退出", userId); + Session remove = SESSION_MAP.remove(userId); + if (remove != null) { + boolean containsValue = SESSION_MAP.containsValue(remove); + log.info("用户【userId={}】退出{},当前连接用户总数:{}", userId, containsValue ? "失败" : "成功", SESSION_MAP.size()); + return containsValue; + } else { + return true; + } + } + + /** + * 获取在线用户列表 + * + * @return 返回用户集合 + */ + public static Map getUsers() { + return SESSION_MAP; + } + + /** + * 向所有在线人发送消息 + * + * @param message 消息内容 + */ + public static void sendMessageToAll(String message) { + SESSION_MAP.forEach((userId, session) -> { + if (session.isOpen()) { + sendMessage(session, message); + } + }); + } + + /** + * 异步发送文本消息 + * + * @param session 用户session + * @param message 消息内容 + */ + public static void sendMessageAsync(Session session, String message) { + if (session.isOpen()) { + // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程? + session.getAsyncRemote().sendText(message); + } else { + log.warn("用户【session={}】不在线", session.getId()); + } + } + + /** + * 同步发送文本消息 + * + * @param session 用户session + * @param message 消息内容 + */ + public static void sendMessage(Session session, String message) { + try { + if (session.isOpen()) { + // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程? + session.getBasicRemote().sendText(message); + } else { + log.warn("用户【session={}】不在线", session.getId()); + } + } catch (IOException e) { + log.error("发送消息异常", e); + } + + } + + /** + * 根据用户id发送消息 + * + * @param userId 用户id + * @param message 消息内容 + */ + public static void sendMessage(String userId, String message) { + Session session = SESSION_MAP.get(userId); + //判断是否存在该用户的session,并且是否在线 + if (session == null || !session.isOpen()) { + return; + } + sendMessage(session, message); + } + + + /** + * 获取session中的指定参数值 + * + * @param key 参数key + * @param session 用户session + */ + public static String getParam(@NotNull String key, Session session) { + //TODO 目前只针对获取一个key的值,后期根据情况拓展多个 或者直接在onClose onOpen上获取参数? + String value = null; + Map> parameters = session.getRequestParameterMap(); + if (MapUtil.isNotEmpty(parameters)) { + value = parameters.get(key).get(0); + } else { + String queryString = session.getQueryString(); + if (!StrUtil.isEmpty(queryString)) { + String[] params = Strings.split(queryString, '&'); + for (String paramPair : params) { + String[] nameValues = Strings.split(paramPair, '='); + if (key.equals(nameValues[0])) { + value = nameValues[1]; + } + } + } + } + return value; + } +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm new file mode 100644 index 0000000..a8c1f62 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm @@ -0,0 +1,111 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}; + +import org.springframework.web.bind.annotation.*; +import javax.annotation.Resource; +import org.springframework.validation.annotation.Validated; +#if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end + +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; + +import javax.validation.constraints.*; +import javax.validation.*; +import javax.servlet.http.*; +import java.util.*; +import java.io.IOException; + +import ${PageResultClassName}; +import ${CommonResultClassName}; +import static ${CommonResultClassName}.success; + +import ${ExcelUtilsClassName}; + +import ${OperateLogClassName}; +import static ${OperateTypeEnumClassName}.*; + +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${basePackage}.module.${table.moduleName}.convert.${table.businessName}.${table.className}Convert; +import ${basePackage}.module.${table.moduleName}.service.${table.businessName}.${table.className}Service; + +@Tag(name = "${sceneEnum.name} - ${table.classComment}") +@RestController +##二级的 businessName 暂时不算在 HTTP 路径上,可以根据需要写 +@RequestMapping("/${table.moduleName}/${simpleClassName_strikeCase}") +@Validated +public class ${sceneEnum.prefixClass}${table.className}Controller { + + @Resource + private ${table.className}Service ${classNameVar}Service; + + @PostMapping("/create") + @Operation(summary = "创建${table.classComment}") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')")#end + + public CommonResult<${primaryColumn.javaType}> create${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO) { + return success(${classNameVar}Service.create${simpleClassName}(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新${table.classComment}") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')")#end + + public CommonResult update${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO) { + ${classNameVar}Service.update${simpleClassName}(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除${table.classComment}") + @Parameter(name = "id", description = "编号", required = true) +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')")#end + + public CommonResult delete${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { + ${classNameVar}Service.delete${simpleClassName}(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得${table.classComment}") + @Parameter(name = "id", description = "编号", required = true, example = "1024") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end + + public CommonResult<${sceneEnum.prefixClass}${table.className}RespVO> get${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) { + ${table.className}DO ${classNameVar} = ${classNameVar}Service.get${simpleClassName}(id); + return success(${table.className}Convert.INSTANCE.convert(${classNameVar})); + } + + @GetMapping("/list") + @Operation(summary = "获得${table.classComment}列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end + + public CommonResult> get${simpleClassName}List(@RequestParam("ids") Collection<${primaryColumn.javaType}> ids) { + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(ids); + return success(${table.className}Convert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得${table.classComment}分页") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end + + public CommonResult> get${simpleClassName}Page(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageVO) { + PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(pageVO); + return success(${table.className}Convert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出${table.classComment} Excel") +#if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:export')")#end + + @OperateLog(type = EXPORT) + public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(exportReqVO); + // 导出 Excel + List<${sceneEnum.prefixClass}${table.className}ExcelVO> datas = ${table.className}Convert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${sceneEnum.prefixClass}${table.className}ExcelVO.class, datas); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/_column.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/_column.vm new file mode 100644 index 0000000..98b09f2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/_column.vm @@ -0,0 +1,13 @@ +## 提供给 baseVO、createVO、updateVO 生成字段 + @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) +#if (!${column.nullable})## 判断 @NotEmpty 和 @NotNull 注解 +#if (${field.fieldType} == 'String') + @NotEmpty(message = "${column.columnComment}不能为空") +#else + @NotNull(message = "${column.columnComment}不能为空") +#end +#end +#if (${column.javaType} == "LocalDateTime")## 时间类型 + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) +#end + private ${column.javaType} ${column.javaField}; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/baseVO.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/baseVO.vm new file mode 100644 index 0000000..cc612e3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/baseVO.vm @@ -0,0 +1,39 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +#foreach ($column in $columns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#end +#if (${column.javaType} == "LocalDateTime") +import java.time.LocalDateTime; +#end +#end +import javax.validation.constraints.*; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.createOperation} && ${column.updateOperation} && ${column.listOperationResult} + && ${column.javaType} == "LocalDateTime")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end + +/** + * ${table.classComment} Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class ${sceneEnum.prefixClass}${table.className}BaseVO { + +#foreach ($column in $columns) +#if (${column.createOperation} && ${column.updateOperation} && ${column.listOperationResult})##通用操作 + #parse("codegen/java/controller/vo/_column.vm") + +#end +#end +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/createReqVO.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/createReqVO.vm new file mode 100644 index 0000000..d4f6f8e --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/createReqVO.vm @@ -0,0 +1,30 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.*; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.createOperation} && (!${column.updateOperation} || !${column.listOperationResult}) + && ${column.javaType} == "LocalDateTime")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment}创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ${sceneEnum.prefixClass}${table.className}CreateReqVO extends ${sceneEnum.prefixClass}${table.className}BaseVO { + +#foreach ($column in $columns) +#if (${column.createOperation} && (!${column.updateOperation} || !${column.listOperationResult}))##不是通用字段 + #parse("codegen/java/controller/vo/_column.vm") + +#end +#end +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/excelVO.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/excelVO.vm new file mode 100644 index 0000000..15c6660 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/excelVO.vm @@ -0,0 +1,45 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +#foreach ($column in $columns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#end +#if (${column.javaType} == "LocalDateTime") +import java.time.LocalDateTime; +#end +#end + +import com.alibaba.excel.annotation.ExcelProperty; +#foreach ($column in $columns) +#if ("$!column.dictType" != "")## 有设置数据字典 +import ${DictFormatClassName}; +import ${DictConvertClassName}; + +#break +#end +#end + +/** + * ${table.classComment} Excel VO + * + * @author ${table.author} + */ +@Data +public class ${sceneEnum.prefixClass}${table.className}ExcelVO { + +#foreach ($column in $columns) + #if (${column.listOperationResult})##返回字段 + #if ("$!column.dictType" != "")##处理枚举值 + @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class) + @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中 + #else + @ExcelProperty("${column.columnComment}") + #end + private ${column.javaType} ${column.javaField}; + + #end +#end +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/exportReqVO.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/exportReqVO.vm new file mode 100644 index 0000000..d3ef4aa --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/exportReqVO.vm @@ -0,0 +1,39 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import ${PageParamClassName}; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.listOperation} && ${column.javaType} == "LocalDateTime")## 时间类型 +import java.time.LocalDateTime; +import org.springframework.format.annotation.DateTimeFormat; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end +## 字段模板 +#macro(columnTpl $prefix $prefixStr) + @Schema(description = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment} Excel 导出 Request VO,参数和 ${table.className}PageReqVO 是一致的") +@Data +public class ${sceneEnum.prefixClass}${table.className}ExportReqVO { + +#foreach ($column in $columns) +#if (${column.listOperation})##查询操作 +#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 + @Schema(description = "${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private ${column.javaType}[] ${column.javaField}; +#else##情况二,非 Between 的时间 + #columnTpl('', '') +#end + +#end +#end +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm new file mode 100644 index 0000000..6f9868d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm @@ -0,0 +1,41 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import ${PageParamClassName}; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.listOperation} && ${column.javaType} == "LocalDateTime")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end +## 字段模板 +#macro(columnTpl $prefix $prefixStr) + @Schema(description = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment}分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ${sceneEnum.prefixClass}${table.className}PageReqVO extends PageParam { + +#foreach ($column in $columns) +#if (${column.listOperation})##查询操作 +#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 + @Schema(description = "${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private ${column.javaType}[] ${column.javaField}; +#else##情况二,非 Between 的时间 + #columnTpl('', '') +#end + +#end +#end +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm new file mode 100644 index 0000000..517d8bc --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm @@ -0,0 +1,25 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +#foreach ($column in $columns) +#if (${column.javaType} == "LocalDateTime") +import java.time.LocalDateTime; +#break +#end +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment} Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ${sceneEnum.prefixClass}${table.className}RespVO extends ${sceneEnum.prefixClass}${table.className}BaseVO { + +#foreach ($column in $columns) +#if (${column.listOperationResult} && (!${column.createOperation} || !${column.updateOperation}))##不是通用字段 + @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) + private ${column.javaType} ${column.javaField}; + +#end +#end +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/updateReqVO.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/updateReqVO.vm new file mode 100644 index 0000000..48d7432 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/controller/vo/updateReqVO.vm @@ -0,0 +1,30 @@ +package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; +import javax.validation.constraints.*; +## 处理 Date 字段的引入 +#foreach ($column in $columns) +#if (${column.updateOperation} && (!${column.createOperation} || !${column.listOperationResult}) + && ${column.javaType} == "LocalDateTime")## 时间类型 +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; +import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +#break +#end +#end + +@Schema(description = "${sceneEnum.name} - ${table.classComment}更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ${sceneEnum.prefixClass}${table.className}UpdateReqVO extends ${sceneEnum.prefixClass}${table.className}BaseVO { + +#foreach ($column in $columns) +#if (${column.updateOperation} && (!${column.createOperation} || !${column.listOperationResult}))##不是通用字段 + #parse("codegen/java/controller/vo/_column.vm") + +#end +#end +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/convert/convert.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/convert/convert.vm new file mode 100644 index 0000000..6176e0f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/convert/convert.vm @@ -0,0 +1,34 @@ +package ${basePackage}.module.${table.moduleName}.convert.${table.businessName}; + +import java.util.*; + +import ${PageResultClassName}; + +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; + +/** + * ${table.classComment} Convert + * + * @author ${table.author} + */ +@Mapper +public interface ${table.className}Convert { + + ${table.className}Convert INSTANCE = Mappers.getMapper(${table.className}Convert.class); + + ${table.className}DO convert(${sceneEnum.prefixClass}${table.className}CreateReqVO bean); + + ${table.className}DO convert(${sceneEnum.prefixClass}${table.className}UpdateReqVO bean); + + ${sceneEnum.prefixClass}${table.className}RespVO convert(${table.className}DO bean); + + List<${sceneEnum.prefixClass}${table.className}RespVO> convertList(List<${table.className}DO> list); + + PageResult<${sceneEnum.prefixClass}${table.className}RespVO> convertPage(PageResult<${table.className}DO> page); + + List<${sceneEnum.prefixClass}${table.className}ExcelVO> convertList02(List<${table.className}DO> list); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/do.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/do.vm new file mode 100644 index 0000000..d551d4b --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/do.vm @@ -0,0 +1,47 @@ +package ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}; + +import lombok.*; +import java.util.*; +#foreach ($column in $columns) +#if (${column.javaType} == "BigDecimal") +import java.math.BigDecimal; +#end +#if (${column.javaType} == "LocalDateTime") +import java.time.LocalDateTime; +#end +#end +import com.baomidou.mybatisplus.annotation.*; +import ${BaseDOClassName}; + +/** + * ${table.classComment} DO + * + * @author ${table.author} + */ +@TableName("${table.tableName.toLowerCase()}") +@KeySequence("${table.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ${table.className}DO extends BaseDO { + +#foreach ($column in $columns) +#if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 + /** + * ${column.columnComment} + #if ("$!column.dictType" != "")##处理枚举值 + * + * 枚举 {@link TODO ${column.dictType} 对应的类} + #end + */ + #if (${column.primaryKey})##处理主键 + @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end + #end + private ${column.javaType} ${column.javaField}; +#end +#end + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm new file mode 100644 index 0000000..615ae33 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm @@ -0,0 +1,66 @@ +package ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}; + +import java.util.*; + +import ${PageResultClassName}; +import ${QueryWrapperClassName}; +import ${BaseMapperClassName}; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import org.apache.ibatis.annotations.Mapper; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; + +## 字段模板 +#macro(listCondition) +#foreach ($column in $columns) +#if (${column.listOperation}) +#set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 +#if (${column.listOperationCondition} == "=")##情况一,= 的时候 + .eqIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "!=")##情况二,!= 的时候 + .neIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == ">")##情况三,> 的时候 + .gtIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == ">=")##情况四,>= 的时候 + .geIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "<")##情况五,< 的时候 + .ltIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "<=")##情况五,<= 的时候 + .leIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "LIKE")##情况七,Like 的时候 + .likeIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#if (${column.listOperationCondition} == "BETWEEN")##情况八,Between 的时候 + .betweenIfPresent(${table.className}DO::get${JavaField}, reqVO.get${JavaField}()) +#end +#end +#end +#end +/** + * ${table.classComment} Mapper + * + * @author ${table.author} + */ +@Mapper +public interface ${table.className}Mapper extends BaseMapperX<${table.className}DO> { + + default PageResult<${table.className}DO> selectPage(${sceneEnum.prefixClass}${table.className}PageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX<${table.className}DO>() + #listCondition() + .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 + + } + + default List<${table.className}DO> selectList(${sceneEnum.prefixClass}${table.className}ExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX<${table.className}DO>() + #listCondition() + .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序 + + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm new file mode 100644 index 0000000..d930db9 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm @@ -0,0 +1,12 @@ + + + + + + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm new file mode 100644 index 0000000..6a12224 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm @@ -0,0 +1,3 @@ +// TODO 待办:请将下面的错误码复制到 yunxi-module-${table.moduleName}-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!! +// ========== ${table.classComment} TODO 补充编号 ========== +ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${table.classComment}不存在"); diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/service/service.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/service/service.vm new file mode 100644 index 0000000..b8c6376 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/service/service.vm @@ -0,0 +1,70 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import java.util.*; +import javax.validation.*; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${PageResultClassName}; + +/** + * ${table.classComment} Service 接口 + * + * @author ${table.author} + */ +public interface ${table.className}Service { + + /** + * 创建${table.classComment} + * + * @param createReqVO 创建信息 + * @return 编号 + */ + ${primaryColumn.javaType} create${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO); + + /** + * 更新${table.classComment} + * + * @param updateReqVO 更新信息 + */ + void update${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO); + + /** + * 删除${table.classComment} + * + * @param id 编号 + */ + void delete${simpleClassName}(${primaryColumn.javaType} id); + + /** + * 获得${table.classComment} + * + * @param id 编号 + * @return ${table.classComment} + */ + ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id); + + /** + * 获得${table.classComment}列表 + * + * @param ids 编号 + * @return ${table.classComment}列表 + */ + List<${table.className}DO> get${simpleClassName}List(Collection<${primaryColumn.javaType}> ids); + + /** + * 获得${table.classComment}分页 + * + * @param pageReqVO 分页查询 + * @return ${table.classComment}分页 + */ + PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO); + + /** + * 获得${table.classComment}列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return ${table.classComment}列表 + */ + List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO); + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm new file mode 100644 index 0000000..a732039 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm @@ -0,0 +1,82 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import org.springframework.validation.annotation.Validated; + +import java.util.*; +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${PageResultClassName}; + +import ${basePackage}.module.${table.moduleName}.convert.${table.businessName}.${table.className}Convert; +import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; + +import static ${ServiceExceptionUtilClassName}.exception; +import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; + +/** + * ${table.classComment} Service 实现类 + * + * @author ${table.author} + */ +@Service +@Validated +public class ${table.className}ServiceImpl implements ${table.className}Service { + + @Resource + private ${table.className}Mapper ${classNameVar}Mapper; + + @Override + public ${primaryColumn.javaType} create${simpleClassName}(${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO) { + // 插入 + ${table.className}DO ${classNameVar} = ${table.className}Convert.INSTANCE.convert(createReqVO); + ${classNameVar}Mapper.insert(${classNameVar}); + // 返回 + return ${classNameVar}.getId(); + } + + @Override + public void update${simpleClassName}(${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO) { + // 校验存在 + validate${simpleClassName}Exists(updateReqVO.getId()); + // 更新 + ${table.className}DO updateObj = ${table.className}Convert.INSTANCE.convert(updateReqVO); + ${classNameVar}Mapper.updateById(updateObj); + } + + @Override + public void delete${simpleClassName}(${primaryColumn.javaType} id) { + // 校验存在 + validate${simpleClassName}Exists(id); + // 删除 + ${classNameVar}Mapper.deleteById(id); + } + + private void validate${simpleClassName}Exists(${primaryColumn.javaType} id) { + if (${classNameVar}Mapper.selectById(id) == null) { + throw exception(${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + } + + @Override + public ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id) { + return ${classNameVar}Mapper.selectById(id); + } + + @Override + public List<${table.className}DO> get${simpleClassName}List(Collection<${primaryColumn.javaType}> ids) { + return ${classNameVar}Mapper.selectBatchIds(ids); + } + + @Override + public PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) { + return ${classNameVar}Mapper.selectPage(pageReqVO); + } + + @Override + public List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO) { + return ${classNameVar}Mapper.selectList(exportReqVO); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm new file mode 100644 index 0000000..ec1b45a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm @@ -0,0 +1,165 @@ +package ${basePackage}.module.${table.moduleName}.service.${table.businessName}; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; + +import javax.annotation.Resource; + +import ${baseFrameworkPackage}.test.core.ut.BaseDbUnitTest; + +import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*; +import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO; +import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper; +import ${PageResultClassName}; + +import javax.annotation.Resource; +import org.springframework.context.annotation.Import; +import java.util.*; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.*; +import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*; +import static ${baseFrameworkPackage}.test.core.util.AssertUtils.*; +import static ${baseFrameworkPackage}.test.core.util.RandomUtils.*; +import static ${LocalDateTimeUtilsClassName}.*; +import static ${ObjectUtilsClassName}.*; +import static ${DateUtilsClassName}.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +## 字段模板 +#macro(getPageCondition $VO) + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class, o -> { // 等会查询到 + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + o.set$JavaField(null); + #end + #end + }); + ${classNameVar}Mapper.insert(db${simpleClassName}); + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + // 测试 ${column.javaField} 不匹配 + ${classNameVar}Mapper.insert(cloneIgnoreId(db${simpleClassName}, o -> o.set$JavaField(null))); + #end + #end + // 准备参数 + ${sceneEnum.prefixClass}${table.className}${VO} reqVO = new ${sceneEnum.prefixClass}${table.className}${VO}(); + #foreach ($column in $columns) + #if (${column.listOperation}) + #set ($JavaField = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})##首字母大写 + #if (${column.listOperationCondition} == "BETWEEN")## BETWEEN 的情况 + reqVO.set${JavaField}(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + #else + reqVO.set$JavaField(null); + #end + #end + #end +#end +/** + * {@link ${table.className}ServiceImpl} 的单元测试类 + * + * @author ${table.author} + */ +@Import(${table.className}ServiceImpl.class) +public class ${table.className}ServiceImplTest extends BaseDbUnitTest { + + @Resource + private ${table.className}ServiceImpl ${classNameVar}Service; + + @Resource + private ${table.className}Mapper ${classNameVar}Mapper; + + @Test + public void testCreate${simpleClassName}_success() { + // 准备参数 + ${sceneEnum.prefixClass}${table.className}CreateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}CreateReqVO.class); + + // 调用 + ${primaryColumn.javaType} ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(reqVO); + // 断言 + assertNotNull(${classNameVar}Id); + // 校验记录的属性是否正确 + ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(${classNameVar}Id); + assertPojoEquals(reqVO, ${classNameVar}); + } + + @Test + public void testUpdate${simpleClassName}_success() { + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); + ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 + // 准备参数 + ${sceneEnum.prefixClass}${table.className}UpdateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}UpdateReqVO.class, o -> { + o.setId(db${simpleClassName}.getId()); // 设置更新的 ID + }); + + // 调用 + ${classNameVar}Service.update${simpleClassName}(reqVO); + // 校验是否更新正确 + ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, ${classNameVar}); + } + + @Test + public void testUpdate${simpleClassName}_notExists() { + // 准备参数 + ${sceneEnum.prefixClass}${table.className}UpdateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}UpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> ${classNameVar}Service.update${simpleClassName}(reqVO), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + + @Test + public void testDelete${simpleClassName}_success() { + // mock 数据 + ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class); + ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据 + // 准备参数 + ${primaryColumn.javaType} id = db${simpleClassName}.getId(); + + // 调用 + ${classNameVar}Service.delete${simpleClassName}(id); + // 校验数据不存在了 + assertNull(${classNameVar}Mapper.selectById(id)); + } + + @Test + public void testDelete${simpleClassName}_notExists() { + // 准备参数 + ${primaryColumn.javaType} id = random${primaryColumn.javaType}Id(); + + // 调用, 并断言异常 + assertServiceException(() -> ${classNameVar}Service.delete${simpleClassName}(id), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGet${simpleClassName}Page() { + #getPageCondition("PageReqVO") + + // 调用 + PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(db${simpleClassName}, pageResult.getList().get(0)); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGet${simpleClassName}List() { + #getPageCondition("ExportReqVO") + + // 调用 + List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(db${simpleClassName}, list.get(0)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/sql/h2.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/sql/h2.vm new file mode 100644 index 0000000..21d762f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/sql/h2.vm @@ -0,0 +1,35 @@ +-- 将该建表 SQL 语句,添加到 yunxi-module-${table.moduleName}-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "${table.tableName.toLowerCase()}" ( +#foreach ($column in $columns) +#if (${column.javaType} == 'Long') + #set ($dataType='bigint') +#elseif (${column.javaType} == 'Integer') + #set ($dataType='int') +#elseif (${column.javaType} == 'Boolean') + #set ($dataType='bit') +#elseif (${column.javaType} == 'Date') + #set ($dataType='datetime') +#else + #set ($dataType='varchar') +#end + #if (${column.primaryKey})##处理主键 + "${column.javaField}"#if (${column.javaType} == 'String') ${dataType} NOT NULL#else ${dataType} NOT NULL GENERATED BY DEFAULT AS IDENTITY#end, + #else + #if (${column.columnName} == 'create_time') + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + #elseif (${column.columnName} == 'update_time') + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + #elseif (${column.columnName} == 'creator' || ${column.columnName} == 'updater') + "${column.columnName}" ${dataType} DEFAULT '', + #elseif (${column.columnName} == 'deleted') + "deleted" bit NOT NULL DEFAULT FALSE, + #else + "${column.columnName.toLowerCase()}" ${dataType}#if (${column.nullable} == false) NOT NULL#end, + #end + #end +#end + PRIMARY KEY ("${primaryColumn.columnName.toLowerCase()}") +) COMMENT '${table.tableComment}'; + +-- 将该删表 SQL 语句,添加到 yunxi-module-${table.moduleName}-biz 模块的 test/resources/sql/clean.sql 文件里 +DELETE FROM "${table.tableName}"; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/sql/sql.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/sql/sql.vm new file mode 100644 index 0000000..902ca74 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/sql/sql.vm @@ -0,0 +1,28 @@ +-- 菜单 SQL +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status, component_name +) +VALUES ( + '${table.classComment}管理', '', 2, 0, ${table.parentMenuId}, + '${simpleClassName_strikeCase}', '', '${table.moduleName}/${classNameVar}/index', 0, '${table.className}' +); + +-- 按钮父菜单ID +-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码 +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +#set ($functionNames = ['查询', '创建', '更新', '删除', '导出']) +#set ($functionOps = ['query', 'create', 'update', 'delete', 'export']) +#foreach ($functionName in $functionNames) +#set ($index = $foreach.count - 1) +INSERT INTO system_menu( + name, permission, type, sort, parent_id, + path, icon, component, status +) +VALUES ( + '${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId, + '', '', '', 0 +); +#end diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm new file mode 100644 index 0000000..5e9da32 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm @@ -0,0 +1,55 @@ +import request from '@/utils/request' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +// 创建${table.classComment} +export function create${simpleClassName}(data) { + return request({ + url: '${baseURL}/create', + method: 'post', + data: data + }) +} + +// 更新${table.classComment} +export function update${simpleClassName}(data) { + return request({ + url: '${baseURL}/update', + method: 'put', + data: data + }) +} + +// 删除${table.classComment} +export function delete${simpleClassName}(id) { + return request({ + url: '${baseURL}/delete?id=' + id, + method: 'delete' + }) +} + +// 获得${table.classComment} +export function get${simpleClassName}(id) { + return request({ + url: '${baseURL}/get?id=' + id, + method: 'get' + }) +} + +// 获得${table.classComment}分页 +export function get${simpleClassName}Page(query) { + return request({ + url: '${baseURL}/page', + method: 'get', + params: query + }) +} + +// 导出${table.classComment} Excel +export function export${simpleClassName}Excel(query) { + return request({ + url: '${baseURL}/export-excel', + method: 'get', + params: query, + responseType: 'blob' + }) +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm new file mode 100644 index 0000000..7a6add6 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm @@ -0,0 +1,369 @@ + + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm new file mode 100644 index 0000000..401796d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm @@ -0,0 +1,46 @@ +import request from '@/config/axios' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +export interface ${simpleClassName}VO { +#foreach ($column in $columns) +#if ($column.createOperation || $column.updateOperation) +#if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "short" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") + ${column.javaField}: number +#elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime") + ${column.javaField}: Date +#else + ${column.javaField}: ${column.javaType.toLowerCase()} +#end +#end +#end +} + +// 查询${table.classComment}列表 +export const get${simpleClassName}Page = async (params) => { + return await request.get({ url: `${baseURL}/page`, params }) +} + +// 查询${table.classComment}详情 +export const get${simpleClassName} = async (id: number) => { + return await request.get({ url: `${baseURL}/get?id=` + id }) +} + +// 新增${table.classComment} +export const create${simpleClassName} = async (data: ${simpleClassName}VO) => { + return await request.post({ url: `${baseURL}/create`, data }) +} + +// 修改${table.classComment} +export const update${simpleClassName} = async (data: ${simpleClassName}VO) => { + return await request.put({ url: `${baseURL}/update`, data }) +} + +// 删除${table.classComment} +export const delete${simpleClassName} = async (id: number) => { + return await request.delete({ url: `${baseURL}/delete?id=` + id }) +} + +// 导出${table.classComment} Excel +export const export${simpleClassName} = async (params) => { + return await request.download({ url: `${baseURL}/export-excel`, params }) +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm new file mode 100644 index 0000000..4e23f2f --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm @@ -0,0 +1,234 @@ + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm new file mode 100644 index 0000000..1e98abe --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm @@ -0,0 +1,287 @@ + + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm new file mode 100644 index 0000000..48cd542 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/api/api.ts.vm @@ -0,0 +1,46 @@ +import request from '@/config/axios' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +export interface ${simpleClassName}VO { + #foreach ($column in $columns) + #if ($column.createOperation || $column.updateOperation) + #if(${column.javaType.toLowerCase()} == "long" || ${column.javaType.toLowerCase()} == "integer" || ${column.javaType.toLowerCase()} == "double" || ${column.javaType.toLowerCase()} == "bigdecimal") + ${column.javaField}: number + #elseif(${column.javaType.toLowerCase()} == "date" || ${column.javaType.toLowerCase()} == "localdatetime") + ${column.javaField}: Date + #else + ${column.javaField}: ${column.javaType.toLowerCase()} + #end + #end + #end +} + +// 查询${table.classComment}列表 +export const get${simpleClassName}Page = async (params) => { + return await request.get({ url: '${baseURL}/page', params }) +} + +// 查询${table.classComment}详情 +export const get${simpleClassName} = async (id: number) => { + return await request.get({ url: '${baseURL}/get?id=' + id }) +} + +// 新增${table.classComment} +export const create${simpleClassName} = async (data: ${simpleClassName}VO) => { + return await request.post({ url: '${baseURL}/create', data }) +} + +// 修改${table.classComment} +export const update${simpleClassName} = async (data: ${simpleClassName}VO) => { + return await request.put({ url: '${baseURL}/update', data }) +} + +// 删除${table.classComment} +export const delete${simpleClassName} = async (id: number) => { + return await request.delete({ url: '${baseURL}/delete?id=' + id }) +} + +// 导出${table.classComment} Excel +export const export${simpleClassName}Api = async (params) => { + return await request.download({ url: '${baseURL}/export-excel', params }) +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm new file mode 100644 index 0000000..cbc6a17 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm @@ -0,0 +1,129 @@ +import type { CrudSchema } from '@/hooks/web/useCrudSchemas' +#foreach ($column in $columns) + #if ($column.listOperationResult && $column.htmlType == "datetime") +import { dateFormatter } from '@/utils/formatTime' + #break + #end +#end + +// 表单校验 +export const rules = reactive({ +#foreach ($column in $columns) +#if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 +#set($comment=$column.columnComment) + $column.javaField: [required], +#end +#end +}) + +// CrudSchema https://doc.iocoder.cn/vue3/crud-schema/ +const crudSchemas = reactive([ +#foreach($column in $columns) +#if ($column.listOperation || $column.listOperationResult || $column.createOperation || $column.updateOperation) +#set ($dictType = $column.dictType) +#set ($javaField = $column.javaField) +#set ($javaType = $column.javaType) + { + label: '${column.columnComment}', + field: '${column.javaField}', +## ========= 字典部分 ========= + #if ("" != $dictType)## 有数据字典 + dictType: DICT_TYPE.$dictType.toUpperCase(), + #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short") + dictClass: 'number', + #elseif ($javaType == "String") + dictClass: 'string', + #elseif ($javaType == "Boolean") + dictClass: 'boolean', + #end + #end +## ========= Table 表格部分 ========= + #if (!$column.listOperationResult) + isTable: false, + #else + #if ($column.htmlType == "datetime") + formatter: dateFormatter, + #end + #end +## ========= Search 表格部分 ========= + #if ($column.listOperation) + isSearch: true, + #if ($column.htmlType == "datetime") + search: { + component: 'DatePicker', + componentProps: { + valueFormat: 'YYYY-MM-DD HH:mm:ss', + type: 'daterange', + defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')] + } + }, + #end + #end +## ========= Form 表单部分 ========= + #if ((!$column.createOperation && !$column.updateOperation) || $column.primaryKey) + isForm: false, + #else + #if($column.htmlType == "imageUpload")## 图片上传 + form: { + component: 'UploadImg' + }, + #elseif($column.htmlType == "fileUpload")## 文件上传 + form: { + component: 'UploadFile' + }, + #elseif($column.htmlType == "editor")## 文本编辑器 + form: { + component: 'Editor', + componentProps: { + valueHtml: '', + height: 200 + } + }, + #elseif($column.htmlType == "select")## 下拉框 + form: { + component: 'SelectV2' + }, + #elseif($column.htmlType == "checkbox")## 多选框 + form: { + component: 'Checkbox' + }, + #elseif($column.htmlType == "radio")## 单选框 + form: { + component: 'Radio' + }, + #elseif($column.htmlType == "datetime")## 时间框 + form: { + component: 'DatePicker', + componentProps: { + type: 'datetime', + valueFormat: 'x' + } + }, + #elseif($column.htmlType == "textarea")## 文本框 + form: { + component: 'Input', + componentProps: { + type: 'textarea', + rows: 4 + }, + colProps: { + span: 24 + } + }, + #elseif(${javaType.toLowerCase()} == "long" || ${javaType.toLowerCase()} == "integer")## 文本框 + form: { + component: 'InputNumber', + value: 0 + }, + #end + #end + }, +#end +#end + { + label: '操作', + field: 'action', + isForm: false + } +]) +export const { allSchemas } = useCrudSchemas(crudSchemas) diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm new file mode 100644 index 0000000..45b8aa2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm @@ -0,0 +1,65 @@ + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm new file mode 100644 index 0000000..c2cbeff --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm @@ -0,0 +1,85 @@ + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/api/api.ts.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/api/api.ts.vm new file mode 100644 index 0000000..c0a53ee --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/api/api.ts.vm @@ -0,0 +1,32 @@ +import { defHttp } from '@/utils/http/axios' +#set ($baseURL = "/${table.moduleName}/${simpleClassName_strikeCase}") + +// 查询${table.classComment}列表 +export function get${simpleClassName}Page(params) { + return defHttp.get({ url: '${baseURL}/page', params }) +} + +// 查询${table.classComment}详情 +export function get${simpleClassName}(id: number) { + return defHttp.get({ url: '${baseURL}/get?id=' + id }) +} + +// 新增${table.classComment} +export function create${simpleClassName}(data) { + return defHttp.post({ url: '${baseURL}/create', data }) +} + +// 修改${table.classComment} +export function update${simpleClassName}(data) { + return defHttp.put({ url: '${baseURL}/update', data }) +} + +// 删除${table.classComment} +export function delete${simpleClassName}(id: number) { + return defHttp.delete({ url: '${baseURL}/delete?id=' + id }) +} + +// 导出${table.classComment} Excel +export function export${simpleClassName}(params) { + return defHttp.download({ url: '${baseURL}/export-excel', params }, '${table.classComment}.xls') +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm new file mode 100644 index 0000000..6f0c01d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/data.ts.vm @@ -0,0 +1,208 @@ +import { BasicColumn, FormSchema, useRender } from '@/components/Table' +import { DICT_TYPE, getDictOptions } from '@/utils/dict' + +export const columns: BasicColumn[] = [ +#foreach($column in $columns) +#if ($column.listOperationResult) + #set ($dictType=$column.dictType) + #set ($javaField = $column.javaField) + #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + #set ($comment=$column.columnComment) +#if ($column.javaType == "LocalDateTime")## 时间类型 + { + title: '${comment}', + dataIndex: '${javaField}', + width: 180, + customRender: ({ text }) => { + return useRender.renderDate(text) + } + }, +#elseif("" != $column.dictType)## 数据字典 + { + title: '${comment}', + dataIndex: '${javaField}', + width: 180, + customRender: ({ text }) => { + return useRender.renderDict(text, DICT_TYPE.$dictType.toUpperCase()) + } + }, +#else + { + title: '${comment}', + dataIndex: '${javaField}', + width: 160 + }, +#end +#end +#end +] + +export const searchFormSchema: FormSchema[] = [ +#foreach($column in $columns) +#if ($column.listOperation) + #set ($dictType=$column.dictType) + #set ($javaField = $column.javaField) + #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + #set ($comment=$column.columnComment) + { + label: '${comment}', + field: '${javaField}', + #if ($column.htmlType == "input") + component: 'Input', + #elseif ($column.htmlType == "select" || $column.htmlType == "radio") + component: 'Select', + componentProps: { + #if ("" != $dictType)## 设置了 dictType 数据字典的情况 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase()) + #else## 未设置 dictType 数据字典的情况 + options: [] + #end + }, + #elseif($column.htmlType == "datetime") + component: 'RangePicker', + #end + colProps: { span: 8 } + }, +#end +#end +] + +export const createFormSchema: FormSchema[] = [ + { + label: '编号', + field: 'id', + show: false, + component: 'Input' + }, +#foreach($column in $columns) +#if ($column.createOperation) + #set ($dictType = $column.dictType) + #set ($javaField = $column.javaField) + #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) + #set ($comment = $column.columnComment) +#if (!$column.primaryKey)## 忽略主键,不用在表单里 + { + label: '${comment}', + field: '${javaField}', + #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 + required: true, + #end + #if ($column.htmlType == "input") + component: 'Input' + #elseif($column.htmlType == "imageUpload")## 图片上传 + component: 'FileUpload', + componentProps: { + fileType: 'file', + maxCount: 1 + } + #elseif($column.htmlType == "fileUpload")## 文件上传 + component: 'FileUpload', + componentProps: { + fileType: 'image', + maxCount: 1 + } + #elseif($column.htmlType == "editor")## 文本编辑器 + component: 'Editor' + #elseif($column.htmlType == "select")## 下拉框 + component: 'Select', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number') + #else##没数据字典 + options:[] + #end + } + #elseif($column.htmlType == "checkbox")## 多选框 + component: 'Checkbox', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number') + #else##没数据字典 + options:[] + #end + } + #elseif($column.htmlType == "radio")## 单选框 + component: 'RadioButtonGroup', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number') + #else##没数据字典 + options:[] + #end + } + #elseif($column.htmlType == "datetime")## 时间框 + component: 'DatePicker' + #elseif($column.htmlType == "textarea")## 文本域 + component: 'InputTextArea' + #end + }, +#end +#end +#end +] + +export const updateFormSchema: FormSchema[] = [ + { + label: '编号', + field: 'id', + show: false, + component: 'Input' + }, +#foreach($column in $columns) +#if ($column.updateOperation) +#set ($dictType = $column.dictType) +#set ($javaField = $column.javaField) +#set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)}) +#set ($comment = $column.columnComment) +#if (!$column.primaryKey)## 忽略主键,不用在表单里 + { + label: '${comment}', + field: '${javaField}', + #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键 + required: true, + #end + #if ($column.htmlType == "input") + component: 'Input' + #elseif($column.htmlType == "imageUpload")## 图片上传 + component: 'Upload' + #elseif($column.htmlType == "fileUpload")## 文件上传 + component: 'Upload' + #elseif($column.htmlType == "editor")## 文本编辑器 + component: 'Editor' + #elseif($column.htmlType == "select")## 下拉框 + component: 'Select', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number') + #else##没数据字典 + options:[] + #end + } + #elseif($column.htmlType == "checkbox")## 多选框 + component: 'Checkbox', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number') + #else##没数据字典 + options:[] + #end + } + #elseif($column.htmlType == "radio")## 单选框 + component: 'RadioButtonGroup', + componentProps: { + #if ("" != $dictType)## 有数据字典 + options: getDictOptions(DICT_TYPE.$dictType.toUpperCase(), 'number') + #else##没数据字典 + options:[] + #end + } + #elseif($column.htmlType == "datetime")## 时间框 + component: 'DatePicker' + #elseif($column.htmlType == "textarea")## 文本域 + component: 'InputTextArea' + #end + }, +#end +#end +#end +] \ No newline at end of file diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/form.vue.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/form.vue.vm new file mode 100644 index 0000000..f0157e2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/form.vue.vm @@ -0,0 +1,57 @@ + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/index.vue.vm b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/index.vue.vm new file mode 100644 index 0000000..e5faddd --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/codegen/vue3_vben/views/index.vue.vm @@ -0,0 +1,92 @@ + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/file/erweima.jpg b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/file/erweima.jpg new file mode 100644 index 0000000..1447283 Binary files /dev/null and b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/file/erweima.jpg differ diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/mapper/null/.gitkeep b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/mapper/null/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/mapper/test/TestDemoMapper.xml b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/mapper/test/TestDemoMapper.xml new file mode 100644 index 0000000..8bdc827 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/main/resources/mapper/test/TestDemoMapper.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java new file mode 100644 index 0000000..4c5abe0 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.infra.dal.mysql.codegen; + +import com.yunxi.scm.module.tool.dal.dataobject.codegen.SchemaColumnDO; +import com.yunxi.scm.module.tool.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; + +import javax.annotation.Resource; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SchemaColumnMapperTest extends BaseDbUnitTest { + + @Resource + private SchemaColumnMapper schemaColumnMapper; + + @Test + public void testSelectListByTableName() { + List columns = schemaColumnMapper.selectListByTableName("", "inf_config"); + assertTrue(columns.size() > 0); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/mysql/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/mysql/package-info.java new file mode 100644 index 0000000..7495957 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/mysql/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.infra.dal.mysql; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/package-info.java new file mode 100644 index 0000000..dbf2410 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/dal/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.infra.dal; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenEngineTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenEngineTest.java new file mode 100644 index 0000000..bfe94dc --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenEngineTest.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.infra.service.codegen; + +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenColumnDO; +import com.yunxi.scm.module.infra.dal.dataobject.codegen.CodegenTableDO; +import com.yunxi.scm.module.infra.dal.mysql.codegen.CodegenColumnMapper; +import com.yunxi.scm.module.infra.dal.mysql.codegen.CodegenTableMapper; +import com.yunxi.scm.module.infra.service.codegen.inner.CodegenEngine; +import com.yunxi.scm.module.infra.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +public class CodegenEngineTest extends BaseDbUnitTest { + + @Resource + private CodegenTableMapper codegenTableMapper; + @Resource + private CodegenColumnMapper codegenColumnMapper; + + @Resource + private CodegenEngine codegenEngine; + + @Test + public void testExecute() { + CodegenTableDO table = codegenTableMapper.selectById(20); + List columns = codegenColumnMapper.selectListByTableId(table.getId()); + Map result = codegenEngine.execute(table, columns); + result.forEach((s, s2) -> System.out.println(s2)); +// System.out.println(result.get("vue/views/system/test/index.vue")); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenSQLParserTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenSQLParserTest.java new file mode 100644 index 0000000..8288aa2 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenSQLParserTest.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.infra.service.codegen; + +import com.yunxi.scm.module.infra.service.codegen.inner.CodegenSQLParser; +import com.yunxi.scm.module.infra.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; + +public class CodegenSQLParserTest extends BaseDbUnitTest { + + @Test + public void testParse() { + String sql = "CREATE TABLE `infra_test_demo` (\n" + + " `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',\n" + + " `name` varchar(100) NOT NULL DEFAULT '' COMMENT '名字',\n" + + " `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态',\n" + + " `type` tinyint(4) NOT NULL COMMENT '类型',\n" + + " `category` tinyint(4) NOT NULL COMMENT '分类',\n" + + " `remark` varchar(500) DEFAULT NULL COMMENT '备注',\n" + + " `create_by` varchar(64) DEFAULT '' COMMENT '创建者',\n" + + " `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n" + + " `update_by` varchar(64) DEFAULT '' COMMENT '更新者',\n" + + " `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',\n" + + " `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',\n" + + " PRIMARY KEY (`id`) USING BTREE\n" + + ") ENGINE=InnoDB AUTO_INCREMENT=108 DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表';"; + CodegenSQLParser.parse(sql); + // TODO 芋艿:后续完善断言 + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenServiceImplTest.java new file mode 100644 index 0000000..21be130 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/codegen/CodegenServiceImplTest.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.infra.service.codegen; + +import com.yunxi.scm.module.infra.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; + +import javax.annotation.Resource; + +class CodegenServiceImplTest extends BaseDbUnitTest { + + @Resource + private CodegenServiceImpl codegenService; + + @Test + public void tetCreateCodegenTable() { + codegenService.createCodegen(0L, "infra_test_demo"); +// infraCodegenService.createCodegenTable("infra_codegen_table"); +// infraCodegenService.createCodegen("infra_codegen_column"); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/package-info.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/package-info.java new file mode 100644 index 0000000..b4bd5c3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/service/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.infra.service; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/test/BaseDbAndRedisIntegrationTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/test/BaseDbAndRedisIntegrationTest.java new file mode 100644 index 0000000..65d840c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/test/BaseDbAndRedisIntegrationTest.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.infra.test; + +import com.yunxi.scm.framework.datasource.config.YunxiDataSourceAutoConfiguration; +import com.yunxi.scm.framework.mybatis.config.YunxiMybatisAutoConfiguration; +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseDbAndRedisIntegrationTest { + + @Import({ + // DB 配置类 + DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类 + YunxiDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + // MyBatis 配置类 + YunxiMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YunxiRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/test/BaseRedisIntegrationTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/test/BaseRedisIntegrationTest.java new file mode 100644 index 0000000..f153e16 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test-integration/java/com/yunxi/scm/module/infra/test/BaseRedisIntegrationTest.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.infra.test; + +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseRedisIntegrationTest { + + @Import({ + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YunxiRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/DefaultDatabaseQueryTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/DefaultDatabaseQueryTest.java new file mode 100644 index 0000000..54c58d3 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/DefaultDatabaseQueryTest.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.infra.service; + +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.generator.query.DefaultQuery; +import com.baomidou.mybatisplus.generator.config.DataSourceConfig; +import com.baomidou.mybatisplus.generator.config.builder.ConfigBuilder; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; + +import java.util.List; + +public class DefaultDatabaseQueryTest { + + public static void main(String[] args) { +// DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder("jdbc:oracle:thin:@127.0.0.1:1521:xe", +// "root", "123456").build(); + DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder("jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro", + "root", "123456").build(); +// StrategyConfig strategyConfig = new StrategyConfig.Builder().build(); + + ConfigBuilder builder = new ConfigBuilder(null, dataSourceConfig, null, null, null, null); + + DefaultQuery query = new DefaultQuery(builder); + + long time = System.currentTimeMillis(); + List tableInfos = query.queryTables(); + for (TableInfo tableInfo : tableInfos) { + if (StrUtil.startWithAny(tableInfo.getName().toLowerCase(), "act_", "flw_", "qrtz_")) { + continue; + } + System.out.println(String.format("CREATE SEQUENCE %s_seq MINVALUE 1;", tableInfo.getName())); +// System.out.println(String.format("DELETE FROM %s WHERE deleted = '1';", tableInfo.getName())); + } + System.out.println(tableInfos.size()); + System.out.println(System.currentTimeMillis() - time); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/config/ConfigServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/config/ConfigServiceImplTest.java new file mode 100644 index 0000000..36ac0e7 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/config/ConfigServiceImplTest.java @@ -0,0 +1,253 @@ +package com.yunxi.scm.module.infra.service.config; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.framework.test.core.util.RandomUtils; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.config.vo.ConfigUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.config.ConfigDO; +import com.yunxi.scm.module.infra.dal.mysql.config.ConfigMapper; +import com.yunxi.scm.module.infra.enums.config.ConfigTypeEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; + +@Import(ConfigServiceImpl.class) +public class ConfigServiceImplTest extends BaseDbUnitTest { + + @Resource + private ConfigServiceImpl configService; + + @Resource + private ConfigMapper configMapper; + + @Test + public void testCreateConfig_success() { + // 准备参数 + ConfigCreateReqVO reqVO = randomPojo(ConfigCreateReqVO.class); + + // 调用 + Long configId = configService.createConfig(reqVO); + // 断言 + assertNotNull(configId); + // 校验记录的属性是否正确 + ConfigDO config = configMapper.selectById(configId); + assertPojoEquals(reqVO, config); + assertEquals(ConfigTypeEnum.CUSTOM.getType(), config.getType()); + } + + @Test + public void testUpdateConfig_success() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + ConfigUpdateReqVO reqVO = randomPojo(ConfigUpdateReqVO.class, o -> { + o.setId(dbConfig.getId()); // 设置更新的 ID + }); + + // 调用 + configService.updateConfig(reqVO); + // 校验是否更新正确 + ConfigDO config = configMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, config); + } + + @Test + public void testDeleteConfig_success() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(o -> { + o.setType(ConfigTypeEnum.CUSTOM.getType()); // 只能删除 CUSTOM 类型 + }); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbConfig.getId(); + + // 调用 + configService.deleteConfig(id); + // 校验数据不存在了 + assertNull(configMapper.selectById(id)); + } + + @Test + public void testDeleteConfig_canNotDeleteSystemType() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(o -> { + o.setType(ConfigTypeEnum.SYSTEM.getType()); // SYSTEM 不允许删除 + }); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbConfig.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> configService.deleteConfig(id), CONFIG_CAN_NOT_DELETE_SYSTEM_TYPE); + } + + @Test + public void testValidateConfigExists_success() { + // mock 数据 + ConfigDO dbConfigDO = randomConfigDO(); + configMapper.insert(dbConfigDO);// @Sql: 先插入出一条存在的数据 + + // 调用成功 + configService.validateConfigExists(dbConfigDO.getId()); + } + + @Test + public void testValidateConfigExist_notExists() { + assertServiceException(() -> configService.validateConfigExists(randomLongId()), CONFIG_NOT_EXISTS); + } + + @Test + public void testValidateConfigKeyUnique_success() { + // 调用,成功 + configService.validateConfigKeyUnique(randomLongId(), randomString()); + } + + @Test + public void testValidateConfigKeyUnique_keyDuplicateForCreate() { + // 准备参数 + String key = randomString(); + // mock 数据 + configMapper.insert(randomConfigDO(o -> o.setConfigKey(key))); + + // 调用,校验异常 + assertServiceException(() -> configService.validateConfigKeyUnique(null, key), + CONFIG_KEY_DUPLICATE); + } + + @Test + public void testValidateConfigKeyUnique_keyDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String key = randomString(); + // mock 数据 + configMapper.insert(randomConfigDO(o -> o.setConfigKey(key))); + + // 调用,校验异常 + assertServiceException(() -> configService.validateConfigKeyUnique(id, key), + CONFIG_KEY_DUPLICATE); + } + + @Test + public void testGetConfigPage() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(o -> { // 等会查询到 + o.setName("芋艿"); + o.setConfigKey("yunai"); + o.setType(ConfigTypeEnum.SYSTEM.getType()); + o.setCreateTime(buildTime(2021, 2, 1)); + }); + configMapper.insert(dbConfig); + // 测试 name 不匹配 + configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setName("土豆"))); + // 测试 key 不匹配 + configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setConfigKey("tudou"))); + // 测试 type 不匹配 + configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setType(ConfigTypeEnum.CUSTOM.getType()))); + // 测试 createTime 不匹配 + configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + ConfigPageReqVO reqVO = new ConfigPageReqVO(); + reqVO.setName("艿"); + reqVO.setKey("nai"); + reqVO.setType(ConfigTypeEnum.SYSTEM.getType()); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 15, 2021, 2, 15)); + + // 调用 + PageResult pageResult = configService.getConfigPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbConfig, pageResult.getList().get(0)); + } + + @Test + public void testGetConfigList() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(o -> { // 等会查询到 + o.setName("芋艿"); + o.setConfigKey("yunai"); + o.setType(ConfigTypeEnum.SYSTEM.getType()); + o.setCreateTime(buildTime(2021, 2, 1)); + }); + configMapper.insert(dbConfig); + // 测试 name 不匹配 + configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setName("土豆"))); + // 测试 key 不匹配 + configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setConfigKey("tudou"))); + // 测试 type 不匹配 + configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setType(ConfigTypeEnum.CUSTOM.getType()))); + // 测试 createTime 不匹配 + configMapper.insert(cloneIgnoreId(dbConfig, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + ConfigExportReqVO reqVO = new ConfigExportReqVO(); + reqVO.setName("艿"); + reqVO.setKey("nai"); + reqVO.setType(ConfigTypeEnum.SYSTEM.getType()); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 15, 2021, 2, 15)); + + // 调用 + List list = configService.getConfigList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbConfig, list.get(0)); + } + + @Test + public void testGetConfig() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbConfig.getId(); + + // 调用 + ConfigDO config = configService.getConfig(id); + // 断言 + assertNotNull(config); + assertPojoEquals(dbConfig, config); + } + + @Test + public void testGetConfigByKey() { + // mock 数据 + ConfigDO dbConfig = randomConfigDO(); + configMapper.insert(dbConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + String key = dbConfig.getConfigKey(); + + // 调用 + ConfigDO config = configService.getConfigByKey(key); + // 断言 + assertNotNull(config); + assertPojoEquals(dbConfig, config); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static ConfigDO randomConfigDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setType(randomEle(ConfigTypeEnum.values()).getType()); // 保证 key 的范围 + }; + return RandomUtils.randomPojo(ConfigDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigServiceImplTest.java new file mode 100644 index 0000000..89d01d4 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/db/DataSourceConfigServiceImplTest.java @@ -0,0 +1,205 @@ +package com.yunxi.scm.module.infra.service.db; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.crypto.symmetric.AES; +import com.yunxi.scm.framework.mybatis.core.type.EncryptTypeHandler; +import com.yunxi.scm.framework.mybatis.core.util.JdbcUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.db.vo.DataSourceConfigUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.yunxi.scm.module.infra.dal.mysql.db.DataSourceConfigMapper; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.DATA_SOURCE_CONFIG_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * {@link DataSourceConfigServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(DataSourceConfigServiceImpl.class) +public class DataSourceConfigServiceImplTest extends BaseDbUnitTest { + + @Resource + private DataSourceConfigServiceImpl dataSourceConfigService; + + @Resource + private DataSourceConfigMapper dataSourceConfigMapper; + + @MockBean + private AES aes; + + @MockBean + private DynamicDataSourceProperties dynamicDataSourceProperties; + + @BeforeEach + public void setUp() { + // mock 一个空实现的 StringEncryptor,避免 EncryptTypeHandler 报错 + ReflectUtil.setFieldValue(EncryptTypeHandler.class, "aes", aes); + when(aes.encryptBase64(anyString())).then((Answer) invocation -> invocation.getArgument(0)); + when(aes.decryptStr(anyString())).then((Answer) invocation -> invocation.getArgument(0)); + + // mock DynamicDataSourceProperties + when(dynamicDataSourceProperties.getPrimary()).thenReturn("primary"); + when(dynamicDataSourceProperties.getDatasource()).thenReturn(MapUtil.of("primary", + new DataSourceProperty().setUrl("http://localhost:3306").setUsername("yunai").setPassword("tudou"))); + } + + @Test + public void testCreateDataSourceConfig_success() { + try (MockedStatic databaseUtilsMock = mockStatic(JdbcUtils.class)) { + // 准备参数 + DataSourceConfigCreateReqVO reqVO = randomPojo(DataSourceConfigCreateReqVO.class); + // mock 方法 + databaseUtilsMock.when(() -> JdbcUtils.isConnectionOK(eq(reqVO.getUrl()), + eq(reqVO.getUsername()), eq(reqVO.getPassword()))).thenReturn(true); + + // 调用 + Long dataSourceConfigId = dataSourceConfigService.createDataSourceConfig(reqVO); + // 断言 + assertNotNull(dataSourceConfigId); + // 校验记录的属性是否正确 + DataSourceConfigDO dataSourceConfig = dataSourceConfigMapper.selectById(dataSourceConfigId); + assertPojoEquals(reqVO, dataSourceConfig); + } + } + + @Test + public void testUpdateDataSourceConfig_success() { + try (MockedStatic databaseUtilsMock = mockStatic(JdbcUtils.class)) { + // mock 数据 + DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class); + dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DataSourceConfigUpdateReqVO reqVO = randomPojo(DataSourceConfigUpdateReqVO.class, o -> { + o.setId(dbDataSourceConfig.getId()); // 设置更新的 ID + }); + // mock 方法 + databaseUtilsMock.when(() -> JdbcUtils.isConnectionOK(eq(reqVO.getUrl()), + eq(reqVO.getUsername()), eq(reqVO.getPassword()))).thenReturn(true); + + // 调用 + dataSourceConfigService.updateDataSourceConfig(reqVO); + // 校验是否更新正确 + DataSourceConfigDO dataSourceConfig = dataSourceConfigMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, dataSourceConfig); + } + } + + @Test + public void testUpdateDataSourceConfig_notExists() { + // 准备参数 + DataSourceConfigUpdateReqVO reqVO = randomPojo(DataSourceConfigUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> dataSourceConfigService.updateDataSourceConfig(reqVO), DATA_SOURCE_CONFIG_NOT_EXISTS); + } + + @Test + public void testDeleteDataSourceConfig_success() { + // mock 数据 + DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class); + dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDataSourceConfig.getId(); + + // 调用 + dataSourceConfigService.deleteDataSourceConfig(id); + // 校验数据不存在了 + assertNull(dataSourceConfigMapper.selectById(id)); + } + + @Test + public void testDeleteDataSourceConfig_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> dataSourceConfigService.deleteDataSourceConfig(id), DATA_SOURCE_CONFIG_NOT_EXISTS); + } + + @Test // 测试使用 password 查询,可以查询到数据 + public void testSelectPassword() { + // mock 数据 + DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class); + dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据 + + // 调用 + DataSourceConfigDO result = dataSourceConfigMapper.selectOne(DataSourceConfigDO::getPassword, + EncryptTypeHandler.encrypt(dbDataSourceConfig.getPassword())); + assertPojoEquals(dbDataSourceConfig, result); + } + + @Test + public void testGetDataSourceConfig_master() { + // 准备参数 + Long id = 0L; + // mock 方法 + + // 调用 + DataSourceConfigDO dataSourceConfig = dataSourceConfigService.getDataSourceConfig(id); + // 断言 + assertEquals(id, dataSourceConfig.getId()); + assertEquals("primary", dataSourceConfig.getName()); + assertEquals("http://localhost:3306", dataSourceConfig.getUrl()); + assertEquals("yunai", dataSourceConfig.getUsername()); + assertEquals("tudou", dataSourceConfig.getPassword()); + } + + @Test + public void testGetDataSourceConfig_normal() { + // mock 数据 + DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class); + dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDataSourceConfig.getId(); + + // 调用 + DataSourceConfigDO dataSourceConfig = dataSourceConfigService.getDataSourceConfig(id); + // 断言 + assertPojoEquals(dbDataSourceConfig, dataSourceConfig); + } + + @Test + public void testGetDataSourceConfigList() { + // mock 数据 + DataSourceConfigDO dbDataSourceConfig = randomPojo(DataSourceConfigDO.class); + dataSourceConfigMapper.insert(dbDataSourceConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + + // 调用 + List dataSourceConfigList = dataSourceConfigService.getDataSourceConfigList(); + // 断言 + assertEquals(2, dataSourceConfigList.size()); + // master + assertEquals(0L, dataSourceConfigList.get(0).getId()); + assertEquals("primary", dataSourceConfigList.get(0).getName()); + assertEquals("http://localhost:3306", dataSourceConfigList.get(0).getUrl()); + assertEquals("yunai", dataSourceConfigList.get(0).getUsername()); + assertEquals("tudou", dataSourceConfigList.get(0).getPassword()); + // normal + assertPojoEquals(dbDataSourceConfig, dataSourceConfigList.get(1)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/db/DatabaseTableServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/db/DatabaseTableServiceImplTest.java new file mode 100644 index 0000000..af45c2a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/db/DatabaseTableServiceImplTest.java @@ -0,0 +1,89 @@ +package com.yunxi.scm.module.infra.service.db; + +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.dal.dataobject.db.DataSourceConfigDO; +import com.baomidou.mybatisplus.generator.config.po.TableField; +import com.baomidou.mybatisplus.generator.config.po.TableInfo; +import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; +import org.apache.ibatis.type.JdbcType; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import(DatabaseTableServiceImpl.class) +public class DatabaseTableServiceImplTest extends BaseDbUnitTest { + + @Resource + private DatabaseTableServiceImpl databaseTableService; + + @MockBean + private DataSourceConfigService dataSourceConfigService; + + @Test + public void testGetTableList() { + // 准备参数 + Long dataSourceConfigId = randomLongId(); + // mock 方法 + DataSourceConfigDO dataSourceConfig = new DataSourceConfigDO().setUsername("sa").setPassword("") + .setUrl("jdbc:h2:mem:testdb"); + when(dataSourceConfigService.getDataSourceConfig(eq(dataSourceConfigId))) + .thenReturn(dataSourceConfig); + + // 调用 + List tables = databaseTableService.getTableList(dataSourceConfigId, + "config", "参数"); + // 断言 + assertEquals(1, tables.size()); + assertTableInfo(tables.get(0)); + } + + @Test + public void testGetTable() { + // 准备参数 + Long dataSourceConfigId = randomLongId(); + // mock 方法 + DataSourceConfigDO dataSourceConfig = new DataSourceConfigDO().setUsername("sa").setPassword("") + .setUrl("jdbc:h2:mem:testdb"); + when(dataSourceConfigService.getDataSourceConfig(eq(dataSourceConfigId))) + .thenReturn(dataSourceConfig); + + // 调用 + TableInfo tableInfo = databaseTableService.getTable(dataSourceConfigId, "infra_config"); + // 断言 + assertTableInfo(tableInfo); + } + + private void assertTableInfo(TableInfo tableInfo) { + assertEquals("infra_config", tableInfo.getName()); + assertEquals("参数配置表", tableInfo.getComment()); + assertEquals(13, tableInfo.getFields().size()); + // id 字段 + TableField idField = tableInfo.getFields().get(0); + assertEquals("id", idField.getName()); + assertEquals(JdbcType.BIGINT, idField.getMetaInfo().getJdbcType()); + assertEquals("编号", idField.getComment()); + assertFalse(idField.getMetaInfo().isNullable()); + assertTrue(idField.isKeyFlag()); + assertTrue(idField.isKeyIdentityFlag()); + assertEquals(DbColumnType.LONG, idField.getColumnType()); + assertEquals("id", idField.getPropertyName()); + // name 字段 + TableField nameField = tableInfo.getFields().get(3); + assertEquals("name", nameField.getName()); + assertEquals(JdbcType.VARCHAR, nameField.getMetaInfo().getJdbcType()); + assertEquals("名字", nameField.getComment()); + assertFalse(nameField.getMetaInfo().isNullable()); + assertFalse(nameField.isKeyFlag()); + assertFalse(nameField.isKeyIdentityFlag()); + assertEquals(DbColumnType.STRING, nameField.getColumnType()); + assertEquals("name", nameField.getPropertyName()); + } +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/file/FileConfigServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/file/FileConfigServiceImplTest.java new file mode 100644 index 0000000..cd00c2d --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/file/FileConfigServiceImplTest.java @@ -0,0 +1,273 @@ +package com.yunxi.scm.module.infra.service.file; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.file.core.client.FileClient; +import com.yunxi.scm.framework.file.core.client.FileClientConfig; +import com.yunxi.scm.framework.file.core.client.FileClientFactory; +import com.yunxi.scm.framework.file.core.client.local.LocalFileClient; +import com.yunxi.scm.framework.file.core.client.local.LocalFileClientConfig; +import com.yunxi.scm.framework.file.core.enums.FileStorageEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.file.vo.config.FileConfigUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileConfigDO; +import com.yunxi.scm.module.infra.dal.mysql.file.FileConfigMapper; +import lombok.Data; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_DELETE_FAIL_MASTER; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.FILE_CONFIG_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** +* {@link FileConfigServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(FileConfigServiceImpl.class) +public class FileConfigServiceImplTest extends BaseDbUnitTest { + + @Resource + private FileConfigServiceImpl fileConfigService; + + @Resource + private FileConfigMapper fileConfigMapper; + + @MockBean + private Validator validator; + @MockBean + private FileClientFactory fileClientFactory; + + @Test + public void testInitLocalCache() { + // mock 数据 + FileConfigDO configDO1 = randomFileConfigDO().setId(1L).setMaster(true); + fileConfigMapper.insert(configDO1); + FileConfigDO configDO2 = randomFileConfigDO().setId(2L).setMaster(false); + fileConfigMapper.insert(configDO2); + // mock fileClientFactory 获得 master + FileClient masterFileClient = mock(FileClient.class); + when(fileClientFactory.getFileClient(eq(1L))).thenReturn(masterFileClient); + + // 调用 + fileConfigService.initLocalCache(); + // 断言 fileClientFactory 调用 + verify(fileClientFactory).createOrUpdateFileClient(eq(1L), + eq(configDO1.getStorage()), eq(configDO1.getConfig())); + verify(fileClientFactory).createOrUpdateFileClient(eq(2L), + eq(configDO2.getStorage()), eq(configDO2.getConfig())); + assertSame(masterFileClient, fileConfigService.getMasterFileClient()); + // 断言 fileConfigCache 缓存 + assertEquals(2, fileConfigService.getFileConfigCache().size()); + assertEquals(configDO1, fileConfigService.getFileConfigCache().get(0)); + assertEquals(configDO2, fileConfigService.getFileConfigCache().get(1)); + } + + @Test + public void testCreateFileConfig_success() { + // 准备参数 + Map config = MapUtil.builder().put("basePath", "/yunai") + .put("domain", "https://www.iocoder.cn").build(); + FileConfigCreateReqVO reqVO = randomPojo(FileConfigCreateReqVO.class, + o -> o.setStorage(FileStorageEnum.LOCAL.getStorage()).setConfig(config)); + + // 调用 + Long fileConfigId = fileConfigService.createFileConfig(reqVO); + // 断言 + assertNotNull(fileConfigId); + // 校验记录的属性是否正确 + FileConfigDO fileConfig = fileConfigMapper.selectById(fileConfigId); + assertPojoEquals(reqVO, fileConfig, "config"); + assertFalse(fileConfig.getMaster()); + assertEquals("/yunai", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath()); + assertEquals("https://www.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain()); + } + + @Test + public void testUpdateFileConfig_success() { + // mock 数据 + FileConfigDO dbFileConfig = randomPojo(FileConfigDO.class, o -> o.setStorage(FileStorageEnum.LOCAL.getStorage()) + .setConfig(new LocalFileClientConfig().setBasePath("/yunai").setDomain("https://www.iocoder.cn"))); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + FileConfigUpdateReqVO reqVO = randomPojo(FileConfigUpdateReqVO.class, o -> { + o.setId(dbFileConfig.getId()); // 设置更新的 ID + Map config = MapUtil.builder().put("basePath", "/yunai2") + .put("domain", "https://doc.iocoder.cn").build(); + o.setConfig(config); + }); + + // 调用 + fileConfigService.updateFileConfig(reqVO); + // 校验是否更新正确 + FileConfigDO fileConfig = fileConfigMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, fileConfig, "config"); + assertEquals("/yunai2", ((LocalFileClientConfig) fileConfig.getConfig()).getBasePath()); + assertEquals("https://doc.iocoder.cn", ((LocalFileClientConfig) fileConfig.getConfig()).getDomain()); + } + + @Test + public void testUpdateFileConfig_notExists() { + // 准备参数 + FileConfigUpdateReqVO reqVO = randomPojo(FileConfigUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> fileConfigService.updateFileConfig(reqVO), FILE_CONFIG_NOT_EXISTS); + } + + @Test + public void testUpdateFileConfigMaster_success() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + FileConfigDO masterFileConfig = randomFileConfigDO().setMaster(true); + fileConfigMapper.insert(masterFileConfig);// @Sql: 先插入出一条存在的数据 + + // 调用 + fileConfigService.updateFileConfigMaster(dbFileConfig.getId()); + // 断言数据 + assertTrue(fileConfigMapper.selectById(dbFileConfig.getId()).getMaster()); + assertFalse(fileConfigMapper.selectById(masterFileConfig.getId()).getMaster()); + } + + @Test + public void testUpdateFileConfigMaster_notExists() { + // 调用, 并断言异常 + assertServiceException(() -> fileConfigService.updateFileConfigMaster(randomLongId()), FILE_CONFIG_NOT_EXISTS); + } + + @Test + public void testDeleteFileConfig_success() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbFileConfig.getId(); + + // 调用 + fileConfigService.deleteFileConfig(id); + // 校验数据不存在了 + assertNull(fileConfigMapper.selectById(id)); + } + + @Test + public void testDeleteFileConfig_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> fileConfigService.deleteFileConfig(id), FILE_CONFIG_NOT_EXISTS); + } + + @Test + public void testDeleteFileConfig_master() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(true); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbFileConfig.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> fileConfigService.deleteFileConfig(id), FILE_CONFIG_DELETE_FAIL_MASTER); + } + + @Test + public void testGetFileConfigPage() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setName("芋道源码") + .setStorage(FileStorageEnum.LOCAL.getStorage()); + dbFileConfig.setCreateTime(LocalDateTimeUtil.parse("2020-01-23", DatePattern.NORM_DATE_PATTERN));// 等会查询到 + fileConfigMapper.insert(dbFileConfig); + // 测试 name 不匹配 + fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setName("源码"))); + // 测试 storage 不匹配 + fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setStorage(FileStorageEnum.DB.getStorage()))); + // 测试 createTime 不匹配 + fileConfigMapper.insert(cloneIgnoreId(dbFileConfig, o -> o.setCreateTime(LocalDateTimeUtil.parse("2020-11-23", DatePattern.NORM_DATE_PATTERN)))); + // 准备参数 + FileConfigPageReqVO reqVO = new FileConfigPageReqVO(); + reqVO.setName("芋道"); + reqVO.setStorage(FileStorageEnum.LOCAL.getStorage()); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2020, 1, 1), + buildTime(2020, 1, 24)})); + + // 调用 + PageResult pageResult = fileConfigService.getFileConfigPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbFileConfig, pageResult.getList().get(0)); + } + + @Test + public void testFileConfig() throws Exception { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbFileConfig.getId(); + // mock 获得 Client + FileClient fileClient = mock(FileClient.class); + when(fileClientFactory.getFileClient(eq(id))).thenReturn(fileClient); + when(fileClient.upload(any(), any(), any())).thenReturn("https://www.iocoder.cn"); + + // 调用,并断言 + assertEquals("https://www.iocoder.cn", fileConfigService.testFileConfig(id)); + } + + @Test + public void testGetFileConfig() { + // mock 数据 + FileConfigDO dbFileConfig = randomFileConfigDO().setMaster(false); + fileConfigMapper.insert(dbFileConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbFileConfig.getId(); + + // 调用,并断言 + assertPojoEquals(dbFileConfig, fileConfigService.getFileConfig(id)); + } + + @Test + public void testGetFileClient() { + // 准备参数 + Long id = randomLongId(); + // mock 获得 Client + FileClient fileClient = new LocalFileClient(id, new LocalFileClientConfig()); + when(fileClientFactory.getFileClient(eq(id))).thenReturn(fileClient); + + // 调用,并断言 + assertSame(fileClient, fileConfigService.getFileClient(id)); + } + + private FileConfigDO randomFileConfigDO() { + return randomPojo(FileConfigDO.class).setStorage(randomEle(FileStorageEnum.values()).getStorage()) + .setConfig(new EmptyFileClientConfig()); + } + + @Data + public static class EmptyFileClientConfig implements FileClientConfig, Serializable { + + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/file/FileServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/file/FileServiceImplTest.java new file mode 100644 index 0000000..8b7dc91 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/file/FileServiceImplTest.java @@ -0,0 +1,142 @@ +package com.yunxi.scm.module.infra.service.file; + +import cn.hutool.core.io.resource.ResourceUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.file.core.client.FileClient; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.framework.test.core.util.AssertUtils; +import com.yunxi.scm.module.infra.controller.admin.file.vo.file.FilePageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.file.FileDO; +import com.yunxi.scm.module.infra.dal.mysql.file.FileMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.*; + +@Import({FileServiceImpl.class}) +public class FileServiceImplTest extends BaseDbUnitTest { + + @Resource + private FileService fileService; + + @Resource + private FileMapper fileMapper; + + @MockBean + private FileConfigService fileConfigService; + + @Test + public void testGetFilePage() { + // mock 数据 + FileDO dbFile = randomPojo(FileDO.class, o -> { // 等会查询到 + o.setPath("yunai"); + o.setType("image/jpg"); + o.setCreateTime(buildTime(2021, 1, 15)); + }); + fileMapper.insert(dbFile); + // 测试 path 不匹配 + fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> o.setPath("tudou"))); + // 测试 type 不匹配 + fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> { + o.setType("image/png"); + })); + // 测试 createTime 不匹配 + fileMapper.insert(ObjectUtils.cloneIgnoreId(dbFile, o -> { + o.setCreateTime(buildTime(2020, 1, 15)); + })); + // 准备参数 + FilePageReqVO reqVO = new FilePageReqVO(); + reqVO.setPath("yunai"); + reqVO.setType("jp"); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2021, 1, 10), buildTime(2021, 1, 20)})); + + // 调用 + PageResult pageResult = fileService.getFilePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + AssertUtils.assertPojoEquals(dbFile, pageResult.getList().get(0)); + } + + @Test + public void testCreateFile_success() throws Exception { + // 准备参数 + String path = randomString(); + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + // mock Master 文件客户端 + FileClient client = mock(FileClient.class); + when(fileConfigService.getMasterFileClient()).thenReturn(client); + String url = randomString(); + when(client.upload(same(content), same(path), eq("image/jpeg"))).thenReturn(url); + when(client.getId()).thenReturn(10L); + String name = "单测文件名"; + // 调用 + String result = fileService.createFile(name, path, content); + // 断言 + assertEquals(result, url); + // 校验数据 + FileDO file = fileMapper.selectOne(FileDO::getPath, path); + assertEquals(10L, file.getConfigId()); + assertEquals(path, file.getPath()); + assertEquals(url, file.getUrl()); + assertEquals("image/jpeg", file.getType()); + assertEquals(content.length, file.getSize()); + } + + @Test + public void testDeleteFile_success() throws Exception { + // mock 数据 + FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg")); + fileMapper.insert(dbFile);// @Sql: 先插入出一条存在的数据 + // mock Master 文件客户端 + FileClient client = mock(FileClient.class); + when(fileConfigService.getFileClient(eq(10L))).thenReturn(client); + // 准备参数 + Long id = dbFile.getId(); + + // 调用 + fileService.deleteFile(id); + // 校验数据不存在了 + assertNull(fileMapper.selectById(id)); + // 校验调用 + verify(client).delete(eq("tudou.jpg")); + } + + @Test + public void testDeleteFile_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> fileService.deleteFile(id), FILE_NOT_EXISTS); + } + + @Test + public void testGetFileContent() throws Exception { + // 准备参数 + Long configId = 10L; + String path = "tudou.jpg"; + // mock 方法 + FileClient client = mock(FileClient.class); + when(fileConfigService.getFileClient(eq(10L))).thenReturn(client); + byte[] content = new byte[]{}; + when(client.getContent(eq("tudou.jpg"))).thenReturn(content); + + // 调用 + byte[] result = fileService.getFileContent(configId, path); + // 断言 + assertSame(result, content); + } +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/job/JobLogServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/job/JobLogServiceImplTest.java new file mode 100644 index 0000000..dfeafa9 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/job/JobLogServiceImplTest.java @@ -0,0 +1,203 @@ +package com.yunxi.scm.module.infra.service.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.log.JobLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobLogDO; +import com.yunxi.scm.module.infra.dal.mysql.job.JobLogMapper; +import com.yunxi.scm.module.infra.enums.job.JobLogStatusEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static java.util.Collections.singleton; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Import(JobLogServiceImpl.class) +public class JobLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private JobLogServiceImpl jobLogService; + @Resource + private JobLogMapper jobLogMapper; + + @Test + public void testCreateJobLog() { + // 准备参数 + JobLogDO reqVO = randomPojo(JobLogDO.class, o -> o.setExecuteIndex(1)); + + // 调用 + Long id = jobLogService.createJobLog(reqVO.getJobId(), reqVO.getBeginTime(), + reqVO.getHandlerName(), reqVO.getHandlerParam(), reqVO.getExecuteIndex()); + // 断言 + assertNotNull(id); + // 校验记录的属性是否正确 + JobLogDO job = jobLogMapper.selectById(id); + assertEquals(JobLogStatusEnum.RUNNING.getStatus(), job.getStatus()); + } + + @Test + public void testUpdateJobLogResultAsync_success() { + // mock 数据 + JobLogDO log = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + o.setStatus(JobLogStatusEnum.RUNNING.getStatus()); + }); + jobLogMapper.insert(log); + // 准备参数 + Long logId = log.getId(); + LocalDateTime endTime = randomLocalDateTime(); + Integer duration = randomInteger(); + boolean success = true; + String result = randomString(); + + // 调用 + jobLogService.updateJobLogResultAsync(logId, endTime, duration, success, result); + // 校验记录的属性是否正确 + JobLogDO dbLog = jobLogMapper.selectById(log.getId()); + assertEquals(endTime, dbLog.getEndTime()); + assertEquals(duration, dbLog.getDuration()); + assertEquals(JobLogStatusEnum.SUCCESS.getStatus(), dbLog.getStatus()); + assertEquals(result, dbLog.getResult()); + } + + @Test + public void testUpdateJobLogResultAsync_failure() { + // mock 数据 + JobLogDO log = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + o.setStatus(JobLogStatusEnum.RUNNING.getStatus()); + }); + jobLogMapper.insert(log); + // 准备参数 + Long logId = log.getId(); + LocalDateTime endTime = randomLocalDateTime(); + Integer duration = randomInteger(); + boolean success = false; + String result = randomString(); + + // 调用 + jobLogService.updateJobLogResultAsync(logId, endTime, duration, success, result); + // 校验记录的属性是否正确 + JobLogDO dbLog = jobLogMapper.selectById(log.getId()); + assertEquals(endTime, dbLog.getEndTime()); + assertEquals(duration, dbLog.getDuration()); + assertEquals(JobLogStatusEnum.FAILURE.getStatus(), dbLog.getStatus()); + assertEquals(result, dbLog.getResult()); + } + + @Test + public void testGetJobLog() { + // mock 数据 + JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> o.setExecuteIndex(1)); + jobLogMapper.insert(dbJobLog); + // 准备参数 + Long id = dbJobLog.getId(); + + // 调用 + JobLogDO jobLog = jobLogService.getJobLog(id); + // 断言 + assertPojoEquals(dbJobLog, jobLog); + } + + @Test + public void testGetJobLogList() { + // mock 数据 + JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> o.setExecuteIndex(1)); + jobLogMapper.insert(dbJobLog); + // 测试 handlerName 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> {})); + // 准备参数 + Collection ids = singleton(dbJobLog.getId()); + + // 调用 + List list = jobLogService.getJobLogList(ids); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbJobLog, list.get(0)); + } + + @Test + public void testGetJobPage() { + // mock 数据 + JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + o.setHandlerName("handlerName 单元测试"); + o.setStatus(JobLogStatusEnum.SUCCESS.getStatus()); + o.setBeginTime(buildTime(2021, 1, 8)); + o.setEndTime(buildTime(2021, 1, 8)); + }); + jobLogMapper.insert(dbJobLog); + // 测试 jobId 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setJobId(randomLongId()))); + // 测试 handlerName 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setHandlerName(randomString()))); + // 测试 beginTime 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setBeginTime(buildTime(2021, 1, 7)))); + // 测试 endTime 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setEndTime(buildTime(2021, 1, 9)))); + // 测试 status 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setStatus(JobLogStatusEnum.FAILURE.getStatus()))); + // 准备参数 + JobLogPageReqVO reqVo = new JobLogPageReqVO(); + reqVo.setJobId(dbJobLog.getJobId()); + reqVo.setHandlerName("单元"); + reqVo.setBeginTime(dbJobLog.getBeginTime()); + reqVo.setEndTime(dbJobLog.getEndTime()); + reqVo.setStatus(JobLogStatusEnum.SUCCESS.getStatus()); + + // 调用 + PageResult pageResult = jobLogService.getJobLogPage(reqVo); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbJobLog, pageResult.getList().get(0)); + } + + @Test + public void testGetJobList_export() { + // mock 数据 + JobLogDO dbJobLog = randomPojo(JobLogDO.class, o -> { + o.setExecuteIndex(1); + o.setHandlerName("handlerName 单元测试"); + o.setStatus(JobLogStatusEnum.SUCCESS.getStatus()); + o.setBeginTime(buildTime(2021, 1, 8)); + o.setEndTime(buildTime(2021, 1, 8)); + }); + jobLogMapper.insert(dbJobLog); + // 测试 jobId 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setJobId(randomLongId()))); + // 测试 handlerName 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setHandlerName(randomString()))); + // 测试 beginTime 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setBeginTime(buildTime(2021, 1, 7)))); + // 测试 endTime 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setEndTime(buildTime(2021, 1, 9)))); + // 测试 status 不匹配 + jobLogMapper.insert(cloneIgnoreId(dbJobLog, o -> o.setStatus(JobLogStatusEnum.FAILURE.getStatus()))); + // 准备参数 + JobLogExportReqVO reqVo = new JobLogExportReqVO(); + reqVo.setJobId(dbJobLog.getJobId()); + reqVo.setHandlerName("单元"); + reqVo.setBeginTime(dbJobLog.getBeginTime()); + reqVo.setEndTime(dbJobLog.getEndTime()); + reqVo.setStatus(JobLogStatusEnum.SUCCESS.getStatus()); + + // 调用 + List list = jobLogService.getJobLogList(reqVo); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbJobLog, list.get(0)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/job/JobServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/job/JobServiceImplTest.java new file mode 100644 index 0000000..3becc9c --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/job/JobServiceImplTest.java @@ -0,0 +1,290 @@ +package com.yunxi.scm.module.infra.service.job; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.quartz.core.scheduler.SchedulerManager; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.job.vo.job.JobUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.job.JobDO; +import com.yunxi.scm.module.infra.dal.mysql.job.JobMapper; +import com.yunxi.scm.module.infra.enums.job.JobStatusEnum; +import org.junit.jupiter.api.Test; +import org.quartz.SchedulerException; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.*; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +@Import(JobServiceImpl.class) +public class JobServiceImplTest extends BaseDbUnitTest { + + @Resource + private JobServiceImpl jobService; + @Resource + private JobMapper jobMapper; + @MockBean + private SchedulerManager schedulerManager; + + @Test + public void testCreateJob_cronExpressionValid() { + // 准备参数。Cron 表达式为 String 类型,默认随机字符串。 + JobCreateReqVO reqVO = randomPojo(JobCreateReqVO.class); + + // 调用,并断言异常 + assertServiceException(() -> jobService.createJob(reqVO), JOB_CRON_EXPRESSION_VALID); + } + + @Test + public void testCreateJob_jobHandlerExists() throws SchedulerException { + // 准备参数 指定 Cron 表达式 + JobCreateReqVO reqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + + // 调用 + jobService.createJob(reqVO); + // 调用,并断言异常 + assertServiceException(() -> jobService.createJob(reqVO), JOB_HANDLER_EXISTS); + } + + @Test + public void testCreateJob_success() throws SchedulerException { + // 准备参数 指定 Cron 表达式 + JobCreateReqVO reqVO = randomPojo(JobCreateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + + // 调用 + Long jobId = jobService.createJob(reqVO); + // 断言 + assertNotNull(jobId); + // 校验记录的属性是否正确 + JobDO job = jobMapper.selectById(jobId); + assertPojoEquals(reqVO, job); + assertEquals(JobStatusEnum.NORMAL.getStatus(), job.getStatus()); + // 校验调用 + verify(schedulerManager).addJob(eq(job.getId()), eq(job.getHandlerName()), eq(job.getHandlerParam()), + eq(job.getCronExpression()), eq(reqVO.getRetryCount()), eq(reqVO.getRetryInterval())); + } + + @Test + public void testUpdateJob_jobNotExists(){ + // 准备参数 + JobUpdateReqVO reqVO = randomPojo(JobUpdateReqVO.class, o -> o.setCronExpression("0 0/1 * * * ? *")); + + // 调用,并断言异常 + assertServiceException(() -> jobService.updateJob(reqVO), JOB_NOT_EXISTS); + } + + @Test + public void testUpdateJob_onlyNormalStatus(){ + // mock 数据 + JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.INIT.getStatus())); + jobMapper.insert(job); + // 准备参数 + JobUpdateReqVO updateReqVO = randomPojo(JobUpdateReqVO.class, o -> { + o.setId(job.getId()); + o.setCronExpression("0 0/1 * * * ? *"); + }); + + // 调用,并断言异常 + assertServiceException(() -> jobService.updateJob(updateReqVO), + JOB_UPDATE_ONLY_NORMAL_STATUS); + } + + @Test + public void testUpdateJob_success() throws SchedulerException { + // mock 数据 + JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus())); + jobMapper.insert(job); + // 准备参数 + JobUpdateReqVO updateReqVO = randomPojo(JobUpdateReqVO.class, o -> { + o.setId(job.getId()); + o.setCronExpression("0 0/1 * * * ? *"); + }); + + // 调用 + jobService.updateJob(updateReqVO); + // 校验记录的属性是否正确 + JobDO updateJob = jobMapper.selectById(updateReqVO.getId()); + assertPojoEquals(updateReqVO, updateJob); + // 校验调用 + verify(schedulerManager).updateJob(eq(job.getHandlerName()), eq(updateReqVO.getHandlerParam()), + eq(updateReqVO.getCronExpression()), eq(updateReqVO.getRetryCount()), eq(updateReqVO.getRetryInterval())); + } + + @Test + public void testUpdateJobStatus_changeStatusInvalid() { + // 调用,并断言异常 + assertServiceException(() -> jobService.updateJobStatus(1L, JobStatusEnum.INIT.getStatus()), + JOB_CHANGE_STATUS_INVALID); + } + + @Test + public void testUpdateJobStatus_changeStatusEquals() { + // mock 数据 + JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus())); + jobMapper.insert(job); + + // 调用,并断言异常 + assertServiceException(() -> jobService.updateJobStatus(job.getId(), job.getStatus()), + JOB_CHANGE_STATUS_EQUALS); + } + + @Test + public void testUpdateJobStatus_stopSuccess() throws SchedulerException { + // mock 数据 + JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus())); + jobMapper.insert(job); + + // 调用 + jobService.updateJobStatus(job.getId(), JobStatusEnum.STOP.getStatus()); + // 校验记录的属性是否正确 + JobDO dbJob = jobMapper.selectById(job.getId()); + assertEquals(JobStatusEnum.STOP.getStatus(), dbJob.getStatus()); + // 校验调用 + verify(schedulerManager).pauseJob(eq(job.getHandlerName())); + } + + @Test + public void testUpdateJobStatus_normalSuccess() throws SchedulerException { + // mock 数据 + JobDO job = randomPojo(JobDO.class, o -> o.setStatus(JobStatusEnum.STOP.getStatus())); + jobMapper.insert(job); + + // 调用 + jobService.updateJobStatus(job.getId(), JobStatusEnum.NORMAL.getStatus()); + // 校验记录的属性是否正确 + JobDO dbJob = jobMapper.selectById(job.getId()); + assertEquals(JobStatusEnum.NORMAL.getStatus(), dbJob.getStatus()); + // 校验调用 + verify(schedulerManager).resumeJob(eq(job.getHandlerName())); + } + + @Test + public void testTriggerJob_success() throws SchedulerException { + // mock 数据 + JobDO job = randomPojo(JobDO.class); + jobMapper.insert(job); + + // 调用 + jobService.triggerJob(job.getId()); + // 校验调用 + verify(schedulerManager).triggerJob(eq(job.getId()), + eq(job.getHandlerName()), eq(job.getHandlerParam())); + } + + @Test + public void testDeleteJob_success() throws SchedulerException { + // mock 数据 + JobDO job = randomPojo(JobDO.class); + jobMapper.insert(job); + + // 调用 + jobService.deleteJob(job.getId()); + // 校验不存在 + assertNull(jobMapper.selectById(job.getId())); + // 校验调用 + verify(schedulerManager).deleteJob(eq(job.getHandlerName())); + } + + @Test + public void testGetJobList() { + // mock 数据 + JobDO dbJob = randomPojo(JobDO.class, o -> { + o.setStatus(randomEle(JobStatusEnum.values()).getStatus()); // 保证 status 的范围 + }); + jobMapper.insert(dbJob); + // 测试 id 不匹配 + jobMapper.insert(cloneIgnoreId(dbJob, o -> {})); + + // 准备参数 + Collection ids = singletonList(dbJob.getId()); + // 调用 + List list = jobService.getJobList(ids); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbJob, list.get(0)); + } + + @Test + public void testGetJobPage() { + // mock 数据 + JobDO dbJob = randomPojo(JobDO.class, o -> { + o.setName("定时任务测试"); + o.setHandlerName("handlerName 单元测试"); + o.setStatus(JobStatusEnum.INIT.getStatus()); + }); + jobMapper.insert(dbJob); + // 测试 name 不匹配 + jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setName("土豆"))); + // 测试 status 不匹配 + jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus()))); + // 测试 handlerName 不匹配 + jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setHandlerName(randomString()))); + // 准备参数 + JobPageReqVO reqVo = new JobPageReqVO(); + reqVo.setName("定时"); + reqVo.setStatus(JobStatusEnum.INIT.getStatus()); + reqVo.setHandlerName("单元"); + + // 调用 + PageResult pageResult = jobService.getJobPage(reqVo); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbJob, pageResult.getList().get(0)); + } + + @Test + public void testGetJobList_export() { + // mock 数据 + JobDO dbJob = randomPojo(JobDO.class, o -> { + o.setName("定时任务测试"); + o.setHandlerName("handlerName 单元测试"); + o.setStatus(JobStatusEnum.INIT.getStatus()); + }); + jobMapper.insert(dbJob); + // 测试 name 不匹配 + jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setName("土豆"))); + // 测试 status 不匹配 + jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setStatus(JobStatusEnum.NORMAL.getStatus()))); + // 测试 handlerName 不匹配 + jobMapper.insert(cloneIgnoreId(dbJob, o -> o.setHandlerName(randomString()))); + // 准备参数 + JobExportReqVO reqVo = new JobExportReqVO(); + reqVo.setName("定时"); + reqVo.setStatus(JobStatusEnum.INIT.getStatus()); + reqVo.setHandlerName("单元"); + + // 调用 + List list = jobService.getJobList(reqVo); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbJob, list.get(0)); + } + + @Test + public void testGetJob() { + // mock 数据 + JobDO dbJob = randomPojo(JobDO.class); + jobMapper.insert(dbJob); + // 调用 + JobDO job = jobService.getJob(dbJob.getId()); + // 断言 + assertPojoEquals(dbJob, job); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogServiceImplTest.java new file mode 100644 index 0000000..6d9d1f1 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/logger/ApiAccessLogServiceImplTest.java @@ -0,0 +1,133 @@ +package com.yunxi.scm.module.infra.service.logger; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apiaccesslog.ApiAccessLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiAccessLogDO; +import com.yunxi.scm.module.infra.dal.mysql.logger.ApiAccessLogMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Import(ApiAccessLogServiceImpl.class) +public class ApiAccessLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private ApiAccessLogServiceImpl apiAccessLogService; + + @Resource + private ApiAccessLogMapper apiAccessLogMapper; + + @Test + public void testGetApiAccessLogPage() { + ApiAccessLogDO apiAccessLogDO = randomPojo(ApiAccessLogDO.class, o -> { + o.setUserId(2233L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setApplicationName("yunxi-test"); + o.setRequestUrl("foo"); + o.setBeginTime(buildTime(2021, 3, 13)); + o.setDuration(1000); + o.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + }); + apiAccessLogMapper.insert(apiAccessLogDO); + // 测试 userId 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setUserId(3344L))); + // 测试 userType 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 applicationName 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setApplicationName("test"))); + // 测试 requestUrl 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setRequestUrl("bar"))); + // 测试 beginTime 不匹配:构造一个早期时间 2021-02-06 00:00:00 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setBeginTime(buildTime(2021, 2, 6)))); + // 测试 duration 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setDuration(100))); + // 测试 resultCode 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setResultCode(2))); + // 准备参数 + ApiAccessLogPageReqVO reqVO = new ApiAccessLogPageReqVO(); + reqVO.setUserId(2233L); + reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); + reqVO.setApplicationName("yunxi-test"); + reqVO.setRequestUrl("foo"); + reqVO.setBeginTime(buildBetweenTime(2021, 3, 13, 2021, 3, 13)); + reqVO.setDuration(1000); + reqVO.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + + // 调用 + PageResult pageResult = apiAccessLogService.getApiAccessLogPage(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(apiAccessLogDO, pageResult.getList().get(0)); + } + + @Test + public void testGetApiAccessLogList() { + ApiAccessLogDO apiAccessLogDO = randomPojo(ApiAccessLogDO.class, o -> { + o.setUserId(2233L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setApplicationName("yunxi-test"); + o.setRequestUrl("foo"); + o.setBeginTime(buildTime(2021, 3, 13)); + o.setDuration(1000); + o.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + }); + apiAccessLogMapper.insert(apiAccessLogDO); + // 测试 userId 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setUserId(3344L))); + // 测试 userType 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 applicationName 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setApplicationName("test"))); + // 测试 requestUrl 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setRequestUrl("bar"))); + // 测试 beginTime 不匹配:构造一个早期时间 2021-02-06 00:00:00 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setBeginTime(buildTime(2021, 2, 6)))); + // 测试 duration 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setDuration(100))); + // 测试 resultCode 不匹配 + apiAccessLogMapper.insert(cloneIgnoreId(apiAccessLogDO, o -> o.setResultCode(2))); + // 准备参数 + ApiAccessLogExportReqVO reqVO = new ApiAccessLogExportReqVO(); + reqVO.setUserId(2233L); + reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); + reqVO.setApplicationName("yunxi-test"); + reqVO.setRequestUrl("foo"); + reqVO.setBeginTime(buildBetweenTime(2021, 3, 13, 2021, 3, 13)); + reqVO.setDuration(1000); + reqVO.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + + // 调用 + List list = apiAccessLogService.getApiAccessLogList(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, list.size()); + assertPojoEquals(apiAccessLogDO, list.get(0)); + } + + @Test + public void testCreateApiAccessLog() { + // 准备参数 + ApiAccessLogCreateReqDTO createDTO = randomPojo(ApiAccessLogCreateReqDTO.class); + + // 调用 + apiAccessLogService.createApiAccessLog(createDTO); + // 断言 + ApiAccessLogDO apiAccessLogDO = apiAccessLogMapper.selectOne(null); + assertPojoEquals(createDTO, apiAccessLogDO); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogServiceImplTest.java new file mode 100644 index 0000000..d3dad0a --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/logger/ApiErrorLogServiceImplTest.java @@ -0,0 +1,184 @@ +package com.yunxi.scm.module.infra.service.logger; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.logger.vo.apierrorlog.ApiErrorLogPageReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.logger.ApiErrorLogDO; +import com.yunxi.scm.module.infra.dal.mysql.logger.ApiErrorLogMapper; +import com.yunxi.scm.module.infra.enums.logger.ApiErrorLogProcessStatusEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_NOT_FOUND; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.API_ERROR_LOG_PROCESSED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Import(ApiErrorLogServiceImpl.class) +public class ApiErrorLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private ApiErrorLogServiceImpl apiErrorLogService; + + @Resource + private ApiErrorLogMapper apiErrorLogMapper; + + @Test + public void testGetApiErrorLogPage() { + // mock 数据 + ApiErrorLogDO apiErrorLogDO = randomPojo(ApiErrorLogDO.class, o -> { + o.setUserId(2233L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setApplicationName("yunxi-test"); + o.setRequestUrl("foo"); + o.setExceptionTime(buildTime(2021, 3, 13)); + o.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); + }); + apiErrorLogMapper.insert(apiErrorLogDO); + // 测试 userId 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, o -> o.setUserId(3344L))); + // 测试 userType 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 applicationName 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setApplicationName("test"))); + // 测试 requestUrl 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setRequestUrl("bar"))); + // 测试 exceptionTime 不匹配:构造一个早期时间 2021-02-06 00:00:00 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setExceptionTime(buildTime(2021, 2, 6)))); + // 测试 progressStatus 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setProcessStatus(ApiErrorLogProcessStatusEnum.DONE.getStatus()))); + // 准备参数 + ApiErrorLogPageReqVO reqVO = new ApiErrorLogPageReqVO(); + reqVO.setUserId(2233L); + reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); + reqVO.setApplicationName("yunxi-test"); + reqVO.setRequestUrl("foo"); + reqVO.setExceptionTime(buildBetweenTime(2021, 3, 1, 2021, 3, 31)); + reqVO.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); + + // 调用 + PageResult pageResult = apiErrorLogService.getApiErrorLogPage(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(apiErrorLogDO, pageResult.getList().get(0)); + } + + @Test + public void testGetApiErrorLogList() { + // mock 数据 + ApiErrorLogDO apiErrorLogDO = randomPojo(ApiErrorLogDO.class, o -> { + o.setUserId(2233L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setApplicationName("yunxi-test"); + o.setRequestUrl("foo"); + o.setExceptionTime(buildTime(2021, 3, 13)); + o.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); + }); + apiErrorLogMapper.insert(apiErrorLogDO); + // 测试 userId 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, o -> o.setUserId(3344L))); + // 测试 userType 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 applicationName 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setApplicationName("test"))); + // 测试 requestUrl 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setRequestUrl("bar"))); + // 测试 exceptionTime 不匹配:构造一个早期时间 2021-02-06 00:00:00 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setExceptionTime(buildTime(2021, 2, 6)))); + // 测试 progressStatus 不匹配 + apiErrorLogMapper.insert(cloneIgnoreId(apiErrorLogDO, logDO -> logDO.setProcessStatus(ApiErrorLogProcessStatusEnum.DONE.getStatus()))); + // 准备参数 + ApiErrorLogExportReqVO reqVO = new ApiErrorLogExportReqVO(); + reqVO.setUserId(2233L); + reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); + reqVO.setApplicationName("yunxi-test"); + reqVO.setRequestUrl("foo"); + reqVO.setExceptionTime(buildBetweenTime(2021, 3, 1, 2021, 3, 31)); + reqVO.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); + + // 调用 + List list = apiErrorLogService.getApiErrorLogList(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, list.size()); + assertPojoEquals(apiErrorLogDO, list.get(0)); + } + + @Test + public void testCreateApiErrorLog() { + // 准备参数 + ApiErrorLogCreateReqDTO createDTO = randomPojo(ApiErrorLogCreateReqDTO.class); + + // 调用 + apiErrorLogService.createApiErrorLog(createDTO); + // 断言 + ApiErrorLogDO apiErrorLogDO = apiErrorLogMapper.selectOne(null); + assertPojoEquals(createDTO, apiErrorLogDO); + assertEquals(ApiErrorLogProcessStatusEnum.INIT.getStatus(), apiErrorLogDO.getProcessStatus()); + } + + @Test + public void testUpdateApiErrorLogProcess_success() { + // 准备参数 + ApiErrorLogDO apiErrorLogDO = randomPojo(ApiErrorLogDO.class, + o -> o.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus())); + apiErrorLogMapper.insert(apiErrorLogDO); + // 准备参数 + Long id = apiErrorLogDO.getId(); + Integer processStatus = randomEle(ApiErrorLogProcessStatusEnum.values()).getStatus(); + Long processUserId = randomLongId(); + + // 调用 + apiErrorLogService.updateApiErrorLogProcess(id, processStatus, processUserId); + // 断言 + ApiErrorLogDO dbApiErrorLogDO = apiErrorLogMapper.selectById(apiErrorLogDO.getId()); + assertEquals(processStatus, dbApiErrorLogDO.getProcessStatus()); + assertEquals(processUserId, dbApiErrorLogDO.getProcessUserId()); + assertNotNull(dbApiErrorLogDO.getProcessTime()); + } + + @Test + public void testUpdateApiErrorLogProcess_processed() { + // 准备参数 + ApiErrorLogDO apiErrorLogDO = randomPojo(ApiErrorLogDO.class, + o -> o.setProcessStatus(ApiErrorLogProcessStatusEnum.DONE.getStatus())); + apiErrorLogMapper.insert(apiErrorLogDO); + // 准备参数 + Long id = apiErrorLogDO.getId(); + Integer processStatus = randomEle(ApiErrorLogProcessStatusEnum.values()).getStatus(); + Long processUserId = randomLongId(); + + // 调用,并断言异常 + assertServiceException(() -> + apiErrorLogService.updateApiErrorLogProcess(id, processStatus, processUserId), + API_ERROR_LOG_PROCESSED); + } + + @Test + public void testUpdateApiErrorLogProcess_notFound() { + // 准备参数 + Long id = randomLongId(); + Integer processStatus = randomEle(ApiErrorLogProcessStatusEnum.values()).getStatus(); + Long processUserId = randomLongId(); + + // 调用,并断言异常 + assertServiceException(() -> + apiErrorLogService.updateApiErrorLogProcess(id, processStatus, processUserId), + API_ERROR_LOG_NOT_FOUND); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/test/TestDemoServiceImplTest.java b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/test/TestDemoServiceImplTest.java new file mode 100644 index 0000000..02d39e1 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/java/com/yunxi/scm/module/infra/service/test/TestDemoServiceImplTest.java @@ -0,0 +1,186 @@ +package com.yunxi.scm.module.infra.service.test; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoCreateReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoExportReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoPageReqVO; +import com.yunxi.scm.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO; +import com.yunxi.scm.module.infra.dal.dataobject.test.TestDemoDO; +import com.yunxi.scm.module.infra.dal.mysql.test.TestDemoMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.TEST_DEMO_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link TestDemoServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(TestDemoServiceImpl.class) +public class TestDemoServiceImplTest extends BaseDbUnitTest { + + @Resource + private TestDemoServiceImpl testDemoService; + + @Resource + private TestDemoMapper testDemoMapper; + + @Test + public void testCreateTestDemo_success() { + // 准备参数 + TestDemoCreateReqVO reqVO = randomPojo(TestDemoCreateReqVO.class); + + // 调用 + Long testDemoId = testDemoService.createTestDemo(reqVO); + // 断言 + assertNotNull(testDemoId); + // 校验记录的属性是否正确 + TestDemoDO testDemo = testDemoMapper.selectById(testDemoId); + assertPojoEquals(reqVO, testDemo); + } + + @Test + public void testUpdateTestDemo_success() { + // mock 数据 + TestDemoDO dbTestDemo = randomPojo(TestDemoDO.class); + testDemoMapper.insert(dbTestDemo);// @Sql: 先插入出一条存在的数据 + // 准备参数 + TestDemoUpdateReqVO reqVO = randomPojo(TestDemoUpdateReqVO.class, o -> { + o.setId(dbTestDemo.getId()); // 设置更新的 ID + }); + + // 调用 + testDemoService.updateTestDemo(reqVO); + // 校验是否更新正确 + TestDemoDO testDemo = testDemoMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, testDemo); + } + + @Test + public void testUpdateTestDemo_notExists() { + // 准备参数 + TestDemoUpdateReqVO reqVO = randomPojo(TestDemoUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> testDemoService.updateTestDemo(reqVO), TEST_DEMO_NOT_EXISTS); + } + + @Test + public void testDeleteTestDemo_success() { + // mock 数据 + TestDemoDO dbTestDemo = randomPojo(TestDemoDO.class); + testDemoMapper.insert(dbTestDemo);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTestDemo.getId(); + + // 调用 + testDemoService.deleteTestDemo(id); + // 校验数据不存在了 + assertNull(testDemoMapper.selectById(id)); + } + + @Test + public void testDeleteTestDemo_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> testDemoService.deleteTestDemo(id), TEST_DEMO_NOT_EXISTS); + } + + @Test + public void testGetTestDemoPage() { + // mock 数据 + TestDemoDO dbTestDemo = randomPojo(TestDemoDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setType(1); + o.setCategory(2); + o.setRemark("哈哈哈"); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + testDemoMapper.insert(dbTestDemo); + // 测试 name 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setName("不匹配"))); + // 测试 status 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 type 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setType(2))); + // 测试 category 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setCategory(1))); + // 测试 remark 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setRemark("呵呵呵"))); + // 测试 createTime 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + TestDemoPageReqVO reqVO = new TestDemoPageReqVO(); + reqVO.setName("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setType(1); + reqVO.setCategory(2); + reqVO.setRemark("哈哈哈"); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2021, 11, 10),buildTime(2021, 11, 12)})); + + // 调用 + PageResult pageResult = testDemoService.getTestDemoPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTestDemo, pageResult.getList().get(0)); + } + + @Test + public void testGetTestDemoList() { + // mock 数据 + TestDemoDO dbTestDemo = randomPojo(TestDemoDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setType(1); + o.setCategory(2); + o.setRemark("哈哈哈"); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + testDemoMapper.insert(dbTestDemo); + // 测试 name 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setName("不匹配"))); + // 测试 status 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 type 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setType(2))); + // 测试 category 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setCategory(1))); + // 测试 remark 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setRemark("呵呵呵"))); + // 测试 createTime 不匹配 + testDemoMapper.insert(cloneIgnoreId(dbTestDemo, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + TestDemoExportReqVO reqVO = new TestDemoExportReqVO(); + reqVO.setName("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setType(1); + reqVO.setCategory(2); + reqVO.setRemark("哈哈哈"); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2021, 11, 10),buildTime(2021, 11, 12)})); + + // 调用 + List list = testDemoService.getTestDemoList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbTestDemo, list.get(0)); + } + +} diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..b994814 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,50 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis-plus: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + type-aliases-package: ${yunxi.info.base-package}.module.*.dal.dataobject + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/logback.xml b/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/sql/clean.sql b/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..3dc20f7 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,10 @@ +DELETE FROM "infra_config"; +DELETE FROM "infra_file_config"; +DELETE FROM "infra_file"; +DELETE FROM "infra_job"; +DELETE FROM "infra_job_log"; +DELETE FROM "infra_api_access_log"; +DELETE FROM "infra_api_error_log"; +DELETE FROM "infra_file_config"; +DELETE FROM "infra_test_demo"; +DELETE FROM "infra_data_source_config"; diff --git a/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..e076ca8 --- /dev/null +++ b/yunxi-module-infra/yunxi-module-infra-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,172 @@ + +CREATE TABLE IF NOT EXISTS "infra_config" ( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', + "category" varchar(50) NOT NULL, + "type" tinyint NOT NULL, + "name" varchar(100) NOT NULL DEFAULT '' COMMENT '名字', + "config_key" varchar(100) NOT NULL DEFAULT '', + "value" varchar(500) NOT NULL DEFAULT '', + "visible" bit NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '参数配置表'; + +CREATE TABLE IF NOT EXISTS "infra_file_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "storage" tinyint NOT NULL, + "remark" varchar(255), + "master" bit(1) NOT NULL, + "config" varchar(4096) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '文件配置表'; + +CREATE TABLE IF NOT EXISTS "infra_file" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "config_id" bigint NOT NULL, + "name" varchar(256), + "path" varchar(512), + "url" varchar(1024), + "type" varchar(63) DEFAULT NULL, + "size" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '文件表'; + +CREATE TABLE IF NOT EXISTS "infra_job" ( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '任务编号', + "name" varchar(32) NOT NULL COMMENT '任务名称', + "status" tinyint(4) NOT NULL COMMENT '任务状态', + "handler_name" varchar(64) NOT NULL COMMENT '处理器的名字', + "handler_param" varchar(255) DEFAULT NULL COMMENT '处理器的参数', + "cron_expression" varchar(32) NOT NULL COMMENT 'CRON 表达式', + "retry_count" int(11) NOT NULL DEFAULT '0' COMMENT '重试次数', + "retry_interval" int(11) NOT NULL DEFAULT '0' COMMENT '重试间隔', + "monitor_timeout" int(11) NOT NULL DEFAULT '0' COMMENT '监控超时时间', + "creator" varchar(64) DEFAULT '' COMMENT '创建者', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + "updater" varchar(64) DEFAULT '' COMMENT '更新者', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + "deleted" bit NOT NULL DEFAULT FALSE COMMENT '是否删除', + PRIMARY KEY ("id") +) COMMENT='定时任务表'; + +CREATE TABLE IF NOT EXISTS "infra_job_log" ( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '日志编号', + "job_id" bigint(20) NOT NULL COMMENT '任务编号', + "handler_name" varchar(64) NOT NULL COMMENT '处理器的名字', + "handler_param" varchar(255) DEFAULT NULL COMMENT '处理器的参数', + "execute_index" tinyint(4) NOT NULL DEFAULT '1' COMMENT '第几次执行', + "begin_time" datetime NOT NULL COMMENT '开始执行时间', + "end_time" datetime DEFAULT NULL COMMENT '结束执行时间', + "duration" int(11) DEFAULT NULL COMMENT '执行时长', + "status" tinyint(4) NOT NULL COMMENT '任务状态', + "result" varchar(4000) DEFAULT '' COMMENT '结果数据', + "creator" varchar(64) DEFAULT '' COMMENT '创建者', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + "updater" varchar(64) DEFAULT '' COMMENT '更新者', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + "deleted" bit(1) NOT NULL DEFAULT FALSE COMMENT '是否删除', + PRIMARY KEY ("id") +)COMMENT='定时任务日志表'; + +CREATE TABLE IF NOT EXISTS "infra_api_access_log" ( + "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, + "trace_id" varchar(64) not null default '', + "user_id" bigint not null default '0', + "user_type" tinyint not null default '0', + "application_name" varchar(50) not null, + "request_method" varchar(16) not null default '', + "request_url" varchar(255) not null default '', + "request_params" varchar(8000) not null default '', + "user_ip" varchar(50) not null, + "user_agent" varchar(512) not null, + "begin_time" timestamp not null, + "end_time" timestamp not null, + "duration" integer not null, + "result_code" integer not null default '0', + "result_msg" varchar(512) default '', + "creator" varchar(64) default '', + "create_time" timestamp not null default current_timestamp, + "updater" varchar(64) default '', + "update_time" timestamp not null default current_timestamp, + "deleted" bit not null default false, + "tenant_id" bigint not null default '0', + primary key ("id") + ) COMMENT 'API 访问日志表'; + +CREATE TABLE IF NOT EXISTS "infra_api_error_log" ( + "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, + "trace_id" varchar(64) not null, + "user_id" bigint not null default '0', + "user_type" tinyint not null default '0', + "application_name" varchar(50) not null, + "request_method" varchar(16) not null, + "request_url" varchar(255) not null, + "request_params" varchar(8000) not null, + "user_ip" varchar(50) not null, + "user_agent" varchar(512) not null, + "exception_time" timestamp not null, + "exception_name" varchar(128) not null default '', + "exception_message" clob not null, + "exception_root_cause_message" clob not null, + "exception_stack_trace" clob not null, + "exception_class_name" varchar(512) not null, + "exception_file_name" varchar(512) not null, + "exception_method_name" varchar(512) not null, + "exception_line_number" integer not null, + "process_status" tinyint not null, + "process_time" timestamp default null, + "process_user_id" bigint default '0', + "creator" varchar(64) default '', + "create_time" timestamp not null default current_timestamp, + "updater" varchar(64) default '', + "update_time" timestamp not null default current_timestamp, + "deleted" bit not null default false, + "tenant_id" bigint not null default '0', + primary key ("id") +) COMMENT '系统异常日志'; + +CREATE TABLE IF NOT EXISTS "infra_test_demo" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL, + "status" tinyint NOT NULL, + "type" tinyint NOT NULL, + "category" tinyint NOT NULL, + "remark" varchar(500), + "creator" varchar(64) DEFAULT '''', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '''', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '字典类型表'; + +CREATE TABLE IF NOT EXISTS "infra_data_source_config" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL, + "url" varchar(1024) NOT NULL, + "username" varchar(255) NOT NULL, + "password" varchar(255) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '数据源配置表'; diff --git a/yunxi-module-mall/pom.xml b/yunxi-module-mall/pom.xml new file mode 100644 index 0000000..27af511 --- /dev/null +++ b/yunxi-module-mall/pom.xml @@ -0,0 +1,29 @@ + + + + yunxi + com.yunxi.scm + ${revision} + + 4.0.0 + + yunxi-module-mall + pom + + ${project.artifactId} + + + 商城大模块,由 product 商品、promotion 营销、trade 交易等组成 + + + yunxi-module-promotion-api + yunxi-module-promotion-biz + yunxi-module-product-api + yunxi-module-product-biz + yunxi-module-trade-api + yunxi-module-trade-biz + + + diff --git a/yunxi-module-mall/yunxi-module-product-api/pom.xml b/yunxi-module-mall/yunxi-module-product-api/pom.xml new file mode 100644 index 0000000..88a6da4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + com.yunxi.scm + yunxi-module-mall + ${revision} + + + yunxi-module-product-api + jar + + ${project.artifactId} + + product 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/comment/ProductCommentApi.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/comment/ProductCommentApi.java new file mode 100644 index 0000000..d396872 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/comment/ProductCommentApi.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.product.api.comment; + +import com.yunxi.scm.module.product.api.comment.dto.ProductCommentCreateReqDTO; + +/** + * 产品评论 API 接口 + * + * @author HUIHUI + */ +public interface ProductCommentApi { + + /** + * 创建评论 + * + * @param createReqDTO 评论参数 + * @return 返回评论创建后的 id + */ + Long createComment(ProductCommentCreateReqDTO createReqDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/comment/dto/ProductCommentCreateReqDTO.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/comment/dto/ProductCommentCreateReqDTO.java new file mode 100644 index 0000000..a183d16 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/comment/dto/ProductCommentCreateReqDTO.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.product.api.comment.dto; + +import lombok.Data; + +import java.util.List; + +/** + * 评论创建请求 DTO + * + * @author HUIHUI + */ +@Data +public class ProductCommentCreateReqDTO { + + /** + * 商品 SKU 编号 + */ + private Long skuId; + /** + * 订单编号 + */ + private Long orderId; + /** + * 交易订单项编号 + */ + private Long orderItemId; + + /** + * 评分星级 1-5 分 + */ + private Integer scores; + /** + * 描述星级 1-5 分 + */ + private Integer descriptionScores; + /** + * 服务星级 1-5 分 + */ + private Integer benefitScores; + /** + * 评论内容 + */ + private String content; + /** + * 评论图片地址数组,以逗号分隔最多上传 9 张 + */ + private List picUrls; + + /** + * 是否匿名 + */ + private Boolean anonymous; + /** + * 评价人 + */ + private Long userId; + + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/package-info.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/package-info.java new file mode 100644 index 0000000..08f2efc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.product.api; \ No newline at end of file diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/property/ProductPropertyValueApi.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/property/ProductPropertyValueApi.java new file mode 100644 index 0000000..5c087ff --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/property/ProductPropertyValueApi.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.product.api.property; + +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; + +import java.util.Collection; +import java.util.List; + +/** + * 商品属性值 API 接口 + * + * @author 芋道源码 + */ +public interface ProductPropertyValueApi { + + /** + * 根据编号数组,获得属性值列表 + * + * @param ids 编号数组 + * @return 属性值明细列表 + */ + List getPropertyValueDetailList(Collection ids); + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/property/dto/ProductPropertyValueDetailRespDTO.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/property/dto/ProductPropertyValueDetailRespDTO.java new file mode 100644 index 0000000..0709fe1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/property/dto/ProductPropertyValueDetailRespDTO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.product.api.property.dto; + +import lombok.Data; + +/** + * 商品属性项的明细 Response DTO + * + * @author 芋道源码 + */ +@Data +public class ProductPropertyValueDetailRespDTO { + + /** + * 属性的编号 + */ + private Long propertyId; + + /** + * 属性的名称 + */ + private String propertyName; + + /** + * 属性值的编号 + */ + private Long valueId; + + /** + * 属性值的名称 + */ + private String valueName; + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/ProductSkuApi.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/ProductSkuApi.java new file mode 100644 index 0000000..c06d95a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/ProductSkuApi.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.product.api.sku; + +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; + +import java.util.Collection; +import java.util.List; + +/** + * 商品 SKU API 接口 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface ProductSkuApi { + + /** + * 查询 SKU 信息 + * + * @param id SKU 编号 + * @return SKU 信息 + */ + ProductSkuRespDTO getSku(Long id); + + /** + * 批量查询 SKU 数组 + * + * @param ids SKU 编号列表 + * @return SKU 数组 + */ + List getSkuList(Collection ids); + + /** + * 批量查询 SKU 数组 + * + * @param spuIds SPU 编号列表 + * @return SKU 数组 + */ + List getSkuListBySpuId(Collection spuIds); + + /** + * 更新 SKU 库存 + * + * @param updateStockReqDTO 更新请求 + */ + void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/dto/ProductSkuRespDTO.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/dto/ProductSkuRespDTO.java new file mode 100644 index 0000000..0d219fe --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/dto/ProductSkuRespDTO.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.product.api.sku.dto; + +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import lombok.Data; + +import java.util.List; + +/** + * 商品 SKU 信息 Response DTO + * + * @author LeeYan9 + * @since 2022-08-26 + */ +@Data +public class ProductSkuRespDTO { + + /** + * 商品 SKU 编号,自增 + */ + private Long id; + /** + * SPU 编号 + */ + private Long spuId; + + /** + * 属性数组 + */ + private List properties; + /** + * 销售价格,单位:分 + */ + private Integer price; + /** + * 市场价,单位:分 + */ + private Integer marketPrice; + /** + * 成本价,单位:分 + */ + private Integer costPrice; + /** + * SKU 的条形码 + */ + private String barCode; + /** + * 图片地址 + */ + private String picUrl; + /** + * 库存 + */ + private Integer stock; + /** + * 商品重量,单位:kg 千克 + */ + private Double weight; + /** + * 商品体积,单位:m^3 平米 + */ + private Double volume; + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/dto/ProductSkuUpdateStockReqDTO.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/dto/ProductSkuUpdateStockReqDTO.java new file mode 100644 index 0000000..64128a5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/sku/dto/ProductSkuUpdateStockReqDTO.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.product.api.sku.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 商品 SKU 更新库存 Request DTO + * + * @author LeeYan9 + * @since 2022-08-26 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSkuUpdateStockReqDTO { + + /** + * 商品 SKU + */ + @NotNull(message = "商品 SKU 不能为空") + private List items; + + @Data + public static class Item { + + /** + * 商品 SKU 编号 + */ + @NotNull(message = "商品 SKU 编号不能为空") + private Long id; + + /** + * 库存变化数量 + * + * 正数:增加库存 + * 负数:扣减库存 + */ + @NotNull(message = "库存变化数量不能为空") + private Integer incrCount; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/spu/ProductSpuApi.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/spu/ProductSpuApi.java new file mode 100644 index 0000000..e46d414 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/spu/ProductSpuApi.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.product.api.spu; + +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; + +import java.util.Collection; +import java.util.List; + +/** + * 商品 SPU API 接口 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface ProductSpuApi { + + /** + * 批量查询 SPU 数组 + * + * @param ids SPU 编号列表 + * @return SPU 数组 + */ + List getSpuList(Collection ids); + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/spu/dto/ProductSpuRespDTO.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/spu/dto/ProductSpuRespDTO.java new file mode 100644 index 0000000..a586ff1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/api/spu/dto/ProductSpuRespDTO.java @@ -0,0 +1,130 @@ +package com.yunxi.scm.module.product.api.spu.dto; + +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import lombok.Data; + +import java.util.List; + +// TODO @LeeYan9: ProductSpuRespDTO +/** + * 商品 SPU 信息 Response DTO + * + * @author LeeYan9 + * @since 2022-08-26 + */ +@Data +public class ProductSpuRespDTO { + + /** + * 商品 SPU 编号,自增 + */ + private Long id; + + // ========== 基本信息 ========= + + /** + * 商品名称 + */ + private String name; + /** + * 关键字 + */ + private String keyword; + /** + * 商品简介 + */ + private String introduction; + /** + * 商品详情 + */ + private String description; + // TODO @芋艿:是不是要删除 + /** + * 商品条码(一维码) + */ + private String barCode; + + /** + * 商品分类编号 + */ + private Long categoryId; + /** + * 商品品牌编号 + */ + private Long brandId; + /** + * 商品封面图 + */ + private String picUrl; + /** + * 商品轮播图 + */ + private List sliderPicUrls; + /** + * 商品视频 + */ + private String videoUrl; + + /** + * 排序字段 + */ + private Integer sort; + /** + * 商品状态 + *

+ * 枚举 {@link ProductSpuStatusEnum} + */ + private Integer status; + + // ========== SKU 相关字段 ========= + + /** + * 规格类型 + * + * false - 单规格 + * true - 多规格 + */ + private Boolean specType; + /** + * 商品价格,单位使用:分 + */ + private Integer price; + /** + * 市场价,单位使用:分 + */ + private Integer marketPrice; + /** + * 成本价,单位使用:分 + */ + private Integer costPrice; + /** + * 库存 + */ + private Integer stock; + + // ========== 物流相关字段 ========= + + /** + * 物流配置模板编号 + * + * 对应 TradeDeliveryExpressTemplateDO 的 id 编号 + */ + private Long deliveryTemplateId; + + // ========== 统计相关字段 ========= + + /** + * 商品销量 + */ + private Integer salesCount; + /** + * 虚拟销量 + */ + private Integer virtualSalesCount; + /** + * 商品点击量 + */ + private Integer clickCount; + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/DictTypeConstants.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/DictTypeConstants.java new file mode 100644 index 0000000..0641c68 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/DictTypeConstants.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.module.product.enums; + +/** + * product 字典类型的枚举类 + * + * @author HUIHUI + */ +public interface DictTypeConstants { + + String PRODUCT_UNIT = "product_unit"; // 商品单位 + String PRODUCT_SPU_STATUS = "product_spu_status"; // 商品 SPU 状态 + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/ErrorCodeConstants.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..42c468c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/ErrorCodeConstants.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.product.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Product 错误码枚举类 + * + * product 系统,使用 1-008-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 商品分类相关 1008001000 ============ + ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1008001000, "商品分类不存在"); + ErrorCode CATEGORY_PARENT_NOT_EXISTS = new ErrorCode(1008001001, "父分类不存在"); + ErrorCode CATEGORY_PARENT_NOT_FIRST_LEVEL = new ErrorCode(1008001002, "父分类不能是二级分类"); + ErrorCode CATEGORY_EXISTS_CHILDREN = new ErrorCode(1008001003, "存在子分类,无法删除"); + ErrorCode CATEGORY_DISABLED = new ErrorCode(1008001004, "商品分类({})已禁用,无法使用"); + ErrorCode CATEGORY_HAVE_BIND_SPU = new ErrorCode(1008001005, "类别下存在商品,无法删除"); + + // ========== 商品品牌相关编号 1008002000 ========== + ErrorCode BRAND_NOT_EXISTS = new ErrorCode(1008002000, "品牌不存在"); + ErrorCode BRAND_DISABLED = new ErrorCode(1008002001, "品牌已禁用"); + ErrorCode BRAND_NAME_EXISTS = new ErrorCode(1008002002, "品牌名称已存在"); + + // ========== 商品属性项 1008003000 ========== + ErrorCode PROPERTY_NOT_EXISTS = new ErrorCode(1008003000, "属性项不存在"); + ErrorCode PROPERTY_EXISTS = new ErrorCode(1008003001, "属性项的名称已存在"); + ErrorCode PROPERTY_DELETE_FAIL_VALUE_EXISTS = new ErrorCode(1008003002, "属性项下存在属性值,无法删除"); + + // ========== 商品属性值 1008004000 ========== + ErrorCode PROPERTY_VALUE_NOT_EXISTS = new ErrorCode(1008004000, "属性值不存在"); + ErrorCode PROPERTY_VALUE_EXISTS = new ErrorCode(1008004001, "属性值的名称已存在"); + + // ========== 商品 SPU 1008005000 ========== + ErrorCode SPU_NOT_EXISTS = new ErrorCode(1008005000, "商品 SPU 不存在"); + ErrorCode SPU_SAVE_FAIL_CATEGORY_LEVEL_ERROR = new ErrorCode(1008005001, "商品分类不正确,原因:必须使用第二级的商品分类及以下"); + ErrorCode SPU_NOT_ENABLE = new ErrorCode(1008005002, "商品 SPU 不处于上架状态"); + ErrorCode SPU_NOT_RECYCLE = new ErrorCode(1008005003, "商品 SPU 不处于回收站状态"); + + // ========== 商品 SKU 1008006000 ========== + ErrorCode SKU_NOT_EXISTS = new ErrorCode(1008006000, "商品 SKU 不存在"); + ErrorCode SKU_PROPERTIES_DUPLICATED = new ErrorCode(1008006001, "商品 SKU 的属性组合存在重复"); + ErrorCode SPU_ATTR_NUMBERS_MUST_BE_EQUALS = new ErrorCode(1008006002, "一个 SPU 下的每个 SKU,其属性项必须一致"); + ErrorCode SPU_SKU_NOT_DUPLICATE = new ErrorCode(1008006003, "一个 SPU 下的每个 SKU,必须不重复"); + ErrorCode SKU_STOCK_NOT_ENOUGH = new ErrorCode(1008006004, "商品 SKU 库存不足"); + + // ========== 商品 评价 1008007000 ========== + ErrorCode COMMENT_NOT_EXISTS = new ErrorCode(1008007000, "商品评价不存在"); + ErrorCode COMMENT_ORDER_EXISTS = new ErrorCode(1008007001, "订单的商品评价已存在"); + + // ========== 商品 收藏 1008008000 ========== + ErrorCode FAVORITE_EXISTS = new ErrorCode(1008008000, "该商品已经被收藏"); + ErrorCode FAVORITE_NOT_EXISTS = new ErrorCode(1008008001, "商品收藏不存在"); + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/ProductConstants.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/ProductConstants.java new file mode 100644 index 0000000..863c5cc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/ProductConstants.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.product.enums; + +/** + * Product 常量 + * + * @author HUIHUI + */ +public interface ProductConstants { + + /** + * 警戒库存 TODO 警戒库存暂时为 10,后期需要使用常量或者数据库配置替换 + */ + int ALERT_STOCK = 10; + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/comment/ProductCommentAuditStatusEnum.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/comment/ProductCommentAuditStatusEnum.java new file mode 100644 index 0000000..2d873a5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/comment/ProductCommentAuditStatusEnum.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.product.enums.comment; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 商品评论的审批状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum ProductCommentAuditStatusEnum implements IntArrayValuable { + + NONE(1, "待审核"), + APPROVE(2, "审批通过"), + REJECT(2, "审批不通过"),; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductCommentAuditStatusEnum::getStatus).toArray(); + + /** + * 审批状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/comment/ProductCommentScoresEnum.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/comment/ProductCommentScoresEnum.java new file mode 100644 index 0000000..0c95319 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/comment/ProductCommentScoresEnum.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.product.enums.comment; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 商品评论的星级枚举 + * + * @author wangzhs + */ +@Getter +@AllArgsConstructor +public enum ProductCommentScoresEnum implements IntArrayValuable { + + ONE(1, "1星"), + TWO(2, "2星"), + THREE(3, "3星"), + FOUR(4, "4星"), + FIVE(5, "5星"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductCommentScoresEnum::getScores).toArray(); + + /** + * 星级 + */ + private final Integer scores; + + /** + * 星级名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/group/ProductGroupStyleEnum.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/group/ProductGroupStyleEnum.java new file mode 100644 index 0000000..121ff4a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/group/ProductGroupStyleEnum.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.product.enums.group; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 商品分组的样式枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum ProductGroupStyleEnum implements IntArrayValuable { + + ONE(1, "每列一个"), + TWO(2, "每列两个"), + THREE(2, "每列三个"),; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductGroupStyleEnum::getStyle).toArray(); + + /** + * 列表样式 + */ + private final Integer style; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/spu/ProductSpuStatusEnum.java b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/spu/ProductSpuStatusEnum.java new file mode 100644 index 0000000..cab8ac8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-api/src/main/java/com/yunxi/scm/module/product/enums/spu/ProductSpuStatusEnum.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.product.enums.spu; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 商品 SPU 状态 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum ProductSpuStatusEnum implements IntArrayValuable { + + RECYCLE(-1, "回收站"), + DISABLE(0, "下架"), + ENABLE(1, "上架"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ProductSpuStatusEnum::getStatus).toArray(); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + + /** + * 判断是否处于【上架】状态 + * + * @param status 状态 + * @return 是否处于【上架】状态 + */ + public static boolean isEnable(Integer status) { + return ENABLE.getStatus().equals(status); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/pom.xml b/yunxi-module-mall/yunxi-module-product-biz/pom.xml new file mode 100644 index 0000000..bbd7021 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/pom.xml @@ -0,0 +1,77 @@ + + + + com.yunxi.scm + yunxi-module-mall + ${revision} + + 4.0.0 + yunxi-module-product-biz + jar + + ${project.artifactId} + + product 模块,主要实现商品相关功能 + 例如:品牌、商品分类、spu、sku等功能。 + + + + + com.yunxi.scm + yunxi-module-product-api + ${revision} + + + + com.yunxi.scm + yunxi-module-trade-api + ${revision} + + + com.yunxi.scm + yunxi-module-member-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-dict + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + + + + diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/comment/ProductCommentApiImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/comment/ProductCommentApiImpl.java new file mode 100644 index 0000000..39c335f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/comment/ProductCommentApiImpl.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.product.api.comment; + +import com.yunxi.scm.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import com.yunxi.scm.module.product.service.comment.ProductCommentService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 商品评论 API 实现类 + * + * @author HUIHUI + */ +@Service +@Validated +public class ProductCommentApiImpl implements ProductCommentApi { + + @Resource + private ProductCommentService productCommentService; + + @Override + public Long createComment(ProductCommentCreateReqDTO createReqDTO) { + return productCommentService.createComment(createReqDTO); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/package-info.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/package-info.java new file mode 100644 index 0000000..b09085e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.product.api; diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/property/ProductPropertyValueApiImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/property/ProductPropertyValueApiImpl.java new file mode 100644 index 0000000..b175ad5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/property/ProductPropertyValueApiImpl.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.product.api.property; + +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.product.convert.propertyvalue.ProductPropertyValueConvert; +import com.yunxi.scm.module.product.service.property.ProductPropertyValueService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * 商品属性值 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ProductPropertyValueApiImpl implements ProductPropertyValueApi { + + @Resource + private ProductPropertyValueService productPropertyValueService; + + @Override + public List getPropertyValueDetailList(Collection ids) { + return ProductPropertyValueConvert.INSTANCE.convertList02( + productPropertyValueService.getPropertyValueDetailList(ids)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/sku/ProductSkuApiImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/sku/ProductSkuApiImpl.java new file mode 100644 index 0000000..73b2872 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/sku/ProductSkuApiImpl.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.product.api.sku; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; +import com.yunxi.scm.module.product.convert.sku.ProductSkuConvert; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * 商品 SKU API 实现类 + * + * @author LeeYan9 + * @since 2022-09-06 + */ +@Service +@Validated +public class ProductSkuApiImpl implements ProductSkuApi { + + @Resource + private ProductSkuService productSkuService; + + @Override + public ProductSkuRespDTO getSku(Long id) { + ProductSkuDO sku = productSkuService.getSku(id); + return ProductSkuConvert.INSTANCE.convert02(sku); + } + + @Override + public List getSkuList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + List skus = productSkuService.getSkuList(ids); + return ProductSkuConvert.INSTANCE.convertList04(skus); + } + + @Override + public List getSkuListBySpuId(Collection spuIds) { + if (CollUtil.isEmpty(spuIds)) { + return Collections.emptyList(); + } + List skus = productSkuService.getSkuListBySpuId(spuIds); + return ProductSkuConvert.INSTANCE.convertList04(skus); + } + + @Override + public void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO) { + productSkuService.updateSkuStock(updateStockReqDTO); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/spu/ProductSpuApiImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/spu/ProductSpuApiImpl.java new file mode 100644 index 0000000..0351809 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/api/spu/ProductSpuApiImpl.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.product.api.spu; + +import cn.hutool.core.collection.CollectionUtil; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.product.convert.spu.ProductSpuConvert; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.dal.mysql.spu.ProductSpuMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * 商品 SPU API 接口实现类 + * + * @author LeeYan9 + * @since 2022-09-06 + */ +@Service +@Validated +public class ProductSpuApiImpl implements ProductSpuApi { + + @Resource + private ProductSpuMapper productSpuMapper; + + @Override + public List getSpuList(Collection spuIds) { + if (CollectionUtil.isEmpty(spuIds)) { + return Collections.emptyList(); + } + List productSpuDOList = productSpuMapper.selectBatchIds(spuIds); + return ProductSpuConvert.INSTANCE.convertList2(productSpuDOList); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/ProductBrandController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/ProductBrandController.java new file mode 100644 index 0000000..6cdcb96 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/ProductBrandController.java @@ -0,0 +1,92 @@ +package com.yunxi.scm.module.product.controller.admin.brand; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.brand.vo.*; +import com.yunxi.scm.module.product.convert.brand.ProductBrandConvert; +import com.yunxi.scm.module.product.dal.dataobject.brand.ProductBrandDO; +import com.yunxi.scm.module.product.service.brand.ProductBrandService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 商品品牌") +@RestController +@RequestMapping("/product/brand") +@Validated +public class ProductBrandController { + + @Resource + private ProductBrandService brandService; + + @PostMapping("/create") + @Operation(summary = "创建品牌") + @PreAuthorize("@ss.hasPermission('product:brand:create')") + public CommonResult createBrand(@Valid @RequestBody ProductBrandCreateReqVO createReqVO) { + return success(brandService.createBrand(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新品牌") + @PreAuthorize("@ss.hasPermission('product:brand:update')") + public CommonResult updateBrand(@Valid @RequestBody ProductBrandUpdateReqVO updateReqVO) { + brandService.updateBrand(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除品牌") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('product:brand:delete')") + public CommonResult deleteBrand(@RequestParam("id") Long id) { + brandService.deleteBrand(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得品牌") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('product:brand:query')") + public CommonResult getBrand(@RequestParam("id") Long id) { + ProductBrandDO brand = brandService.getBrand(id); + return success(ProductBrandConvert.INSTANCE.convert(brand)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取品牌精简信息列表", description = "主要用于前端的下拉选项") + public CommonResult> getSimpleBrandList() { + // 获取品牌列表,只要开启状态的 + List list = brandService.getBrandListByStatus(CommonStatusEnum.ENABLE.getStatus()); + // 排序后,返回给前端 + return success(ProductBrandConvert.INSTANCE.convertList1(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得品牌分页") + @PreAuthorize("@ss.hasPermission('product:brand:query')") + public CommonResult> getBrandPage(@Valid ProductBrandPageReqVO pageVO) { + PageResult pageResult = brandService.getBrandPage(pageVO); + return success(ProductBrandConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list") + @Operation(summary = "获得品牌列表") + @PreAuthorize("@ss.hasPermission('product:brand:query')") + public CommonResult> getBrandList(@Valid ProductBrandListReqVO listVO) { + List list = brandService.getBrandList(listVO); + list.sort(Comparator.comparing(ProductBrandDO::getSort)); + return success(ProductBrandConvert.INSTANCE.convertList(list)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandBaseVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandBaseVO.java new file mode 100644 index 0000000..0de836d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandBaseVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.product.controller.admin.brand.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 商品品牌 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ProductBrandBaseVO { + + @Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "苹果") + @NotNull(message = "品牌名称不能为空") + private String name; + + @Schema(description = "品牌图片", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "品牌图片不能为空") + private String picUrl; + + @Schema(description = "品牌排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "品牌排序不能为空") + private Integer sort; + + @Schema(description = "品牌描述", example = "描述") + private String description; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandCreateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandCreateReqVO.java new file mode 100644 index 0000000..ff7ff91 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.product.controller.admin.brand.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 商品品牌创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductBrandCreateReqVO extends ProductBrandBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandListReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandListReqVO.java new file mode 100644 index 0000000..b5dd525 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandListReqVO.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.module.product.controller.admin.brand.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 商品品牌分页 Request VO") +@Data +public class ProductBrandListReqVO { + + @Schema(description = "品牌名称", example = "苹果") + private String name; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandPageReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandPageReqVO.java new file mode 100644 index 0000000..42eaf86 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandPageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.product.controller.admin.brand.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 商品品牌分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductBrandPageReqVO extends PageParam { + + @Schema(description = "品牌名称", example = "苹果") + private String name; + + @Schema(description = "状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandRespVO.java new file mode 100644 index 0000000..28a2d24 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.product.controller.admin.brand.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 品牌 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductBrandRespVO extends ProductBrandBaseVO { + + @Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandSimpleRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandSimpleRespVO.java new file mode 100644 index 0000000..604be23 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandSimpleRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.product.controller.admin.brand.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 品牌精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductBrandSimpleRespVO { + + @Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "品牌名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "苹果") + private String name; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandUpdateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandUpdateReqVO.java new file mode 100644 index 0000000..f513b6f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/brand/vo/ProductBrandUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.product.controller.admin.brand.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 商品品牌更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductBrandUpdateReqVO extends ProductBrandBaseVO { + + @Schema(description = "品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "品牌编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/ProductCategoryController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/ProductCategoryController.java new file mode 100644 index 0000000..c28ba36 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/ProductCategoryController.java @@ -0,0 +1,76 @@ +package com.yunxi.scm.module.product.controller.admin.category; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryListReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryRespVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; +import com.yunxi.scm.module.product.convert.category.ProductCategoryConvert; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; +import com.yunxi.scm.module.product.service.category.ProductCategoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 商品分类") +@RestController +@RequestMapping("/product/category") +@Validated +public class ProductCategoryController { + + @Resource + private ProductCategoryService categoryService; + + @PostMapping("/create") + @Operation(summary = "创建商品分类") + @PreAuthorize("@ss.hasPermission('product:category:create')") + public CommonResult createCategory(@Valid @RequestBody ProductCategoryCreateReqVO createReqVO) { + return success(categoryService.createCategory(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新商品分类") + @PreAuthorize("@ss.hasPermission('product:category:update')") + public CommonResult updateCategory(@Valid @RequestBody ProductCategoryUpdateReqVO updateReqVO) { + categoryService.updateCategory(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除商品分类") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('product:category:delete')") + public CommonResult deleteCategory(@RequestParam("id") Long id) { + categoryService.deleteCategory(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得商品分类") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('product:category:query')") + public CommonResult getCategory(@RequestParam("id") Long id) { + ProductCategoryDO category = categoryService.getCategory(id); + return success(ProductCategoryConvert.INSTANCE.convert(category)); + } + + @GetMapping("/list") + @Operation(summary = "获得商品分类列表") + @PreAuthorize("@ss.hasPermission('product:category:query')") + public CommonResult> getCategoryList(@Valid ProductCategoryListReqVO treeListReqVO) { + List list = categoryService.getEnableCategoryList(treeListReqVO); + list.sort(Comparator.comparing(ProductCategoryDO::getSort)); + return success(ProductCategoryConvert.INSTANCE.convertList(list)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryBaseVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryBaseVO.java new file mode 100644 index 0000000..6978a12 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryBaseVO.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.product.controller.admin.category.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +/** +* 商品分类 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ProductCategoryBaseVO { + + @Schema(description = "父分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "父分类编号不能为空") + private Long parentId; + + @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "办公文具") + @NotBlank(message = "分类名称不能为空") + private String name; + + @Schema(description = "移动端分类图", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "移动端分类图不能为空") + private String picUrl; + + @Schema(description = "PC 端分类图") + private String bigPicUrl; + + @Schema(description = "分类排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sort; + + @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "开启状态不能为空") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryCreateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryCreateReqVO.java new file mode 100644 index 0000000..dd0be29 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryCreateReqVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.product.controller.admin.category.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; + +@Schema(description = "管理后台 - 商品分类创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductCategoryCreateReqVO extends ProductCategoryBaseVO { + + @Schema(description = "分类描述", example = "描述") + private String description; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryListReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryListReqVO.java new file mode 100644 index 0000000..700f485 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryListReqVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.product.controller.admin.category.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 商品分类列表查询 Request VO") +@Data +public class ProductCategoryListReqVO { + + @Schema(description = "分类名称", example = "办公文具") + private String name; + + @Schema(description = "开启状态", example = "0") + private Integer status; + + @Schema(description = "父分类编号", example = "1") + private Long parentId; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryRespVO.java new file mode 100644 index 0000000..a1b3721 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.product.controller.admin.category.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 商品分类 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductCategoryRespVO extends ProductCategoryBaseVO { + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryUpdateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryUpdateReqVO.java new file mode 100644 index 0000000..adf59bb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/category/vo/ProductCategoryUpdateReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.product.controller.admin.category.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 商品分类更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductCategoryUpdateReqVO extends ProductCategoryBaseVO { + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "分类编号不能为空") + private Long id; + + @Schema(description = "分类描述", example = "描述") + private String description; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/ProductCommentController.http b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/ProductCommentController.http new file mode 100644 index 0000000..e69de29 diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/ProductCommentController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/ProductCommentController.java new file mode 100644 index 0000000..052f93a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/ProductCommentController.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.module.product.controller.admin.comment; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.comment.vo.*; +import com.yunxi.scm.module.product.convert.comment.ProductCommentConvert; +import com.yunxi.scm.module.product.dal.dataobject.comment.ProductCommentDO; +import com.yunxi.scm.module.product.service.comment.ProductCommentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 商品评价") +@RestController +@RequestMapping("/product/comment") +@Validated +public class ProductCommentController { + + @Resource + private ProductCommentService productCommentService; + + @GetMapping("/page") + @Operation(summary = "获得商品评价分页") + @PreAuthorize("@ss.hasPermission('product:comment:query')") + public CommonResult> getCommentPage(@Valid ProductCommentPageReqVO pageVO) { + PageResult pageResult = productCommentService.getCommentPage(pageVO); + return success(ProductCommentConvert.INSTANCE.convertPage(pageResult)); + } + + @PutMapping("/update-visible") + @Operation(summary = "显示 / 隐藏评论") + @PreAuthorize("@ss.hasPermission('product:comment:update')") + public CommonResult updateCommentVisible(@Valid @RequestBody ProductCommentUpdateVisibleReqVO updateReqVO) { + productCommentService.updateCommentVisible(updateReqVO); + return success(true); + } + + @PutMapping("/reply") + @Operation(summary = "商家回复") + @PreAuthorize("@ss.hasPermission('product:comment:update')") + public CommonResult commentReply(@Valid @RequestBody ProductCommentReplyReqVO replyVO) { + productCommentService.replyComment(replyVO, getLoginUserId()); + return success(true); + } + + @PutMapping("/create") + @Operation(summary = "添加自评") + @PreAuthorize("@ss.hasPermission('product:comment:update')") + public CommonResult createComment(@Valid @RequestBody ProductCommentCreateReqVO createReqVO) { + productCommentService.createComment(createReqVO); + return success(true); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentBaseVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentBaseVO.java new file mode 100644 index 0000000..95fda3b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentBaseVO.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.product.controller.admin.comment.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; + +@Data +public class ProductCommentBaseVO { + + @Schema(description = "评价人", requiredMode = Schema.RequiredMode.REQUIRED, example = "16868") + @NotNull(message = "评价人不能为空") + private Long userId; + + @Schema(description = "评价订单项", requiredMode = Schema.RequiredMode.REQUIRED, example = "19292") + @NotNull(message = "评价订单项不能为空") + private Long orderItemId; + + @Schema(description = "评价人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小姑凉") + @NotNull(message = "评价人名称不能为空") + private String userNickname; + + @Schema(description = "评价人头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + @NotNull(message = "评价人头像不能为空") + private String userAvatar; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "描述星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "描述星级不能为空") + private Integer descriptionScores; + + @Schema(description = "服务星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "服务星级分不能为空") + private Integer benefitScores; + + @Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "穿起来非常丝滑凉快") + @NotNull(message = "评论内容不能为空") + private String content; + + @Schema(description = "评论图片地址数组,以逗号分隔最多上传 9 张", requiredMode = Schema.RequiredMode.REQUIRED, example = "[https://www.iocoder.cn/xx.png]") + @Size(max = 9, message = "评论图片地址数组长度不能超过 9 张") + private List picUrls; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentCreateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentCreateReqVO.java new file mode 100644 index 0000000..6d869c6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.product.controller.admin.comment.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 商品评价创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductCommentCreateReqVO extends ProductCommentBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentPageReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentPageReqVO.java new file mode 100644 index 0000000..0c509c6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentPageReqVO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.product.controller.admin.comment.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.product.enums.comment.ProductCommentScoresEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 商品评价分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductCommentPageReqVO extends PageParam { + + @Schema(description = "评价人名称", example = "王二狗") + private String userNickname; + + @Schema(description = "交易订单编号", example = "24428") + private Long orderId; + + @Schema(description = "商品SPU编号", example = "29502") + private Long spuId; + + @Schema(description = "商品SPU名称", example = "感冒药") + private String spuName; + + @Schema(description = "评分星级 1-5 分", example = "5") + @InEnum(ProductCommentScoresEnum.class) + private Integer scores; + + @Schema(description = "商家是否回复", example = "true") + private Boolean replyStatus; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentReplyReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentReplyReqVO.java new file mode 100644 index 0000000..7ee9c81 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentReplyReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.product.controller.admin.comment.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 商品评价的商家回复 Request VO") +@Data +@ToString(callSuper = true) +public class ProductCommentReplyReqVO { + + @Schema(description = "评价编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15721") + @NotNull(message = "评价编号不能为空") + private Long id; + + @Schema(description = "商家回复内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "谢谢亲") + @NotEmpty(message = "商家回复内容不能为空") + private String replyContent; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentRespVO.java new file mode 100644 index 0000000..2b3a011 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentRespVO.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.product.controller.admin.comment.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 商品评价 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductCommentRespVO extends ProductCommentBaseVO { + + @Schema(description = "订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24965") + private Long id; + + @Schema(description = "是否匿名", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean anonymous; + + @Schema(description = "交易订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24428") + private Long orderId; + + @Schema(description = "是否可见:[true:显示 false:隐藏]", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean visible; + + @Schema(description = "商家是否回复:[1:回复 0:未回复]", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean replyStatus; + + @Schema(description = "回复管理员编号", example = "9527") + private Long replyUserId; + + @Schema(description = "商家回复内容", example = "感谢好评哦亲(づ ̄3 ̄)づ╭❤~") + private String replyContent; + + @Schema(description = "商家回复时间", example = "2023-08-08 12:20:55") + private LocalDateTime replyTime; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "评分星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Integer scores; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉丝滑透气小短袖") + @NotNull(message = "商品 SPU 编号不能为空") + private Long spuId; + + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "赵六") + @NotNull(message = "商品 SPU 名称不能为空") + private String spuName; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentUpdateVisibleReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentUpdateVisibleReqVO.java new file mode 100644 index 0000000..96693fd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/comment/vo/ProductCommentUpdateVisibleReqVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.product.controller.admin.comment.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 商品评价可见修改 Request VO") +@Data +@ToString(callSuper = true) +public class ProductCommentUpdateVisibleReqVO { + + @Schema(description = "评价编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15721") + @NotNull(message = "评价编号不能为空") + private Long id; + + @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + @NotNull(message = "是否可见不能为空") + private Boolean visible; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/ProductPropertyController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/ProductPropertyController.java new file mode 100644 index 0000000..2c053cc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/ProductPropertyController.java @@ -0,0 +1,100 @@ +package com.yunxi.scm.module.product.controller.admin.property; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.*; +import com.yunxi.scm.module.product.convert.property.ProductPropertyConvert; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyValueDO; +import com.yunxi.scm.module.product.service.property.ProductPropertyService; +import com.yunxi.scm.module.product.service.property.ProductPropertyValueService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 商品属性项") +@RestController +@RequestMapping("/product/property") +@Validated +public class ProductPropertyController { + + @Resource + private ProductPropertyService productPropertyService; + @Resource + private ProductPropertyValueService productPropertyValueService; + + @PostMapping("/create") + @Operation(summary = "创建属性项") + @PreAuthorize("@ss.hasPermission('product:property:create')") + public CommonResult createProperty(@Valid @RequestBody ProductPropertyCreateReqVO createReqVO) { + return success(productPropertyService.createProperty(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新属性项") + @PreAuthorize("@ss.hasPermission('product:property:update')") + public CommonResult updateProperty(@Valid @RequestBody ProductPropertyUpdateReqVO updateReqVO) { + productPropertyService.updateProperty(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除属性项") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('product:property:delete')") + public CommonResult deleteProperty(@RequestParam("id") Long id) { + productPropertyService.deleteProperty(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得属性项") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('product:property:query')") + public CommonResult getProperty(@RequestParam("id") Long id) { + return success(ProductPropertyConvert.INSTANCE.convert(productPropertyService.getProperty(id))); + } + + @GetMapping("/list") + @Operation(summary = "获得属性项列表") + @PreAuthorize("@ss.hasPermission('product:property:query')") + public CommonResult> getPropertyList(@Valid ProductPropertyListReqVO listReqVO) { + return success(ProductPropertyConvert.INSTANCE.convertList(productPropertyService.getPropertyList(listReqVO))); + } + + @GetMapping("/page") + @Operation(summary = "获得属性项分页") + @PreAuthorize("@ss.hasPermission('product:property:query')") + public CommonResult> getPropertyPage(@Valid ProductPropertyPageReqVO pageVO) { + return success(ProductPropertyConvert.INSTANCE.convertPage(productPropertyService.getPropertyPage(pageVO))); + } + + @PostMapping("/get-value-list") + @Operation(summary = "获得属性项列表") + @PreAuthorize("@ss.hasPermission('product:property:query')") + public CommonResult> getPropertyAndValueList( + @Valid @RequestBody ProductPropertyListReqVO listReqVO) { + // 查询属性项 + List keys = productPropertyService.getPropertyList(listReqVO); + if (CollUtil.isEmpty(keys)) { + return success(Collections.emptyList()); + } + // 查询属性值 + List values = productPropertyValueService.getPropertyValueListByPropertyId( + convertSet(keys, ProductPropertyDO::getId)); + return success(ProductPropertyConvert.INSTANCE.convertList(keys, values)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/ProductPropertyValueController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/ProductPropertyValueController.java new file mode 100644 index 0000000..6ab335b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/ProductPropertyValueController.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.product.controller.admin.property; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.crypto.symmetric.AES; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValuePageReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueRespVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueUpdateReqVO; +import com.yunxi.scm.module.product.convert.propertyvalue.ProductPropertyValueConvert; +import com.yunxi.scm.module.product.service.property.ProductPropertyValueService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import java.util.Arrays; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 商品属性值") +@RestController +@RequestMapping("/product/property/value") +@Validated +public class ProductPropertyValueController { + + @Resource + private ProductPropertyValueService productPropertyValueService; + + @PostMapping("/create") + @Operation(summary = "创建属性值") + @PreAuthorize("@ss.hasPermission('product:property:create')") + public CommonResult createPropertyValue(@Valid @RequestBody ProductPropertyValueCreateReqVO createReqVO) { + return success(productPropertyValueService.createPropertyValue(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新属性值") + @PreAuthorize("@ss.hasPermission('product:property:update')") + public CommonResult updatePropertyValue(@Valid @RequestBody ProductPropertyValueUpdateReqVO updateReqVO) { + productPropertyValueService.updatePropertyValue(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除属性值") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('product:property:delete')") + public CommonResult deletePropertyValue(@RequestParam("id") Long id) { + productPropertyValueService.deletePropertyValue(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得属性值") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('product:property:query')") + public CommonResult getPropertyValue(@RequestParam("id") Long id) { + return success(ProductPropertyValueConvert.INSTANCE.convert(productPropertyValueService.getPropertyValue(id))); + } + + @GetMapping("/page") + @Operation(summary = "获得属性值分页") + @PreAuthorize("@ss.hasPermission('product:property:query')") + public CommonResult> getPropertyValuePage(@Valid ProductPropertyValuePageReqVO pageVO) { + return success(ProductPropertyValueConvert.INSTANCE.convertPage(productPropertyValueService.getPropertyValuePage(pageVO))); + } +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyAndValueRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyAndValueRespVO.java new file mode 100644 index 0000000..25694c5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyAndValueRespVO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 商品属性项 + 属性值 Response VO") +@Data +public class ProductPropertyAndValueRespVO { + + @Schema(description = "属性项的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "属性项的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") + private String name; + + /** + * 属性值的集合 + */ + private List values; + + @Schema(description = "管理后台 - 属性值的简单 Response VO") + @Data + public static class Value { + + @Schema(description = "属性值的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long id; + + @Schema(description = "属性值的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") + private String name; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyBaseVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyBaseVO.java new file mode 100644 index 0000000..da937e3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyBaseVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +/** + * 商品属性项 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class ProductPropertyBaseVO { + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") + @NotBlank(message = "名称不能为空") + private String name; + + @Schema(description = "备注", example = "颜色") + private String remark; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyCreateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyCreateReqVO.java new file mode 100644 index 0000000..af19aae --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyCreateReqVO.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 属性项创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductPropertyCreateReqVO extends ProductPropertyBaseVO { + + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyListReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyListReqVO.java new file mode 100644 index 0000000..d20a470 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyListReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 属性项 List Request VO") +@Data +@ToString(callSuper = true) +public class ProductPropertyListReqVO { + + @Schema(description = "属性名称", example = "颜色") + private String name; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyPageReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyPageReqVO.java new file mode 100644 index 0000000..a17544a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyPageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.property; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 属性项 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductPropertyPageReqVO extends PageParam { + + @Schema(description = "名称", example = "颜色") + private String name; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyRespVO.java new file mode 100644 index 0000000..66b916b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 属性项 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductPropertyRespVO extends ProductPropertyBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyUpdateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyUpdateReqVO.java new file mode 100644 index 0000000..378df38 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/property/ProductPropertyUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 属性项更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductPropertyUpdateReqVO extends ProductPropertyBaseVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "主键不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueBaseVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueBaseVO.java new file mode 100644 index 0000000..6e2f0d5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueBaseVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.value; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** +* 属性值 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ProductPropertyValueBaseVO { + + @Schema(description = "属性项的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "属性项的编号不能为空") + private Long propertyId; + + @Schema(description = "名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") + @NotEmpty(message = "名称名字不能为空") + private String name; + + @Schema(description = "备注", example = "颜色") + private String remark; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueCreateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueCreateReqVO.java new file mode 100644 index 0000000..29135b5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.value; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 商品属性值创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductPropertyValueCreateReqVO extends ProductPropertyValueBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueDetailRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueDetailRespVO.java new file mode 100644 index 0000000..2b58fb6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueDetailRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.value; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 商品属性值的明细 Response VO") +@Data +public class ProductPropertyValueDetailRespVO { + + @Schema(description = "属性的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long propertyId; + + @Schema(description = "属性的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") + private String propertyName; + + @Schema(description = "属性值的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long valueId; + + @Schema(description = "属性值的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") + private String valueName; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValuePageReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValuePageReqVO.java new file mode 100644 index 0000000..5772b3b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValuePageReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.value; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 商品属性值分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductPropertyValuePageReqVO extends PageParam { + + @Schema(description = "属性项的编号", example = "1024") + private String propertyId; + + @Schema(description = "名称", example = "红色") + private String name; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueRespVO.java new file mode 100644 index 0000000..0c3f025 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.value; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 商品属性值 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductPropertyValueRespVO extends ProductPropertyValueBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long id; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueUpdateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueUpdateReqVO.java new file mode 100644 index 0000000..7772528 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/property/vo/value/ProductPropertyValueUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.product.controller.admin.property.vo.value; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 商品属性值更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductPropertyValueUpdateReqVO extends ProductPropertyValueBaseVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "主键不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/ProductSkuController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/ProductSkuController.java new file mode 100644 index 0000000..ca60581 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/ProductSkuController.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.product.controller.admin.sku; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuOptionRespVO; +import com.yunxi.scm.module.product.convert.sku.ProductSkuConvert; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 商品 sku") +@RestController +@RequestMapping("/product/sku") +@Validated +public class ProductSkuController { + + @Resource + private ProductSkuService productSkuService; + @Resource + private ProductSpuService productSpuService; + + @GetMapping("/get-option-list") + @Operation(summary = "获得商品 SKU 选项的列表") +// @PreAuthorize("@ss.hasPermission('product:sku:query')") + public CommonResult> getSkuOptionList() { + // 获得 SKU 列表 + List skus = productSkuService.getSkuList(); + if (CollUtil.isEmpty(skus)) { + return success(Collections.emptyList()); + } + + // 获得对应的 SPU 映射 + Map spuMap = productSpuService.getSpuMap(convertSet(skus, ProductSkuDO::getSpuId)); + // 转换为返回结果 + List skuVOs = ProductSkuConvert.INSTANCE.convertList05(skus); + skuVOs.forEach(sku -> MapUtils.findAndThen(spuMap, sku.getSpuId(), + spu -> sku.setSpuId(spu.getId()).setSpuName(spu.getName()))); + return success(skuVOs); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuBaseVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuBaseVO.java new file mode 100644 index 0000000..0db21b8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuBaseVO.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.product.controller.admin.sku.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** +* 商品 SKU Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ProductSkuBaseVO { + + @Schema(description = "商品 SKU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖") + @NotEmpty(message = "商品 SKU 名字不能为空") + private String name; + + @Schema(description = "销售价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999") + @NotNull(message = "销售价格,单位:分不能为空") + private Integer price; + + @Schema(description = "市场价", example = "2999") + private Integer marketPrice; + + @Schema(description = "成本价", example = "19") + private Integer costPrice; + + @Schema(description = "条形码", example = "15156165456") + private String barCode; + + @Schema(description = "图片地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + @NotNull(message = "图片地址不能为空") + private String picUrl; + + @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + @NotNull(message = "库存不能为空") + private Integer stock; + + @Schema(description = "预警预存", example = "10") + private Integer warnStock; + + @Schema(description = "商品重量,单位:kg 千克", example = "1.2") + private Double weight; + + @Schema(description = "商品体积,单位:m^3 平米", example = "2.5") + private Double volume; + + @Schema(description = "一级分销的佣金,单位:分", example = "199") + private Integer subCommissionFirstPrice; + + @Schema(description = "二级分销的佣金,单位:分", example = "19") + private Integer subCommissionSecondPrice; + + @Schema(description = "属性数组") + private List properties; + + @Schema(description = "商品属性") + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Property { + + @Schema(description = "属性编号", example = "10") + private Long propertyId; + + @Schema(description = "属性名字", example = "颜色") + private String propertyName; + + @Schema(description = "属性值编号", example = "10") + private Long valueId; + + @Schema(description = "属性值名字", example = "红色") + private String valueName; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuCreateOrUpdateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuCreateOrUpdateReqVO.java new file mode 100644 index 0000000..e6554c4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuCreateOrUpdateReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.product.controller.admin.sku.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 商品 SKU 创建/更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductSkuCreateOrUpdateReqVO extends ProductSkuBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuOptionRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuOptionRespVO.java new file mode 100644 index 0000000..e8b0d53 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuOptionRespVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.product.controller.admin.sku.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 商品 SKU 选项 Response VO") // 用于前端 SELECT 选项 +@Data +public class ProductSkuOptionRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "商品 SKU 名字", example = "红色") + private String name; + + @Schema(description = "销售价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private String price; + + @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer stock; + + // ========== 商品 SPU 信息 ========== + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long spuId; + + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "iPhone 11") + private String spuName; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuRespVO.java new file mode 100644 index 0000000..be08e31 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/sku/vo/ProductSkuRespVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.product.controller.admin.sku.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 商品 SKU Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductSkuRespVO extends ProductSkuBaseVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/ProductSpuController.http b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/ProductSpuController.http new file mode 100644 index 0000000..4ab7b4f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/ProductSpuController.http @@ -0,0 +1,4 @@ +### 获得商品 SPU 明细 +GET {{baseUrl}}/product/spu/get-detail?id=4 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/ProductSpuController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/ProductSpuController.java new file mode 100644 index 0000000..4244c2e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/ProductSpuController.java @@ -0,0 +1,134 @@ +package com.yunxi.scm.module.product.controller.admin.spu; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.product.controller.admin.spu.vo.*; +import com.yunxi.scm.module.product.convert.spu.ProductSpuConvert; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS; + +@Tag(name = "管理后台 - 商品 SPU") +@RestController +@RequestMapping("/product/spu") +@Validated +public class ProductSpuController { + + @Resource + private ProductSpuService productSpuService; + @Resource + private ProductSkuService productSkuService; + + @PostMapping("/create") + @Operation(summary = "创建商品 SPU") + @PreAuthorize("@ss.hasPermission('product:spu:create')") + public CommonResult createProductSpu(@Valid @RequestBody ProductSpuCreateReqVO createReqVO) { + return success(productSpuService.createSpu(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新商品 SPU") + @PreAuthorize("@ss.hasPermission('product:spu:update')") + public CommonResult updateSpu(@Valid @RequestBody ProductSpuUpdateReqVO updateReqVO) { + productSpuService.updateSpu(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新商品 SPU Status") + @PreAuthorize("@ss.hasPermission('product:spu:update')") + public CommonResult updateStatus(@Valid @RequestBody ProductSpuUpdateStatusReqVO updateReqVO) { + productSpuService.updateSpuStatus(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除商品 SPU") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('product:spu:delete')") + public CommonResult deleteSpu(@RequestParam("id") Long id) { + productSpuService.deleteSpu(id); + return success(true); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得商品 SPU 明细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('product:spu:query')") + public CommonResult getSpuDetail(@RequestParam("id") Long id) { + // 获得商品 SPU + ProductSpuDO spu = productSpuService.getSpu(id); + if (spu == null) { + throw exception(SPU_NOT_EXISTS); + } + // 查询商品 SKU + List skus = productSkuService.getSkuListBySpuId(spu.getId()); + return success(ProductSpuConvert.INSTANCE.convertForSpuDetailRespVO(spu, skus)); + } + + @GetMapping("/get-simple-list") + @Operation(summary = "获得商品 SPU 精简列表") + @PreAuthorize("@ss.hasPermission('product:spu:query')") + public CommonResult> getSpuSimpleList() { + List list = productSpuService.getSpuList(); + return success(ProductSpuConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/list") + @Operation(summary = "获得商品 SPU 详情列表") + @Parameter(name = "spuIds", description = "spu 编号列表", required = true, example = "[1,2,3]") + @PreAuthorize("@ss.hasPermission('product:spu:query')") + public CommonResult> getSpuList(@RequestParam("spuIds") Collection spuIds) { + return success(ProductSpuConvert.INSTANCE.convertForSpuDetailRespListVO( + productSpuService.getSpuList(spuIds), productSkuService.getSkuListBySpuId(spuIds))); + } + + @GetMapping("/page") + @Operation(summary = "获得商品 SPU 分页") + @PreAuthorize("@ss.hasPermission('product:spu:query')") + public CommonResult> getSpuPage(@Valid ProductSpuPageReqVO pageVO) { + return success(ProductSpuConvert.INSTANCE.convertPage(productSpuService.getSpuPage(pageVO))); + } + + @GetMapping("/get-count") + @Operation(summary = "获得商品 SPU 分页 tab count") + @PreAuthorize("@ss.hasPermission('product:spu:query')") + public CommonResult> getSpuCount() { + return success(productSpuService.getTabsCount()); + } + + @GetMapping("/export") + @Operation(summary = "导出商品") + @PreAuthorize("@ss.hasPermission('product:spu:export')") + @OperateLog(type = EXPORT) + public void exportUserList(@Validated ProductSpuExportReqVO reqVO, + HttpServletResponse response) throws IOException { + List spuList = productSpuService.getSpuList(reqVO); + // 导出 Excel + List datas = ProductSpuConvert.INSTANCE.convertList03(spuList); + ExcelUtils.write(response, "商品列表.xls", "数据", ProductSpuExcelVO.class, datas); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuBaseVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuBaseVO.java new file mode 100644 index 0000000..9560f7d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuBaseVO.java @@ -0,0 +1,114 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** +* 商品 SPU Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author HUIHUI + */ +@Data +public class ProductSpuBaseVO { + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖") + @NotEmpty(message = "商品名称不能为空") + private String name; + + @Schema(description = "关键字", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉丝滑不出汗") + @NotEmpty(message = "商品关键字不能为空") + private String keyword; + + @Schema(description = "商品简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖简介") + @NotEmpty(message = "商品简介不能为空") + private String introduction; + + @Schema(description = "商品详情", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖详情") + @NotEmpty(message = "商品详情不能为空") + private String description; + + @Schema(description = "商品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品分类不能为空") + private Long categoryId; + + @Schema(description = "商品品牌编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品品牌不能为空") + private Long brandId; + + @Schema(description = "商品封面图", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + @NotEmpty(message = "商品封面图不能为空") + private String picUrl; + + @Schema(description = "商品轮播图", requiredMode = Schema.RequiredMode.REQUIRED, example = "[https://www.iocoder.cn/xx.png, https://www.iocoder.cn/xxx.png]") + private List sliderPicUrls; + + @Schema(description = "商品视频", example = "https://www.iocoder.cn/xx.mp4") + private String videoUrl; + + @Schema(description = "单位", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品单位不能为空") + private Integer unit; + + @Schema(description = "排序字段", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品排序字段不能为空") + private Integer sort; + + // ========== SKU 相关字段 ========= + + @Schema(description = "规格类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "商品规格类型不能为空") + private Boolean specType; + + // ========== 物流相关字段 ========= + + @Schema(description = "物流配置模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "111") + @NotNull(message = "物流配置模板编号不能为空") + private Long deliveryTemplateId; + + // ========== 营销相关字段 ========= + + @Schema(description = "是否热卖推荐", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "商品推荐不能为空") + private Boolean recommendHot; + + @Schema(description = "是否优惠推荐", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "商品推荐不能为空") + private Boolean recommendBenefit; + + @Schema(description = "是否精品推荐", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "商品推荐不能为空") + private Boolean recommendBest; + + @Schema(description = "是否新品推荐", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "商品推荐不能为空") + private Boolean recommendNew; + + @Schema(description = "是否优品推荐", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "商品推荐不能为空") + private Boolean recommendGood; + + @Schema(description = "赠送积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "111") + @NotNull(message = "商品赠送积分不能为空") + private Integer giveIntegral; + + @Schema(description = "赠送的优惠劵编号的数组", example = "[1, 10]") // TODO 这块前端还未实现 + private List giveCouponTemplateIds; + + @Schema(description = "分销类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "商品分销类型不能为空") + private Boolean subCommissionType; + + @Schema(description = "活动展示顺序", example = "[1, 3, 2, 4, 5]") // TODO 这块前端还未实现 + private List activityOrders; + + // ========== 统计相关字段 ========= + + @Schema(description = "虚拟销量", example = "芋道") + private Integer virtualSalesCount; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuCreateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuCreateReqVO.java new file mode 100644 index 0000000..eda3110 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuCreateReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import java.util.List; + +@Schema(description = "管理后台 - 商品 SPU 创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductSpuCreateReqVO extends ProductSpuBaseVO { + + // ========== SKU 相关字段 ========= + + @Schema(description = "SKU 数组") + @Valid + private List skus; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java new file mode 100644 index 0000000..fa0584a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuDetailRespVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 商品 SPU 详细 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductSpuDetailRespVO extends ProductSpuBaseVO { + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1212") + private Long id; + + @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000") + private Integer salesCount; + + @Schema(description = "浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000") + private Integer browseCount; + + @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + // ========== SKU 相关字段 ========= + + @Schema(description = "SKU 数组") + private List skus; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuExcelVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuExcelVO.java new file mode 100644 index 0000000..cfa610a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuExcelVO.java @@ -0,0 +1,112 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.product.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + + +import java.time.LocalDateTime; + +/** + * 商品 Spu Excel 导出 VO TODO 暂定 + * + * @author HUIHUI + */ +@Data +public class ProductSpuExcelVO { + + @ExcelProperty("商品编号") + private Long id; + + @ExcelProperty("商品名称") + private String name; + + @ExcelProperty("关键字") + private String keyword; + + @ExcelProperty("商品简介") + private String introduction; + + @ExcelProperty("商品详情") + private String description; + + @ExcelProperty("条形码") + private String barCode; + + @ExcelProperty("商品分类编号") + private Long categoryId; + + @ExcelProperty("商品品牌编号") + private Long brandId; + + @ExcelProperty("商品封面图") + private String picUrl; + + @ExcelProperty("商品视频") + private String videoUrl; + + @ExcelProperty(value = "商品单位", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PRODUCT_UNIT) + private Integer unit; + + @ExcelProperty("排序字段") + private Integer sort; + + @ExcelProperty(value = "商品状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.PRODUCT_SPU_STATUS) + private Integer status; + + @ExcelProperty("规格类型") + private Boolean specType; + + @ExcelProperty("商品价格") + private Integer price; + + @ExcelProperty("市场价") + private Integer marketPrice; + + @ExcelProperty("成本价") + private Integer costPrice; + + @ExcelProperty("库存") + private Integer stock; + + @ExcelProperty("物流配置模板编号") + private Long deliveryTemplateId; + + @ExcelProperty("是否热卖推荐") + private Boolean recommendHot; + + @ExcelProperty("是否优惠推荐") + private Boolean recommendBenefit; + + @ExcelProperty("是否精品推荐") + private Boolean recommendBest; + + @ExcelProperty("是否新品推荐") + private Boolean recommendNew; + + @ExcelProperty("是否优品推荐") + private Boolean recommendGood; + + @ExcelProperty("赠送积分") + private Integer giveIntegral; + + @ExcelProperty("分销类型") + private Boolean subCommissionType; + + @ExcelProperty("商品销量") + private Integer salesCount; + + @ExcelProperty("虚拟销量") + private Integer virtualSalesCount; + + @ExcelProperty("商品点击量") + private Integer browseCount; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuExportReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuExportReqVO.java new file mode 100644 index 0000000..cba8cba --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuExportReqVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 商品 SPU 导出 Request VO,参数和 ProductSpuPageReqVO 是一致的") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class ProductSpuExportReqVO { + + @Schema(description = "商品名称", example = "清凉小短袖") + private String name; + + @Schema(description = "前端请求的tab类型", example = "1") + private Integer tabType; + + @Schema(description = "商品分类编号", example = "100") + private Long categoryId; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuPageReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuPageReqVO.java new file mode 100644 index 0000000..41ed35d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuPageReqVO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 商品 SPU 分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductSpuPageReqVO extends PageParam { + + /** + * 出售中商品 + */ + public static final Integer FOR_SALE = 0; + + /** + * 仓库中商品 + */ + public static final Integer IN_WAREHOUSE = 1; + + /** + * 已售空商品 + */ + public static final Integer SOLD_OUT = 2; + + /** + * 警戒库存 + */ + public static final Integer ALERT_STOCK = 3; + + /** + * 商品回收站 + */ + public static final Integer RECYCLE_BIN = 4; + + @Schema(description = "商品名称", example = "清凉小短袖") + private String name; + + @Schema(description = "前端请求的tab类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer tabType; + + @Schema(description = "商品分类编号", example = "1") + private Long categoryId; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuRespVO.java new file mode 100644 index 0000000..bda1dbd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuRespVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 商品 SPU Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductSpuRespVO extends ProductSpuBaseVO { + + @Schema(description = "spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "111") + private Long id; + + @Schema(description = "商品价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999") + private Integer price; + + @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + private Integer salesCount; + + @Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "199") + private Integer marketPrice; + + @Schema(description = "成本价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "19") + private Integer costPrice; + + @Schema(description = "商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000") + private Integer stock; + + @Schema(description = "商品创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2023-05-24 00:00:00") + private LocalDateTime createTime; + + @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuSimpleRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuSimpleRespVO.java new file mode 100644 index 0000000..15c1156 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuSimpleRespVO.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - 商品 SPU 精简 Response VO") +@Data +@ToString(callSuper = true) +public class ProductSpuSimpleRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "213") + private Long id; + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉小短袖") + private String name; + + @Schema(description = "商品价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999") + private Integer price; + + @Schema(description = "商品市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "199") + private Integer marketPrice; + + @Schema(description = "商品成本价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "19") + private Integer costPrice; + + @Schema(description = "商品库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + private Integer stock; + + // ========== 统计相关字段 ========= + + @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Integer salesCount; + + @Schema(description = "商品虚拟销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20000") + private Integer virtualSalesCount; + + @Schema(description = "商品浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + private Integer browseCount; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuUpdateReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuUpdateReqVO.java new file mode 100644 index 0000000..0274b45 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuUpdateReqVO.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 商品 SPU 更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ProductSpuUpdateReqVO extends ProductSpuBaseVO { + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品编号不能为空") + private Long id; + + @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999") + private Integer salesCount; + + @Schema(description = "浏览量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1999") + private Integer browseCount; + + @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(ProductSpuStatusEnum.class) + private Integer status; + + // ========== SKU 相关字段 ========= + + @Schema(description = "SKU 数组") + @Valid + private List skus; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuUpdateStatusReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuUpdateStatusReqVO.java new file mode 100644 index 0000000..91e11da --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/admin/spu/vo/ProductSpuUpdateStatusReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.product.controller.admin.spu.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 商品 SPU Status 更新 Request VO") +@Data +public class ProductSpuUpdateStatusReqVO{ + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品编号不能为空") + private Long id; + + @Schema(description = "商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品状态不能为空") + @InEnum(ProductSpuStatusEnum.class) + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/category/AppCategoryController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/category/AppCategoryController.java new file mode 100644 index 0000000..1a6959c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/category/AppCategoryController.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.product.controller.app.category; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.product.controller.app.category.vo.AppCategoryRespVO; +import com.yunxi.scm.module.product.convert.category.ProductCategoryConvert; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; +import com.yunxi.scm.module.product.service.category.ProductCategoryService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.Comparator; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 商品分类") +@RestController +@RequestMapping("/product/category") +@Validated +public class AppCategoryController { + + @Resource + private ProductCategoryService categoryService; + + @GetMapping("/list") + @Operation(summary = "获得商品分类列表") + public CommonResult> getProductCategoryList() { + List list = categoryService.getEnableCategoryList(); + list.sort(Comparator.comparing(ProductCategoryDO::getSort)); + return success(ProductCategoryConvert.INSTANCE.convertList03(list)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/category/vo/AppCategoryRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/category/vo/AppCategoryRespVO.java new file mode 100644 index 0000000..467df8b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/category/vo/AppCategoryRespVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.product.controller.app.category.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Data +@Schema(description = "用户 APP - 商品分类 Response VO") +public class AppCategoryRespVO { + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Long id; + + @Schema(description = "父分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "父分类编号不能为空") + private Long parentId; + + @Schema(description = "分类名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "办公文具") + @NotBlank(message = "分类名称不能为空") + private String name; + + @Schema(description = "分类图片", requiredMode = Schema.RequiredMode.REQUIRED) + @NotBlank(message = "分类图片不能为空") + private String picUrl; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/AppCommentController.http b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/AppCommentController.http new file mode 100644 index 0000000..e69de29 diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/AppProductCommentController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/AppProductCommentController.java new file mode 100644 index 0000000..6ed4359 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/AppProductCommentController.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.product.controller.app.comment; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentPageReqVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentStatisticsRespVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppProductCommentRespVO; +import com.yunxi.scm.module.product.convert.comment.ProductCommentConvert; +import com.yunxi.scm.module.product.dal.dataobject.comment.ProductCommentDO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.service.comment.ProductCommentService; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.context.annotation.Lazy; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 商品评价") +@RestController +@RequestMapping("/product/comment") +@Validated +public class AppProductCommentController { + + @Resource + private ProductCommentService productCommentService; + + @Resource + @Lazy + private ProductSkuService productSkuService; + + @GetMapping("/list") + @Operation(summary = "获得最近的 n 条商品评价") + @Parameters({ + @Parameter(name = "spuId", description = "商品 SPU 编号", required = true, example = "1024"), + @Parameter(name = "count", description = "数量", required = true, example = "10") + }) + public CommonResult> getCommentList( + @RequestParam("spuId") Long spuId, + @RequestParam(value = "count", defaultValue = "10") Integer count) { + return success(productCommentService.getCommentList(spuId, count)); + } + + @GetMapping("/page") + @Operation(summary = "获得商品评价分页") + public CommonResult> getCommentPage(@Valid AppCommentPageReqVO pageVO) { + // TODO @puhui999:写到 convert 里,可以更简洁哈。 + PageResult commentDOPage = productCommentService.getCommentPage(pageVO, Boolean.TRUE); + Set skuIds = CollectionUtils.convertSet(commentDOPage.getList(), ProductCommentDO::getSkuId); + List skuList = productSkuService.getSkuList(skuIds); + Map skuDOMap = Maps.newLinkedHashMapWithExpectedSize(skuIds.size()); + if (CollUtil.isNotEmpty(skuList)) { + skuDOMap.putAll(CollectionUtils.convertMap(skuList, ProductSkuDO::getId, c -> c)); + } + PageResult page = ProductCommentConvert.INSTANCE.convertPage02(commentDOPage, skuDOMap); + return success(page); + } + + // TODO 芋艿:需要搞下 + @GetMapping("/statistics") + @Operation(summary = "获得商品的评价统计") + public CommonResult getCommentStatistics(@Valid @RequestParam("spuId") Long spuId) { + return success(productCommentService.getCommentStatistics(spuId, Boolean.TRUE)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppCommentPageReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppCommentPageReqVO.java new file mode 100644 index 0000000..4341629 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppCommentPageReqVO.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.product.controller.app.comment.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - 商品评价分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppCommentPageReqVO extends PageParam { + + /** + * 好评 + */ + public static final Integer GOOD_COMMENT = 1; + /** + * 中评 + */ + public static final Integer MEDIOCRE_COMMENT = 2; + /** + * 差评 + */ + public static final Integer NEGATIVE_COMMENT = 3; + + @Schema(description = "商品SPU编号", example = "29502") + @NotNull(message = "商品SPU编号不能为空") + private Long spuId; + + @Schema(description = "app 评论页 tab 类型 (0 全部、1 好评、2 中评、3 差评)", example = "0") + @NotNull(message = "商品SPU编号不能为空") + private Integer type; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppCommentStatisticsRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppCommentStatisticsRespVO.java new file mode 100644 index 0000000..a60ee74 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppCommentStatisticsRespVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.product.controller.app.comment.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "APP - 商品评价页评论分类数统计 Response VO") +@Data +@ToString(callSuper = true) +public class AppCommentStatisticsRespVO { + + @Schema(description = "好评数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15721") + private Long goodCount; + + @Schema(description = "中评数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15721") + private Long mediocreCount; + + @Schema(description = "差评数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "15721") + private Long negativeCount; + + @Schema(description = "总平均分", requiredMode = Schema.RequiredMode.REQUIRED, example = "3.55") + private Double scores; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppProductCommentRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppProductCommentRespVO.java new file mode 100644 index 0000000..c21c721 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/comment/vo/AppProductCommentRespVO.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.product.controller.app.comment.vo; + +import com.yunxi.scm.module.product.controller.app.property.vo.value.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 App - 商品评价详情 Response VO") +@Data +@ToString(callSuper = true) +public class AppProductCommentRespVO { + + @Schema(description = "评价人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15721") + private Long userId; + + @Schema(description = "评价人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + private String userNickname; + + @Schema(description = "评价人头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + private String userAvatar; + + @Schema(description = "订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24965") + private Long id; + + @Schema(description = "是否匿名", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean anonymous; + + @Schema(description = "交易订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24428") + private Long orderId; + + @Schema(description = "交易订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8233") + private Long orderItemId; + + @Schema(description = "商家是否回复", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean replyStatus; + + @Schema(description = "回复管理员编号", example = "22212") + private Long replyUserId; + + @Schema(description = "商家回复内容", example = "亲,你的好评就是我的动力(*^▽^*)") + private String replyContent; + + @Schema(description = "商家回复时间") + private LocalDateTime replyTime; + + @Schema(description = "追加评价内容", example = "穿了很久都很丝滑诶") + private String additionalContent; + + @Schema(description = "追评评价图片地址数组,以逗号分隔最多上传 9 张", example = "[https://www.iocoder.cn/xx.png, https://www.iocoder.cn/xxx.png]") + private List additionalPicUrls; + + @Schema(description = "追加评价时间") + private LocalDateTime additionalTime; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "91192") + @NotNull(message = "商品 SPU 编号不能为空") + private Long spuId; + + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "清凉丝滑小短袖") + @NotNull(message = "商品 SPU 名称不能为空") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "81192") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "商品 SKU 属性", requiredMode = Schema.RequiredMode.REQUIRED) + private List skuProperties; + + @Schema(description = "评分星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "评分星级 1-5 分不能为空") + private Integer scores; + + @Schema(description = "描述星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "描述星级 1-5 分不能为空") + private Integer descriptionScores; + + @Schema(description = "服务星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "服务星级 1-5 分不能为空") + private Integer benefitScores; + + @Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "哇,真的很丝滑凉快诶,好评") + @NotNull(message = "评论内容不能为空") + private String content; + + @Schema(description = "评论图片地址数组,以逗号分隔最多上传 9 张", requiredMode = Schema.RequiredMode.REQUIRED, example = "[https://www.iocoder.cn/xx.png]") + @Size(max = 9, message = "评论图片地址数组长度不能超过 9 张") + private List picUrls; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/AppFavoriteController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/AppFavoriteController.java new file mode 100644 index 0000000..b13d9f4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/AppFavoriteController.java @@ -0,0 +1,91 @@ +package com.yunxi.scm.module.product.controller.app.favorite; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.security.core.annotations.PreAuthenticated; +import com.yunxi.scm.module.product.controller.app.favorite.vo.AppFavoriteBatchReqVO; +import com.yunxi.scm.module.product.controller.app.favorite.vo.AppFavoritePageReqVO; +import com.yunxi.scm.module.product.controller.app.favorite.vo.AppFavoriteReqVO; +import com.yunxi.scm.module.product.controller.app.favorite.vo.AppFavoriteRespVO; +import com.yunxi.scm.module.product.convert.favorite.ProductFavoriteConvert; +import com.yunxi.scm.module.product.dal.dataobject.favorite.ProductFavoriteDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.service.favorite.ProductFavoriteService; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 商品收藏") +@RestController +@RequestMapping("/product/favorite") +public class AppFavoriteController { + + @Resource + private ProductFavoriteService productFavoriteService; + @Resource + private ProductSpuService productSpuService; + + @PostMapping(value = "/create") + @Operation(summary = "添加商品收藏") + @PreAuthenticated + public CommonResult createFavorite(@RequestBody @Valid AppFavoriteReqVO reqVO) { + return success(productFavoriteService.createFavorite(getLoginUserId(), reqVO.getSpuId())); + } + + @DeleteMapping(value = "/delete") + @Operation(summary = "取消单个商品收藏") + @PreAuthenticated + public CommonResult deleteFavorite(@RequestBody @Valid AppFavoriteReqVO reqVO) { + productFavoriteService.deleteFavorite(getLoginUserId(), reqVO.getSpuId()); + return success(Boolean.TRUE); + } + + @DeleteMapping(value = "/delete-list") + @Operation(summary = "取消多个商品收藏") + @PreAuthenticated + public CommonResult deleteFavoriteList(@RequestBody @Valid AppFavoriteBatchReqVO reqVO) { + // todo @jason:待实现 +// productFavoriteService.deleteFavorite(getLoginUserId(), reqVO.getSpuId()); + return success(Boolean.TRUE); + } + + @GetMapping(value = "/page") + @Operation(summary = "获得商品收藏分页") + @PreAuthenticated + public CommonResult> getFavoritePage(AppFavoritePageReqVO reqVO) { + PageResult favoritePage = productFavoriteService.getFavoritePage(getLoginUserId(), reqVO); + if (CollUtil.isEmpty(favoritePage.getList())) { + return success(PageResult.empty()); + } + + // 得到商品 spu 信息 + List favorites = favoritePage.getList(); + List spuIds = convertList(favorites, ProductFavoriteDO::getSpuId); + List spus = productSpuService.getSpuList(spuIds); + + // 转换 VO 结果 + PageResult pageResult = new PageResult<>(favoritePage.getTotal()); + pageResult.setList(ProductFavoriteConvert.INSTANCE.convertList(favorites, spus)); + return success(pageResult); + } + + @GetMapping(value = "/exits") + @Operation(summary = "检查是否收藏过商品") + @PreAuthenticated + public CommonResult isFavoriteExists(AppFavoriteReqVO reqVO) { + ProductFavoriteDO favoriteDO = productFavoriteService.getFavorite(getLoginUserId(), reqVO.getSpuId()); + return success(Objects.nonNull(favoriteDO)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteBatchReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteBatchReqVO.java new file mode 100644 index 0000000..8e45189 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteBatchReqVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.product.controller.app.favorite.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.List; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "用户 APP - 商品收藏的批量 Request VO") // 用于收藏、取消收藏、获取收藏 +@Data +public class AppFavoriteBatchReqVO { + + @Schema(description = "商品 SPU 编号数组", requiredMode = REQUIRED, example = "29502") + @NotEmpty(message = "商品 SPU 编号数组不能为空") + private List spuIds; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoritePageReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoritePageReqVO.java new file mode 100644 index 0000000..5b37002 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoritePageReqVO.java @@ -0,0 +1,10 @@ +package com.yunxi.scm.module.product.controller.app.favorite.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 商品收藏分页查询 Request VO") +@Data +public class AppFavoritePageReqVO extends PageParam { +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteReqVO.java new file mode 100644 index 0000000..981b455 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.product.controller.app.favorite.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "用户 APP - 商品收藏的单个 Request VO") // 用于收藏、取消收藏、获取收藏 +@Data +public class AppFavoriteReqVO { + + @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502") + @NotNull(message = "商品 SPU 编号不能为空") + private Long spuId; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteRespVO.java new file mode 100644 index 0000000..b5dd119 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/favorite/vo/AppFavoriteRespVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.product.controller.app.favorite.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; + +@Schema(description = "用户 App - 商品收藏 Response VO") +@Data +public class AppFavoriteRespVO { + + @Schema(description = "编号", requiredMode = REQUIRED, example = "1") + private Long id; + + @Schema(description = "商品 SPU 编号", requiredMode = REQUIRED, example = "29502") + private Long spuId; + + // ========== 商品相关字段 ========== + + @Schema(description = "商品 SPU 名称", example = "赵六") + private String spuName; + + @Schema(description = "商品封面图", example = "https://domain/pic.png") + private String picUrl; + + @Schema(description = "商品单价", example = "100") + private Integer price; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/package-info.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/package-info.java new file mode 100644 index 0000000..670c741 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,无时间作用,避免 package 缩进 + */ +package com.yunxi.scm.module.product.controller.app.property; diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/vo/property/package-info.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/vo/property/package-info.java new file mode 100644 index 0000000..df8fb52 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/vo/property/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,无时间作用,避免 package 缩进 + */ +package com.yunxi.scm.module.product.controller.app.property.vo.property; diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/vo/value/AppProductPropertyValueDetailRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/vo/value/AppProductPropertyValueDetailRespVO.java new file mode 100644 index 0000000..f4f8684 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/property/vo/value/AppProductPropertyValueDetailRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.product.controller.app.property.vo.value; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 商品属性值的明细 Response VO") +@Data +public class AppProductPropertyValueDetailRespVO { + + @Schema(description = "属性的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long propertyId; + + @Schema(description = "属性的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") + private String propertyName; + + @Schema(description = "属性值的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long valueId; + + @Schema(description = "属性值的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") + private String valueName; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/AppProductSpuController.http b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/AppProductSpuController.http new file mode 100644 index 0000000..c391b58 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/AppProductSpuController.http @@ -0,0 +1,18 @@ +### 获得订单交易的分页(默认) +GET {{appApi}}/product/spu/page?pageNo=1&pageSize=10 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +### 获得订单交易的分页(价格) +GET {{appApi}}/product/spu/page?pageNo=1&pageSize=10&sortField=price&sortAsc=true +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +### 获得订单交易的分页(销售) +GET {{appApi}}/product/spu/page?pageNo=1&pageSize=10&sortField=salesCount&sortAsc=true +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +### 获得商品 SPU 明细 +GET {{appApi}}/product/spu/get-detail?id=102 +tenant-id: {{appTenentId}} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/AppProductSpuController.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/AppProductSpuController.java new file mode 100644 index 0000000..4ea1256 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/AppProductSpuController.java @@ -0,0 +1,83 @@ +package com.yunxi.scm.module.product.controller.app.spu; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuDetailRespVO; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuPageRespVO; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuPageReqVO; +import com.yunxi.scm.module.product.convert.spu.ProductSpuConvert; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SPU_NOT_ENABLE; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS; + +@Tag(name = "用户 APP - 商品 SPU") +@RestController +@RequestMapping("/product/spu") +@Validated +public class AppProductSpuController { + + @Resource + private ProductSpuService productSpuService; + @Resource + private ProductSkuService productSkuService; + + @GetMapping("/list") + @Operation(summary = "获得商品 SPU 列表") + @Parameters({ + @Parameter(name = "recommendType", description = "推荐类型", required = true), // 参见 AppProductSpuPageReqVO.RECOMMEND_TYPE_XXX 常量 + @Parameter(name = "count", description = "数量", required = true) + }) + public CommonResult> getSpuList( + @RequestParam("recommendType") String recommendType, + @RequestParam(value = "count", defaultValue = "10") Integer count) { + List list = productSpuService.getSpuList(recommendType, count); + return success(ProductSpuConvert.INSTANCE.convertListForGetSpuList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得商品 SPU 分页") + public CommonResult> getSpuPage(@Valid AppProductSpuPageReqVO pageVO) { + PageResult pageResult = productSpuService.getSpuPage(pageVO); + return success(ProductSpuConvert.INSTANCE.convertPageForGetSpuPage(pageResult)); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得商品 SPU 明细") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult getSpuDetail(@RequestParam("id") Long id) { + // 获得商品 SPU + ProductSpuDO spu = productSpuService.getSpu(id); + if (spu == null) { + throw exception(SPU_NOT_EXISTS); + } + if (!ProductSpuStatusEnum.isEnable(spu.getStatus())) { + throw exception(SPU_NOT_ENABLE); + } + + // 查询商品 SKU + List skus = productSkuService.getSkuListBySpuId(spu.getId()); + // 拼接 + return success(ProductSpuConvert.INSTANCE.convertForGetSpuDetail(spu, skus)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuDetailRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuDetailRespVO.java new file mode 100644 index 0000000..a727f0e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuDetailRespVO.java @@ -0,0 +1,109 @@ +package com.yunxi.scm.module.product.controller.app.spu.vo; + +import com.yunxi.scm.module.product.controller.app.property.vo.value.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 商品 SPU 明细 Response VO") +@Data +public class AppProductSpuDetailRespVO { + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + // ========== 基本信息 ========= + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "商品简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是一个快乐简介") + private String introduction; + + @Schema(description = "商品详情", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是商品描述") + private String description; + + @Schema(description = "商品分类编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long categoryId; + + @Schema(description = "商品封面图", requiredMode = Schema.RequiredMode.REQUIRED) + private String picUrl; + + @Schema(description = "商品轮播图", requiredMode = Schema.RequiredMode.REQUIRED) + private List sliderPicUrls; + + @Schema(description = "商品视频", requiredMode = Schema.RequiredMode.REQUIRED) + private String videoUrl; + + @Schema(description = "单位名", requiredMode = Schema.RequiredMode.REQUIRED, example = "个") + private String unitName; + + // ========== 营销相关字段 ========= + + @Schema(description = "活动排序数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private List activityOrders; + + // ========== SKU 相关字段 ========= + + @Schema(description = "规格类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean specType; + + @Schema(description = "商品价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer price; + + @Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer marketPrice; + + @Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级,计算出折扣后价格 + private Integer vipPrice; + + @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private Integer stock; + + /** + * SKU 数组 + */ + private List skus; + + // ========== 统计相关字段 ========= + + @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer salesCount; + + @Schema(description = "用户 App - 商品 SPU 明细的 SKU 信息") + @Data + public static class Sku { + + @Schema(description = "商品 SKU 编号", example = "1") + private Long id; + + /** + * 商品属性数组 + */ + private List properties; + + @Schema(description = "销售价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer price; + + @Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer marketPrice; + + @Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级,计算出折扣后价格 + private Integer vipPrice; + + @Schema(description = "图片地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + private String picUrl; + + @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer stock; + + @Schema(description = "商品重量", example = "1") // 单位:kg 千克 + private Double weight; + + @Schema(description = "商品体积", example = "1024") // 单位:m^3 平米 + private Double volume; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuPageReqVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuPageReqVO.java new file mode 100644 index 0000000..4d45ef5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuPageReqVO.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.product.controller.app.spu.vo; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.AssertTrue; + +@Schema(description = "用户 App - 商品 SPU 分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppProductSpuPageReqVO extends PageParam { + + public static final String SORT_FIELD_PRICE = "price"; + public static final String SORT_FIELD_SALES_COUNT = "salesCount"; + + public static final String RECOMMEND_TYPE_HOT = "hot"; + public static final String RECOMMEND_TYPE_BENEFIT = "benefit"; + public static final String RECOMMEND_TYPE_BEST = "best"; + public static final String RECOMMEND_TYPE_NEW = "new"; + public static final String RECOMMEND_TYPE_GOOD = "good"; + + @Schema(description = "分类编号", example = "1") + private Long categoryId; + + @Schema(description = "关键字", example = "好看") + private String keyword; + + @Schema(description = "排序字段", example = "price") // 参见 AppProductSpuPageReqVO.SORT_FIELD_XXX 常量 + private String sortField; + + @Schema(description = "排序方式", example = "true") + private Boolean sortAsc; + + @Schema(description = "推荐类型", example = "hot") // 参见 AppProductSpuPageReqVO.RECOMMEND_TYPE_XXX 常量 + private String recommendType; + + @AssertTrue(message = "排序字段不合法") + @JsonIgnore + public boolean isSortFieldValid() { + if (StrUtil.isEmpty(sortField)) { + return true; + } + return StrUtil.equalsAny(sortField, SORT_FIELD_PRICE, SORT_FIELD_SALES_COUNT); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java new file mode 100644 index 0000000..398561b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/controller/app/spu/vo/AppProductSpuPageRespVO.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.product.controller.app.spu.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 商品 SPU Response VO") +@Data +public class AppProductSpuPageRespVO { + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "分类编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long categoryId; + + @Schema(description = "商品封面图", requiredMode = Schema.RequiredMode.REQUIRED) + private String picUrl; + + @Schema(description = "商品轮播图", requiredMode = Schema.RequiredMode.REQUIRED) + private List sliderPicUrls; + + @Schema(description = "单位名", requiredMode = Schema.RequiredMode.REQUIRED, example = "个") + private String unitName; + + // ========== SKU 相关字段 ========= + + @Schema(description = "规格类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean specType; + + @Schema(description = "商品价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer price; + + @Schema(description = "市场价,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer marketPrice; + + @Schema(description = "VIP 价格,单位使用:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "968") // 通过会员等级,计算出折扣后价格 + private Integer vipPrice; + + @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private Integer stock; + + // ========== 营销相关字段 ========= + + @Schema(description = "活动排序数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private List activityOrders; + + // ========== 统计相关字段 ========= + + @Schema(description = "商品销量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer salesCount; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/brand/ProductBrandConvert.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/brand/ProductBrandConvert.java new file mode 100644 index 0000000..b3dca10 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/brand/ProductBrandConvert.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.product.convert.brand; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandRespVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandSimpleRespVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.brand.ProductBrandDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 品牌 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ProductBrandConvert { + + ProductBrandConvert INSTANCE = Mappers.getMapper(ProductBrandConvert.class); + + ProductBrandDO convert(ProductBrandCreateReqVO bean); + + ProductBrandDO convert(ProductBrandUpdateReqVO bean); + + ProductBrandRespVO convert(ProductBrandDO bean); + + List convertList1(List list); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/category/ProductCategoryConvert.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/category/ProductCategoryConvert.java new file mode 100644 index 0000000..0801364 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/category/ProductCategoryConvert.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.product.convert.category; + +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryRespVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; +import com.yunxi.scm.module.product.controller.app.category.vo.AppCategoryRespVO; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 商品分类 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ProductCategoryConvert { + + ProductCategoryConvert INSTANCE = Mappers.getMapper(ProductCategoryConvert.class); + + ProductCategoryDO convert(ProductCategoryCreateReqVO bean); + + ProductCategoryDO convert(ProductCategoryUpdateReqVO bean); + + ProductCategoryRespVO convert(ProductCategoryDO bean); + + List convertList(List list); + + List convertList03(List list); +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/comment/ProductCommentConvert.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/comment/ProductCommentConvert.java new file mode 100644 index 0000000..fee3f01 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/comment/ProductCommentConvert.java @@ -0,0 +1,109 @@ +package com.yunxi.scm.module.product.convert.comment; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentRespVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentStatisticsRespVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppProductCommentRespVO; +import com.yunxi.scm.module.product.controller.app.property.vo.value.AppProductPropertyValueDetailRespVO; +import com.yunxi.scm.module.product.dal.dataobject.comment.ProductCommentDO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Map; + +/** + * 商品评价 Convert + * + * @author wangzhs + */ +@Mapper +public interface ProductCommentConvert { + + ProductCommentConvert INSTANCE = Mappers.getMapper(ProductCommentConvert.class); + + ProductCommentRespVO convert(ProductCommentDO bean); + + @Mapping(target = "scores", expression = "java(calculateOverallScore(goodCount, mediocreCount, negativeCount))") + AppCommentStatisticsRespVO convert(Long goodCount, Long mediocreCount, Long negativeCount); + @Named("calculateOverallScore") + default double calculateOverallScore(long goodCount, long mediocreCount, long negativeCount) { + return (goodCount * 5 + mediocreCount * 3 + negativeCount) / (double) (goodCount + mediocreCount + negativeCount); + } + + List convertList(List list); + + PageResult convertPage(PageResult page); + + PageResult convertPage01(PageResult pageResult); + + default PageResult convertPage02(PageResult pageResult, + Map skuMap) { + PageResult page = convertPage01(pageResult); + page.getList().forEach(item -> { + // 判断用户是否选择匿名 + if (ObjectUtil.equal(item.getAnonymous(), true)) { + item.setUserNickname(ProductCommentDO.NICKNAME_ANONYMOUS); + } + MapUtils.findAndThen(skuMap, item.getSkuId(), + sku -> item.setSkuProperties(convertList01(sku.getProperties()))); + }); + return page; + } + List convertList01(List properties); + + /** + * 计算综合评分 + * + * @param descriptionScores 描述星级 + * @param benefitScores 服务星级 + * @return 综合评分 + */ + @Named("convertScores") + default Integer convertScores(Integer descriptionScores, Integer benefitScores) { + // 计算评价最终综合评分 最终星数 = (商品评星 + 服务评星) / 2 + BigDecimal sumScore = new BigDecimal(descriptionScores + benefitScores); + BigDecimal divide = sumScore.divide(BigDecimal.valueOf(2L), 0, RoundingMode.DOWN); + return divide.intValue(); + } + + ProductCommentDO convert(ProductCommentCreateReqDTO createReqDTO); + + @Mapping(target = "scores", + expression = "java(convertScores(createReqDTO.getDescriptionScores(), createReqDTO.getBenefitScores()))") + default ProductCommentDO convert(ProductCommentCreateReqDTO createReqDTO, ProductSpuDO spuDO, MemberUserRespDTO user) { + ProductCommentDO commentDO = convert(createReqDTO); + if (user != null) { + commentDO.setUserId(user.getId()); + commentDO.setUserNickname(user.getNickname()); + commentDO.setUserAvatar(user.getAvatar()); + } + if (spuDO != null) { + commentDO.setSpuId(spuDO.getId()); + commentDO.setSpuName(spuDO.getName()); + } + return commentDO; + } + + @Mapping(target = "userId", constant = "0L") + @Mapping(target = "orderId", constant = "0L") + @Mapping(target = "orderItemId", constant = "0L") + @Mapping(target = "anonymous", expression = "java(Boolean.FALSE)") + @Mapping(target = "scores", + expression = "java(convertScores(createReq.getDescriptionScores(), createReq.getBenefitScores()))") + ProductCommentDO convert(ProductCommentCreateReqVO createReq); + + List convertList02(List list); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/favorite/ProductFavoriteConvert.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/favorite/ProductFavoriteConvert.java new file mode 100644 index 0000000..40b25d0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/favorite/ProductFavoriteConvert.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.product.convert.favorite; + +import com.yunxi.scm.module.product.controller.app.favorite.vo.AppFavoriteRespVO; +import com.yunxi.scm.module.product.dal.dataobject.favorite.ProductFavoriteDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; + +@Mapper +public interface ProductFavoriteConvert { + + ProductFavoriteConvert INSTANCE = Mappers.getMapper(ProductFavoriteConvert.class); + + ProductFavoriteDO convert(Long userId, Long spuId); + + @Mapping(target = "id", source = "favorite.id") + @Mapping(target = "spuName", source = "spu.name") + AppFavoriteRespVO convert(ProductSpuDO spu, ProductFavoriteDO favorite); + + default List convertList(List favorites, List spus) { + List resultList = new ArrayList<>(favorites.size()); + Map spuMap = convertMap(spus, ProductSpuDO::getId); + for (ProductFavoriteDO favorite : favorites) { + ProductSpuDO spuDO = spuMap.get(favorite.getSpuId()); + resultList.add(convert(spuDO, favorite)); + } + return resultList; + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/property/ProductPropertyConvert.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/property/ProductPropertyConvert.java new file mode 100644 index 0000000..127a16e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/property/ProductPropertyConvert.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.product.convert.property; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyAndValueRespVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyRespVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyValueDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +/** + * 属性项 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ProductPropertyConvert { + + ProductPropertyConvert INSTANCE = Mappers.getMapper(ProductPropertyConvert.class); + + ProductPropertyDO convert(ProductPropertyCreateReqVO bean); + + ProductPropertyDO convert(ProductPropertyUpdateReqVO bean); + + ProductPropertyRespVO convert(ProductPropertyDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default List convertList(List keys, List values) { + Map> valueMap = CollectionUtils.convertMultiMap(values, ProductPropertyValueDO::getPropertyId); + return CollectionUtils.convertList(keys, key -> { + ProductPropertyAndValueRespVO respVO = convert02(key); + // 如果属性值为空value不为null,返回空列表 + if (CollUtil.isEmpty(values)) { + respVO.setValues(Collections.emptyList()); + }else { + respVO.setValues(convertList02(valueMap.get(key.getId()))); + } + return respVO; + }); + } + ProductPropertyAndValueRespVO convert02(ProductPropertyDO bean); + List convertList02(List list); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/propertyvalue/ProductPropertyValueConvert.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/propertyvalue/ProductPropertyValueConvert.java new file mode 100644 index 0000000..bdf61ae --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/propertyvalue/ProductPropertyValueConvert.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.product.convert.propertyvalue; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueRespVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyValueDO; +import com.yunxi.scm.module.product.service.property.bo.ProductPropertyValueDetailRespBO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 属性值 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ProductPropertyValueConvert { + + ProductPropertyValueConvert INSTANCE = Mappers.getMapper(ProductPropertyValueConvert.class); + + ProductPropertyValueDO convert(ProductPropertyValueCreateReqVO bean); + + ProductPropertyValueDO convert(ProductPropertyValueUpdateReqVO bean); + + ProductPropertyValueRespVO convert(ProductPropertyValueDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default List convertList(List values, List keys) { + Map keyMap = convertMap(keys, ProductPropertyDO::getId); + return CollectionUtils.convertList(values, value -> { + ProductPropertyValueDetailRespBO valueDetail = new ProductPropertyValueDetailRespBO() + .setValueId(value.getId()).setValueName(value.getName()); + // 设置属性项 + MapUtils.findAndThen(keyMap, value.getPropertyId(), + key -> valueDetail.setPropertyId(key.getId()).setPropertyName(key.getName())); + return valueDetail; + }); + } + + List convertList02(List list); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/sku/ProductSkuConvert.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/sku/ProductSkuConvert.java new file mode 100644 index 0000000..793a525 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/sku/ProductSkuConvert.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.product.convert.sku; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuOptionRespVO; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuRespVO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 商品 SKU Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ProductSkuConvert { + + ProductSkuConvert INSTANCE = Mappers.getMapper(ProductSkuConvert.class); + + ProductSkuDO convert(ProductSkuCreateOrUpdateReqVO bean); + + ProductSkuRespVO convert(ProductSkuDO bean); + + List convertList(List list); + + List convertList06(List list); + + default List convertList06(List list, Long spuId) { + List result = convertList06(list); + result.forEach(item -> item.setSpuId(spuId)); + return result; + } + + ProductSkuRespDTO convert02(ProductSkuDO bean); + + List convertList04(List list); + + List convertList05(List skus); + + /** + * 获得 SPU 的库存变化 Map + * + * @param items SKU 库存变化 + * @param skus SKU 列表 + * @return SPU 的库存变化 Map + */ + default Map convertSpuStockMap(List items, + List skus) { + Map skuIdAndSpuIdMap = convertMap(skus, ProductSkuDO::getId, ProductSkuDO::getSpuId); // SKU 与 SKU 编号的 Map 关系 + Map spuIdAndStockMap = new HashMap<>(); // SPU 的库存变化 Map 关系 + items.forEach(item -> { + Long spuId = skuIdAndSpuIdMap.get(item.getId()); + if (spuId == null) { + return; + } + Integer stock = spuIdAndStockMap.getOrDefault(spuId, 0) + item.getIncrCount(); + spuIdAndStockMap.put(spuId, stock); + }); + return spuIdAndStockMap; + } + + default String buildPropertyKey(ProductSkuDO bean) { + if (CollUtil.isEmpty(bean.getProperties())) { + return StrUtil.EMPTY; + } + List properties = new ArrayList<>(bean.getProperties()); + properties.sort(Comparator.comparing(ProductSkuDO.Property::getValueId)); + return properties.stream().map(m -> String.valueOf(m.getValueId())).collect(Collectors.joining()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/spu/ProductSpuConvert.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/spu/ProductSpuConvert.java new file mode 100644 index 0000000..47a5846 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/convert/spu/ProductSpuConvert.java @@ -0,0 +1,124 @@ +package com.yunxi.scm.module.product.convert.spu; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.dict.core.util.DictFrameworkUtils; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.product.controller.admin.spu.vo.*; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuDetailRespVO; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuPageReqVO; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuPageRespVO; +import com.yunxi.scm.module.product.convert.sku.ProductSkuConvert; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.enums.DictTypeConstants; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static cn.hutool.core.util.ObjectUtil.defaultIfNull; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMultiMap; + +/** + * 商品 SPU Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ProductSpuConvert { + + ProductSpuConvert INSTANCE = Mappers.getMapper(ProductSpuConvert.class); + + ProductSpuDO convert(ProductSpuCreateReqVO bean); + + ProductSpuDO convert(ProductSpuUpdateReqVO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + ProductSpuPageReqVO convert(AppProductSpuPageReqVO bean); + + List convertList2(List list); + + List convertList02(List list); + + @Mapping(target = "price", expression = "java(spu.getPrice() / 100)") + @Mapping(target = "marketPrice", expression = "java(spu.getMarketPrice() / 100)") + @Mapping(target = "costPrice", expression = "java(spu.getCostPrice() / 100)") + ProductSpuExcelVO convert(ProductSpuDO spu); + + default List convertList03(List list) { + List spuExcelVOs = new ArrayList<>(); + list.forEach(spu -> { + ProductSpuExcelVO spuExcelVO = convert(spu); + spuExcelVOs.add(spuExcelVO); + }); + return spuExcelVOs; + } + + ProductSpuDetailRespVO convert03(ProductSpuDO spu); + + // ========== 用户 App 相关 ========== + + PageResult convertPageForGetSpuPage(PageResult page); + + default List convertListForGetSpuList(List list) { + // 处理虚拟销量 + list.forEach(spu -> spu.setSalesCount(spu.getSalesCount() + spu.getVirtualSalesCount())); + // 处理 VO 字段 + List voList = convertListForGetSpuList0(list); + for (int i = 0; i < list.size(); i++) { + ProductSpuDO spu = list.get(i); + AppProductSpuPageRespVO spuVO = voList.get(i); + spuVO.setUnitName(DictFrameworkUtils.getDictDataLabel(DictTypeConstants.PRODUCT_UNIT, spu.getUnit())); + // 计算 vip 价格 TODO 芋艿:临时的逻辑,等 vip 支持后 + spuVO.setVipPrice((int) (spuVO.getPrice() * 0.9)); + } + return voList; + } + @Named("convertListForGetSpuList0") + List convertListForGetSpuList0(List list); + + default AppProductSpuDetailRespVO convertForGetSpuDetail(ProductSpuDO spu, List skus) { + // 处理 SPU + AppProductSpuDetailRespVO spuVO = convertForGetSpuDetail(spu) + .setSalesCount(spu.getSalesCount() + defaultIfNull(spu.getVirtualSalesCount(), 0)) + .setUnitName(DictFrameworkUtils.getDictDataLabel(DictTypeConstants.PRODUCT_UNIT, spu.getUnit())); + // 处理 SKU + spuVO.setSkus(convertListForGetSpuDetail(skus)); + // 计算 vip 价格 TODO 芋艿:临时的逻辑,等 vip 支持后 + if (true) { + spuVO.setVipPrice((int) (spuVO.getPrice() * 0.9)); + spuVO.getSkus().forEach(sku -> sku.setVipPrice((int) (sku.getPrice() * 0.9))); + } + return spuVO; + } + + AppProductSpuDetailRespVO convertForGetSpuDetail(ProductSpuDO spu); + + List convertListForGetSpuDetail(List skus); + + default ProductSpuDetailRespVO convertForSpuDetailRespVO(ProductSpuDO spu, List skus) { + ProductSpuDetailRespVO detailRespVO = convert03(spu); + detailRespVO.setSkus(ProductSkuConvert.INSTANCE.convertList(skus)); + return detailRespVO; + } + + default List convertForSpuDetailRespListVO(List spus, List skus) { + List vos = new ArrayList<>(spus.size()); + Map> skuMultiMap = convertMultiMap(skus, ProductSkuDO::getSpuId); + // TODO @puhui999:可以直接使用 CollUtils.convertList + spus.forEach(spu -> { + ProductSpuDetailRespVO detailRespVO = convert03(spu); + detailRespVO.setSkus(ProductSkuConvert.INSTANCE.convertList(skuMultiMap.get(spu.getId()))); + vos.add(detailRespVO); + }); + return vos; + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/brand/ProductBrandDO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/brand/ProductBrandDO.java new file mode 100644 index 0000000..6d07f40 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/brand/ProductBrandDO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.product.dal.dataobject.brand; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 商品品牌 DO + * + * @author 芋道源码 + */ +@TableName("product_brand") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProductBrandDO extends BaseDO { + + /** + * 品牌编号 + */ + @TableId + private Long id; + /** + * 品牌名称 + */ + private String name; + /** + * 品牌图片 + */ + private String picUrl; + /** + * 品牌排序 + */ + private Integer sort; + /** + * 品牌描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + // TODO 芋艿:firstLetter 首字母 + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/category/ProductCategoryDO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/category/ProductCategoryDO.java new file mode 100644 index 0000000..ac2c93c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/category/ProductCategoryDO.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.module.product.dal.dataobject.category; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 商品分类 DO + * + * @author 芋道源码 + */ +@TableName("product_category") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProductCategoryDO extends BaseDO { + + /** + * 父分类编号 - 根分类 + */ + public static final Long PARENT_ID_NULL = 0L; + /** + * 限定分类层级 + */ + public static final int CATEGORY_LEVEL = 2; + + /** + * 分类编号 + */ + @TableId + private Long id; + /** + * 父分类编号 + */ + private Long parentId; + /** + * 分类名称 + */ + private String name; + /** + * 移动端分类图 + * + * 建议 180*180 分辨率 + */ + private String picUrl; + /** + * PC 端分类图 + * + * 建议 468*340 分辨率 + */ + private String bigPicUrl; + /** + * 分类排序 + */ + private Integer sort; + /** + * 开启状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/comment/ProductCommentDO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/comment/ProductCommentDO.java new file mode 100644 index 0000000..b52c4f9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/comment/ProductCommentDO.java @@ -0,0 +1,143 @@ +package com.yunxi.scm.module.product.dal.dataobject.comment; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 商品评论 DO + * + * @author 芋道源码 + */ +@TableName(value = "product_comment", autoResultMap = true) +@KeySequence("product_comment_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProductCommentDO extends BaseDO { + + /** + * 默认匿名昵称 + */ + public static final String NICKNAME_ANONYMOUS = "匿名用户"; + + /** + * 评论编号,主键自增 + */ + @TableId + private Long id; + + /** + * 评价人的用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 评价人名称 + */ + private String userNickname; + /** + * 评价人头像 + */ + private String userAvatar; + /** + * 是否匿名 + */ + private Boolean anonymous; + + /** + * 交易订单编号 + * + * 关联 TradeOrderDO 的 id 编号 + */ + private Long orderId; + /** + * 交易订单项编号 + * + * 关联 TradeOrderItemDO 的 id 编号 + */ + private Long orderItemId; + + /** + * 商品 SPU 编号 + * 关联 {@link ProductSpuDO#getId()} + */ + private Long spuId; + /** + * 商品 SPU 名称 + */ + private String spuName; + /** + * 商品 SKU 编号 + * + * 关联 {@link ProductSkuDO#getId()} + */ + private Long skuId; + + /** + * 是否可见 + * + * true:显示 + * false:隐藏 + */ + private Boolean visible; + /** + * 评分星级 + * + * 1-5 分 + */ + private Integer scores; + /** + * 描述星级 + * + * 1-5 星 + */ + private Integer descriptionScores; + /** + * 服务星级 + * + * 1-5 星 + */ + private Integer benefitScores; + /** + * 评论内容 + */ + private String content; + /** + * 评论图片地址数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List picUrls; + + /** + * 商家是否回复 + */ + private Boolean replyStatus; + /** + * 回复管理员编号 + * 关联 AdminUserDO 的 id 编号 + */ + private Long replyUserId; + /** + * 商家回复内容 + */ + private String replyContent; + /** + * 商家回复时间 + */ + private LocalDateTime replyTime; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/favorite/ProductFavoriteDO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/favorite/ProductFavoriteDO.java new file mode 100644 index 0000000..b339ccb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/favorite/ProductFavoriteDO.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.product.dal.dataobject.favorite; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 商品收藏 DO + * + * @author 芋道源码 + */ +@TableName("product_favorite") +@KeySequence("product_favorite_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProductFavoriteDO extends BaseDO { + + /** + * 编号,主键自增 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 商品 SPU 编号 + * + * 关联 {@link ProductSpuDO#getId()} + */ + private Long spuId; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/property/ProductPropertyDO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/property/ProductPropertyDO.java new file mode 100644 index 0000000..a595d85 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/property/ProductPropertyDO.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.module.product.dal.dataobject.property; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 商品属性项 DO + * + * @author 芋道源码 + */ +@TableName("product_property") +@KeySequence("product_property_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProductPropertyDO extends BaseDO { + + /** + * SPU 单规格时,默认属性 id + */ + public static final Long ID_DEFAULT = 0L; + /** + * SPU 单规格时,默认属性名字 + */ + public static final String NAME_DEFAULT = "默认"; + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 名称 + */ + private String name; + /** + * 状态 + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/property/ProductPropertyValueDO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/property/ProductPropertyValueDO.java new file mode 100644 index 0000000..b77a813 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/property/ProductPropertyValueDO.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.product.dal.dataobject.property; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + + +/** + * 商品属性值 DO + * + * @author 芋道源码 + */ +@TableName("product_property_value") +@KeySequence("product_property_value_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProductPropertyValueDO extends BaseDO { + + /** + * SPU 单规格时,默认属性值 id + */ + public static final Long ID_DEFAULT = 0L; + /** + * SPU 单规格时,默认属性值名字 + */ + public static final String NAME_DEFAULT = "默认"; + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 属性项的编号 + * + * 关联 {@link ProductPropertyDO#getId()} + */ + private Long propertyId; + /** + * 名称 + */ + private String name; + /** + * 备注 + * + */ + private String remark; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/sku/ProductSkuDO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/sku/ProductSkuDO.java new file mode 100644 index 0000000..80e82a0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/sku/ProductSkuDO.java @@ -0,0 +1,156 @@ +package com.yunxi.scm.module.product.dal.dataobject.sku; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyValueDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * 商品 SKU DO + * + * @author 芋道源码 + */ +@TableName(value = "product_sku", autoResultMap = true) +@KeySequence("product_sku_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProductSkuDO extends BaseDO { + + /** + * 商品 SKU 编号,自增 + */ + @TableId + private Long id; + /** + * SPU 编号 + * + * 关联 {@link ProductSpuDO#getId()} + */ + private Long spuId; + /** + * 属性数组,JSON 格式 + */ + @TableField(typeHandler = PropertyTypeHandler.class) + private List properties; + /** + * 商品价格,单位:分 + */ + private Integer price; + /** + * 市场价,单位:分 + */ + private Integer marketPrice; + /** + * 成本价,单位:分 + */ + private Integer costPrice; + /** + * 商品条码 + */ + private String barCode; + /** + * 图片地址 + */ + private String picUrl; + /** + * 库存 + */ + private Integer stock; + /** + * 商品重量,单位:kg 千克 + */ + private Double weight; + /** + * 商品体积,单位:m^3 平米 + */ + private Double volume; + + /** + * 一级分销的佣金,单位:分 + */ + private Integer subCommissionFirstPrice; + /** + * 二级分销的佣金,单位:分 + */ + private Integer subCommissionSecondPrice; + + // ========== 营销相关字段 ========= + + // ========== 统计相关字段 ========= + /** + * 商品销量 + */ + private Integer salesCount; + + /** + * 商品属性 + */ + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Property { + + /** + * 属性编号 + * 关联 {@link ProductPropertyDO#getId()} + */ + private Long propertyId; + /** + * 属性名字 + * 冗余 {@link ProductPropertyDO#getName()} + * + * 注意:每次属性名字发生变化时,需要更新该冗余 + */ + private String propertyName; + + /** + * 属性值编号 + * 关联 {@link ProductPropertyValueDO#getId()} + */ + private Long valueId; + /** + * 属性值名字 + * 冗余 {@link ProductPropertyValueDO#getName()} + * + * 注意:每次属性值名字发生变化时,需要更新该冗余 + */ + private String valueName; + + } + + // TODO @芋艿:可以找一些新的思路 + public static class PropertyTypeHandler extends AbstractJsonTypeHandler { + + @Override + protected Object parse(String json) { + return JsonUtils.parseArray(json, Property.class); + } + + @Override + protected String toJson(Object obj) { + return JsonUtils.toJsonString(obj); + } + + } + + // TODO 芋艿:integral from y + // TODO 芋艿:pinkPrice from y + // TODO 芋艿:seckillPrice from y + // TODO 芋艿:pinkStock from y + // TODO 芋艿:seckillStock from y + +} + diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/spu/ProductSpuDO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/spu/ProductSpuDO.java new file mode 100644 index 0000000..4ff4864 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/dataobject/spu/ProductSpuDO.java @@ -0,0 +1,212 @@ +package com.yunxi.scm.module.product.dal.dataobject.spu; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.product.dal.dataobject.brand.ProductBrandDO; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * 商品 SPU DO + * + * @author 芋道源码 + */ +@TableName(value = "product_spu", autoResultMap = true) +@KeySequence("product_spu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ProductSpuDO extends BaseDO { + + /** + * 商品 SPU 编号,自增 + */ + @TableId + private Long id; + + // ========== 基本信息 ========= + + /** + * 商品名称 + */ + private String name; + /** + * 关键字 + */ + private String keyword; + /** + * 商品简介 + */ + private String introduction; + /** + * 商品详情 + */ + private String description; + // TODO @芋艿:是不是要删除 + /** + * 商品条码(一维码) + */ + private String barCode; + + /** + * 商品分类编号 + * + * 关联 {@link ProductCategoryDO#getId()} + */ + private Long categoryId; + /** + * 商品品牌编号 + * + * 关联 {@link ProductBrandDO#getId()} + */ + private Long brandId; + /** + * 商品封面图 + */ + private String picUrl; + /** + * 商品轮播图 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List sliderPicUrls; + /** + * 商品视频 + */ + private String videoUrl; + + /** + * 单位 + * + * 对应 product_unit 数据字典 + */ + private Integer unit; + /** + * 排序字段 + */ + private Integer sort; + /** + * 商品状态 + * + * 枚举 {@link ProductSpuStatusEnum} + */ + private Integer status; + + // ========== SKU 相关字段 ========= + + /** + * 规格类型 + * + * false - 单规格 + * true - 多规格 + */ + private Boolean specType; + /** + * 商品价格,单位使用:分 + * + * 基于其对应的 {@link ProductSkuDO#getPrice()} sku单价最低的商品的 + */ + private Integer price; + /** + * 市场价,单位使用:分 + * + * 基于其对应的 {@link ProductSkuDO#getMarketPrice()} sku单价最低的商品的 + */ + private Integer marketPrice; + /** + * 成本价,单位使用:分 + * + * 基于其对应的 {@link ProductSkuDO#getCostPrice()} sku单价最低的商品的 + */ + private Integer costPrice; + /** + * 库存 + * + * 基于其对应的 {@link ProductSkuDO#getStock()} 求和 + */ + private Integer stock; + + // ========== 物流相关字段 ========= + + /** + * 物流配置模板编号 + * + * 对应 TradeDeliveryExpressTemplateDO 的 id 编号 + */ + private Long deliveryTemplateId; + + // ========== 营销相关字段 ========= + /** + * 是否热卖推荐 + */ + private Boolean recommendHot; + /** + * 是否优惠推荐 + */ + private Boolean recommendBenefit; + /** + * 是否精品推荐 + */ + private Boolean recommendBest; + /** + * 是否新品推荐 + */ + private Boolean recommendNew; + /** + * 是否优品推荐 + */ + private Boolean recommendGood; + + /** + * 赠送积分 + */ + private Integer giveIntegral; + /** + * 赠送的优惠劵编号的数组 + * + * 对应 CouponTemplateDO 的 id 属性 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List giveCouponTemplateIds; + + /** + * 分销类型 + * + * false - 默认 + * true - 自行设置 + */ + private Boolean subCommissionType; + + /** + * 活动展示顺序 + * + * 对应 PromotionTypeEnum 枚举 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List activityOrders; + + // ========== 统计相关字段 ========= + + /** + * 商品销量 + */ + private Integer salesCount; + /** + * 虚拟销量 + */ + private Integer virtualSalesCount; + /** + * 浏览量 + */ + private Integer browseCount; +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/brand/ProductBrandMapper.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/brand/ProductBrandMapper.java new file mode 100644 index 0000000..a6eda3e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/brand/ProductBrandMapper.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.product.dal.mysql.brand; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandListReqVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandPageReqVO; +import com.yunxi.scm.module.product.dal.dataobject.brand.ProductBrandDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ProductBrandMapper extends BaseMapperX { + + default PageResult selectPage(ProductBrandPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(ProductBrandDO::getName, reqVO.getName()) + .eqIfPresent(ProductBrandDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(ProductBrandDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(ProductBrandDO::getId)); + } + + + default List selectList(ProductBrandListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(ProductBrandDO::getName, reqVO.getName())); + } + + default ProductBrandDO selectByName(String name) { + return selectOne(ProductBrandDO::getName, name); + } + + default List selectListByStatus(Integer status) { + return selectList(ProductBrandDO::getStatus, status); + } +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/category/ProductCategoryMapper.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/category/ProductCategoryMapper.java new file mode 100644 index 0000000..cffc8fb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/category/ProductCategoryMapper.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.product.dal.mysql.category; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryListReqVO; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 商品分类 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface ProductCategoryMapper extends BaseMapperX { + + default List selectList(ProductCategoryListReqVO listReqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(ProductCategoryDO::getName, listReqVO.getName()) + .eqIfPresent(ProductCategoryDO::getParentId, listReqVO.getParentId()) + .eqIfPresent(ProductCategoryDO::getStatus, listReqVO.getStatus()) + .orderByDesc(ProductCategoryDO::getId)); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(ProductCategoryDO::getParentId, parentId); + } + + default List selectListByStatus(Integer status) { + return selectList(ProductCategoryDO::getStatus, status); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/comment/ProductCommentMapper.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/comment/ProductCommentMapper.java new file mode 100644 index 0000000..0a320ed --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/comment/ProductCommentMapper.java @@ -0,0 +1,79 @@ +package com.yunxi.scm.module.product.dal.mysql.comment; + + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentPageReqVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentPageReqVO; +import com.yunxi.scm.module.product.dal.dataobject.comment.ProductCommentDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ProductCommentMapper extends BaseMapperX { + + default PageResult selectPage(ProductCommentPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(ProductCommentDO::getUserNickname, reqVO.getUserNickname()) + .eqIfPresent(ProductCommentDO::getOrderId, reqVO.getOrderId()) + .eqIfPresent(ProductCommentDO::getSpuId, reqVO.getSpuId()) + .eqIfPresent(ProductCommentDO::getScores, reqVO.getScores()) + .betweenIfPresent(ProductCommentDO::getCreateTime, reqVO.getCreateTime()) + .likeIfPresent(ProductCommentDO::getSpuName, reqVO.getSpuName()) + .orderByDesc(ProductCommentDO::getId)); + } + + static void appendTabQuery(LambdaQueryWrapperX queryWrapper, Integer type) { + // TODO @puhui999:是不是不用 apply 拉?直接用 mybatis 的方法就好啦 + // 构建好评查询语句:好评计算 总评 >= 4 + if (ObjectUtil.equal(type, AppCommentPageReqVO.GOOD_COMMENT)) { + queryWrapper.apply("scores >= 4"); + } + // 构建中评查询语句:中评计算 总评 >= 3 且 总评 < 4 + if (ObjectUtil.equal(type, AppCommentPageReqVO.MEDIOCRE_COMMENT)) { + queryWrapper.apply("scores >=3 and scores < 4"); + } + // 构建差评查询语句:差评计算 总评 < 3 + if (ObjectUtil.equal(type, AppCommentPageReqVO.NEGATIVE_COMMENT)) { + queryWrapper.apply("scores < 3"); + } + } + + default PageResult selectPage(AppCommentPageReqVO reqVO, Boolean visible) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eqIfPresent(ProductCommentDO::getSpuId, reqVO.getSpuId()) + .eqIfPresent(ProductCommentDO::getVisible, visible); + // 构建评价查询语句 + appendTabQuery(queryWrapper, reqVO.getType()); + // 按评价时间排序最新的显示在前面 + queryWrapper.orderByDesc(ProductCommentDO::getCreateTime); + return selectPage(reqVO, queryWrapper); + } + + default ProductCommentDO selectByUserIdAndOrderItemIdAndSpuId(Long userId, Long orderItemId, Long skuId) { + return selectOne(new LambdaQueryWrapperX() + .eq(ProductCommentDO::getUserId, userId) + .eq(ProductCommentDO::getOrderItemId, orderItemId) + .eq(ProductCommentDO::getSpuId, skuId)); + } + + default Long selectCountBySpuId(Long spuId, Boolean visible, Integer type) { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .eqIfPresent(ProductCommentDO::getSpuId, spuId) + .eqIfPresent(ProductCommentDO::getVisible, visible); + // 构建评价查询语句 + appendTabQuery(queryWrapper, type); + return selectCount(queryWrapper); + } + + default PageResult selectCommentList(Long spuId, Integer count) { + // 构建分页查询条件 + return selectPage(new PageParam().setPageSize(count), new LambdaQueryWrapperX() + .eqIfPresent(ProductCommentDO::getSpuId, spuId) + .orderByDesc(ProductCommentDO::getCreateTime) + ); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/favorite/ProductFavoriteMapper.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/favorite/ProductFavoriteMapper.java new file mode 100644 index 0000000..912630f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/favorite/ProductFavoriteMapper.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.product.dal.mysql.favorite; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.product.controller.app.favorite.vo.AppFavoritePageReqVO; +import com.yunxi.scm.module.product.dal.dataobject.favorite.ProductFavoriteDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ProductFavoriteMapper extends BaseMapperX { + + default ProductFavoriteDO selectByUserIdAndSpuId(Long userId, Long spuId) { + return selectOne(ProductFavoriteDO::getUserId, userId, + ProductFavoriteDO::getSpuId, spuId); + } + + default PageResult selectPageByUserAndType(Long userId, AppFavoritePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapper() + .eq(ProductFavoriteDO::getUserId, userId) + .orderByDesc(ProductFavoriteDO::getId)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/property/ProductPropertyMapper.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/property/ProductPropertyMapper.java new file mode 100644 index 0000000..2782159 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/property/ProductPropertyMapper.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.product.dal.mysql.property; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyListReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyPageReqVO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface ProductPropertyMapper extends BaseMapperX { + + default PageResult selectPage(ProductPropertyPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(ProductPropertyDO::getName, reqVO.getName()) + .betweenIfPresent(ProductPropertyDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(ProductPropertyDO::getId)); + } + + default ProductPropertyDO selectByName(String name) { + return selectOne(ProductPropertyDO::getName, name); + } + + default List selectList(ProductPropertyListReqVO listReqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(ProductPropertyDO::getName, listReqVO.getName())); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/property/ProductPropertyValueMapper.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/property/ProductPropertyValueMapper.java new file mode 100644 index 0000000..84a558a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/property/ProductPropertyValueMapper.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.product.dal.mysql.property; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValuePageReqVO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyValueDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface ProductPropertyValueMapper extends BaseMapperX { + + default List selectListByPropertyId(Collection propertyIds) { + return selectList(new LambdaQueryWrapperX() + .inIfPresent(ProductPropertyValueDO::getPropertyId, propertyIds)); + } + + default ProductPropertyValueDO selectByName(Long propertyId, String name) { + return selectOne(new LambdaQueryWrapperX() + .eq(ProductPropertyValueDO::getPropertyId, propertyId) + .eq(ProductPropertyValueDO::getName, name)); + } + + default void deleteByPropertyId(Long propertyId) { + delete(new LambdaQueryWrapperX() + .eq(ProductPropertyValueDO::getPropertyId, propertyId)); + } + + default PageResult selectPage(ProductPropertyValuePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ProductPropertyValueDO::getPropertyId, reqVO.getPropertyId()) + .likeIfPresent(ProductPropertyValueDO::getName, reqVO.getName()) + .orderByDesc(ProductPropertyValueDO::getId)); + } + + default Integer selectCountByPropertyId(Long propertyId) { + return selectCount(ProductPropertyValueDO::getPropertyId, propertyId).intValue(); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/sku/ProductSkuMapper.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/sku/ProductSkuMapper.java new file mode 100644 index 0000000..08f9533 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/sku/ProductSkuMapper.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.product.dal.mysql.sku; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface ProductSkuMapper extends BaseMapperX { + + default List selectListBySpuId(Long spuId) { + return selectList(ProductSkuDO::getSpuId, spuId); + } + + default List selectListBySpuId(Collection spuIds) { + return selectList(ProductSkuDO::getSpuId, spuIds); + } + + default void deleteBySpuId(Long spuId) { + delete(new LambdaQueryWrapperX().eq(ProductSkuDO::getSpuId, spuId)); + } + + /** + * 更新 SKU 库存(增加) + * + * @param id 编号 + * @param incrCount 增加库存(正数) + */ + default void updateStockIncr(Long id, Integer incrCount) { + Assert.isTrue(incrCount > 0); + LambdaUpdateWrapper lambdaUpdateWrapper = new LambdaUpdateWrapper() + .setSql(" stock = stock + " + incrCount) + .eq(ProductSkuDO::getId, id); + update(null, lambdaUpdateWrapper); + } + + /** + * 更新 SKU 库存(减少) + * + * @param id 编号 + * @param incrCount 减少库存(负数) + * @return 更新条数 + */ + default int updateStockDecr(Long id, Integer incrCount) { + Assert.isTrue(incrCount < 0); + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + .setSql(" stock = stock + " + incrCount) // 负数,所以使用 + 号 + .eq(ProductSkuDO::getId, id) + .ge(ProductSkuDO::getStock, -incrCount); // cas 逻辑 + return update(null, updateWrapper); + } + + default List selectListByAlarmStock() { + return selectList(new QueryWrapper().apply("stock <= warn_stock")); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/spu/ProductSpuMapper.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/spu/ProductSpuMapper.java new file mode 100644 index 0000000..1f4bc98 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/dal/mysql/spu/ProductSpuMapper.java @@ -0,0 +1,169 @@ +package com.yunxi.scm.module.product.dal.mysql.spu; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.framework.mybatis.core.query.QueryWrapperX; +import com.yunxi.scm.module.product.controller.admin.spu.vo.ProductSpuExportReqVO; +import com.yunxi.scm.module.product.controller.admin.spu.vo.ProductSpuPageReqVO; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuPageReqVO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.enums.ProductConstants; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +@Mapper +public interface ProductSpuMapper extends BaseMapperX { + + /** + * 获取商品 SPU 分页列表数据 + * + * @param reqVO 分页请求参数 + * @return 商品 SPU 分页列表数据 + */ + default PageResult selectPage(ProductSpuPageReqVO reqVO) { + Integer tabType = reqVO.getTabType(); + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX() + .likeIfPresent(ProductSpuDO::getName, reqVO.getName()) + .eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId()) + .betweenIfPresent(ProductSpuDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(ProductSpuDO::getSort); + appendTabQuery(tabType, queryWrapper); + return selectPage(reqVO, queryWrapper); + } + + /** + * 查询触发警戒库存的 SPU 数量 + * + * @return 触发警戒库存的 SPU 数量 + */ + default Long selectCount() { + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + // 库存小于等于警戒库存 + queryWrapper.le(ProductSpuDO::getStock, ProductConstants.ALERT_STOCK) + // 如果库存触发警戒库存且状态为回收站的话则不计入触发警戒库存的个数 + .notIn(ProductSpuDO::getStatus, ProductSpuStatusEnum.RECYCLE.getStatus()); + return selectCount(queryWrapper); + } + + /** + * 获得商品 SPU 分页,提供给用户 App 使用 + */ + default PageResult selectPage(AppProductSpuPageReqVO pageReqVO, Set categoryIds) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + // 关键字匹配,目前只匹配商品名 + .likeIfPresent(ProductSpuDO::getName, pageReqVO.getKeyword()) + // 分类 + .inIfPresent(ProductSpuDO::getCategoryId, categoryIds); + // 上架状态 且有库存 + query.eq(ProductSpuDO::getStatus, ProductSpuStatusEnum.ENABLE.getStatus()).gt(ProductSpuDO::getStock, 0); + // 推荐类型的过滤条件 + if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_HOT)) { + query.eq(ProductSpuDO::getRecommendHot, true); + } else if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_BENEFIT)) { + query.eq(ProductSpuDO::getRecommendBenefit, true); + } else if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_BEST)) { + query.eq(ProductSpuDO::getRecommendBest, true); + } else if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_NEW)) { + query.eq(ProductSpuDO::getRecommendNew, true); + } else if (ObjUtil.equal(pageReqVO.getRecommendType(), AppProductSpuPageReqVO.RECOMMEND_TYPE_GOOD)) { + query.eq(ProductSpuDO::getRecommendGood, true); + } + + // 排序逻辑 + if (Objects.equals(pageReqVO.getSortField(), AppProductSpuPageReqVO.SORT_FIELD_SALES_COUNT)) { + query.last(String.format(" ORDER BY (sales_count + virtual_sales_count) %s, sort DESC, id DESC", + pageReqVO.getSortAsc() ? "ASC" : "DESC")); + } else if (Objects.equals(pageReqVO.getSortField(), AppProductSpuPageReqVO.SORT_FIELD_PRICE)) { + query.orderBy(true, pageReqVO.getSortAsc(), ProductSpuDO::getPrice) + .orderByDesc(ProductSpuDO::getSort).orderByDesc(ProductSpuDO::getId); + } else { + query.orderByDesc(ProductSpuDO::getSort).orderByDesc(ProductSpuDO::getId); + } + return selectPage(pageReqVO, query); + } + + default List selectListByRecommendType(String recommendType, Integer count) { + QueryWrapperX query = new QueryWrapperX<>(); + // 上架状态 且有库存 + query.eq("status", ProductSpuStatusEnum.ENABLE.getStatus()).gt("stock", 0); + // 推荐类型的过滤条件 + if (ObjUtil.equal(recommendType, AppProductSpuPageReqVO.RECOMMEND_TYPE_HOT)) { + query.eq("recommend_hot", true); + } else if (ObjUtil.equal(recommendType, AppProductSpuPageReqVO.RECOMMEND_TYPE_GOOD)) { + query.eq("recommend_good", true); + } + // 设置最大长度 + query.limitN(count); + return selectList(query); + } + + /** + * 更新商品 SPU 库存 + * + * @param id 商品 SPU 编号 + * @param incrCount 增加的库存数量 + */ + default void updateStock(Long id, Integer incrCount) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() + // 负数,所以使用 + 号 + .setSql(" stock = stock +" + incrCount) + .eq(ProductSpuDO::getId, id); + update(null, updateWrapper); + } + + /** + * 获得 Spu 列表 + * + * @param reqVO 查询条件 + * @return Spu 列表 + */ + default List selectList(ProductSpuExportReqVO reqVO) { + Integer tabType = reqVO.getTabType(); + LambdaQueryWrapperX queryWrapper = new LambdaQueryWrapperX<>(); + queryWrapper.eqIfPresent(ProductSpuDO::getName, reqVO.getName()); + queryWrapper.eqIfPresent(ProductSpuDO::getCategoryId, reqVO.getCategoryId()); + queryWrapper.betweenIfPresent(ProductSpuDO::getCreateTime, reqVO.getCreateTime()); + appendTabQuery(tabType, queryWrapper); + return selectList(queryWrapper); + } + + /** + * 添加后台 Tab 选项的查询条件 + * + * @param tabType 标签类型 + * @param query 查询条件 + */ + static void appendTabQuery(Integer tabType, LambdaQueryWrapperX query) { + // 出售中商品 + if (ObjectUtil.equals(ProductSpuPageReqVO.FOR_SALE, tabType)) { + query.eqIfPresent(ProductSpuDO::getStatus, ProductSpuStatusEnum.ENABLE.getStatus()); + } + // 仓储中商品 + if (ObjectUtil.equals(ProductSpuPageReqVO.IN_WAREHOUSE, tabType)) { + query.eqIfPresent(ProductSpuDO::getStatus, ProductSpuStatusEnum.DISABLE.getStatus()); + } + // 已售空商品 + if (ObjectUtil.equals(ProductSpuPageReqVO.SOLD_OUT, tabType)) { + query.eqIfPresent(ProductSpuDO::getStock, 0); + } + // 警戒库存 + if (ObjectUtil.equals(ProductSpuPageReqVO.ALERT_STOCK, tabType)) { + query.le(ProductSpuDO::getStock, ProductConstants.ALERT_STOCK) + // 如果库存触发警戒库存且状态为回收站的话则不在警戒库存列表展示 + .notIn(ProductSpuDO::getStatus, ProductSpuStatusEnum.RECYCLE.getStatus()); + } + // 回收站 + if (ObjectUtil.equals(ProductSpuPageReqVO.RECYCLE_BIN, tabType)) { + query.eqIfPresent(ProductSpuDO::getStatus, ProductSpuStatusEnum.RECYCLE.getStatus()); + } + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/package-info.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/package-info.java new file mode 100644 index 0000000..f6f0026 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 product 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.product.framework; diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/web/config/ProductWebConfiguration.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/web/config/ProductWebConfiguration.java new file mode 100644 index 0000000..fa588d3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/web/config/ProductWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.product.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * product 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class ProductWebConfiguration { + + /** + * product 模块的 API 分组 + */ + @Bean + public GroupedOpenApi productGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("product"); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/web/package-info.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/web/package-info.java new file mode 100644 index 0000000..e644fd4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * product 模块的 web 配置 + */ +package com.yunxi.scm.module.product.framework.web; diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/package-info.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/package-info.java new file mode 100644 index 0000000..71ec9f1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/package-info.java @@ -0,0 +1,8 @@ +/** + * trade 模块,主要实现交易相关功能 + * 例如:订单、退款、购物车等功能。 + * + * 1. Controller URL:以 /product/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 product_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.product; diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/brand/ProductBrandService.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/brand/ProductBrandService.java new file mode 100644 index 0000000..3239167 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/brand/ProductBrandService.java @@ -0,0 +1,86 @@ +package com.yunxi.scm.module.product.service.brand; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.brand.vo.*; +import com.yunxi.scm.module.product.dal.dataobject.brand.ProductBrandDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 商品品牌 Service 接口 + * + * @author 芋道源码 + */ +public interface ProductBrandService { + + /** + * 创建品牌 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createBrand(@Valid ProductBrandCreateReqVO createReqVO); + + /** + * 更新品牌 + * + * @param updateReqVO 更新信息 + */ + void updateBrand(@Valid ProductBrandUpdateReqVO updateReqVO); + + /** + * 删除品牌 + * + * @param id 编号 + */ + void deleteBrand(Long id); + + /** + * 获得品牌 + * + * @param id 编号 + * @return 品牌 + */ + ProductBrandDO getBrand(Long id); + + /** + * 获得品牌列表 + * + * @param ids 编号 + * @return 品牌列表 + */ + List getBrandList(Collection ids); + + /** + * 获得品牌列表 + * + * @param listReqVO 请求参数 + * @return 品牌列表 + */ + List getBrandList(ProductBrandListReqVO listReqVO); + + /** + * 验证选择的商品分类是否合法 + * + * @param id 分类编号 + */ + void validateProductBrand(Long id); + + /** + * 获得品牌分页 + * + * @param pageReqVO 分页查询 + * @return 品牌分页 + */ + PageResult getBrandPage(ProductBrandPageReqVO pageReqVO); + + /** + * 获取指定状态的品牌列表 + * + * @param status 状态 + * @return 返回品牌列表 + */ + List getBrandListByStatus(Integer status); +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/brand/ProductBrandServiceImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/brand/ProductBrandServiceImpl.java new file mode 100644 index 0000000..ea79144 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/brand/ProductBrandServiceImpl.java @@ -0,0 +1,122 @@ +package com.yunxi.scm.module.product.service.brand; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandListReqVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandPageReqVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandUpdateReqVO; +import com.yunxi.scm.module.product.convert.brand.ProductBrandConvert; +import com.yunxi.scm.module.product.dal.dataobject.brand.ProductBrandDO; +import com.yunxi.scm.module.product.dal.mysql.brand.ProductBrandMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.*; + +/** + * 品牌 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ProductBrandServiceImpl implements ProductBrandService { + + @Resource + private ProductBrandMapper brandMapper; + + @Override + public Long createBrand(ProductBrandCreateReqVO createReqVO) { + // 校验 + validateBrandNameUnique(null, createReqVO.getName()); + + // 插入 + ProductBrandDO brand = ProductBrandConvert.INSTANCE.convert(createReqVO); + brandMapper.insert(brand); + // 返回 + return brand.getId(); + } + + @Override + public void updateBrand(ProductBrandUpdateReqVO updateReqVO) { + // 校验存在 + validateBrandExists(updateReqVO.getId()); + validateBrandNameUnique(updateReqVO.getId(), updateReqVO.getName()); + // 更新 + ProductBrandDO updateObj = ProductBrandConvert.INSTANCE.convert(updateReqVO); + brandMapper.updateById(updateObj); + } + + @Override + public void deleteBrand(Long id) { + // 校验存在 + validateBrandExists(id); + // 删除 + brandMapper.deleteById(id); + } + + private void validateBrandExists(Long id) { + if (brandMapper.selectById(id) == null) { + throw exception(BRAND_NOT_EXISTS); + } + } + + @VisibleForTesting + public void validateBrandNameUnique(Long id, String name) { + ProductBrandDO brand = brandMapper.selectByName(name); + if (brand == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(BRAND_NAME_EXISTS); + } + if (!brand.getId().equals(id)) { + throw exception(BRAND_NAME_EXISTS); + } + } + + @Override + public ProductBrandDO getBrand(Long id) { + return brandMapper.selectById(id); + } + + @Override + public List getBrandList(Collection ids) { + return brandMapper.selectBatchIds(ids); + } + + @Override + public List getBrandList(ProductBrandListReqVO listReqVO) { + return brandMapper.selectList(listReqVO); + } + + @Override + public void validateProductBrand(Long id) { + ProductBrandDO brand = brandMapper.selectById(id); + if (brand == null) { + throw exception(BRAND_NOT_EXISTS); + } + if (brand.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { + throw exception(BRAND_DISABLED); + } + } + + @Override + public PageResult getBrandPage(ProductBrandPageReqVO pageReqVO) { + return brandMapper.selectPage(pageReqVO); + } + + @Override + public List getBrandListByStatus(Integer status) { + return brandMapper.selectListByStatus(status); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/category/ProductCategoryService.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/category/ProductCategoryService.java new file mode 100644 index 0000000..64b936d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/category/ProductCategoryService.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.product.service.category; + +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryListReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 商品分类 Service 接口 + * + * @author 芋道源码 + */ +public interface ProductCategoryService { + + /** + * 创建商品分类 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createCategory(@Valid ProductCategoryCreateReqVO createReqVO); + + /** + * 更新商品分类 + * + * @param updateReqVO 更新信息 + */ + void updateCategory(@Valid ProductCategoryUpdateReqVO updateReqVO); + + /** + * 删除商品分类 + * + * @param id 编号 + */ + void deleteCategory(Long id); + + /** + * 获得商品分类 + * + * @param id 编号 + * @return 商品分类 + */ + ProductCategoryDO getCategory(Long id); + + /** + * 校验商品分类 + * + * @param id 分类编号 + */ + void validateCategory(Long id); + + /** + * 获得商品分类的层级 + * + * @param id 编号 + * @return 商品分类的层级 + */ + Integer getCategoryLevel(Long id); + + /** + * 获得商品分类列表 + * + * @param listReqVO 查询条件 + * @return 商品分类列表 + */ + List getEnableCategoryList(ProductCategoryListReqVO listReqVO); + + /** + * 获得开启状态的商品分类列表 + * + * @return 商品分类列表 + */ + List getEnableCategoryList(); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/category/ProductCategoryServiceImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/category/ProductCategoryServiceImpl.java new file mode 100644 index 0000000..15643ca --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/category/ProductCategoryServiceImpl.java @@ -0,0 +1,149 @@ +package com.yunxi.scm.module.product.service.category; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryListReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; +import com.yunxi.scm.module.product.convert.category.ProductCategoryConvert; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; +import com.yunxi.scm.module.product.dal.mysql.category.ProductCategoryMapper; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO.PARENT_ID_NULL; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.*; + +/** + * 商品分类 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ProductCategoryServiceImpl implements ProductCategoryService { + + @Resource + private ProductCategoryMapper productCategoryMapper; + @Resource + @Lazy // 循环依赖,避免报错 + private ProductSpuService productSpuService; + + @Override + public Long createCategory(ProductCategoryCreateReqVO createReqVO) { + // 校验父分类存在 + validateParentProductCategory(createReqVO.getParentId()); + + // 插入 + ProductCategoryDO category = ProductCategoryConvert.INSTANCE.convert(createReqVO); + productCategoryMapper.insert(category); + // 返回 + return category.getId(); + } + + @Override + public void updateCategory(ProductCategoryUpdateReqVO updateReqVO) { + // 校验分类是否存在 + validateProductCategoryExists(updateReqVO.getId()); + // 校验父分类存在 + validateParentProductCategory(updateReqVO.getParentId()); + + // 更新 + ProductCategoryDO updateObj = ProductCategoryConvert.INSTANCE.convert(updateReqVO); + productCategoryMapper.updateById(updateObj); + } + + @Override + public void deleteCategory(Long id) { + // 校验分类是否存在 + validateProductCategoryExists(id); + // 校验是否还有子分类 + if (productCategoryMapper.selectCountByParentId(id) > 0) { + throw exception(CATEGORY_EXISTS_CHILDREN); + } + // 校验分类是否绑定了 SPU + Long spuCount = productSpuService.getSpuCountByCategoryId(id); + if (spuCount > 0) { + throw exception(CATEGORY_HAVE_BIND_SPU); + } + // 删除 + productCategoryMapper.deleteById(id); + } + + private void validateParentProductCategory(Long id) { + // 如果是根分类,无需验证 + if (Objects.equals(id, PARENT_ID_NULL)) { + return; + } + // 父分类不存在 + ProductCategoryDO category = productCategoryMapper.selectById(id); + if (category == null) { + throw exception(CATEGORY_PARENT_NOT_EXISTS); + } + // 父分类不能是二级分类 + if (!Objects.equals(category.getParentId(), PARENT_ID_NULL)) { + throw exception(CATEGORY_PARENT_NOT_FIRST_LEVEL); + } + } + + private void validateProductCategoryExists(Long id) { + ProductCategoryDO category = productCategoryMapper.selectById(id); + if (category == null) { + throw exception(CATEGORY_NOT_EXISTS); + } + } + + @Override + public ProductCategoryDO getCategory(Long id) { + return productCategoryMapper.selectById(id); + } + + @Override + public void validateCategory(Long id) { + ProductCategoryDO category = productCategoryMapper.selectById(id); + if (category == null) { + throw exception(CATEGORY_NOT_EXISTS); + } + if (Objects.equals(category.getStatus(), CommonStatusEnum.DISABLE.getStatus())) { + throw exception(CATEGORY_DISABLED, category.getName()); + } + } + + @Override + public Integer getCategoryLevel(Long id) { + if (Objects.equals(id, PARENT_ID_NULL)) { + return 0; + } + int level = 1; + // for 的原因,是因为避免脏数据,导致可能的死循环。一般不会超过 100 层哈 + for (int i = 0; i < Byte.MAX_VALUE; i++) { + // 如果没有父节点,break 结束 + ProductCategoryDO category = productCategoryMapper.selectById(id); + if (category == null + || Objects.equals(category.getParentId(), PARENT_ID_NULL)) { + break; + } + // 继续递归父节点 + level++; + id = category.getParentId(); + } + return level; + } + + @Override + public List getEnableCategoryList(ProductCategoryListReqVO listReqVO) { + return productCategoryMapper.selectList(listReqVO); + } + + @Override + public List getEnableCategoryList() { + return productCategoryMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/comment/ProductCommentService.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/comment/ProductCommentService.java new file mode 100644 index 0000000..3a4837c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/comment/ProductCommentService.java @@ -0,0 +1,94 @@ +package com.yunxi.scm.module.product.service.comment; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentPageReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentReplyReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentUpdateVisibleReqVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentPageReqVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentStatisticsRespVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppProductCommentRespVO; +import com.yunxi.scm.module.product.dal.dataobject.comment.ProductCommentDO; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import java.util.List; + +/** + * 商品评论 Service 接口 + * + * @author wangzhs + */ +@Service +@Validated +public interface ProductCommentService { + + /** + * 获得商品评价分页 + * + * @param pageReqVO 分页查询 + * @return 商品评价分页 + */ + PageResult getCommentPage(ProductCommentPageReqVO pageReqVO); + + /** + * 修改评论是否可见 + * + * @param updateReqVO 修改评论可见 + */ + void updateCommentVisible(ProductCommentUpdateVisibleReqVO updateReqVO); + + /** + * 商家回复 + * + * @param replyVO 商家回复 + * @param loginUserId 管理后台商家登陆人 ID + */ + void replyComment(ProductCommentReplyReqVO replyVO, Long loginUserId); + + /** + * 获得商品评价分页 + * + * @param pageVO 分页查询 + * @param visible 是否可见 + * @return 商品评价分页 + */ + PageResult getCommentPage(AppCommentPageReqVO pageVO, Boolean visible); + + /** + * 创建商品评论 + * 后台管理员创建评论使用 + * + * @param createReqVO 商品评价创建 Request VO 对象 + */ + void createComment(ProductCommentCreateReqVO createReqVO); + + /** + * 创建评论 + * 创建商品评论 APP 端创建商品评论使用 + * + * @param createReqDTO 创建请求 dto + * @return 返回评论 id + */ + Long createComment(ProductCommentCreateReqDTO createReqDTO); + + /** + * 获得商品的评价统计 + * + * @param spuId spu id + * @param visible 是否可见 + * @return 评价统计 + */ + AppCommentStatisticsRespVO getCommentStatistics(Long spuId, Boolean visible); + + /** + * 得到评论列表 + * + * @param spuId 商品 id + * @param count 数量 + * @return {@link Object} + */ + List getCommentList(Long spuId, Integer count); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/comment/ProductCommentServiceImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/comment/ProductCommentServiceImpl.java new file mode 100644 index 0000000..949e056 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/comment/ProductCommentServiceImpl.java @@ -0,0 +1,162 @@ +package com.yunxi.scm.module.product.service.comment; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.api.user.MemberUserApi; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentPageReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentReplyReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentUpdateVisibleReqVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentPageReqVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentStatisticsRespVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppProductCommentRespVO; +import com.yunxi.scm.module.product.convert.comment.ProductCommentConvert; +import com.yunxi.scm.module.product.dal.dataobject.comment.ProductCommentDO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.dal.mysql.comment.ProductCommentMapper; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.*; + +/** + * 商品评论 Service 实现类 + * + * @author wangzhs + */ +@Service +@Validated +public class ProductCommentServiceImpl implements ProductCommentService { + + @Resource + private ProductCommentMapper productCommentMapper; + + @Resource + private ProductSpuService productSpuService; + + @Resource + @Lazy + private ProductSkuService productSkuService; + + @Resource + private MemberUserApi memberUserApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateCommentVisible(ProductCommentUpdateVisibleReqVO updateReqVO) { + // 校验评论是否存在 + ProductCommentDO productCommentDO = validateCommentExists(updateReqVO.getId()); + productCommentDO.setVisible(updateReqVO.getVisible()); + + // 更新可见状态 + productCommentMapper.updateById(productCommentDO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void replyComment(ProductCommentReplyReqVO replyVO, Long loginUserId) { + // 校验评论是否存在 + ProductCommentDO productCommentDO = validateCommentExists(replyVO.getId()); + productCommentDO.setReplyTime(LocalDateTime.now()); + productCommentDO.setReplyUserId(loginUserId); + productCommentDO.setReplyStatus(Boolean.TRUE); + productCommentDO.setReplyContent(replyVO.getReplyContent()); + + // 回复评论 + productCommentMapper.updateById(productCommentDO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void createComment(ProductCommentCreateReqVO createReqVO) { + // 校验评论 + validateComment(createReqVO.getSkuId(), createReqVO.getUserId(), createReqVO.getOrderItemId()); + + ProductCommentDO commentDO = ProductCommentConvert.INSTANCE.convert(createReqVO); + productCommentMapper.insert(commentDO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createComment(ProductCommentCreateReqDTO createReqDTO) { + // 通过 sku ID 拿到 spu 相关信息 + ProductSkuDO sku = productSkuService.getSku(createReqDTO.getSkuId()); + if (sku == null) { + throw exception(SKU_NOT_EXISTS); + } + // 校验 spu 如果存在返回详情 + ProductSpuDO spuDO = validateSpu(sku.getSpuId()); + // 校验评论 + validateComment(spuDO.getId(), createReqDTO.getUserId(), createReqDTO.getOrderId()); + // 获取用户详细信息 + MemberUserRespDTO user = memberUserApi.getUser(createReqDTO.getUserId()); + + // 创建评论 + ProductCommentDO commentDO = ProductCommentConvert.INSTANCE.convert(createReqDTO, spuDO, user); + productCommentMapper.insert(commentDO); + return commentDO.getId(); + } + + private void validateComment(Long skuId, Long userId, Long orderItemId) { + // 判断当前订单的当前商品用户是否评价过 + ProductCommentDO exist = productCommentMapper.selectByUserIdAndOrderItemIdAndSpuId(userId, orderItemId, skuId); + if (null != exist) { + throw exception(COMMENT_ORDER_EXISTS); + } + } + + private ProductSpuDO validateSpu(Long spuId) { + ProductSpuDO spu = productSpuService.getSpu(spuId); + if (null == spu) { + throw exception(SPU_NOT_EXISTS); + } + return spu; + } + + private ProductCommentDO validateCommentExists(Long id) { + ProductCommentDO productComment = productCommentMapper.selectById(id); + if (productComment == null) { + throw exception(COMMENT_NOT_EXISTS); + } + return productComment; + } + + @Override + public AppCommentStatisticsRespVO getCommentStatistics(Long spuId, Boolean visible) { + return ProductCommentConvert.INSTANCE.convert( + // 查询商品 id = spuId 的所有好评数量 + productCommentMapper.selectCountBySpuId(spuId, visible, AppCommentPageReqVO.GOOD_COMMENT), + // 查询商品 id = spuId 的所有中评数量 + productCommentMapper.selectCountBySpuId(spuId, visible, AppCommentPageReqVO.MEDIOCRE_COMMENT), + // 查询商品 id = spuId 的所有差评数量 + productCommentMapper.selectCountBySpuId(spuId, visible, AppCommentPageReqVO.NEGATIVE_COMMENT) + ); + } + + @Override + public List getCommentList(Long spuId, Integer count) { + return ProductCommentConvert.INSTANCE.convertList02(productCommentMapper.selectCommentList(spuId, count).getList()); + } + + @Override + public PageResult getCommentPage(AppCommentPageReqVO pageVO, Boolean visible) { + return productCommentMapper.selectPage(pageVO, visible); + } + + @Override + public PageResult getCommentPage(ProductCommentPageReqVO pageReqVO) { + return productCommentMapper.selectPage(pageReqVO); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/favorite/ProductFavoriteService.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/favorite/ProductFavoriteService.java new file mode 100644 index 0000000..5c9d038 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/favorite/ProductFavoriteService.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.product.service.favorite; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.app.favorite.vo.AppFavoritePageReqVO; +import com.yunxi.scm.module.product.dal.dataobject.favorite.ProductFavoriteDO; + +import javax.validation.Valid; + +/** + * 商品收藏 Service 接口 + * + * @author jason + */ +public interface ProductFavoriteService { + + /** + * 创建商品收藏 + * + * @param userId 用户 id + * @param spuId SPU 编号 + */ + Long createFavorite(Long userId, Long spuId); + + /** + * 取消商品收藏 + * + * @param userId 用户 id + * @param spuId SPU 编号 + */ + void deleteFavorite(Long userId, Long spuId); + + /** + * 分页查询用户收藏列表 + * + * @param userId 用户 id + * @param reqVO 请求 vo + */ + PageResult getFavoritePage(Long userId, @Valid AppFavoritePageReqVO reqVO); + + /** + * 获取收藏过商品 + * + * @param userId 用户id + * @param spuId SPU 编号 + */ + ProductFavoriteDO getFavorite(Long userId, Long spuId); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/favorite/ProductFavoriteServiceImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/favorite/ProductFavoriteServiceImpl.java new file mode 100644 index 0000000..ae59c34 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/favorite/ProductFavoriteServiceImpl.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.product.service.favorite; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.app.favorite.vo.AppFavoritePageReqVO; +import com.yunxi.scm.module.product.convert.favorite.ProductFavoriteConvert; +import com.yunxi.scm.module.product.dal.dataobject.favorite.ProductFavoriteDO; +import com.yunxi.scm.module.product.dal.mysql.favorite.ProductFavoriteMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.FAVORITE_EXISTS; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.FAVORITE_NOT_EXISTS; + +/** + * 商品收藏 Service 实现类 + * + * @author jason + */ +@Service +@Validated +public class ProductFavoriteServiceImpl implements ProductFavoriteService { + + @Resource + private ProductFavoriteMapper productFavoriteMapper; + + @Override + public Long createFavorite(Long userId, Long spuId) { + ProductFavoriteDO favorite = productFavoriteMapper.selectByUserIdAndSpuId(userId, spuId); + if (Objects.nonNull(favorite)) { + throw exception(FAVORITE_EXISTS); + } + + ProductFavoriteDO entity = ProductFavoriteConvert.INSTANCE.convert(userId, spuId); + productFavoriteMapper.insert(entity); + return entity.getId(); + } + + @Override + public void deleteFavorite(Long userId, Long spuId) { + ProductFavoriteDO favorite = productFavoriteMapper.selectByUserIdAndSpuId(userId, spuId); + if (Objects.isNull(favorite)) { + throw exception(FAVORITE_NOT_EXISTS); + } + + productFavoriteMapper.deleteById(favorite.getId()); + } + + @Override + public PageResult getFavoritePage(Long userId, @Valid AppFavoritePageReqVO reqVO) { + return productFavoriteMapper.selectPageByUserAndType(userId, reqVO); + } + + @Override + public ProductFavoriteDO getFavorite(Long userId, Long spuId) { + return productFavoriteMapper.selectByUserIdAndSpuId(userId, spuId); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyService.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyService.java new file mode 100644 index 0000000..f5e7732 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyService.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.module.product.service.property; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.*; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 商品属性项 Service 接口 + * + * @author 芋道源码 + */ +public interface ProductPropertyService { + + /** + * 创建属性项 + * 注意,如果已经存在该属性项,直接返回它的编号即可 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createProperty(@Valid ProductPropertyCreateReqVO createReqVO); + + /** + * 更新属性项 + * + * @param updateReqVO 更新信息 + */ + void updateProperty(@Valid ProductPropertyUpdateReqVO updateReqVO); + + /** + * 删除属性项 + * + * @param id 编号 + */ + void deleteProperty(Long id); + + /** + * 获得属性项列表 + * + * @param listReqVO 集合查询 + * @return 属性项集合 + */ + List getPropertyList(ProductPropertyListReqVO listReqVO); + + /** + * 获取属性名称分页 + * + * @param pageReqVO 分页条件 + * @return 属性项分页 + */ + PageResult getPropertyPage(ProductPropertyPageReqVO pageReqVO); + + /** + * 获得指定编号的属性项 + * + * @param id 编号 + * @return 属性项 + */ + ProductPropertyDO getProperty(Long id); + + /** + * 根据属性项的编号的集合,获得对应的属性项数组 + * + * @param ids 属性项的编号的集合 + * @return 属性项数组 + */ + List getPropertyList(Collection ids); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyServiceImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyServiceImpl.java new file mode 100644 index 0000000..7f04da9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyServiceImpl.java @@ -0,0 +1,119 @@ +package com.yunxi.scm.module.product.service.property; + +import cn.hutool.core.util.ObjUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyListReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyPageReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.property.ProductPropertyUpdateReqVO; +import com.yunxi.scm.module.product.convert.property.ProductPropertyConvert; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; +import com.yunxi.scm.module.product.dal.mysql.property.ProductPropertyMapper; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.*; + +/** + * 商品属性项 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ProductPropertyServiceImpl implements ProductPropertyService { + + @Resource + private ProductPropertyMapper productPropertyMapper; + + @Resource + @Lazy // 延迟加载,解决循环依赖问题 + private ProductPropertyValueService productPropertyValueService; + + @Resource + private ProductSkuService productSkuService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createProperty(ProductPropertyCreateReqVO createReqVO) { + // 如果已经添加过该属性项,直接返回 + ProductPropertyDO dbProperty = productPropertyMapper.selectByName(createReqVO.getName()); + if (dbProperty != null) { + return dbProperty.getId(); + } + + // 插入 + ProductPropertyDO property = ProductPropertyConvert.INSTANCE.convert(createReqVO); + productPropertyMapper.insert(property); + // 返回 + return property.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateProperty(ProductPropertyUpdateReqVO updateReqVO) { + validatePropertyExists(updateReqVO.getId()); + // 校验名字重复 + ProductPropertyDO productPropertyDO = productPropertyMapper.selectByName(updateReqVO.getName()); + if (productPropertyDO != null && + ObjUtil.notEqual(productPropertyDO.getId(), updateReqVO.getId())) { + throw exception(PROPERTY_EXISTS); + } + + // 更新 + ProductPropertyDO updateObj = ProductPropertyConvert.INSTANCE.convert(updateReqVO); + productPropertyMapper.updateById(updateObj); + // 更新 sku 相关属性 + productSkuService.updateSkuProperty(updateObj.getId(), updateObj.getName()); + } + + @Override + public void deleteProperty(Long id) { + // 校验存在 + validatePropertyExists(id); + // 校验其下是否有规格值 + if (productPropertyValueService.getPropertyValueCountByPropertyId(id) > 0) { + throw exception(PROPERTY_DELETE_FAIL_VALUE_EXISTS); + } + + // 删除 + productPropertyMapper.deleteById(id); + // 同步删除属性值 + productPropertyValueService.deletePropertyValueByPropertyId(id); + } + + private void validatePropertyExists(Long id) { + if (productPropertyMapper.selectById(id) == null) { + throw exception(PROPERTY_NOT_EXISTS); + } + } + + @Override + public List getPropertyList(ProductPropertyListReqVO listReqVO) { + return productPropertyMapper.selectList(listReqVO); + } + + @Override + public PageResult getPropertyPage(ProductPropertyPageReqVO pageReqVO) { + return productPropertyMapper.selectPage(pageReqVO); + } + + @Override + public ProductPropertyDO getProperty(Long id) { + return productPropertyMapper.selectById(id); + } + + @Override + public List getPropertyList(Collection ids) { + return productPropertyMapper.selectBatchIds(ids); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyValueService.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyValueService.java new file mode 100644 index 0000000..723761f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyValueService.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.product.service.property; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValuePageReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyValueDO; +import com.yunxi.scm.module.product.service.property.bo.ProductPropertyValueDetailRespBO; + +import java.util.Collection; +import java.util.List; + +/** + * 商品属性值 Service 接口 + * + * @author LuoWenFeng + */ +public interface ProductPropertyValueService { + + /** + * 创建属性值 + * 注意,如果已经存在该属性值,直接返回它的编号即可 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createPropertyValue(ProductPropertyValueCreateReqVO createReqVO); + + /** + * 更新属性值 + * + * @param updateReqVO 更新信息 + */ + void updatePropertyValue(ProductPropertyValueUpdateReqVO updateReqVO); + + /** + * 删除属性值 + * + * @param id 编号 + */ + void deletePropertyValue(Long id); + + /** + * 获得属性值 + * + * @param id 编号 + * @return 属性值 + */ + ProductPropertyValueDO getPropertyValue(Long id); + + /** + * 根据属性项编号数组,获得属性值列表 + * + * @param propertyIds 属性项目编号数组 + * @return 属性值列表 + */ + List getPropertyValueListByPropertyId(Collection propertyIds); + + /** + * 根据编号数组,获得属性值列表 + * + * @param ids 编号数组 + * @return 属性值明细列表 + */ + List getPropertyValueDetailList(Collection ids); + + /** + * 根据属性项编号,活的属性值数量 + * + * @param propertyId 属性项编号数 + * @return 属性值数量 + */ + Integer getPropertyValueCountByPropertyId(Long propertyId); + + /** + * 获取属性值的分页 + * + * @param pageReqVO 查询条件 + * @return 属性值的分页 + */ + PageResult getPropertyValuePage(ProductPropertyValuePageReqVO pageReqVO); + + /** + * 删除指定属性项编号下的属性值们 + * + * @param propertyId 属性项的编号 + */ + void deletePropertyValueByPropertyId(Long propertyId); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyValueServiceImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyValueServiceImpl.java new file mode 100644 index 0000000..30802c7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/ProductPropertyValueServiceImpl.java @@ -0,0 +1,134 @@ +package com.yunxi.scm.module.product.service.property; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValuePageReqVO; +import com.yunxi.scm.module.product.controller.admin.property.vo.value.ProductPropertyValueUpdateReqVO; +import com.yunxi.scm.module.product.convert.propertyvalue.ProductPropertyValueConvert; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyValueDO; +import com.yunxi.scm.module.product.dal.mysql.property.ProductPropertyValueMapper; +import com.yunxi.scm.module.product.service.property.bo.ProductPropertyValueDetailRespBO; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.PROPERTY_VALUE_EXISTS; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.PROPERTY_VALUE_NOT_EXISTS; + +/** + * 商品属性值 Service 实现类 + * + * @author LuoWenFeng + */ +@Service +@Validated +public class ProductPropertyValueServiceImpl implements ProductPropertyValueService { + + @Resource + private ProductPropertyValueMapper productPropertyValueMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private ProductPropertyService productPropertyService; + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private ProductSkuService productSkuService; + + @Override + public Long createPropertyValue(ProductPropertyValueCreateReqVO createReqVO) { + // 如果已经添加过该属性值,直接返回 + ProductPropertyValueDO dbValue = productPropertyValueMapper.selectByName( + createReqVO.getPropertyId(), createReqVO.getName()); + if (dbValue != null) { + return dbValue.getId(); + } + + // 新增 + ProductPropertyValueDO value = ProductPropertyValueConvert.INSTANCE.convert(createReqVO); + productPropertyValueMapper.insert(value); + return value.getId(); + } + + @Override + public void updatePropertyValue(ProductPropertyValueUpdateReqVO updateReqVO) { + validatePropertyValueExists(updateReqVO.getId()); + // 校验名字唯一 + ProductPropertyValueDO productPropertyValueDO = productPropertyValueMapper.selectByName + (updateReqVO.getPropertyId(), updateReqVO.getName()); + if (productPropertyValueDO != null && !productPropertyValueDO.getId().equals(updateReqVO.getId())) { + throw exception(PROPERTY_VALUE_EXISTS); + } + + // 更新 + ProductPropertyValueDO updateObj = ProductPropertyValueConvert.INSTANCE.convert(updateReqVO); + productPropertyValueMapper.updateById(updateObj); + // 更新 sku 相关属性 + productSkuService.updateSkuPropertyValue(updateObj.getId(), updateObj.getName()); + } + + @Override + public void deletePropertyValue(Long id) { + validatePropertyValueExists(id); + productPropertyValueMapper.deleteById(id); + } + + private void validatePropertyValueExists(Long id) { + if (productPropertyValueMapper.selectById(id) == null) { + throw exception(PROPERTY_VALUE_NOT_EXISTS); + } + } + + @Override + public ProductPropertyValueDO getPropertyValue(Long id) { + return productPropertyValueMapper.selectById(id); + } + + @Override + public List getPropertyValueListByPropertyId(Collection propertyIds) { + return productPropertyValueMapper.selectListByPropertyId(propertyIds); + } + + @Override + public List getPropertyValueDetailList(Collection ids) { + // 获得属性值列表 + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + List values = productPropertyValueMapper.selectBatchIds(ids); + if (CollUtil.isEmpty(values)) { + return Collections.emptyList(); + } + // 获得属性项列表 + List keys = productPropertyService.getPropertyList( + convertSet(values, ProductPropertyValueDO::getPropertyId)); + // 组装明细 + return ProductPropertyValueConvert.INSTANCE.convertList(values, keys); + } + + @Override + public Integer getPropertyValueCountByPropertyId(Long propertyId) { + return productPropertyValueMapper.selectCountByPropertyId(propertyId); + } + + @Override + public PageResult getPropertyValuePage(ProductPropertyValuePageReqVO pageReqVO) { + return productPropertyValueMapper.selectPage(pageReqVO); + } + + @Override + public void deletePropertyValueByPropertyId(Long propertyId) { + productPropertyValueMapper.deleteByPropertyId(propertyId); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/bo/ProductPropertyValueDetailRespBO.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/bo/ProductPropertyValueDetailRespBO.java new file mode 100644 index 0000000..3e20650 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/property/bo/ProductPropertyValueDetailRespBO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.product.service.property.bo; + +import lombok.Data; + +/** + * 商品属性项的明细 Response BO + * + * @author 芋道源码 + */ +@Data +public class ProductPropertyValueDetailRespBO { + + /** + * 属性的编号 + */ + private Long propertyId; + + /** + * 属性的名称 + */ + private String propertyName; + + /** + * 属性值的编号 + */ + private Long valueId; + + /** + * 属性值的名称 + */ + private String valueName; + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/sku/ProductSkuService.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/sku/ProductSkuService.java new file mode 100644 index 0000000..0873240 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/sku/ProductSkuService.java @@ -0,0 +1,127 @@ +package com.yunxi.scm.module.product.service.sku; + +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; + +import java.util.Collection; +import java.util.List; + +/** + * 商品 SKU Service 接口 + * + * @author 芋道源码 + */ +public interface ProductSkuService { + + /** + * 删除商品 SKU + * + * @param id 编号 + */ + void deleteSku(Long id); + + /** + * 获得商品 SKU 信息 + * + * @param id 编号 + * @return 商品 SKU 信息 + */ + ProductSkuDO getSku(Long id); + + /** + * 获得商品 SKU 列表 + * + * @return 商品sku列表 + */ + List getSkuList(); + + /** + * 获得商品 SKU 列表 + * + * @param ids 编号 + * @return 商品sku列表 + */ + List getSkuList(Collection ids); + + /** + * 对 sku 的组合的属性等进行合法性校验 + * + * @param list sku组合的集合 + */ + void validateSkuList(List list, Boolean specType); + + /** + * 批量创建 SKU + * + * @param spuId 商品 SPU 编号 + * @param list SKU 对象集合 + */ + void createSkuList(Long spuId, List list); + + /** + * 根据 SPU 编号,批量更新它的 SKU 信息 + * + * @param spuId SPU 编码 + * @param skus SKU 的集合 + */ + void updateSkuList(Long spuId, List skus); + + /** + * 更新 SKU 库存(增量) + *

+ * 如果更新的库存不足,会抛出异常 + * + * @param updateStockReqDTO 更行请求 + */ + void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO); + + /** + * 获得商品 SKU 集合 + * + * @param spuId spu 编号 + * @return 商品sku 集合 + */ + List getSkuListBySpuId(Long spuId); + + /** + * 获得 spu 对应的 SKU 集合 + * + * @param spuIds spu 编码集合 + * @return 商品 sku 集合 + */ + List getSkuListBySpuId(Collection spuIds); + + /** + * 通过 spuId 删除 sku 信息 + * + * @param spuId spu 编码 + */ + void deleteSkuBySpuId(Long spuId); + + /** + * 获得库存预警的 SKU 数组 + * + * @return SKU 数组 + */ + List getSkuListByAlarmStock(); + + /** + * 更新 sku 属性 + * + * @param propertyId 属性 id + * @param propertyName 属性名 + * @return int 影响的行数 + */ + int updateSkuProperty(Long propertyId, String propertyName); + + /** + * 更新 sku 属性值 + * + * @param propertyValueId 属性值 id + * @param propertyValueName 属性值名字 + * @return int 影响的行数 + */ + int updateSkuPropertyValue(Long propertyValueId, String propertyValueName); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/sku/ProductSkuServiceImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/sku/ProductSkuServiceImpl.java new file mode 100644 index 0000000..5e1471e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/sku/ProductSkuServiceImpl.java @@ -0,0 +1,274 @@ +package com.yunxi.scm.module.product.service.sku; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuBaseVO; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; +import com.yunxi.scm.module.product.convert.sku.ProductSkuConvert; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyDO; +import com.yunxi.scm.module.product.dal.dataobject.property.ProductPropertyValueDO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.mysql.sku.ProductSkuMapper; +import com.yunxi.scm.module.product.service.property.ProductPropertyService; +import com.yunxi.scm.module.product.service.property.ProductPropertyValueService; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.*; +import java.util.stream.Collectors; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.*; + +/** + * 商品 SKU Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ProductSkuServiceImpl implements ProductSkuService { + + @Resource + private ProductSkuMapper productSkuMapper; + + @Resource + @Lazy // 循环依赖,避免报错 + private ProductSpuService productSpuService; + @Resource + @Lazy // 循环依赖,避免报错 + private ProductPropertyService productPropertyService; + @Resource + private ProductPropertyValueService productPropertyValueService; + + @Override + public void deleteSku(Long id) { + // 校验存在 + validateSkuExists(id); + // 删除 + productSkuMapper.deleteById(id); + } + + private void validateSkuExists(Long id) { + if (productSkuMapper.selectById(id) == null) { + throw exception(SKU_NOT_EXISTS); + } + } + + @Override + public ProductSkuDO getSku(Long id) { + return productSkuMapper.selectById(id); + } + + @Override + public List getSkuList() { + return productSkuMapper.selectList(); + } + + @Override + public List getSkuList(Collection ids) { + return productSkuMapper.selectBatchIds(ids); + } + + @Override + public void validateSkuList(List skus, Boolean specType) { + // 0、校验skus是否为空 + if (CollUtil.isEmpty(skus)) { + throw exception(SKU_NOT_EXISTS); + } + // 单规格,赋予单规格默认属性 + if (ObjectUtil.equal(specType, false)) { + ProductSkuCreateOrUpdateReqVO skuVO = skus.get(0); + List properties = new ArrayList<>(); + ProductSkuBaseVO.Property property = new ProductSkuBaseVO.Property(); + property.setPropertyId(ProductPropertyDO.ID_DEFAULT); + property.setPropertyName(ProductPropertyDO.NAME_DEFAULT); + property.setValueId(ProductPropertyValueDO.ID_DEFAULT); + property.setValueName(ProductPropertyValueDO.NAME_DEFAULT); + properties.add(property); + skuVO.setProperties(properties); + return; // 单规格不需要后续的校验 + } + + // 1、校验属性项存在 + Set propertyIds = skus.stream().filter(p -> p.getProperties() != null) + // 遍历多个 Property 属性 + .flatMap(p -> p.getProperties().stream()) + // 将每个 Property 转换成对应的 propertyId,最后形成集合 + .map(ProductSkuCreateOrUpdateReqVO.Property::getPropertyId) + .collect(Collectors.toSet()); + List propertyList = productPropertyService.getPropertyList(propertyIds); + if (propertyList.size() != propertyIds.size()) { + throw exception(PROPERTY_NOT_EXISTS); + } + + // 2. 校验,一个 SKU 下,没有重复的属性。校验方式是,遍历每个 SKU ,看看是否有重复的属性 propertyId + Map propertyValueMap = convertMap(productPropertyValueService.getPropertyValueListByPropertyId(propertyIds), ProductPropertyValueDO::getId); + skus.forEach(sku -> { + Set skuPropertyIds = convertSet(sku.getProperties(), propertyItem -> propertyValueMap.get(propertyItem.getValueId()).getPropertyId()); + if (skuPropertyIds.size() != sku.getProperties().size()) { + throw exception(SKU_PROPERTIES_DUPLICATED); + } + }); + + // 3. 再校验,每个 Sku 的属性值的数量,是一致的。 + int attrValueIdsSize = skus.get(0).getProperties().size(); + for (int i = 1; i < skus.size(); i++) { + if (attrValueIdsSize != skus.get(i).getProperties().size()) { + throw exception(SPU_ATTR_NUMBERS_MUST_BE_EQUALS); + } + } + + // 4. 最后校验,每个 Sku 之间不是重复的 + // 每个元素,都是一个 Sku 的 attrValueId 集合。这样,通过最外层的 Set ,判断是否有重复的. + Set> skuAttrValues = new HashSet<>(); + for (ProductSkuCreateOrUpdateReqVO sku : skus) { + // 添加失败,说明重复 + if (!skuAttrValues.add(convertSet(sku.getProperties(), ProductSkuCreateOrUpdateReqVO.Property::getValueId))) { + throw exception(SPU_SKU_NOT_DUPLICATE); + } + } + } + + @Override + public void createSkuList(Long spuId, List skuCreateReqList) { + productSkuMapper.insertBatch(ProductSkuConvert.INSTANCE.convertList06(skuCreateReqList, spuId)); + } + + @Override + public List getSkuListBySpuId(Long spuId) { + return productSkuMapper.selectListBySpuId(spuId); + } + + @Override + public List getSkuListBySpuId(Collection spuIds) { + return productSkuMapper.selectListBySpuId(spuIds); + } + + @Override + public void deleteSkuBySpuId(Long spuId) { + productSkuMapper.deleteBySpuId(spuId); + } + + @Override + public List getSkuListByAlarmStock() { + return productSkuMapper.selectListByAlarmStock(); + } + + @Override + public int updateSkuProperty(Long propertyId, String propertyName) { + // 获取所有的 sku + List skuDOList = productSkuMapper.selectList(); + // 处理后需要更新的 sku + List updateSkus = new ArrayList<>(); + if (CollUtil.isEmpty(skuDOList)) { + return 0; + } + skuDOList.stream().filter(sku -> sku.getProperties() != null) + .forEach(sku -> sku.getProperties().forEach(property -> { + if (property.getPropertyId().equals(propertyId)) { + property.setPropertyName(propertyName); + updateSkus.add(sku); + } + })); + if (CollUtil.isEmpty(updateSkus)) { + return 0; + } + + productSkuMapper.updateBatch(updateSkus); + return updateSkus.size(); + } + + @Override + public int updateSkuPropertyValue(Long propertyValueId, String propertyValueName) { + // 获取所有的 sku + List skuDOList = productSkuMapper.selectList(); + // 处理后需要更新的 sku + List updateSkus = new ArrayList<>(); + if (CollUtil.isEmpty(skuDOList)) { + return 0; + } + skuDOList.stream() + .filter(sku -> sku.getProperties() != null) + .forEach(sku -> sku.getProperties().forEach(property -> { + if (property.getValueId().equals(propertyValueId)) { + property.setValueName(propertyValueName); + updateSkus.add(sku); + } + })); + if (CollUtil.isEmpty(updateSkus)) { + return 0; + } + + productSkuMapper.updateBatch(updateSkus); + return updateSkus.size(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateSkuList(Long spuId, List skus) { + // 构建属性与 SKU 的映射关系; + Map existsSkuMap = convertMap(productSkuMapper.selectListBySpuId(spuId), + ProductSkuConvert.INSTANCE::buildPropertyKey, ProductSkuDO::getId); + + // 拆分三个集合,新插入的、需要更新的、需要删除的 + List insertSkus = new ArrayList<>(); + List updateSkus = new ArrayList<>(); + List allUpdateSkus = ProductSkuConvert.INSTANCE.convertList06(skus, spuId); + allUpdateSkus.forEach(sku -> { + String propertiesKey = ProductSkuConvert.INSTANCE.buildPropertyKey(sku); + // 1、找得到的,进行更新 + Long existsSkuId = existsSkuMap.remove(propertiesKey); + if (existsSkuId != null) { + sku.setId(existsSkuId); + updateSkus.add(sku); + return; + } + // 2、找不到,进行插入 + sku.setSpuId(spuId); + insertSkus.add(sku); + }); + + // 执行最终的批量操作 + if (CollUtil.isNotEmpty(insertSkus)) { + productSkuMapper.insertBatch(insertSkus); + } + if (CollUtil.isNotEmpty(updateSkus)) { + updateSkus.forEach(sku -> productSkuMapper.updateById(sku)); + } + if (CollUtil.isNotEmpty(existsSkuMap)) { + productSkuMapper.deleteBatchIds(existsSkuMap.values()); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateSkuStock(ProductSkuUpdateStockReqDTO updateStockReqDTO) { + // 更新 SKU 库存 + updateStockReqDTO.getItems().forEach(item -> { + if (item.getIncrCount() > 0) { + productSkuMapper.updateStockIncr(item.getId(), item.getIncrCount()); + } else if (item.getIncrCount() < 0) { + int updateStockIncr = productSkuMapper.updateStockDecr(item.getId(), item.getIncrCount()); + if (updateStockIncr == 0) { + throw exception(SKU_STOCK_NOT_ENOUGH); + } + } + }); + + // 更新 SPU 库存 + List skus = productSkuMapper.selectBatchIds( + convertSet(updateStockReqDTO.getItems(), ProductSkuUpdateStockReqDTO.Item::getId)); + Map spuStockIncrCounts = ProductSkuConvert.INSTANCE.convertSpuStockMap( + updateStockReqDTO.getItems(), skus); + productSpuService.updateSpuStock(spuStockIncrCounts); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/spu/ProductSpuService.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/spu/ProductSpuService.java new file mode 100644 index 0000000..af3e878 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/spu/ProductSpuService.java @@ -0,0 +1,139 @@ +package com.yunxi.scm.module.product.service.spu; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.controller.admin.spu.vo.*; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuPageReqVO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 商品 SPU Service 接口 + * + * @author 芋道源码 + */ +public interface ProductSpuService { + + /** + * 创建商品 SPU + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSpu(@Valid ProductSpuCreateReqVO createReqVO); + + /** + * 更新商品 SPU + * + * @param updateReqVO 更新信息 + */ + void updateSpu(@Valid ProductSpuUpdateReqVO updateReqVO); + + /** + * 删除商品 SPU + * + * @param id 编号 + */ + void deleteSpu(Long id); + + /** + * 获得商品 SPU + * + * @param id 编号 + * @return 商品 SPU + */ + ProductSpuDO getSpu(Long id); + + /** + * 获得商品 SPU 列表 + * + * @param ids 编号数组 + * @return 商品 SPU 列表 + */ + List getSpuList(Collection ids); + + /** + * 获得商品 SPU 映射 + * + * @param ids 编号数组 + * @return 商品 SPU 映射 + */ + default Map getSpuMap(Collection ids) { + return convertMap(getSpuList(ids), ProductSpuDO::getId); + } + + /** + * 获得所有商品 SPU 列表 + * + * @return 商品 SPU 列表 + */ + List getSpuList(); + + /** + * 获得所有商品 SPU 列表 + * + * @param reqVO 导出条件 + * @return 商品 SPU 列表 + */ + List getSpuList(ProductSpuExportReqVO reqVO); + + /** + * 获得商品 SPU 分页,提供给挂你兰后台使用 + * + * @param pageReqVO 分页查询 + * @return 商品spu分页 + */ + PageResult getSpuPage(ProductSpuPageReqVO pageReqVO); + + /** + * 获得商品 SPU 分页,提供给用户 App 使用 + * + * @param pageReqVO 分页查询 + * @return 商品 SPU 分页 + */ + PageResult getSpuPage(AppProductSpuPageReqVO pageReqVO); + + /** + * 获得商品 SPU 列表,提供给用户 App 使用 + * + * @param recommendType 推荐类型 + * @param count 数量 + * @return 商品 SPU 列表 + */ + List getSpuList(String recommendType, Integer count); + + /** + * 更新商品 SPU 库存(增量) + * + * @param stockIncrCounts SPU 编号与库存变化(增量)的映射 + */ + void updateSpuStock(Map stockIncrCounts); + + /** + * 更新 SPU 状态 + * + * @param updateReqVO 更新请求 + */ + void updateSpuStatus(ProductSpuUpdateStatusReqVO updateReqVO); + + /** + * 获取 SPU 列表标签对应的 Count 数量 + * + * @return Count 数量 + */ + Map getTabsCount(); + + /** + * 通过分类 categoryId 查询 SPU 个数 + * + * @param categoryId 分类 categoryId + * @return SPU 数量 + */ + Long getSpuCountByCategoryId(Long categoryId); + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/spu/ProductSpuServiceImpl.java b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/spu/ProductSpuServiceImpl.java new file mode 100644 index 0000000..65689fd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/main/java/com/yunxi/scm/module/product/service/spu/ProductSpuServiceImpl.java @@ -0,0 +1,252 @@ +package com.yunxi.scm.module.product.service.spu; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryListReqVO; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; +import com.yunxi.scm.module.product.controller.admin.spu.vo.*; +import com.yunxi.scm.module.product.controller.app.spu.vo.AppProductSpuPageReqVO; +import com.yunxi.scm.module.product.convert.spu.ProductSpuConvert; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.dal.mysql.spu.ProductSpuMapper; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.yunxi.scm.module.product.service.brand.ProductBrandService; +import com.yunxi.scm.module.product.service.category.ProductCategoryService; +import com.yunxi.scm.module.product.service.property.ProductPropertyValueService; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import com.google.common.collect.Maps; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.getMinValue; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.getSumValue; +import static com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO.CATEGORY_LEVEL; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.*; + +/** + * 商品 SPU Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class ProductSpuServiceImpl implements ProductSpuService { + + @Resource + private ProductSpuMapper productSpuMapper; + + @Resource + @Lazy // 循环依赖,避免报错 + private ProductSkuService productSkuService; + @Resource + private ProductBrandService brandService; + @Resource + private ProductCategoryService categoryService; + @Resource + @Lazy // 循环依赖,避免报错 + private ProductPropertyValueService productPropertyValueService; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createSpu(ProductSpuCreateReqVO createReqVO) { + // 校验分类、品牌 + validateCategory(createReqVO.getCategoryId()); + brandService.validateProductBrand(createReqVO.getBrandId()); + // 校验 SKU + List skuSaveReqList = createReqVO.getSkus(); + productSkuService.validateSkuList(skuSaveReqList, createReqVO.getSpecType()); + + ProductSpuDO spu = ProductSpuConvert.INSTANCE.convert(createReqVO); + // 初始化 SPU 中 SKU 相关属性 + initSpuFromSkus(spu, skuSaveReqList); + // 插入 SPU + productSpuMapper.insert(spu); + // 插入 SKU + productSkuService.createSkuList(spu.getId(), skuSaveReqList); + // 返回 + return spu.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateSpu(ProductSpuUpdateReqVO updateReqVO) { + // 校验 SPU 是否存在 + validateSpuExists(updateReqVO.getId()); + // 校验分类、品牌 + validateCategory(updateReqVO.getCategoryId()); + brandService.validateProductBrand(updateReqVO.getBrandId()); + // 校验SKU + List skuSaveReqList = updateReqVO.getSkus(); + productSkuService.validateSkuList(skuSaveReqList, updateReqVO.getSpecType()); + + // 更新 SPU + ProductSpuDO updateObj = ProductSpuConvert.INSTANCE.convert(updateReqVO); + initSpuFromSkus(updateObj, skuSaveReqList); + productSpuMapper.updateById(updateObj); + // 批量更新 SKU + productSkuService.updateSkuList(updateObj.getId(), updateReqVO.getSkus()); + } + + /** + * 基于 SKU 的信息,初始化 SPU 的信息 + * 主要是计数相关的字段,例如说市场价、最大最小价、库存等等 + * + * @param spu 商品 SPU + * @param skus 商品 SKU 数组 + */ + private void initSpuFromSkus(ProductSpuDO spu, List skus) { + // sku 单价最低的商品的价格 + spu.setPrice(getMinValue(skus, ProductSkuCreateOrUpdateReqVO::getPrice)); + // sku 单价最低的商品的市场价格 + spu.setMarketPrice(getMinValue(skus, ProductSkuCreateOrUpdateReqVO::getMarketPrice)); + // sku 单价最低的商品的成本价格 + spu.setCostPrice(getMinValue(skus, ProductSkuCreateOrUpdateReqVO::getCostPrice)); + // sku 单价最低的商品的条形码 + spu.setBarCode(getMinValue(skus, ProductSkuCreateOrUpdateReqVO::getBarCode)); + // skus 库存总数 + spu.setStock(getSumValue(skus, ProductSkuCreateOrUpdateReqVO::getStock, Integer::sum)); + // 若是 spu 已有状态则不处理 + if (spu.getStatus() == null) { + // 默认状态为上架 + spu.setStatus(ProductSpuStatusEnum.ENABLE.getStatus()); + // 默认商品销量 + spu.setSalesCount(0); + // 默认商品浏览量 + spu.setBrowseCount(0); + } + } + + /** + * 校验商品分类是否合法 + * + * @param id 商品分类编号 + */ + private void validateCategory(Long id) { + categoryService.validateCategory(id); + // 校验层级 + if (categoryService.getCategoryLevel(id) < CATEGORY_LEVEL) { + throw exception(SPU_SAVE_FAIL_CATEGORY_LEVEL_ERROR); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteSpu(Long id) { + // 校验存在 + validateSpuExists(id); + // 校验商品状态不是回收站不能删除 + ProductSpuDO spuDO = productSpuMapper.selectById(id); + // 判断 SPU 状态是否为回收站 + if (ObjectUtil.notEqual(spuDO.getStatus(), ProductSpuStatusEnum.RECYCLE.getStatus())) { + throw exception(SPU_NOT_RECYCLE); + } + + // 删除 SPU + productSpuMapper.deleteById(id); + // 删除关联的 SKU + productSkuService.deleteSkuBySpuId(id); + } + + private void validateSpuExists(Long id) { + if (productSpuMapper.selectById(id) == null) { + throw exception(SPU_NOT_EXISTS); + } + } + + @Override + public ProductSpuDO getSpu(Long id) { + return productSpuMapper.selectById(id); + } + + @Override + public List getSpuList(Collection ids) { + return productSpuMapper.selectBatchIds(ids); + } + + @Override + public List getSpuList() { + return productSpuMapper.selectList(); + } + + @Override + public List getSpuList(ProductSpuExportReqVO reqVO) { + return productSpuMapper.selectList(reqVO); + } + + @Override + public PageResult getSpuPage(ProductSpuPageReqVO pageReqVO) { + return productSpuMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getSpuPage(AppProductSpuPageReqVO pageReqVO) { + // 查找时,如果查找某个分类编号,则包含它的子分类。因为顶级分类不包含商品 + Set categoryIds = new HashSet<>(); + if (pageReqVO.getCategoryId() != null && pageReqVO.getCategoryId() > 0) { + categoryIds.add(pageReqVO.getCategoryId()); + List categoryChildren = categoryService.getEnableCategoryList(new ProductCategoryListReqVO() + .setParentId(pageReqVO.getCategoryId()).setStatus(CommonStatusEnum.ENABLE.getStatus())); + categoryIds.addAll(CollectionUtils.convertList(categoryChildren, ProductCategoryDO::getId)); + } + // 分页查询 + return productSpuMapper.selectPage(pageReqVO, categoryIds); + } + + @Override + public List getSpuList(String recommendType, Integer count) { + return productSpuMapper.selectListByRecommendType(recommendType, count); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateSpuStock(Map stockIncrCounts) { + stockIncrCounts.forEach((id, incCount) -> productSpuMapper.updateStock(id, incCount)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateSpuStatus(ProductSpuUpdateStatusReqVO updateReqVO) { + // 校验存在 + validateSpuExists(updateReqVO.getId()); + + // 更新状态 + ProductSpuDO productSpuDO = productSpuMapper.selectById(updateReqVO.getId()).setStatus(updateReqVO.getStatus()); + productSpuMapper.updateById(productSpuDO); + } + + @Override + public Map getTabsCount() { + Map counts = Maps.newLinkedHashMapWithExpectedSize(5); + // 查询销售中的商品数量 + counts.put(ProductSpuPageReqVO.FOR_SALE, + productSpuMapper.selectCount(ProductSpuDO::getStatus, ProductSpuStatusEnum.ENABLE.getStatus())); + // 查询仓库中的商品数量 + counts.put(ProductSpuPageReqVO.IN_WAREHOUSE, + productSpuMapper.selectCount(ProductSpuDO::getStatus, ProductSpuStatusEnum.DISABLE.getStatus())); + // 查询售空的商品数量 + counts.put(ProductSpuPageReqVO.SOLD_OUT, + productSpuMapper.selectCount(ProductSpuDO::getStock, 0)); + // 查询触发警戒库存的商品数量 + counts.put(ProductSpuPageReqVO.ALERT_STOCK, + productSpuMapper.selectCount()); + // 查询回收站中的商品数量 + counts.put(ProductSpuPageReqVO.RECYCLE_BIN, + productSpuMapper.selectCount(ProductSpuDO::getStatus, ProductSpuStatusEnum.RECYCLE.getStatus())); + return counts; + } + + @Override + public Long getSpuCountByCategoryId(Long categoryId) { + return productSpuMapper.selectCount(ProductSpuDO::getCategoryId, categoryId); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/brand/ProductBrandServiceImplTest.java b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/brand/ProductBrandServiceImplTest.java new file mode 100644 index 0000000..6c4315b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/brand/ProductBrandServiceImplTest.java @@ -0,0 +1,133 @@ +package com.yunxi.scm.module.product.service.brand; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandPageReqVO; +import com.yunxi.scm.module.product.controller.admin.brand.vo.ProductBrandUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.brand.ProductBrandDO; +import com.yunxi.scm.module.product.dal.mysql.brand.ProductBrandMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.BRAND_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** +* {@link ProductBrandServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(ProductBrandServiceImpl.class) +public class ProductBrandServiceImplTest extends BaseDbUnitTest { + + @Resource + private ProductBrandServiceImpl brandService; + + @Resource + private ProductBrandMapper brandMapper; + + @Test + public void testCreateBrand_success() { + // 准备参数 + ProductBrandCreateReqVO reqVO = randomPojo(ProductBrandCreateReqVO.class); + + // 调用 + Long brandId = brandService.createBrand(reqVO); + // 断言 + assertNotNull(brandId); + // 校验记录的属性是否正确 + ProductBrandDO brand = brandMapper.selectById(brandId); + assertPojoEquals(reqVO, brand); + } + + @Test + public void testUpdateBrand_success() { + // mock 数据 + ProductBrandDO dbBrand = randomPojo(ProductBrandDO.class); + brandMapper.insert(dbBrand);// @Sql: 先插入出一条存在的数据 + // 准备参数 + ProductBrandUpdateReqVO reqVO = randomPojo(ProductBrandUpdateReqVO.class, o -> { + o.setId(dbBrand.getId()); // 设置更新的 ID + }); + + // 调用 + brandService.updateBrand(reqVO); + // 校验是否更新正确 + ProductBrandDO brand = brandMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, brand); + } + + @Test + public void testUpdateBrand_notExists() { + // 准备参数 + ProductBrandUpdateReqVO reqVO = randomPojo(ProductBrandUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> brandService.updateBrand(reqVO), BRAND_NOT_EXISTS); + } + + @Test + public void testDeleteBrand_success() { + // mock 数据 + ProductBrandDO dbBrand = randomPojo(ProductBrandDO.class); + brandMapper.insert(dbBrand);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbBrand.getId(); + + // 调用 + brandService.deleteBrand(id); + // 校验数据不存在了 + assertNull(brandMapper.selectById(id)); + } + + @Test + public void testDeleteBrand_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> brandService.deleteBrand(id), BRAND_NOT_EXISTS); + } + + @Test + public void testGetBrandPage() { + // mock 数据 + ProductBrandDO dbBrand = randomPojo(ProductBrandDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2022, 2, 1)); + }); + brandMapper.insert(dbBrand); + // 测试 name 不匹配 + brandMapper.insert(cloneIgnoreId(dbBrand, o -> o.setName("源码"))); + // 测试 status 不匹配 + brandMapper.insert(cloneIgnoreId(dbBrand, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + brandMapper.insert(cloneIgnoreId(dbBrand, o -> o.setCreateTime(buildTime(2022, 3, 1)))); + // 准备参数 + ProductBrandPageReqVO reqVO = new ProductBrandPageReqVO(); + reqVO.setName("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2022, 1, 1), buildTime(2022, 2, 25)})); + + // 调用 + PageResult pageResult = brandService.getBrandPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbBrand, pageResult.getList().get(0)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/category/ProductCategoryServiceImplTest.java b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/category/ProductCategoryServiceImplTest.java new file mode 100644 index 0000000..44a5028 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/category/ProductCategoryServiceImplTest.java @@ -0,0 +1,161 @@ +package com.yunxi.scm.module.product.service.category; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryListReqVO; +import com.yunxi.scm.module.product.controller.admin.category.vo.ProductCategoryUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO; +import com.yunxi.scm.module.product.dal.mysql.category.ProductCategoryMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.product.dal.dataobject.category.ProductCategoryDO.PARENT_ID_NULL; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.CATEGORY_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link ProductCategoryServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(ProductCategoryServiceImpl.class) +public class ProductCategoryServiceImplTest extends BaseDbUnitTest { + + @Resource + private ProductCategoryServiceImpl productCategoryService; + + @Resource + private ProductCategoryMapper productCategoryMapper; + + @Test + public void testCreateCategory_success() { + // 准备参数 + ProductCategoryCreateReqVO reqVO = randomPojo(ProductCategoryCreateReqVO.class); + + // mock 父类 + ProductCategoryDO parentProductCategory = randomPojo(ProductCategoryDO.class, o -> { + reqVO.setParentId(o.getId()); + o.setParentId(PARENT_ID_NULL); + }); + productCategoryMapper.insert(parentProductCategory); + + // 调用 + Long categoryId = productCategoryService.createCategory(reqVO); + // 断言 + assertNotNull(categoryId); + // 校验记录的属性是否正确 + ProductCategoryDO category = productCategoryMapper.selectById(categoryId); + assertPojoEquals(reqVO, category); + } + + @Test + public void testUpdateCategory_success() { + // mock 数据 + ProductCategoryDO dbCategory = randomPojo(ProductCategoryDO.class); + productCategoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据 + // 准备参数 + ProductCategoryUpdateReqVO reqVO = randomPojo(ProductCategoryUpdateReqVO.class, o -> { + o.setId(dbCategory.getId()); // 设置更新的 ID + }); + // mock 父类 + ProductCategoryDO parentProductCategory = randomPojo(ProductCategoryDO.class, o -> o.setId(reqVO.getParentId())); + productCategoryMapper.insert(parentProductCategory); + + // 调用 + productCategoryService.updateCategory(reqVO); + // 校验是否更新正确 + ProductCategoryDO category = productCategoryMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, category); + } + + @Test + public void testUpdateCategory_notExists() { + // 准备参数 + ProductCategoryUpdateReqVO reqVO = randomPojo(ProductCategoryUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> productCategoryService.updateCategory(reqVO), CATEGORY_NOT_EXISTS); + } + + @Test + public void testDeleteCategory_success() { + // mock 数据 + ProductCategoryDO dbCategory = randomPojo(ProductCategoryDO.class); + productCategoryMapper.insert(dbCategory);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbCategory.getId(); + + // 调用 + productCategoryService.deleteCategory(id); + // 校验数据不存在了 + assertNull(productCategoryMapper.selectById(id)); + } + + @Test + public void testDeleteCategory_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> productCategoryService.deleteCategory(id), CATEGORY_NOT_EXISTS); + } + + @Test + public void testGetCategoryLevel() { + // mock 数据 + ProductCategoryDO category1 = randomPojo(ProductCategoryDO.class, + o -> o.setParentId(PARENT_ID_NULL)); + productCategoryMapper.insert(category1); + ProductCategoryDO category2 = randomPojo(ProductCategoryDO.class, + o -> o.setParentId(category1.getId())); + productCategoryMapper.insert(category2); + ProductCategoryDO category3 = randomPojo(ProductCategoryDO.class, + o -> o.setParentId(category2.getId())); + productCategoryMapper.insert(category3); + + // 调用,并断言 + assertEquals(productCategoryService.getCategoryLevel(category1.getId()), 1); + assertEquals(productCategoryService.getCategoryLevel(category2.getId()), 2); + assertEquals(productCategoryService.getCategoryLevel(category3.getId()), 3); + } + + @Test + public void testGetCategoryList() { + // mock 数据 + ProductCategoryDO dbCategory = randomPojo(ProductCategoryDO.class, o -> { // 等会查询到 + o.setName("奥特曼"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setParentId(PARENT_ID_NULL); + }); + productCategoryMapper.insert(dbCategory); + // 测试 name 不匹配 + productCategoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName("奥特块"))); + // 测试 status 不匹配 + productCategoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 parentId 不匹配 + productCategoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setParentId(3333L))); + // 准备参数 + ProductCategoryListReqVO reqVO = new ProductCategoryListReqVO(); + reqVO.setName("特曼"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setParentId(PARENT_ID_NULL); + + // 调用 + List list = productCategoryService.getEnableCategoryList(reqVO); + List all = productCategoryService.getEnableCategoryList(new ProductCategoryListReqVO()); + // 断言 + assertEquals(1, list.size()); + assertEquals(4, all.size()); + assertPojoEquals(dbCategory, list.get(0)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/comment/ProductCommentServiceImplTest.java b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/comment/ProductCommentServiceImplTest.java new file mode 100644 index 0000000..b4da4d6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/comment/ProductCommentServiceImplTest.java @@ -0,0 +1,196 @@ +package com.yunxi.scm.module.product.service.comment; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentPageReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentReplyReqVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentRespVO; +import com.yunxi.scm.module.product.controller.admin.comment.vo.ProductCommentUpdateVisibleReqVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentPageReqVO; +import com.yunxi.scm.module.product.controller.app.comment.vo.AppCommentStatisticsRespVO; +import com.yunxi.scm.module.product.convert.comment.ProductCommentConvert; +import com.yunxi.scm.module.product.dal.dataobject.comment.ProductCommentDO; +import com.yunxi.scm.module.product.dal.mysql.comment.ProductCommentMapper; +import com.yunxi.scm.module.product.enums.comment.ProductCommentScoresEnum; +import com.yunxi.scm.module.product.service.sku.ProductSkuService; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import com.yunxi.scm.module.trade.api.order.TradeOrderApi; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.Lazy; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Date; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +// TODO 芋艿:单测详细 review 下 +/** + * {@link ProductCommentServiceImpl} 的单元测试类 + * + * @author wangzhs + */ +@Import(ProductCommentServiceImpl.class) +public class ProductCommentServiceImplTest extends BaseDbUnitTest { + + @Resource + private ProductCommentMapper productCommentMapper; + + @Resource + @Lazy + private ProductCommentServiceImpl productCommentService; + + @MockBean + private TradeOrderApi tradeOrderApi; + @MockBean + private ProductSpuService productSpuService; + @MockBean + private ProductSkuService productSkuService; + + public String generateNo() { + return DateUtil.format(new Date(), "yyyyMMddHHmmss") + RandomUtil.randomInt(100000, 999999); + } + + public Long generateId() { + return RandomUtil.randomLong(100000, 999999); + } + + @Test + public void testCreateCommentAndGet_success() { + // mock 测试 + ProductCommentDO productComment = randomPojo(ProductCommentDO.class); + productCommentMapper.insert(productComment); + + // 断言 + // 校验记录的属性是否正确 + ProductCommentDO comment = productCommentMapper.selectById(productComment.getId()); + assertPojoEquals(productComment, comment); + } + + @Test + public void testGetCommentPage_success() { + // 准备参数 + ProductCommentDO productComment = randomPojo(ProductCommentDO.class, o -> { + o.setUserNickname("王二狗"); + o.setSpuName("感冒药"); + o.setScores(ProductCommentScoresEnum.FOUR.getScores()); + o.setReplyStatus(Boolean.TRUE); + o.setVisible(Boolean.TRUE); + o.setId(generateId()); + o.setUserId(generateId()); + o.setAnonymous(Boolean.TRUE); + o.setOrderId(generateId()); + o.setOrderItemId(generateId()); + o.setSpuId(generateId()); + o.setSkuId(generateId()); + o.setDescriptionScores(ProductCommentScoresEnum.FOUR.getScores()); + o.setBenefitScores(ProductCommentScoresEnum.FOUR.getScores()); + o.setContent("真好吃"); + o.setReplyUserId(generateId()); + o.setReplyContent("确实"); + o.setReplyTime(LocalDateTime.now()); + o.setCreateTime(LocalDateTime.now()); + o.setUpdateTime(LocalDateTime.now()); + }); + productCommentMapper.insert(productComment); + + Long orderId = productComment.getOrderId(); + Long spuId = productComment.getSpuId(); + + // 测试 userNickname 不匹配 + productCommentMapper.insert(cloneIgnoreId(productComment, o -> o.setUserNickname("王三").setScores(ProductCommentScoresEnum.ONE.getScores()))); + // 测试 orderId 不匹配 + productCommentMapper.insert(cloneIgnoreId(productComment, o -> o.setOrderId(generateId()))); + // 测试 spuId 不匹配 + productCommentMapper.insert(cloneIgnoreId(productComment, o -> o.setSpuId(generateId()))); + // 测试 spuName 不匹配 + productCommentMapper.insert(cloneIgnoreId(productComment, o -> o.setSpuName("感康"))); + // 测试 scores 不匹配 + productCommentMapper.insert(cloneIgnoreId(productComment, o -> o.setScores(ProductCommentScoresEnum.ONE.getScores()))); + // 测试 replied 不匹配 + productCommentMapper.insert(cloneIgnoreId(productComment, o -> o.setReplyStatus(Boolean.FALSE))); + // 测试 visible 不匹配 + productCommentMapper.insert(cloneIgnoreId(productComment, o -> o.setVisible(Boolean.FALSE))); + + // 调用 + ProductCommentPageReqVO productCommentPageReqVO = new ProductCommentPageReqVO(); + productCommentPageReqVO.setUserNickname("王二"); + productCommentPageReqVO.setOrderId(orderId); + productCommentPageReqVO.setSpuId(spuId); + productCommentPageReqVO.setSpuName("感冒药"); + productCommentPageReqVO.setScores(ProductCommentScoresEnum.FOUR.getScores()); + productCommentPageReqVO.setReplyStatus(Boolean.TRUE); + + PageResult commentPage = productCommentService.getCommentPage(productCommentPageReqVO); + PageResult result = ProductCommentConvert.INSTANCE.convertPage(productCommentMapper.selectPage(productCommentPageReqVO)); + assertEquals(result.getTotal(), commentPage.getTotal()); + + PageResult all = productCommentService.getCommentPage(new ProductCommentPageReqVO()); + assertEquals(8, all.getTotal()); + + // 测试获取所有商品分页评论数据 + PageResult result1 = productCommentService.getCommentPage(new AppCommentPageReqVO(), Boolean.TRUE); + assertEquals(7, result1.getTotal()); + + // 测试获取所有商品分页中评数据 + PageResult result2 = productCommentService.getCommentPage(new AppCommentPageReqVO().setType(AppCommentPageReqVO.MEDIOCRE_COMMENT), Boolean.TRUE); + assertEquals(2, result2.getTotal()); + + // 测试获取指定 spuId 商品分页中评数据 + PageResult result3 = productCommentService.getCommentPage(new AppCommentPageReqVO().setSpuId(spuId).setType(AppCommentPageReqVO.MEDIOCRE_COMMENT), Boolean.TRUE); + assertEquals(2, result3.getTotal()); + + // 测试分页 tab count + AppCommentStatisticsRespVO tabsCount = productCommentService.getCommentStatistics(spuId, Boolean.TRUE); + assertEquals(4, tabsCount.getGoodCount()); + assertEquals(2, tabsCount.getMediocreCount()); + assertEquals(0, tabsCount.getNegativeCount()); + + } + + @Test + public void testUpdateCommentVisible_success() { + // mock 测试 + ProductCommentDO productComment = randomPojo(ProductCommentDO.class, o -> { + o.setVisible(Boolean.TRUE); + }); + productCommentMapper.insert(productComment); + + Long productCommentId = productComment.getId(); + + ProductCommentUpdateVisibleReqVO updateReqVO = new ProductCommentUpdateVisibleReqVO(); + updateReqVO.setId(productCommentId); + updateReqVO.setVisible(Boolean.FALSE); + productCommentService.updateCommentVisible(updateReqVO); + + ProductCommentDO productCommentDO = productCommentMapper.selectById(productCommentId); + assertFalse(productCommentDO.getVisible()); + } + + + @Test + public void testCommentReply_success() { + // mock 测试 + ProductCommentDO productComment = randomPojo(ProductCommentDO.class); + productCommentMapper.insert(productComment); + + Long productCommentId = productComment.getId(); + + ProductCommentReplyReqVO replyVO = new ProductCommentReplyReqVO(); + replyVO.setId(productCommentId); + replyVO.setReplyContent("测试"); + productCommentService.replyComment(replyVO, 1L); + + ProductCommentDO productCommentDO = productCommentMapper.selectById(productCommentId); + assertEquals("测试", productCommentDO.getReplyContent()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/sku/ProductSkuServiceTest.java b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/sku/ProductSkuServiceTest.java new file mode 100644 index 0000000..ee97ffb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/sku/ProductSkuServiceTest.java @@ -0,0 +1,205 @@ +package com.yunxi.scm.module.product.service.sku; + +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.framework.test.core.util.AssertUtils; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; +import com.yunxi.scm.module.product.dal.dataobject.sku.ProductSkuDO; +import com.yunxi.scm.module.product.dal.mysql.sku.ProductSkuMapper; +import com.yunxi.scm.module.product.service.property.ProductPropertyService; +import com.yunxi.scm.module.product.service.property.ProductPropertyValueService; +import com.yunxi.scm.module.product.service.spu.ProductSpuService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; + +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.verify; + +/** + * {@link ProductSkuServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +@Import(ProductSkuServiceImpl.class) +public class ProductSkuServiceTest extends BaseDbUnitTest { + + @Resource + private ProductSkuService productSkuService; + + @Resource + private ProductSkuMapper productSkuMapper; + + @MockBean + private ProductSpuService productSpuService; + @MockBean + private ProductPropertyService productPropertyService; + @MockBean + private ProductPropertyValueService productPropertyValueService; + + public Long generateId() { + return RandomUtil.randomLong(100000, 999999); + } + + @Test + public void testUpdateSkuList() { + // mock 数据 + ProductSkuDO sku01 = randomPojo(ProductSkuDO.class, o -> { // 测试更新 + o.setSpuId(1L); + o.setProperties(singletonList(new ProductSkuDO.Property( + 10L, "颜色", 20L, "红色"))); + }); + productSkuMapper.insert(sku01); + ProductSkuDO sku02 = randomPojo(ProductSkuDO.class, o -> { // 测试删除 + o.setSpuId(1L); + o.setProperties(singletonList(new ProductSkuDO.Property( + 10L, "颜色", 30L, "蓝色"))); + + }); + productSkuMapper.insert(sku02); + // 准备参数 + Long spuId = 1L; + String spuName = "测试商品"; + List skus = Arrays.asList( + randomPojo(ProductSkuCreateOrUpdateReqVO.class, o -> { // 测试更新 + o.setProperties(singletonList(new ProductSkuCreateOrUpdateReqVO.Property( + 10L, "颜色", 20L, "红色"))); + }), + randomPojo(ProductSkuCreateOrUpdateReqVO.class, o -> { // 测试新增 + o.setProperties(singletonList(new ProductSkuCreateOrUpdateReqVO.Property( + 10L, "颜色", 20L, "红色"))); + }) + ); + + // 调用 + productSkuService.updateSkuList(spuId, skus); + // 断言 + List dbSkus = productSkuMapper.selectListBySpuId(spuId); + assertEquals(dbSkus.size(), 2); + // 断言更新的 + assertEquals(dbSkus.get(0).getId(), sku01.getId()); + assertPojoEquals(dbSkus.get(0), skus.get(0), "properties"); + assertEquals(skus.get(0).getProperties().size(), 1); + assertPojoEquals(dbSkus.get(0).getProperties().get(0), skus.get(0).getProperties().get(0)); + // 断言新增的 + assertNotEquals(dbSkus.get(1).getId(), sku02.getId()); + assertPojoEquals(dbSkus.get(1), skus.get(1), "properties"); + assertEquals(skus.get(1).getProperties().size(), 1); + assertPojoEquals(dbSkus.get(1).getProperties().get(0), skus.get(1).getProperties().get(0)); + } + + @Test + public void testUpdateSkuStock_incrSuccess() { + // 准备参数 + ProductSkuUpdateStockReqDTO updateStockReqDTO = new ProductSkuUpdateStockReqDTO() + .setItems(singletonList(new ProductSkuUpdateStockReqDTO.Item().setId(1L).setIncrCount(10))); + // mock 数据 + productSkuMapper.insert(randomPojo(ProductSkuDO.class, o -> { + o.setId(1L).setSpuId(10L).setStock(20); + o.getProperties().forEach(p -> { + // 指定 id 范围 解决 Value too long + p.setPropertyId(generateId()); + p.setValueId(generateId()); + }); + })); + + // 调用 + productSkuService.updateSkuStock(updateStockReqDTO); + // 断言 + ProductSkuDO sku = productSkuMapper.selectById(1L); + assertEquals(sku.getStock(), 30); + verify(productSpuService).updateSpuStock(argThat(spuStockIncrCounts -> { + assertEquals(spuStockIncrCounts.size(), 1); + assertEquals(spuStockIncrCounts.get(10L), 10); + return true; + })); + } + + @Test + public void testUpdateSkuStock_decrSuccess() { + // 准备参数 + ProductSkuUpdateStockReqDTO updateStockReqDTO = new ProductSkuUpdateStockReqDTO() + .setItems(singletonList(new ProductSkuUpdateStockReqDTO.Item().setId(1L).setIncrCount(-10))); + // mock 数据 + productSkuMapper.insert(randomPojo(ProductSkuDO.class, o -> { + o.setId(1L).setSpuId(10L).setStock(20); + o.getProperties().forEach(p -> { + // 指定 id 范围 解决 Value too long + p.setPropertyId(generateId()); + p.setValueId(generateId()); + }); + })); + + // 调用 + productSkuService.updateSkuStock(updateStockReqDTO); + // 断言 + ProductSkuDO sku = productSkuMapper.selectById(1L); + assertEquals(sku.getStock(), 10); + verify(productSpuService).updateSpuStock(argThat(spuStockIncrCounts -> { + assertEquals(spuStockIncrCounts.size(), 1); + assertEquals(spuStockIncrCounts.get(10L), -10); + return true; + })); + } + + @Test + public void testUpdateSkuStock_decrFail() { + // 准备参数 + ProductSkuUpdateStockReqDTO updateStockReqDTO = new ProductSkuUpdateStockReqDTO() + .setItems(singletonList(new ProductSkuUpdateStockReqDTO.Item().setId(1L).setIncrCount(-30))); + // mock 数据 + productSkuMapper.insert(randomPojo(ProductSkuDO.class, o -> { + o.setId(1L).setSpuId(10L).setStock(20); + o.getProperties().forEach(p -> { + // 指定 id 范围 解决 Value too long + p.setPropertyId(generateId()); + p.setValueId(generateId()); + }); + })); + // 调用并断言 + AssertUtils.assertServiceException(() -> productSkuService.updateSkuStock(updateStockReqDTO), + SKU_STOCK_NOT_ENOUGH); + } + + @Test + public void testDeleteSku_success() { + ProductSkuDO dbSku = randomPojo(ProductSkuDO.class, o -> { + o.setId(generateId()).setSpuId(generateId()); + o.getProperties().forEach(p -> { + // 指定 id 范围 解决 Value too long + p.setPropertyId(generateId()); + p.setValueId(generateId()); + }); + }); + // mock 数据 + productSkuMapper.insert(dbSku); + // 准备参数 + Long id = dbSku.getId(); + + // 调用 + productSkuService.deleteSku(id); + // 校验数据不存在了 + assertNull(productSkuMapper.selectById(id)); + } + + @Test + public void testDeleteSku_notExists() { + // 准备参数 + Long id = 1L; + + // 调用, 并断言异常 + assertServiceException(() -> productSkuService.deleteSku(id), SKU_NOT_EXISTS); + } +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/spu/ProductSpuServiceImplTest.java b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/spu/ProductSpuServiceImplTest.java new file mode 100644 index 0000000..b787258 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/java/com/yunxi/scm/module/product/service/spu/ProductSpuServiceImplTest.java @@ -0,0 +1,503 @@ +package com.yunxi.scm.module.product.service.spu; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.product.controller.admin.sku.vo.ProductSkuCreateOrUpdateReqVO; +import com.yunxi.scm.module.product.controller.admin.spu.vo.ProductSpuCreateReqVO; +import com.yunxi.scm.module.product.controller.admin.spu.vo.ProductSpuPageReqVO; +import com.yunxi.scm.module.product.controller.admin.spu.vo.ProductSpuRespVO; +import com.yunxi.scm.module.product.controller.admin.spu.vo.ProductSpuUpdateReqVO; +import com.yunxi.scm.module.product.convert.spu.ProductSpuConvert; +import com.yunxi.scm.module.product.dal.dataobject.spu.ProductSpuDO; +import com.yunxi.scm.module.product.dal.mysql.spu.ProductSpuMapper; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.yunxi.scm.module.product.service.brand.ProductBrandServiceImpl; +import com.yunxi.scm.module.product.service.category.ProductCategoryServiceImpl; +import com.yunxi.scm.module.product.service.property.ProductPropertyService; +import com.yunxi.scm.module.product.service.property.ProductPropertyValueService; +import com.yunxi.scm.module.product.service.sku.ProductSkuServiceImpl; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.math.RoundingMode; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static org.assertj.core.util.Lists.newArrayList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +// TODO @芋艿:review 下单元测试 + +/** + * {@link ProductSpuServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(ProductSpuServiceImpl.class) +public class ProductSpuServiceImplTest extends BaseDbUnitTest { + + @Resource + private ProductSpuServiceImpl productSpuService; + + @Resource + private ProductSpuMapper productSpuMapper; + + @MockBean + private ProductSkuServiceImpl productSkuService; + @MockBean + private ProductCategoryServiceImpl categoryService; + @MockBean + private ProductBrandServiceImpl brandService; + @MockBean + private ProductPropertyService productPropertyService; + @MockBean + private ProductPropertyValueService productPropertyValueService; + + public String generateNo() { + return DateUtil.format(new Date(), "yyyyMMddHHmmss") + RandomUtil.randomInt(100000, 999999); + } + + public Long generateId() { + return RandomUtil.randomLong(100000, 999999); + } + + public int generaInt(){return RandomUtil.randomInt(1,9999999);} + + // TODO @芋艿:单测后续 review 哈 + + @Test + public void testCreateSpu_success() { + // 准备参数 + ProductSkuCreateOrUpdateReqVO skuCreateOrUpdateReqVO = randomPojo(ProductSkuCreateOrUpdateReqVO.class,o->{ + // 限制范围为正整数 + o.setCostPrice(generaInt()); + o.setPrice(generaInt()); + o.setMarketPrice(generaInt()); + o.setStock(generaInt()); + o.setWarnStock(10); + o.setSubCommissionFirstPrice(generaInt()); + o.setSubCommissionSecondPrice(generaInt()); + // 限制分数为两位数 + o.setWeight(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP)); + o.setVolume(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP)); + }); + ProductSpuCreateReqVO createReqVO = randomPojo(ProductSpuCreateReqVO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setSkus(newArrayList(skuCreateOrUpdateReqVO,skuCreateOrUpdateReqVO,skuCreateOrUpdateReqVO)); + }); + when(categoryService.getCategoryLevel(eq(createReqVO.getCategoryId()))).thenReturn(2); + Long spu = productSpuService.createSpu(createReqVO); + ProductSpuDO productSpuDO = productSpuMapper.selectById(spu); + assertPojoEquals(createReqVO, productSpuDO); + } + + @Test + public void testUpdateSpu_success() { + // 准备参数 + ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(generaInt()); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + }); + productSpuMapper.insert(createReqVO); + // 准备参数 + ProductSkuCreateOrUpdateReqVO skuCreateOrUpdateReqVO = randomPojo(ProductSkuCreateOrUpdateReqVO.class,o->{ + // 限制范围为正整数 + o.setCostPrice(generaInt()); + o.setPrice(generaInt()); + o.setMarketPrice(generaInt()); + o.setStock(generaInt()); + o.setWarnStock(10); + o.setSubCommissionFirstPrice(generaInt()); + o.setSubCommissionSecondPrice(generaInt()); + // 限制分数为两位数 + o.setWeight(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP)); + o.setVolume(RandomUtil.randomDouble(10,2, RoundingMode.HALF_UP)); + }); + // 准备参数 + ProductSpuUpdateReqVO reqVO = randomPojo(ProductSpuUpdateReqVO.class, o -> { + o.setId(createReqVO.getId()); // 设置更新的 ID + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + o.setStatus(0); + o.setSkus(newArrayList(skuCreateOrUpdateReqVO,skuCreateOrUpdateReqVO,skuCreateOrUpdateReqVO)); + }); + when(categoryService.getCategoryLevel(eq(reqVO.getCategoryId()))).thenReturn(2); + // 调用 + productSpuService.updateSpu(reqVO); + // 校验是否更新正确 + ProductSpuDO spu = productSpuMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, spu); + } + + @Test + public void testValidateSpuExists_exception() { + ProductSpuUpdateReqVO reqVO = randomPojo(ProductSpuUpdateReqVO.class); + // 调用 + Assertions.assertThrows(ServiceException.class, () -> productSpuService.updateSpu(reqVO)); + } + + @Test + void deleteSpu() { + // 准备参数 + ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(generaInt()); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + o.setStatus(-1); // 加入回收站才可删除 + }); + productSpuMapper.insert(createReqVO); + + // 调用 + productSpuService.deleteSpu(createReqVO.getId()); + + Assertions.assertNull(productSpuMapper.selectById(createReqVO.getId())); + } + + @Test + void getSpu() { + // 准备参数 + ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(generaInt()); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + }); + productSpuMapper.insert(createReqVO); + + ProductSpuDO spu = productSpuService.getSpu(createReqVO.getId()); + assertPojoEquals(createReqVO, spu); + } + + @Test + void getSpuList() { + // 准备参数 + ArrayList createReqVOs = Lists.newArrayList(randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(generaInt()); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + }), randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(generaInt()); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + })); + productSpuMapper.insertBatch(createReqVOs); + + // 调用 + List spuList = productSpuService.getSpuList(createReqVOs.stream().map(ProductSpuDO::getId).collect(Collectors.toList())); + Assertions.assertIterableEquals(createReqVOs, spuList); + } + + @Test + void getSpuPage_alarmStock_empty() { + // 准备参数 + ArrayList createReqVOs = Lists.newArrayList(randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(11); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + }), randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(11); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + })); + productSpuMapper.insertBatch(createReqVOs); + // 调用 + ProductSpuPageReqVO productSpuPageReqVO = new ProductSpuPageReqVO(); + productSpuPageReqVO.setTabType(ProductSpuPageReqVO.ALERT_STOCK); + + PageResult spuPage = productSpuService.getSpuPage(productSpuPageReqVO); + + PageResult result = PageResult.empty(); + Assertions.assertIterableEquals(result.getList(), spuPage.getList()); + assertEquals(spuPage.getTotal(), result.getTotal()); + } + + @Test + void getSpuPage_alarmStock() { + // 准备参数 + ArrayList createReqVOs = Lists.newArrayList(randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(5); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + }), randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(9); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + })); + productSpuMapper.insertBatch(createReqVOs); + // 调用 + ProductSpuPageReqVO productSpuPageReqVO = new ProductSpuPageReqVO(); + productSpuPageReqVO.setTabType(ProductSpuPageReqVO.ALERT_STOCK); + PageResult spuPage = productSpuService.getSpuPage(productSpuPageReqVO); + assertEquals(createReqVOs.size(), spuPage.getTotal()); + } + + @Test + void testGetSpuPage() { + // 准备参数 + ProductSpuDO createReqVO = randomPojo(ProductSpuDO.class,o->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(generaInt()); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + }); + + // 准备参数 + productSpuMapper.insert(createReqVO); + // 测试 status 不匹配 + productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setStatus(ProductSpuStatusEnum.DISABLE.getStatus()))); + productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setStatus(ProductSpuStatusEnum.RECYCLE.getStatus()))); + // 测试 SpecType 不匹配 + productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setSpecType(true))); + // 测试 BrandId 不匹配 + productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setBrandId(generateId()))); + // 测试 CategoryId 不匹配 + productSpuMapper.insert(cloneIgnoreId(createReqVO, o -> o.setCategoryId(generateId()))); + + // 调用 + ProductSpuPageReqVO productSpuPageReqVO = new ProductSpuPageReqVO(); + // 查询条件 按需打开 + //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.ALERT_STOCK); + //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.RECYCLE_BIN); + //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.FOR_SALE); + //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.IN_WAREHOUSE); + //productSpuPageReqVO.setTabType(ProductSpuPageReqVO.SOLD_OUT); + //productSpuPageReqVO.setName(createReqVO.getName()); + //productSpuPageReqVO.setCategoryId(createReqVO.getCategoryId()); + + PageResult spuPage = productSpuService.getSpuPage(productSpuPageReqVO); + + PageResult result = ProductSpuConvert.INSTANCE.convertPage(productSpuMapper.selectPage(productSpuPageReqVO)); + assertEquals(result.getTotal(), spuPage.getTotal()); + } + + /** + * 生成笛卡尔积 + * + * @param data 数据 + * @return 笛卡尔积 + */ + public static List> cartesianProduct(List> data) { + List> res = null; // 结果集(当前为第N个List,则该处存放的就为前N-1个List的笛卡尔积集合) + for (List list : data) { // 遍历数据 + List> temp = new ArrayList<>(); // 临时结果集,存放本次循环后生成的笛卡尔积集合 + if (res == null) { // 结果集为null表示第一次循环既list为第一个List + for (T t : list) { // 便利第一个List + // 利用stream生成List,第一个List的笛卡尔积集合约等于自己本身(需要创建一个List并把对象添加到当中),存放到临时结果集 + temp.add(Stream.of(t).collect(Collectors.toList())); + } + res = temp; // 将临时结果集赋值给结果集 + continue; // 跳过本次循环 + } + // 不为第一个List,计算前面的集合(笛卡尔积)和当前List的笛卡尔积集合 + for (T t : list) { // 便利 + for (List rl : res) { // 便利前面的笛卡尔积集合 + // 利用stream生成List + temp.add(Stream.concat(rl.stream(), Stream.of(t)).collect(Collectors.toList())); + } + } + res = temp; // 将临时结果集赋值给结果集 + } + // 返回结果 + return res; + } + + @Test + public void testUpdateSpuStock() { + // 准备参数 + Map stockIncrCounts = MapUtil.builder(1L, 10).put(2L, -20).build(); + // mock 方法(数据) + productSpuMapper.insert(randomPojo(ProductSpuDO.class, o ->{ + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(generaInt()); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + o.setId(1L).setStock(20); + })); + productSpuMapper.insert(randomPojo(ProductSpuDO.class, o -> { + o.setCategoryId(generateId()); + o.setBrandId(generateId()); + o.setDeliveryTemplateId(generateId()); + o.setUnit(RandomUtil.randomInt(1,20)); // 限制商品单位范围 + o.setSort(RandomUtil.randomInt(1,100)); // 限制排序范围 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setVirtualSalesCount(generaInt()); // 限制范围为正整数 + o.setActivityOrders(newArrayList(1,3,2,4,5)); // 活动排序 + o.setPrice(generaInt()); // 限制范围为正整数 + o.setMarketPrice(generaInt()); // 限制范围为正整数 + o.setCostPrice(generaInt()); // 限制范围为正整数 + o.setStock(generaInt()); // 限制范围为正整数 + o.setGiveIntegral(generaInt()); // 限制范围为正整数 + o.setSalesCount(generaInt()); // 限制范围为正整数 + o.setBrowseCount(generaInt()); // 限制范围为正整数 + o.setId(2L).setStock(30); + })); + + // 调用 + productSpuService.updateSpuStock(stockIncrCounts); + // 断言 + assertEquals(productSpuService.getSpu(1L).getStock(), 30); + assertEquals(productSpuService.getSpu(2L).getStock(), 10); + } + +} diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..b994814 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,50 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis-plus: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + type-aliases-package: ${yunxi.info.base-package}.module.*.dal.dataobject + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/logback.xml b/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/sql/clean.sql b/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..e9616cd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,7 @@ +DELETE FROM "product_sku"; +DELETE FROM "product_spu"; +DELETE FROM "product_category"; +DELETE FROM "product_brand"; +DELETE FROM "product_property"; +DELETE FROM "product_property_value"; +DELETE FROM "product_comment"; diff --git a/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..f0f0c70 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-product-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,157 @@ +CREATE TABLE IF NOT EXISTS `product_sku` ( + `id` bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `spu_id` bigint NOT NULL COMMENT 'spu编号', + `properties` varchar(512) DEFAULT NULL COMMENT '属性数组,JSON 格式', + `price` int NOT NULL DEFAULT '-1' COMMENT '商品价格,单位:分', + `market_price` int DEFAULT NULL COMMENT '市场价,单位:分', + `cost_price` int NOT NULL DEFAULT '-1' COMMENT '成本价,单位: 分', + `bar_code` varchar(64) DEFAULT NULL COMMENT 'SKU 的条形码', + `pic_url` varchar(256) NOT NULL COMMENT '图片地址', + `stock` int DEFAULT NULL COMMENT '库存', + `weight` double DEFAULT NULL COMMENT '商品重量,单位:kg 千克', + `volume` double DEFAULT NULL COMMENT '商品体积,单位:m^3 平米', + `sub_commission_first_price` int DEFAULT NULL COMMENT '一级分销的佣金,单位:分', + `sub_commission_second_price` int DEFAULT NULL COMMENT '二级分销的佣金,单位:分', + `sales_count` int DEFAULT NULL COMMENT '商品销量', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY("id") +) COMMENT '商品sku'; + +CREATE TABLE IF NOT EXISTS `product_spu` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '商品 SPU 编号,自增', + `name` varchar(128) NOT NULL COMMENT '商品名称', + `keyword` varchar(256) NOT NULL COMMENT '关键字', + `introduction` varchar(256) NOT NULL COMMENT '商品简介', + `description` text NOT NULL COMMENT '商品详情', + `bar_code` varchar(64) NOT NULL COMMENT '条形码', + `category_id` bigint NOT NULL COMMENT '商品分类编号', + `brand_id` int DEFAULT NULL COMMENT '商品品牌编号', + `pic_url` varchar(256) NOT NULL COMMENT '商品封面图', + `slider_pic_urls` varchar(2000) DEFAULT '' COMMENT '商品轮播图地址\n 数组,以逗号分隔\n 最多上传15张', + `video_url` varchar(256) DEFAULT NULL COMMENT '商品视频', + `unit` tinyint NOT NULL COMMENT '单位', + `sort` int NOT NULL DEFAULT '0' COMMENT '排序字段', + `status` tinyint NOT NULL COMMENT '商品状态: 0 上架(开启) 1 下架(禁用)-1 回收', + `spec_type` bit(1) NOT NULL COMMENT '规格类型:0 单规格 1 多规格', + `price` int NOT NULL DEFAULT '-1' COMMENT '商品价格,单位使用:分', + `market_price` int NOT NULL COMMENT '市场价,单位使用:分', + `cost_price` int NOT NULL DEFAULT '-1' COMMENT '成本价,单位: 分', + `stock` int NOT NULL DEFAULT '0' COMMENT '库存', + `delivery_template_id` bigint NOT NULL COMMENT '物流配置模板编号', + `recommend_hot` bit(1) NOT NULL COMMENT '是否热卖推荐: 0 默认 1 热卖', + `recommend_benefit` bit(1) NOT NULL COMMENT '是否优惠推荐: 0 默认 1 优选', + `recommend_best` bit(1) NOT NULL COMMENT '是否精品推荐: 0 默认 1 精品', + `recommend_new` bit(1) NOT NULL COMMENT '是否新品推荐: 0 默认 1 新品', + `recommend_good` bit(1) NOT NULL COMMENT '是否优品推荐', + `give_integral` int NOT NULL COMMENT '赠送积分', + `give_coupon_template_ids` varchar(512) DEFAULT '' COMMENT '赠送的优惠劵编号的数组', + `sub_commission_type` bit(1) NOT NULL COMMENT '分销类型', + `activity_orders` varchar(16) NOT NULL DEFAULT '' COMMENT '活动显示排序0=默认, 1=秒杀,2=砍价,3=拼团', + `sales_count` int DEFAULT '0' COMMENT '商品销量', + `virtual_sales_count` int DEFAULT '0' COMMENT '虚拟销量', + `browse_count` int DEFAULT '0' COMMENT '商品点击量', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY("id") +) COMMENT '商品spu'; + +CREATE TABLE IF NOT EXISTS `product_category` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '分类编号', + `parent_id` bigint NOT NULL COMMENT '父分类编号', + `name` varchar(255) NOT NULL COMMENT '分类名称', + `pic_url` varchar(255) NOT NULL COMMENT '移动端分类图', + `big_pic_url` varchar(255) DEFAULT NULL COMMENT 'PC 端分类图', + `sort` int DEFAULT '0' COMMENT '分类排序', + `status` tinyint NOT NULL COMMENT '开启状态', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY("id") +) COMMENT '商品分类'; + +CREATE TABLE IF NOT EXISTS `product_brand` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '品牌编号', + `name` varchar(255) NOT NULL COMMENT '品牌名称', + `pic_url` varchar(255) NOT NULL COMMENT '品牌图片', + `sort` int DEFAULT '0' COMMENT '品牌排序', + `description` varchar(1024) DEFAULT NULL COMMENT '品牌描述', + `status` tinyint NOT NULL COMMENT '状态', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY("id") +) COMMENT '商品品牌'; + +CREATE TABLE IF NOT EXISTS `product_property` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `name` varchar(64) DEFAULT NULL COMMENT '规格名称', + `status` tinyint DEFAULT NULL COMMENT '状态: 0 开启 ,1 禁用', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + PRIMARY KEY("id") +) COMMENT '规格名称'; + +CREATE TABLE IF NOT EXISTS `product_property_value` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', + `property_id` bigint DEFAULT NULL COMMENT '规格键id', + `name` varchar(128) DEFAULT NULL COMMENT '规格值名字', + `status` tinyint DEFAULT NULL COMMENT '状态: 1 开启 ,2 禁用', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + `remark` varchar(255) DEFAULT NULL COMMENT '备注', + PRIMARY KEY("id") +) COMMENT '规格值'; + +DROP TABLE IF EXISTS `product_comment` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '评论编号,主键自增', + `user_id` bigint DEFAULT NULL COMMENT '评价人的用户编号关联 MemberUserDO 的 id 编号', + `user_nickname` varchar(255) DEFAULT NULL COMMENT '评价人名称', + `user_avatar` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '评价人头像', + `anonymous` bit(1) DEFAULT NULL COMMENT '是否匿名', + `order_id` bigint DEFAULT NULL COMMENT '交易订单编号关联 TradeOrderDO 的 id 编号', + `order_item_id` bigint DEFAULT NULL COMMENT '交易订单项编号关联 TradeOrderItemDO 的 id 编号', + `spu_id` bigint DEFAULT NULL COMMENT '商品 SPU 编号关联 ProductSpuDO 的 id', + `spu_name` varchar(255) DEFAULT NULL COMMENT '商品 SPU 名称', + `sku_id` bigint DEFAULT NULL COMMENT '商品 SKU 编号关联 ProductSkuDO 的 id 编号', + `visible` bit(1) DEFAULT NULL COMMENT '是否可见true:显示false:隐藏', + `scores` tinyint DEFAULT NULL COMMENT '评分星级1-5分', + `description_scores` tinyint DEFAULT NULL COMMENT '描述星级1-5 星', + `benefit_scores` tinyint DEFAULT NULL COMMENT '服务星级1-5 星', + `content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '评论内容', + `pic_urls` varchar(4096) DEFAULT NULL COMMENT '评论图片地址数组', + `reply_status` bit(1) DEFAULT NULL COMMENT '商家是否回复', + `reply_user_id` bigint DEFAULT NULL COMMENT '回复管理员编号关联 AdminUserDO 的 id 编号', + `reply_content` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL COMMENT '商家回复内容', + `reply_time` datetime DEFAULT NULL COMMENT '商家回复时间', + `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除', + `tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 26 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '商品评论'; \ No newline at end of file diff --git a/yunxi-module-mall/yunxi-module-promotion-api/pom.xml b/yunxi-module-mall/yunxi-module-promotion-api/pom.xml new file mode 100644 index 0000000..ec75bbe --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/pom.xml @@ -0,0 +1,40 @@ + + + + com.yunxi.scm + yunxi-module-mall + ${revision} + + 4.0.0 + yunxi-module-promotion-api + jar + + ${project.artifactId} + + market 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + + com.fasterxml.jackson.core + jackson-databind + true + + + + diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/combination/CombinationApi.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/combination/CombinationApi.java new file mode 100644 index 0000000..d57d1a5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/combination/CombinationApi.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.promotion.api.combination; + +import com.yunxi.scm.module.promotion.api.combination.dto.CombinationRecordReqDTO; + +import javax.validation.Valid; + +// TODO @puhui999:CombinationRecordApi 分成活动、记录哈 +// TODO @芋艿:后面也再撸撸这几个接口 +/** + * 拼团活动 API 接口 + * + * @author HUIHUI + */ +public interface CombinationApi { + + /** + * 创建开团记录 + * + * @param reqDTO 请求 DTO + */ + void createRecord(@Valid CombinationRecordReqDTO reqDTO); + + /** + * 获取开团记录状态 + * + * @param userId 用户编号 + * @param orderId 订单编号 + */ + boolean validateRecordStatusIsSuccess(Long userId, Long orderId); + + /** + * 更新开团记录状态 + * + * @param userId 用户编号 + * @param orderId 订单编号 + * @param status 状态值 + */ + void updateRecordStatus(Long userId, Long orderId, Integer status); + + /** + * 更新开团记录状态和开始时间 + * + * @param userId 用户编号 + * @param orderId 订单编号 + * @param status 状态值 + */ + void updateRecordStatusAndStartTime(Long userId, Long orderId, Integer status); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/combination/dto/CombinationRecordReqDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/combination/dto/CombinationRecordReqDTO.java new file mode 100644 index 0000000..7f342f8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/combination/dto/CombinationRecordReqDTO.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.promotion.api.combination.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +// TODO @puhui999:CombinationRecordCreateReqDTO,这样更容易知道是创建噢 +/** + * 拼团记录 Request DTO + * + * @author HUIHUI + */ +@Data +public class CombinationRecordReqDTO { + + /** + * 拼团活动编号 + */ + @NotNull(message = "拼团活动编号不能为空") + private Long activityId; + /** + * spu 编号 + */ + @NotNull(message = "spu 编号不能为空") + private Long spuId; + /** + * sku 编号 + */ + @NotNull(message = "sku 编号不能为空") + private Long skuId; + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 订单编号 + */ + @NotNull(message = "订单编号不能为空") + private Long orderId; + /** + * 团长编号 + */ + @NotNull(message = "团长编号不能为空") + private Long headId; + /** + * 商品名字 + */ + @NotEmpty(message = "商品名字不能为空") + private String spuName; + /** + * 商品图片 + */ + @NotEmpty(message = "商品图片不能为空") + private String picUrl; + /** + * 拼团商品单价 + */ + @NotNull(message = "拼团商品单价不能为空") + private Integer combinationPrice; + /** + * 用户昵称 + */ + @NotEmpty(message = "用户昵称不能为空") + private String nickname; + /** + * 用户头像 + */ + @NotEmpty(message = "用户头像不能为空") + private String avatar; + /** + * 开团状态:正在开团 拼团成功 拼团失败 TODO 等待支付 + */ + @NotNull(message = "开团状态不能为空") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/CouponApi.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/CouponApi.java new file mode 100644 index 0000000..bfa65ee --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/CouponApi.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.promotion.api.coupon; + +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponRespDTO; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponUseReqDTO; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponValidReqDTO; + +import javax.validation.Valid; + +/** + * 优惠劵 API 接口 + * + * @author 芋道源码 + */ +public interface CouponApi { + + /** + * 使用优惠劵 + * + * @param useReqDTO 使用请求 + */ + void useCoupon(@Valid CouponUseReqDTO useReqDTO); + + /** + * 校验优惠劵 + * + * @param validReqDTO 校验请求 + * @return 优惠劵 + */ + CouponRespDTO validateCoupon(@Valid CouponValidReqDTO validReqDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponRespDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponRespDTO.java new file mode 100644 index 0000000..8a3b77a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponRespDTO.java @@ -0,0 +1,109 @@ +package com.yunxi.scm.module.promotion.api.coupon.dto; + +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponStatusEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponTakeTypeEnum; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 优惠劵 Response DTO + * + * @author 芋道源码 + */ +@Data +public class CouponRespDTO { + + // ========== 基本信息 BEGIN ========== + /** + * 优惠劵编号 + */ + private Long id; + /** + * 优惠劵模板编号 + */ + private Integer templateId; + /** + * 优惠劵名 + */ + private String name; + /** + * 优惠码状态 + * + * 枚举 {@link CouponStatusEnum} + */ + private Integer status; + + // ========== 基本信息 END ========== + + // ========== 领取情况 BEGIN ========== + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 字段 + */ + private Long userId; + /** + * 领取类型 + * + * 枚举 {@link CouponTakeTypeEnum} + */ + private Integer takeType; + // ========== 领取情况 END ========== + + // ========== 使用规则 BEGIN ========== + /** + * 是否设置满多少金额可用,单位:分 + */ + private Integer usePrice; + /** + * 生效开始时间 + */ + private LocalDateTime validStartTime; + /** + * 生效结束时间 + */ + private LocalDateTime validEndTime; + /** + * 商品范围 + */ + private Integer productScope; + /** + * 商品 SPU 编号的数组 + */ + private List productSpuIds; + // ========== 使用规则 END ========== + + // ========== 使用效果 BEGIN ========== + /** + * 折扣类型 + */ + private Integer discountType; + /** + * 折扣百分比 + */ + private Integer discountPercent; + /** + * 优惠金额,单位:分 + */ + private Integer discountPrice; + /** + * 折扣上限,仅在 {@link #discountType} 等于 {@link PromotionDiscountTypeEnum#PERCENT} 时生效 + */ + private Integer discountLimitPrice; + // ========== 使用效果 END ========== + + // ========== 使用情况 BEGIN ========== + /** + * 使用订单号 + */ + private Long useOrderId; + /** + * 使用时间 + */ + private LocalDateTime useTime; + + // ========== 使用情况 END ========== +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponUseReqDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponUseReqDTO.java new file mode 100644 index 0000000..c365645 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponUseReqDTO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.promotion.api.coupon.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 优惠劵使用 Request DTO + * + * @author 芋道源码 + */ +@Data +public class CouponUseReqDTO { + + /** + * 优惠劵编号 + */ + @NotNull(message = "优惠劵编号不能为空") + private Long id; + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + + /** + * 订单编号 + */ + @NotNull(message = "订单编号不能为空") + private Long orderId; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponValidReqDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponValidReqDTO.java new file mode 100644 index 0000000..e764316 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/coupon/dto/CouponValidReqDTO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.promotion.api.coupon.dto; + +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 优惠劵使用 Request DTO + * + * @author 芋道源码 + */ +@Data +public class CouponValidReqDTO { + + /** + * 优惠劵编号 + */ + @NotNull(message = "优惠劵编号不能为空") + private Long id; + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/discount/DiscountActivityApi.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/discount/DiscountActivityApi.java new file mode 100644 index 0000000..9ee58e7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/discount/DiscountActivityApi.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.promotion.api.discount; + +import com.yunxi.scm.module.promotion.api.discount.dto.DiscountProductRespDTO; + +import java.util.Collection; +import java.util.List; + +/** + * 限时折扣 API 接口 + * + * @author 芋道源码 + */ +public interface DiscountActivityApi { + + /** + * 获得商品匹配的的限时折扣信息 + * + * @param skuIds 商品 SKU 编号数组 + * @return 限时折扣信息 + */ + List getMatchDiscountProductList(Collection skuIds); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/discount/dto/DiscountProductRespDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/discount/dto/DiscountProductRespDTO.java new file mode 100644 index 0000000..e0f890e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/discount/dto/DiscountProductRespDTO.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.promotion.api.discount.dto; + +import lombok.Data; + +/** + * 限时折扣活动商品 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DiscountProductRespDTO { + + /** + * 编号,主键自增 + */ + private Long id; + /** + * 商品 SPU 编号 + */ + private Long spuId; + /** + * 商品 SKU 编号 + */ + private Long skuId; + /** + * 折扣类型 + */ + private Integer discountType; + /** + * 折扣百分比 + */ + private Integer discountPercent; + /** + * 优惠金额,单位:分 + */ + private Integer discountPrice; + + // ========== 活动字段 ========== + /** + * 限时折扣活动的编号 + */ + private Long activityId; + /** + * 活动标题 + */ + private String activityName; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/PriceApi.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/PriceApi.java new file mode 100644 index 0000000..8d65de9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/PriceApi.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.promotion.api.price; + +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateReqDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateRespDTO; + +/** + * 价格 API 接口 + * + * @author 芋道源码 + */ +public interface PriceApi { + + /** + * 计算商品的价格 + * + * @param calculateReqDTO 价格请求 + * @return 价格相应 + */ + PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/CouponMeetRespDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/CouponMeetRespDTO.java new file mode 100644 index 0000000..a37944b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/CouponMeetRespDTO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.promotion.api.price.dto; + +import lombok.Data; + +/** + * 优惠劵的匹配信息 Response DTO + * + * why 放在 price 包下?主要获取的时候,需要涉及到较多的价格计算逻辑,放在 price 可以更好的复用逻辑 + * + * @author 芋道源码 + */ +@Data +public class CouponMeetRespDTO { + + /** + * 优惠劵编号 + */ + private Long id; + + // ========== 非优惠劵的基本信息字段 ========== + /** + * 是否匹配 + */ + private Boolean meet; + /** + * 不匹配的提示,即 {@link #meet} = true 才有值 + * + * 例如说: + * 1. 所结算商品没有符合条件的商品 + * 2. 差 XXX 元可用优惠劵 + * 3. 优惠劵未到使用时间 + */ + private String meetTip; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/PriceCalculateReqDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/PriceCalculateReqDTO.java new file mode 100644 index 0000000..e5f95eb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/PriceCalculateReqDTO.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.module.promotion.api.price.dto; + +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 价格计算 Request DTO + * + * @author 芋道源码 + */ +@Data +@Deprecated +public class PriceCalculateReqDTO { + + /** + * 用户编号 + * + * 对应 MemberUserDO 的 id 编号 + */ + private Long userId; + + /** + * 优惠劵编号 + */ + private Long couponId; + + /** + * 收货地址编号 + */ + private Long addressId; + + /** + * 商品 SKU 数组 + */ + @NotNull(message = "商品数组不能为空") + private List items; + + /** + * 商品 SKU + */ + @Data + public static class Item { + + /** + * SKU 编号 + */ + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + /** + * SKU 数量 + */ + @NotNull(message = "商品 SKU 数量不能为空") + @Min(value = 0L, message = "商品 SKU 数量必须大于等于 0") // 可传递 0 数量,用于购物车未选中的情况 + private Integer count; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/PriceCalculateRespDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/PriceCalculateRespDTO.java new file mode 100644 index 0000000..1fa21fe --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/price/dto/PriceCalculateRespDTO.java @@ -0,0 +1,252 @@ +package com.yunxi.scm.module.promotion.api.price.dto; + +import com.yunxi.scm.module.promotion.enums.common.PromotionTypeEnum; +import lombok.Data; + +import java.util.List; + +/** + * 价格计算 Response DTO + * + * 整体设计,参考 taobao 的技术文档: + * 1. 订单管理 + * 2. 常用订单金额说明 + * + * 举个例子:订单图 + * 输入: + * 1. 订单实付: trade.payment = 198.00;订单邮费:5 元; + * 2. 商品级优惠 圣诞价: 省 29.00 元 和 圣诞价:省 150.00 元; 订单级优惠,圣诞 2:省 5.00 元; + * 分摊: + * 1. 商品 1:原价 108 元,优惠 29 元,子订单实付 79 元,分摊主订单优惠 1.99 元; + * 2. 商品 2:原价 269 元,优惠 150 元,子订单实付 119 元,分摊主订单优惠 3.01 元; + * + * @author 芋道源码 + */ +@Data +@Deprecated +public class PriceCalculateRespDTO { + + /** + * 订单 + */ + private Order order; + + /** + * 营销活动数组 + * + * 只对应 {@link Order#items} 商品匹配的活动 + */ + private List promotions; + + // TODO @芋艿:需要改造下,主要是价格字段 + /** + * 订单 + */ + @Data + public static class Order { + + /** + * 商品原价(总),单位:分 + * + * 基于 {@link OrderItem#getOriginalPrice()} 求和 + * + * 对应 taobao 的 trade.total_fee 字段 + */ + private Integer totalPrice; + /** + * 订单优惠(总),单位:分 + * + * 订单级优惠:对主订单的优惠,常见如:订单满 200 元减 10 元;订单满 80 包邮。 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 优惠劵减免金额(总),单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + /** + * 积分减免金额(总),单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + /** + * 运费金额,单位:分 + */ + private Integer deliveryPrice; + /** + * 最终购买金额(总),单位:分 + * + * = {@link OrderItem#getPayPrice()} 求和 + * - {@link #couponPrice} + * - {@link #pointPrice} + * + {@link #deliveryPrice} + * - {@link #discountPrice} + */ + private Integer payPrice; + /** + * 商品 SKU 数组 + */ + private List items; + + // ========== 营销基本信息 ========== + /** + * 优惠劵编号 + */ + private Long couponId; + + } + + /** + * 订单商品 SKU + */ + @Data + public static class OrderItem { + + /** + * SPU 编号 + */ + private Long spuId; + /** + * SKU 编号 + */ + private Long skuId; + /** + * 购买数量 + */ + private Integer count; + + /** + * 商品原价(总),单位:分 + * + * = {@link #originalUnitPrice} * {@link #getCount()} + */ + private Integer originalPrice; + /** + * 商品原价(单),单位:分 + * + * 对应 ProductSkuDO 的 price 字段 + * 对应 taobao 的 order.price 字段 + */ + private Integer originalUnitPrice; + /** + * 商品优惠(总),单位:分 + * + * 商品级优惠:对单个商品的,常见如:商品原价的 8 折;商品原价的减 50 元 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 子订单实付金额,不算主订单分摊金额,单位:分 + * + * = {@link #originalPrice} + * - {@link #discountPrice} + * + * 对应 taobao 的 order.payment 字段 + */ + private Integer payPrice; + + /** + * 子订单分摊金额(总),单位:分 + * 需要分摊 {@link Order#discountPrice}、{@link Order#couponPrice}、{@link Order#pointPrice} + * + * 对应 taobao 的 order.part_mjz_discount 字段 + * 淘宝说明:子订单分摊优惠基础逻辑:一般正常优惠券和满减优惠按照子订单的金额进行分摊,特殊情况如果优惠券是指定商品使用的,只会分摊到对应商品子订单上不分摊。 + */ + private Integer orderPartPrice; + /** + * 分摊后子订单实付金额(总),单位:分 + * + * = {@link #payPrice} + * - {@link #orderPartPrice} + * + * 对应 taobao 的 divide_order_fee 字段 + */ + private Integer orderDividePrice; + + } + + /** + * 营销明细 + */ + @Data + @Deprecated + public static class Promotion { + + /** + * 营销编号 + * + * 例如说:营销活动的编号、优惠劵的编号 + */ + private Long id; + /** + * 营销名字 + */ + private String name; + /** + * 营销类型 + * + * 枚举 {@link PromotionTypeEnum} + */ + private Integer type; + /** + * 营销级别 + * + * 枚举 @link PromotionLevelEnum} TODO PromotionLevelEnum 没有这个枚举类 + */ + private Integer level; + /** + * 计算时的原价(总),单位:分 + */ + private Integer totalPrice; + /** + * 计算时的优惠(总),单位:分 + */ + private Integer discountPrice; + /** + * 匹配的商品 SKU 数组 + */ + private List items; + + // ========== 匹配情况 ========== + + /** + * 是否满足优惠条件 + */ + private Boolean match; + /** + * 满足条件的提示 + * + * 如果 {@link #match} = true 满足,则提示“圣诞价:省 150.00 元” + * 如果 {@link #match} = false 不满足,则提示“购满 85 元,可减 40 元” + */ + private String description; + + } + + /** + * 营销匹配的商品 SKU + */ + @Data + public static class PromotionItem { + + /** + * 商品 SKU 编号 + */ + private Long skuId; + /** + * 计算时的原价(总),单位:分 + */ + private Integer originalPrice; + /** + * 计算时的优惠(总),单位:分 + */ + private Integer discountPrice; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/reward/RewardActivityApi.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/reward/RewardActivityApi.java new file mode 100644 index 0000000..d99046b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/reward/RewardActivityApi.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.promotion.api.reward; + +import com.yunxi.scm.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; + +import java.util.Collection; +import java.util.List; + +/** + * 满减送活动 API 接口 + * + * @author 芋道源码 + */ +public interface RewardActivityApi { + + + /** + * 基于指定的 SPU 编号数组,获得它们匹配的满减送活动 + * + * @param spuIds SPU 编号数组 + * @return 满减送活动列表 + */ + List getMatchRewardActivityList(Collection spuIds); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java new file mode 100644 index 0000000..5541e70 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/api/reward/dto/RewardActivityMatchRespDTO.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.module.promotion.api.reward.dto; + +import com.yunxi.scm.module.promotion.enums.common.PromotionConditionTypeEnum; +import lombok.Data; + +import java.util.List; + +/** + * 满减送活动的匹配 Response DTO + * + * @author 芋道源码 + */ +@Data +public class RewardActivityMatchRespDTO { + + /** + * 活动编号,主键自增 + */ + private Long id; + /** + * 活动标题 + */ + private String name; + /** + * 条件类型 + * + * 枚举 {@link PromotionConditionTypeEnum} + */ + private Integer conditionType; + /** + * 优惠规则的数组 + */ + private List rules; + + /** + * 商品 SPU 编号的数组 + */ + private List spuIds; + + // TODO 芋艿:后面 RewardActivityRespDTO 有了之后,Rule 可以放过去 + /** + * 优惠规则 + */ + @Data + public static class Rule { + + /** + * 优惠门槛 + * + * 1. 满 N 元,单位:分 + * 2. 满 N 件 + */ + private Integer limit; + /** + * 优惠价格,单位:分 + */ + private Integer discountPrice; + /** + * 是否包邮 + */ + private Boolean freeDelivery; + /** + * 赠送的积分 + */ + private Integer point; + /** + * 赠送的优惠劵编号的数组 + */ + private List couponIds; + /** + * 赠送的优惠卷数量的数组 + */ + private List couponCounts; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/ErrorCodeConstants.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..ecf330e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/ErrorCodeConstants.java @@ -0,0 +1,69 @@ +package com.yunxi.scm.module.promotion.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Promotion 错误码枚举类 + *

+ * promotion 系统,使用 1-013-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 促销活动相关 1013001000 ============ + ErrorCode DISCOUNT_ACTIVITY_NOT_EXISTS = new ErrorCode(1013001000, "限时折扣活动不存在"); + ErrorCode DISCOUNT_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1013001001, "存在商品参加了其它限时折扣活动"); + ErrorCode DISCOUNT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1013001002, "限时折扣活动已关闭,不能修改"); + ErrorCode DISCOUNT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1013001003, "限时折扣活动未关闭,不能删除"); + ErrorCode DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1013001004, "限时折扣活动已关闭,不能重复关闭"); + ErrorCode DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1013001005, "限时折扣活动已结束,不能关闭"); + + // ========== Banner 相关 1013002000 ============ + ErrorCode BANNER_NOT_EXISTS = new ErrorCode(1013002000, "Banner 不存在"); + + // ========== Coupon 相关 1013003000 ============ + ErrorCode COUPON_NO_MATCH_SPU = new ErrorCode(1013003000, "优惠劵没有可使用的商品!"); + ErrorCode COUPON_NO_MATCH_MIN_PRICE = new ErrorCode(1013003001, "所结算的商品中未满足使用的金额"); + + // ========== 优惠劵模板 1013004000 ========== + ErrorCode COUPON_TEMPLATE_NOT_EXISTS = new ErrorCode(1013004000, "优惠劵模板不存在"); + ErrorCode COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL = new ErrorCode(1013004001, "发放数量不能小于已领取数量({})"); + + // ========== 优惠劵模板 1013005000 ========== + ErrorCode COUPON_NOT_EXISTS = new ErrorCode(1013005000, "优惠券不存在"); + ErrorCode COUPON_DELETE_FAIL_USED = new ErrorCode(1013005001, "回收优惠劵失败,优惠劵已被使用"); + ErrorCode COUPON_STATUS_NOT_UNUSED = new ErrorCode(1013005002, "优惠劵不处于待使用状态"); + ErrorCode COUPON_VALID_TIME_NOT_NOW = new ErrorCode(1013005003, "优惠券不在使用时间范围内"); + + // ========== 满减送活动 1013006000 ========== + ErrorCode REWARD_ACTIVITY_NOT_EXISTS = new ErrorCode(1013006000, "满减送活动不存在"); + ErrorCode REWARD_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1013006001, "存在商品参加了其它满减送活动"); + ErrorCode REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1013006002, "满减送活动已关闭,不能修改"); + ErrorCode REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED = new ErrorCode(1013006003, "满减送活动未关闭,不能删除"); + ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1013006004, "满减送活动已关闭,不能重复关闭"); + ErrorCode REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1013006005, "满减送活动已结束,不能关闭"); + + // ========== TODO 空着 1013007000 ============ + + // ========== 秒杀活动 1013008000 ========== + ErrorCode SECKILL_ACTIVITY_NOT_EXISTS = new ErrorCode(1013008000, "秒杀活动不存在"); + ErrorCode SECKILL_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1013008002, "存在商品参加了其它秒杀活动"); + ErrorCode SECKILL_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED = new ErrorCode(1013008003, "秒杀活动已关闭,不能修改"); + ErrorCode SECKILL_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END = new ErrorCode(1013008004, "秒杀活动未关闭或未结束,不能删除"); + ErrorCode SECKILL_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED = new ErrorCode(1013008005, "秒杀活动已关闭,不能重复关闭"); + ErrorCode SECKILL_ACTIVITY_CLOSE_FAIL_STATUS_END = new ErrorCode(1013008006, "秒杀活动已结束,不能关闭"); + + // ========== 秒杀时段 1013009000 ========== + ErrorCode SECKILL_TIME_NOT_EXISTS = new ErrorCode(1013009000, "秒杀时段不存在"); + ErrorCode SECKILL_TIME_CONFLICTS = new ErrorCode(1013009001, "秒杀时段冲突"); + ErrorCode SECKILL_TIME_EQUAL = new ErrorCode(1013009002, "秒杀时段开始时间和结束时间不能相等"); + ErrorCode SECKILL_START_TIME_BEFORE_END_TIME = new ErrorCode(1013009003, "秒杀时段开始时间不能在结束时间之后"); + ErrorCode SECKILL_TIME_DISABLE = new ErrorCode(1013009004, "秒杀时段已关闭"); + + // ========== 拼团活动 1013010000 ========== + ErrorCode COMBINATION_ACTIVITY_NOT_EXISTS = new ErrorCode(1013010000, "拼团活动不存在"); + ErrorCode COMBINATION_ACTIVITY_SPU_CONFLICTS = new ErrorCode(1013010001, "存在商品参加了其它拼团活动"); + ErrorCode COMBINATION_ACTIVITY_STATUS_DISABLE = new ErrorCode(1013010002, "拼团活动已关闭不能修改"); + ErrorCode COMBINATION_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END = new ErrorCode(1013010003, "拼团活动未关闭或未结束,不能删除"); + ErrorCode COMBINATION_RECORD_NOT_EXISTS = new ErrorCode(1013010004, "拼团不存在"); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/combination/CombinationRecordStatusEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/combination/CombinationRecordStatusEnum.java new file mode 100644 index 0000000..1bfd343 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/combination/CombinationRecordStatusEnum.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.promotion.enums.combination; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 拼团状态枚举 + * + * @author HUIHUI + */ +@AllArgsConstructor +@Getter +public enum CombinationRecordStatusEnum implements IntArrayValuable { + + WAITING(0, "未付款"), + IN_PROGRESS(1, "进行中"), + SUCCESS(2, "拼团成功"), + FAILED(3, "拼团失败"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CombinationRecordStatusEnum::getStatus).toArray(); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionActivityStatusEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionActivityStatusEnum.java new file mode 100644 index 0000000..1a335ca --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionActivityStatusEnum.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.promotion.enums.common; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 促销活动的状态枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum PromotionActivityStatusEnum implements IntArrayValuable { + + WAIT(10, "未开始"), + RUN(20, "进行中"), + END(30, "已结束"), + CLOSE(40, "已关闭"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionActivityStatusEnum::getStatus).toArray(); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionConditionTypeEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionConditionTypeEnum.java new file mode 100644 index 0000000..c57f4b5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionConditionTypeEnum.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.promotion.enums.common; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 营销的条件类型枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum PromotionConditionTypeEnum implements IntArrayValuable { + + PRICE(10, "满 N 元"), + COUNT(20, "满 N 件"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionConditionTypeEnum::getType).toArray(); + + /** + * 类型值 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionDiscountTypeEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionDiscountTypeEnum.java new file mode 100644 index 0000000..a8d9351 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionDiscountTypeEnum.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.promotion.enums.common; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 优惠类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PromotionDiscountTypeEnum implements IntArrayValuable { + + PRICE(1, "满减"), // 具体金额 + PERCENT(2, "折扣"), // 百分比 + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionDiscountTypeEnum::getType).toArray(); + + /** + * 优惠类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionProductScopeEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionProductScopeEnum.java new file mode 100644 index 0000000..6eef6fa --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionProductScopeEnum.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.promotion.enums.common; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 营销的商品范围枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PromotionProductScopeEnum implements IntArrayValuable { + + ALL(1, "全部商品参与"), + SPU(2, "指定商品参与"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionProductScopeEnum::getScope).toArray(); + + /** + * 范围值 + */ + private final Integer scope; + /** + * 范围名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionTypeEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionTypeEnum.java new file mode 100644 index 0000000..53a391d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/common/PromotionTypeEnum.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.promotion.enums.common; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 营销类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PromotionTypeEnum implements IntArrayValuable { + + SECKILL_ACTIVITY(1, "秒杀活动"), + BARGAIN_ACTIVITY(2, "拼团活动"), + COMBINATION_ACTIVITY(3, "砍价活动"), + + DISCOUNT_ACTIVITY(4, "限时折扣"), + REWARD_ACTIVITY(5, "满减送"), + + MEMBER(6, "会员折扣"), + COUPON(7, "优惠劵") + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(PromotionTypeEnum::getType).toArray(); + + /** + * 类型值 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponStatusEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponStatusEnum.java new file mode 100644 index 0000000..cde91d1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponStatusEnum.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.promotion.enums.coupon; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 优惠劵状态枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CouponStatusEnum implements IntArrayValuable { + + UNUSED(1, "未使用"), + USED(2, "已使用"), + EXPIRE(3, "已过期"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponStatusEnum::getStatus).toArray(); + + /** + * 值 + */ + private final Integer status; + /** + * 名字 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponTakeTypeEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponTakeTypeEnum.java new file mode 100644 index 0000000..4b95839 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponTakeTypeEnum.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.promotion.enums.coupon; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 优惠劵领取方式 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CouponTakeTypeEnum implements IntArrayValuable { + + BY_USER(1, "直接领取"), // 用户可在首页、每日领劵直接领取 + BY_ADMIN(2, "指定发放"), // 后台指定会员赠送优惠劵 + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponTakeTypeEnum::getValue).toArray(); + + /** + * 值 + */ + private final Integer value; + /** + * 名字 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponTemplateValidityTypeEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponTemplateValidityTypeEnum.java new file mode 100644 index 0000000..c8bdc25 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/coupon/CouponTemplateValidityTypeEnum.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.promotion.enums.coupon; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 优惠劵模板的有限期类型的枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum CouponTemplateValidityTypeEnum implements IntArrayValuable { + + DATE(1, "固定日期"), + TERM(2, "领取之后"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(CouponTemplateValidityTypeEnum::getType).toArray(); + + /** + * 值 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/decorate/DecorateComponentEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/decorate/DecorateComponentEnum.java new file mode 100644 index 0000000..a3a4246 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/decorate/DecorateComponentEnum.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.promotion.enums.decorate; + +import lombok.Getter; + +/** + * 页面组件枚举 + * + * @author jason + */ +@Getter +public enum DecorateComponentEnum { + + NAV_MENU("nav-menu", "导航菜单"), + ROLLING_BANNER("rolling-banner", "滚动横幅广告"), + PRODUCT_CATEGORY("product-category", "商品分类"); + + /** + * 页面组件代码 + */ + private final String code; + + /** + * 页面组件说明 + */ + private final String desc; + + DecorateComponentEnum(String code, String desc) { + this.code = code; + this.desc = desc; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/decorate/DecoratePageEnum.java b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/decorate/DecoratePageEnum.java new file mode 100644 index 0000000..3e31f2c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-api/src/main/java/com/yunxi/scm/module/promotion/enums/decorate/DecoratePageEnum.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.promotion.enums.decorate; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 装修页面枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum DecoratePageEnum implements IntArrayValuable { + + INDEX(1, "首页"); + + private static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DecoratePageEnum::getId).toArray(); + + /** + * 页面 id + */ + private final Integer id; + /** + * 页面名称 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/pom.xml b/yunxi-module-mall/yunxi-module-promotion-biz/pom.xml new file mode 100644 index 0000000..e46e53a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/pom.xml @@ -0,0 +1,77 @@ + + + + com.yunxi.scm + yunxi-module-mall + ${revision} + + 4.0.0 + jar + yunxi-module-promotion-biz + + ${project.artifactId} + + + market模块,主要实现营销相关功能 + 例如:营销活动、banner广告、优惠券、优惠码等功能。 + + + + + com.yunxi.scm + yunxi-module-promotion-api + ${revision} + + + com.yunxi.scm + yunxi-module-product-api + ${revision} + + + com.yunxi.scm + yunxi-module-member-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-weixin + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + + + + diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/combination/CombinationApiImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/combination/CombinationApiImpl.java new file mode 100644 index 0000000..c9e6510 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/combination/CombinationApiImpl.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.promotion.api.combination; + +import com.yunxi.scm.module.promotion.api.combination.dto.CombinationRecordReqDTO; +import com.yunxi.scm.module.promotion.service.combination.CombinationActivityService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +/** + * 拼团活动 API 实现类 + * + * @author HUIHUI + */ +@Service +public class CombinationApiImpl implements CombinationApi { + + @Resource + private CombinationActivityService activityService; + + @Override + public void createRecord(CombinationRecordReqDTO reqDTO) { + activityService.createRecord(reqDTO); + } + + @Override + public boolean validateRecordStatusIsSuccess(Long userId, Long orderId) { + return activityService.validateRecordStatusIsSuccess(userId, orderId); + } + + @Override + public void updateRecordStatus(Long userId, Long orderId, Integer status) { + activityService.updateRecordStatusByUserIdAndOrderId(userId, orderId, status); + } + + @Override + public void updateRecordStatusAndStartTime(Long userId, Long orderId, Integer status) { + activityService.updateRecordStatusAndStartTimeByUserIdAndOrderId(userId, orderId, status, LocalDateTime.now()); + } + + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/coupon/CouponApiImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/coupon/CouponApiImpl.java new file mode 100644 index 0000000..c8b9511 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/coupon/CouponApiImpl.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.promotion.api.coupon; + + +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponRespDTO; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponUseReqDTO; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponValidReqDTO; +import com.yunxi.scm.module.promotion.convert.coupon.CouponConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; +import com.yunxi.scm.module.promotion.service.coupon.CouponService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 优惠劵 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class CouponApiImpl implements CouponApi { + + @Resource + private CouponService couponService; + + @Override + public void useCoupon(CouponUseReqDTO useReqDTO) { + couponService.useCoupon(useReqDTO.getId(), useReqDTO.getUserId(), + useReqDTO.getOrderId()); + } + + @Override + public CouponRespDTO validateCoupon(CouponValidReqDTO validReqDTO) { + CouponDO coupon = couponService.validCoupon(validReqDTO.getId(), validReqDTO.getUserId()); + return CouponConvert.INSTANCE.convert(coupon); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/discount/DiscountActivityApiImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/discount/DiscountActivityApiImpl.java new file mode 100644 index 0000000..72bfbdc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/discount/DiscountActivityApiImpl.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.promotion.api.discount; + +import com.yunxi.scm.module.promotion.api.discount.dto.DiscountProductRespDTO; +import com.yunxi.scm.module.promotion.convert.discount.DiscountActivityConvert; +import com.yunxi.scm.module.promotion.service.discount.DiscountActivityService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * 限时折扣 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class DiscountActivityApiImpl implements DiscountActivityApi { + + @Resource + private DiscountActivityService discountActivityService; + + @Override + public List getMatchDiscountProductList(Collection skuIds) { + return DiscountActivityConvert.INSTANCE.convertList02(discountActivityService.getMatchDiscountProductList(skuIds)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/price/PriceApiImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/price/PriceApiImpl.java new file mode 100644 index 0000000..9bea8cc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/price/PriceApiImpl.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.promotion.api.price; + +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateReqDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateRespDTO; +import com.yunxi.scm.module.promotion.service.price.PriceService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 价格 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PriceApiImpl implements PriceApi { + + @Resource + private PriceService priceService; + + @Override + public PriceCalculateRespDTO calculatePrice(PriceCalculateReqDTO calculateReqDTO) { + //return priceService.calculatePrice(calculateReqDTO); TODO 没有 calculatePrice 这个方法 + + return null; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/reward/RewardActivityApiImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/reward/RewardActivityApiImpl.java new file mode 100644 index 0000000..d7d49f7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/api/reward/RewardActivityApiImpl.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.promotion.api.reward; + +import com.yunxi.scm.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; +import com.yunxi.scm.module.promotion.service.reward.RewardActivityService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * 满减送活动 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class RewardActivityApiImpl implements RewardActivityApi { + + @Resource + private RewardActivityService rewardActivityService; + + @Override + public List getMatchRewardActivityList(Collection spuIds) { + return rewardActivityService.getMatchRewardActivityList(spuIds); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/BannerController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/BannerController.java new file mode 100644 index 0000000..ed00cb0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/BannerController.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.promotion.controller.admin.banner; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerRespVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.banner.BannerConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.banner.BannerDO; +import com.yunxi.scm.module.promotion.service.banner.BannerService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - Banner 管理") +@RestController +@RequestMapping("/market/banner") +@Validated +public class BannerController { + + @Resource + private BannerService bannerService; + + @PostMapping("/create") + @Operation(summary = "创建 Banner") + @PreAuthorize("@ss.hasPermission('market:banner:create')") + public CommonResult createBanner(@Valid @RequestBody BannerCreateReqVO createReqVO) { + return success(bannerService.createBanner(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新 Banner") + @PreAuthorize("@ss.hasPermission('market:banner:update')") + public CommonResult updateBanner(@Valid @RequestBody BannerUpdateReqVO updateReqVO) { + bannerService.updateBanner(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除 Banner") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('market:banner:delete')") + public CommonResult deleteBanner(@RequestParam("id") Long id) { + bannerService.deleteBanner(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得 Banner") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('market:banner:query')") + public CommonResult getBanner(@RequestParam("id") Long id) { + BannerDO banner = bannerService.getBanner(id); + return success(BannerConvert.INSTANCE.convert(banner)); + } + + @GetMapping("/page") + @Operation(summary = "获得 Banner 分页") + @PreAuthorize("@ss.hasPermission('market:banner:query')") + public CommonResult> getBannerPage(@Valid BannerPageReqVO pageVO) { + PageResult pageResult = bannerService.getBannerPage(pageVO); + return success(BannerConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerBaseVO.java new file mode 100644 index 0000000..a560c45 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerBaseVO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.promotion.controller.admin.banner.vo; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * Banner Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * @author xia + */ +@Data +public class BannerBaseVO { + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "标题不能为空") + private String title; + + @Schema(description = "跳转链接", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "跳转链接不能为空") + private String url; + + @Schema(description = "图片地址", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "图片地址不能为空") + private String picUrl; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "排序不能为空") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "备注") + private String memo; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerCreateReqVO.java new file mode 100644 index 0000000..e110044 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerCreateReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.promotion.controller.admin.banner.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * @author xia + */ +@Schema(description = "管理后台 - Banner 创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BannerCreateReqVO extends BannerBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerPageReqVO.java new file mode 100644 index 0000000..a0999d9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerPageReqVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.promotion.controller.admin.banner.vo; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * @author xia + */ +@Schema(description = "管理后台 - Banner 分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BannerPageReqVO extends PageParam { + + @Schema(description = "标题") + private String title; + + + @Schema(description = "状态") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerRespVO.java new file mode 100644 index 0000000..a595b6e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerRespVO.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.promotion.controller.admin.banner.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +@Schema(description = "管理后台 - Banner Response VO") +@Data +@ToString(callSuper = true) +public class BannerRespVO extends BannerBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerUpdateReqVO.java new file mode 100644 index 0000000..ba1bc91 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/banner/vo/BannerUpdateReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.promotion.controller.admin.banner.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +/** + * @author xia + */ +@Schema(description = "管理后台 - Banner更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class BannerUpdateReqVO extends BannerBaseVO { + + @Schema(description = "banner 编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "banner 编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/CombinationActivityController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/CombinationActivityController.java new file mode 100644 index 0000000..4332bea --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/CombinationActivityController.java @@ -0,0 +1,114 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.product.api.spu.ProductSpuApi; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.*; +import com.yunxi.scm.module.promotion.convert.combination.CombinationActivityConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationProductDO; +import com.yunxi.scm.module.promotion.service.combination.CombinationActivityService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static cn.hutool.core.collection.CollectionUtil.newArrayList; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 拼团活动") +@RestController +@RequestMapping("/promotion/combination-activity") +@Validated +public class CombinationActivityController { + + @Resource + private CombinationActivityService combinationActivityService; + @Resource + private ProductSpuApi spuApi; + + @PostMapping("/create") + @Operation(summary = "创建拼团活动") + @PreAuthorize("@ss.hasPermission('promotion:combination-activity:create')") + public CommonResult createCombinationActivity(@Valid @RequestBody CombinationActivityCreateReqVO createReqVO) { + return success(combinationActivityService.createCombinationActivity(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新拼团活动") + @PreAuthorize("@ss.hasPermission('promotion:combination-activity:update')") + public CommonResult updateCombinationActivity(@Valid @RequestBody CombinationActivityUpdateReqVO updateReqVO) { + combinationActivityService.updateCombinationActivity(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除拼团活动") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:combination-activity:delete')") + public CommonResult deleteCombinationActivity(@RequestParam("id") Long id) { + combinationActivityService.deleteCombinationActivity(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得拼团活动") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')") + public CommonResult getCombinationActivity(@RequestParam("id") Long id) { + CombinationActivityDO activity = combinationActivityService.getCombinationActivity(id); + List products = combinationActivityService.getProductsByActivityIds(newArrayList(id)); + return success(CombinationActivityConvert.INSTANCE.convert(activity, products)); + } + + @GetMapping("/list") + @Operation(summary = "获得拼团活动列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')") + public CommonResult> getCombinationActivityList(@RequestParam("ids") Collection ids) { + List list = combinationActivityService.getCombinationActivityList(ids); + return success(CombinationActivityConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得拼团活动分页") + @PreAuthorize("@ss.hasPermission('promotion:combination-activity:query')") + public CommonResult> getCombinationActivityPage( + @Valid CombinationActivityPageReqVO pageVO) { + PageResult pageResult = combinationActivityService.getCombinationActivityPage(pageVO); + // TODO @puhui999:可以不一定 aIds,直接批量查询结果出来;下面也是类似; + Set aIds = CollectionUtils.convertSet(pageResult.getList(), CombinationActivityDO::getId); + List products = combinationActivityService.getProductsByActivityIds(aIds); + Set spuIds = CollectionUtils.convertSet(pageResult.getList(), CombinationActivityDO::getSpuId); + List spus = spuApi.getSpuList(spuIds); + return success(CombinationActivityConvert.INSTANCE.convertPage(pageResult, products, spus)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出拼团活动 Excel") + @PreAuthorize("@ss.hasPermission('promotion:combination-activity:export')") + @OperateLog(type = EXPORT) + public void exportCombinationActivityExcel(@Valid CombinationActivityExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = combinationActivityService.getCombinationActivityList(exportReqVO); + // 导出 Excel + List datas = CombinationActivityConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "拼团活动.xls", "数据", CombinationActivityExcelVO.class, datas); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityBaseVO.java new file mode 100644 index 0000000..bd02cdf --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityBaseVO.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 拼团活动 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class CombinationActivityBaseVO { + + @Schema(description = "拼团名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "越拼越省钱") + @NotNull(message = "拼团名称不能为空") + private String name; + + @Schema(description = "商品 SPU 编号,关联 ProductSpuDO 的 id", example = "[1,2,3]") + @NotNull(message = "拼团商品不能为空") + private Long spuId; + + @Schema(description = "总限购数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "16218") + @NotNull(message = "总限购数量不能为空") + private Integer totalLimitCount; + + @Schema(description = "单次限购数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "28265") + @NotNull(message = "单次限购数量不能为空") + private Integer singleLimitCount; + + // TODO @puhui999:是不是弄成 2 个字段会好点哈。开始、结束 + @Schema(description = "活动时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @NotNull(message = "活动时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] activityTime; + + @Schema(description = "开团人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "25222") + @NotNull(message = "开团人数不能为空") + private Integer userSize; + + @Schema(description = "限制时长(小时)", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "限制时长不能为空") + private Integer limitDuration; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityCreateReqVO.java new file mode 100644 index 0000000..2ff4d03 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityCreateReqVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity; + +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductCreateReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import java.util.List; + +@Schema(description = "管理后台 - 拼团活动创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CombinationActivityCreateReqVO extends CombinationActivityBaseVO { + + @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityExcelVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityExcelVO.java new file mode 100644 index 0000000..1f3cf7e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityExcelVO.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +// TODO @puhui999:如无必要,导出都可以删除哈 +/** + * 拼团活动 Excel VO + * + * @author HUIHUI + */ +@Data +public class CombinationActivityExcelVO { + + @ExcelProperty("活动编号") + private Long id; + + @ExcelProperty("拼团名称") + private String name; + + @ExcelProperty("商品 SPU 编号关联 ProductSpuDO 的 id") + private Long spuId; + + @ExcelProperty("总限购数量") + private Integer totalLimitCount; + + @ExcelProperty("单次限购数量") + private Integer singleLimitCount; + + @ExcelProperty("开始时间") + private LocalDateTime startTime; + + @ExcelProperty("结束时间") + private LocalDateTime endTime; + + @ExcelProperty("开团人数") + private Integer userSize; + + @ExcelProperty("开团组数") + private Integer totalNum; + + @ExcelProperty("成团组数") + private Integer successNum; + + @ExcelProperty("参与人数") + private Integer orderUserCount; + + @ExcelProperty("虚拟成团") + private Integer virtualGroup; + + @ExcelProperty(value = "活动状态:0开启 1关闭", converter = DictConvert.class) + @DictFormat("common_status") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中 + private Integer status; + + @ExcelProperty("限制时长(小时)") + private Integer limitDuration; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityExportReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityExportReqVO.java new file mode 100644 index 0000000..a3af74c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityExportReqVO.java @@ -0,0 +1,61 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +// TODO @puhui999:如无必要,导出都可以删除哈 +@Schema(description = "管理后台 - 拼团活动 Excel 导出 Request VO,参数和 CombinationActivityPageReqVO 是一致的") +@Data +public class CombinationActivityExportReqVO { + + @Schema(description = "拼团名称", example = "赵六") + private String name; + + @Schema(description = "商品 SPU 编号关联 ProductSpuDO 的 id", example = "14016") + private Long spuId; + + @Schema(description = "总限购数量", example = "16218") + private Integer totalLimitCount; + + @Schema(description = "单次限购数量", example = "28265") + private Integer singleLimitCount; + + @Schema(description = "开始时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] startTime; + + @Schema(description = "结束时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] endTime; + + @Schema(description = "开团人数") + private Integer userSize; + + @Schema(description = "开团组数") + private Integer totalNum; + + @Schema(description = "成团组数") + private Integer successNum; + + @Schema(description = "参与人数", example = "25222") + private Integer orderUserCount; + + @Schema(description = "虚拟成团") + private Integer virtualGroup; + + @Schema(description = "活动状态:0开启 1关闭", example = "0") + private Integer status; + + @Schema(description = "限制时长(小时)") + private Integer limitDuration; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityPageReqVO.java new file mode 100644 index 0000000..cbdb19c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityPageReqVO.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 拼团活动分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CombinationActivityPageReqVO extends PageParam { + + @Schema(description = "拼团名称", example = "赵六") + private String name; + + @Schema(description = "商品 SPU 编号关联 ProductSpuDO 的 id", example = "14016") + private Long spuId; + + @Schema(description = "总限购数量", example = "16218") + private Integer totalLimitCount; + + @Schema(description = "单次限购数量", example = "28265") + private Integer singleLimitCount; + + @Schema(description = "开始时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] startTime; + + @Schema(description = "结束时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] endTime; + + @Schema(description = "开团人数") + private Integer userSize; + + @Schema(description = "开团组数") + private Integer totalNum; + + @Schema(description = "成团组数") + private Integer successNum; + + @Schema(description = "参与人数", example = "25222") + private Integer orderUserCount; + + @Schema(description = "虚拟成团") + private Integer virtualGroup; + + @Schema(description = "活动状态:0开启 1关闭", example = "0") + private Integer status; + + @Schema(description = "限制时长(小时)") + private Integer limitDuration; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java new file mode 100644 index 0000000..5e22034 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityRespVO.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity; + +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 拼团活动 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CombinationActivityRespVO extends CombinationActivityBaseVO { + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "618大促") + private String spuName; + + @Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + private String picUrl; + + @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22901") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "开团人数", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开团人数不能为空") + private Integer userSize; + + @Schema(description = "开团组数", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开团组数不能为空") + private Integer totalNum; + + @Schema(description = "成团组数", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "成团组数不能为空") + private Integer successNum; + + @Schema(description = "虚拟成团", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "虚拟成团不能为空") + private Integer virtualGroup; + + @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "活动状态不能为空") + private Integer status; + + @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityUpdateReqVO.java new file mode 100644 index 0000000..710157d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/activity/CombinationActivityUpdateReqVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity; + +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductUpdateReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 拼团活动更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CombinationActivityUpdateReqVO extends CombinationActivityBaseVO { + + @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22901") + @NotNull(message = "活动编号不能为空") + private Long id; + + @Schema(description = "拼团商品", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductBaseVO.java new file mode 100644 index 0000000..63437d4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductBaseVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 拼团商品 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class CombinationProductBaseVO { + + @Schema(description = "商品 spuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "30563") + @NotNull(message = "商品 spuId 不能为空") + private Long spuId; + + @Schema(description = "商品 skuId", requiredMode = Schema.RequiredMode.REQUIRED, example = "30563") + @NotNull(message = "商品 skuId 不能为空") + private Long skuId; + + @Schema(description = "拼团价格,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "27682") + @NotNull(message = "拼团价格,单位分不能为空") + private Integer activePrice; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductCreateReqVO.java new file mode 100644 index 0000000..f29590a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 拼团商品创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CombinationProductCreateReqVO extends CombinationProductBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductExcelVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductExcelVO.java new file mode 100644 index 0000000..9ea5167 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductExcelVO.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.product; + +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +// TODO @puhui999:可以考虑删除 excel 导出哈 +/** + * 拼团商品 Excel VO + * + * @author HUIHUI + */ +@Data +public class CombinationProductExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("拼团活动编号") + private Long activityId; + + @ExcelProperty("商品 SPU 编号") + private Long spuId; + + @ExcelProperty("商品 SKU 编号") + private Long skuId; + + @ExcelProperty("拼团商品状态") + private Integer activityStatus; + + @ExcelProperty("活动开始时间点") + private LocalDateTime activityStartTime; + + @ExcelProperty("活动结束时间点") + private LocalDateTime activityEndTime; + + @ExcelProperty("拼团价格,单位分") + private Integer activePrice; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductExportReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductExportReqVO.java new file mode 100644 index 0000000..ebcb2e4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductExportReqVO.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +// TODO @puhui999:可以考虑删除 excel 导出哈 +@Schema(description = "管理后台 - 拼团商品 Excel 导出 Request VO,参数和 CombinationProductPageReqVO 是一致的") +@Data +public class CombinationProductExportReqVO { + + @Schema(description = "拼团活动编号", example = "6829") + private Long activityId; + + @Schema(description = "商品 SPU 编号", example = "18731") + private Long spuId; + + @Schema(description = "商品 SKU 编号", example = "31675") + private Long skuId; + + @Schema(description = "拼团商品状态", example = "2") + private Integer activityStatus; + + @Schema(description = "活动开始时间点") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] activityStartTime; + + @Schema(description = "活动结束时间点") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] activityEndTime; + + @Schema(description = "拼团价格,单位分", example = "27682") + private Integer activePrice; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductPageReqVO.java new file mode 100644 index 0000000..18abf9b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductPageReqVO.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.product; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 拼团商品分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CombinationProductPageReqVO extends PageParam { + + @Schema(description = "拼团活动编号", example = "6829") + private Long activityId; + + @Schema(description = "商品 SPU 编号", example = "18731") + private Long spuId; + + @Schema(description = "商品 SKU 编号", example = "31675") + private Long skuId; + + @Schema(description = "拼团商品状态", example = "2") + private Integer activityStatus; + + @Schema(description = "活动开始时间点") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] activityStartTime; + + @Schema(description = "活动结束时间点") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] activityEndTime; + + @Schema(description = "拼团价格,单位分", example = "27682") + private Integer activePrice; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductRespVO.java new file mode 100644 index 0000000..311caef --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 拼团商品 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CombinationProductRespVO extends CombinationProductBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "28322") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductUpdateReqVO.java new file mode 100644 index 0000000..7084990 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/combination/vo/product/CombinationProductUpdateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.promotion.controller.admin.combination.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 拼团商品更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CombinationProductUpdateReqVO extends CombinationProductBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/CouponController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/CouponController.java new file mode 100644 index 0000000..64d1959 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/CouponController.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.module.member.api.user.MemberUserApi; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon.CouponPageItemRespVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; +import com.yunxi.scm.module.promotion.convert.coupon.CouponConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; +import com.yunxi.scm.module.promotion.service.coupon.CouponService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Map; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 优惠劵") +@RestController +@RequestMapping("/promotion/coupon") +@Validated +public class CouponController { + + @Resource + private CouponService couponService; + @Resource + private MemberUserApi memberUserApi; + +// @GetMapping("/get") +// @Operation(summary = "获得优惠劵") +// @Parameter(name = "id", description = "编号", required = true, example = "1024") +// @PreAuthorize("@ss.hasPermission('promotion:coupon:query')") +// public CommonResult getCoupon(@RequestParam("id") Long id) { +// CouponDO coupon = couponService.getCoupon(id); +// return success(CouponConvert.INSTANCE.convert(coupon)); +// } + + @DeleteMapping("/delete") + @Operation(summary = "回收优惠劵") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:coupon:delete')") + public CommonResult deleteCoupon(@RequestParam("id") Long id) { + couponService.deleteCoupon(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得优惠劵分页") + @PreAuthorize("@ss.hasPermission('promotion:coupon:query')") + public CommonResult> getCouponPage(@Valid CouponPageReqVO pageVO) { + PageResult pageResult = couponService.getCouponPage(pageVO); + PageResult pageResulVO = CouponConvert.INSTANCE.convertPage(pageResult); + if (CollUtil.isEmpty(pageResulVO.getList())) { + return success(pageResulVO); + } + // 读取用户信息,进行拼接 + Set userIds = convertSet(pageResult.getList(), CouponDO::getUserId); + Map userMap = memberUserApi.getUserMap(userIds); + pageResulVO.getList().forEach(itemRespVO -> MapUtils.findAndThen(userMap, itemRespVO.getUserId(), + userRespDTO -> itemRespVO.setNickname(userRespDTO.getNickname()))); + return success(pageResulVO); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/CouponTemplateController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/CouponTemplateController.java new file mode 100644 index 0000000..e0b00a0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/CouponTemplateController.java @@ -0,0 +1,79 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.*; +import com.yunxi.scm.module.promotion.convert.coupon.CouponTemplateConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponTemplateDO; +import com.yunxi.scm.module.promotion.service.coupon.CouponTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 优惠劵模板") +@RestController +@RequestMapping("/promotion/coupon-template") +@Validated +public class CouponTemplateController { + + @Resource + private CouponTemplateService couponTemplateService; + + @PostMapping("/create") + @Operation(summary = "创建优惠劵模板") + @PreAuthorize("@ss.hasPermission('promotion:coupon-template:create')") + public CommonResult createCouponTemplate(@Valid @RequestBody CouponTemplateCreateReqVO createReqVO) { + return success(couponTemplateService.createCouponTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新优惠劵模板") + @PreAuthorize("@ss.hasPermission('promotion:coupon-template:update')") + public CommonResult updateCouponTemplate(@Valid @RequestBody CouponTemplateUpdateReqVO updateReqVO) { + couponTemplateService.updateCouponTemplate(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新优惠劵模板状态") + @PreAuthorize("@ss.hasPermission('promotion:coupon-template:update')") + public CommonResult updateCouponTemplateStatus(@Valid @RequestBody CouponTemplateUpdateStatusReqVO reqVO) { + couponTemplateService.updateCouponTemplateStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除优惠劵模板") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:coupon-template:delete')") + public CommonResult deleteCouponTemplate(@RequestParam("id") Long id) { + couponTemplateService.deleteCouponTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得优惠劵模板") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('promotion:coupon-template:query')") + public CommonResult getCouponTemplate(@RequestParam("id") Long id) { + CouponTemplateDO couponTemplate = couponTemplateService.getCouponTemplate(id); + return success(CouponTemplateConvert.INSTANCE.convert(couponTemplate)); + } + + @GetMapping("/page") + @Operation(summary = "获得优惠劵模板分页") + @PreAuthorize("@ss.hasPermission('promotion:coupon-template:query')") + public CommonResult> getCouponTemplatePage(@Valid CouponTemplatePageReqVO pageVO) { + PageResult pageResult = couponTemplateService.getCouponTemplatePage(pageVO); + return success(CouponTemplateConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponBaseVO.java new file mode 100644 index 0000000..47167f1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponBaseVO.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static com.yunxi.scm.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** +* 优惠劵 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class CouponBaseVO { + + // ========== 基本信息 BEGIN ========== + @Schema(description = "优惠劵模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "优惠劵模板编号不能为空") + private Integer templateId; + + @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送") + @NotNull(message = "优惠劵名不能为空") + private String name; + + @Schema(description = "优惠码状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + // ========== 基本信息 END ========== + + // ========== 领取情况 BEGIN ========== + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "领取方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "领取方式不能为空") + private Integer takeType; + // ========== 领取情况 END ========== + + // ========== 使用规则 BEGIN ========== + @Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 单位:分;0 - 不限制 + @NotNull(message = "是否设置满多少金额可用不能为空") + private Integer usePrice; + + @Schema(description = "固定日期 - 生效开始时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime validStartTime; + + @Schema(description = "固定日期 - 生效结束时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime validEndTime; + + @Schema(description = "商品范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品范围不能为空") + @InEnum(PromotionProductScopeEnum.class) + private Integer productScope; + + @Schema(description = "商品 SPU 编号的数组", example = "1,3") + private List productSpuIds; + // ========== 使用规则 END ========== + + // ========== 使用效果 BEGIN ========== + @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优惠类型不能为空") + @InEnum(PromotionDiscountTypeEnum.class) + private Integer discountType; + + @Schema(description = "折扣百分比", example = "80") // 例如说,80% 为 80 + private Integer discountPercent; + + @Schema(description = "优惠金额", example = "10") + @Min(value = 0, message = "优惠金额需要大于等于 0") + private Integer discountPrice; + + @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 + private Integer discountLimitPrice; + // ========== 使用效果 END ========== + + // ========== 使用情况 BEGIN ========== + + @Schema(description = "使用订单号", example = "4096") + private Long useOrderId; + + @Schema(description = "使用时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime useTime; + + // ========== 使用情况 END ========== + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponPageItemRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponPageItemRespVO.java new file mode 100644 index 0000000..4f42a72 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponPageItemRespVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 优惠劵分页的每一项 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CouponPageItemRespVO extends CouponRespVO { + + @Schema(description = "用户昵称", example = "老芋艿") + private String nickname; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponPageReqVO.java new file mode 100644 index 0000000..57a0690 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponPageReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 优惠劵分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CouponPageReqVO extends PageParam { + + @Schema(description = "优惠劵模板编号", example = "2048") + private Long templateId; + + @Schema(description = "优惠码状态", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "用户昵称", example = "芋艿") + private String nickname; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponRespVO.java new file mode 100644 index 0000000..d9c417e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/coupon/CouponRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 优惠劵 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CouponRespVO extends CouponBaseVO { + + @Schema(description = "优惠劵编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java new file mode 100644 index 0000000..edf31f1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateBaseVO.java @@ -0,0 +1,154 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static com.yunxi.scm.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +/** +* 优惠劵模板 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class CouponTemplateBaseVO { + + @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送") + @NotNull(message = "优惠劵名不能为空") + private String name; + + @Schema(description = "发行总量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") // -1 - 则表示不限制发放数量 + @NotNull(message = "发行总量不能为空") + private Integer totalCount; + + @Schema(description = "每人限领个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") // -1 - 则表示不限制 + @NotNull(message = "每人限领个数不能为空") + private Integer takeLimitCount; + + @Schema(description = "领取方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "领取方式不能为空") + private Integer takeType; + + @Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 单位:分;0 - 不限制 + @NotNull(message = "是否设置满多少金额可用不能为空") + private Integer usePrice; + + @Schema(description = "商品范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品范围不能为空") + @InEnum(PromotionProductScopeEnum.class) + private Integer productScope; + + @Schema(description = "商品 SPU 编号的数组", example = "1,3") + private List productSpuIds; + + @Schema(description = "生效日期类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "生效日期类型不能为空") + @InEnum(CouponTemplateValidityTypeEnum.class) + private Integer validityType; + + @Schema(description = "固定日期 - 生效开始时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime validStartTime; + + @Schema(description = "固定日期 - 生效结束时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime validEndTime; + + @Schema(description = "领取日期 - 开始天数") + @Min(value = 0L, message = "开始天数必须大于 0") + private Integer fixedStartTerm; + + @Schema(description = "领取日期 - 结束天数") + @Min(value = 1L, message = "开始天数必须大于 1") + private Integer fixedEndTerm; + + @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优惠类型不能为空") + @InEnum(PromotionDiscountTypeEnum.class) + private Integer discountType; + + @Schema(description = "折扣百分比", example = "80") // 例如说,80% 为 80 + private Integer discountPercent; + + @Schema(description = "优惠金额", example = "10") + @Min(value = 0, message = "优惠金额需要大于等于 0") + private Integer discountPrice; + + @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 + private Integer discountLimitPrice; + + @AssertTrue(message = "商品 SPU 编号的数组不能为空") + @JsonIgnore + public boolean isProductSpuIdsValid() { + return Objects.equals(productScope, PromotionProductScopeEnum.ALL.getScope()) // 全部范围时,可以为空 + || CollUtil.isNotEmpty(productSpuIds); + } + + @AssertTrue(message = "生效开始时间不能为空") + @JsonIgnore + public boolean isValidStartTimeValid() { + return ObjectUtil.notEqual(validityType, CouponTemplateValidityTypeEnum.DATE.getType()) + || validStartTime != null; + } + + @AssertTrue(message = "生效结束时间不能为空") + @JsonIgnore + public boolean isValidEndTimeValid() { + return ObjectUtil.notEqual(validityType, CouponTemplateValidityTypeEnum.DATE.getType()) + || validEndTime != null; + } + + @AssertTrue(message = "开始天数不能为空") + @JsonIgnore + public boolean isFixedStartTermValid() { + return ObjectUtil.notEqual(validityType, CouponTemplateValidityTypeEnum.TERM.getType()) + || fixedStartTerm != null; + } + + @AssertTrue(message = "结束天数不能为空") + @JsonIgnore + public boolean isFixedEndTermValid() { + return ObjectUtil.notEqual(validityType, CouponTemplateValidityTypeEnum.TERM.getType()) + || fixedEndTerm != null; + } + + @AssertTrue(message = "折扣百分比需要大于等于 1,小于等于 99") + @JsonIgnore + public boolean isDiscountPercentValid() { + return ObjectUtil.notEqual(discountType, PromotionDiscountTypeEnum.PERCENT.getType()) + || (discountPercent != null && discountPercent >= 1 && discountPercent<= 99); + } + + @AssertTrue(message = "优惠金额不能为空") + @JsonIgnore + public boolean isDiscountPriceValid() { + return ObjectUtil.notEqual(discountType, PromotionDiscountTypeEnum.PRICE.getType()) + || discountPrice != null; + } + + @AssertTrue(message = "折扣上限不能为空") + @JsonIgnore + public boolean isDiscountLimitPriceValid() { + return ObjectUtil.notEqual(discountType, PromotionDiscountTypeEnum.PERCENT.getType()) + || discountLimitPrice != null; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateCreateReqVO.java new file mode 100644 index 0000000..8b6c242 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 优惠劵模板创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CouponTemplateCreateReqVO extends CouponTemplateBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplatePageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplatePageReqVO.java new file mode 100644 index 0000000..9774dc6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplatePageReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 优惠劵模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CouponTemplatePageReqVO extends PageParam { + + @Schema(description = "优惠劵名", example = "你好") + private String name; + + @Schema(description = "状态", example = "1") + private Integer status; + + @Schema(description = "优惠类型", example = "1") + private Integer discountType; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateRespVO.java new file mode 100644 index 0000000..24f38ff --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateRespVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 优惠劵模板 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CouponTemplateRespVO extends CouponTemplateBaseVO { + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "领取优惠券的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer takeCount; + + @Schema(description = "使用优惠券的次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Integer useCount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateUpdateReqVO.java new file mode 100644 index 0000000..1fd4a61 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 优惠劵模板更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class CouponTemplateUpdateReqVO extends CouponTemplateBaseVO { + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "模板编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateUpdateStatusReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateUpdateStatusReqVO.java new file mode 100644 index 0000000..da9a3df --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/coupon/vo/template/CouponTemplateUpdateStatusReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 优惠劵模板更新状态 Request VO") +@Data +public class CouponTemplateUpdateStatusReqVO { + + @Schema(description = "优惠劵模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "优惠劵模板编号不能为空") + private Long id; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/DecorateComponentController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/DecorateComponentController.java new file mode 100644 index 0000000..d6ac4c7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/DecorateComponentController.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.promotion.controller.admin.decorate; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.promotion.controller.admin.decorate.vo.DecorateComponentSaveReqVO; +import com.yunxi.scm.module.promotion.controller.admin.decorate.vo.DecorateComponentRespVO; +import com.yunxi.scm.module.promotion.enums.decorate.DecoratePageEnum; +import com.yunxi.scm.module.promotion.service.decorate.DecorateComponentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.module.promotion.convert.decorate.DecorateComponentConvert.INSTANCE; + +@Tag(name = "管理后台 - 店铺页面装修") +@RestController +@RequestMapping("/promotion/decorate") +@Validated +public class DecorateComponentController { + @Resource + private DecorateComponentService decorateComponentService; + + @PostMapping("/page-save") + @Operation(summary = "保存页面装修") + // TODO 加权限 + public CommonResult savePageComponents(@Valid @RequestBody DecorateComponentSaveReqVO reqVO) { + decorateComponentService.savePageComponents(reqVO); + return success(true); + } + + @GetMapping("/get-page-components") + @Operation(summary = "获取装修页面组件") + @Parameter(name = "pageId", description = "页面 id", required = true) + // TODO 加权限 + public CommonResult getPageComponents( + @RequestParam("pageId") @InEnum(DecoratePageEnum.class) Integer pageId) { + return success(INSTANCE.convert2(pageId, decorateComponentService.getPageComponents(pageId))); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/vo/DecorateComponentRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/vo/DecorateComponentRespVO.java new file mode 100644 index 0000000..eeb67d6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/vo/DecorateComponentRespVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.promotion.controller.admin.decorate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 页面装修 Resp VO") +@Data +public class DecorateComponentRespVO { + + @Schema(description = "页面 id ", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer pageId; + + @Schema(description = "页面组件", requiredMode = Schema.RequiredMode.REQUIRED, example = "TODO") + private List components; + + @Schema(description = "管理后台 - 页面组件 Resp VO") + @Data + public static class ComponentRespVO { + + @Schema(description = "组件编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "组件编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "nav-menu") + private String code; + + @Schema(description = "组件的内容配置项", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "TODO") + private String value; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/vo/DecorateComponentSaveReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/vo/DecorateComponentSaveReqVO.java new file mode 100644 index 0000000..5e3f8fb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/decorate/vo/DecorateComponentSaveReqVO.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.promotion.controller.admin.decorate.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.promotion.enums.decorate.DecoratePageEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 页面装修的保存 Request VO ") +@Data +public class DecorateComponentSaveReqVO { + + @Schema(description = "页面 id ", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "页面 id 不能为空") + @InEnum(DecoratePageEnum.class) + private Integer pageId; + + @Schema(description = "页面组件列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "TODO") + @NotEmpty(message = "页面组件列表不能为空") + @Valid + private List components; + + @Schema(description = "管理后台 - 页面装修组件 Request VO") + @Data + public static class ComponentReqVO { + + @Schema(description = "组件编码", example = "1") + private Long id; + + @Schema(description = "组件编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "nav-menu") + @NotEmpty(message = "组件编码不能为空") + private String code; + + @Schema(description = "组件对应值, json 字符串, 含内容配置,具体数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "TODO") + @NotEmpty(message = "组件值为空") + private String value; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/DiscountActivityController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/DiscountActivityController.java new file mode 100644 index 0000000..37cb1fd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/DiscountActivityController.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.module.promotion.controller.admin.discount; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.*; +import com.yunxi.scm.module.promotion.convert.discount.DiscountActivityConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountProductDO; +import com.yunxi.scm.module.promotion.service.discount.DiscountActivityService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 限时折扣活动") +@RestController +@RequestMapping("/promotion/discount-activity") +@Validated +public class DiscountActivityController { + + @Resource + private DiscountActivityService discountActivityService; + + @PostMapping("/create") + @Operation(summary = "创建限时折扣活动") + @PreAuthorize("@ss.hasPermission('promotion:discount-activity:create')") + public CommonResult createDiscountActivity(@Valid @RequestBody DiscountActivityCreateReqVO createReqVO) { + return success(discountActivityService.createDiscountActivity(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新限时折扣活动") + @PreAuthorize("@ss.hasPermission('promotion:discount-activity:update')") + public CommonResult updateDiscountActivity(@Valid @RequestBody DiscountActivityUpdateReqVO updateReqVO) { + discountActivityService.updateDiscountActivity(updateReqVO); + return success(true); + } + + @PutMapping("/close") + @Operation(summary = "关闭限时折扣活动") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:discount-activity:close')") + public CommonResult closeRewardActivity(@RequestParam("id") Long id) { + discountActivityService.closeRewardActivity(id); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除限时折扣活动") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:discount-activity:delete')") + public CommonResult deleteDiscountActivity(@RequestParam("id") Long id) { + discountActivityService.deleteDiscountActivity(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得限时折扣活动") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('promotion:discount-activity:query')") + public CommonResult getDiscountActivity(@RequestParam("id") Long id) { + DiscountActivityDO discountActivity = discountActivityService.getDiscountActivity(id); + if (discountActivity == null) { + return success(null); + } + // 拼接结果 + List discountProducts = discountActivityService.getDiscountProductsByActivityId(id); + return success(DiscountActivityConvert.INSTANCE.convert(discountActivity, discountProducts)); + } + + @GetMapping("/page") + @Operation(summary = "获得限时折扣活动分页") + @PreAuthorize("@ss.hasPermission('promotion:discount-activity:query')") + public CommonResult> getDiscountActivityPage(@Valid DiscountActivityPageReqVO pageVO) { + PageResult pageResult = discountActivityService.getDiscountActivityPage(pageVO); + return success(DiscountActivityConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityBaseVO.java new file mode 100644 index 0000000..7501df0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityBaseVO.java @@ -0,0 +1,81 @@ +package com.yunxi.scm.module.promotion.controller.admin.discount.vo; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* 限时折扣活动 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DiscountActivityBaseVO { + + @Schema(description = "活动标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "一个标题") + @NotNull(message = "活动标题不能为空") + private String name; + + @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开始时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime startTime; + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "结束时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endTime; + + @Schema(description = "备注", example = "我是备注") + private String remark; + + @Schema(description = "商品") + @Data + public static class Product { + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品 SPU 编号不能为空") + private Long spuId; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "优惠类型不能为空") + @InEnum(PromotionDiscountTypeEnum.class) + private Integer discountType; + + @Schema(description = "折扣百分比", example = "80") // 例如说,80% 为 80 + private Integer discountPercent; + + @Schema(description = "优惠金额", example = "10") + @Min(value = 0, message = "优惠金额需要大于等于 0") + private Integer discountPrice; + + @AssertTrue(message = "折扣百分比需要大于等于 1,小于等于 99") + @JsonIgnore + public boolean isDiscountPercentValid() { + return ObjectUtil.notEqual(discountType, PromotionDiscountTypeEnum.PERCENT.getType()) + || (discountPercent != null && discountPercent >= 1 && discountPercent<= 99); + } + + @AssertTrue(message = "优惠金额不能为空") + @JsonIgnore + public boolean isDiscountPriceValid() { + return ObjectUtil.notEqual(discountType, PromotionDiscountTypeEnum.PRICE.getType()) + || discountPrice != null; + } + + } +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityCreateReqVO.java new file mode 100644 index 0000000..8ce3c76 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityCreateReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.promotion.controller.admin.discount.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import java.util.List; + +@Schema(description = "管理后台 - 限时折扣活动创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DiscountActivityCreateReqVO extends DiscountActivityBaseVO { + + /** + * 商品列表 + */ + @NotEmpty(message = "商品列表不能为空") + @Valid + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityDetailRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityDetailRespVO.java new file mode 100644 index 0000000..907b1a7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityDetailRespVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.promotion.controller.admin.discount.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 限时折扣活动的详细 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DiscountActivityDetailRespVO extends DiscountActivityRespVO { + + /** + * 商品列表 + */ + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityPageReqVO.java new file mode 100644 index 0000000..679ca4e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityPageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.promotion.controller.admin.discount.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 限时折扣活动分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DiscountActivityPageReqVO extends PageParam { + + @Schema(description = "活动标题", example = "一个标题") + private String name; + + @Schema(description = "活动状态", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityRespVO.java new file mode 100644 index 0000000..61a4371 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityRespVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.promotion.controller.admin.discount.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 限时折扣活动 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DiscountActivityRespVO extends DiscountActivityBaseVO { + + @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "活动状态不能为空") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityUpdateReqVO.java new file mode 100644 index 0000000..26d69e1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/discount/vo/DiscountActivityUpdateReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.promotion.controller.admin.discount.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 限时折扣活动更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DiscountActivityUpdateReqVO extends DiscountActivityBaseVO { + + @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "活动编号不能为空") + private Long id; + + /** + * 商品列表 + */ + @NotEmpty(message = "商品列表不能为空") + @Valid + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/RewardActivityController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/RewardActivityController.java new file mode 100644 index 0000000..f917b30 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/RewardActivityController.java @@ -0,0 +1,83 @@ +package com.yunxi.scm.module.promotion.controller.admin.reward; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityRespVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.reward.RewardActivityConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.reward.RewardActivityDO; +import com.yunxi.scm.module.promotion.service.reward.RewardActivityService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 满减送活动") +@RestController +@RequestMapping("/promotion/reward-activity") +@Validated +public class RewardActivityController { + + @Resource + private RewardActivityService rewardActivityService; + + @PostMapping("/create") + @Operation(summary = "创建满减送活动") + @PreAuthorize("@ss.hasPermission('promotion:reward-activity:create')") + public CommonResult createRewardActivity(@Valid @RequestBody RewardActivityCreateReqVO createReqVO) { + return success(rewardActivityService.createRewardActivity(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新满减送活动") + @PreAuthorize("@ss.hasPermission('promotion:reward-activity:update')") + public CommonResult updateRewardActivity(@Valid @RequestBody RewardActivityUpdateReqVO updateReqVO) { + rewardActivityService.updateRewardActivity(updateReqVO); + return success(true); + } + + @PutMapping("/close") + @Operation(summary = "关闭满减送活动") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:reward-activity:close')") + public CommonResult closeRewardActivity(@RequestParam("id") Long id) { + rewardActivityService.closeRewardActivity(id); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除满减送活动") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:reward-activity:delete')") + public CommonResult deleteRewardActivity(@RequestParam("id") Long id) { + rewardActivityService.deleteRewardActivity(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得满减送活动") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") + public CommonResult getRewardActivity(@RequestParam("id") Long id) { + RewardActivityDO rewardActivity = rewardActivityService.getRewardActivity(id); + return success(RewardActivityConvert.INSTANCE.convert(rewardActivity)); + } + + @GetMapping("/page") + @Operation(summary = "获得满减送活动分页") + @PreAuthorize("@ss.hasPermission('promotion:reward-activity:query')") + public CommonResult> getRewardActivityPage(@Valid RewardActivityPageReqVO pageVO) { + PageResult pageResult = rewardActivityService.getRewardActivityPage(pageVO); + return success(RewardActivityConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java new file mode 100644 index 0000000..c878575 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityBaseVO.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.promotion.controller.admin.reward.vo; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionConditionTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.Future; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* 满减送活动 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class RewardActivityBaseVO { + + @Schema(description = "活动标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "满啦满啦") + @NotNull(message = "活动标题不能为空") + private String name; + + @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开始时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime startTime; + + @Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "结束时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Future(message = "结束时间必须大于当前时间") + private LocalDateTime endTime; + + @Schema(description = "备注", example = "biubiubiu") + private String remark; + + @Schema(description = "条件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "条件类型不能为空") + @InEnum(value = PromotionConditionTypeEnum.class, message = "条件类型必须是 {value}") + private Integer conditionType; + + @Schema(description = "商品范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品范围不能为空") + @InEnum(value = PromotionConditionTypeEnum.class, message = "商品范围必须是 {value}") + private Integer productScope; + + @Schema(description = "商品 SPU 编号的数组", example = "1,2,3") + private List productSpuIds; + + /** + * 优惠规则的数组 + */ + @Valid // 校验下子对象 + private List rules; + + @Schema(description = "优惠规则") + @Data + public static class Rule { + + @Schema(description = "优惠门槛", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") // 1. 满 N 元,单位:分; 2. 满 N 件 + @Min(value = 1L, message = "优惠门槛必须大于等于 1") + private Integer limit; + + @Schema(description = "优惠价格", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @Min(value = 1L, message = "优惠价格必须大于等于 1") + private Integer discountPrice; + + @Schema(description = "是否包邮", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean freeDelivery; + + @Schema(description = "赠送的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @Min(value = 1L, message = "赠送的积分必须大于等于 1") + private Integer point; + + @Schema(description = "赠送的优惠劵编号的数组", example = "1,2,3") + private List couponIds; + + @Schema(description = "赠送的优惠卷数量的数组", example = "1,2,3") + private List couponCounts; + + @AssertTrue(message = "优惠劵和数量必须一一对应") + @JsonIgnore + public boolean isCouponCountsValid() { + return CollUtil.size(couponCounts) == CollUtil.size(couponCounts); + } + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityCreateReqVO.java new file mode 100644 index 0000000..007e8d0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.promotion.controller.admin.reward.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 满减送活动创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RewardActivityCreateReqVO extends RewardActivityBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityPageReqVO.java new file mode 100644 index 0000000..eed24b5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityPageReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.promotion.controller.admin.reward.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 满减送活动分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RewardActivityPageReqVO extends PageParam { + + @Schema(description = "活动标题", example = "满啦满啦") + private String name; + + @Schema(description = "活动状态", example = "1") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityRespVO.java new file mode 100644 index 0000000..9567eb6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.promotion.controller.admin.reward.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 满减送活动 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RewardActivityRespVO extends RewardActivityBaseVO { + + @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer id; + + @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityUpdateReqVO.java new file mode 100644 index 0000000..09c1a59 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/reward/vo/RewardActivityUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.promotion.controller.admin.reward.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 满减送活动更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class RewardActivityUpdateReqVO extends RewardActivityBaseVO { + + @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "活动编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/SeckillActivityController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/SeckillActivityController.java new file mode 100644 index 0000000..aed0d61 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/SeckillActivityController.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.product.api.spu.ProductSpuApi; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.*; +import com.yunxi.scm.module.promotion.convert.seckill.seckillactivity.SeckillActivityConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillProductDO; +import com.yunxi.scm.module.promotion.service.seckill.seckillactivity.SeckillActivityService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 秒杀活动") +@RestController +@RequestMapping("/promotion/seckill-activity") +@Validated +public class SeckillActivityController { + + @Resource + private SeckillActivityService seckillActivityService; + @Resource + private ProductSpuApi spuApi; + + @PostMapping("/create") + @Operation(summary = "创建秒杀活动") + @PreAuthorize("@ss.hasPermission('promotion:seckill-activity:create')") + public CommonResult createSeckillActivity(@Valid @RequestBody SeckillActivityCreateReqVO createReqVO) { + return success(seckillActivityService.createSeckillActivity(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新秒杀活动") + @PreAuthorize("@ss.hasPermission('promotion:seckill-activity:update')") + public CommonResult updateSeckillActivity(@Valid @RequestBody SeckillActivityUpdateReqVO updateReqVO) { + seckillActivityService.updateSeckillActivity(updateReqVO); + return success(true); + } + + @PutMapping("/close") + @Operation(summary = "关闭秒杀活动") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:seckill-activity:close')") + public CommonResult closeSeckillActivity(@RequestParam("id") Long id) { + seckillActivityService.closeSeckillActivity(id); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除秒杀活动") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:seckill-activity:delete')") + public CommonResult deleteSeckillActivity(@RequestParam("id") Long id) { + seckillActivityService.deleteSeckillActivity(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得秒杀活动") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('promotion:seckill-activity:query')") + public CommonResult getSeckillActivity(@RequestParam("id") Long id) { + SeckillActivityDO seckillActivity = seckillActivityService.getSeckillActivity(id); + List seckillProducts = seckillActivityService.getSeckillProductListByActivityId(id); + return success(SeckillActivityConvert.INSTANCE.convert(seckillActivity, seckillProducts)); + } + + @GetMapping("/list") + @Operation(summary = "获得秒杀活动列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('promotion:seckill-activity:query')") + public CommonResult> getSeckillActivityList(@RequestParam("ids") Collection ids) { + List list = seckillActivityService.getSeckillActivityList(ids); + return success(SeckillActivityConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得秒杀活动分页") + @PreAuthorize("@ss.hasPermission('promotion:seckill-activity:query')") + public CommonResult> getSeckillActivityPage(@Valid SeckillActivityPageReqVO pageVO) { + PageResult pageResult = seckillActivityService.getSeckillActivityPage(pageVO); + Set aIds = CollectionUtils.convertSet(pageResult.getList(), SeckillActivityDO::getId); + List seckillProducts = seckillActivityService.getSeckillProductListByActivityId(aIds); + Set spuIds = CollectionUtils.convertSet(pageResult.getList(), SeckillActivityDO::getSpuId); + List spuList = spuApi.getSpuList(spuIds); + return success(SeckillActivityConvert.INSTANCE.convertPage(pageResult, seckillProducts, spuList)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/SeckillConfigController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/SeckillConfigController.java new file mode 100644 index 0000000..c0e3dfb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/SeckillConfigController.java @@ -0,0 +1,94 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.*; +import com.yunxi.scm.module.promotion.convert.seckill.seckillconfig.SeckillConfigConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillconfig.SeckillConfigDO; +import com.yunxi.scm.module.promotion.service.seckill.seckillconfig.SeckillConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 秒杀时段") +@RestController +@RequestMapping("/promotion/seckill-config") +@Validated +public class SeckillConfigController { + + @Resource + private SeckillConfigService seckillConfigService; + + @PostMapping("/create") + @Operation(summary = "创建秒杀时段") + @PreAuthorize("@ss.hasPermission('promotion:seckill-config:create')") + public CommonResult createSeckillConfig(@Valid @RequestBody SeckillConfigCreateReqVO createReqVO) { + return success(seckillConfigService.createSeckillConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新秒杀时段") + @PreAuthorize("@ss.hasPermission('promotion:seckill-config:update')") + public CommonResult updateSeckillConfig(@Valid @RequestBody SeckillConfigUpdateReqVO updateReqVO) { + seckillConfigService.updateSeckillConfig(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "修改时段配置状态") + @PreAuthorize("@ss.hasPermission('system:seckill-config:update')") + public CommonResult updateSeckillConfigStatus(@Valid @RequestBody SeckillConfigUpdateStatusReqVo reqVO) { + seckillConfigService.updateSeckillConfigStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除秒杀时段") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('promotion:seckill-config:delete')") + public CommonResult deleteSeckillConfig(@RequestParam("id") Long id) { + seckillConfigService.deleteSeckillConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得秒杀时段") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('promotion:seckill-config:query')") + public CommonResult getSeckillConfig(@RequestParam("id") Long id) { + SeckillConfigDO seckillConfig = seckillConfigService.getSeckillConfig(id); + return success(SeckillConfigConvert.INSTANCE.convert(seckillConfig)); + } + + @GetMapping("/list") + @Operation(summary = "获得所有秒杀时段列表") + @PreAuthorize("@ss.hasPermission('promotion:seckill-config:query')") + public CommonResult> getSeckillConfigList() { + List list = seckillConfigService.getSeckillConfigList(); + return success(SeckillConfigConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获得所有开启状态的秒杀时段精简列表", description = "主要用于前端的下拉选项") + public CommonResult> getListAllSimple() { + List list = seckillConfigService.getListAllSimple(); + return success(SeckillConfigConvert.INSTANCE.convertList1(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得秒杀活动分页") + @PreAuthorize("@ss.hasPermission('promotion:seckill-config:query')") + public CommonResult> getSeckillActivityPage(@Valid SeckillConfigPageReqVO pageVO) { + PageResult pageResult = seckillConfigService.getSeckillConfigPage(pageVO); + return success(SeckillConfigConvert.INSTANCE.convertPage(pageResult)); + } +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityBaseVO.java new file mode 100644 index 0000000..c98fd7f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityBaseVO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 秒杀活动基地签证官 + * 秒杀活动 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author HUIHUI + */ +@Data +public class SeckillActivityBaseVO { + + @Schema(description = "秒杀活动商品id", requiredMode = Schema.RequiredMode.REQUIRED, example = "[121,1212]") + @NotNull(message = "秒杀活动商品不能为空") + private Long spuId; + + @Schema(description = "秒杀活动名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "618大促") + @NotNull(message = "秒杀活动名称不能为空") + private String name; + + @Schema(description = "备注", example = "清仓大甩卖割韭菜") + private String remark; + + @Schema(description = "活动开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "活动开始时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime startTime; + + @Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "活动结束时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime endTime; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "排序不能为空") + private Integer sort; + + @Schema(description = "秒杀时段 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]") + @NotNull(message = "秒杀时段不能为空") + private List configIds; + + @Schema(description = "总限购数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12877") + private Integer totalLimitCount; + + @Schema(description = "单次限够数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "31683") + private Integer singleLimitCount; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityCreateReqVO.java new file mode 100644 index 0000000..94a60bc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityCreateReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity; + + +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductCreateReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 秒杀活动创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillActivityCreateReqVO extends SeckillActivityBaseVO { + + @Schema(description = "秒杀商品", requiredMode = Schema.RequiredMode.REQUIRED) + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityDetailRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityDetailRespVO.java new file mode 100644 index 0000000..fb7fbff --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityDetailRespVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity; + +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 秒杀活动的详细 Response VO") +@Data +@ToString(callSuper = true) +public class SeckillActivityDetailRespVO extends SeckillActivityBaseVO{ + + @Schema(description = "秒杀活动id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "秒杀商品", requiredMode = Schema.RequiredMode.REQUIRED) + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityPageReqVO.java new file mode 100644 index 0000000..734636a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityPageReqVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; +import static com.yunxi.scm.framework.common.util.date.DateUtils.TIME_ZONE_DEFAULT; + +@Schema(description = "管理后台 - 秒杀活动分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillActivityPageReqVO extends PageParam { + + @Schema(description = "秒杀活动名称", example = "晚九点限时秒杀") + private String name; + + @Schema(description = "活动状态", example = "进行中") + private Integer status; + + @Schema(description = "秒杀时段id", example = "1") + private Long configId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java new file mode 100644 index 0000000..96e0282 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityRespVO.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity; + +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 秒杀活动 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillActivityRespVO extends SeckillActivityBaseVO { + + @Schema(description = "商品名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "618大促") + private String spuName; + + @Schema(description = "商品主图", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + private String picUrl; + + @Schema(description = "秒杀活动id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "秒杀商品", requiredMode = Schema.RequiredMode.REQUIRED) + private List products; + + @Schema(description = "活动状态 开启:0 禁用:1", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "订单实付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "22354") + private Integer totalPrice; + + @Schema(description = "秒杀库存", example = "10") + private Integer stock; + + @Schema(description = "秒杀总库存", example = "20") + private Integer totalStock; + + @Schema(description = "新增订单数", example = "20") + private Integer orderCount; + + @Schema(description = "付款人数", example = "20") + private Integer userCount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityUpdateReqVO.java new file mode 100644 index 0000000..bd909a0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/activity/SeckillActivityUpdateReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity; + +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductUpdateReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 秒杀活动更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillActivityUpdateReqVO extends SeckillActivityBaseVO { + + @Schema(description = "秒杀活动id", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "秒杀商品", requiredMode = Schema.RequiredMode.REQUIRED) + private List products; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigBaseVO.java new file mode 100644 index 0000000..c8a5cb3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigBaseVO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; +import java.time.LocalTime; +import java.util.List; + +/** + * 秒杀时段 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author HUIHUI + */ +@Data +public class SeckillConfigBaseVO { + + @Schema(description = "秒杀时段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "早上场") + @NotNull(message = "秒杀时段名称不能为空") + private String name; + + @Schema(description = "开始时间点", requiredMode = Schema.RequiredMode.REQUIRED, example = "09:00:00") + @NotNull(message = "开始时间点不能为空") + private String startTime; + + @Schema(description = "结束时间点", requiredMode = Schema.RequiredMode.REQUIRED, example = "16:00:00") + @NotNull(message = "结束时间点不能为空") + private String endTime; + + @Schema(description = "秒杀轮播图", requiredMode = Schema.RequiredMode.REQUIRED, example = "[https://www.iocoder.cn/xx.png]") + @NotNull(message = "秒杀轮播图不能为空") + private List sliderPicUrls; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "状态不能为空") + private Integer status; + + @AssertTrue(message = "秒杀时段开始时间和结束时间不能相等") + @JsonIgnore + public boolean isValidStartTimeValid() { + return !LocalTime.parse(startTime).equals(LocalTime.parse(endTime)); + } + + @AssertTrue(message = "秒杀时段开始时间不能在结束时间之后") + @JsonIgnore + public boolean isValidEndTimeValid() { + return !LocalTime.parse(startTime).isAfter(LocalTime.parse(endTime)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigCreateReqVO.java new file mode 100644 index 0000000..1996a6d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 秒杀时段创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillConfigCreateReqVO extends SeckillConfigBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigPageReqVO.java new file mode 100644 index 0000000..852f0c5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigPageReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 秒杀时段分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillConfigPageReqVO extends PageParam { + + @Schema(description = "秒杀时段名称", example = "上午场") + private String name; + + @Schema(description = "状态", example = "0") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigRespVO.java new file mode 100644 index 0000000..af3d991 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 秒杀时段 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillConfigRespVO extends SeckillConfigBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "秒杀活动数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer seckillActivityCount; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigSimpleRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigSimpleRespVO.java new file mode 100644 index 0000000..deaeb69 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigSimpleRespVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 秒杀时段配置精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SeckillConfigSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "秒杀时段名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "早上场") + @NotNull(message = "秒杀时段名称不能为空") + private String name; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigUpdateReqVO.java new file mode 100644 index 0000000..467f0e8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 秒杀时段更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillConfigUpdateReqVO extends SeckillConfigBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigUpdateStatusReqVo.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigUpdateStatusReqVo.java new file mode 100644 index 0000000..2399d7b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/config/SeckillConfigUpdateStatusReqVo.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 修改时段配置状态 Request VO") +@Data +public class SeckillConfigUpdateStatusReqVo { + + @Schema(description = "时段配置编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "时段配置编号不能为空") + private Long id; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductBaseVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductBaseVO.java new file mode 100644 index 0000000..c0d03bd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductBaseVO.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * 秒杀参与商品 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author HUIHUI + */ +@Data +public class SeckillProductBaseVO { + + @Schema(description = "商品sku_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "30563") + @NotNull(message = "商品sku_id不能为空") + private Long skuId; + + @Schema(description = "秒杀金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "6689") + @NotNull(message = "秒杀金额,单位:分不能为空") + private Integer seckillPrice; + + @Schema(description = "秒杀库存", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "秒杀库存不能为空") + private Integer stock; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductCreateReqVO.java new file mode 100644 index 0000000..2e75769 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductCreateReqVO.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +@Schema(description = "管理后台 - 秒杀参与商品创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillProductCreateReqVO extends SeckillProductBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductRespVO.java new file mode 100644 index 0000000..fd1a6d5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 秒杀参与商品 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillProductRespVO extends SeckillProductBaseVO { + + @Schema(description = "秒杀参与商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "256") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductUpdateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductUpdateReqVO.java new file mode 100644 index 0000000..bca73d6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/admin/seckill/vo/product/SeckillProductUpdateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 秒杀参与商品更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillProductUpdateReqVO extends SeckillProductBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/activity/AppActivityController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/activity/AppActivityController.java new file mode 100644 index 0000000..46b442b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/activity/AppActivityController.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.module.promotion.controller.app.activity; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.promotion.controller.app.activity.vo.AppActivityRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.*; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 营销活动") // 用于提供跨多个活动的 HTTP 接口 +@RestController +@RequestMapping("/promotion/activity") +@Validated +public class AppActivityController { + + @GetMapping("/list-by-spu-id") + @Operation(summary = "获得单个商品,近期参与的每个活动") // 每种活动,只返回一个 + @Parameter(name = "spuId", description = "商品编号", required = true) + public CommonResult> getActivityListBySpuId(@RequestParam("spuId") Long spuId) { + // TODO 芋艿,实现 + List randomList = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 3; i++) { // 生成5个随机对象 + AppActivityRespVO vo = new AppActivityRespVO(); + vo.setId(random.nextLong()); // 随机生成一个长整型 ID + vo.setType(i + 1); // 随机生成一个介于0到2之间的整数,对应枚举类型的三种类型之一 + vo.setName(String.format("活动%d", random.nextInt(100))); // 随机生成一个类似于“活动XX”的活动名称,XX为0到99之间的随机整数 + vo.setStartTime(LocalDateTime.now()); // 随机生成一个在过去的一年内的开始时间(以毫秒为单位) + vo.setEndTime(LocalDateTime.now()); // 随机生成一个在未来的一年内的结束时间(以毫秒为单位) + randomList.add(vo); + } + return success(randomList); + } + + @GetMapping("/list-by-spu-ids") + @Operation(summary = "获得多个商品,近期参与的每个活动") // 每种活动,只返回一个;key 为 SPU 编号 + @Parameter(name = "spuIds", description = "商品编号数组", required = true) + public CommonResult>> getActivityListBySpuIds(@RequestParam("spuIds") List spuIds) { + // TODO 芋艿,实现 + List randomList = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 5; i++) { // 生成5个随机对象 + AppActivityRespVO vo = new AppActivityRespVO(); + vo.setId(random.nextLong()); // 随机生成一个长整型 ID + vo.setType(random.nextInt(3)); // 随机生成一个介于0到2之间的整数,对应枚举类型的三种类型之一 + vo.setName(String.format("活动%d", random.nextInt(100))); // 随机生成一个类似于“活动XX”的活动名称,XX为0到99之间的随机整数 + vo.setStartTime(LocalDateTime.now()); // 随机生成一个在过去的一年内的开始时间(以毫秒为单位) + vo.setEndTime(LocalDateTime.now()); // 随机生成一个在未来的一年内的结束时间(以毫秒为单位) + randomList.add(vo); + } + Map> map = new HashMap<>(); + map.put(109L, randomList); + map.put(2L, randomList); + return success(map); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/activity/vo/AppActivityRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/activity/vo/AppActivityRespVO.java new file mode 100644 index 0000000..aceceab --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/activity/vo/AppActivityRespVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.promotion.controller.app.activity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 营销活动 Response VO") +@Data +public class AppActivityRespVO { + + @Schema(description = "活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "活动类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + // 对应 PromotionTypeEnum 枚举 + private Integer type; + + @Schema(description = "活动名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "618 大促") + private String name; + + @Schema(description = "活动开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + + @Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/AppArticleCategoryController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/AppArticleCategoryController.java new file mode 100644 index 0000000..c909795 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/AppArticleCategoryController.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.promotion.controller.app.article; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.promotion.controller.app.article.vo.category.AppArticleCategoryRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 文章分类") +@RestController +@RequestMapping("/promotion/article-category") +@Validated +public class AppArticleCategoryController { + + @RequestMapping("/list") + @Operation(summary = "获得文章分类列表") + // TODO @芋艿:swagger 注解 + public CommonResult> getArticleCategoryList() { + List appArticleRespVOList = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 10; i++) { + AppArticleCategoryRespVO appArticleRespVO = new AppArticleCategoryRespVO(); + appArticleRespVO.setId((long) (i + 1)); + appArticleRespVO.setName("分类 - " + i); + appArticleRespVO.setPicUrl("https://www.iocoder.cn/" + (i + 1) + ".png"); + appArticleRespVOList.add(appArticleRespVO); + } + return success(appArticleRespVOList); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/AppArticleController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/AppArticleController.java new file mode 100644 index 0000000..a6fb353 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/AppArticleController.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.promotion.controller.app.article; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.app.article.vo.article.AppArticlePageReqVO; +import com.yunxi.scm.module.promotion.controller.app.article.vo.article.AppArticleRespVO; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 文章") +@RestController +@RequestMapping("/promotion/article") +@Validated +public class AppArticleController { + + @RequestMapping("/list") + // TODO @芋艿:swagger 注解 + public CommonResult> getArticleList(@RequestParam(value = "recommendHot", required = false) Boolean recommendHot, + @RequestParam(value = "recommendBanner", required = false) Boolean recommendBanner) { + List appArticleRespVOList = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 10; i++) { + AppArticleRespVO appArticleRespVO = new AppArticleRespVO(); + appArticleRespVO.setId((long) (i + 1)); + appArticleRespVO.setTitle("芋道源码 - " + i + "模块"); + appArticleRespVO.setAuthor("芋道源码"); + appArticleRespVO.setCategoryId((long) random.nextInt(10000)); + appArticleRespVO.setPicUrl("https://www.iocoder.cn/" + (i + 1) + ".png"); + appArticleRespVO.setIntroduction("我是简介"); + appArticleRespVO.setDescription("我是详细"); + appArticleRespVO.setCreateTime(LocalDateTime.now()); + appArticleRespVO.setBrowseCount(random.nextInt(10000)); + appArticleRespVO.setSpuId((long) random.nextInt(10000)); + appArticleRespVOList.add(appArticleRespVO); + } + return success(appArticleRespVOList); + } + + @RequestMapping("/page") + // TODO @芋艿:swagger 注解 + public CommonResult> getArticlePage(AppArticlePageReqVO pageReqVO) { + List appArticleRespVOList = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 10; i++) { + AppArticleRespVO appArticleRespVO = new AppArticleRespVO(); + appArticleRespVO.setId((long) (i + 1)); + appArticleRespVO.setTitle("芋道源码 - " + i + "模块"); + appArticleRespVO.setAuthor("芋道源码"); + appArticleRespVO.setCategoryId((long) random.nextInt(10000)); + appArticleRespVO.setPicUrl("https://www.iocoder.cn/" + (i + 1) + ".png"); + appArticleRespVO.setIntroduction("我是简介"); + appArticleRespVO.setDescription("我是详细"); + appArticleRespVO.setCreateTime(LocalDateTime.now()); + appArticleRespVO.setBrowseCount(random.nextInt(10000)); + appArticleRespVO.setSpuId((long) random.nextInt(10000)); + appArticleRespVOList.add(appArticleRespVO); + } + return success(new PageResult<>(appArticleRespVOList, 10L)); + } + + @RequestMapping("/get") + // TODO @芋艿:swagger 注解 + public CommonResult getArticlePage(@RequestParam("id") Long id) { + Random random = new Random(); + AppArticleRespVO appArticleRespVO = new AppArticleRespVO(); + appArticleRespVO.setId((long) (1)); + appArticleRespVO.setTitle("芋道源码 - " + 0 + "模块"); + appArticleRespVO.setAuthor("芋道源码"); + appArticleRespVO.setCategoryId((long) random.nextInt(10000)); + appArticleRespVO.setPicUrl("https://www.iocoder.cn/" + (0 + 1) + ".png"); + appArticleRespVO.setIntroduction("我是简介"); + appArticleRespVO.setDescription("我是详细"); + appArticleRespVO.setCreateTime(LocalDateTime.now()); + appArticleRespVO.setBrowseCount(random.nextInt(10000)); + appArticleRespVO.setSpuId((long) random.nextInt(10000)); + appArticleRespVO.setSpuId(633L); + return success(appArticleRespVO); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/article/AppArticlePageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/article/AppArticlePageReqVO.java new file mode 100644 index 0000000..f28a2e0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/article/AppArticlePageReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.promotion.controller.app.article.vo.article; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "应用 App - 文章的分页 Request VO") +@Data +public class AppArticlePageReqVO extends PageParam { + + @Schema(description = "分类编号", example = "2048") + private Long categoryId; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/article/AppArticleRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/article/AppArticleRespVO.java new file mode 100644 index 0000000..388c7be --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/article/AppArticleRespVO.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.promotion.controller.app.article.vo.article; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "应用 App - 文章 Response VO") +@Data +public class AppArticleRespVO { + + @Schema(description = "文章编号", required = true, example = "1") + private Long id; + + @Schema(description = "文章标题", required = true, example = "芋道源码 - 促销模块") + private String title; + + @Schema(description = "文章作者", required = true, example = "芋道源码") + private String author; + + @Schema(description = "分类编号", required = true, example = "2048") + private Long categoryId; + + @Schema(description = "图文封面", required = true, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "文章简介", required = true, example = "我是简介") + private String introduction; + + @Schema(description = "文章内容", required = true, example = "我是详细") + private String description; + + @Schema(description = "发布时间", required = true) + private LocalDateTime createTime; + + @Schema(description = "浏览量", required = true, example = "1024") + private Integer browseCount; + + @Schema(description = "关联的商品 SPU 编号", example = "1024") + private Long spuId; + +// TODO 芋艿:下面 2 个字段,后端要存储,前端不用返回; +// @Schema(description = "是否热卖推荐", required = true, example = "true") +// private Boolean recommendHot; +// +// @Schema(description = "是否 Banner 推荐", required = true, example = "true") +// private Boolean recommendBanner; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/category/AppArticleCategoryRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/category/AppArticleCategoryRespVO.java new file mode 100644 index 0000000..971bf52 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/article/vo/category/AppArticleCategoryRespVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.promotion.controller.app.article.vo.category; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "应用 App - 文章分类 Response VO") +@Data +public class AppArticleCategoryRespVO { + + @Schema(description = "分类编号", required = true, example = "1") + private Long id; + + @Schema(description = "分类名称", required = true, example = "技术") + private String name; + + @Schema(description = "分类图标", required = true, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + // TODO 芋艿:下面 2 个字段,后端要存储,前端不用返回; +// @Schema(description = "状态", required = true, example = "1") +// private Integer status; +// +// @Schema(description = "排序", required = true, example = "1024") +// private Integer sort; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/banner/AppBannerController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/banner/AppBannerController.java new file mode 100644 index 0000000..9433529 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/banner/AppBannerController.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.promotion.controller.app.banner; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.promotion.controller.app.banner.vo.AppBannerRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@RestController +@RequestMapping("/promotion/banner") +@Tag(name = "用户 APP - 首页 Banner") +@Validated +public class AppBannerController { + + @GetMapping("/list") + @Operation(summary = "获得 banner 列表") + // todo @芋艿:swagger 注解,待补全 + // TODO @芋艿:可以增加缓存,提升性能 + // TODO @芋艿:position = 1 时,首页;position = 10 时,拼团活动页 + public CommonResult> getBannerList(@RequestParam("position") Integer position) { + List bannerList = new ArrayList<>(); + AppBannerRespVO banner1 = new AppBannerRespVO(); + banner1.setUrl("https://www.example.com/link1"); + banner1.setPicUrl("https://api.java.crmeb.net/crmebimage/public/content/2022/08/04/0f78716213f64bfa83f191d51a832cbf73f6axavoy.jpg"); + bannerList.add(banner1); + AppBannerRespVO banner2 = new AppBannerRespVO(); + banner2.setUrl("https://www.example.com/link2"); + banner2.setPicUrl("https://api.java.crmeb.net/crmebimage/public/content/2023/01/11/be09e755268b43ee90b0db3a3e1b7132r7a6t2wvsm.jpg"); + bannerList.add(banner2); + return success(bannerList); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/banner/vo/AppBannerRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/banner/vo/AppBannerRespVO.java new file mode 100644 index 0000000..2e48f46 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/banner/vo/AppBannerRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.promotion.controller.app.banner.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - Banner Response VO") +@Data +public class AppBannerRespVO { + + @Schema(description = "跳转链接", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "跳转链接不能为空") + private String url; + + @Schema(description = "图片地址", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "图片地址不能为空") + private String picUrl; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainActivityController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainActivityController.java new file mode 100644 index 0000000..ae915ec --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainActivityController.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.module.promotion.controller.app.bargain.vo.activity.AppBargainActivityDetailRespVO; +import com.yunxi.scm.module.promotion.controller.app.bargain.vo.activity.AppBargainActivityRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 砍价活动") +@RestController +@RequestMapping("/promotion/bargain-activity") +@Validated +public class AppBargainActivityController { + + @GetMapping("/page") + @Operation(summary = "获得砍价活动活动") // TODO 芋艿:只查询进行中,且在时间范围内的 + // TODO 芋艿:缺少 swagger 注解 + public CommonResult> getBargainActivityPage(PageParam pageReqVO) { + List activityList = new ArrayList<>(); + AppBargainActivityRespVO activity1 = new AppBargainActivityRespVO(); + activity1.setId(1L); + activity1.setName("618 大砍价"); + activity1.setSpuId(2048L); + activity1.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + activity1.setMarketPrice(50); + activity1.setBargainPrice(100); + activity1.setStartTime(LocalDateTimeUtils.addTime(Duration.ofDays(-2))); + activity1.setEndTime(LocalDateTimeUtils.addTime(Duration.ofDays(1))); + activity1.setStock(10); + activityList.add(activity1); + + AppBargainActivityRespVO activity2 = new AppBargainActivityRespVO(); + activity2.setId(2L); + activity2.setName("双十一砍价"); + activity2.setSpuId(4096L); + activity2.setPicUrl("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKXMYJOomfp7cebz3cIeb8sHk3GGSIJtWEgREe3j7J1WoAbTvIOicpcNdFkWAziatBSMod8b5RyS4CQ/132"); + activity2.setMarketPrice(100); + activity2.setBargainPrice(200); + activity2.setStartTime(LocalDateTimeUtils.addTime(Duration.ofDays(-2))); + activity2.setEndTime(LocalDateTimeUtils.addTime(Duration.ofDays(1))); + activity2.setStock(0); + activityList.add(activity2); + + return success(new PageResult<>(activityList, 10L)); + } + + @GetMapping("/list") + @Operation(summary = "获得砍价活动列表", description = "用于小程序首页") + // TODO 芋艿:增加 Spring Cache + // TODO 芋艿:缺少 swagger 注解 + public CommonResult> getBargainActivityList( + @RequestParam(name = "count", defaultValue = "6") Integer count) { + List activityList = new ArrayList<>(); + AppBargainActivityRespVO activity1 = new AppBargainActivityRespVO(); + activity1.setId(1L); + activity1.setName("618 大砍价"); + activity1.setSpuId(2048L); + activity1.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + activity1.setMarketPrice(50); + activity1.setBargainPrice(100); + activityList.add(activity1); + + AppBargainActivityRespVO activity2 = new AppBargainActivityRespVO(); + activity2.setId(2L); + activity2.setName("双十一砍价"); + activity2.setSpuId(4096L); + activity2.setPicUrl("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKXMYJOomfp7cebz3cIeb8sHk3GGSIJtWEgREe3j7J1WoAbTvIOicpcNdFkWAziatBSMod8b5RyS4CQ/132"); + activity2.setMarketPrice(100); + activity2.setBargainPrice(200); + activityList.add(activity2); + + return success(activityList); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得砍价活动详情") + // TODO 芋艿:缺少 swagger 注解 + public CommonResult getBargainActivityDetail(@RequestParam("id") Long id) { + AppBargainActivityDetailRespVO activity = new AppBargainActivityDetailRespVO(); + activity.setId(2L); + activity.setName("618 大砍价"); + activity.setSpuId(2048L); + activity.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + activity.setMarketPrice(50); + activity.setBargainPrice(100); + activity.setStock(10); + activity.setUnitName("件"); + activity.setPrice(40); + activity.setStartTime(LocalDateTimeUtils.addTime(Duration.ofDays(-2))); + activity.setEndTime(LocalDateTimeUtils.addTime(Duration.ofDays(-10))); + activity.setDescription("我吃西红柿"); + activity.setSuccessCount(10); + return success(activity); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainHelpController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainHelpController.java new file mode 100644 index 0000000..d83d3a7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainHelpController.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.promotion.controller.app.bargain.vo.help.AppBargainHelpCreateReqVO; +import com.yunxi.scm.module.promotion.controller.app.bargain.vo.help.AppBargainHelpRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 砍价助力") +@RestController +@RequestMapping("/promotion/bargain-help") +@Validated +public class AppBargainHelpController { + + @PostMapping("/create") + @Operation(summary = "创建砍价助力", description = "给拼团记录砍一刀") // 返回结果为砍价金额,单位:分 + public CommonResult createBargainHelp(@RequestBody AppBargainHelpCreateReqVO reqVO) { + return success(20L); + } + + @GetMapping("/list") + @Operation(summary = "获得砍价助力列表") + // TODO 芋艿:swagger + public CommonResult> getBargainHelpList(@RequestParam("recordId") Long recordId) { + List list = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + AppBargainHelpRespVO vo = new AppBargainHelpRespVO(); + vo.setNickname("用户" + i); + vo.setAvatar("https://www.iocoder.cn/avatar/" + i + ".jpg"); + vo.setReducePrice((i + 1) * 100); + vo.setCreateTime(LocalDateTime.now()); + list.add(vo); + } + return success(list); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainRecordController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainRecordController.java new file mode 100644 index 0000000..8e003d1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/AppBargainRecordController.java @@ -0,0 +1,145 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.module.promotion.controller.app.bargain.vo.record.AppBargainRecordCreateReqVO; +import com.yunxi.scm.module.promotion.controller.app.bargain.vo.record.AppBargainRecordDetailRespVO; +import com.yunxi.scm.module.promotion.controller.app.bargain.vo.record.AppBargainRecordRespVO; +import com.yunxi.scm.module.promotion.controller.app.bargain.vo.record.AppBargainRecordSummaryRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.Duration; +import java.util.ArrayList; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 砍价记录") +@RestController +@RequestMapping("/promotion/bargain-record") +@Validated +public class AppBargainRecordController { + + @GetMapping("/get-summary") + @Operation(summary = "获得砍价记录的概要信息", description = "用于小程序首页") + // TODO 芋艿:增加 @Cache 缓存,1 分钟过期 + public CommonResult getBargainRecordSummary() { + AppBargainRecordSummaryRespVO summary = new AppBargainRecordSummaryRespVO(); + summary.setUserCount(1024); + summary.setSuccessRecords(new ArrayList<>()); + AppBargainRecordSummaryRespVO.Record record1 = new AppBargainRecordSummaryRespVO.Record(); + record1.setNickname("王**"); + record1.setAvatar("https://www.iocoder.cn/xxx.jpg"); + record1.setActivityName("天蚕土豆"); + AppBargainRecordSummaryRespVO.Record record2 = new AppBargainRecordSummaryRespVO.Record(); + record2.setNickname("张**"); + record2.setAvatar("https://www.iocoder.cn/yyy.jpg"); + record2.setActivityName("斗罗大陆"); + summary.getSuccessRecords().add(record1); + summary.getSuccessRecords().add(record2); + return success(summary); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得砍价记录的明细") + // TODO 芋艿:swagger;id 和 activityId 二选一 + public CommonResult getBargainRecordDetail( + @RequestParam(value = "id", required = false) Long id, + @RequestParam(value = "activityId", required = false) Long activityId) { + AppBargainRecordDetailRespVO detail = new AppBargainRecordDetailRespVO(); + detail.setId(1L); + detail.setUserId(1L); + detail.setSpuId(1L); + detail.setSkuId(1L); + detail.setPrice(500); + detail.setActivityId(1L); + detail.setBargainPrice(150); + detail.setPrice(200); + detail.setPayPrice(180); + detail.setStatus(1); + detail.setExpireTime(LocalDateTimeUtils.addTime(Duration.ofDays(2))); + return success(detail); + } + + @GetMapping("/page") + @Operation(summary = "获得砍价记录的分页") + public CommonResult> getBargainRecordPage(PageParam pageParam) { + PageResult page = new PageResult<>(); + page.setList(new ArrayList<>()); + AppBargainRecordRespVO record1 = new AppBargainRecordRespVO(); + record1.setId(1L); + record1.setUserId(1L); + record1.setSpuId(1L); + record1.setSkuId(1L); + record1.setPrice(500); + record1.setActivityId(1L); + record1.setBargainPrice(150); + record1.setPrice(200); + record1.setPayPrice(180); + record1.setStatus(1); + record1.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + record1.setExpireTime(LocalDateTimeUtils.addTime(Duration.ofDays(2))); + page.getList().add(record1); + + AppBargainRecordRespVO record2 = new AppBargainRecordRespVO(); + record2.setId(1L); + record2.setUserId(1L); + record2.setSpuId(1L); + record2.setSkuId(1L); + record2.setPrice(500); + record2.setActivityId(1L); + record2.setBargainPrice(150); + record2.setPrice(200); + record2.setPayPrice(280); + record2.setStatus(2); + record2.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + record2.setExpireTime(LocalDateTimeUtils.addTime(Duration.ofDays(2))); + page.getList().add(record2); + + AppBargainRecordRespVO record3 = new AppBargainRecordRespVO(); + record3.setId(1L); + record3.setUserId(1L); + record3.setSpuId(1L); + record3.setSkuId(1L); + record3.setPrice(500); + record3.setActivityId(1L); + record3.setBargainPrice(150); + record3.setPrice(200); + record3.setPayPrice(380); + record3.setStatus(2); + record3.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + record3.setExpireTime(LocalDateTimeUtils.addTime(Duration.ofDays(2))); + record3.setOrderId(100L); + page.getList().add(record3); + + AppBargainRecordRespVO record4 = new AppBargainRecordRespVO(); + record4.setId(1L); + record4.setUserId(1L); + record4.setSpuId(1L); + record4.setSkuId(1L); + record4.setPrice(500); + record4.setActivityId(1L); + record4.setBargainPrice(150); + record4.setPrice(200); + record4.setPayPrice(380); + record4.setStatus(3); + record4.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + record4.setExpireTime(LocalDateTimeUtils.addTime(Duration.ofDays(2))); + record4.setOrderId(100L); + page.getList().add(record4); + + page.setTotal(1L); + return success(page); + } + + @PostMapping("/create") + @Operation(summary = "创建砍价记录", description = "参与拼团活动") + public CommonResult createBargainRecord(@RequestBody AppBargainRecordCreateReqVO reqVO) { + return success(1L); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/activity/AppBargainActivityDetailRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/activity/AppBargainActivityDetailRespVO.java new file mode 100644 index 0000000..5ff1f25 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/activity/AppBargainActivityDetailRespVO.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 砍价活动的明细 Response VO") +@Data +public class AppBargainActivityDetailRespVO { + + @Schema(description = "砍价活动编号", required = true, example = "1024") + private Long id; + + @Schema(description = "砍价活动名称", required = true, example = "618 大砍价") + private String name; + + @Schema(description = "活动开始时间", required = true) + private LocalDateTime startTime; + + @Schema(description = "活动结束时间", required = true) + private LocalDateTime endTime; + + @Schema(description = "商品 SPU 编号", required = true, example = "2048") + private Long spuId; + + @Schema(description = "商品 SKU 编号", required = true, example = "1024") + private Long skuId; + + @Schema(description = "商品价格,单位:分", required = true, example = "100") + private Integer price; + + @Schema(description = "商品描述", required = true, example = "我要吃西红柿") + private String description; + + @Schema(description = "砍价库存", required = true, example = "512") + private Integer stock; + + @Schema(description = "商品图片", required = true, example = "4096") // 从 SPU 的 picUrl 读取 + private String picUrl; + + @Schema(description = "商品市场价,单位:分", required = true, example = "50") // 从 SPU 的 marketPrice 读取 + private Integer marketPrice; + + @Schema(description = "商品单位", required = true, example = "个") // 从 SPU 的 unit 读取,然后转换 + private String unitName; + + @Schema(description = "砍价最低金额,单位:分", required = true, example = "100") // 从砍价商品里取最低价 + private Integer bargainPrice; + + @Schema(description = "砍价成功数量", required = true, example = "100") + private Integer successCount; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/activity/AppBargainActivityRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/activity/AppBargainActivityRespVO.java new file mode 100644 index 0000000..442c7da --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/activity/AppBargainActivityRespVO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 砍价活动 Response VO") +@Data +public class AppBargainActivityRespVO { + + @Schema(description = "砍价活动编号", required = true, example = "1024") + private Long id; + + @Schema(description = "砍价活动名称", required = true, example = "618 大砍价") + private String name; + + @Schema(description = "活动开始时间", required = true) + private LocalDateTime startTime; + + @Schema(description = "活动结束时间", required = true) + private LocalDateTime endTime; + + @Schema(description = "商品 SPU 编号", required = true, example = "2048") + private Long spuId; + + @Schema(description = "商品 SKU 编号", required = true, example = "1024") + private Long skuId; + + @Schema(description = "砍价库存", required = true, example = "512") + private Integer stock; + + @Schema(description = "商品图片", required = true, example = "4096") // 从 SPU 的 picUrl 读取 + private String picUrl; + + @Schema(description = "商品市场价,单位:分", required = true, example = "50") // 从 SPU 的 marketPrice 读取 + private Integer marketPrice; + + @Schema(description = "砍价最低金额,单位:分", required = true, example = "100") // 从砍价商品里取最低价 + private Integer bargainPrice; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/help/AppBargainHelpCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/help/AppBargainHelpCreateReqVO.java new file mode 100644 index 0000000..8db7398 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/help/AppBargainHelpCreateReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain.vo.help; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - 砍价助力的创建 Request VO") +@Data +public class AppBargainHelpCreateReqVO { + + @Schema(description = "砍价记录编号", required = true, example = "1024") + @NotNull(message = "砍价记录编号不能为空") + private Long recordId; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/help/AppBargainHelpRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/help/AppBargainHelpRespVO.java new file mode 100644 index 0000000..10f624b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/help/AppBargainHelpRespVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain.vo.help; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 砍价助力 Response VO") +@Data +public class AppBargainHelpRespVO { + + @Schema(description = "助力用户的昵称", required = true, example = "1024") + private String nickname; + + @Schema(description = "助力用户的头像", required = true, example = "1024") + private String avatar; + + @Schema(description = "助力用户的砍价金额", required = true, example = "1024") + private Integer reducePrice; + + @Schema(description = "创建时间", required = true, example = "1024") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordCreateReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordCreateReqVO.java new file mode 100644 index 0000000..117b966 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordCreateReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - 砍价记录的创建 Request VO") +@Data +public class AppBargainRecordCreateReqVO { + + @Schema(description = "砍价活动编号", required = true, example = "1024") + @NotNull(message = "砍价活动编号不能为空") + private Long activityId; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordDetailRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordDetailRespVO.java new file mode 100644 index 0000000..26354e8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordDetailRespVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 砍价记录的明细 Response VO") +@Data +public class AppBargainRecordDetailRespVO { + + public static final int HELP_ACTION_NONE = 1; // 帮砍动作 - 未帮砍,可以帮砍 + public static final int HELP_ACTION_FULL = 2; // 帮砍动作 - 未帮砍,无法帮砍(可帮砍次数已满) + public static final int HELP_ACTION_SUCCESS = 3; // 帮砍动作 - 已帮砍 + + private Long id; + private Long userId; + private Long spuId; + private Long skuId; + private Long activityId; + private Integer bargainPrice; + private Integer price; + private Integer payPrice; + private Integer status; + + private LocalDateTime expireTime; + + private Long orderId; + private Boolean payStatus; + + private Integer helpAction; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordRespVO.java new file mode 100644 index 0000000..044425d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordRespVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 砍价记录的 Response VO") +@Data +public class AppBargainRecordRespVO { + + // TODO @芋艿:status;如果砍价对应的订单支付超时,算失败么?砍价的支付时间,以 expireTime 为准么? + + private Long id; + private Long userId; + private Long spuId; + private Long skuId; + private Long activityId; + private Integer bargainPrice; + private Integer price; + private Integer payPrice; + private Integer status; + private LocalDateTime expireTime; + + private Long orderId; + private Boolean payStatus; + private Long payOrderId; + + private String activityName; + private String picUrl; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordSummaryRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordSummaryRespVO.java new file mode 100644 index 0000000..f1aecc0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/bargain/vo/record/AppBargainRecordSummaryRespVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.promotion.controller.app.bargain.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 砍价记录的简要概括 Response VO") +@Data +public class AppBargainRecordSummaryRespVO { + + @Schema(description = "砍价用户数量", required = true, example = "1024") + private Integer userCount; + + @Schema(description = "成功砍价的记录", required = true) // 只返回最近的 7 个 + private List successRecords; + + @Schema(description = "成功砍价记录") + @Data + public static class Record { + + @Schema(description = "用户昵称", required = true, example = "王**") + private String nickname; + + @Schema(description = "用户头像", required = true, example = "https://www.iocoder.cn/xxx.jpg") + private String avatar; + + @Schema(description = "活动名称", required = true, example = "天蚕土豆") + private String activityName; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/AppCombinationActivityController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/AppCombinationActivityController.java new file mode 100644 index 0000000..aba6a2d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/AppCombinationActivityController.java @@ -0,0 +1,129 @@ +package com.yunxi.scm.module.promotion.controller.app.combination; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.app.combination.vo.activity.AppCombinationActivityDetailRespVO; +import com.yunxi.scm.module.promotion.controller.app.combination.vo.activity.AppCombinationActivityRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 拼团活动") +@RestController +@RequestMapping("/promotion/combination-activity") +@Validated +public class AppCombinationActivityController { + + @GetMapping("/list") + @Operation(summary = "获得拼团活动列表", description = "用于小程序首页") + // TODO 芋艿:增加 Spring Cache + // TODO 芋艿:缺少 swagger 注解 + public CommonResult> getCombinationActivityList( + @RequestParam(name = "count", defaultValue = "6") Integer count) { + List activityList = new ArrayList<>(); + AppCombinationActivityRespVO activity1 = new AppCombinationActivityRespVO(); + activity1.setId(1L); + activity1.setName("618 大拼团"); + activity1.setUserSize(3); + activity1.setSpuId(2048L); + activity1.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + activity1.setMarketPrice(50); + activity1.setCombinationPrice(100); + activityList.add(activity1); + + AppCombinationActivityRespVO activity2 = new AppCombinationActivityRespVO(); + activity2.setId(2L); + activity2.setName("双十一拼团"); + activity2.setUserSize(5); + activity2.setSpuId(4096L); + activity2.setPicUrl("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKXMYJOomfp7cebz3cIeb8sHk3GGSIJtWEgREe3j7J1WoAbTvIOicpcNdFkWAziatBSMod8b5RyS4CQ/132"); + activity2.setMarketPrice(100); + activity2.setCombinationPrice(200); + activityList.add(activity2); + + return success(activityList); + } + + @GetMapping("/page") + @Operation(summary = "获得拼团活动分页") + public CommonResult> getCombinationActivityPage(PageParam pageParam) { + List activityList = new ArrayList<>(); + AppCombinationActivityRespVO activity1 = new AppCombinationActivityRespVO(); + activity1.setId(1L); + activity1.setName("618 大拼团"); + activity1.setUserSize(3); + activity1.setSpuId(2048L); + activity1.setPicUrl("商品图片地址"); + activity1.setMarketPrice(50); + activity1.setCombinationPrice(100); + activityList.add(activity1); + + AppCombinationActivityRespVO activity2 = new AppCombinationActivityRespVO(); + activity2.setId(2L); + activity2.setName("双十一拼团"); + activity2.setUserSize(5); + activity2.setSpuId(4096L); + activity2.setPicUrl("商品图片地址"); + activity2.setMarketPrice(100); + activity2.setCombinationPrice(200); + activityList.add(activity2); + + return success(new PageResult<>(activityList, 2L)); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得拼团活动明细") + @Parameter(name = "id", description = "活动编号", required = true, example = "1024") + public CommonResult getCombinationActivityDetail(@RequestParam("id") Long id) { + // TODO 芋艿:如果禁用的时候,需要抛出异常; + AppCombinationActivityDetailRespVO obj = new AppCombinationActivityDetailRespVO(); + // 设置其属性的值 + obj.setId(id); + obj.setName("晚九点限时秒杀"); + obj.setStatus(1); + obj.setStartTime(LocalDateTime.of(2023, 6, 15, 0, 0, 0)); + obj.setEndTime(LocalDateTime.of(2023, 6, 20, 23, 59, 0)); + obj.setUserSize(2); + obj.setSuccessCount(100); + obj.setSpuId(633L); + obj.setSingleLimitCount(2); + obj.setTotalLimitCount(3); + + // 创建一个Product对象的列表 + List productList = new ArrayList<>(); + // 创建三个新的Product对象并设置其属性的值 + AppCombinationActivityDetailRespVO.Product product1 = new AppCombinationActivityDetailRespVO.Product(); + product1.setSkuId(1L); + product1.setCombinationPrice(100); + // 将第一个Product对象添加到列表中 + productList.add(product1); + // 创建第二个Product对象并设置其属性的值 + AppCombinationActivityDetailRespVO.Product product2 = new AppCombinationActivityDetailRespVO.Product(); + product2.setSkuId(2L); + product2.setCombinationPrice(200); + // 将第二个Product对象添加到列表中 + productList.add(product2); + // 创建第三个Product对象并设置其属性的值 + AppCombinationActivityDetailRespVO.Product product3 = new AppCombinationActivityDetailRespVO.Product(); + product3.setSkuId(3L); + product3.setCombinationPrice(300); + // 将第三个Product对象添加到列表中 + productList.add(product3); + // 将Product列表设置为对象的属性值 + obj.setProducts(productList); + return success(obj); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/AppCombinationRecordController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/AppCombinationRecordController.java new file mode 100644 index 0000000..d338c6e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/AppCombinationRecordController.java @@ -0,0 +1,109 @@ +package com.yunxi.scm.module.promotion.controller.app.combination; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.module.promotion.controller.app.combination.vo.record.AppCombinationRecordDetailRespVO; +import com.yunxi.scm.module.promotion.controller.app.combination.vo.record.AppCombinationRecordRespVO; +import com.yunxi.scm.module.promotion.controller.app.combination.vo.record.AppCombinationRecordSummaryRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.constraints.Max; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 APP - 拼团活动") +@RestController +@RequestMapping("/promotion/combination-record") +@Validated +public class AppCombinationRecordController { + + @GetMapping("/get-summary") + @Operation(summary = "获得拼团记录的概要信息", description = "用于小程序首页") + // TODO 芋艿:增加 @Cache 缓存,1 分钟过期 + public CommonResult getCombinationRecordSummary() { + AppCombinationRecordSummaryRespVO summary = new AppCombinationRecordSummaryRespVO(); + summary.setUserCount(1024); + summary.setAvatars(new ArrayList<>()); + summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLjFK35Wvia9lJKHoXfQuHhk0qZbvpPNxrAiaEKF7aL2k4I8kuqrdTWwliamdPHeyAA7DjAg725X2GIQ/132"); + summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTK1pXgdj5DvBMwrbe8v3tFibSWeQATEsAibt3fllD8XwJ460P2r6KS3WCQvDefuv1bVpDhNCle6CTCA/132"); + summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTL7KRGHBE62N0awFyBesmmxiaCicf1fJ7E7UCh6zA8GWlT1QC1zT01gG4OxI7BWDESkdPZ5o7tno4hA/132"); + summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/ouwtwJycbic2JrCoZjETict0klxd1uRuicRneKk00ewMcCClxVcVHQT91Sh9MJGtwibf1fOicD1WpwSP4icJM6eQq1AA/132"); + summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/RpUrhwens58qc99OcGs993xL4M5QPOe05ekqF9Eia440kRicAlicicIdQWicHBmy2bzLgHzHguWEzHHxnIgeictL7bLA/132"); + summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/S4tfqmxc8GZGsKc1K4mnhpvtG16gtMrLnTQfDibhr7jJich9LRI5RQKZDoqEjZM3azMib5nic7F4ZXKMEgYyLO08KA/132"); + summary.getAvatars().add("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKXMYJOomfp7cebz3cIeb8sHk3GGSIJtWEgREe3j7J1WoAbTvIOicpcNdFkWAziatBSMod8b5RyS4CQ/132"); + return success(summary); + } + + @GetMapping("/get-head-list") + @Operation(summary = "获得最近 n 条拼团记录(团长发起的)") + // TODO @芋艿:注解要补全 + public CommonResult> getHeadCombinationRecordList( + @RequestParam(value = "activityId", required = false) Long activityId, + @RequestParam("status") Integer status, + @RequestParam(value = "count", defaultValue = "20") @Max(20) Integer count) { + List list = new ArrayList<>(); + for (int i = 1; i <= count; i++) { + AppCombinationRecordRespVO record = new AppCombinationRecordRespVO(); + record.setId((long) i); + record.setNickname("用户" + i); + record.setAvatar("头像" + i); + record.setExpireTime(new Date()); + record.setUserSize(10); + record.setUserCount(i); + record.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + record.setActivityId(1L); + record.setSpuName("活动:" + i); + list.add(record); + } + return success(list); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得拼团记录明细") + @Parameter(name = "id", description = "拼团记录编号", required = true, example = "1024") + public CommonResult getCombinationRecordDetail(@RequestParam("id") Long id) { + AppCombinationRecordDetailRespVO detail = new AppCombinationRecordDetailRespVO(); + // 团长 + AppCombinationRecordRespVO headRecord = new AppCombinationRecordRespVO(); + headRecord.setId(1L); + headRecord.setNickname("用户" + 1); + headRecord.setAvatar("头像" + 1); + headRecord.setExpireTime(DateUtils.addTime(Duration.ofDays(1))); + headRecord.setUserSize(10); + headRecord.setUserCount(3); + headRecord.setStatus(1); + headRecord.setActivityId(10L); + headRecord.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + headRecord.setCombinationPrice(100); + detail.setHeadRecord(headRecord); + // 团员 + List list = new ArrayList<>(); + for (int i = 1; i <= 2; i++) { + AppCombinationRecordRespVO record = new AppCombinationRecordRespVO(); + record.setId((long) i); + record.setNickname("用户" + i); + record.setAvatar("头像" + i); + record.setExpireTime(new Date()); + record.setUserSize(10); + record.setUserCount(i); + record.setStatus(1); + list.add(record); + } + detail.setMemberRecords(list); + // 订单编号 + detail.setOrderId(100L); + return success(detail); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityDetailRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityDetailRespVO.java new file mode 100644 index 0000000..b2437e8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityDetailRespVO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.promotion.controller.app.combination.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 App - 拼团活动明细 Response VO") +@Data +public class AppCombinationActivityDetailRespVO { + + @Schema(description = "拼团活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "拼团活动名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "618 大拼团") + private String name; + + @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "活动开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + + @Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "拼团人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer userSize; + + @Schema(description = "成功的拼团数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer successCount; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long spuId; + + @Schema(description = "总共限购数量", example = "10") + private Integer totalLimitCount; + + @Schema(description = "单次限购数量", example = "5") + private Integer singleLimitCount; + + @Schema(description = "商品信息数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List products; + + @Schema(description = "商品信息") + @Data + public static class Product { + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + private Long skuId; + + @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer combinationPrice; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java new file mode 100644 index 0000000..dfad560 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/activity/AppCombinationActivityRespVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.promotion.controller.app.combination.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 拼团活动 Response VO") +@Data +public class AppCombinationActivityRespVO { + + @Schema(description = "拼团活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "拼团活动名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "618 大拼团") + private String name; + + @Schema(description = "拼团人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3") + private Integer userSize; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long spuId; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + // 从 SPU 的 picUrl 读取 + private String picUrl; + + @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + // 从 SPU 的 marketPrice 读取 + private Integer marketPrice; + + @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + // 从拼团商品里取最低价 + private Integer combinationPrice; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordDetailRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordDetailRespVO.java new file mode 100644 index 0000000..31982b8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordDetailRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.promotion.controller.app.combination.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 拼团记录详细 Response VO") +@Data +public class AppCombinationRecordDetailRespVO { + + @Schema(description = "团长的拼团记录", requiredMode = Schema.RequiredMode.REQUIRED) + private AppCombinationRecordRespVO headRecord; + + @Schema(description = "成员的拼团记录", requiredMode = Schema.RequiredMode.REQUIRED) + private List memberRecords; + + @Schema(description = "当前用户参团记录对应的订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + // 如果没参团,返回 null + private Long orderId; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordRespVO.java new file mode 100644 index 0000000..1aefd5b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordRespVO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.promotion.controller.app.combination.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Schema(description = "用户 App - 拼团记录 Response VO") +@Data +public class AppCombinationRecordRespVO { + + @Schema(description = "拼团记录编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "拼团活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long activityId; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String avatar; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date expireTime; + + @Schema(description = "可参团人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer userSize; + + @Schema(description = "已参团人数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + private Integer userCount; + + @Schema(description = "拼团状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "商品名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是大黄豆") + private String spuName; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "拼团金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer combinationPrice; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordSummaryRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordSummaryRespVO.java new file mode 100644 index 0000000..0a8397f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/combination/vo/record/AppCombinationRecordSummaryRespVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.promotion.controller.app.combination.vo.record; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 拼团记录的简要概括 Response VO") +@Data +public class AppCombinationRecordSummaryRespVO { + + @Schema(description = "拼团用户数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer userCount; + + @Schema(description = "拼团用户头像列表", requiredMode = Schema.RequiredMode.REQUIRED) // 只返回最近的 7 个 + private List avatars; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/AppCouponController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/AppCouponController.java new file mode 100644 index 0000000..1900f71 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/AppCouponController.java @@ -0,0 +1,96 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchReqVO; +import com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon.AppCouponMatchRespVO; +import com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon.AppCouponPageReqVO; +import com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon.AppCouponRespVO; +import com.yunxi.scm.module.promotion.controller.app.coupon.vo.template.AppCouponTemplatePageReqVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 优惠劵") +@RestController +@RequestMapping("/promotion/coupon") +@Validated +public class AppCouponController { + + // TODO 芋艿:待实现 + @PostMapping("/take") + @Operation(summary = "领取优惠劵") + public CommonResult takeCoupon(@RequestBody AppCouponTemplatePageReqVO pageReqVO) { + return success(1L); + } + + // TODO 芋艿:待实现 + @GetMapping("/match-list") + @Operation(summary = "获得匹配指定商品的优惠劵列表") + public CommonResult> getMatchCouponList(AppCouponMatchReqVO matchReqVO) { + List list = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 10; i++) { + AppCouponMatchRespVO vo = new AppCouponMatchRespVO(); + vo.setId(i + 1L); + vo.setName("优惠劵" + (i + 1)); + vo.setUsePrice(random.nextInt(100) * 100); + vo.setValidStartTime(LocalDateTime.now().plusDays(random.nextInt(10))); + vo.setValidEndTime(LocalDateTime.now().plusDays(random.nextInt(20) + 10)); + vo.setDiscountType(random.nextInt(2) + 1); + if (vo.getDiscountType() == 1) { + vo.setDiscountPercent(null); + vo.setDiscountPrice(random.nextInt(50) * 100); + vo.setDiscountLimitPrice(null); + } else { + vo.setDiscountPercent(random.nextInt(90) + 10); + vo.setDiscountPrice(null); + vo.setDiscountLimitPrice(random.nextInt(200) * 100); + } + vo.setMatch(random.nextBoolean()); + if (!vo.getMatch()) { + vo.setDescription("不符合条件噢"); + } + list.add(vo); + } + return success(list); + } + + // TODO 芋艿:待实现 + @GetMapping("/page") + @Operation(summary = "优惠劵列表", description = "我的优惠劵") + public CommonResult> takeCoupon(AppCouponPageReqVO pageReqVO) { + List list = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 10; i++) { + AppCouponRespVO vo = new AppCouponRespVO(); + vo.setId(i + 1L); + vo.setName("优惠劵" + (i + 1)); + vo.setStatus(pageReqVO.getStatus()); + vo.setUsePrice(random.nextInt(100) * 100); + vo.setValidStartTime(LocalDateTime.now().plusDays(random.nextInt(10))); + vo.setValidEndTime(LocalDateTime.now().plusDays(random.nextInt(20) + 10)); + vo.setDiscountType(random.nextInt(2) + 1); + if (vo.getDiscountType() == 1) { + vo.setDiscountPercent(null); + vo.setDiscountPrice(random.nextInt(50) * 100); + vo.setDiscountLimitPrice(null); + } else { + vo.setDiscountPercent(random.nextInt(90) + 10); + vo.setDiscountPrice(null); + vo.setDiscountLimitPrice(random.nextInt(200) * 100); + } + list.add(vo); + } + return success(new PageResult<>(list, 20L)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/AppCouponTemplateController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/AppCouponTemplateController.java new file mode 100644 index 0000000..83678f7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/AppCouponTemplateController.java @@ -0,0 +1,114 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.app.coupon.vo.template.AppCouponTemplatePageReqVO; +import com.yunxi.scm.module.promotion.controller.app.coupon.vo.template.AppCouponTemplateRespVO; +import com.yunxi.scm.module.promotion.service.coupon.CouponTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 优惠劵模板") +@RestController +@RequestMapping("/promotion/coupon-template") +@Validated +public class AppCouponTemplateController { + + @Resource + private CouponTemplateService couponTemplateService; + + // TODO 芋艿:待实现 + @GetMapping("/list") + @Operation(summary = "获得优惠劵模版列表") + @Parameters({ + @Parameter(name = "spuId", description = "商品 SPU 编号"), // 目前主要给商品详情使用 + @Parameter(name = "useType", description = "使用类型"), + @Parameter(name = "count", description = "数量", required = true) + }) + public CommonResult> getCouponTemplateList(@RequestParam(value = "spuId", required = false) Long spuId, + @RequestParam(value = "useType", required = false) Integer useType, + @RequestParam(value = "count", required = false, defaultValue = "10") Integer count) { + List list = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 10; i++) { + AppCouponTemplateRespVO vo = new AppCouponTemplateRespVO(); + vo.setId(i + 1L); + vo.setName("优惠劵" + (i + 1)); + vo.setTakeLimitCount(random.nextInt(10) + 1); + vo.setUsePrice(random.nextInt(100) * 100); + vo.setValidityType(random.nextInt(2) + 1); + if (vo.getValidityType() == 1) { + vo.setValidStartTime(LocalDateTime.now().plusDays(random.nextInt(10))); + vo.setValidEndTime(LocalDateTime.now().plusDays(random.nextInt(20) + 10)); + } else { + vo.setFixedStartTerm(random.nextInt(10)); + vo.setFixedEndTerm(random.nextInt(10) + vo.getFixedStartTerm() + 1); + } + vo.setDiscountType(random.nextInt(2) + 1); + if (vo.getDiscountType() == 1) { + vo.setDiscountPercent(null); + vo.setDiscountPrice(random.nextInt(50) * 100); + vo.setDiscountLimitPrice(null); + } else { + vo.setDiscountPercent(random.nextInt(90) + 10); + vo.setDiscountPrice(null); + vo.setDiscountLimitPrice(random.nextInt(200) * 100); + } + vo.setTakeStatus(random.nextBoolean()); + list.add(vo); + } + return success(list); + } + + // TODO 芋艿:待实现;和 getCouponTemplateList 类似 + @GetMapping("/page") + @Operation(summary = "获得优惠劵模版分页") + public CommonResult> getCouponTemplatePage(AppCouponTemplatePageReqVO pageReqVO) { + List list = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 10; i++) { + AppCouponTemplateRespVO vo = new AppCouponTemplateRespVO(); + vo.setId(i + 1L); + vo.setName("优惠劵" + (i + 1)); + vo.setTakeLimitCount(random.nextInt(10) + 1); + vo.setUsePrice(random.nextInt(100) * 100); + vo.setValidityType(random.nextInt(2) + 1); + if (vo.getValidityType() == 1) { + vo.setValidStartTime(LocalDateTime.now().plusDays(random.nextInt(10))); + vo.setValidEndTime(LocalDateTime.now().plusDays(random.nextInt(20) + 10)); + } else { + vo.setFixedStartTerm(random.nextInt(10)); + vo.setFixedEndTerm(random.nextInt(10) + vo.getFixedStartTerm() + 1); + } + vo.setDiscountType(random.nextInt(2) + 1); + if (vo.getDiscountType() == 1) { + vo.setDiscountPercent(null); + vo.setDiscountPrice(random.nextInt(50) * 100); + vo.setDiscountLimitPrice(null); + } else { + vo.setDiscountPercent(random.nextInt(90) + 10); + vo.setDiscountPrice(null); + vo.setDiscountLimitPrice(random.nextInt(200) * 100); + } + vo.setTakeStatus(random.nextBoolean()); + list.add(vo); + } + return success(new PageResult<>(list, 20L)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java new file mode 100644 index 0000000..4eb999f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "用户 App - 优惠劵的匹配 Request VO") +@Data +public class AppCouponMatchReqVO { + + @Schema(description = "商品金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "商品金额不能为空") + private Integer price; + + @Schema(description = "商品 SPU 编号的数组", required = true, example = "[1, 2]") + @NotEmpty(message = "商品 SPU 编号不能为空") + private List spuIds; + + @Schema(description = "商品 SKU 编号的数组", required = true, example = "[1, 2]") + @NotEmpty(message = "商品 SKU 编号不能为空") + private List skuIds; + + @Schema(description = "分类编号的数组", required = true, example = "[10, 20]") + @NotEmpty(message = "分类编号不能为空") + private List categoryIds; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java new file mode 100644 index 0000000..8e92b5f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponMatchRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 优惠劵 Response VO") +@Data +public class AppCouponMatchRespVO extends AppCouponRespVO { + + @Schema(description = "是否匹配", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean match; + + @Schema(description = "匹配条件的提示", example = "所结算商品没有符合条件的商品") + private String description; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponPageReqVO.java new file mode 100644 index 0000000..19cb253 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponPageReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "用户 App - 优惠劵分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppCouponPageReqVO extends PageParam { + + @Schema(description = "优惠劵状态", example = "1") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java new file mode 100644 index 0000000..b58689c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponRespVO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 优惠劵 Response VO") +@Data +public class AppCouponRespVO { + + @Schema(description = "优惠劵编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送") + private String name; + + @Schema(description = "优惠劵状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") // 参见 CouponStatusEnum 枚举 + private Integer status; + + @Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + // 单位:分;0 - 不限制 + private Integer usePrice; + + @Schema(description = "固定日期 - 生效开始时间") + private LocalDateTime validStartTime; + + @Schema(description = "固定日期 - 生效结束时间") + private LocalDateTime validEndTime; + + @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer discountType; + + @Schema(description = "折扣百分比", example = "80") // 例如说,80% 为 80 + private Integer discountPercent; + + @Schema(description = "优惠金额", example = "10") + @Min(value = 0, message = "优惠金额需要大于等于 0") + private Integer discountPrice; + + @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 + private Integer discountLimitPrice; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponTakeReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponTakeReqVO.java new file mode 100644 index 0000000..108c1f7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/coupon/AppCouponTakeReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon.vo.coupon; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - 优惠劵领取 Request VO") +@Data +public class AppCouponTakeReqVO { + + @Schema(description = "优惠劵模板编号", example = "1") + @NotNull(message = "优惠劵模板编号不能为空") + private Long templateId; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/template/AppCouponTemplatePageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/template/AppCouponTemplatePageReqVO.java new file mode 100644 index 0000000..6ad18ed --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/template/AppCouponTemplatePageReqVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon.vo.template; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "用户 App - 优惠劵模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppCouponTemplatePageReqVO extends PageParam { + + @Schema(description = "使用类型", example = "1") + // TODO 芋艿:这里要限制下枚举的使用 + private Integer useType; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java new file mode 100644 index 0000000..39a20f0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/coupon/vo/template/AppCouponTemplateRespVO.java @@ -0,0 +1,69 @@ +package com.yunxi.scm.module.promotion.controller.app.coupon.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 优惠劵模板 Response VO") +@Data +public class AppCouponTemplateRespVO { + + @Schema(description = "优惠劵模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "优惠劵名", requiredMode = Schema.RequiredMode.REQUIRED, example = "春节送送送") + private String name; + + @Schema(description = "每人限领个数", requiredMode = Schema.RequiredMode.REQUIRED, example = "66") // -1 - 则表示不限制 + private Integer takeLimitCount; + + @Schema(description = "是否设置满多少金额可用", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + // 单位:分;0 - 不限制 + private Integer usePrice; + + // TODO 芋艿:这两要改的 +// @Schema(description = "商品范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") +// @InEnum(PromotionProductScopeEnum.class) +// private Integer productScope; +// +// @Schema(description = "商品 SPU 编号的数组", example = "1,3") +// private List productSpuIds; + + @Schema(description = "生效日期类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer validityType; + + @Schema(description = "固定日期 - 生效开始时间") + private LocalDateTime validStartTime; + + @Schema(description = "固定日期 - 生效结束时间") + private LocalDateTime validEndTime; + + @Schema(description = "领取日期 - 开始天数") + @Min(value = 0L, message = "开始天数必须大于 0") + private Integer fixedStartTerm; + + @Schema(description = "领取日期 - 结束天数") + @Min(value = 1L, message = "开始天数必须大于 1") + private Integer fixedEndTerm; + + @Schema(description = "优惠类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer discountType; + + @Schema(description = "折扣百分比", example = "80") // 例如说,80% 为 80 + private Integer discountPercent; + + @Schema(description = "优惠金额", example = "10") + @Min(value = 0, message = "优惠金额需要大于等于 0") + private Integer discountPrice; + + @Schema(description = "折扣上限", example = "100") // 单位:分,仅在 discountType 为 PERCENT 使用 + private Integer discountLimitPrice; + + // ========== 用户相关字段 ========== + + @Schema(description = "是否已领取", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean takeStatus; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/decorate/AppDecorateController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/decorate/AppDecorateController.java new file mode 100644 index 0000000..dd2ed11 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/decorate/AppDecorateController.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.promotion.controller.app.decorate; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.promotion.controller.app.decorate.vo.AppDecorateComponentRespVO; +import com.yunxi.scm.module.promotion.enums.decorate.DecoratePageEnum; +import com.yunxi.scm.module.promotion.service.decorate.DecorateComponentService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.module.promotion.convert.decorate.DecorateComponentConvert.INSTANCE; + +@Tag(name = "用户 APP - 店铺装修") +@RestController +@RequestMapping("/promotion/decorate") +@Validated +public class AppDecorateController { + + @Resource + private DecorateComponentService decorateComponentService; + + @GetMapping("/get-page-components") + @Operation(summary = "获取装修页面组件") + @Parameter(name = "pageId", description = "页面 id", required = true) + public CommonResult getPageComponents( + @RequestParam("pageId") @InEnum(DecoratePageEnum.class) Integer pageId) { + return success(INSTANCE.appConvert(pageId, decorateComponentService.getPageComponents(pageId))); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/decorate/vo/AppDecorateComponentRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/decorate/vo/AppDecorateComponentRespVO.java new file mode 100644 index 0000000..207ae57 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/decorate/vo/AppDecorateComponentRespVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.promotion.controller.app.decorate.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 页面装修 Resp VO") +@Data +public class AppDecorateComponentRespVO { + + @Schema(description = "页面 id ", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer pageId; + + @Schema(description = "页面组件", requiredMode = Schema.RequiredMode.REQUIRED, example = "TODO") + private List components; + + @Schema(description = "用户 App - 页面组件 Resp VO") + @Data + public static class AppComponentRespVO { + + @Schema(description = "组件编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "nav-menu") + private String code; + + @Schema(description = "组件的内容配置项", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "TODO") + private String value; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/AppSeckillActivityController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/AppSeckillActivityController.java new file mode 100644 index 0000000..974d7e9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/AppSeckillActivityController.java @@ -0,0 +1,136 @@ +package com.yunxi.scm.module.promotion.controller.app.seckill; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityDetailRespVO; +import com.yunxi.scm.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityNowRespVO; +import com.yunxi.scm.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.app.seckill.vo.activity.AppSeckillActivityRespVO; +import com.yunxi.scm.module.promotion.controller.app.seckill.vo.config.AppSeckillConfigRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 秒杀活动") +@RestController +@RequestMapping("/promotion/seckill-activity") +@Validated +public class AppSeckillActivityController { + + @GetMapping("/get-now") + @Operation(summary = "获得当前秒杀活动") // 提供给首页使用 + // TODO 芋艿:需要增加 spring cache + public CommonResult getNowSeckillActivity() { + AppSeckillActivityNowRespVO respVO = new AppSeckillActivityNowRespVO(); + respVO.setConfig(new AppSeckillConfigRespVO().setId(10L).setStartTime("01:00").setEndTime("09:59")); + List activityList = new ArrayList<>(); + AppSeckillActivityRespVO activity1 = new AppSeckillActivityRespVO(); + activity1.setId(1L); + activity1.setName("618 大秒杀"); + activity1.setSpuId(2048L); + activity1.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + activity1.setMarketPrice(50); + activity1.setSeckillPrice(100); + activityList.add(activity1); + + AppSeckillActivityRespVO activity2 = new AppSeckillActivityRespVO(); + activity2.setId(2L); + activity2.setName("双十一大秒杀"); + activity2.setSpuId(4096L); + activity2.setPicUrl("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKXMYJOomfp7cebz3cIeb8sHk3GGSIJtWEgREe3j7J1WoAbTvIOicpcNdFkWAziatBSMod8b5RyS4CQ/132"); + activity2.setMarketPrice(100); + activity2.setSeckillPrice(200); + activityList.add(activity2); + respVO.setActivities(activityList); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "获得秒杀活动分页") + // TODO @芋艿:分页参数 + public CommonResult> getSeckillActivityPage(AppSeckillActivityPageReqVO pageReqVO) { + List activityList = new ArrayList<>(); + AppSeckillActivityRespVO activity1 = new AppSeckillActivityRespVO(); + activity1.setId(1L); + activity1.setName("618 大秒杀"); + activity1.setSpuId(2048L); + activity1.setPicUrl("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg"); + activity1.setMarketPrice(50); + activity1.setSeckillPrice(100); + activity1.setUnitName("个"); + activity1.setStock(1); + activity1.setTotalStock(2); + activityList.add(activity1); + + AppSeckillActivityRespVO activity2 = new AppSeckillActivityRespVO(); + activity2.setId(2L); + activity2.setName("双十一大秒杀"); + activity2.setSpuId(4096L); + activity2.setPicUrl("https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKXMYJOomfp7cebz3cIeb8sHk3GGSIJtWEgREe3j7J1WoAbTvIOicpcNdFkWAziatBSMod8b5RyS4CQ/132"); + activity2.setMarketPrice(100); + activity2.setSeckillPrice(200); + activity2.setUnitName("套"); + activity2.setStock(2); + activity2.setTotalStock(3); + activityList.add(activity2); + return success(new PageResult<>(activityList, 100L)); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得秒杀活动明细") + @Parameter(name = "id", description = "活动编号", required = true, example = "1024") + public CommonResult getSeckillActivity(@RequestParam("id") Long id) { + // TODO 芋艿:如果禁用的时候,需要抛出异常; + AppSeckillActivityDetailRespVO obj = new AppSeckillActivityDetailRespVO(); + // 设置其属性的值 + obj.setId(id); + obj.setName("晚九点限时秒杀"); + obj.setStatus(1); + obj.setStartTime(LocalDateTime.of(2023, 6, 16, 0, 0, 0)); + obj.setEndTime(LocalDateTime.of(2023, 6, 20, 23, 59, 0)); + obj.setSpuId(633L); + obj.setSingleLimitCount(2); + obj.setTotalLimitCount(3); + obj.setStock(100); + obj.setTotalStock(200); + + // 创建一个Product对象的列表 + List productList = new ArrayList<>(); + // 创建三个新的Product对象并设置其属性的值 + AppSeckillActivityDetailRespVO.Product product1 = new AppSeckillActivityDetailRespVO.Product(); + product1.setSkuId(1L); + product1.setSeckillPrice(100); + product1.setStock(50); + // 将第一个Product对象添加到列表中 + productList.add(product1); + // 创建第二个Product对象并设置其属性的值 + AppSeckillActivityDetailRespVO.Product product2 = new AppSeckillActivityDetailRespVO.Product(); + product2.setSkuId(2L); + product2.setSeckillPrice(200); + product2.setStock(100); + // 将第二个Product对象添加到列表中 + productList.add(product2); + // 创建第三个Product对象并设置其属性的值 + AppSeckillActivityDetailRespVO.Product product3 = new AppSeckillActivityDetailRespVO.Product(); + product3.setSkuId(3L); + product3.setSeckillPrice(300); + product3.setStock(150); + // 将第三个Product对象添加到列表中 + productList.add(product3); + // 将Product列表设置为对象的属性值 + obj.setProducts(productList); + return success(obj); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/AppSeckillConfigController.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/AppSeckillConfigController.java new file mode 100644 index 0000000..233886b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/AppSeckillConfigController.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.promotion.controller.app.seckill; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.promotion.controller.app.seckill.vo.config.AppSeckillConfigRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Arrays; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 秒杀时间段") +@RestController +@RequestMapping("/promotion/seckill-config") +@Validated +public class AppSeckillConfigController { + + @GetMapping("/list") + @Operation(summary = "获得秒杀时间段列表") + public CommonResult> getSeckillConfigList() { + return success(Arrays.asList( + new AppSeckillConfigRespVO().setId(1L).setStartTime("00:00").setEndTime("09:59") + .setSliderPicUrls(Arrays.asList("https://demo26.crmeb.net/uploads/attach/2021/11/15/a79f5d2ea6bf0c3c11b2127332dfe2df.jpg", + "https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKXMYJOomfp7cebz3cIeb8sHk3GGSIJtWEgREe3j7J1WoAbTvIOicpcNdFkWAziatBSMod8b5RyS4CQ/132")), + new AppSeckillConfigRespVO().setId(2L).setStartTime("10:00").setEndTime("12:59"), + new AppSeckillConfigRespVO().setId(2L).setStartTime("13:00").setEndTime("22:59"), + new AppSeckillConfigRespVO().setId(2L).setStartTime("23:00").setEndTime("23:59") + )); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityDetailRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityDetailRespVO.java new file mode 100644 index 0000000..05ac59d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityDetailRespVO.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.promotion.controller.app.seckill.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 App - 秒杀活动的详细 Response VO") +@Data +public class AppSeckillActivityDetailRespVO { + + @Schema(description = "秒杀活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "秒杀活动名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "晚九点限时秒杀") + private String name; + + @Schema(description = "活动状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + // TODO @芋艿:开始时间、结束时间,要和场次结合起来;就是要算到当前场次,是几点哈; + + @Schema(description = "活动开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime startTime; + + @Schema(description = "活动结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime endTime; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long spuId; + + @Schema(description = "总共限购数量", example = "10") + private Integer totalLimitCount; + + @Schema(description = "单次限购数量", example = "5") + private Integer singleLimitCount; + + @Schema(description = "秒杀库存(剩余)", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer stock; + + @Schema(description = "秒杀库存(总计)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer totalStock; + + @Schema(description = "商品信息数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List products; + + @Schema(description = "商品信息") + @Data + public static class Product { + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + private Long skuId; + + @Schema(description = "秒杀金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer seckillPrice; + + @Schema(description = "秒杀限量库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer stock; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityNowRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityNowRespVO.java new file mode 100644 index 0000000..cc9908e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityNowRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.promotion.controller.app.seckill.vo.activity; + +import com.yunxi.scm.module.promotion.controller.app.seckill.vo.config.AppSeckillConfigRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 当前秒杀活动 Response VO") +@Data +public class AppSeckillActivityNowRespVO { + + @Schema(description = "秒杀时间段", requiredMode = Schema.RequiredMode.REQUIRED) + private AppSeckillConfigRespVO config; + + @Schema(description = "秒杀活动数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List activities; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityPageReqVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityPageReqVO.java new file mode 100644 index 0000000..c7cc6f2 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityPageReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.promotion.controller.app.seckill.vo.activity; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "用户 App - 商品评价分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppSeckillActivityPageReqVO extends PageParam { + + @Schema(description = "秒杀配置编号", example = "1024") + private Long configId; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java new file mode 100644 index 0000000..466609e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/activity/AppSeckillActivityRespVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.promotion.controller.app.seckill.vo.activity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 秒杀活动 Response VO") +@Data +public class AppSeckillActivityRespVO { + + @Schema(description = "秒杀活动编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "秒杀活动名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "晚九点限时秒杀") + private String name; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long spuId; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "4096") + // 从 SPU 的 picUrl 读取 + private String picUrl; + @Schema(description = "单位名", requiredMode = Schema.RequiredMode.REQUIRED, example = "个") + private String unitName; + @Schema(description = "商品市场价,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + // 从 SPU 的 marketPrice 读取 + private Integer marketPrice; + + @Schema(description = "秒杀库存(剩余)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer stock; + @Schema(description = "秒杀库存(总共)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Integer totalStock; + + @Schema(description = "秒杀金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + // 从秒杀商品里取最低价 + private Integer seckillPrice; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/config/AppSeckillConfigRespVO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/config/AppSeckillConfigRespVO.java new file mode 100644 index 0000000..20f75e2 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/controller/app/seckill/vo/config/AppSeckillConfigRespVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.promotion.controller.app.seckill.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 秒杀时间段 Response VO") +@Data +public class AppSeckillConfigRespVO { + + @Schema(description = "秒杀时间段编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "开始时间点", requiredMode = Schema.RequiredMode.REQUIRED, example = "09:00") + private String startTime; + @Schema(description = "结束时间点", requiredMode = Schema.RequiredMode.REQUIRED, example = "09:59") + private String endTime; + + @Schema(description = "轮播图", requiredMode = Schema.RequiredMode.REQUIRED) + private List sliderPicUrls; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/banner/BannerConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/banner/BannerConvert.java new file mode 100644 index 0000000..629625b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/banner/BannerConvert.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.promotion.convert.banner; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerRespVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.banner.BannerDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface BannerConvert { + + BannerConvert INSTANCE = Mappers.getMapper(BannerConvert.class); + + List convertList(List list); + + PageResult convertPage(PageResult pageResult); + + BannerRespVO convert(BannerDO banner); + + BannerDO convert(BannerCreateReqVO createReqVO); + + BannerDO convert(BannerUpdateReqVO updateReqVO); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/combination/CombinationActivityConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/combination/CombinationActivityConvert.java new file mode 100644 index 0000000..524e42e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/combination/CombinationActivityConvert.java @@ -0,0 +1,138 @@ +package com.yunxi.scm.module.promotion.convert.combination; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.promotion.api.combination.dto.CombinationRecordReqDTO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityExcelVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityRespVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductBaseVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductRespVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationProductDO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationRecordDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 拼团活动 Convert + * + * @author HUIHUI + */ +@Mapper +public interface CombinationActivityConvert { + + CombinationActivityConvert INSTANCE = Mappers.getMapper(CombinationActivityConvert.class); + + @Mappings({ + @Mapping(target = "startTime", expression = "java(bean.getActivityTime()[0])"), + @Mapping(target = "endTime", expression = "java(bean.getActivityTime()[1])") + }) + CombinationActivityDO convert(CombinationActivityCreateReqVO bean); + + @Mappings({ + @Mapping(target = "startTime", expression = "java(bean.getActivityTime()[0])"), + @Mapping(target = "endTime", expression = "java(bean.getActivityTime()[1])") + }) + CombinationActivityDO convert(CombinationActivityUpdateReqVO bean); + + @Named("mergeTime") + default LocalDateTime[] mergeTime(LocalDateTime startTime, LocalDateTime endTime) { + // TODO 有点怪第一次这样写 hh + LocalDateTime[] localDateTime = new LocalDateTime[2]; + localDateTime[0] = startTime; + localDateTime[1] = endTime; + return localDateTime; + } + + @Mappings({ + @Mapping(target = "activityTime", expression = "java(mergeTime(bean.getStartTime(),bean.getEndTime()))") + }) + CombinationActivityRespVO convert(CombinationActivityDO bean); + + CombinationProductRespVO convert(CombinationProductDO bean); + + default CombinationActivityRespVO convert(CombinationActivityDO bean, List productDOs) { + CombinationActivityRespVO respVO = convert(bean); + respVO.setProducts(convertList2(productDOs)); + return respVO; + } + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default PageResult convertPage(PageResult page, + List productList, + List spuList) { + // TODO @puhui999:c -> c 可以去掉哈 + Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId, c -> c); + PageResult pageResult = convertPage(page); + pageResult.getList().forEach(item -> { + // TODO @puhui999:最好 MapUtils.findAndThen,万一没找到呢,啊哈哈。 + item.setSpuName(spuMap.get(item.getSpuId()).getName()); + item.setPicUrl(spuMap.get(item.getSpuId()).getPicUrl()); + item.setProducts(convertList2(productList)); + }); + return pageResult; + } + + List convertList2(List productDOs); + + List convertList02(List list); + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(target = "activityId", source = "activityDO.id"), + @Mapping(target = "spuId", source = "activityDO.spuId"), + @Mapping(target = "skuId", source = "vo.skuId"), + @Mapping(target = "activePrice", source = "vo.activePrice"), + @Mapping(target = "activityStartTime", source = "activityDO.startTime"), + @Mapping(target = "activityEndTime", source = "activityDO.endTime") + }) + CombinationProductDO convert(CombinationActivityDO activityDO, CombinationProductBaseVO vo); + + default List convertList(CombinationActivityDO activityDO, List products) { + List list = new ArrayList<>(); + products.forEach(sku -> { + CombinationProductDO productDO = convert(activityDO, sku); + // TODO 状态设置 + productDO.setActivityStatus(CommonStatusEnum.ENABLE.getStatus()); + list.add(productDO); + }); + return list; + } + + // TODO @puhui999:这个方法的参数,调整成 productDOs、vos、activityDO;因为 productDOs 是主角; + // 然后,这个方法,感觉不是为了 convert,而是为了补全; + default List convertList1(CombinationActivityDO activityDO, + List vos, + List productDOs) { + Map longMap = convertMap(productDOs, CombinationProductDO::getSkuId, CombinationProductDO::getId); + List list = new ArrayList<>(); + vos.forEach(sku -> { + CombinationProductDO productDO = convert(activityDO, sku); + productDO.setId(longMap.get(sku.getSkuId())); + // TODO @puhui999:是是不是用 activityDO 的状态; + productDO.setActivityStatus(CommonStatusEnum.ENABLE.getStatus()); + list.add(productDO); + }); + return list; + } + + CombinationRecordDO convert(CombinationRecordReqDTO reqDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/coupon/CouponConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/coupon/CouponConvert.java new file mode 100644 index 0000000..d7d74c1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/coupon/CouponConvert.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.promotion.convert.coupon; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon.CouponPageItemRespVO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 优惠劵 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface CouponConvert { + + CouponConvert INSTANCE = Mappers.getMapper(CouponConvert.class); + + PageResult convertPage(PageResult page); + + CouponRespDTO convert(CouponDO bean); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/coupon/CouponTemplateConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/coupon/CouponTemplateConvert.java new file mode 100644 index 0000000..9ce04f7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/coupon/CouponTemplateConvert.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.promotion.convert.coupon; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateRespVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponTemplateDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 优惠劵模板 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface CouponTemplateConvert { + + CouponTemplateConvert INSTANCE = Mappers.getMapper(CouponTemplateConvert.class); + + CouponTemplateDO convert(CouponTemplateCreateReqVO bean); + + CouponTemplateDO convert(CouponTemplateUpdateReqVO bean); + + CouponTemplateRespVO convert(CouponTemplateDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/decorate/DecorateComponentConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/decorate/DecorateComponentConvert.java new file mode 100644 index 0000000..aaf3df9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/decorate/DecorateComponentConvert.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.promotion.convert.decorate; + +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.promotion.controller.admin.decorate.vo.DecorateComponentSaveReqVO; +import com.yunxi.scm.module.promotion.controller.admin.decorate.vo.DecorateComponentRespVO; +import com.yunxi.scm.module.promotion.controller.app.decorate.vo.AppDecorateComponentRespVO; +import com.yunxi.scm.module.promotion.dal.dataobject.decorate.DecorateComponentDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +import static com.yunxi.scm.module.promotion.controller.admin.decorate.vo.DecorateComponentRespVO.*; +import static com.yunxi.scm.module.promotion.controller.app.decorate.vo.AppDecorateComponentRespVO.*; + +@Mapper +public interface DecorateComponentConvert { + + DecorateComponentConvert INSTANCE = Mappers.getMapper(DecorateComponentConvert.class); + + default List convertList(Integer pageId, List components) { + return CollectionUtils.convertList(components, c -> convert(pageId, c)); + } + + default DecorateComponentRespVO convert2(Integer pageId, List list) { + List components = CollectionUtils.convertList(list, this::convert3); + return new DecorateComponentRespVO().setPageId(pageId).setComponents(components); + } + + DecorateComponentDO convert(Integer pageId, DecorateComponentSaveReqVO.ComponentReqVO reqVO); + + ComponentRespVO convert3(DecorateComponentDO componentDO); + + // ========== App convert ========== + default AppDecorateComponentRespVO appConvert(Integer pageId, List list) { + List components = CollectionUtils.convertList(list, this::appConvert2); + return new AppDecorateComponentRespVO().setPageId(pageId).setComponents(components); + } + + AppComponentRespVO appConvert2(DecorateComponentDO bean); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/discount/DiscountActivityConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/discount/DiscountActivityConvert.java new file mode 100644 index 0000000..6866f75 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/discount/DiscountActivityConvert.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.promotion.convert.discount; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.api.discount.dto.DiscountProductRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.*; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountProductDO; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 限时折扣活动 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface DiscountActivityConvert { + + DiscountActivityConvert INSTANCE = Mappers.getMapper(DiscountActivityConvert.class); + + DiscountActivityDO convert(DiscountActivityCreateReqVO bean); + + DiscountActivityDO convert(DiscountActivityUpdateReqVO bean); + + DiscountActivityRespVO convert(DiscountActivityDO bean); + + List convertList(List list); + + List convertList02(List list); + + PageResult convertPage(PageResult page); + + DiscountProductDO convert(DiscountActivityBaseVO.Product bean); + + DiscountActivityDetailRespVO convert(DiscountActivityDO activity, List products); + + // =========== 比较是否相等 ========== + /** + * 比较两个限时折扣商品是否相等 + * + * @param productDO 数据库中的商品 + * @param productVO 前端传入的商品 + * @return 是否匹配 + */ + @SuppressWarnings("DuplicatedCode") + default boolean isEquals(DiscountProductDO productDO, DiscountActivityBaseVO.Product productVO) { + if (ObjectUtil.notEqual(productDO.getSpuId(), productVO.getSpuId()) + || ObjectUtil.notEqual(productDO.getSkuId(), productVO.getSkuId()) + || ObjectUtil.notEqual(productDO.getDiscountType(), productVO.getDiscountType())) { + return false; + } + if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PRICE.getType())) { + return ObjectUtil.equal(productDO.getDiscountPrice(), productVO.getDiscountPrice()); + } + if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PERCENT.getType())) { + return ObjectUtil.equal(productDO.getDiscountPercent(), productVO.getDiscountPercent()); + } + return true; + } + + /** + * 比较两个限时折扣商品是否相等 + * 注意,比较时忽略 id 编号 + * + * @param productDO 商品 1 + * @param productVO 商品 2 + * @return 是否匹配 + */ + @SuppressWarnings("DuplicatedCode") + default boolean isEquals(DiscountProductDO productDO, DiscountProductDO productVO) { + if (ObjectUtil.notEqual(productDO.getSpuId(), productVO.getSpuId()) + || ObjectUtil.notEqual(productDO.getSkuId(), productVO.getSkuId()) + || ObjectUtil.notEqual(productDO.getDiscountType(), productVO.getDiscountType())) { + return false; + } + if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PRICE.getType())) { + return ObjectUtil.equal(productDO.getDiscountPrice(), productVO.getDiscountPrice()); + } + if (productDO.getDiscountType().equals(PromotionDiscountTypeEnum.PERCENT.getType())) { + return ObjectUtil.equal(productDO.getDiscountPercent(), productVO.getDiscountPercent()); + } + return true; + } + + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/price/PriceConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/price/PriceConvert.java new file mode 100644 index 0000000..fd90e0c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/price/PriceConvert.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.promotion.convert.price; + +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.promotion.api.price.dto.CouponMeetRespDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateReqDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateRespDTO; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Mapper +public interface PriceConvert { + + PriceConvert INSTANCE = Mappers.getMapper(PriceConvert.class); + + default PriceCalculateRespDTO convert(PriceCalculateReqDTO calculateReqDTO, List skuList) { + // 创建 PriceCalculateRespDTO 对象 + PriceCalculateRespDTO priceCalculate = new PriceCalculateRespDTO(); + // 创建它的 Order 属性 + PriceCalculateRespDTO.Order order = new PriceCalculateRespDTO.Order().setTotalPrice(0).setDiscountPrice(0) + .setCouponPrice(0).setPointPrice(0).setDeliveryPrice(0).setPayPrice(0) + .setItems(new ArrayList<>()).setCouponId(calculateReqDTO.getCouponId()); + priceCalculate.setOrder(order).setPromotions(new ArrayList<>()); + // 创建它的 OrderItem 属性 + Map skuIdCountMap = CollectionUtils.convertMap(calculateReqDTO.getItems(), + PriceCalculateReqDTO.Item::getSkuId, PriceCalculateReqDTO.Item::getCount); + skuList.forEach(sku -> { + Integer count = skuIdCountMap.get(sku.getId()); + PriceCalculateRespDTO.OrderItem orderItem = new PriceCalculateRespDTO.OrderItem() + .setSpuId(sku.getSpuId()).setSkuId(sku.getId()).setCount(count) + .setOriginalUnitPrice(sku.getPrice()).setOriginalPrice(sku.getPrice() * count) + .setDiscountPrice(0).setOrderPartPrice(0); + orderItem.setPayPrice(orderItem.getOriginalPrice()).setOrderDividePrice(orderItem.getOriginalPrice()); + priceCalculate.getOrder().getItems().add(orderItem); + // 补充价格信息到 Order 中 + order.setTotalPrice(order.getTotalPrice() + orderItem.getOriginalPrice()) + .setPayPrice(order.getTotalPrice()); + }); + return priceCalculate; + } + + CouponMeetRespDTO convert(CouponDO coupon); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/reward/RewardActivityConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/reward/RewardActivityConvert.java new file mode 100644 index 0000000..4fddf0d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/reward/RewardActivityConvert.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.promotion.convert.reward; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityRespVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.reward.RewardActivityDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 满减送活动 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface RewardActivityConvert { + + RewardActivityConvert INSTANCE = Mappers.getMapper(RewardActivityConvert.class); + + RewardActivityDO convert(RewardActivityCreateReqVO bean); + + RewardActivityDO convert(RewardActivityUpdateReqVO bean); + + RewardActivityRespVO convert(RewardActivityDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/seckill/seckillactivity/SeckillActivityConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/seckill/seckillactivity/SeckillActivityConvert.java new file mode 100644 index 0000000..8a8845f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/seckill/seckillactivity/SeckillActivityConvert.java @@ -0,0 +1,102 @@ +package com.yunxi.scm.module.promotion.convert.seckill.seckillactivity; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityDetailRespVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityRespVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductBaseVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductRespVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillProductDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 秒杀活动 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface SeckillActivityConvert { + + SeckillActivityConvert INSTANCE = Mappers.getMapper(SeckillActivityConvert.class); + + SeckillActivityDO convert(SeckillActivityCreateReqVO bean); + + SeckillActivityDO convert(SeckillActivityUpdateReqVO bean); + + SeckillActivityRespVO convert(SeckillActivityDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default PageResult convertPage(PageResult page, List seckillProducts, List spuList) { + Map spuMap = CollectionUtils.convertMap(spuList, ProductSpuRespDTO::getId, c -> c); + PageResult pageResult = convertPage(page); + pageResult.getList().forEach(item -> { + item.setSpuName(spuMap.get(item.getSpuId()).getName()); + item.setPicUrl(spuMap.get(item.getSpuId()).getPicUrl()); + item.setProducts(convertList2(seckillProducts)); + }); + return pageResult; + } + + SeckillActivityDetailRespVO convert1(SeckillActivityDO seckillActivity); + + default SeckillActivityDetailRespVO convert(SeckillActivityDO seckillActivity, List seckillProducts) { + SeckillActivityDetailRespVO respVO = convert1(seckillActivity); + respVO.setProducts(convertList2(seckillProducts)); + return respVO; + } + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(target = "activityId", source = "activityDO.id"), + @Mapping(target = "configIds", source = "activityDO.configIds"), + @Mapping(target = "spuId", source = "activityDO.spuId"), + @Mapping(target = "skuId", source = "vo.skuId"), + @Mapping(target = "seckillPrice", source = "vo.seckillPrice"), + @Mapping(target = "stock", source = "vo.stock"), + @Mapping(target = "activityStartTime", source = "activityDO.startTime"), + @Mapping(target = "activityEndTime", source = "activityDO.endTime") + }) + SeckillProductDO convert(SeckillActivityDO activityDO, SeckillProductBaseVO vo); + + default List convertList(SeckillActivityDO activityDO, List products) { + List list = new ArrayList<>(); + products.forEach(sku -> { + SeckillProductDO productDO = convert(activityDO, sku); + productDO.setActivityStatus(CommonStatusEnum.ENABLE.getStatus()); + list.add(productDO); + }); + return list; + } + + // TODO @puhui999:同拼团那个 convert 想通的情况哈。 + default List convertList1(SeckillActivityDO activityDO, List vos, List productDOs) { + Map longMap = CollectionUtils.convertMap(productDOs, SeckillProductDO::getSkuId, SeckillProductDO::getId); + List list = new ArrayList<>(); + vos.forEach(sku -> { + SeckillProductDO productDO = convert(activityDO, sku); + productDO.setId(longMap.get(sku.getSkuId())); + productDO.setActivityStatus(CommonStatusEnum.ENABLE.getStatus()); + list.add(productDO); + }); + return list; + } + + List convertList2(List productDOs); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/seckill/seckillconfig/SeckillConfigConvert.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/seckill/seckillconfig/SeckillConfigConvert.java new file mode 100644 index 0000000..96414f3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/convert/seckill/seckillconfig/SeckillConfigConvert.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.promotion.convert.seckill.seckillconfig; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigRespVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigSimpleRespVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillconfig.SeckillConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 秒杀时段 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface SeckillConfigConvert { + + SeckillConfigConvert INSTANCE = Mappers.getMapper(SeckillConfigConvert.class); + + SeckillConfigDO convert(SeckillConfigCreateReqVO bean); + + SeckillConfigDO convert(SeckillConfigUpdateReqVO bean); + + SeckillConfigRespVO convert(SeckillConfigDO bean); + + List convertList(List list); + + List convertList1(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/banner/BannerDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/banner/BannerDO.java new file mode 100644 index 0000000..ca5bb09 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/banner/BannerDO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.banner; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * banner DO + * + * @author xia + */ +@TableName("market_banner") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BannerDO extends BaseDO { + + /** + * 编号 + */ + private Long id; + /** + * 标题 + */ + private String title; + /** + * 跳转链接 + */ + private String url; + /** + * 图片链接 + */ + private String picUrl; + /** + * 排序 + */ + private Integer sort; + + /** + * 状态 {@link com.yunxi.scm.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String memo; + + // TODO 芋艿 点击次数。&& 其他数据相关 + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationActivityDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationActivityDO.java new file mode 100644 index 0000000..7280e9d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationActivityDO.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +// TODO @puhui999:是不是应该在 combination 哈? +/** + * 拼团活动 DO + * + * @author HUIHUI + */ +@TableName("promotion_combination_activity") +@KeySequence("promotion_combination_activity_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CombinationActivityDO extends BaseDO { + + /** + * 活动编号 + */ + @TableId + private Long id; + /** + * 拼团名称 + */ + private String name; + /** + * 商品 SPU 编号 + * + * 关联 ProductSpuDO 的 id + */ + private Long spuId; + /** + * 总限购数量 + */ + private Integer totalLimitCount; + /** + * 单次限购数量 + */ + private Integer singleLimitCount; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 结束时间 + */ + private LocalDateTime endTime; + /** + * 几人团 + */ + private Integer userSize; + /** + * 开团组数 + */ + private Integer totalNum; + /** + * 成团组数 + */ + private Integer successNum; + /** + * 参与人数 + */ + private Integer orderUserCount; + /** + * 虚拟成团 + */ + private Integer virtualGroup; + /** + * 活动状态:0开启 1关闭 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 限制时长(小时) + */ + private Integer limitDuration; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationProductDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationProductDO.java new file mode 100644 index 0000000..6b05b89 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationProductDO.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 拼团商品 DO + * + * @author HUIHUI + */ +@TableName("promotion_combination_product") +@KeySequence("promotion_combination_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CombinationProductDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 拼团活动编号 + */ + private Long activityId; + /** + * 商品 SPU 编号 + */ + private Long spuId; + /** + * 商品 SKU 编号 + */ + private Long skuId; + /** + * 拼团商品状态 + */ + private Integer activityStatus; + /** + * 活动开始时间点 + */ + private LocalDateTime activityStartTime; + /** + * 活动结束时间点 + */ + private LocalDateTime activityEndTime; + /** + * 拼团价格,单位分 + */ + private Integer activePrice; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationRecordDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationRecordDO.java new file mode 100644 index 0000000..648ba1d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/combination/combinationactivity/CombinationRecordDO.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.promotion.enums.combination.CombinationRecordStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 拼团记录 DO + * + * @author HUIHUI + */ +@TableName("promotion_combination_record") +@KeySequence("promotion_combination_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CombinationRecordDO extends BaseDO { + + @TableId + private Long id; + /** + * 拼团活动编号 + */ + private Long activityId; + /** + * SPU 编号 + */ + private Long spuId; + /** + * SKU 编号 + */ + private Long skuId; + /** + * 用户编号 + */ + private Long userId; + /** + * 订单编号 + */ + private Long orderId; + /** + * 团长编号 + * + * 关联 {@link CombinationRecordDO#getUserId()} + */ + private Long headId; + /** + * 商品名字 + */ + private String spuName; + /** + * 商品图片 + */ + private String picUrl; + /** + * 拼团商品单价 + */ + private Integer combinationPrice; + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + /** + * 开团状态 + * + * 关联 {@link CombinationRecordStatusEnum} + */ + private Integer status; + /** + * 是否虚拟成团 + */ + private Boolean virtualGroup; + /** + * 过期时间,单位:小时 + * + * 关联 {@link CombinationActivityDO#getLimitDuration()} + */ + private Integer expireTime; + /** + * 开始时间 (订单付款后开始的时间) + */ + private LocalDateTime startTime; + /** + * 结束时间(成团时间/失败时间) + */ + private LocalDateTime endTime; + /** + * 开团需要人数 + * + * 关联 {@link CombinationActivityDO#getUserSize()} + */ + private Integer userSize; + /** + * 已加入拼团人数 + */ + private Integer userCount; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/coupon/CouponDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/coupon/CouponDO.java new file mode 100644 index 0000000..8c6e54c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/coupon/CouponDO.java @@ -0,0 +1,139 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.coupon; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.LongListTypeHandler; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponStatusEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponTakeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 优惠劵 DO + * + * @author 芋道源码 + */ +@TableName(value = "promotion_coupon", autoResultMap = true) +@KeySequence("promotion_coupon_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class CouponDO extends BaseDO { + + // ========== 基本信息 BEGIN ========== + /** + * 优惠劵编号 + */ + private Long id; + /** + * 优惠劵模板编号 + * + * 关联 {@link CouponTemplateDO#getId()} + */ + private Integer templateId; + /** + * 优惠劵名 + * + * 冗余 {@link CouponTemplateDO#getName()} + */ + private String name; + /** + * 优惠码状态 + * + * 枚举 {@link CouponStatusEnum} + */ + private Integer status; + + // ========== 基本信息 END ========== + + // ========== 领取情况 BEGIN ========== + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 字段 + */ + private Long userId; + /** + * 领取类型 + * + * 枚举 {@link CouponTakeTypeEnum} + */ + private Integer takeType; + // ========== 领取情况 END ========== + + // ========== 使用规则 BEGIN ========== + /** + * 是否设置满多少金额可用,单位:分 + * + * 冗余 {@link CouponTemplateDO#getUsePrice()} + */ + private Integer usePrice; + /** + * 生效开始时间 + */ + private LocalDateTime validStartTime; + /** + * 生效结束时间 + */ + private LocalDateTime validEndTime; + /** + * 商品范围 + * + * 枚举 {@link PromotionProductScopeEnum} + */ + private Integer productScope; + /** + * 商品 SPU 编号的数组 + * + * 冗余 {@link CouponTemplateDO#getProductSpuIds()} + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List productSpuIds; + // ========== 使用规则 END ========== + + // ========== 使用效果 BEGIN ========== + /** + * 折扣类型 + * + * 冗余 {@link CouponTemplateDO#getDiscountType()} + */ + private Integer discountType; + /** + * 折扣百分比 + * + * 冗余 {@link CouponTemplateDO#getDiscountPercent()} + */ + private Integer discountPercent; + /** + * 优惠金额,单位:分 + * + * 冗余 {@link CouponTemplateDO#getDiscountPrice()} + */ + private Integer discountPrice; + /** + * 折扣上限,仅在 {@link #discountType} 等于 {@link PromotionDiscountTypeEnum#PERCENT} 时生效 + * + * 冗余 {@link CouponTemplateDO#getDiscountLimitPrice()} + */ + private Integer discountLimitPrice; + // ========== 使用效果 END ========== + + // ========== 使用情况 BEGIN ========== + /** + * 使用订单号 + */ + private Long useOrderId; + /** + * 使用时间 + */ + private LocalDateTime useTime; + + // ========== 使用情况 END ========== + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java new file mode 100644 index 0000000..766484a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java @@ -0,0 +1,162 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.coupon; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.LongListTypeHandler; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponTakeTypeEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 优惠劵模板 DO + * + * 当用户领取时,会生成 {@link CouponDO} 优惠劵 + * + * @author 芋道源码 + */ +@TableName(value = "promotion_coupon_template", autoResultMap = true) +@KeySequence("promotion_coupon_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class CouponTemplateDO extends BaseDO { + + // ========== 基本信息 BEGIN ========== + /** + * 模板编号,自增唯一 + */ + @TableId + private Long id; + /** + * 优惠劵名 + */ + private String name; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + // ========== 基本信息 END ========== + + // ========== 领取规则 BEGIN ========== + /** + * 发放数量 + * + * -1 - 则表示不限制发放数量 + */ + private Integer totalCount; + /** + * 每人限领个数 + * + * -1 - 则表示不限制 + */ + private Integer takeLimitCount; + /** + * 领取方式 + * + * 枚举 {@link CouponTakeTypeEnum} + */ + private Integer takeType; + // ========== 领取规则 END ========== + + // ========== 使用规则 BEGIN ========== + /** + * 是否设置满多少金额可用,单位:分 + * + * 0 - 不限制 + * 大于 0 - 多少金额可用 + */ + private Integer usePrice; + /** + * 商品范围 + * + * 枚举 {@link PromotionProductScopeEnum} + */ + private Integer productScope; + /** + * 商品 SPU 编号的数组 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List productSpuIds; + /** + * 生效日期类型 + * + * 枚举 {@link CouponTemplateValidityTypeEnum} + */ + private Integer validityType; + /** + * 固定日期 - 生效开始时间 + * + * 当 {@link #validityType} 为 {@link CouponTemplateValidityTypeEnum#DATE} + */ + private LocalDateTime validStartTime; + /** + * 固定日期 - 生效结束时间 + * + * 当 {@link #validityType} 为 {@link CouponTemplateValidityTypeEnum#DATE} + */ + private LocalDateTime validEndTime; + /** + * 领取日期 - 开始天数 + * + * 当 {@link #validityType} 为 {@link CouponTemplateValidityTypeEnum#TERM} + */ + private Integer fixedStartTerm; + /** + * 领取日期 - 结束天数 + * + * 当 {@link #validityType} 为 {@link CouponTemplateValidityTypeEnum#TERM} + */ + private Integer fixedEndTerm; + // ========== 使用规则 END ========== + + // ========== 使用效果 BEGIN ========== + /** + * 折扣类型 + * + * 枚举 {@link PromotionDiscountTypeEnum} + */ + private Integer discountType; + /** + * 折扣百分比 + * + * 例如,80% 为 80 + */ + private Integer discountPercent; + /** + * 优惠金额,单位:分 + * + * 当 {@link #discountType} 为 {@link PromotionDiscountTypeEnum#PRICE} 生效 + */ + private Integer discountPrice; + /** + * 折扣上限,仅在 {@link #discountType} 等于 {@link PromotionDiscountTypeEnum#PERCENT} 时生效 + * + * 例如,折扣上限为 20 元,当使用 8 折优惠券,订单金额为 1000 元时,最高只可折扣 20 元,而非 80 元。 + */ + private Integer discountLimitPrice; + // ========== 使用效果 END ========== + + // ========== 统计信息 BEGIN ========== + /** + * 领取优惠券的数量 + */ + private Integer takeCount; + /** + * 使用优惠券的次数 + */ + private Integer useCount; + // ========== 统计信息 END ========== + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/decorate/DecorateComponentDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/decorate/DecorateComponentDO.java new file mode 100644 index 0000000..9ee12bc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/decorate/DecorateComponentDO.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.decorate; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.promotion.enums.decorate.DecoratePageEnum; +import com.yunxi.scm.module.promotion.enums.decorate.DecorateComponentEnum; +import com.baomidou.mybatisplus.annotation.*; + +import lombok.Data; + +/** + * 页面装修组件 DO, 一个页面由多个组件构成 + * + * @author jason + */ +@TableName(value ="promotion_decorate_component") +@KeySequence("promotion_decorate_component_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DecorateComponentDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 所属页面 id + * 枚举 {@link DecoratePageEnum#getId()} + */ + private Integer pageId; + + /** + * 组件编码 + * 枚举 {@link DecorateComponentEnum#getCode()} + */ + private String code; + + /** + * 组件值:json 格式。包含配置和数据 + */ + private String value; + + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/discount/DiscountActivityDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/discount/DiscountActivityDO.java new file mode 100644 index 0000000..5ed4912 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/discount/DiscountActivityDO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.discount; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * 限时折扣活动 DO + * + * 一个活动下,可以有 {@link DiscountProductDO} 商品; + * 一个商品,在指定时间段内,只能属于一个活动; + * + * @author 芋道源码 + */ +@TableName(value = "promotion_discount_activity", autoResultMap = true) +@KeySequence("promotion_discount_activity_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class DiscountActivityDO extends BaseDO { + + /** + * 活动编号,主键自增 + */ + @TableId + private Long id; + /** + * 活动标题 + */ + private String name; + // TODO 芋艿:状态调整,只有开启和关闭; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + * + * 活动被关闭后,不允许再次开启。 + */ + private Integer status; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 结束时间 + */ + private LocalDateTime endTime; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/discount/DiscountProductDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/discount/DiscountProductDO.java new file mode 100644 index 0000000..f4c6ab0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/discount/DiscountProductDO.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.discount; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 限时折扣商品 DO + * + * @author 芋道源码 + */ +@TableName(value = "promotion_discount_product", autoResultMap = true) +@KeySequence("promotion_discount_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class DiscountProductDO extends BaseDO { + + /** + * 编号,主键自增 + */ + @TableId + private Long id; + + // TODO 芋艿:把 activity 所有的字段冗余过来 + /** + * 限时折扣活动的编号 + * + * 关联 {@link DiscountActivityDO#getId()} + */ + private Long activityId; + + /** + * 商品 SPU 编号 + * + * 关联 ProductSpuDO 的 id 编号 + */ + private Long spuId; + /** + * 商品 SKU 编号 + * + * 关联 ProductSkuDO 的 id 编号 + */ + private Long skuId; + + /** + * 折扣类型 + * + * 枚举 {@link PromotionDiscountTypeEnum} + */ + private Integer discountType; + /** + * 折扣百分比 + * + * 例如,80% 为 80 + */ + private Integer discountPercent; + /** + * 优惠金额,单位:分 + * + * 当 {@link #discountType} 为 {@link PromotionDiscountTypeEnum#PRICE} 生效 + */ + private Integer discountPrice; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/reward/RewardActivityDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/reward/RewardActivityDO.java new file mode 100644 index 0000000..0458dbb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/reward/RewardActivityDO.java @@ -0,0 +1,134 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.reward; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.LongListTypeHandler; +import com.yunxi.scm.module.promotion.enums.common.PromotionActivityStatusEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionConditionTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 满减送活动 DO + * + * @author 芋道源码 + */ +@TableName(value = "promotion_reward_activity", autoResultMap = true) +@KeySequence("promotion_reward_activity_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class RewardActivityDO extends BaseDO { + + /** + * 活动编号,主键自增 + */ + @TableId + private Long id; + /** + * 活动标题 + */ + private String name; + // TODO @芋艿:改成开启、禁用两种状态 + /** + * 状态 + * + * 枚举 {@link PromotionActivityStatusEnum} + */ + private Integer status; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 结束时间 + */ + private LocalDateTime endTime; + /** + * 备注 + */ + private String remark; + /** + * 条件类型 + * + * 枚举 {@link PromotionConditionTypeEnum} + */ + private Integer conditionType; + /** + * 商品范围 + * + * 枚举 {@link PromotionProductScopeEnum} + */ + private Integer productScope; + /** + * 商品 SPU 编号的数组 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List productSpuIds; + /** + * 优惠规则的数组 + */ + @TableField(typeHandler = RuleTypeHandler.class) + private List rules; + + /** + * 优惠规则 + */ + @Data + public static class Rule implements Serializable { + + /** + * 优惠门槛 + * + * 1. 满 N 元,单位:分 + * 2. 满 N 件 + */ + private Integer limit; + /** + * 优惠价格,单位:分 + */ + private Integer discountPrice; + /** + * 是否包邮 + */ + private Boolean freeDelivery; + /** + * 赠送的积分 + */ + private Integer point; + /** + * 赠送的优惠劵编号的数组 + */ + private List couponIds; + /** + * 赠送的优惠卷数量的数组 + */ + private List couponCounts; + + } + + // TODO @芋艿:可以找一些新的思路 + public static class RuleTypeHandler extends AbstractJsonTypeHandler> { + + @Override + protected List parse(String json) { + return JsonUtils.parseArray(json, Rule.class); + } + + @Override + protected String toJson(List obj) { + return JsonUtils.toJsonString(obj); + } + + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillactivity/SeckillActivityDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillactivity/SeckillActivityDO.java new file mode 100644 index 0000000..128e4fd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillactivity/SeckillActivityDO.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.LongListTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 秒杀活动 DO + * + * @author halfninety + */ +@TableName(value = "promotion_seckill_activity", autoResultMap = true) +@KeySequence("promotion_seckill_activity_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SeckillActivityDO extends BaseDO { + + /** + * 秒杀活动编号 + */ + @TableId + private Long id; + /** + * 秒杀活动商品 + */ + private Long spuId; + /** + * 秒杀活动名称 + */ + private String name; + /** + * 活动状态 + * + * 枚举 {@link CommonStatusEnum 对应的类} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 活动开始时间 + */ + private LocalDateTime startTime; + /** + * 活动结束时间 + */ + private LocalDateTime endTime; + /** + * 排序 + */ + private Integer sort; + /** + * 秒杀时段 id + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List configIds; + /** + * 新增订单数 + */ + private Integer orderCount; + /** + * 付款人数 + */ + private Integer userCount; + /** + * 订单实付金额,单位:分 + */ + private Long totalPrice; + /** + * 总限购数量 + */ + private Integer totalLimitCount; + /** + * 单次限够数量 + */ + private Integer singleLimitCount; + /** + * 秒杀库存 + */ + private Integer stock; + /** + * 秒杀总库存 + */ + private Integer totalStock; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillactivity/SeckillProductDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillactivity/SeckillProductDO.java new file mode 100644 index 0000000..bd231c7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillactivity/SeckillProductDO.java @@ -0,0 +1,81 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.LongListTypeHandler; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillconfig.SeckillConfigDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 秒杀参与商品 DO + * + * @author HUIHUI + */ +@TableName("promotion_seckill_product") +@KeySequence("promotion_seckill_product_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SeckillProductDO extends BaseDO { + + /** + * 秒杀参与商品编号 + */ + @TableId + private Long id; + /** + * 秒杀活动 id + * + * 关联 {@link SeckillActivityDO#getId()} + */ + private Long activityId; + /** + * 秒杀时段 id + * + * 关联 {@link SeckillConfigDO#getId()} + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List configIds; + /** + * 商品 SPU 编号 + */ + private Long spuId; + /** + * 商品 SKU 编号 + */ + private Long skuId; + /** + * 秒杀金额,单位:分 + */ + private Integer seckillPrice; + /** + * 秒杀库存 + */ + private Integer stock; + + /** + * 秒杀商品状态 + * + * 枚举 {@link CommonStatusEnum 对应的类} + */ + private Integer activityStatus; + /** + * 活动开始时间点 + */ + private LocalDateTime activityStartTime; + /** + * 活动结束时间点 + */ + private LocalDateTime activityEndTime; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillconfig/SeckillConfigDO.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillconfig/SeckillConfigDO.java new file mode 100644 index 0000000..3f62b2e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/dataobject/seckill/seckillconfig/SeckillConfigDO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillconfig; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * 秒杀时段 DO + * + * @author 芋道源码 + */ +@TableName(value = "promotion_seckill_config", autoResultMap = true) +@KeySequence("promotion_seckill_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SeckillConfigDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 秒杀时段名称 + */ + private String name; + /** + * 开始时间点 + */ + private String startTime; + /** + * 结束时间点 + */ + private String endTime; + /** + * 秒杀轮播图 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List sliderPicUrls; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum 对应的类} + */ + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/banner/BannerMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/banner/BannerMapper.java new file mode 100644 index 0000000..1799282 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/banner/BannerMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.promotion.dal.mysql.banner; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.banner.BannerDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * Banner Mapper + * + * @author xia + */ +@Mapper +public interface BannerMapper extends BaseMapperX { + + default PageResult selectPage(BannerPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(BannerDO::getTitle, reqVO.getTitle()) + .eqIfPresent(BannerDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BannerDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(BannerDO::getSort)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationActivityMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationActivityMapper.java new file mode 100644 index 0000000..a928c4f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationActivityMapper.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.promotion.dal.mysql.combination.combinationactivity; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityExportReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationActivityDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +// TODO @puhui999:是不是应该在 combination 哈? +/** + * 拼团活动 Mapper + * + * @author HUIHUI + */ +@Mapper +public interface CombinationActivityMapper extends BaseMapperX { + + default PageResult selectPage(CombinationActivityPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(CombinationActivityDO::getName, reqVO.getName()) + .orderByDesc(CombinationActivityDO::getId)); + } + + default List selectList(CombinationActivityExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(CombinationActivityDO::getName, reqVO.getName()) + .eqIfPresent(CombinationActivityDO::getStatus, reqVO.getStatus()) + .orderByDesc(CombinationActivityDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(CombinationActivityDO::getStatus, status); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationProductMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationProductMapper.java new file mode 100644 index 0000000..092f991 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationProductMapper.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.promotion.dal.mysql.combination.combinationactivity; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductExportReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationProductDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * 拼团商品 Mapper + * + * @author HUIHUI + */ +@Mapper +public interface CombinationProductMapper extends BaseMapperX { + + default PageResult selectPage(CombinationProductPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(CombinationProductDO::getActivityId, reqVO.getActivityId()) + .eqIfPresent(CombinationProductDO::getSpuId, reqVO.getSpuId()) + .eqIfPresent(CombinationProductDO::getSkuId, reqVO.getSkuId()) + .eqIfPresent(CombinationProductDO::getActivityStatus, reqVO.getActivityStatus()) + .betweenIfPresent(CombinationProductDO::getActivityStartTime, reqVO.getActivityStartTime()) + .betweenIfPresent(CombinationProductDO::getActivityEndTime, reqVO.getActivityEndTime()) + .eqIfPresent(CombinationProductDO::getActivePrice, reqVO.getActivePrice()) + .betweenIfPresent(CombinationProductDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(CombinationProductDO::getId)); + } + + default List selectList(CombinationProductExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(CombinationProductDO::getActivityId, reqVO.getActivityId()) + .eqIfPresent(CombinationProductDO::getSpuId, reqVO.getSpuId()) + .eqIfPresent(CombinationProductDO::getSkuId, reqVO.getSkuId()) + .eqIfPresent(CombinationProductDO::getActivityStatus, reqVO.getActivityStatus()) + .betweenIfPresent(CombinationProductDO::getActivityStartTime, reqVO.getActivityStartTime()) + .betweenIfPresent(CombinationProductDO::getActivityEndTime, reqVO.getActivityEndTime()) + .eqIfPresent(CombinationProductDO::getActivePrice, reqVO.getActivePrice()) + .betweenIfPresent(CombinationProductDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(CombinationProductDO::getId)); + } + + default List selectListByActivityIds(Collection ids) { + return selectList(CombinationProductDO::getActivityId, ids); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationRecordMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationRecordMapper.java new file mode 100644 index 0000000..959adf3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/combination/combinationactivity/CombinationRecordMapper.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.promotion.dal.mysql.combination.combinationactivity; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationRecordDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 拼团记录 Mapper + * + * @author HUIHUI + */ +@Mapper +public interface CombinationRecordMapper extends BaseMapperX { + + default CombinationRecordDO selectRecord(Long userId, Long orderId) { + return selectOne(CombinationRecordDO::getUserId, userId, + CombinationRecordDO::getOrderId, orderId); + } + + default List selectListByHeadIdAndStatus(Long headId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(CombinationRecordDO::getHeadId, headId) + .eq(CombinationRecordDO::getStatus, status)); + } + + default List selectListByStatus(Integer status) { + return selectList(CombinationRecordDO::getStatus, status); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/coupon/CouponMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/coupon/CouponMapper.java new file mode 100644 index 0000000..f5b26c9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/coupon/CouponMapper.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.promotion.dal.mysql.coupon; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * 优惠劵 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface CouponMapper extends BaseMapperX { + + default PageResult selectPage(CouponPageReqVO reqVO, Collection userIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(CouponDO::getTemplateId, reqVO.getTemplateId()) + .eqIfPresent(CouponDO::getStatus, reqVO.getStatus()) + .inIfPresent(CouponDO::getUserId, userIds) + .betweenIfPresent(CouponDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(CouponDO::getId)); + } + + default List selectListByUserIdAndStatus(Long userId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(CouponDO::getUserId, userId).eq(CouponDO::getStatus, status)); + } + + default CouponDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(new LambdaQueryWrapperX() + .eq(CouponDO::getId, id).eq(CouponDO::getUserId, userId)); + } + + default int delete(Long id, Collection whereStatuses) { + return update(null, new LambdaUpdateWrapper() + .eq(CouponDO::getId, id).in(CouponDO::getStatus, whereStatuses) + .set(CouponDO::getDeleted, 1)); + } + + default int updateByIdAndStatus(Long id, Integer status, CouponDO updateObj) { + return update(updateObj, new LambdaUpdateWrapper() + .eq(CouponDO::getId, id).eq(CouponDO::getStatus, status)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java new file mode 100644 index 0000000..5bf1eec --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.promotion.dal.mysql.coupon; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponTemplateDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +/** + * 优惠劵模板 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface CouponTemplateMapper extends BaseMapperX { + + default PageResult selectPage(CouponTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(CouponTemplateDO::getName, reqVO.getName()) + .eqIfPresent(CouponTemplateDO::getStatus, reqVO.getStatus()) + .eqIfPresent(CouponTemplateDO::getDiscountType, reqVO.getDiscountType()) + .betweenIfPresent(CouponTemplateDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(CouponTemplateDO::getId)); + } + + void updateTakeCount(@Param("id") Long id, @Param("incrCount") Integer incrCount); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/decorate/DecorateComponentMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/decorate/DecorateComponentMapper.java new file mode 100644 index 0000000..614b3ee --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/decorate/DecorateComponentMapper.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.promotion.dal.mysql.decorate; + +import com.yunxi.scm.module.promotion.dal.dataobject.decorate.DecorateComponentDO; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface DecorateComponentMapper extends BaseMapperX { + + default List selectByPage(Integer pageId){ + return selectList(DecorateComponentDO::getPageId, pageId); + } + +} + + + + diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/discount/DiscountActivityMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/discount/DiscountActivityMapper.java new file mode 100644 index 0000000..bfede80 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/discount/DiscountActivityMapper.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.promotion.dal.mysql.discount; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountActivityDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * 限时折扣活动 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface DiscountActivityMapper extends BaseMapperX { + + default PageResult selectPage(DiscountActivityPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DiscountActivityDO::getName, reqVO.getName()) + .eqIfPresent(DiscountActivityDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DiscountActivityDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(DiscountActivityDO::getId)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/discount/DiscountProductMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/discount/DiscountProductMapper.java new file mode 100644 index 0000000..8ae30e1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/discount/DiscountProductMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.promotion.dal.mysql.discount; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountProductDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * 限时折扣商城 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface DiscountProductMapper extends BaseMapperX { + + default List selectListBySkuId(Collection skuIds) { + return selectList(DiscountProductDO::getSkuId, skuIds); + } + + default List selectListByActivityId(Long activityId) { + return selectList(DiscountProductDO::getActivityId, activityId); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/reward/RewardActivityMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/reward/RewardActivityMapper.java new file mode 100644 index 0000000..7288e0e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/reward/RewardActivityMapper.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.promotion.dal.mysql.reward; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.reward.RewardActivityDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * 满减送活动 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface RewardActivityMapper extends BaseMapperX { + + default PageResult selectPage(RewardActivityPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(RewardActivityDO::getName, reqVO.getName()) + .eqIfPresent(RewardActivityDO::getStatus, reqVO.getStatus()) + .orderByDesc(RewardActivityDO::getId)); + } + + default List selectListByStatus(Collection statuses) { + return selectList(RewardActivityDO::getStatus, statuses); + } + + default List selectListByProductScopeAndStatus(Integer productScope, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(RewardActivityDO::getProductScope, productScope) + .eq(RewardActivityDO::getStatus, status)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java new file mode 100644 index 0000000..ff17749 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillactivity/SeckillActivityMapper.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.promotion.dal.mysql.seckill.seckillactivity; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillActivityDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 秒杀活动 Mapper + * + * @author halfninety + */ +@Mapper +public interface SeckillActivityMapper extends BaseMapperX { + + default PageResult selectPage(SeckillActivityPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SeckillActivityDO::getName, reqVO.getName()) + .eqIfPresent(SeckillActivityDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SeckillActivityDO::getCreateTime, reqVO.getCreateTime()) + .apply(ObjectUtil.isNotNull(reqVO.getConfigId()), "FIND_IN_SET(" + reqVO.getConfigId() + ",time_ids) > 0") + .orderByDesc(SeckillActivityDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(SeckillActivityDO::getStatus, status)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillactivity/SeckillProductMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillactivity/SeckillProductMapper.java new file mode 100644 index 0000000..d15d130 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillactivity/SeckillProductMapper.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.promotion.dal.mysql.seckill.seckillactivity; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillProductDO; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +/** + * 秒杀活动商品 Mapper + * + * @author halfninety + */ +@Mapper +public interface SeckillProductMapper extends BaseMapperX { + + default List selectListByActivityId(Long id) { + return selectList(SeckillProductDO::getActivityId, id); + } + + default List selectListByActivityId(Collection ids) { + return selectList(SeckillProductDO::getActivityId, ids); + } + + default List selectListBySkuIds(Collection skuIds) { + return selectList(SeckillProductDO::getSkuId, skuIds); + } + + default void updateTimeIdsByActivityId(Long id, List timeIds) { + new LambdaUpdateChainWrapper<>(this) + .set(SeckillProductDO::getConfigIds, CollUtil.join(timeIds, ",")) + .eq(SeckillProductDO::getActivityId, id) + .update(); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillconfig/SeckillConfigMapper.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillconfig/SeckillConfigMapper.java new file mode 100644 index 0000000..3a0cc2e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/dal/mysql/seckill/seckillconfig/SeckillConfigMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.promotion.dal.mysql.seckill.seckillconfig; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillconfig.SeckillConfigDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SeckillConfigMapper extends BaseMapperX { + + default PageResult selectPage(SeckillConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SeckillConfigDO::getName, reqVO.getName()) + .eqIfPresent(SeckillConfigDO::getStatus, reqVO.getStatus()) + .orderByDesc(SeckillConfigDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(SeckillConfigDO::getStatus, status); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/package-info.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/package-info.java new file mode 100644 index 0000000..70ae24b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 promotion 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.promotion.framework; diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/web/config/PromotionWebConfiguration.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/web/config/PromotionWebConfiguration.java new file mode 100644 index 0000000..9f2cd49 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/web/config/PromotionWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.promotion.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * promotion 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class PromotionWebConfiguration { + + /** + * promotion 模块的 API 分组 + */ + @Bean + public GroupedOpenApi promotionGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("promotion"); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/web/package-info.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/web/package-info.java new file mode 100644 index 0000000..c16fbe0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * promotion 模块的 web 配置 + */ +package com.yunxi.scm.module.promotion.framework.web; diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/package-info.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/package-info.java new file mode 100644 index 0000000..a5b1a1e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/package-info.java @@ -0,0 +1,8 @@ +/** + * promotion 模块,我们放营销业务。 + * 例如说:营销活动、banner、优惠券等等 + * + * 1. Controller URL:以 /promotion/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 promotion_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.promotion; diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/banner/BannerService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/banner/BannerService.java new file mode 100644 index 0000000..82fcdd1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/banner/BannerService.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.promotion.service.banner; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.banner.BannerDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 首页 Banner Service 接口 + * + * @author xia + */ +public interface BannerService { + + /** + * 创建 Banner + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createBanner(@Valid BannerCreateReqVO createReqVO); + + /** + * 更新 Banner + * + * @param updateReqVO 更新信息 + */ + void updateBanner(@Valid BannerUpdateReqVO updateReqVO); + + /** + * 删除 Banner + * + * @param id 编号 + */ + void deleteBanner(Long id); + + /** + * 获得 Banner + * + * @param id 编号 + * @return Banner + */ + BannerDO getBanner(Long id); + + /** + * 获得所有 Banner列表 + * @return Banner列表 + */ + List getBannerList(); + + /** + * 获得 Banner 分页 + * + * @param pageReqVO 分页查询 + * @return Banner分页 + */ + PageResult getBannerPage(BannerPageReqVO pageReqVO); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/banner/BannerServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/banner/BannerServiceImpl.java new file mode 100644 index 0000000..021f200 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/banner/BannerServiceImpl.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.promotion.service.banner; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.banner.vo.BannerUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.banner.BannerConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.banner.BannerDO; +import com.yunxi.scm.module.promotion.dal.mysql.banner.BannerMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.BANNER_NOT_EXISTS; + +/** + * 首页 banner 实现类 + * + * @author xia + */ +@Service +@Validated +public class BannerServiceImpl implements BannerService { + + @Resource + private BannerMapper bannerMapper; + + @Override + public Long createBanner(BannerCreateReqVO createReqVO) { + // 插入 + BannerDO banner = BannerConvert.INSTANCE.convert(createReqVO); + bannerMapper.insert(banner); + // 返回 + return banner.getId(); + } + + @Override + public void updateBanner(BannerUpdateReqVO updateReqVO) { + // 校验存在 + this.validateBannerExists(updateReqVO.getId()); + // 更新 + BannerDO updateObj = BannerConvert.INSTANCE.convert(updateReqVO); + bannerMapper.updateById(updateObj); + } + + @Override + public void deleteBanner(Long id) { + // 校验存在 + this.validateBannerExists(id); + // 删除 + bannerMapper.deleteById(id); + } + + private void validateBannerExists(Long id) { + if (bannerMapper.selectById(id) == null) { + throw exception(BANNER_NOT_EXISTS); + } + } + + @Override + public BannerDO getBanner(Long id) { + return bannerMapper.selectById(id); + } + + @Override + public List getBannerList() { + return bannerMapper.selectList(); + } + + @Override + public PageResult getBannerPage(BannerPageReqVO pageReqVO) { + return bannerMapper.selectPage(pageReqVO); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityService.java new file mode 100644 index 0000000..4d1946a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityService.java @@ -0,0 +1,123 @@ +package com.yunxi.scm.module.promotion.service.combination; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.api.combination.dto.CombinationRecordReqDTO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityExportReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationProductDO; + +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +/** + * 拼团活动 Service 接口 + * + * @author HUIHUI + */ +public interface CombinationActivityService { + + /** + * 创建拼团活动 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createCombinationActivity(@Valid CombinationActivityCreateReqVO createReqVO); + + /** + * 更新拼团活动 + * + * @param updateReqVO 更新信息 + */ + void updateCombinationActivity(@Valid CombinationActivityUpdateReqVO updateReqVO); + + /** + * 删除拼团活动 + * + * @param id 编号 + */ + void deleteCombinationActivity(Long id); + + /** + * 获得拼团活动 + * + * @param id 编号 + * @return 拼团活动 + */ + CombinationActivityDO getCombinationActivity(Long id); + + /** + * 获得拼团活动列表 + * + * @param ids 编号 + * @return 拼团活动列表 + */ + List getCombinationActivityList(Collection ids); + + /** + * 获得拼团活动分页 + * + * @param pageReqVO 分页查询 + * @return 拼团活动分页 + */ + PageResult getCombinationActivityPage(CombinationActivityPageReqVO pageReqVO); + + /** + * 获得拼团活动列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 拼团活动列表 + */ + List getCombinationActivityList(CombinationActivityExportReqVO exportReqVO); + + /** + * 获得拼团活动商品列表 + * + * @param ids 拼团活动 ids + * @return 拼团活动的商品列表 + */ + List getProductsByActivityIds(Collection ids); + + // TODO @puhui999:拆一个 CombinationRecordService 里,方法名可以简洁成 updateRecordStatusByOrderId;service 方法可以稍微简单一点,如果是 update 方法 + /** + * 更新拼团状态 + * + * @param userId 用户编号 + * @param orderId 订单编号 + * @param status 状态 + */ + void updateRecordStatusByUserIdAndOrderId(Long userId, Long orderId, Integer status); + + /** + * 更新拼团状态和开始时间 + * + * @param userId 用户编号 + * @param orderId 订单编号 + * @param status 状态 + * @param startTime 开始时间 + */ + void updateRecordStatusAndStartTimeByUserIdAndOrderId(Long userId, Long orderId, Integer status, LocalDateTime startTime); + + // TODO @puhui999:拆一个 CombinationRecordService 里 + /** + * 创建拼团记录 + * + * @param reqDTO 创建信息 + */ + void createRecord(CombinationRecordReqDTO reqDTO); + + /** + * 获得拼团状态 + * + * @param userId 用户编号 + * @param orderId 订单编号 + * @return 拼团状态 + */ + boolean validateRecordStatusIsSuccess(Long userId, Long orderId); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityServiceImpl.java new file mode 100644 index 0000000..a33969e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityServiceImpl.java @@ -0,0 +1,286 @@ +package com.yunxi.scm.module.promotion.service.combination; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.product.api.sku.ProductSkuApi; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.spu.ProductSpuApi; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.promotion.api.combination.dto.CombinationRecordReqDTO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityExportReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.product.CombinationProductUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.combination.CombinationActivityConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationProductDO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationRecordDO; +import com.yunxi.scm.module.promotion.dal.mysql.combination.combinationactivity.CombinationActivityMapper; +import com.yunxi.scm.module.promotion.dal.mysql.combination.combinationactivity.CombinationProductMapper; +import com.yunxi.scm.module.promotion.dal.mysql.combination.combinationactivity.CombinationRecordMapper; +import com.yunxi.scm.module.promotion.enums.combination.CombinationRecordStatusEnum; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.*; +import static com.yunxi.scm.module.promotion.util.PromotionUtils.validateProductSkuExistence; + +/** + * 拼团活动 Service 实现类 + * + * @author HUIHUI + */ +@Service +@Validated +public class CombinationActivityServiceImpl implements CombinationActivityService { + + @Resource + private CombinationActivityMapper combinationActivityMapper; + @Resource + private CombinationRecordMapper recordMapper; + @Resource + private CombinationProductMapper combinationProductMapper; + @Resource + private ProductSpuApi productSpuApi; + @Resource + private ProductSkuApi productSkuApi; + + @Override + public Long createCombinationActivity(CombinationActivityCreateReqVO createReqVO) { + // 校验商品 SPU 是否存在是否参加的别的活动 + validateProductCombinationConflict(createReqVO.getSpuId(), null); + // 获取所选 spu下的所有 sku + List skus = productSkuApi.getSkuListBySpuId(CollectionUtil.newArrayList(createReqVO.getSpuId())); + // 校验商品 sku 是否存在 + validateProductSkuExistence(skus, createReqVO.getProducts(), CombinationProductCreateReqVO::getSkuId); + + // TODO 艿艿 有个小问题:现在有活动时间和限制时长,活动时间的结束时间早于设置的限制时间怎么算状态比如: + // 活动时间 2023-08-05 15:00:00 - 2023-08-05 15:20:00 限制时长 2小时,那么活动时间结束就结束还是加时到满两小时 + // 插入拼团活动 + CombinationActivityDO activityDO = CombinationActivityConvert.INSTANCE.convert(createReqVO); + // TODO 营销相关属性初始化 + activityDO.setTotalNum(0); + activityDO.setSuccessNum(0); + activityDO.setOrderUserCount(0); + activityDO.setVirtualGroup(0); + activityDO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + combinationActivityMapper.insert(activityDO); + // 插入商品 + List productDOs = CombinationActivityConvert.INSTANCE.convertList(activityDO, createReqVO.getProducts()); + combinationProductMapper.insertBatch(productDOs); + // 返回 + return activityDO.getId(); + } + + private void validateProductCombinationConflict(Long spuId, Long activityId) { + // 校验商品 spu 是否存在 + List spuList = productSpuApi.getSpuList(CollUtil.newArrayList(spuId)); + if (CollUtil.isEmpty(spuList)) { + throw exception(SPU_NOT_EXISTS); + } + // 查询所有开启的拼团活动 + List activityDOs = combinationActivityMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + // 更新时排除自己 + if (activityId != null) { + activityDOs.removeIf(item -> ObjectUtil.equal(item.getId(), activityId)); + } + // 过滤出所有 spuIds 有交集的活动 + List doList = CollectionUtils.convertList(activityDOs, c -> c, s -> ObjectUtil.equal(s.getId(), spuId)); + if (CollUtil.isNotEmpty(doList)) { + throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS); + } + } + + @Override + public void updateCombinationActivity(CombinationActivityUpdateReqVO updateReqVO) { + // 校验存在 + CombinationActivityDO activityDO = validateCombinationActivityExists(updateReqVO.getId()); + // 校验状态 + if (ObjectUtil.equal(activityDO.getStatus(), CommonStatusEnum.DISABLE.getStatus())) { + throw exception(COMBINATION_ACTIVITY_STATUS_DISABLE); + } + // 校验商品冲突 + validateProductCombinationConflict(updateReqVO.getSpuId(), updateReqVO.getId()); + // 获取所选 spu下的所有 sku + List skus = productSkuApi.getSkuListBySpuId(CollectionUtil.newArrayList(updateReqVO.getSpuId())); + // 校验商品 sku 是否存在 + validateProductSkuExistence(skus, updateReqVO.getProducts(), CombinationProductUpdateReqVO::getSkuId); + + // 更新 + CombinationActivityDO updateObj = CombinationActivityConvert.INSTANCE.convert(updateReqVO); + combinationActivityMapper.updateById(updateObj); + // 更新商品 + updateCombinationProduct(updateObj, updateReqVO.getProducts()); + } + + /** + * 更新秒杀商品 + * TODO 更新商品要不要封装成通用方法 + * + * @param updateObj DO + * @param products 商品配置 + */ + private void updateCombinationProduct(CombinationActivityDO updateObj, List products) { + List combinationProductDOs = combinationProductMapper.selectListByActivityIds(CollUtil.newArrayList(updateObj.getId())); + // 数据库中的活动商品 + Set convertSet = CollectionUtils.convertSet(combinationProductDOs, CombinationProductDO::getSkuId); + // 前端传过来的活动商品 + Set convertSet1 = CollectionUtils.convertSet(products, CombinationProductUpdateReqVO::getSkuId); + // 删除后台存在的前端不存在的商品 + List d = CollectionUtils.filterList(convertSet, item -> !convertSet1.contains(item)); + if (CollUtil.isNotEmpty(d)) { + combinationProductMapper.deleteBatchIds(d); + } + // 前端存在的后端不存在的商品 + List c = CollectionUtils.filterList(convertSet1, item -> !convertSet.contains(item)); + if (CollUtil.isNotEmpty(c)) { + List vos = CollectionUtils.filterList(products, item -> c.contains(item.getSkuId())); + List productDOs = CombinationActivityConvert.INSTANCE.convertList(updateObj, vos); + combinationProductMapper.insertBatch(productDOs); + } + // 更新已存在的商品 + List u = CollectionUtils.filterList(convertSet1, convertSet::contains); + if (CollUtil.isNotEmpty(u)) { + List vos = CollectionUtils.filterList(products, item -> u.contains(item.getSkuId())); + List productDOs = CombinationActivityConvert.INSTANCE.convertList1(updateObj, vos, combinationProductDOs); + combinationProductMapper.updateBatch(productDOs); + } + } + + @Override + public void deleteCombinationActivity(Long id) { + // 校验存在 + CombinationActivityDO activityDO = validateCombinationActivityExists(id); + // 校验状态 + if (ObjectUtil.equal(activityDO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(COMBINATION_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END); + } + + // 删除 + combinationActivityMapper.deleteById(id); + } + + private CombinationActivityDO validateCombinationActivityExists(Long id) { + CombinationActivityDO activityDO = combinationActivityMapper.selectById(id); + if (activityDO == null) { + throw exception(COMBINATION_ACTIVITY_NOT_EXISTS); + } + return activityDO; + } + + @Override + public CombinationActivityDO getCombinationActivity(Long id) { + return validateCombinationActivityExists(id); + } + + @Override + public List getCombinationActivityList(Collection ids) { + return combinationActivityMapper.selectBatchIds(ids); + } + + @Override + public PageResult getCombinationActivityPage(CombinationActivityPageReqVO pageReqVO) { + return combinationActivityMapper.selectPage(pageReqVO); + } + + @Override + public List getCombinationActivityList(CombinationActivityExportReqVO exportReqVO) { + return combinationActivityMapper.selectList(exportReqVO); + } + + @Override + public List getProductsByActivityIds(Collection ids) { + return combinationProductMapper.selectListByActivityIds(ids); + } + + @Override + public void updateRecordStatusByUserIdAndOrderId(Long userId, Long orderId, Integer status) { + // 校验拼团是否存在 + CombinationRecordDO recordDO = validateCombinationRecord(userId, orderId); + + // 更新状态 + recordDO.setStatus(status); + recordMapper.updateById(recordDO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateRecordStatusAndStartTimeByUserIdAndOrderId(Long userId, Long orderId, Integer status, LocalDateTime startTime) { + CombinationRecordDO recordDO = validateCombinationRecord(userId, orderId); + // 更新状态 + recordDO.setStatus(status); + // 更新开始时间 + recordDO.setStartTime(startTime); + recordMapper.updateById(recordDO); + + // 更新拼团参入人数 + List recordDOs = recordMapper.selectListByHeadIdAndStatus(recordDO.getHeadId(), status); + if (CollUtil.isNotEmpty(recordDOs)) { + recordDOs.forEach(item -> { + item.setUserCount(recordDOs.size()); + // 校验拼团是否满足要求 + if (recordDOs.size() >= recordDO.getUserSize()) { + item.setStatus(CombinationRecordStatusEnum.SUCCESS.getStatus()); + } + }); + } + recordMapper.updateBatch(recordDOs); + } + + private CombinationRecordDO validateCombinationRecord(Long userId, Long orderId) { + // 校验拼团是否存在 + CombinationRecordDO recordDO = recordMapper.selectRecord(userId, orderId); + if (recordDO == null) { + throw exception(COMBINATION_RECORD_NOT_EXISTS); + } + return recordDO; + } + + @Override + public void createRecord(CombinationRecordReqDTO reqDTO) { + // 校验拼团活动 + CombinationActivityDO activity = validateCombinationActivityExists(reqDTO.getActivityId()); + // TODO @puhui999:需要校验下,它当前是不是已经参加了该拼团; + // TODO @puhui999: 父拼团是否存在,是否已经满了 + + CombinationRecordDO record = CombinationActivityConvert.INSTANCE.convert(reqDTO); + record.setVirtualGroup(false); + // TODO @puhui999:过期时间,应该是 Date 哈; + record.setExpireTime(activity.getLimitDuration()); + record.setUserSize(activity.getUserSize()); + recordMapper.insert(record); + } + + @Override + public boolean validateRecordStatusIsSuccess(Long userId, Long orderId) { + CombinationRecordDO record = validateCombinationRecord(userId, orderId); + // TODO @puhui999:可以搞个 getRecrod 方法,然后业务通过 CombinationRecordStatusEnum.isSuccess 方法校验 + return ObjectUtil.equal(record.getStatus(), CombinationRecordStatusEnum.SUCCESS.getStatus()); + } + + // TODO @puhui999:status 传入进来搞哈; + /** + * APP 端获取开团记录 + * + * @return 开团记录 + */ + public List getRecordList() { + return recordMapper.selectListByStatus(CombinationRecordStatusEnum.IN_PROGRESS.getStatus()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponService.java new file mode 100644 index 0000000..1db5daa --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponService.java @@ -0,0 +1,70 @@ +package com.yunxi.scm.module.promotion.service.coupon; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; + +import java.util.List; + +/** + * 优惠劵 Service 接口 + * + * @author 芋道源码 + */ +public interface CouponService { + + /** + * 校验优惠劵,包括状态、有限期 + * + * 1. 如果校验通过,则返回优惠劵信息 + * 2. 如果校验不通过,则直接抛出业务异常 + * + * @param id 优惠劵编号 + * @param userId 用户编号 + * @return 优惠劵信息 + */ + CouponDO validCoupon(Long id, Long userId); + + /** + * 校验优惠劵,包括状态、有限期 + * + * @see #validCoupon(Long, Long) 逻辑相同,只是入参不同 + * + * @param coupon 优惠劵 + */ + void validCoupon(CouponDO coupon); + + /** + * 获得优惠劵分页 + * + * @param pageReqVO 分页查询 + * @return 优惠劵分页 + */ + PageResult getCouponPage(CouponPageReqVO pageReqVO); + + /** + * 使用优惠劵 + * + * @param id 优惠劵编号 + * @param userId 用户编号 + * @param orderId 订单编号 + */ + void useCoupon(Long id, Long userId, Long orderId); + + /** + * 回收优惠劵 + * + * @param id 优惠劵编号 + */ + void deleteCoupon(Long id); + + /** + * 获得用户的优惠劵列表 + * + * @param userId 用户编号 + * @param status 优惠劵状态 + * @return 优惠劵列表 + */ + List getCouponList(Long userId, Integer status); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponServiceImpl.java new file mode 100644 index 0000000..823d270 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponServiceImpl.java @@ -0,0 +1,123 @@ +package com.yunxi.scm.module.promotion.service.coupon; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.module.member.api.user.MemberUserApi; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.coupon.CouponPageReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; +import com.yunxi.scm.module.promotion.dal.mysql.coupon.CouponMapper; +import com.yunxi.scm.module.promotion.enums.coupon.CouponStatusEnum; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.*; +import static java.util.Arrays.asList; + +/** + * 优惠劵 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class CouponServiceImpl implements CouponService { + + @Resource + private CouponTemplateService couponTemplateService; + + @Resource + private CouponMapper couponMapper; + + @Resource + private MemberUserApi memberUserApi; + + @Override + public CouponDO validCoupon(Long id, Long userId) { + CouponDO coupon = couponMapper.selectByIdAndUserId(id, userId); + if (coupon == null) { + throw exception(COUPON_NOT_EXISTS); + } + validCoupon(coupon); + return coupon; + } + + @Override + public void validCoupon(CouponDO coupon) { + // 校验状态 + if (ObjectUtil.notEqual(coupon.getStatus(), CouponStatusEnum.UNUSED.getStatus())) { + throw exception(COUPON_STATUS_NOT_UNUSED); + } + // 校验有效期;为避免定时器没跑,实际优惠劵已经过期 + if (LocalDateTimeUtils.isBetween(coupon.getValidStartTime(), coupon.getValidEndTime())) { + throw exception(COUPON_VALID_TIME_NOT_NOW); + } + } + + @Override + public PageResult getCouponPage(CouponPageReqVO pageReqVO) { + // 获得用户编号 + Set userIds = null; + if (StrUtil.isNotEmpty(pageReqVO.getNickname())) { + userIds = CollectionUtils.convertSet(memberUserApi.getUserListByNickname(pageReqVO.getNickname()), + MemberUserRespDTO::getId); + if (CollUtil.isEmpty(userIds)) { + return PageResult.empty(); + } + } + // 分页查询 + return couponMapper.selectPage(pageReqVO, userIds); + } + + @Override + public void useCoupon(Long id, Long userId, Long orderId) { + // 校验优惠劵 + validCoupon(id, userId); + // 更新状态 + int updateCount = couponMapper.updateByIdAndStatus(id, CouponStatusEnum.UNUSED.getStatus(), + new CouponDO().setStatus(CouponStatusEnum.USED.getStatus()) + .setUseOrderId(orderId).setUseTime(LocalDateTime.now())); + if (updateCount == 0) { + throw exception(COUPON_STATUS_NOT_UNUSED); + } + } + + @Override + @Transactional + public void deleteCoupon(Long id) { + // 校验存在 + validateCouponExists(id); + + // 更新优惠劵 + int deleteCount = couponMapper.delete(id, + asList(CouponStatusEnum.UNUSED.getStatus(), CouponStatusEnum.EXPIRE.getStatus())); + if (deleteCount == 0) { + throw exception(COUPON_DELETE_FAIL_USED); + } + // 减少优惠劵模板的领取数量 -1 + couponTemplateService.updateCouponTemplateTakeCount(id, -1); + } + + @Override + public List getCouponList(Long userId, Integer status) { + return couponMapper.selectListByUserIdAndStatus(userId, status); + } + + private void validateCouponExists(Long id) { + if (couponMapper.selectById(id) == null) { + throw exception(COUPON_NOT_EXISTS); + } + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateService.java new file mode 100644 index 0000000..6290d67 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateService.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.module.promotion.service.coupon; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponTemplateDO; + +import javax.validation.Valid; + +/** + * 优惠劵模板 Service 接口 + * + * @author 芋道源码 + */ +public interface CouponTemplateService { + + /** + * 创建优惠劵模板 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createCouponTemplate(@Valid CouponTemplateCreateReqVO createReqVO); + + /** + * 更新优惠劵模板 + * + * @param updateReqVO 更新信息 + */ + void updateCouponTemplate(@Valid CouponTemplateUpdateReqVO updateReqVO); + + /** + * 更新优惠劵模板的状态 + * + * @param id 编号 + * @param status 状态 + */ + void updateCouponTemplateStatus(Long id, Integer status); + + /** + * 删除优惠劵模板 + * + * @param id 编号 + */ + void deleteCouponTemplate(Long id); + + /** + * 获得优惠劵模板 + * + * @param id 编号 + * @return 优惠劵模板 + */ + CouponTemplateDO getCouponTemplate(Long id); + + /** + * 获得优惠劵模板分页 + * + * @param pageReqVO 分页查询 + * @return 优惠劵模板分页 + */ + PageResult getCouponTemplatePage(CouponTemplatePageReqVO pageReqVO); + + /** + * 更新优惠劵模板的领取数量 + * + * @param id 优惠劵模板编号 + * @param incrCount 增加数量 + */ + void updateCouponTemplateTakeCount(Long id, int incrCount); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateServiceImpl.java new file mode 100644 index 0000000..aa69470 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateServiceImpl.java @@ -0,0 +1,94 @@ +package com.yunxi.scm.module.promotion.service.coupon; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.coupon.CouponTemplateConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponTemplateDO; +import com.yunxi.scm.module.promotion.dal.mysql.coupon.CouponTemplateMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.*; + +/** + * 优惠劵模板 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class CouponTemplateServiceImpl implements CouponTemplateService { + + @Resource + private CouponTemplateMapper couponTemplateMapper; + + @Override + public Long createCouponTemplate(CouponTemplateCreateReqVO createReqVO) { + // 插入 + CouponTemplateDO couponTemplate = CouponTemplateConvert.INSTANCE.convert(createReqVO) + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + couponTemplateMapper.insert(couponTemplate); + // 返回 + return couponTemplate.getId(); + } + + @Override + public void updateCouponTemplate(CouponTemplateUpdateReqVO updateReqVO) { + // 校验存在 + CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId()); + // 校验发放数量不能过小 + if (updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) { + throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount()); + } + + // 更新 + CouponTemplateDO updateObj = CouponTemplateConvert.INSTANCE.convert(updateReqVO); + couponTemplateMapper.updateById(updateObj); + } + + @Override + public void updateCouponTemplateStatus(Long id, Integer status) { + // 校验存在 + validateCouponTemplateExists(id); + // 更新 + couponTemplateMapper.updateById(new CouponTemplateDO().setId(id).setStatus(status)); + } + + @Override + public void deleteCouponTemplate(Long id) { + // 校验存在 + validateCouponTemplateExists(id); + // 删除 + couponTemplateMapper.deleteById(id); + } + + private CouponTemplateDO validateCouponTemplateExists(Long id) { + CouponTemplateDO couponTemplate = couponTemplateMapper.selectById(id); + if (couponTemplate == null) { + throw exception(COUPON_TEMPLATE_NOT_EXISTS); + } + return couponTemplate; + } + + @Override + public CouponTemplateDO getCouponTemplate(Long id) { + return couponTemplateMapper.selectById(id); + } + + @Override + public PageResult getCouponTemplatePage(CouponTemplatePageReqVO pageReqVO) { + return couponTemplateMapper.selectPage(pageReqVO); + } + + @Override + public void updateCouponTemplateTakeCount(Long id, int incrCount) { + couponTemplateMapper.updateTakeCount(id, incrCount); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentService.java new file mode 100644 index 0000000..88dddc5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentService.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.promotion.service.decorate; + +import com.yunxi.scm.module.promotion.controller.admin.decorate.vo.DecorateComponentSaveReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.decorate.DecorateComponentDO; +import com.yunxi.scm.module.promotion.enums.decorate.DecoratePageEnum; + +import java.util.List; + +/** + * 装修组件 Service 接口 + * + * @author jason + */ +public interface DecorateComponentService { + + /** + * 店铺装修页面保存 + * + * @param reqVO 请求 VO + */ + void savePageComponents(DecorateComponentSaveReqVO reqVO); + + /** + * 根据页面 id。获取页面的组件信息 + * + * @param pageId 页面类型 {@link DecoratePageEnum#getId()} + */ + List getPageComponents(Integer pageId); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentServiceImpl.java new file mode 100644 index 0000000..72aac01 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentServiceImpl.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.promotion.service.decorate; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.module.promotion.controller.admin.decorate.vo.DecorateComponentSaveReqVO; +import com.yunxi.scm.module.promotion.controller.admin.decorate.vo.DecorateComponentSaveReqVO.ComponentReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.decorate.DecorateComponentDO; +import com.yunxi.scm.module.promotion.dal.mysql.decorate.DecorateComponentMapper; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.promotion.convert.decorate.DecorateComponentConvert.INSTANCE; + +/** + * 装修组件 Service 实现 + * + * @author jason + */ +@Service +public class DecorateComponentServiceImpl implements DecorateComponentService { + + @Resource + private DecorateComponentMapper decorateComponentMapper; + + @Override + public void savePageComponents(DecorateComponentSaveReqVO reqVO) { + // 1.新增或修改页面组件 + List oldList = decorateComponentMapper.selectByPage(reqVO.getPageId()); + + decorateComponentMapper.saveOrUpdateBatch(INSTANCE.convertList(reqVO.getPageId(), reqVO.getComponents())); + // 2.删除相关组件 + Set deleteIds = convertSet(oldList, DecorateComponentDO::getId); + deleteIds.removeAll(convertSet(reqVO.getComponents(), ComponentReqVO::getId, vo->Objects.nonNull(vo.getId()))); + if (CollUtil.isNotEmpty(deleteIds)) { + decorateComponentMapper.deleteBatchIds(deleteIds); + } + } + + @Override + public List getPageComponents(Integer pageId) { + return decorateComponentMapper.selectByPage(pageId); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityService.java new file mode 100644 index 0000000..9a7da66 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityService.java @@ -0,0 +1,84 @@ +package com.yunxi.scm.module.promotion.service.discount; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountProductDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 限时折扣 Service 接口 + * + * @author 芋道源码 + */ +public interface DiscountActivityService { + + /** + * 基于指定 SKU 编号数组,获得匹配的限时折扣商品 + * + * 注意,匹配的条件,仅仅是日期符合,并且处于开启状态 + * + * @param skuIds SKU 编号数组 + * @return 匹配的限时折扣商品 + */ + List getMatchDiscountProductList(Collection skuIds); + + /** + * 创建限时折扣活动 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDiscountActivity(@Valid DiscountActivityCreateReqVO createReqVO); + + /** + * 更新限时折扣活动 + * + * @param updateReqVO 更新信息 + */ + void updateDiscountActivity(@Valid DiscountActivityUpdateReqVO updateReqVO); + + /** + * 关闭限时折扣活动 + * + * @param id 编号 + */ + void closeRewardActivity(Long id); + + /** + * 删除限时折扣活动 + * + * @param id 编号 + */ + void deleteDiscountActivity(Long id); + + /** + * 获得限时折扣活动 + * + * @param id 编号 + * @return 限时折扣活动 + */ + DiscountActivityDO getDiscountActivity(Long id); + + /** + * 获得限时折扣活动分页 + * + * @param pageReqVO 分页查询 + * @return 限时折扣活动分页 + */ + PageResult getDiscountActivityPage(DiscountActivityPageReqVO pageReqVO); + + /** + * 获得活动编号,对应对应的商品列表 + * + * @param activityId 活动编号 + * @return 活动的商品列表 + */ + List getDiscountProductsByActivityId(Long activityId); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityServiceImpl.java new file mode 100644 index 0000000..eaf0260 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityServiceImpl.java @@ -0,0 +1,178 @@ +package com.yunxi.scm.module.promotion.service.discount; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.discount.DiscountActivityConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountProductDO; +import com.yunxi.scm.module.promotion.dal.mysql.discount.DiscountActivityMapper; +import com.yunxi.scm.module.promotion.dal.mysql.discount.DiscountProductMapper; +import com.yunxi.scm.module.promotion.enums.common.PromotionActivityStatusEnum; +import com.yunxi.scm.module.promotion.util.PromotionUtils; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.*; + +/** + * 限时折扣 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class DiscountActivityServiceImpl implements DiscountActivityService { + + @Resource + private DiscountActivityMapper discountActivityMapper; + @Resource + private DiscountProductMapper discountProductMapper; + + @Override + public List getMatchDiscountProductList(Collection skuIds) { + // TODO 芋艿:开启、满足 skuId、日期内 + return null; + } + + @Override + public Long createDiscountActivity(DiscountActivityCreateReqVO createReqVO) { + // 校验商品是否冲突 + validateDiscountActivityProductConflicts(null, createReqVO.getProducts()); + + // 插入活动 + DiscountActivityDO discountActivity = DiscountActivityConvert.INSTANCE.convert(createReqVO) + .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())); + discountActivityMapper.insert(discountActivity); + // 插入商品 + List discountProducts = convertList(createReqVO.getProducts(), + product -> DiscountActivityConvert.INSTANCE.convert(product).setActivityId(discountActivity.getId())); + discountProductMapper.insertBatch(discountProducts); + // 返回 + return discountActivity.getId(); + } + + @Override + public void updateDiscountActivity(DiscountActivityUpdateReqVO updateReqVO) { + // 校验存在 + DiscountActivityDO discountActivity = validateDiscountActivityExists(updateReqVO.getId()); + if (discountActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢 + throw exception(DISCOUNT_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED); + } + // 校验商品是否冲突 + validateDiscountActivityProductConflicts(updateReqVO.getId(), updateReqVO.getProducts()); + + // 更新活动 + DiscountActivityDO updateObj = DiscountActivityConvert.INSTANCE.convert(updateReqVO) + .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime())); + discountActivityMapper.updateById(updateObj); + // 更新商品 + updateDiscountProduct(updateReqVO); + } + + private void updateDiscountProduct(DiscountActivityUpdateReqVO updateReqVO) { + List dbDiscountProducts = discountProductMapper.selectListByActivityId(updateReqVO.getId()); + // 计算要删除的记录 + List deleteIds = convertList(dbDiscountProducts, DiscountProductDO::getId, + discountProductDO -> updateReqVO.getProducts().stream() + .noneMatch(product -> DiscountActivityConvert.INSTANCE.isEquals(discountProductDO, product))); + if (CollUtil.isNotEmpty(deleteIds)) { + discountProductMapper.deleteBatchIds(deleteIds); + } + // 计算新增的记录 + List newDiscountProducts = convertList(updateReqVO.getProducts(), + product -> DiscountActivityConvert.INSTANCE.convert(product).setActivityId(updateReqVO.getId())); + newDiscountProducts.removeIf(product -> dbDiscountProducts.stream().anyMatch( + dbProduct -> DiscountActivityConvert.INSTANCE.isEquals(dbProduct, product))); // 如果匹配到,说明是更新的 + if (CollectionUtil.isNotEmpty(newDiscountProducts)) { + discountProductMapper.insertBatch(newDiscountProducts); + } + } + + // TODO 芋艿:校验逻辑简化,只查询时间冲突的活动,开启状态的。 + /** + * 校验商品是否冲突 + * + * @param id 编号 + * @param products 商品列表 + */ + private void validateDiscountActivityProductConflicts(Long id, List products) { + if (CollUtil.isEmpty(products)) { + return; + } + // 查询商品参加的活动 + List discountActivityProductList = null; +// getRewardProductListBySkuIds( +// convertSet(products, DiscountActivityBaseVO.Product::getSkuId), +// asList(PromotionActivityStatusEnum.WAIT.getStatus(), PromotionActivityStatusEnum.RUN.getStatus())); + if (id != null) { // 排除自己这个活动 + discountActivityProductList.removeIf(product -> id.equals(product.getActivityId())); + } + // 如果非空,则说明冲突 + if (CollUtil.isNotEmpty(discountActivityProductList)) { + throw exception(DISCOUNT_ACTIVITY_SPU_CONFLICTS); + } + } + + @Override + public void closeRewardActivity(Long id) { + // 校验存在 + DiscountActivityDO dbDiscountActivity = validateDiscountActivityExists(id); + if (dbDiscountActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢 + throw exception(DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED); + } + if (dbDiscountActivity.getStatus().equals(PromotionActivityStatusEnum.END.getStatus())) { // 已关闭的活动,不能关闭噢 + throw exception(DISCOUNT_ACTIVITY_CLOSE_FAIL_STATUS_END); + } + + // 更新为关闭。 + DiscountActivityDO updateObj = new DiscountActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); + discountActivityMapper.updateById(updateObj); + } + + @Override + public void deleteDiscountActivity(Long id) { + // 校验存在 + DiscountActivityDO discountActivity = validateDiscountActivityExists(id); + if (!discountActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动,不能删除噢 + throw exception(DISCOUNT_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED); + } + + // 删除 + discountActivityMapper.deleteById(id); + } + + private DiscountActivityDO validateDiscountActivityExists(Long id) { + DiscountActivityDO discountActivity = discountActivityMapper.selectById(id); + if (discountActivity == null) { + throw exception(DISCOUNT_ACTIVITY_NOT_EXISTS); + } + return discountActivity; + } + + @Override + public DiscountActivityDO getDiscountActivity(Long id) { + return discountActivityMapper.selectById(id); + } + + @Override + public PageResult getDiscountActivityPage(DiscountActivityPageReqVO pageReqVO) { + return discountActivityMapper.selectPage(pageReqVO); + } + + @Override + public List getDiscountProductsByActivityId(Long activityId) { + return discountProductMapper.selectListByActivityId(activityId); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/price/PriceService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/price/PriceService.java new file mode 100644 index 0000000..6ecaa97 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/price/PriceService.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.promotion.service.price; + +import com.yunxi.scm.module.promotion.api.price.dto.CouponMeetRespDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateReqDTO; + +import java.util.List; + +/** + * 价格计算 Service 接口 + * + * @author 芋道源码 + */ +public interface PriceService { + + /** + * 获得优惠劵的匹配信息列表 + * + * @param calculateReqDTO 价格请求 + * @return 价格响应 + */ + List getMeetCouponList(PriceCalculateReqDTO calculateReqDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/price/PriceServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/price/PriceServiceImpl.java new file mode 100644 index 0000000..3cfe9c6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/price/PriceServiceImpl.java @@ -0,0 +1,96 @@ +package com.yunxi.scm.module.promotion.service.price; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.promotion.api.price.dto.CouponMeetRespDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateReqDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateRespDTO; +import com.yunxi.scm.module.promotion.convert.price.PriceConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; +import com.yunxi.scm.module.promotion.enums.coupon.CouponStatusEnum; +import com.yunxi.scm.module.promotion.service.coupon.CouponService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.getSumValue; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.COUPON_VALID_TIME_NOT_NOW; + +/** + * 价格计算 Service 实现类 + * + * 优惠计算顺序:min(限时折扣, 会员折扣) > 满减送 > 优惠券。 + * 参考文档: + * 1. 有赞文档:限时折扣、满减送、优惠券哪个优先计算? + * + * TODO 芋艿:进一步完善 + * 1. 限时折扣:指定金额、减免金额、折扣 + * 2. 满减送:循环、折扣 + * 3. 优惠劵:待定 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class PriceServiceImpl implements PriceService { + + @Resource + private CouponService couponService; + + @Override + public List getMeetCouponList(PriceCalculateReqDTO calculateReqDTO) { + // 先计算一轮价格 +// PriceCalculateRespDTO priceCalculate = calculatePrice(calculateReqDTO); + PriceCalculateRespDTO priceCalculate = null; + + // 获得用户的待使用优惠劵 + List couponList = couponService.getCouponList(calculateReqDTO.getUserId(), CouponStatusEnum.UNUSED.getStatus()); + if (CollUtil.isEmpty(couponList)) { + return Collections.emptyList(); + } + + // 获得优惠劵的匹配信息 + return CollectionUtils.convertList(couponList, coupon -> { + CouponMeetRespDTO couponMeetRespDTO = PriceConvert.INSTANCE.convert(coupon); + try { + // 校验优惠劵 + couponService.validCoupon(coupon); + + // 获得匹配的商品 SKU 数组 + // TODO 芋艿:后续处理 +// List orderItems = getMatchCouponOrderItems(priceCalculate, coupon); + List orderItems = null; + if (CollUtil.isEmpty(orderItems)) { + return couponMeetRespDTO.setMeet(false).setMeetTip("所结算商品没有符合条件的商品"); + } + + // 计算是否满足优惠劵的使用金额 + Integer originPrice = getSumValue(orderItems, PriceCalculateRespDTO.OrderItem::getOrderDividePrice, Integer::sum); + assert originPrice != null; + if (originPrice < coupon.getUsePrice()) { + return couponMeetRespDTO.setMeet(false) +// .setMeetTip(String.format("差 %s 元可用优惠劵", formatPrice(coupon.getUsePrice() - originPrice))); + .setMeetTip("所结算的商品中未满足使用的金额"); + } + } catch (ServiceException serviceException) { + couponMeetRespDTO.setMeet(false); + if (serviceException.getCode().equals(COUPON_VALID_TIME_NOT_NOW.getCode())) { + couponMeetRespDTO.setMeetTip("优惠劵未到使用时间"); + } else { + log.error("[getMeetCouponList][calculateReqDTO({}) 获得优惠劵匹配信息异常]", calculateReqDTO, serviceException); + couponMeetRespDTO.setMeetTip("优惠劵不满足使用条件"); + } + return couponMeetRespDTO; + } + // 满足 + return couponMeetRespDTO.setMeet(true); + }); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityService.java new file mode 100644 index 0000000..d41bb24 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityService.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.promotion.service.reward; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.reward.RewardActivityDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 满减送活动 Service 接口 + * + * @author 芋道源码 + */ +public interface RewardActivityService { + + /** + * 创建满减送活动 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createRewardActivity(@Valid RewardActivityCreateReqVO createReqVO); + + /** + * 更新满减送活动 + * + * @param updateReqVO 更新信息 + */ + void updateRewardActivity(@Valid RewardActivityUpdateReqVO updateReqVO); + + /** + * 关闭满减送活动 + * + * @param id 活动编号 + */ + void closeRewardActivity(Long id); + + /** + * 删除满减送活动 + * + * @param id 编号 + */ + void deleteRewardActivity(Long id); + + /** + * 获得满减送活动 + * + * @param id 编号 + * @return 满减送活动 + */ + RewardActivityDO getRewardActivity(Long id); + + /** + * 获得满减送活动分页 + * + * @param pageReqVO 分页查询 + * @return 满减送活动分页 + */ + PageResult getRewardActivityPage(RewardActivityPageReqVO pageReqVO); + + /** + * 基于指定的 SPU 编号数组,获得它们匹配的满减送活动 + * + * @param spuIds SPU 编号数组 + * @return 满减送活动列表 + */ + List getMatchRewardActivityList(Collection spuIds); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityServiceImpl.java new file mode 100644 index 0000000..e6b49ae --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityServiceImpl.java @@ -0,0 +1,166 @@ +package com.yunxi.scm.module.promotion.service.reward; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.reward.RewardActivityConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.reward.RewardActivityDO; +import com.yunxi.scm.module.promotion.dal.mysql.reward.RewardActivityMapper; +import com.yunxi.scm.module.promotion.enums.common.PromotionActivityStatusEnum; +import com.yunxi.scm.module.promotion.util.PromotionUtils; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.*; +import static java.util.Arrays.asList; + +/** + * 满减送活动 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class RewardActivityServiceImpl implements RewardActivityService { + + @Resource + private RewardActivityMapper rewardActivityMapper; + + @Override + public Long createRewardActivity(RewardActivityCreateReqVO createReqVO) { + // 校验商品是否冲突 + validateRewardActivitySpuConflicts(null, createReqVO.getProductSpuIds()); + + // 插入 + RewardActivityDO rewardActivity = RewardActivityConvert.INSTANCE.convert(createReqVO) + .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())); + rewardActivityMapper.insert(rewardActivity); + // 返回 + return rewardActivity.getId(); + } + + @Override + public void updateRewardActivity(RewardActivityUpdateReqVO updateReqVO) { + // 校验存在 + RewardActivityDO dbRewardActivity = validateRewardActivityExists(updateReqVO.getId()); + if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能修改噢 + throw exception(REWARD_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED); + } + // 校验商品是否冲突 + validateRewardActivitySpuConflicts(updateReqVO.getId(), updateReqVO.getProductSpuIds()); + + // 更新 + RewardActivityDO updateObj = RewardActivityConvert.INSTANCE.convert(updateReqVO) + .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime())); + rewardActivityMapper.updateById(updateObj); + } + + @Override + public void closeRewardActivity(Long id) { + // 校验存在 + RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); + if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 已关闭的活动,不能关闭噢 + throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED); + } + if (dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.END.getStatus())) { // 已关闭的活动,不能关闭噢 + throw exception(REWARD_ACTIVITY_CLOSE_FAIL_STATUS_END); + } + + // 更新 + RewardActivityDO updateObj = new RewardActivityDO().setId(id).setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); + rewardActivityMapper.updateById(updateObj); + } + + @Override + public void deleteRewardActivity(Long id) { + // 校验存在 + RewardActivityDO dbRewardActivity = validateRewardActivityExists(id); + if (!dbRewardActivity.getStatus().equals(PromotionActivityStatusEnum.CLOSE.getStatus())) { // 未关闭的活动,不能删除噢 + throw exception(REWARD_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED); + } + + // 删除 + rewardActivityMapper.deleteById(id); + } + + private RewardActivityDO validateRewardActivityExists(Long id) { + RewardActivityDO activity = rewardActivityMapper.selectById(id); + if (activity == null) { + throw exception(REWARD_ACTIVITY_NOT_EXISTS); + } + return activity; + } + + // TODO @芋艿:逻辑有问题,需要优化;要分成全场、和指定来校验; + /** + * 校验商品参加的活动是否冲突 + * + * @param id 活动编号 + * @param spuIds 商品 SPU 编号数组 + */ + private void validateRewardActivitySpuConflicts(Long id, Collection spuIds) { + if (CollUtil.isEmpty(spuIds)) { + return; + } + // 查询商品参加的活动 + List rewardActivityList = getRewardActivityListBySpuIds(spuIds, + asList(PromotionActivityStatusEnum.WAIT.getStatus(), PromotionActivityStatusEnum.RUN.getStatus())); + if (id != null) { // 排除自己这个活动 + rewardActivityList.removeIf(activity -> id.equals(activity.getId())); + } + // 如果非空,则说明冲突 + if (CollUtil.isNotEmpty(rewardActivityList)) { + throw exception(REWARD_ACTIVITY_SPU_CONFLICTS); + } + } + + /** + * 获得商品参加的满减送活动的数组 + * + * @param spuIds 商品 SPU 编号数组 + * @param statuses 活动状态数组 + * @return 商品参加的满减送活动的数组 + */ + private List getRewardActivityListBySpuIds(Collection spuIds, + Collection statuses) { + List list = rewardActivityMapper.selectListByStatus(statuses); + return CollUtil.filter(list, activity -> CollUtil.containsAny(activity.getProductSpuIds(), spuIds)); + } + + @Override + public RewardActivityDO getRewardActivity(Long id) { + return rewardActivityMapper.selectById(id); + } + + @Override + public PageResult getRewardActivityPage(RewardActivityPageReqVO pageReqVO) { + return rewardActivityMapper.selectPage(pageReqVO); + } + + @Override + public List getMatchRewardActivityList(Collection spuIds) { + // TODO 芋艿:待实现;先指定,然后再全局的; +// // 如果有全局活动,则直接选择它 +// List allActivities = rewardActivityMapper.selectListByProductScopeAndStatus( +// PromotionProductScopeEnum.ALL.getScope(), PromotionActivityStatusEnum.RUN.getStatus()); +// if (CollUtil.isNotEmpty(allActivities)) { +// return MapUtil.builder(allActivities.get(0), spuIds).build(); +// } +// +// // 查询某个活动参加的活动 +// List productActivityList = getRewardActivityListBySpuIds(spuIds, +// singleton(PromotionActivityStatusEnum.RUN.getStatus())); +// return convertMap(productActivityList, activity -> activity, +// rewardActivityDO -> intersectionDistinct(rewardActivityDO.getProductSpuIds(), spuIds)); // 求交集返回 + return null; + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillactivity/SeckillActivityService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillactivity/SeckillActivityService.java new file mode 100644 index 0000000..17c1caf --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillactivity/SeckillActivityService.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.promotion.service.seckill.seckillactivity; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillProductDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 秒杀活动 Service 接口 + * + * @author halfninety + */ +public interface SeckillActivityService { + + /** + * 创建秒杀活动 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSeckillActivity(@Valid SeckillActivityCreateReqVO createReqVO); + + /** + * 更新秒杀活动 + * + * @param updateReqVO 更新信息 + */ + void updateSeckillActivity(@Valid SeckillActivityUpdateReqVO updateReqVO); + + /** + * 关闭秒杀活动 + * + * @param id 编号 + */ + void closeSeckillActivity(Long id); + + /** + * 删除秒杀活动 + * + * @param id 编号 + */ + void deleteSeckillActivity(Long id); + + /** + * 获得秒杀活动 + * + * @param id 编号 + * @return 秒杀活动 + */ + SeckillActivityDO getSeckillActivity(Long id); + + /** + * 获得秒杀活动列表 + * + * @param ids 编号 + * @return 秒杀活动列表 + */ + List getSeckillActivityList(Collection ids); + + /** + * 获得秒杀活动分页 + * + * @param pageReqVO 分页查询 + * @return 秒杀活动分页 + */ + PageResult getSeckillActivityPage(SeckillActivityPageReqVO pageReqVO); + + /** + * 通过活动编号获取活动商品 + * + * @param id 活动编号 + * @return 活动商品列表 + */ + List getSeckillProductListByActivityId(Long id); + + /** + * 通过活动编号获取活动商品 + * + * @param ids 活动编号 + * @return 活动商品列表 + */ + List getSeckillProductListByActivityId(Collection ids); + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillactivity/SeckillActivityServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillactivity/SeckillActivityServiceImpl.java new file mode 100644 index 0000000..d019c06 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillactivity/SeckillActivityServiceImpl.java @@ -0,0 +1,238 @@ +package com.yunxi.scm.module.promotion.service.seckill.seckillactivity; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.product.api.sku.ProductSkuApi; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.spu.ProductSpuApi; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.product.SeckillProductUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.seckill.seckillactivity.SeckillActivityConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillProductDO; +import com.yunxi.scm.module.promotion.dal.mysql.seckill.seckillactivity.SeckillActivityMapper; +import com.yunxi.scm.module.promotion.dal.mysql.seckill.seckillactivity.SeckillProductMapper; +import com.yunxi.scm.module.promotion.service.seckill.seckillconfig.SeckillConfigService; +import com.yunxi.scm.module.promotion.util.PromotionUtils; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SPU_NOT_EXISTS; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.*; +import static com.yunxi.scm.module.promotion.util.PromotionUtils.validateProductSkuExistence; + +/** + * 秒杀活动 Service 实现类 + * + * @author halfninety + */ +@Service +@Validated +public class SeckillActivityServiceImpl implements SeckillActivityService { + + @Resource + private SeckillActivityMapper seckillActivityMapper; + @Resource + private SeckillProductMapper seckillProductMapper; + @Resource + private SeckillConfigService seckillConfigService; + @Resource + private ProductSpuApi productSpuApi; + @Resource + private ProductSkuApi productSkuApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createSeckillActivity(SeckillActivityCreateReqVO createReqVO) { + // 校验商品秒秒杀时段是否冲突 + validateProductSpuSeckillConflict(createReqVO.getConfigIds(), createReqVO.getSpuId(), null); + // 获取所选 spu 下的所有 sku + List skus = productSkuApi.getSkuListBySpuId(CollUtil.newArrayList(createReqVO.getSpuId())); + // 校验商品 sku 是否存在 + // TODO @puhui999:直接校验 sku 数量,是不是就完事啦,不需要校验的特别严谨哈; + validateProductSkuExistence(skus, createReqVO.getProducts(), SeckillProductCreateReqVO::getSkuId); + + // 插入秒杀活动 + SeckillActivityDO activity = SeckillActivityConvert.INSTANCE.convert(createReqVO) + .setStatus(PromotionUtils.calculateActivityStatus(createReqVO.getEndTime())) + .setTotalStock(CollectionUtils.getSumValue(createReqVO.getProducts(), SeckillProductCreateReqVO::getStock, Integer::sum)); + seckillActivityMapper.insert(activity); + // 插入商品 + // TODO @puhui999:products 要注意复数哈 + List product = SeckillActivityConvert.INSTANCE.convertList(activity, createReqVO.getProducts()); + seckillProductMapper.insertBatch(product); + return activity.getId(); + } + + private void validateProductSpuSeckillConflict(List configIds, Long spuId, Long activityId) { + // 校验秒杀时段是否存在 + seckillConfigService.validateSeckillConfigExists(configIds); + // 校验商品 spu 是否存在 + List spuList = productSpuApi.getSpuList(CollUtil.newArrayList(spuId)); + if (CollUtil.isEmpty(spuList)) { + throw exception(SPU_NOT_EXISTS); + } + // 查询所有开启的秒杀活动 + List activityDOs = seckillActivityMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + if (activityId != null) { + // 更新时移除本活动 + activityDOs.removeIf(item -> ObjectUtil.equal(item.getId(), activityId)); + } + // 过滤出所有 spuId 有交集的活动 + List activityDOs1 = CollectionUtils.convertList(activityDOs, c -> c, s -> ObjectUtil.equal(s.getSpuId(), spuId)); + if (CollUtil.isNotEmpty(activityDOs1)) { + throw exception(SECKILL_ACTIVITY_SPU_CONFLICTS); + } + List activityDOs2 = CollectionUtils.convertList(activityDOs, c -> c, s -> { + // 判断秒杀时段是否有交集 + // TODO @puhui999:是不是 containsAny 就是有交集呀 + List configIdsClone = CollUtil.newArrayList(s.getConfigIds()); + configIdsClone.retainAll(configIds); + return CollUtil.isNotEmpty(configIdsClone); + }); + if (CollUtil.isNotEmpty(activityDOs2)) { + throw exception(SECKILL_TIME_CONFLICTS); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateSeckillActivity(SeckillActivityUpdateReqVO updateReqVO) { + // 校验存在 + SeckillActivityDO seckillActivity = validateSeckillActivityExists(updateReqVO.getId()); + if (CommonStatusEnum.DISABLE.getStatus().equals(seckillActivity.getStatus())) { + throw exception(SECKILL_ACTIVITY_UPDATE_FAIL_STATUS_CLOSED); + } + // 校验商品是否冲突 + validateProductSpuSeckillConflict(updateReqVO.getConfigIds(), updateReqVO.getSpuId(), updateReqVO.getId()); + // 获取所选 spu下的所有 sku + List skus = productSkuApi.getSkuListBySpuId(CollUtil.newArrayList(updateReqVO.getSpuId())); + // 校验商品 sku 是否存在 + validateProductSkuExistence(skus, updateReqVO.getProducts(), SeckillProductUpdateReqVO::getSkuId); + + // 更新活动 + SeckillActivityDO updateObj = SeckillActivityConvert.INSTANCE.convert(updateReqVO) + .setStatus(PromotionUtils.calculateActivityStatus(updateReqVO.getEndTime())) + .setTotalStock(CollectionUtils.getSumValue(updateReqVO.getProducts(), SeckillProductUpdateReqVO::getStock, Integer::sum)); + seckillActivityMapper.updateById(updateObj); + // 更新商品 + updateSeckillProduct(updateObj, updateReqVO.getProducts()); + } + + + /** + * 更新秒杀商品 + * + * @param updateObj 更新的活动 + * @param products 商品配置 + */ + // TODO @puhui999:我在想,我们是不是可以封装一个 CollUtil 的方法,传入两个数组,判断出哪些是新增、哪些是修改、哪些是删除; + // 例如说,products 先转化成 SeckillProductDO;然后,基于一个 func(key1, key2) 做比对; + // 如果可以跑通,所有涉及到这种逻辑的,都可以服用哈。 + private void updateSeckillProduct(SeckillActivityDO updateObj, List products) { + // 数据库中的活动商品 + List seckillProductDOs = seckillProductMapper.selectListByActivityId(updateObj.getId()); + Set dbSkuIds = CollectionUtils.convertSet(seckillProductDOs, SeckillProductDO::getSkuId); + // 1. 删除后台存在的前端不存在的商品 + // TODO @puhui999:delete 应该是 id,不是 skuId 哈 + Set voSkuIds = CollectionUtils.convertSet(products, SeckillProductUpdateReqVO::getSkuId); + List d = CollectionUtils.filterList(dbSkuIds, item -> !voSkuIds.contains(item)); + if (CollUtil.isNotEmpty(d)) { + seckillProductMapper.deleteBatchIds(d); + } + // 2. 前端存在的后端不存在的商品 + List c = CollectionUtils.filterList(voSkuIds, item -> !dbSkuIds.contains(item)); + if (CollUtil.isNotEmpty(c)) { + List vos = CollectionUtils.filterList(products, item -> c.contains(item.getSkuId())); + List productDOs = SeckillActivityConvert.INSTANCE.convertList(updateObj, vos); + seckillProductMapper.insertBatch(productDOs); + } + // 3. 更新已存在的商品 + List u = CollectionUtils.filterList(voSkuIds, dbSkuIds::contains); + if (CollUtil.isNotEmpty(u)) { + List vos = CollectionUtils.filterList(products, item -> u.contains(item.getSkuId())); + List productDOs = SeckillActivityConvert.INSTANCE.convertList1(updateObj, vos, seckillProductDOs); + seckillProductMapper.updateBatch(productDOs); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) // TODO @puhui999:这个不用加事务哈 + public void closeSeckillActivity(Long id) { + // TODO 待验证没使用过 + // 校验存在 + SeckillActivityDO activity = validateSeckillActivityExists(id); + if (CommonStatusEnum.DISABLE.getStatus().equals(activity.getStatus())) { + throw exception(SECKILL_ACTIVITY_CLOSE_FAIL_STATUS_CLOSED); + } + + // 更新 + SeckillActivityDO updateObj = new SeckillActivityDO().setId(id).setStatus(CommonStatusEnum.DISABLE.getStatus()); + seckillActivityMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteSeckillActivity(Long id) { + // 校验存在 + SeckillActivityDO seckillActivity = this.validateSeckillActivityExists(id); + if (CommonStatusEnum.ENABLE.getStatus().equals(seckillActivity.getStatus())) { + throw exception(SECKILL_ACTIVITY_DELETE_FAIL_STATUS_NOT_CLOSED_OR_END); + } + + // 删除活动 + seckillActivityMapper.deleteById(id); + // 删除活动商品 + List productDOs = seckillProductMapper.selectListByActivityId(id); + Set convertSet = CollectionUtils.convertSet(productDOs, SeckillProductDO::getSkuId); + seckillProductMapper.deleteBatchIds(convertSet); + } + + private SeckillActivityDO validateSeckillActivityExists(Long id) { + SeckillActivityDO seckillActivity = seckillActivityMapper.selectById(id); + if (seckillActivity == null) { + throw exception(SECKILL_ACTIVITY_NOT_EXISTS); + } + return seckillActivity; + } + + @Override + public SeckillActivityDO getSeckillActivity(Long id) { + return validateSeckillActivityExists(id); + } + + @Override + public List getSeckillActivityList(Collection ids) { + return seckillActivityMapper.selectBatchIds(ids); + } + + @Override + public PageResult getSeckillActivityPage(SeckillActivityPageReqVO pageReqVO) { + return seckillActivityMapper.selectPage(pageReqVO); + } + + @Override + public List getSeckillProductListByActivityId(Long id) { + return seckillProductMapper.selectListByActivityId(id); + } + + @Override + public List getSeckillProductListByActivityId(Collection ids) { + return seckillProductMapper.selectListByActivityId(ids); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillconfig/SeckillConfigService.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillconfig/SeckillConfigService.java new file mode 100644 index 0000000..ec147c0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillconfig/SeckillConfigService.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.module.promotion.service.seckill.seckillconfig; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillconfig.SeckillConfigDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 秒杀时段 Service 接口 + * + * @author halfninety + */ +public interface SeckillConfigService { + + /** + * 创建秒杀时段 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSeckillConfig(@Valid SeckillConfigCreateReqVO createReqVO); + + /** + * 更新秒杀时段 + * + * @param updateReqVO 更新信息 + */ + void updateSeckillConfig(@Valid SeckillConfigUpdateReqVO updateReqVO); + + /** + * 删除秒杀时段 + * + * @param id 编号 + */ + void deleteSeckillConfig(Long id); + + /** + * 获得秒杀时段 + * + * @param id 编号 + * @return 秒杀时段 + */ + SeckillConfigDO getSeckillConfig(Long id); + + /** + * 获得所有秒杀时段列表 + * + * @return 所有秒杀时段列表 + */ + List getSeckillConfigList(); + + /** + * 校验秒杀时段是否存在 + * + * @param timeIds 秒杀时段id集合 + */ + void validateSeckillConfigExists(Collection timeIds); + + + /** + * 获得秒杀时间段配置分页数据 + * + * @param pageVO 分页请求参数 + * @return 秒杀时段分页列表 + */ + PageResult getSeckillConfigPage(SeckillConfigPageReqVO pageVO); + + /** + * 获得所有正常状态的时段配置列表 + * + * @return 秒杀时段列表 + */ + List getListAllSimple(); + + /** + * 更新秒杀时段配置状态 + * + * @param id id + * @param status 状态 + */ + void updateSeckillConfigStatus(Long id, Integer status); +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillconfig/SeckillConfigServiceImpl.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillconfig/SeckillConfigServiceImpl.java new file mode 100644 index 0000000..0dad942 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/service/seckill/seckillconfig/SeckillConfigServiceImpl.java @@ -0,0 +1,160 @@ +package com.yunxi.scm.module.promotion.service.seckill.seckillconfig; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigUpdateReqVO; +import com.yunxi.scm.module.promotion.convert.seckill.seckillconfig.SeckillConfigConvert; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillconfig.SeckillConfigDO; +import com.yunxi.scm.module.promotion.dal.mysql.seckill.seckillconfig.SeckillConfigMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalTime; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.*; + +/** + * 秒杀时段 Service 实现类 + * + * @author halfninety + */ +@Service +@Validated +public class SeckillConfigServiceImpl implements SeckillConfigService { + + @Resource + private SeckillConfigMapper seckillConfigMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createSeckillConfig(SeckillConfigCreateReqVO createReqVO) { + // 校验时间段是否冲突 + validateSeckillConfigConflict(createReqVO.getStartTime(), createReqVO.getEndTime(), null); + + // 插入 + SeckillConfigDO seckillConfig = SeckillConfigConvert.INSTANCE.convert(createReqVO); + seckillConfigMapper.insert(seckillConfig); + // 返回 + return seckillConfig.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateSeckillConfig(SeckillConfigUpdateReqVO updateReqVO) { + // 校验存在 + validateSeckillConfigExists(updateReqVO.getId()); + // 校验时间段是否冲突 + validateSeckillConfigConflict(updateReqVO.getStartTime(), updateReqVO.getEndTime(), updateReqVO.getId()); + + // 更新 + SeckillConfigDO updateObj = SeckillConfigConvert.INSTANCE.convert(updateReqVO); + seckillConfigMapper.updateById(updateObj); + } + + // TODO @puhui999: 这个要不合并到更新操作里? 不单独有个操作咧; 更新状态不用那么多必须的参数,更新的时候需要校验时间段 + @Override + public void updateSeckillConfigStatus(Long id, Integer status) { + // 校验秒杀时段是否存在 + validateSeckillConfigExists(id); + + // 更新状态 + seckillConfigMapper.updateById(new SeckillConfigDO().setId(id).setStatus(status)); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteSeckillConfig(Long id) { + // 校验存在 + validateSeckillConfigExists(id); + + // 删除 + seckillConfigMapper.deleteById(id); + } + + private void validateSeckillConfigExists(Long id) { + if (seckillConfigMapper.selectById(id) == null) { + throw exception(SECKILL_TIME_NOT_EXISTS); + } + } + + /** + * 校验时间是否存在冲突 + * + * @param startTime 开始时间 + * @param endTime 结束时间 + */ + private void validateSeckillConfigConflict(String startTime, String endTime, Long seckillConfigId) { + LocalTime startTime1 = LocalTime.parse(startTime); + LocalTime endTime1 = LocalTime.parse(endTime); + // 查询出所有的时段配置 + List configDOs = seckillConfigMapper.selectList(); + // 更新时排除自己 + if (seckillConfigId != null) { + configDOs.removeIf(item -> ObjectUtil.equal(item.getId(), seckillConfigId)); + } + // 过滤出重叠的时段 ids + boolean hasConflict = configDOs.stream().anyMatch(config -> { + LocalTime startTime2 = LocalTime.parse(config.getStartTime()); + LocalTime endTime2 = LocalTime.parse(config.getEndTime()); + // 判断时间是否重叠 + return LocalDateTimeUtils.checkTimeOverlap(startTime1, endTime1, startTime2, endTime2); + }); + + if (hasConflict) { + throw exception(SECKILL_TIME_CONFLICTS); + } + } + + + @Override + public SeckillConfigDO getSeckillConfig(Long id) { + return seckillConfigMapper.selectById(id); + } + + @Override + public List getSeckillConfigList() { + return seckillConfigMapper.selectList(); + } + + @Override + public void validateSeckillConfigExists(Collection configIds) { + if (CollUtil.isEmpty(configIds)) { + throw exception(SECKILL_TIME_NOT_EXISTS); + } + List configDOs = seckillConfigMapper.selectBatchIds(configIds); + if (CollUtil.isEmpty(configDOs)) { + throw exception(SECKILL_TIME_NOT_EXISTS); + } + // 过滤出关闭的时段 + List filterList = CollectionUtils.filterList(configDOs, item -> ObjectUtil.equal(item.getStatus(), CommonStatusEnum.DISABLE.getStatus())); + if (CollUtil.isNotEmpty(filterList)) { + throw exception(SECKILL_TIME_DISABLE); + } + if (configDOs.size() != configIds.size()) { + throw exception(SECKILL_TIME_NOT_EXISTS); + } + } + + @Override + public PageResult getSeckillConfigPage(SeckillConfigPageReqVO pageVO) { + return seckillConfigMapper.selectPage(pageVO); + } + + // TODO @puhui999:改成传入 enable 状态哈。一个通用的 getSeckillConfigList 方法 + @Override + public List getListAllSimple() { + return seckillConfigMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/util/PromotionUtils.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/util/PromotionUtils.java new file mode 100644 index 0000000..0a56d81 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/java/com/yunxi/scm/module/promotion/util/PromotionUtils.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.promotion.util; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS; + +/** + * 活动工具类 + * + * @author 芋道源码 + */ +public class PromotionUtils { + + /** + * 根据时间,计算活动状态 + * + * @param endTime 结束时间 + * @return 活动状态 + */ + public static Integer calculateActivityStatus(LocalDateTime endTime) { + return LocalDateTimeUtils.beforeNow(endTime) ? CommonStatusEnum.DISABLE.getStatus() : CommonStatusEnum.ENABLE.getStatus(); + } + + // TODO @puhui999:写个注释哈。 + public static void validateProductSkuExistence(List skus, List products, Function func) { + // 校验 sku 个数是否一致 + Set skuIdsSet = CollectionUtils.convertSet(products, func); + Set skuIdsSet1 = CollectionUtils.convertSet(skus, ProductSkuRespDTO::getId); + // 校验 skuId 是否存在 + List f = CollectionUtils.filterList(skuIdsSet, s -> !skuIdsSet1.contains(s)); + if (CollUtil.isNotEmpty(f)) { + throw exception(SKU_NOT_EXISTS); + } + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/main/resources/mapper/coupon/CouponTemplateMapper.xml b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/resources/mapper/coupon/CouponTemplateMapper.xml new file mode 100644 index 0000000..29bd3dd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/main/resources/mapper/coupon/CouponTemplateMapper.xml @@ -0,0 +1,11 @@ + + + + + + UPDATE promotion_coupon_template + SET take_count = take_count + #{incrCount} + WHERE id = #{id} + + + diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityServiceImplTest.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityServiceImplTest.java new file mode 100644 index 0000000..05f1800 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/combination/CombinationActivityServiceImplTest.java @@ -0,0 +1,252 @@ +package com.yunxi.scm.module.promotion.service.combination; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityExportReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.combination.vo.activity.CombinationActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.combination.combinationactivity.CombinationActivityDO; +import com.yunxi.scm.module.promotion.dal.mysql.combination.combinationactivity.CombinationActivityMapper; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.COMBINATION_ACTIVITY_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +// TODO 芋艿:等完成后,在补全单测 +/** + * {@link CombinationActivityServiceImpl} 的单元测试类 + * + * @author HUIHUI + */ +@Import(CombinationActivityServiceImpl.class) +public class CombinationActivityServiceImplTest extends BaseDbUnitTest { + + @Resource + private CombinationActivityServiceImpl combinationActivityService; + + @Resource + private CombinationActivityMapper combinationActivityMapper; + + @Test + public void testCreateCombinationActivity_success() { + // 准备参数 + CombinationActivityCreateReqVO reqVO = randomPojo(CombinationActivityCreateReqVO.class); + + // 调用 + Long combinationActivityId = combinationActivityService.createCombinationActivity(reqVO); + // 断言 + assertNotNull(combinationActivityId); + // 校验记录的属性是否正确 + CombinationActivityDO combinationActivity = combinationActivityMapper.selectById(combinationActivityId); + assertPojoEquals(reqVO, combinationActivity); + } + + @Test + public void testUpdateCombinationActivity_success() { + // mock 数据 + CombinationActivityDO dbCombinationActivity = randomPojo(CombinationActivityDO.class); + combinationActivityMapper.insert(dbCombinationActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + CombinationActivityUpdateReqVO reqVO = randomPojo(CombinationActivityUpdateReqVO.class, o -> { + o.setId(dbCombinationActivity.getId()); // 设置更新的 ID + }); + + // 调用 + combinationActivityService.updateCombinationActivity(reqVO); + // 校验是否更新正确 + CombinationActivityDO combinationActivity = combinationActivityMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, combinationActivity); + } + + @Test + public void testUpdateCombinationActivity_notExists() { + // 准备参数 + CombinationActivityUpdateReqVO reqVO = randomPojo(CombinationActivityUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> combinationActivityService.updateCombinationActivity(reqVO), COMBINATION_ACTIVITY_NOT_EXISTS); + } + + @Test + public void testDeleteCombinationActivity_success() { + // mock 数据 + CombinationActivityDO dbCombinationActivity = randomPojo(CombinationActivityDO.class); + combinationActivityMapper.insert(dbCombinationActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbCombinationActivity.getId(); + + // 调用 + combinationActivityService.deleteCombinationActivity(id); + // 校验数据不存在了 + assertNull(combinationActivityMapper.selectById(id)); + } + + @Test + public void testDeleteCombinationActivity_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> combinationActivityService.deleteCombinationActivity(id), COMBINATION_ACTIVITY_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetCombinationActivityPage() { + // mock 数据 + CombinationActivityDO dbCombinationActivity = randomPojo(CombinationActivityDO.class, o -> { // 等会查询到 + o.setName(null); + //o.setSpuId(null); + o.setTotalLimitCount(null); + o.setSingleLimitCount(null); + o.setStartTime(null); + o.setEndTime(null); + o.setUserSize(null); + o.setTotalNum(null); + o.setSuccessNum(null); + o.setOrderUserCount(null); + o.setVirtualGroup(null); + o.setStatus(null); + o.setLimitDuration(null); + o.setCreateTime(null); + }); + combinationActivityMapper.insert(dbCombinationActivity); + // 测试 name 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setName(null))); + // 测试 spuId 不匹配 + //combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setSpuId(null))); + // 测试 totalLimitCount 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setTotalLimitCount(null))); + // 测试 singleLimitCount 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setSingleLimitCount(null))); + // 测试 startTime 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setStartTime(null))); + // 测试 endTime 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setEndTime(null))); + // 测试 userSize 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setUserSize(null))); + // 测试 totalNum 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setTotalNum(null))); + // 测试 successNum 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setSuccessNum(null))); + // 测试 orderUserCount 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setOrderUserCount(null))); + // 测试 virtualGroup 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setVirtualGroup(null))); + // 测试 status 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setStatus(null))); + // 测试 limitDuration 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setLimitDuration(null))); + // 测试 createTime 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setCreateTime(null))); + // 准备参数 + CombinationActivityPageReqVO reqVO = new CombinationActivityPageReqVO(); + reqVO.setName(null); + reqVO.setSpuId(null); + reqVO.setTotalLimitCount(null); + reqVO.setSingleLimitCount(null); + reqVO.setStartTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + reqVO.setEndTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + reqVO.setUserSize(null); + reqVO.setTotalNum(null); + reqVO.setSuccessNum(null); + reqVO.setOrderUserCount(null); + reqVO.setVirtualGroup(null); + reqVO.setStatus(null); + reqVO.setLimitDuration(null); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = combinationActivityService.getCombinationActivityPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbCombinationActivity, pageResult.getList().get(0)); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetCombinationActivityList() { + // mock 数据 + CombinationActivityDO dbCombinationActivity = randomPojo(CombinationActivityDO.class, o -> { // 等会查询到 + o.setName(null); + //o.setSpuId(null); + o.setTotalLimitCount(null); + o.setSingleLimitCount(null); + o.setStartTime(null); + o.setEndTime(null); + o.setUserSize(null); + o.setTotalNum(null); + o.setSuccessNum(null); + o.setOrderUserCount(null); + o.setVirtualGroup(null); + o.setStatus(null); + o.setLimitDuration(null); + o.setCreateTime(null); + }); + combinationActivityMapper.insert(dbCombinationActivity); + // 测试 name 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setName(null))); + // 测试 spuId 不匹配 + //combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setSpuId(null))); + // 测试 totalLimitCount 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setTotalLimitCount(null))); + // 测试 singleLimitCount 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setSingleLimitCount(null))); + // 测试 startTime 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setStartTime(null))); + // 测试 endTime 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setEndTime(null))); + // 测试 userSize 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setUserSize(null))); + // 测试 totalNum 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setTotalNum(null))); + // 测试 successNum 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setSuccessNum(null))); + // 测试 orderUserCount 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setOrderUserCount(null))); + // 测试 virtualGroup 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setVirtualGroup(null))); + // 测试 status 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setStatus(null))); + // 测试 limitDuration 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setLimitDuration(null))); + // 测试 createTime 不匹配 + combinationActivityMapper.insert(cloneIgnoreId(dbCombinationActivity, o -> o.setCreateTime(null))); + // 准备参数 + CombinationActivityExportReqVO reqVO = new CombinationActivityExportReqVO(); + reqVO.setName(null); + reqVO.setSpuId(null); + reqVO.setTotalLimitCount(null); + reqVO.setSingleLimitCount(null); + reqVO.setStartTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + reqVO.setEndTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + reqVO.setUserSize(null); + reqVO.setTotalNum(null); + reqVO.setSuccessNum(null); + reqVO.setOrderUserCount(null); + reqVO.setVirtualGroup(null); + reqVO.setStatus(null); + reqVO.setLimitDuration(null); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + List list = combinationActivityService.getCombinationActivityList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbCombinationActivity, list.get(0)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateServiceImplTest.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateServiceImplTest.java new file mode 100644 index 0000000..12552c6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/coupon/CouponTemplateServiceImplTest.java @@ -0,0 +1,147 @@ +package com.yunxi.scm.module.promotion.service.coupon; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplatePageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.coupon.vo.template.CouponTemplateUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponTemplateDO; +import com.yunxi.scm.module.promotion.dal.mysql.coupon.CouponTemplateMapper; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponTemplateValidityTypeEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** +* {@link CouponTemplateServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(CouponTemplateServiceImpl.class) +public class CouponTemplateServiceImplTest extends BaseDbUnitTest { + + @Resource + private CouponTemplateServiceImpl couponTemplateService; + + @Resource + private CouponTemplateMapper couponTemplateMapper; + + @Test + public void testCreateCouponTemplate_success() { + // 准备参数 + CouponTemplateCreateReqVO reqVO = randomPojo(CouponTemplateCreateReqVO.class, + o -> o.setProductScope(randomEle(PromotionProductScopeEnum.values()).getScope()) + .setValidityType(randomEle(CouponTemplateValidityTypeEnum.values()).getType()) + .setDiscountType(randomEle(PromotionDiscountTypeEnum.values()).getType())); + + // 调用 + Long couponTemplateId = couponTemplateService.createCouponTemplate(reqVO); + // 断言 + assertNotNull(couponTemplateId); + // 校验记录的属性是否正确 + CouponTemplateDO couponTemplate = couponTemplateMapper.selectById(couponTemplateId); + assertPojoEquals(reqVO, couponTemplate); + } + + @Test + public void testUpdateCouponTemplate_success() { + // mock 数据 + CouponTemplateDO dbCouponTemplate = randomPojo(CouponTemplateDO.class); + couponTemplateMapper.insert(dbCouponTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + CouponTemplateUpdateReqVO reqVO = randomPojo(CouponTemplateUpdateReqVO.class, o -> { + o.setId(dbCouponTemplate.getId()); // 设置更新的 ID + // 其它通用字段 + o.setProductScope(randomEle(PromotionProductScopeEnum.values()).getScope()) + .setValidityType(randomEle(CouponTemplateValidityTypeEnum.values()).getType()) + .setDiscountType(randomEle(PromotionDiscountTypeEnum.values()).getType()); + }); + + // 调用 + couponTemplateService.updateCouponTemplate(reqVO); + // 校验是否更新正确 + CouponTemplateDO couponTemplate = couponTemplateMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, couponTemplate); + } + + @Test + public void testUpdateCouponTemplate_notExists() { + // 准备参数 + CouponTemplateUpdateReqVO reqVO = randomPojo(CouponTemplateUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> couponTemplateService.updateCouponTemplate(reqVO), COUPON_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testDeleteCouponTemplate_success() { + // mock 数据 + CouponTemplateDO dbCouponTemplate = randomPojo(CouponTemplateDO.class); + couponTemplateMapper.insert(dbCouponTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbCouponTemplate.getId(); + + // 调用 + couponTemplateService.deleteCouponTemplate(id); + // 校验数据不存在了 + assertNull(couponTemplateMapper.selectById(id)); + } + + @Test + public void testDeleteCouponTemplate_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> couponTemplateService.deleteCouponTemplate(id), COUPON_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testGetCouponTemplatePage() { + // mock 数据 + CouponTemplateDO dbCouponTemplate = randomPojo(CouponTemplateDO.class, o -> { // 等会查询到 + o.setName("芋艿"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType()); + o.setCreateTime(buildTime(2022, 2, 2)); + }); + couponTemplateMapper.insert(dbCouponTemplate); + // 测试 name 不匹配 + couponTemplateMapper.insert(cloneIgnoreId(dbCouponTemplate, o -> o.setName("土豆"))); + // 测试 status 不匹配 + couponTemplateMapper.insert(cloneIgnoreId(dbCouponTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 type 不匹配 + couponTemplateMapper.insert(cloneIgnoreId(dbCouponTemplate, o -> o.setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()))); + // 测试 createTime 不匹配 + couponTemplateMapper.insert(cloneIgnoreId(dbCouponTemplate, o -> o.setCreateTime(buildTime(2022, 1, 1)))); + // 准备参数 + CouponTemplatePageReqVO reqVO = new CouponTemplatePageReqVO(); + reqVO.setName("芋艿"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType()); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2022, 2, 1), buildTime(2022, 2, 3)})); + + // 调用 + PageResult pageResult = couponTemplateService.getCouponTemplatePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbCouponTemplate, pageResult.getList().get(0)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentServiceImplTest.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentServiceImplTest.java new file mode 100644 index 0000000..3caa187 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/decorate/DecorateComponentServiceImplTest.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.promotion.service.decorate; + +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.promotion.dal.dataobject.decorate.DecorateComponentDO; +import com.yunxi.scm.module.promotion.dal.mysql.decorate.DecorateComponentMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import static com.yunxi.scm.module.promotion.enums.decorate.DecorateComponentEnum.ROLLING_BANNER; +import static com.yunxi.scm.module.promotion.enums.decorate.DecoratePageEnum.INDEX; +import static org.mockito.ArgumentMatchers.eq; + +// TODO @芋艿:后续 review 下 +/** + * @author jason + */ +public class DecorateComponentServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private DecorateComponentServiceImpl decoratePageService; + + @Mock + private DecorateComponentMapper decorateComponentMapper; + + @BeforeEach + public void init(){ + + } + + @Test + void testResp(){ + List list = new ArrayList<>(1); + DecorateComponentDO decorateDO = new DecorateComponentDO() + .setPageId(INDEX.getId()).setValue("") + .setCode(ROLLING_BANNER.getCode()).setId(1L); + list.add(decorateDO); + //mock 方法 + Mockito.when(decorateComponentMapper.selectByPage(eq(1))).thenReturn(list); + } +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityServiceImplTest.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityServiceImplTest.java new file mode 100644 index 0000000..d99fddd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/discount/DiscountActivityServiceImplTest.java @@ -0,0 +1,210 @@ +package com.yunxi.scm.module.promotion.service.discount; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityBaseVO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.discount.vo.DiscountActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountActivityDO; +import com.yunxi.scm.module.promotion.dal.dataobject.discount.DiscountProductDO; +import com.yunxi.scm.module.promotion.dal.mysql.discount.DiscountActivityMapper; +import com.yunxi.scm.module.promotion.dal.mysql.discount.DiscountProductMapper; +import com.yunxi.scm.module.promotion.enums.common.PromotionActivityStatusEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.addTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.DISCOUNT_ACTIVITY_NOT_EXISTS; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; + +/** +* {@link DiscountActivityServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(DiscountActivityServiceImpl.class) +public class DiscountActivityServiceImplTest extends BaseDbUnitTest { + + @Resource + private DiscountActivityServiceImpl discountActivityService; + + @Resource + private DiscountActivityMapper discountActivityMapper; + @Resource + private DiscountProductMapper discountProductMapper; + + @Test + public void testCreateDiscountActivity_success() { + // 准备参数 + DiscountActivityCreateReqVO reqVO = randomPojo(DiscountActivityCreateReqVO.class, o -> { + // 用于触发进行中的状态 + o.setStartTime(addTime(Duration.ofDays(1))).setEndTime(addTime(Duration.ofDays(2))); + // 设置商品 + o.setProducts(asList(new DiscountActivityBaseVO.Product().setSpuId(1L).setSkuId(2L) + .setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()).setDiscountPrice(3), + new DiscountActivityBaseVO.Product().setSpuId(10L).setSkuId(20L) + .setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType()).setDiscountPercent(30))); + }); + + // 调用 + Long discountActivityId = discountActivityService.createDiscountActivity(reqVO); + // 断言 + assertNotNull(discountActivityId); + // 校验活动 + DiscountActivityDO discountActivity = discountActivityMapper.selectById(discountActivityId); + assertPojoEquals(reqVO, discountActivity); + assertEquals(discountActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); + // 校验商品 + List discountProducts = discountProductMapper.selectList(DiscountProductDO::getActivityId, discountActivity.getId()); + assertEquals(discountProducts.size(), reqVO.getProducts().size()); + for (int i = 0; i < reqVO.getProducts().size(); i++) { + DiscountActivityBaseVO.Product product = reqVO.getProducts().get(i); + DiscountProductDO discountProduct = discountProducts.get(i); + assertEquals(discountProduct.getActivityId(), discountActivity.getId()); + assertEquals(discountProduct.getSpuId(), product.getSpuId()); + assertEquals(discountProduct.getSkuId(), product.getSkuId()); + assertEquals(discountProduct.getDiscountType(), product.getDiscountType()); + assertEquals(discountProduct.getDiscountPrice(), product.getDiscountPrice()); + assertEquals(discountProduct.getDiscountPercent(), product.getDiscountPercent()); + } + } + + @Test + public void testUpdateDiscountActivity_success() { + // mock 数据(商品) + DiscountActivityDO dbDiscountActivity = randomPojo(DiscountActivityDO.class); + discountActivityMapper.insert(dbDiscountActivity);// @Sql: 先插入出一条存在的数据 + // mock 数据(活动) + DiscountProductDO dbDiscountProduct01 = randomPojo(DiscountProductDO.class, o -> o.setActivityId(dbDiscountActivity.getId()) + .setSpuId(1L).setSkuId(2L).setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()).setDiscountPrice(3).setDiscountPercent(null)); + DiscountProductDO dbDiscountProduct02 = randomPojo(DiscountProductDO.class, o -> o.setActivityId(dbDiscountActivity.getId()) + .setSpuId(10L).setSkuId(20L).setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType()).setDiscountPercent(30).setDiscountPrice(null)); + discountProductMapper.insert(dbDiscountProduct01); + discountProductMapper.insert(dbDiscountProduct02); + // 准备参数 + DiscountActivityUpdateReqVO reqVO = randomPojo(DiscountActivityUpdateReqVO.class, o -> { + o.setId(dbDiscountActivity.getId()); // 设置更新的 ID + // 用于触发进行中的状态 + o.setStartTime(addTime(Duration.ofDays(1))).setEndTime(addTime(Duration.ofDays(2))); + // 设置商品 + o.setProducts(asList(new DiscountActivityBaseVO.Product().setSpuId(1L).setSkuId(2L) + .setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()).setDiscountPrice(3).setDiscountPercent(null), + new DiscountActivityBaseVO.Product().setSpuId(100L).setSkuId(200L) + .setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType()).setDiscountPercent(30).setDiscountPrice(null))); + }); + + // 调用 + discountActivityService.updateDiscountActivity(reqVO); + // 校验活动 + DiscountActivityDO discountActivity = discountActivityMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, discountActivity); + assertEquals(discountActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); + // 校验商品 + List discountProducts = discountProductMapper.selectList(DiscountProductDO::getActivityId, discountActivity.getId()); + assertEquals(discountProducts.size(), reqVO.getProducts().size()); + for (int i = 0; i < reqVO.getProducts().size(); i++) { + DiscountActivityBaseVO.Product product = reqVO.getProducts().get(i); + DiscountProductDO discountProduct = discountProducts.get(i); + assertEquals(discountProduct.getActivityId(), discountActivity.getId()); + assertEquals(discountProduct.getSpuId(), product.getSpuId()); + assertEquals(discountProduct.getSkuId(), product.getSkuId()); + assertEquals(discountProduct.getDiscountType(), product.getDiscountType()); + assertEquals(discountProduct.getDiscountPrice(), product.getDiscountPrice()); + assertEquals(discountProduct.getDiscountPercent(), product.getDiscountPercent()); + } + } + + @Test + public void testCloseDiscountActivity() { + // mock 数据 + DiscountActivityDO dbDiscountActivity = randomPojo(DiscountActivityDO.class, + o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); + discountActivityMapper.insert(dbDiscountActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDiscountActivity.getId(); + + // 调用 + discountActivityService.closeRewardActivity(id); + // 校验状态 + DiscountActivityDO discountActivity = discountActivityMapper.selectById(id); + assertEquals(discountActivity.getStatus(), PromotionActivityStatusEnum.CLOSE.getStatus()); + } + + @Test + public void testUpdateDiscountActivity_notExists() { + // 准备参数 + DiscountActivityUpdateReqVO reqVO = randomPojo(DiscountActivityUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> discountActivityService.updateDiscountActivity(reqVO), DISCOUNT_ACTIVITY_NOT_EXISTS); + } + + @Test + public void testDeleteDiscountActivity_success() { + // mock 数据 + DiscountActivityDO dbDiscountActivity = randomPojo(DiscountActivityDO.class, + o -> o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus())); + discountActivityMapper.insert(dbDiscountActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDiscountActivity.getId(); + + // 调用 + discountActivityService.deleteDiscountActivity(id); + // 校验数据不存在了 + assertNull(discountActivityMapper.selectById(id)); + } + + @Test + public void testDeleteDiscountActivity_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> discountActivityService.deleteDiscountActivity(id), DISCOUNT_ACTIVITY_NOT_EXISTS); + } + + @Test + public void testGetDiscountActivityPage() { + // mock 数据 + DiscountActivityDO dbDiscountActivity = randomPojo(DiscountActivityDO.class, o -> { // 等会查询到 + o.setName("芋艿"); + o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus()); + o.setCreateTime(buildTime(2021, 1, 15)); + }); + discountActivityMapper.insert(dbDiscountActivity); + // 测试 name 不匹配 + discountActivityMapper.insert(cloneIgnoreId(dbDiscountActivity, o -> o.setName("土豆"))); + // 测试 status 不匹配 + discountActivityMapper.insert(cloneIgnoreId(dbDiscountActivity, o -> o.setStatus(PromotionActivityStatusEnum.END.getStatus()))); + // 测试 createTime 不匹配 + discountActivityMapper.insert(cloneIgnoreId(dbDiscountActivity, o -> o.setCreateTime(buildTime(2021, 2, 10)))); + // 准备参数 + DiscountActivityPageReqVO reqVO = new DiscountActivityPageReqVO(); + reqVO.setName("芋艿"); + reqVO.setStatus(PromotionActivityStatusEnum.WAIT.getStatus()); + reqVO.setCreateTime((new LocalDateTime[]{buildTime(2021, 1, 1), buildTime(2021, 1, 31)})); + + // 调用 + PageResult pageResult = discountActivityService.getDiscountActivityPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbDiscountActivity, pageResult.getList().get(0)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/price/PriceServiceTest.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/price/PriceServiceTest.java new file mode 100644 index 0000000..0fe5232 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/price/PriceServiceTest.java @@ -0,0 +1,104 @@ +package com.yunxi.scm.module.promotion.service.price; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.product.api.sku.ProductSkuApi; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.promotion.api.price.dto.CouponMeetRespDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateReqDTO; +import com.yunxi.scm.module.promotion.dal.dataobject.coupon.CouponDO; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.yunxi.scm.module.promotion.enums.coupon.CouponStatusEnum; +import com.yunxi.scm.module.promotion.service.coupon.CouponService; +import com.yunxi.scm.module.promotion.service.reward.RewardActivityService; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.COUPON_VALID_TIME_NOT_NOW; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.when; + +/** + * {@link PriceServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +public class PriceServiceTest extends BaseMockitoUnitTest { + + @InjectMocks + private PriceServiceImpl priceService; + + @Mock + private RewardActivityService rewardActivityService; + @Mock + private CouponService couponService; + @Mock + private ProductSkuApi productSkuApi; + + /** + * 测试满减送活动,不匹配的情况 + */ + @Test + public void testCalculatePrice_rewardActivityNotMeet() { + + } + + @Test + public void testGetMeetCouponList() { + // 准备参数 + PriceCalculateReqDTO calculateReqDTO = new PriceCalculateReqDTO().setUserId(1024L) + .setItems(singletonList(new PriceCalculateReqDTO.Item().setSkuId(10L).setCount(2))); + // mock 方法(商品 SKU 信息) + ProductSkuRespDTO productSku = randomPojo(ProductSkuRespDTO.class, o -> o.setId(10L).setPrice(100)); + when(productSkuApi.getSkuList(eq(asSet(10L)))).thenReturn(singletonList(productSku)); + // mock 方法(情况一:优惠劵未到使用时间) + CouponDO coupon01 = randomPojo(CouponDO.class); + doThrow(new ServiceException(COUPON_VALID_TIME_NOT_NOW)).when(couponService).validCoupon(coupon01); + // mock 方法(情况二:所结算商品没有符合条件的商品) + CouponDO coupon02 = randomPojo(CouponDO.class); + // mock 方法(情况三:使用金额不足) + CouponDO coupon03 = randomPojo(CouponDO.class, o -> o.setProductScope(PromotionProductScopeEnum.ALL.getScope()) + .setUsePrice(300)); + // mock 方法(情况五:满足条件) + CouponDO coupon04 = randomPojo(CouponDO.class, o -> o.setProductScope(PromotionProductScopeEnum.ALL.getScope()) + .setUsePrice(190)); + // mock 方法(获得用户的待使用优惠劵) + when(couponService.getCouponList(eq(1024L), eq(CouponStatusEnum.UNUSED.getStatus()))) + .thenReturn(asList(coupon01, coupon02, coupon03, coupon04)); + // 调用 + List list = priceService.getMeetCouponList(calculateReqDTO); + // 断言 + assertEquals(list.size(), 4); + // 断言情况一:优惠劵未到使用时间 + CouponMeetRespDTO couponMeetRespDTO01 = list.get(0); + assertPojoEquals(couponMeetRespDTO01, coupon01); + assertFalse(couponMeetRespDTO01.getMeet()); + assertEquals(couponMeetRespDTO01.getMeetTip(), "优惠劵未到使用时间"); + // 断言情况二:所结算商品没有符合条件的商品 + CouponMeetRespDTO couponMeetRespDTO02 = list.get(1); + assertPojoEquals(couponMeetRespDTO02, coupon02); + assertFalse(couponMeetRespDTO02.getMeet()); + assertEquals(couponMeetRespDTO02.getMeetTip(), "所结算商品没有符合条件的商品"); + // 断言情况三:差 %s 元可用优惠劵 + CouponMeetRespDTO couponMeetRespDTO03 = list.get(2); + assertPojoEquals(couponMeetRespDTO03, coupon03); + assertFalse(couponMeetRespDTO03.getMeet()); + assertEquals(couponMeetRespDTO03.getMeetTip(), "所结算的商品中未满足使用的金额"); + // 断言情况四:满足条件 + CouponMeetRespDTO couponMeetRespDTO04 = list.get(3); + assertPojoEquals(couponMeetRespDTO04, coupon04); + assertTrue(couponMeetRespDTO04.getMeet()); + assertNull(couponMeetRespDTO04.getMeetTip()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityServiceImplTest.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityServiceImplTest.java new file mode 100644 index 0000000..15fea26 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/reward/RewardActivityServiceImplTest.java @@ -0,0 +1,218 @@ +package com.yunxi.scm.module.promotion.service.reward; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.reward.vo.RewardActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.reward.RewardActivityDO; +import com.yunxi.scm.module.promotion.dal.mysql.reward.RewardActivityMapper; +import com.yunxi.scm.module.promotion.enums.common.PromotionActivityStatusEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionConditionTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.Map; +import java.util.Set; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.addTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.REWARD_ACTIVITY_NOT_EXISTS; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; + +/** +* {@link RewardActivityServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(RewardActivityServiceImpl.class) +public class RewardActivityServiceImplTest extends BaseDbUnitTest { + + @Resource + private RewardActivityServiceImpl rewardActivityService; + + @Resource + private RewardActivityMapper rewardActivityMapper; + + @Test + public void testCreateRewardActivity_success() { + // 准备参数 + RewardActivityCreateReqVO reqVO = randomPojo(RewardActivityCreateReqVO.class, o -> { + o.setConditionType(randomEle(PromotionConditionTypeEnum.values()).getType()); + o.setProductScope(randomEle(PromotionProductScopeEnum.values()).getScope()); + // 用于触发进行中的状态 + o.setStartTime(addTime(Duration.ofDays(1))).setEndTime(addTime(Duration.ofDays(2))); + }); + + // 调用 + Long rewardActivityId = rewardActivityService.createRewardActivity(reqVO); + // 断言 + assertNotNull(rewardActivityId); + // 校验记录的属性是否正确 + RewardActivityDO rewardActivity = rewardActivityMapper.selectById(rewardActivityId); + assertPojoEquals(reqVO, rewardActivity, "rules"); + assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); + for (int i = 0; i < reqVO.getRules().size(); i++) { + assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); + } + } + + @Test + public void testUpdateRewardActivity_success() { + // mock 数据 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); + rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class, o -> { + o.setId(dbRewardActivity.getId()); // 设置更新的 ID + o.setConditionType(randomEle(PromotionConditionTypeEnum.values()).getType()); + o.setProductScope(randomEle(PromotionProductScopeEnum.values()).getScope()); + // 用于触发进行中的状态 + o.setStartTime(addTime(Duration.ofDays(1))).setEndTime(addTime(Duration.ofDays(2))); + }); + + // 调用 + rewardActivityService.updateRewardActivity(reqVO); + // 校验是否更新正确 + RewardActivityDO rewardActivity = rewardActivityMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, rewardActivity, "rules"); + assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.WAIT.getStatus()); + for (int i = 0; i < reqVO.getRules().size(); i++) { + assertPojoEquals(reqVO.getRules().get(i), rewardActivity.getRules().get(i)); + } + } + + @Test + public void testCloseRewardActivity() { + // mock 数据 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.WAIT.getStatus())); + rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbRewardActivity.getId(); + + // 调用 + rewardActivityService.closeRewardActivity(id); + // 校验状态 + RewardActivityDO rewardActivity = rewardActivityMapper.selectById(id); + assertEquals(rewardActivity.getStatus(), PromotionActivityStatusEnum.CLOSE.getStatus()); + } + + @Test + public void testUpdateRewardActivity_notExists() { + // 准备参数 + RewardActivityUpdateReqVO reqVO = randomPojo(RewardActivityUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> rewardActivityService.updateRewardActivity(reqVO), REWARD_ACTIVITY_NOT_EXISTS); + } + + @Test + public void testDeleteRewardActivity_success() { + // mock 数据 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus())); + rewardActivityMapper.insert(dbRewardActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbRewardActivity.getId(); + + // 调用 + rewardActivityService.deleteRewardActivity(id); + // 校验数据不存在了 + assertNull(rewardActivityMapper.selectById(id)); + } + + @Test + public void testDeleteRewardActivity_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> rewardActivityService.deleteRewardActivity(id), REWARD_ACTIVITY_NOT_EXISTS); + } + + @Test + public void testGetRewardActivityPage() { + // mock 数据 + RewardActivityDO dbRewardActivity = randomPojo(RewardActivityDO.class, o -> { // 等会查询到 + o.setName("芋艿"); + o.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); + }); + rewardActivityMapper.insert(dbRewardActivity); + // 测试 name 不匹配 + rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setName("土豆"))); + // 测试 status 不匹配 + rewardActivityMapper.insert(cloneIgnoreId(dbRewardActivity, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()))); + // 准备参数 + RewardActivityPageReqVO reqVO = new RewardActivityPageReqVO(); + reqVO.setName("芋艿"); + reqVO.setStatus(PromotionActivityStatusEnum.CLOSE.getStatus()); + + // 调用 + PageResult pageResult = rewardActivityService.getRewardActivityPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbRewardActivity, pageResult.getList().get(0), "rules"); + } + + @Test + public void testGetRewardActivities_all() { + // mock 数据 + RewardActivityDO allActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) + .setProductScope(PromotionProductScopeEnum.ALL.getScope())); + rewardActivityMapper.insert(allActivity); + RewardActivityDO productActivity = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); + rewardActivityMapper.insert(productActivity); + // 准备参数 + Set spuIds = asSet(1L, 2L); + + // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList + //Map> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds); + // 断言 + //assertEquals(matchRewardActivities.size(), 1); + //Map.Entry> next = matchRewardActivities.entrySet().iterator().next(); + //assertPojoEquals(next.getKey(), allActivity); + //assertEquals(next.getValue(), spuIds); + } + + @Test + public void testGetRewardActivities_product() { + // mock 数据 + RewardActivityDO productActivity01 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L))); + rewardActivityMapper.insert(productActivity01); + RewardActivityDO productActivity02 = randomPojo(RewardActivityDO.class, o -> o.setStatus(PromotionActivityStatusEnum.RUN.getStatus()) + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(singletonList(3L))); + rewardActivityMapper.insert(productActivity02); + // 准备参数 + Set spuIds = asSet(1L, 2L, 3L); + + // 调用 TODO getMatchRewardActivities 没有这个方法,但是找到了 getMatchRewardActivityList + //Map> matchRewardActivities = rewardActivityService.getMatchRewardActivities(spuIds); + // 断言 + //assertEquals(matchRewardActivities.size(), 2); + //matchRewardActivities.forEach((activity, activitySpuIds) -> { + // if (activity.getId().equals(productActivity01.getId())) { + // assertPojoEquals(activity, productActivity01); + // assertEquals(activitySpuIds, asSet(1L, 2L)); + // } else if (activity.getId().equals(productActivity02.getId())) { + // assertPojoEquals(activity, productActivity02); + // assertEquals(activitySpuIds, asSet(3L)); + // } else { + // fail(); + // } + //}); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/seckillactivity/SeckillActivityServiceImplTest.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/seckillactivity/SeckillActivityServiceImplTest.java new file mode 100644 index 0000000..126e74c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/seckillactivity/SeckillActivityServiceImplTest.java @@ -0,0 +1,171 @@ +package com.yunxi.scm.module.promotion.service.seckillactivity; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityPageReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.activity.SeckillActivityUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillactivity.SeckillActivityDO; +import com.yunxi.scm.module.promotion.dal.mysql.seckill.seckillactivity.SeckillActivityMapper; +import com.yunxi.scm.module.promotion.service.seckill.seckillactivity.SeckillActivityServiceImpl; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.SECKILL_ACTIVITY_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** +* {@link SeckillActivityServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(SeckillActivityServiceImpl.class) +@Disabled // TODO 芋艿:未来开启 +public class SeckillActivityServiceImplTest extends BaseDbUnitTest { + + @Resource + private SeckillActivityServiceImpl seckillActivityService; + + @Resource + private SeckillActivityMapper seckillActivityMapper; + + @Test + public void testCreateSeckillActivity_success() { + // 准备参数 + SeckillActivityCreateReqVO reqVO = randomPojo(SeckillActivityCreateReqVO.class); + + // 调用 + Long seckillActivityId = seckillActivityService.createSeckillActivity(reqVO); + // 断言 + assertNotNull(seckillActivityId); + // 校验记录的属性是否正确 + SeckillActivityDO seckillActivity = seckillActivityMapper.selectById(seckillActivityId); + assertPojoEquals(reqVO, seckillActivity); + } + + @Test + public void testUpdateSeckillActivity_success() { + // mock 数据 + SeckillActivityDO dbSeckillActivity = randomPojo(SeckillActivityDO.class); + seckillActivityMapper.insert(dbSeckillActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SeckillActivityUpdateReqVO reqVO = randomPojo(SeckillActivityUpdateReqVO.class, o -> { + o.setId(dbSeckillActivity.getId()); // 设置更新的 ID + }); + + // 调用 + seckillActivityService.updateSeckillActivity(reqVO); + // 校验是否更新正确 + SeckillActivityDO seckillActivity = seckillActivityMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, seckillActivity); + } + + @Test + public void testUpdateSeckillActivity_notExists() { + // 准备参数 + SeckillActivityUpdateReqVO reqVO = randomPojo(SeckillActivityUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> seckillActivityService.updateSeckillActivity(reqVO), SECKILL_ACTIVITY_NOT_EXISTS); + } + + @Test + public void testDeleteSeckillActivity_success() { + // mock 数据 + SeckillActivityDO dbSeckillActivity = randomPojo(SeckillActivityDO.class); + seckillActivityMapper.insert(dbSeckillActivity);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSeckillActivity.getId(); + + // 调用 + seckillActivityService.deleteSeckillActivity(id); + // 校验数据不存在了 + assertNull(seckillActivityMapper.selectById(id)); + } + + @Test + public void testDeleteSeckillActivity_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> seckillActivityService.deleteSeckillActivity(id), SECKILL_ACTIVITY_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetSeckillActivityPage() { + // mock 数据 + SeckillActivityDO dbSeckillActivity = randomPojo(SeckillActivityDO.class, o -> { // 等会查询到 + o.setName(null); + o.setStatus(null); + o.setConfigIds(null); + o.setCreateTime(null); + }); + seckillActivityMapper.insert(dbSeckillActivity); + // 测试 name 不匹配 + seckillActivityMapper.insert(cloneIgnoreId(dbSeckillActivity, o -> o.setName(null))); + // 测试 status 不匹配 + seckillActivityMapper.insert(cloneIgnoreId(dbSeckillActivity, o -> o.setStatus(null))); + // 测试 timeId 不匹配 + seckillActivityMapper.insert(cloneIgnoreId(dbSeckillActivity, o -> o.setConfigIds(null))); + // 测试 createTime 不匹配 + seckillActivityMapper.insert(cloneIgnoreId(dbSeckillActivity, o -> o.setCreateTime(null))); + // 准备参数 + SeckillActivityPageReqVO reqVO = new SeckillActivityPageReqVO(); + reqVO.setName(null); + reqVO.setStatus(null); + reqVO.setConfigId(null); + reqVO.setCreateTime((new LocalDateTime[]{})); + + // 调用 + PageResult pageResult = seckillActivityService.getSeckillActivityPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSeckillActivity, pageResult.getList().get(0)); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetSeckillActivityList() { + // mock 数据 + SeckillActivityDO dbSeckillActivity = randomPojo(SeckillActivityDO.class, o -> { // 等会查询到 + o.setName(null); + o.setStatus(null); + o.setConfigIds(null); + o.setCreateTime(null); + }); + seckillActivityMapper.insert(dbSeckillActivity); + // 测试 name 不匹配 + seckillActivityMapper.insert(cloneIgnoreId(dbSeckillActivity, o -> o.setName(null))); + // 测试 status 不匹配 + seckillActivityMapper.insert(cloneIgnoreId(dbSeckillActivity, o -> o.setStatus(null))); + // 测试 timeId 不匹配 + seckillActivityMapper.insert(cloneIgnoreId(dbSeckillActivity, o -> o.setConfigIds(null))); + // 测试 createTime 不匹配 + seckillActivityMapper.insert(cloneIgnoreId(dbSeckillActivity, o -> o.setCreateTime(null))); + // 准备参数 +// SeckillActivityExportReqVO reqVO = new SeckillActivityExportReqVO(); +// reqVO.setName(null); +// reqVO.setStatus(null); +// reqVO.setTimeId(null); +// reqVO.setCreateTime((new Date[]{})); +// +// // 调用 +// List list = seckillActivityService.getSeckillActivityList(reqVO); +// // 断言 +// assertEquals(1, list.size()); +// assertPojoEquals(dbSeckillActivity, list.get(0)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/seckillconfig/SeckillConfigServiceImplTest.java b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/seckillconfig/SeckillConfigServiceImplTest.java new file mode 100644 index 0000000..8a96434 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/java/com/yunxi/scm/module/promotion/service/seckillconfig/SeckillConfigServiceImplTest.java @@ -0,0 +1,190 @@ +package com.yunxi.scm.module.promotion.service.seckillconfig; + +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigCreateReqVO; +import com.yunxi.scm.module.promotion.controller.admin.seckill.vo.config.SeckillConfigUpdateReqVO; +import com.yunxi.scm.module.promotion.dal.dataobject.seckill.seckillconfig.SeckillConfigDO; +import com.yunxi.scm.module.promotion.dal.mysql.seckill.seckillconfig.SeckillConfigMapper; +import com.yunxi.scm.module.promotion.service.seckill.seckillconfig.SeckillConfigServiceImpl; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.SECKILL_TIME_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * {@link SeckillConfigServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(SeckillConfigServiceImpl.class) +@Disabled // TODO 芋艿:未来开启;后续要 review 下 +public class SeckillConfigServiceImplTest extends BaseDbUnitTest { + + @Resource + private SeckillConfigServiceImpl SeckillConfigService; + + @Resource + private SeckillConfigMapper seckillConfigMapper; + + @Resource + private ObjectMapper objectMapper; + + @Test + public void testJacksonSerializ() { + + // 准备参数 + SeckillConfigCreateReqVO reqVO = randomPojo(SeckillConfigCreateReqVO.class); +// ObjectMapper objectMapper = new ObjectMapper(); + try { + String string = objectMapper.writeValueAsString(reqVO); + System.out.println(string); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + + } + + @Test + public void testCreateSeckillConfig_success() { + // 准备参数 + SeckillConfigCreateReqVO reqVO = randomPojo(SeckillConfigCreateReqVO.class); + + // 调用 + Long SeckillConfigId = SeckillConfigService.createSeckillConfig(reqVO); + // 断言 + assertNotNull(SeckillConfigId); + // 校验记录的属性是否正确 + SeckillConfigDO SeckillConfig = seckillConfigMapper.selectById(SeckillConfigId); + assertPojoEquals(reqVO, SeckillConfig); + } + + @Test + public void testUpdateSeckillConfig_success() { + // mock 数据 + SeckillConfigDO dbSeckillConfig = randomPojo(SeckillConfigDO.class); + seckillConfigMapper.insert(dbSeckillConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SeckillConfigUpdateReqVO reqVO = randomPojo(SeckillConfigUpdateReqVO.class, o -> { + o.setId(dbSeckillConfig.getId()); // 设置更新的 ID + }); + + // 调用 + SeckillConfigService.updateSeckillConfig(reqVO); + // 校验是否更新正确 + SeckillConfigDO SeckillConfig = seckillConfigMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, SeckillConfig); + } + + @Test + public void testUpdateSeckillConfig_notExists() { + // 准备参数 + SeckillConfigUpdateReqVO reqVO = randomPojo(SeckillConfigUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> SeckillConfigService.updateSeckillConfig(reqVO), SECKILL_TIME_NOT_EXISTS); + } + + @Test + public void testDeleteSeckillConfig_success() { + // mock 数据 + SeckillConfigDO dbSeckillConfig = randomPojo(SeckillConfigDO.class); + seckillConfigMapper.insert(dbSeckillConfig);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSeckillConfig.getId(); + + // 调用 + SeckillConfigService.deleteSeckillConfig(id); + // 校验数据不存在了 + assertNull(seckillConfigMapper.selectById(id)); + } + + @Test + public void testDeleteSeckillConfig_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> SeckillConfigService.deleteSeckillConfig(id), SECKILL_TIME_NOT_EXISTS); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetSeckillConfigPage() { + // mock 数据 +// SeckillConfigDO dbSeckillConfig = randomPojo(SeckillConfigDO.class, o -> { // 等会查询到 +// o.setName(null); +// o.setStartTime(null); +// o.setEndTime(null); +// o.setCreateTime(null); +// }); +// seckillConfigMapper.insert(dbSeckillConfig); +// // 测试 name 不匹配 +// seckillConfigMapper.insert(cloneIgnoreId(dbSeckillConfig, o -> o.setName(null))); +// // 测试 startTime 不匹配 +// seckillConfigMapper.insert(cloneIgnoreId(dbSeckillConfig, o -> o.setStartTime(null))); +// // 测试 endTime 不匹配 +// seckillConfigMapper.insert(cloneIgnoreId(dbSeckillConfig, o -> o.setEndTime(null))); +// // 测试 createTime 不匹配 +// seckillConfigMapper.insert(cloneIgnoreId(dbSeckillConfig, o -> o.setCreateTime(null))); +// // 准备参数 +// SeckillConfigPageReqVO reqVO = new SeckillConfigPageReqVO(); +// reqVO.setName(null); +//// reqVO.setStartTime((new LocalTime())); +//// reqVO.setEndTime((new LocalTime[]{})); +//// reqVO.setCreateTime((new Date[]{})); +// +// // 调用 +// PageResult pageResult = SeckillConfigService.getSeckillConfigPage(reqVO); +// // 断言 +// assertEquals(1, pageResult.getTotal()); +// assertEquals(1, pageResult.getList().size()); +// assertPojoEquals(dbSeckillConfig, pageResult.getList().get(0)); + } + + @Test + @Disabled // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解 + public void testGetSeckillConfigList() { + // mock 数据 + SeckillConfigDO dbSeckillConfig = randomPojo(SeckillConfigDO.class, o -> { // 等会查询到 + o.setName(null); + o.setStartTime(null); + o.setEndTime(null); + o.setCreateTime(null); + }); + seckillConfigMapper.insert(dbSeckillConfig); + // 测试 name 不匹配 + seckillConfigMapper.insert(cloneIgnoreId(dbSeckillConfig, o -> o.setName(null))); + // 测试 startTime 不匹配 + seckillConfigMapper.insert(cloneIgnoreId(dbSeckillConfig, o -> o.setStartTime(null))); + // 测试 endTime 不匹配 + seckillConfigMapper.insert(cloneIgnoreId(dbSeckillConfig, o -> o.setEndTime(null))); + // 测试 createTime 不匹配 + seckillConfigMapper.insert(cloneIgnoreId(dbSeckillConfig, o -> o.setCreateTime(null))); + // 准备参数 +// SeckillConfigExportReqVO reqVO = new SeckillConfigExportReqVO(); +// reqVO.setName(null); +// reqVO.setStartTime((new LocalTime[]{})); +// reqVO.setEndTime((new LocalTime[]{})); +// reqVO.setCreateTime((new Date[]{})); +// +// // 调用 +// List list = SeckillConfigService.getSeckillConfigList(reqVO); +// // 断言 +// assertEquals(1, list.size()); +// assertPojoEquals(dbSeckillConfig, list.get(0)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..c81dad6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,49 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm.module diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/logback.xml b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/sql/clean.sql b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..7f3ace7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,8 @@ +DELETE FROM "market_activity"; +DELETE FROM "promotion_coupon_template"; +DELETE FROM "promotion_coupon"; +DELETE FROM "promotion_reward_activity"; +DELETE FROM "promotion_discount_activity"; +DELETE FROM "promotion_discount_product"; +DELETE FROM "promotion_seckill_config"; +DELETE FROM "promotion_combination_activity"; diff --git a/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..ffdacfd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-promotion-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,183 @@ +CREATE TABLE IF NOT EXISTS "market_activity" +( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "title" varchar(50) NOT NULL, + "activity_type" tinyint(4) NOT NULL, + "status" tinyint(4) NOT NULL, + "start_time" datetime NOT NULL, + "end_time" datetime NOT NULL, + "invalid_time" datetime, + "delete_time" datetime, + "time_limited_discount" varchar(2000), + "full_privilege" varchar(2000), + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint(20) NOT NULL, + PRIMARY KEY ("id") +) COMMENT '促销活动'; + +CREATE TABLE IF NOT EXISTS "promotion_coupon_template" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "status" int NOT NULL, + "total_count" int NOT NULL, + "take_limit_count" int NOT NULL, + "take_type" int NOT NULL, + "use_price" int NOT NULL, + "product_scope" int NOT NULL, + "product_spu_ids" varchar, + "validity_type" int NOT NULL, + "valid_start_time" datetime, + "valid_end_time" datetime, + "fixed_start_term" int, + "fixed_end_term" int, + "discount_type" int NOT NULL, + "discount_percent" int, + "discount_price" int, + "discount_limit_price" int, + "take_count" int NOT NULL DEFAULT 0, + "use_count" int NOT NULL DEFAULT 0, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '优惠劵模板'; + +CREATE TABLE IF NOT EXISTS "promotion_coupon" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "template_id" bigint NOT NULL, + "name" varchar NOT NULL, + "status" int NOT NULL, + "user_id" bigint NOT NULL, + "take_type" int NOT NULL, + "useprice" int NOT NULL, + "valid_start_time" datetime NOT NULL, + "valid_end_time" datetime NOT NULL, + "product_scope" int NOT NULL, + "product_spu_ids" varchar, + "discount_type" int NOT NULL, + "discount_percent" int, + "discount_price" int, + "discount_limit_price" int, + "use_order_id" bigint, + "use_time" datetime, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '优惠劵'; + +CREATE TABLE IF NOT EXISTS "promotion_reward_activity" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "status" int NOT NULL, + "start_time" datetime NOT NULL, + "end_time" datetime NOT NULL, + "remark" varchar, + "condition_type" int NOT NULL, + "product_scope" int NOT NULL, + "product_spu_ids" varchar, + "rules" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '满减送活动'; + +CREATE TABLE IF NOT EXISTS "promotion_discount_activity" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "status" int NOT NULL, + "start_time" datetime NOT NULL, + "end_time" datetime NOT NULL, + "remark" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '限时折扣活动'; + +-- 将该建表 SQL 语句,添加到 yunxi-module-promotion-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "promotion_seckill_activity" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "spu_id" bigint NOT NULL, + "name" varchar NOT NULL, + "status" int NOT NULL, + "remark" varchar, + "start_time" varchar NOT NULL, + "end_time" varchar NOT NULL, + "sort" int NOT NULL, + "config_ids" varchar NOT NULL, + "order_count" int NOT NULL, + "user_count" int NOT NULL, + "total_price" int NOT NULL, + "total_limit_count" int, + "single_limit_count" int, + "stock" int, + "total_stock" int, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL, + PRIMARY KEY ("id") +) COMMENT '秒杀活动'; + +CREATE TABLE IF NOT EXISTS "promotion_seckill_config" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "start_time" varchar NOT NULL, + "end_time" varchar NOT NULL, + "pic_url" varchar NOT NULL, + "status" int NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL, + PRIMARY KEY ("id") +) COMMENT '秒杀时段配置'; + +CREATE TABLE IF NOT EXISTS "promotion_combination_activity" +( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "spu_id" bigint, + "total_limit_count" int NOT NULL, + "single_limit_count" int NOT NULL, + "start_time" varchar NOT NULL, + "end_time" varchar NOT NULL, + "user_size" int NOT NULL, + "total_num" int NOT NULL, + "success_num" int NOT NULL, + "order_user_count" int NOT NULL, + "virtual_group" int NOT NULL, + "status" int NOT NULL, + "limit_duration" int NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL, + PRIMARY KEY ("id") +) COMMENT '拼团活动'; \ No newline at end of file diff --git a/yunxi-module-mall/yunxi-module-trade-api/pom.xml b/yunxi-module-mall/yunxi-module-trade-api/pom.xml new file mode 100644 index 0000000..043ce55 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/pom.xml @@ -0,0 +1,26 @@ + + + + com.yunxi.scm + yunxi-module-mall + ${revision} + + 4.0.0 + yunxi-module-trade-api + jar + + ${project.artifactId} + + trade 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/order/TradeOrderApi.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/order/TradeOrderApi.java new file mode 100644 index 0000000..8eac3b4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/order/TradeOrderApi.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.trade.api.order; + +/** + * 订单 API 接口 + * + * @author HUIHUI + */ +public interface TradeOrderApi { + + /** + * 验证订单 + * + * @param userId 用户 id + * @param orderItemId 订单项 id + * @return 校验通过返回订单 id + */ + Long validateOrder(Long userId, Long orderItemId); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/order/dto/TradeOrderRespDTO.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/order/dto/TradeOrderRespDTO.java new file mode 100644 index 0000000..1136f17 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/order/dto/TradeOrderRespDTO.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.module.trade.api.order.dto; + +import com.yunxi.scm.framework.common.enums.TerminalEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderCancelTypeEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderStatusEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderTypeEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 订单信息 Response DTO + * + * @author HUIHUI + */ +@Data +public class TradeOrderRespDTO { + + // ========== 订单基本信息 ========== + /** + * 订单编号,主键自增 + */ + private Long id; + /** + * 订单流水号 + * + * 例如说,1146347329394184195 + */ + private String no; + /** + * 订单类型 + * + * 枚举 {@link TradeOrderTypeEnum} + */ + private Integer type; + /** + * 订单来源 + * + * 枚举 {@link TerminalEnum} + */ + private Integer terminal; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户 IP + */ + private String userIp; + /** + * 用户备注 + */ + private String userRemark; + /** + * 订单状态 + * + * 枚举 {@link TradeOrderStatusEnum} + */ + private Integer status; + /** + * 购买的商品数量 + */ + private Integer productCount; + /** + * 订单完成时间 + */ + private LocalDateTime finishTime; + /** + * 订单取消时间 + */ + private LocalDateTime cancelTime; + /** + * 取消类型 + * + * 枚举 {@link TradeOrderCancelTypeEnum} + */ + private Integer cancelType; + /** + * 商家备注 + */ + private String remark; + /** + * 是否评价 + */ + private Boolean commentStatus; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/package-info.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/package-info.java new file mode 100644 index 0000000..0dcb5e0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/api/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.trade.api; \ No newline at end of file diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/ErrorCodeConstants.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..560c9bc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/ErrorCodeConstants.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.trade.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Trade 错误码枚举类 + * trade 系统,使用 1-011-000-000 段 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface ErrorCodeConstants { + + // ========== Order 模块 1011000000 ========== + ErrorCode ORDER_CREATE_SKU_NOT_FOUND = new ErrorCode(1011000001, "商品 SKU 不存在"); + ErrorCode ORDER_CREATE_SPU_NOT_SALE = new ErrorCode(1011000002, "商品 SPU 不可售卖"); + ErrorCode ORDER_CREATE_SKU_STOCK_NOT_ENOUGH = new ErrorCode(1011000004, "商品 SKU 库存不足"); + ErrorCode ORDER_CREATE_SPU_NOT_FOUND = new ErrorCode(1011000005, "商品 SPU 不可售卖"); + ErrorCode ORDER_CREATE_ADDRESS_NOT_FOUND = new ErrorCode(1011000006, "收货地址不存在"); + + ErrorCode ORDER_ITEM_NOT_FOUND = new ErrorCode(1011000010, "交易订单项不存在"); + ErrorCode ORDER_NOT_FOUND = new ErrorCode(1011000011, "交易订单不存在"); + ErrorCode ORDER_ITEM_UPDATE_AFTER_SALE_STATUS_FAIL = new ErrorCode(1011000012, "交易订单项更新售后状态失败,请重试"); + ErrorCode ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1011000013, "交易订单更新支付状态失败,订单不是【未支付】状态"); + ErrorCode ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR = new ErrorCode(1011000014, "交易订单更新支付状态失败,支付单编号不匹配"); + ErrorCode ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1011000015, "交易订单更新支付状态失败,支付单状态不是【支付成功】状态"); + ErrorCode ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1011000016, "交易订单更新支付状态失败,支付单金额不匹配"); + ErrorCode ORDER_DELIVERY_FAIL_STATUS_NOT_UNDELIVERED = new ErrorCode(1011000017, "交易订单发货失败,订单不是【待发货】状态"); + ErrorCode ORDER_RECEIVE_FAIL_STATUS_NOT_DELIVERED = new ErrorCode(1011000018, "交易订单收货失败,订单不是【待收货】状态"); + ErrorCode ORDER_COMMENT_FAIL_STATUS_NOT_COMPLETED = new ErrorCode(1011000019, "创建交易订单项的评价失败,订单不是【已完成】状态"); + ErrorCode ORDER_COMMENT_STATUS_NOT_FALSE = new ErrorCode(1011000020, "创建交易订单项的评价失败,订单已评价"); + ErrorCode ORDER_DELIVERY_FAIL_REFUND_STATUS_NOT_NONE = new ErrorCode(1011000021, "交易订单发货失败,订单已退款或部分退款"); + ErrorCode ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS = new ErrorCode(1011000022, "交易订单发货失败,拼团未成功"); + + // ========== After Sale 模块 1011000100 ========== + ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1011000100, "售后单不存在"); + ErrorCode AFTER_SALE_CREATE_FAIL_REFUND_PRICE_ERROR = new ErrorCode(1011000101, "申请退款金额错误"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_CANCELED = new ErrorCode(1011000102, "订单已关闭,无法申请售后"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_PAID = new ErrorCode(1011000103, "订单未支付,无法申请售后"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_DELIVERED = new ErrorCode(1011000104, "订单未发货,无法申请【退货退款】售后"); + ErrorCode AFTER_SALE_CREATE_FAIL_ORDER_ITEM_APPLIED = new ErrorCode(1011000105, "订单项已申请售后,无法重复申请"); + ErrorCode AFTER_SALE_AUDIT_FAIL_STATUS_NOT_APPLY = new ErrorCode(1011000106, "审批失败,售后状态不处于审批中"); + ErrorCode AFTER_SALE_UPDATE_STATUS_FAIL = new ErrorCode(1011000107, "操作售后单失败,请刷新后重试"); + ErrorCode AFTER_SALE_DELIVERY_FAIL_STATUS_NOT_SELLER_AGREE = new ErrorCode(1011000108, "退货失败,售后单状态不处于【待买家退货】"); + ErrorCode AFTER_SALE_CONFIRM_FAIL_STATUS_NOT_BUYER_DELIVERY = new ErrorCode(1011000109, "确认收货失败,售后单状态不处于【待确认收货】"); + ErrorCode AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND = new ErrorCode(1011000110, "退款失败,售后单状态不是【待退款】"); + ErrorCode AFTER_SALE_CANCEL_FAIL_STATUS_NOT_APPLY_OR_AGREE = new ErrorCode(1011000111, "取消售后单失败,售后单状态不是【待审核】或【卖家同意】"); + + // ========== Cart 模块 1011002000 ========== + ErrorCode CARD_ITEM_NOT_FOUND = new ErrorCode(1011002000, "购物车项不存在"); + + // ========== Price 相关 1011003000 ============ + ErrorCode PRICE_CALCULATE_PAY_PRICE_ILLEGAL = new ErrorCode(1011003000, "支付价格计算异常,原因:价格小于等于 0"); + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_USER_ADDR_IS_EMPTY = new ErrorCode(1011003001, "计算快递运费异常,收件人地址编号为空"); + ErrorCode PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND = new ErrorCode(1011003002, "计算快递运费异常,找不到对应的运费模板"); + + // ========== 物流 Express 模块 1011004000 ========== + ErrorCode EXPRESS_NOT_EXISTS = new ErrorCode(1011004000, "快递公司不存在"); + ErrorCode EXPRESS_CODE_DUPLICATE = new ErrorCode(1011004001, "已经存在该编码的快递公司"); + ErrorCode EXPRESS_CLIENT_NOT_PROVIDE = new ErrorCode(1011004002, "需要接入快递服务商,比如【快递100】"); + + ErrorCode EXPRESS_API_QUERY_ERROR = new ErrorCode(1011004101, "快递查询接口异常"); + ErrorCode EXPRESS_API_QUERY_FAILED = new ErrorCode(1011004102, "快递查询返回失败,原因:{}"); + + // ========== 物流 Template 模块 1011005000 ========== + ErrorCode EXPRESS_TEMPLATE_NAME_DUPLICATE = new ErrorCode(1011005000, "已经存在该运费模板名"); + ErrorCode EXPRESS_TEMPLATE_NOT_EXISTS = new ErrorCode(1011005001, "运费模板不存在"); + + // ========== 物流 PICK_UP 模块 1011006000 ========== + ErrorCode PICK_UP_STORE_NOT_EXISTS = new ErrorCode(1011006000, "自提门店不存在"); + + // ========== 物流 PICK_UP 模块 1011007000 ========== + // TODO @puhui999:这几个错误码,应该属于订单哈 + ErrorCode ORDER_DELIVERY_FAILED_ITEMS_NOT_EMPTY = new ErrorCode(1011007000, "订单发货失败,请选择发货商品"); + ErrorCode ORDER_DELIVERY_FAILED_ITEM_NOT_EXISTS = new ErrorCode(1011007001, "订单发货失败,所选发货商品不存在"); + ErrorCode ORDER_DELIVERY_FAILED_ITEM_ALREADY_DELIVERY = new ErrorCode(1011007002, "订单发货失败,所选商品已发货"); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/AfterSaleOperateTypeEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/AfterSaleOperateTypeEnum.java new file mode 100644 index 0000000..5e5ce34 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/AfterSaleOperateTypeEnum.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.trade.enums.aftersale; + +/** + * 售后操作类型的枚举 + * + * @author 陈賝 + * @since 2023/6/13 13:53 + */ +// TODO @chenchen:可以 lombok 简化构造方法,和 get 方法 +public enum AfterSaleOperateTypeEnum { + + /** + * 用户申请 + */ + APPLY("用户申请"), + ; + + private final String description; + + AfterSaleOperateTypeEnum(String description) { + this.description = description; + } + + public String description() { + return description; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleStatusEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleStatusEnum.java new file mode 100644 index 0000000..b62ecf4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleStatusEnum.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.trade.enums.aftersale; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +import static cn.hutool.core.util.ArrayUtil.firstMatch; + +/** + * 售后状态的枚举 + * + * 状态流转 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum TradeAfterSaleStatusEnum implements IntArrayValuable { + + /** + * 【申请售后】 + */ + APPLY(10,"申请中", "会员申请退款"), + /** + * 卖家通过售后;【商品待退货】 + */ + SELLER_AGREE(20, "卖家通过", "商家同意退款"), + /** + * 买家已退货,等待卖家收货;【商家待收货】 + */ + BUYER_DELIVERY(30,"待卖家收货", "会员填写退货物流信息"), + /** + * 卖家已收货,等待平台退款;等待退款【等待退款】 + */ + WAIT_REFUND(40, "等待平台退款", "商家收货"), + /** + * 完成退款【退款成功】 + */ + COMPLETE(50, "完成", "商家确认退款"), + /** + * 【买家取消】 + */ + BUYER_CANCEL(61, "买家取消售后", "会员取消退款"), + /** + * 卖家拒绝售后;商家拒绝【商家拒绝】 + */ + SELLER_DISAGREE(62,"卖家拒绝", "商家拒绝退款"), + /** + * 卖家拒绝收货,终止售后;【商家拒收货】 + */ + SELLER_REFUSE(63,"卖家拒绝收货", "商家拒绝收货"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeAfterSaleStatusEnum::getStatus).toArray(); + + /** + * 状态 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + /** + * 操作内容 + * + * 目的:记录售后日志的内容 + */ + private final String content; + + @Override + public int[] array() { + return ARRAYS; + } + + public static TradeAfterSaleStatusEnum valueOf(Integer status) { + return firstMatch(value -> value.getStatus().equals(status), values()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleTypeEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleTypeEnum.java new file mode 100644 index 0000000..ca0f63e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleTypeEnum.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.trade.enums.aftersale; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易售后 - 类型 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum TradeAfterSaleTypeEnum implements IntArrayValuable { + + IN_SALE(10, "售中退款"), // 交易完成前买家申请退款 + AFTER_SALE(20, "售后退款"); // 交易完成后买家申请退款 + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeAfterSaleTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleWayEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleWayEnum.java new file mode 100644 index 0000000..cb59f33 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/aftersale/TradeAfterSaleWayEnum.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.trade.enums.aftersale; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易售后 - 方式 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeAfterSaleWayEnum implements IntArrayValuable { + + REFUND(10, "仅退款"), + RETURN_AND_REFUND(20, "退货退款"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeAfterSaleWayEnum::getWay).toArray(); + + /** + * 方式 + */ + private final Integer way; + /** + * 方式名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java new file mode 100644 index 0000000..22098c6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/delivery/DeliveryExpressChargeModeEnum.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.trade.enums.delivery; + +import cn.hutool.core.util.ArrayUtil; +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 快递配送计费方式枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum DeliveryExpressChargeModeEnum implements IntArrayValuable { + + PIECE(1, "按件"), + WEIGHT(2,"按重量"), + VOLUME(3, "按体积"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DeliveryExpressChargeModeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + /** + * 描述 + */ + private final String desc; + + @Override + public int[] array() { + return ARRAYS; + } + + public static DeliveryExpressChargeModeEnum valueOf(Integer value) { + return ArrayUtil.firstMatch(chargeMode -> chargeMode.getType().equals(value), DeliveryExpressChargeModeEnum.values()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/delivery/DeliveryTypeEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/delivery/DeliveryTypeEnum.java new file mode 100644 index 0000000..0ed4f42 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/delivery/DeliveryTypeEnum.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.trade.enums.delivery; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 配送方式枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DeliveryTypeEnum implements IntArrayValuable { + + NULL(0, "无需物流"), + EXPRESS(1, "快递发货"), + PICK_UP(2, "用户自提"),; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(DeliveryTypeEnum::getMode).toArray(); + + /** + * 配送方式 + */ + private final Integer mode; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderCancelTypeEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderCancelTypeEnum.java new file mode 100644 index 0000000..7a5970c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderCancelTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.trade.enums.order; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单 - 关闭类型 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderCancelTypeEnum implements IntArrayValuable { + + PAY_TIMEOUT(10, "超时未支付"), + AFTER_SALE_CLOSE(20, "退款关闭"), + MEMBER_CANCEL(30, "买家取消"), + PAY_ON_DELIVERY(40, "已通过货到付款交易"),; // TODO 芋艿:这个类型,是不是可以去掉 + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderCancelTypeEnum::getType).toArray(); + + /** + * 关闭类型 + */ + private final Integer type; + /** + * 关闭类型名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderDeliveryStatusEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderDeliveryStatusEnum.java new file mode 100644 index 0000000..1392b57 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderDeliveryStatusEnum.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.trade.enums.order; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + * 交易订单 - 发货状态 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderDeliveryStatusEnum { + + UNDELIVERED(0, "未发货"), + DELIVERED(1, "已发货"), + RECEIVED(2, "已收货"); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderItemAfterSaleStatusEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderItemAfterSaleStatusEnum.java new file mode 100644 index 0000000..ea8b250 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderItemAfterSaleStatusEnum.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.trade.enums.order; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单项 - 售后状态 + * + * @author 芋道源码 + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderItemAfterSaleStatusEnum implements IntArrayValuable { + + NONE(0, "未售后"), + APPLY(10, "售后中"), + SUCCESS(20, "售后成功"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderItemAfterSaleStatusEnum::getStatus).toArray(); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + + /** + * 判断指定状态,是否正处于【未申请】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isNone(Integer status) { + return ObjectUtil.equals(status, NONE.getStatus()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderRefundStatusEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderRefundStatusEnum.java new file mode 100644 index 0000000..f95cf35 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderRefundStatusEnum.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.trade.enums.order; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单 - 退款状态 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderRefundStatusEnum implements IntArrayValuable { + + NONE(0, "未退款"), + PART(10, "部分退款"), + ALL(20, "全部退款"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderRefundStatusEnum::getStatus).toArray(); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderStatusEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderStatusEnum.java new file mode 100644 index 0000000..d04981b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderStatusEnum.java @@ -0,0 +1,116 @@ +package com.yunxi.scm.module.trade.enums.order; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单 - 状态 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderStatusEnum implements IntArrayValuable { + + UNPAID(0, "待支付"), + UNDELIVERED(10, "待发货"), + DELIVERED(20, "已发货"), + COMPLETED(30, "已完成"), + CANCELED(40, "已取消"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderStatusEnum::getStatus).toArray(); + + /** + * 状态值 + */ + private final Integer status; + /** + * 状态名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + + // ========== 问:为什么写了很多 isXXX 和 haveXXX 的判断逻辑呢? ========== + // ========== 答:方便找到某一类判断,哪些业务正在使用 ========== + + /** + * 判断指定状态,是否正处于【未付款】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isUnpaid(Integer status) { + return ObjectUtil.equal(UNPAID.getStatus(), status); + } + + /** + * 判断指定状态,是否正处于【待发货】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isUndelivered(Integer status) { + return ObjectUtil.equal(UNDELIVERED.getStatus(), status); + } + + /** + * 判断指定状态,是否正处于【已发货】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isDelivered(Integer status) { + return ObjectUtil.equals(status, DELIVERED.getStatus()); + } + + /** + * 判断指定状态,是否正处于【已取消】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isCanceled(Integer status) { + return ObjectUtil.equals(status, CANCELED.getStatus()); + } + + /** + * 判断指定状态,是否正处于【已完成】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean isCompleted(Integer status) { + return ObjectUtil.equals(status, COMPLETED.getStatus()); + } + + /** + * 判断指定状态,是否有过【已付款】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean havePaid(Integer status) { + return ObjectUtils.equalsAny(status, UNDELIVERED.getStatus(), + DELIVERED.getStatus(), COMPLETED.getStatus()); + } + + /** + * 判断指定状态,是否有过【已发货】状态 + * + * @param status 指定状态 + * @return 是否 + */ + public static boolean haveDelivered(Integer status) { + return ObjectUtils.equalsAny(status, DELIVERED.getStatus(), COMPLETED.getStatus()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderTypeEnum.java b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderTypeEnum.java new file mode 100644 index 0000000..a9eb69c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-api/src/main/java/com/yunxi/scm/module/trade/enums/order/TradeOrderTypeEnum.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.trade.enums.order; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; + +/** + * 交易订单 - 类型 + * + * @author Sin + */ +@RequiredArgsConstructor +@Getter +public enum TradeOrderTypeEnum implements IntArrayValuable { + + NORMAL(0, "普通订单"), + SECKILL(1, "秒杀订单"), + BARGAIN(2, "砍价订单"), + COMBINATION(3, "拼团订单"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(TradeOrderTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型名 + */ + private final String name; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/pom.xml b/yunxi-module-mall/yunxi-module-trade-biz/pom.xml new file mode 100644 index 0000000..c54ec53 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/pom.xml @@ -0,0 +1,103 @@ + + + + com.yunxi.scm + yunxi-module-mall + ${revision} + + 4.0.0 + yunxi-module-trade-biz + jar + + ${project.artifactId} + + trade 模块,主要实现交易相关功能 + 例如:订单、退款、购物车等功能。 + + + + + com.yunxi.scm + yunxi-module-trade-api + ${revision} + + + com.yunxi.scm + yunxi-module-product-api + ${revision} + + + com.yunxi.scm + yunxi-module-pay-api + ${revision} + + + com.yunxi.scm + yunxi-module-promotion-api + ${revision} + + + com.yunxi.scm + yunxi-module-member-api + ${revision} + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-ip + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-pay + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-dict + + + + + diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/api/order/TradeOrderApiImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/api/order/TradeOrderApiImpl.java new file mode 100644 index 0000000..0e4f3e6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/api/order/TradeOrderApiImpl.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.trade.api.order; + +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.service.order.TradeOrderService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.ORDER_ITEM_NOT_FOUND; + +/** + * 订单 API 接口实现类 + * + * @author HUIHUI + */ +@Service +@Validated +public class TradeOrderApiImpl implements TradeOrderApi { + + @Resource + private TradeOrderService tradeOrderService; + + @Override + public Long validateOrder(Long userId, Long orderItemId) { + // 校验订单项,订单项存在订单就存在 + TradeOrderItemDO item = tradeOrderService.getOrderItem(userId, orderItemId); + if (item == null) { + throw exception(ORDER_ITEM_NOT_FOUND); + } + return item.getOrderId(); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/api/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/api/package-info.java new file mode 100644 index 0000000..0dcb5e0 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/api/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.trade.api; \ No newline at end of file diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/TradeAfterSaleController.http b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/TradeAfterSaleController.http new file mode 100644 index 0000000..d1a2aca --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/TradeAfterSaleController.http @@ -0,0 +1,4 @@ +### 获得交易售后分页 => 成功 +GET {{baseUrl}}/trade/after-sale/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/TradeAfterSaleController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/TradeAfterSaleController.java new file mode 100644 index 0000000..7a02c9b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/TradeAfterSaleController.java @@ -0,0 +1,113 @@ +package com.yunxi.scm.module.trade.controller.admin.aftersale; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.api.user.MemberUserApi; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.product.api.property.ProductPropertyValueApi; +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSaleDisagreeReqVO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSalePageReqVO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSaleRefuseReqVO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSaleRespPageItemVO; +import com.yunxi.scm.module.trade.convert.aftersale.TradeAfterSaleConvert; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleDO; +import com.yunxi.scm.module.trade.service.aftersale.TradeAfterSaleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 交易售后") +@RestController +@RequestMapping("/trade/after-sale") +@Validated +@Slf4j +public class TradeAfterSaleController { + + @Resource + private TradeAfterSaleService afterSaleService; + + @Resource + private MemberUserApi memberUserApi; + @Resource + private ProductPropertyValueApi productPropertyValueApi; + + @GetMapping("/page") + @Operation(summary = "获得交易售后分页") + @PreAuthorize("@ss.hasPermission('trade:after-sale:query')") + public CommonResult> getAfterSalePage(@Valid TradeAfterSalePageReqVO pageVO) { + // 查询售后 + PageResult pageResult = afterSaleService.getAfterSalePage(pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 查询商品属性 + List propertyValueDetails = productPropertyValueApi + .getPropertyValueDetailList(TradeAfterSaleConvert.INSTANCE.convertPropertyValueIds(pageResult.getList())); + // 查询会员 + Map memberUsers = memberUserApi.getUserMap( + convertSet(pageResult.getList(), TradeAfterSaleDO::getUserId)); + return success(TradeAfterSaleConvert.INSTANCE.convertPage(pageResult, memberUsers, propertyValueDetails)); + } + + @PutMapping("/agree") + @Operation(summary = "同意售后") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:agree')") + public CommonResult agreeAfterSale(@RequestParam("id") Long id) { + afterSaleService.agreeAfterSale(getLoginUserId(), id); + return success(true); + } + + @PutMapping("/disagree") + @Operation(summary = "拒绝售后") + @PreAuthorize("@ss.hasPermission('trade:after-sale:disagree')") + public CommonResult disagreeAfterSale(@RequestBody TradeAfterSaleDisagreeReqVO confirmReqVO) { + afterSaleService.disagreeAfterSale(getLoginUserId(), confirmReqVO); + return success(true); + } + + @PutMapping("/receive") + @Operation(summary = "确认收货") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:receive')") + public CommonResult receiveAfterSale(@RequestParam("id") Long id) { + afterSaleService.receiveAfterSale(getLoginUserId(), id); + return success(true); + } + + @PutMapping("/refuse") + @Operation(summary = "确认收货") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:receive')") + public CommonResult refuseAfterSale(TradeAfterSaleRefuseReqVO refuseReqVO) { + afterSaleService.refuseAfterSale(getLoginUserId(), refuseReqVO); + return success(true); + } + + @PostMapping("/refund") + @Operation(summary = "确认退款") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:after-sale:refund')") + public CommonResult refundAfterSale(@RequestParam("id") Long id) { + afterSaleService.refundAfterSale(getLoginUserId(), getClientIP(), id); + return success(true); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleBaseVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleBaseVO.java new file mode 100644 index 0000000..ebce2f3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleBaseVO.java @@ -0,0 +1,119 @@ +package com.yunxi.scm.module.trade.controller.admin.aftersale.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** +* 交易售后 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class TradeAfterSaleBaseVO { + + @Schema(description = "售后流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "202211190847450020500077") + @NotNull(message = "售后流水号不能为空") + private String no; + + @Schema(description = "售后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "售后状态不能为空") + private Integer status; + + @Schema(description = "售后类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + @NotNull(message = "售后类型不能为空") + private Integer type; + + @Schema(description = "售后方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "售后方式不能为空") + private Integer way; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "30337") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "申请原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "不喜欢") + @NotNull(message = "申请原因不能为空") + private String applyReason; + + @Schema(description = "补充描述", example = "你说的对") + private String applyDescription; + + @Schema(description = "补充凭证图片", example = "https://www.iocoder.cn/1.png") + private List applyPicUrls; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18078") + @NotNull(message = "订单编号不能为空") + private Long orderId; + + @Schema(description = "订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2022111917190001") + @NotNull(message = "订单流水号不能为空") + private Long orderNo; + + @Schema(description = "订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "572") + @NotNull(message = "订单项编号不能为空") + private Long orderItemId; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2888") + @NotNull(message = "商品 SPU 编号不能为空") + private Long spuId; + + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotNull(message = "商品 SPU 名称不能为空") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15657") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "商品图片", example = "https://www.iocoder.cn/2.png") + private String picUrl; + + @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20012") + @NotNull(message = "购买数量不能为空") + private Integer count; + + @Schema(description = "审批时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime auditTime; + + @Schema(description = "审批人", example = "30835") + private Long auditUserId; + + @Schema(description = "审批备注", example = "不香") + private String auditReason; + + @Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "18077") + @NotNull(message = "退款金额,单位:分不能为空") + private Integer refundPrice; + + @Schema(description = "支付退款编号", example = "10271") + private Long payRefundId; + + @Schema(description = "退款时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime refundTime; + + @Schema(description = "退货物流公司编号", example = "10") + private Long logisticsId; + + @Schema(description = "退货物流单号", example = "610003952009") + private String logisticsNo; + + @Schema(description = "退货时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime deliveryTime; + + @Schema(description = "收货时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime receiveTime; + + @Schema(description = "收货备注", example = "不喜欢") + private String receiveReason; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleDisagreeReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleDisagreeReqVO.java new file mode 100644 index 0000000..47b592a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleDisagreeReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.trade.controller.admin.aftersale.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 交易售后拒绝 Request VO") +@Data +public class TradeAfterSaleDisagreeReqVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "售后编号不能为空") + private Long id; + + @Schema(description = "审批备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + @NotEmpty(message = "审批备注不能为空") + private String auditReason; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSalePageReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSalePageReqVO.java new file mode 100644 index 0000000..776c8b1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSalePageReqVO.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.trade.controller.admin.aftersale.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleStatusEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleTypeEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleWayEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 交易售后分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TradeAfterSalePageReqVO extends PageParam { + + @Schema(description = "售后流水号", example = "202211190847450020500077") + private String no; + + @Schema(description = "售后状态", example = "10") + @InEnum(value = TradeAfterSaleStatusEnum.class, message = "售后状态必须是 {value}") + private Integer status; + + @Schema(description = "售后类型", example = "20") + @InEnum(value = TradeAfterSaleTypeEnum.class, message = "售后类型必须是 {value}") + private Integer type; + + @Schema(description = "售后方式", example = "10") + @InEnum(value = TradeAfterSaleWayEnum.class, message = "售后方式必须是 {value}") + private Integer way; + + @Schema(description = "订单编号", example = "18078") + private String orderNo; + + @Schema(description = "商品 SPU 名称", example = "李四") + private String spuName; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleRefuseReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleRefuseReqVO.java new file mode 100644 index 0000000..a81b84c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleRefuseReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.trade.controller.admin.aftersale.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 交易售后拒绝收货 Request VO") +@Data +public class TradeAfterSaleRefuseReqVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "售后编号不能为空") + private Long id; + + @Schema(description = "收货备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + @NotNull(message = "收货备注不能为空") + private String refuseMemo; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleRespPageItemVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleRespPageItemVO.java new file mode 100644 index 0000000..8ea4d1c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/TradeAfterSaleRespPageItemVO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.trade.controller.admin.aftersale.vo; + +import com.yunxi.scm.module.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.yunxi.scm.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 交易售后分页的每一条记录 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TradeAfterSaleRespPageItemVO extends TradeAfterSaleBaseVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "27630") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + /** + * 商品属性数组 + */ + private List properties; + + /** + * 用户信息 + */ + private MemberUserRespVO user; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/log/TradeAfterSaleLogRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/log/TradeAfterSaleLogRespVO.java new file mode 100644 index 0000000..f289d55 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/aftersale/vo/log/TradeAfterSaleLogRespVO.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.trade.controller.admin.aftersale.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 交易售后日志 Response VO") +@Data +public class TradeAfterSaleLogRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20669") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "22634") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "用户类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3023") + @NotNull(message = "售后编号不能为空") + private Long afterSaleId; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25870") + @NotNull(message = "订单编号不能为空") + private Long orderId; + + @Schema(description = "订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23154") + @NotNull(message = "订单项编号不能为空") + private Long orderItemId; + + @Schema(description = "售后状态(之前)", example = "2") + private Integer beforeStatus; + + @Schema(description = "售后状态(之后)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "售后状态(之后)不能为空") + private Integer afterStatus; + + @Schema(description = "操作明细", requiredMode = Schema.RequiredMode.REQUIRED, example = "维权完成,退款金额:¥37776.00") + @NotNull(message = "操作明细不能为空") + private String content; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/member/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/member/package-info.java new file mode 100644 index 0000000..259405f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/member/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,可忽略 + */ +package com.yunxi.scm.module.trade.controller.admin.base.member; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/member/user/MemberUserRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/member/user/MemberUserRespVO.java new file mode 100644 index 0000000..cd926de --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/member/user/MemberUserRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.trade.controller.admin.base.member.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 会员用户 Response VO") +@Data +public class MemberUserRespVO { + + @Schema(description = "用户 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String nickname; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/package-info.java new file mode 100644 index 0000000..3c259a8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/package-info.java @@ -0,0 +1,4 @@ +/** + * 放置该模块通用的 VO 类 + */ +package com.yunxi.scm.module.trade.controller.admin.base; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/product/property/ProductPropertyValueDetailRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/product/property/ProductPropertyValueDetailRespVO.java new file mode 100644 index 0000000..46e0c14 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/base/product/property/ProductPropertyValueDetailRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.trade.controller.admin.base.product.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 商品属性值的明细 Response VO") +@Data +public class ProductPropertyValueDetailRespVO { + + @Schema(description = "属性的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long propertyId; + + @Schema(description = "属性的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") + private String propertyName; + + @Schema(description = "属性值的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long valueId; + + @Schema(description = "属性值的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") + private String valueName; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryExpressController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryExpressController.java new file mode 100644 index 0000000..d0e02c9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryExpressController.java @@ -0,0 +1,89 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.*; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressUpdateReqVO; +import com.yunxi.scm.module.trade.convert.delivery.DeliveryExpressConvert; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.yunxi.scm.module.trade.service.delivery.DeliveryExpressService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 快递公司") +@RestController +@RequestMapping("/trade/delivery/express") +@Validated +public class DeliveryExpressController { + + @Resource + private DeliveryExpressService deliveryExpressService; + + @PostMapping("/create") + @Operation(summary = "创建快递公司") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:create')") + public CommonResult createDeliveryExpress(@Valid @RequestBody DeliveryExpressCreateReqVO createReqVO) { + return success(deliveryExpressService.createDeliveryExpress(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新快递公司") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:update')") + public CommonResult updateDeliveryExpress(@Valid @RequestBody DeliveryExpressUpdateReqVO updateReqVO) { + deliveryExpressService.updateDeliveryExpress(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除快递公司") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('trade:delivery:express:delete')") + public CommonResult deleteDeliveryExpress(@RequestParam("id") Long id) { + deliveryExpressService.deleteDeliveryExpress(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得快递公司") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:query')") + public CommonResult getDeliveryExpress(@RequestParam("id") Long id) { + DeliveryExpressDO deliveryExpress = deliveryExpressService.getDeliveryExpress(id); + return success(DeliveryExpressConvert.INSTANCE.convert(deliveryExpress)); + } + + @GetMapping("/page") + @Operation(summary = "获得快递公司分页") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:query')") + public CommonResult> getDeliveryExpressPage(@Valid DeliveryExpressPageReqVO pageVO) { + PageResult pageResult = deliveryExpressService.getDeliveryExpressPage(pageVO); + return success(DeliveryExpressConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出快递公司 Excel") + @PreAuthorize("@ss.hasPermission('trade:delivery:express:export')") + @OperateLog(type = EXPORT) + public void exportDeliveryExpressExcel(@Valid DeliveryExpressExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = deliveryExpressService.getDeliveryExpressList(exportReqVO); + // 导出 Excel + List dataList = DeliveryExpressConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "快递公司.xls", "数据", DeliveryExpressExcelVO.class, dataList); + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryExpressTemplateController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryExpressTemplateController.java new file mode 100644 index 0000000..2b58f58 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryExpressTemplateController.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.*; +import com.yunxi.scm.module.trade.convert.delivery.DeliveryExpressTemplateConvert; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import com.yunxi.scm.module.trade.service.delivery.DeliveryExpressTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 快递运费模板") +@RestController +@RequestMapping("/trade/delivery/express-template") +@Validated +public class DeliveryExpressTemplateController { + + @Resource + private DeliveryExpressTemplateService deliveryExpressTemplateService; + + @PostMapping("/create") + @Operation(summary = "创建快递运费模板") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:create')") + public CommonResult createDeliveryExpressTemplate(@Valid @RequestBody DeliveryExpressTemplateCreateReqVO createReqVO) { + return success(deliveryExpressTemplateService.createDeliveryExpressTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新快递运费模板") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:update')") + public CommonResult updateDeliveryExpressTemplate(@Valid @RequestBody DeliveryExpressTemplateUpdateReqVO updateReqVO) { + deliveryExpressTemplateService.updateDeliveryExpressTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除快递运费模板") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:delete')") + public CommonResult deleteDeliveryExpressTemplate(@RequestParam("id") Long id) { + deliveryExpressTemplateService.deleteDeliveryExpressTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得快递运费模板") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:query')") + public CommonResult getDeliveryExpressTemplate(@RequestParam("id") Long id) { + return success(deliveryExpressTemplateService.getDeliveryExpressTemplate(id)); + } + + @GetMapping("/list") + @Operation(summary = "获得快递运费模板列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:query')") + public CommonResult> getDeliveryExpressTemplateList(@RequestParam("ids") Collection ids) { + List list = deliveryExpressTemplateService.getDeliveryExpressTemplateList(ids); + return success(DeliveryExpressTemplateConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取快递模版精简信息列表", description = "主要用于前端的下拉选项") + public CommonResult> getSimpleTemplateList() { + // 获取运费模版列表,只要开启状态的 + List list = deliveryExpressTemplateService.getDeliveryExpressTemplateList(); + // 排序后,返回给前端 + return success(DeliveryExpressTemplateConvert.INSTANCE.convertList1(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得快递运费模板分页") + @PreAuthorize("@ss.hasPermission('trade:delivery:express-template:query')") + public CommonResult> getDeliveryExpressTemplatePage(@Valid DeliveryExpressTemplatePageReqVO pageVO) { + PageResult pageResult = deliveryExpressTemplateService.getDeliveryExpressTemplatePage(pageVO); + return success(DeliveryExpressTemplateConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryPickUpStoreController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryPickUpStoreController.java new file mode 100644 index 0000000..4cfd616 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/DeliveryPickUpStoreController.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.*; +import com.yunxi.scm.module.trade.convert.delivery.DeliveryPickUpStoreConvert; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import com.yunxi.scm.module.trade.service.delivery.DeliveryPickUpStoreService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 自提门店") +@RestController +@RequestMapping("/trade/delivery/pick-up-store") +@Validated +public class DeliveryPickUpStoreController { + + @Resource + private DeliveryPickUpStoreService deliveryPickUpStoreService; + + @PostMapping("/create") + @Operation(summary = "创建自提门店") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:create')") + public CommonResult createDeliveryPickUpStore(@Valid @RequestBody DeliveryPickUpStoreCreateReqVO createReqVO) { + return success(deliveryPickUpStoreService.createDeliveryPickUpStore(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新自提门店") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:update')") + public CommonResult updateDeliveryPickUpStore(@Valid @RequestBody DeliveryPickUpStoreUpdateReqVO updateReqVO) { + deliveryPickUpStoreService.updateDeliveryPickUpStore(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除自提门店") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:delete')") + public CommonResult deleteDeliveryPickUpStore(@RequestParam("id") Long id) { + deliveryPickUpStoreService.deleteDeliveryPickUpStore(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得自提门店") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:query')") + public CommonResult getDeliveryPickUpStore(@RequestParam("id") Long id) { + DeliveryPickUpStoreDO deliveryPickUpStore = deliveryPickUpStoreService.getDeliveryPickUpStore(id); + return success(DeliveryPickUpStoreConvert.INSTANCE.convert(deliveryPickUpStore)); + } + + @GetMapping("/list") + @Operation(summary = "获得自提门店列表") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:query')") + public CommonResult> getDeliveryPickUpStoreList(@RequestParam("ids") Collection ids) { + List list = deliveryPickUpStoreService.getDeliveryPickUpStoreList(ids); + return success(DeliveryPickUpStoreConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得自提门店分页") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:query')") + public CommonResult> getDeliveryPickUpStorePage(@Valid DeliveryPickUpStorePageReqVO pageVO) { + PageResult pageResult = deliveryPickUpStoreService.getDeliveryPickUpStorePage(pageVO); + return success(DeliveryPickUpStoreConvert.INSTANCE.convertPage(pageResult)); + } + + // TODO @jason:导出去掉好列;简化下,一般用不到哈。 + @GetMapping("/export-excel") + @Operation(summary = "导出自提门店 Excel") + @PreAuthorize("@ss.hasPermission('trade:delivery:pick-up-store:export')") + @OperateLog(type = EXPORT) + public void exportDeliveryPickUpStoreExcel(@Valid DeliveryPickUpStoreExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = deliveryPickUpStoreService.getDeliveryPickUpStoreList(exportReqVO); + // 导出 Excel + List datas = DeliveryPickUpStoreConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "自提门店.xls", "数据", DeliveryPickUpStoreExcelVO.class, datas); + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressBaseVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressBaseVO.java new file mode 100644 index 0000000..a0ecbf8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressBaseVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.express; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 快递公司 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DeliveryExpressBaseVO { + + @Schema(description = "快递公司编码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "快递公司编码不能为空") + private String code; + + @Schema(description = "快递公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotNull(message = "快递公司名称不能为空") + private String name; + + @Schema(description = "快递公司logo") + private String logo; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "排序不能为空") + private Integer sort; + + @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressCreateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressCreateReqVO.java new file mode 100644 index 0000000..243856a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressCreateReqVO.java @@ -0,0 +1,12 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.express; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "管理后台 - 快递公司创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressCreateReqVO extends DeliveryExpressBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java new file mode 100644 index 0000000..96b62a1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.express; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 快递公司 Excel VO + */ +@Data +public class DeliveryExpressExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("快递公司编码") + private String code; + + @ExcelProperty("快递公司名称") + private String name; + + @ExcelProperty("快递公司 logo") + private String logo; + + @ExcelProperty("排序") + private Integer sort; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExportReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExportReqVO.java new file mode 100644 index 0000000..c181df2 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExportReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.express; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 快递公司 Excel 导出 Request VO") +@Data +public class DeliveryExpressExportReqVO { + + @Schema(description = "快递公司编码") + private String code; + + @Schema(description = "快递公司名称", example = "李四") + private String name; + + @Schema(description = "状态(0正常 1停用)", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressPageReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressPageReqVO.java new file mode 100644 index 0000000..74779f6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressPageReqVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.express; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import com.yunxi.scm.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 快递公司分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressPageReqVO extends PageParam { + + @Schema(description = "快递公司编码") + private String code; + + @Schema(description = "快递公司名称", example = "李四") + private String name; + + @Schema(description = "状态(0正常 1停用)", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressRespVO.java new file mode 100644 index 0000000..966c0f9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.express; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 快递公司 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressRespVO extends DeliveryExpressBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6592") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressUpdateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressUpdateReqVO.java new file mode 100644 index 0000000..4a3aba1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/express/DeliveryExpressUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.express; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 快递公司更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressUpdateReqVO extends DeliveryExpressBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6592") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateBaseVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateBaseVO.java new file mode 100644 index 0000000..f3c8aef --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateBaseVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 快递运费模板 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DeliveryExpressTemplateBaseVO { + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotNull(message = "模板名称不能为空") + private String name; + + @Schema(description = "配送计费方式 1:按件 2:按重量 3:按体积", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "配送计费方式 1:按件 2:按重量 3:按体积不能为空") + private Integer chargeMode; + + @Schema(description = "排序", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "排序不能为空") + private Integer sort; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateCreateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateCreateReqVO.java new file mode 100644 index 0000000..ef0c43e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateCreateReqVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import java.util.Collections; +import java.util.List; + +@Schema(description = "管理后台 - 快递运费模板创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressTemplateCreateReqVO extends DeliveryExpressTemplateBaseVO { + + @Schema(description = "区域运费列表") + @Valid + private List templateCharge; + + @Schema(description = "包邮区域列表") + @Valid + private List templateFree; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateDetailRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateDetailRespVO.java new file mode 100644 index 0000000..703a6d4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateDetailRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +@Schema(description = "管理后台 - 快递运费模板的详细 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressTemplateDetailRespVO extends DeliveryExpressTemplateBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "371") + private Long id; + + @Schema(description = "运费模板运费设置", requiredMode = Schema.RequiredMode.REQUIRED) + private List templateCharge; + + @Schema(description = "运费模板包邮区域", requiredMode = Schema.RequiredMode.REQUIRED) + private List templateFree; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplatePageReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplatePageReqVO.java new file mode 100644 index 0000000..30ca149 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplatePageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 快递运费模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressTemplatePageReqVO extends PageParam { + + @Schema(description = "模板名称", example = "王五") + private String name; + + @Schema(description = "配送计费方式 1:按件 2:按重量 3:按体积") + private Integer chargeMode; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateRespVO.java new file mode 100644 index 0000000..32e6685 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 快递运费模板 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressTemplateRespVO extends DeliveryExpressTemplateBaseVO { + + @Schema(description = "编号,自增", requiredMode = Schema.RequiredMode.REQUIRED, example = "371") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateSimpleRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateSimpleRespVO.java new file mode 100644 index 0000000..3cac1de --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateSimpleRespVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + + +@Schema(description = "管理后台 - 模版精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeliveryExpressTemplateSimpleRespVO { + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试模版") + private String name; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateUpdateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateUpdateReqVO.java new file mode 100644 index 0000000..d51337e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/DeliveryExpressTemplateUpdateReqVO.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 快递运费模板更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryExpressTemplateUpdateReqVO extends DeliveryExpressTemplateBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "371") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "区域运费列表") + @Valid + private List templateCharge; + + @Schema(description = "包邮区域列表") + @Valid + private List templateFree; + + @Schema(description = "管理后台 - 快递运费模板区域运费更新 Request VO") + @Data + public static class ExpressTemplateChargeUpdateVO extends ExpressTemplateChargeBaseVO { + + @Schema(description = "编号", example = "6592") + private Long id; + + // TODO @jason:这几个字段,应该不通过前端传递,而是后端查询后去赋值的 + @Schema(description = "配送模板编号", example = "1") + private Long templateId; + + @Schema(description = "配送计费方式", example = "1") + private Integer chargeMode; + + } + + @Schema(description = "管理后台 - 快递运费模板包邮区域更新 Request VO") + @Data + public static class ExpressTemplateFreeUpdateVO extends ExpressTemplateFreeBaseVO { + + @Schema(description = "编号", example = "6592") + private Long id; + + @Schema(description = "配送模板编号", example = "1") + private Long templateId; + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/ExpressTemplateChargeBaseVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/ExpressTemplateChargeBaseVO.java new file mode 100644 index 0000000..4b9bcb5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/ExpressTemplateChargeBaseVO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 快递运费模板运费设置 Base VO,提供给添加运费模板使用 + */ +@Data +public class ExpressTemplateChargeBaseVO { + + @Schema(description = "区域编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,120000]") + @NotEmpty(message = "区域编号列表不能为空") + private List areaIds; + + @Schema(description = "首件数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "首件数量不能为空") + private Double startCount; + + @Schema(description = "起步价", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + @NotNull(message = "起步价不能为空") + private Integer startPrice; + + @Schema(description = "续件数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "续件数量不能为空") + private Double extraCount; + + @Schema(description = "额外价", requiredMode = Schema.RequiredMode.REQUIRED, example = "2000") + @NotNull(message = "额外价不能为空") + private Integer extraPrice; +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/ExpressTemplateFreeBaseVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/ExpressTemplateFreeBaseVO.java new file mode 100644 index 0000000..935cc9d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/expresstemplate/ExpressTemplateFreeBaseVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 快递运费模板包邮 Base VO,提供给添加运费模板使用 + */ +@Data +public class ExpressTemplateFreeBaseVO { + + @Schema(description = "区域编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,120000]") + @NotEmpty(message = "区域编号列表不能为空") + private List areaIds; + + @Schema(description = "包邮金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "5000") + @NotNull(message = "包邮金额不能为空") + private Integer freePrice; + + @Schema(description = "包邮件数", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "包邮件数不能为空") + private Integer freeCount; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreBaseVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreBaseVO.java new file mode 100644 index 0000000..bd403d8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreBaseVO.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.time.LocalTime; + +/** +* 自提门店 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class DeliveryPickUpStoreBaseVO { + + @Schema(description = "门店名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + @NotBlank(message = "门店名称不能为空") + private String name; + + @Schema(description = "门店简介", example = "我是门店简介") + private String introduction; + + @Schema(description = "门店手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601892312") + @NotBlank(message = "门店手机不能为空") + @Mobile + private String phone; + + @Schema(description = "区域编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18733") + @NotNull(message = "区域编号不能为空") + private Integer areaId; + + @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号") + @NotBlank(message = "门店详细地址不能为空") + private String detailAddress; + + @Schema(description = "门店 logo", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + @NotBlank(message = "门店 logo 不能为空") + private String logo; + + @Schema(description = "营业开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "营业开始时间不能为空") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") + private LocalTime openingTime; + + @Schema(description = "营业结束时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "营业结束时间不能为空") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "HH:mm") + private LocalTime closingTime; + + @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "5.88") + @NotNull(message = "纬度不能为空") + private Double latitude; + + @Schema(description = "经度", requiredMode = Schema.RequiredMode.REQUIRED, example = "6.99") + @NotNull(message = "经度不能为空") + private Double longitude; + + @Schema(description = "门店状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "门店状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreCreateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreCreateReqVO.java new file mode 100644 index 0000000..32286b1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup; + +import lombok.*; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 自提门店创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryPickUpStoreCreateReqVO extends DeliveryPickUpStoreBaseVO { + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreExcelVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreExcelVO.java new file mode 100644 index 0000000..d26f64f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreExcelVO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +public class DeliveryPickUpStoreExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("门店名称") + private String name; + + @ExcelProperty("门店简介") + private String introduction; + + @ExcelProperty("门店手机") + private String phone; + + @ExcelProperty("门店所在区域") + private String areaName; + + @ExcelProperty("门店详细地址") + private String detailAddress; + + @ExcelProperty("门店logo") + private String logo; + + // TODO @jason:是不是可以加个 convert? + /** + * easy-excel 好像暂时不支持 LocalTime. 转成string + */ + @ExcelProperty("营业开始时间") + private String openingTime; + + @ExcelProperty("营业结束时间") + private String closingTime; + + @ExcelProperty("纬度") + private String latitude; + + @ExcelProperty("经度") + private String longitude; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreExportReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreExportReqVO.java new file mode 100644 index 0000000..74914bb --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreExportReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 自提门店 Excel 导出 Request VO,参数和 DeliveryPickUpStorePageReqVO 是一致的") +@Data +public class DeliveryPickUpStoreExportReqVO { + + @Schema(description = "门店名称", example = "李四") + private String name; + + @Schema(description = "门店手机") + private String phone; + + @Schema(description = "区域id", example = "18733") + private Integer areaId; + + @Schema(description = "门店状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStorePageReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStorePageReqVO.java new file mode 100644 index 0000000..63770b6 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStorePageReqVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import lombok.*; + +import java.time.LocalTime; +import java.util.*; +import io.swagger.v3.oas.annotations.media.Schema; +import com.yunxi.scm.framework.common.pojo.PageParam; +import org.springframework.format.annotation.DateTimeFormat; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_HOUR_MINUTE_SECOND; +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 自提门店分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryPickUpStorePageReqVO extends PageParam { + + @Schema(description = "门店名称", example = "李四") + private String name; + + @Schema(description = "门店手机") + private String phone; + + @Schema(description = "区域编号", example = "18733") + private Integer areaId; + + @Schema(description = "门店状态", example = "1") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreRespVO.java new file mode 100644 index 0000000..b8f412d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 自提门店 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryPickUpStoreRespVO extends DeliveryPickUpStoreBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreUpdateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreUpdateReqVO.java new file mode 100644 index 0000000..be1cfc5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/delivery/vo/pickup/DeliveryPickUpStoreUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 自提门店更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeliveryPickUpStoreUpdateReqVO extends DeliveryPickUpStoreBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/TradeOrderController.http b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/TradeOrderController.http new file mode 100644 index 0000000..0bf8812 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/TradeOrderController.http @@ -0,0 +1,9 @@ +### 获得交易订单分页 => 成功 +GET {{baseUrl}}/trade/order/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 获得交易订单分页 => 成功 +GET {{baseUrl}}/trade/order/get-detail?id=21 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/TradeOrderController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/TradeOrderController.java new file mode 100644 index 0000000..33856b9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/TradeOrderController.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.module.trade.controller.admin.order; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.api.user.MemberUserApi; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.product.api.property.ProductPropertyValueApi; +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderDetailRespVO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderPageItemRespVO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderPageReqVO; +import com.yunxi.scm.module.trade.convert.order.TradeOrderConvert; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.service.order.TradeOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 交易订单") +@RestController +@RequestMapping("/trade/order") +@Validated +@Slf4j +public class TradeOrderController { + + @Resource + private TradeOrderService tradeOrderService; + + @Resource + private ProductPropertyValueApi productPropertyValueApi; + @Resource + private MemberUserApi memberUserApi; + + @GetMapping("/page") + @Operation(summary = "获得交易订单分页") + @PreAuthorize("@ss.hasPermission('trade:order:query')") + public CommonResult> getOrderPage(TradeOrderPageReqVO reqVO) { + // 查询订单 + PageResult pageResult = tradeOrderService.getOrderPage(reqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 查询用户信息 + Map userMap = memberUserApi.getUserMap(convertSet(pageResult.getList(), TradeOrderDO::getUserId));; + // 查询订单项 + List orderItems = tradeOrderService.getOrderItemListByOrderId( + convertSet(pageResult.getList(), TradeOrderDO::getId)); + // 查询商品属性 + List propertyValueDetails = productPropertyValueApi + .getPropertyValueDetailList(TradeOrderConvert.INSTANCE.convertPropertyValueIds(orderItems)); + // 最终组合 + return success(TradeOrderConvert.INSTANCE.convertPage(pageResult, orderItems, propertyValueDetails, userMap)); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得交易订单详情") + @Parameter(name = "id", description = "订单编号", required = true, example = "1") + @PreAuthorize("@ss.hasPermission('trade:order:query')") + public CommonResult getOrderDetail(@RequestParam("id") Long id) { + // 查询订单 + TradeOrderDO order = tradeOrderService.getOrder(id); + // 查询订单项 + List orderItems = tradeOrderService.getOrderItemListByOrderId(id); + // 查询商品属性 + List propertyValueDetails = productPropertyValueApi + .getPropertyValueDetailList(TradeOrderConvert.INSTANCE.convertPropertyValueIds(orderItems)); + // 查询会员 + MemberUserRespDTO user = memberUserApi.getUser(order.getUserId()); + // 最终组合 + return success(TradeOrderConvert.INSTANCE.convert(order, orderItems, propertyValueDetails, user)); + } + + @PostMapping("/delivery") + @Operation(summary = "发货订单") + @PreAuthorize("@ss.hasPermission('trade:order:delivery')") + public CommonResult deliveryOrder(@RequestBody TradeOrderDeliveryReqVO deliveryReqVO) { + tradeOrderService.deliveryOrder(getLoginUserId(), deliveryReqVO); + return success(true); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderBaseVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderBaseVO.java new file mode 100644 index 0000000..e6f5543 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderBaseVO.java @@ -0,0 +1,138 @@ +package com.yunxi.scm.module.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** +* 交易订单 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class TradeOrderBaseVO { + + // ========== 订单基本信息 ========== + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1146347329394184195") + private String no; + + @Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "订单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "订单来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer terminal; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long userId; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + private String userIp; + + @Schema(description = "用户备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + private String userRemark; + + @Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "购买的商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer productCount; + + @Schema(description = "订单完成时间") + private LocalDateTime finishTime; + + @Schema(description = "订单取消时间") + private LocalDateTime cancelTime; + + @Schema(description = "取消类型", example = "10") + private Integer cancelType; + + @Schema(description = "商家备注", example = "你猜一下") + private String remark; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long payOrderId; + + @Schema(description = "是否已支付", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean payStatus; + + @Schema(description = "付款时间") + private LocalDateTime payTime; + + @Schema(description = "支付渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_lite") + private String payChannelCode; + + @Schema(description = "商品原价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer totalPrice; + + @Schema(description = "订单优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer discountPrice; + + @Schema(description = "运费金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer deliveryPrice; + + @Schema(description = "订单调价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer adjustPrice; + + @Schema(description = "应付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer payPrice; + + // ========== 收件 + 物流基本信息 ========== + + @Schema(description = "配送模板编号", example = "1024") + private Long deliveryTemplateId; + + @Schema(description = "发货物流公司编号", example = "1024") + private Long logisticsId; + + @Schema(description = "发货物流单号", example = "1024") + private String logisticsNo; + + @Schema(description = "发货状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer deliveryStatus; + + @Schema(description = "发货时间") + private LocalDateTime deliveryTime; + + @Schema(description = "收货时间") + private LocalDateTime receiveTime; + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + private String receiverName; + + @Schema(description = "收件人手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000") + private String receiverMobile; + + @Schema(description = "收件人地区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") + private Integer receiverAreaId; + + @Schema(description = "收件人详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "中关村大街 1 号") + private String receiverDetailAddress; + + // ========== 售后基本信息 ========== + + @Schema(description = "售后状态", example = "1") + private Integer afterSaleStatus; + + @Schema(description = "退款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer refundPrice; + + // ========== 营销基本信息 ========== + + @Schema(description = "优惠劵编号", example = "1024") + private Long couponId; + + @Schema(description = "优惠劵减免金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer couponPrice; + + @Schema(description = "积分抵扣的金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer pointPrice; +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderDeliveryReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderDeliveryReqVO.java new file mode 100644 index 0000000..d9284a4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderDeliveryReqVO.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.trade.controller.admin.order.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 订单发货 Request VO") +@Data +public class TradeOrderDeliveryReqVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "订单编号不能为空") + private Long id; + + @Schema(description = "发货类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @InEnum(DeliveryTypeEnum.class) + @NotNull(message = "发货类型不能为空") + private Integer type; + + // TODO @puhui999:还是要校验下 + + @Schema(description = "发货物流公司编号", example = "1") + private Long logisticsId; + + @Schema(description = "发货物流单号", example = "SF123456789") + private String logisticsNo; + + // TODO 订单项商品单独发货;不做单独发 + + @Schema(description = "发货订单项", example = "[1,2,3]") + @NotNull(message = "发货订单项不能为空") + private List orderItemIds; + + // =============== 同城配送 ================ + // TODO + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderDetailRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderDetailRespVO.java new file mode 100644 index 0000000..d747a4b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderDetailRespVO.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.trade.controller.admin.order.vo; + +import com.yunxi.scm.module.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.yunxi.scm.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 交易订单的详情 Response VO") +@Data +public class TradeOrderDetailRespVO extends TradeOrderBaseVO { + + @Schema(description = "收件人地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海 上海市 普陀区") + private String receiverAreaName; + + /** + * 订单项列表 + */ + private List items; + + /** + * 用户信息 + */ + private MemberUserRespVO user; + + @Schema(description = "管理后台 - 交易订单的详情的订单项目") + @Data + public static class Item extends TradeOrderItemBaseVO { + + /** + * 属性数组 + */ + private List properties; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderItemBaseVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderItemBaseVO.java new file mode 100644 index 0000000..af6b63b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderItemBaseVO.java @@ -0,0 +1,67 @@ +package com.yunxi.scm.module.trade.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 交易订单项 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class TradeOrderItemBaseVO { + + // ========== 订单项基本信息 ========== + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long userId; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long orderId; + + // ========== 商品基本信息 ========== + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long spuId; + + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long skuId; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "商品原价(单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer price; + + @Schema(description = "商品优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer discountPrice; + + @Schema(description = "商品实付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer payPrice; + + @Schema(description = "子订单分摊金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer orderPartPrice; + + @Schema(description = "分摊后子订单实付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer orderDividePrice; + + // ========== 营销基本信息 ========== + + // TODO 芋艿:在捉摸一下 + + // ========== 售后基本信息 ========== + + @Schema(description = "售后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer afterSaleStatus; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderPageItemRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderPageItemRespVO.java new file mode 100644 index 0000000..ff21b3d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderPageItemRespVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.trade.controller.admin.order.vo; + +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 交易订单的分页项 Response VO") +@Data +public class TradeOrderPageItemRespVO extends TradeOrderBaseVO { + + @Schema(description = "收件人地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海 上海市 普陀区") + private String receiverAreaName; + + /** + * 订单项列表 + */ + private List items; + + // TODO @xiaobai:使用 MemberUserRespVO 返回哈;DTO 不直接给前端 + /** + * 用户信息 + */ + private MemberUserRespDTO user; + + @Schema(description = "管理后台 - 交易订单的分页项的订单项目") + @Data + public static class Item extends TradeOrderItemBaseVO { + + /** + * 属性数组 + */ + private List properties; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderPageReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderPageReqVO.java new file mode 100644 index 0000000..8ab55cd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/admin/order/vo/TradeOrderPageReqVO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.trade.controller.admin.order.vo; + +import com.yunxi.scm.framework.common.enums.TerminalEnum; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.trade.enums.order.TradeOrderStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 交易订单的分页 Request VO") +@Data +public class TradeOrderPageReqVO extends PageParam { + + @Schema(description = "订单号", example = "88888888") + private String no; + + @Schema(description = "用户编号", example = "1024") + private Long userId; + + @Schema(description = "用户昵称", example = "小王") + private String userNickname; + + @Schema(description = "用户手机号", example = "小王") + @Mobile + private String userMobile; + + @Schema(description = "收件人名称", example = "小红") + private String receiverName; + + @Schema(description = "收件人手机", example = "1560") + @Mobile + private String receiverMobile; + + @Schema(description = "订单类型", example = "1") + private Integer type; + + @Schema(description = "订单状态", example = "1") + @InEnum(value = TradeOrderStatusEnum.class, message = "订单状态必须是 {value}") + private Integer status; + + @Schema(description = "支付渠道", example = "wx_lite") + private String payChannelCode; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "订单来源", example = "10") + @InEnum(value = TerminalEnum.class, message = "订单来源 {value}") + private Integer terminal; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/AppTradeAfterSaleController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/AppTradeAfterSaleController.java new file mode 100644 index 0000000..c9fbeee --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/AppTradeAfterSaleController.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.module.trade.controller.app.aftersale; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSaleCreateReqVO; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSaleDeliveryReqVO; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSalePageItemRespVO; +import com.yunxi.scm.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import com.yunxi.scm.module.trade.enums.aftersale.AfterSaleOperateTypeEnum; +import com.yunxi.scm.module.trade.framework.aftersalelog.core.annotations.AfterSaleLog; +import com.yunxi.scm.module.trade.service.aftersale.TradeAfterSaleService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Arrays; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 交易售后") +@RestController +@RequestMapping("/trade/after-sale") +@Validated +@Slf4j +public class AppTradeAfterSaleController { + + @Resource + private TradeAfterSaleService afterSaleService; + + // TODO 芋艿:待实现 + @GetMapping(value = "/page") + @Operation(summary = "获得售后分页") + public CommonResult> getAfterSalePage() { + AppTradeAfterSalePageItemRespVO vo = new AppTradeAfterSalePageItemRespVO(); + vo.setId(1L); + vo.setNo("1146347329394184195"); + vo.setStatus(61); + vo.setWay(10); + vo.setType(10); + vo.setApplyReason("不想要了"); + vo.setApplyDescription("这个商品我不喜欢,想退款"); + vo.setApplyPicUrls(Arrays.asList("pic_url_1", "pic_url_2", "pic_url_3")); + + // 设置订单相关信息 + vo.setOrderId(2001L); + vo.setOrderNo("23456789009876"); + vo.setOrderItemId(3001L); + vo.setSpuId(4001L); + vo.setSpuName("商品名"); + vo.setSkuId(5001L); + vo.setProperties(Arrays.asList( + new AppProductPropertyValueDetailRespVO().setPropertyId(6001L).setPropertyName("颜色").setValueId(7001L).setValueName("红色"), + new AppProductPropertyValueDetailRespVO().setPropertyId(6002L).setPropertyName("尺寸").setValueId(7002L).setValueName("XL"))); + vo.setPicUrl("https://cdn.pixabay.com/photo/2022/12/06/06/21/lavender-7638368_1280.jpg"); + vo.setCount(2); + + // 设置审批相关信息 + vo.setAuditReason("审核通过"); + + // 设置退款相关信息 + vo.setRefundPrice(1000); + vo.setRefundTime(LocalDateTime.now()); + + // 设置退货相关信息 + vo.setLogisticsId(7001L); + vo.setLogisticsNo("LAGN101010101001"); + vo.setDeliveryTime(LocalDateTime.now()); + vo.setReceiveTime(LocalDateTime.now()); + vo.setReceiveReason("收货正常"); + + return success(new PageResult<>(Arrays.asList(vo), 1L)); +// return success(afterSaleService.getAfterSalePage(getLoginUserId())); + } + + @PostMapping(value = "/create") + @Operation(summary = "申请售后") + @AfterSaleLog(id = "#info.data", content = "'申请售后:售后编号['+#info.data+'],订单编号['+#createReqVO.orderItemId+'], '", operateType = AfterSaleOperateTypeEnum.APPLY) + public CommonResult createAfterSale(@RequestBody AppTradeAfterSaleCreateReqVO createReqVO) { + return success(afterSaleService.createAfterSale(getLoginUserId(), createReqVO)); + } + + @PostMapping(value = "/delivery") + @Operation(summary = "退回货物") + public CommonResult deliveryAfterSale(@RequestBody AppTradeAfterSaleDeliveryReqVO deliveryReqVO) { + afterSaleService.deliveryAfterSale(getLoginUserId(), deliveryReqVO); + return success(true); + } + + @DeleteMapping(value = "/cancel") + @Operation(summary = "取消售后") + @Parameter(name = "id", description = "售后编号", required = true, example = "1") + public CommonResult cancelAfterSale(@RequestParam("id") Long id) { + afterSaleService.cancelAfterSale(getLoginUserId(), id); + return success(true); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSaleCreateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSaleCreateReqVO.java new file mode 100644 index 0000000..0803fbe --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSaleCreateReqVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.trade.controller.app.aftersale.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleWayEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "用户 App - 交易售后创建 Request VO") +@Data +public class AppTradeAfterSaleCreateReqVO { + + @Schema(description = "订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "订单项编号不能为空") + private Long orderItemId; + + @Schema(description = "售后方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "售后方式不能为空") + @InEnum(value = TradeAfterSaleWayEnum.class, message = "售后方式必须是 {value}") + private Integer way; + + @Schema(description = "退款金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + @NotNull(message = "退款金额不能为空") + @Min(value = 1, message = "退款金额必须大于 0") + private Integer refundPrice; + + @Schema(description = "申请原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "申请原因不能为空") + private String applyReason; + + @Schema(description = "补充描述", example = "商品质量不好") + private String applyDescription; + + @Schema(description = "补充凭证图片", example = "https://www.iocoder.cn/1.png, https://www.iocoder.cn/2.png") + private List applyPicUrls; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSaleDeliveryReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSaleDeliveryReqVO.java new file mode 100644 index 0000000..f19d80b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSaleDeliveryReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.trade.controller.app.aftersale.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "用户 App - 交易售后退回货物 Request VO") +@Data +public class AppTradeAfterSaleDeliveryReqVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "售后编号不能为空") + private Long id; + + @Schema(description = "退货物流公司编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "退货物流公司编号不能为空") + private Long logisticsId; + + @Schema(description = "退货物流单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "SF123456789") + @NotNull(message = "退货物流单号不能为空") + private String logisticsNo; + + @Schema(description = "退货时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "退货时间不能为空") + private LocalDateTime deliveryTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSalePageItemRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSalePageItemRespVO.java new file mode 100644 index 0000000..74984ef --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/aftersale/vo/AppTradeAfterSalePageItemRespVO.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.module.trade.controller.app.aftersale.vo; + +import com.yunxi.scm.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "用户 App - 交易售后的分页项 Request VO") +@Data +public class AppTradeAfterSalePageItemRespVO { + + @Schema(description = "售后编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "售后流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1146347329394184195") + private String no; + + @Schema(description = "售后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "售后方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer way; + + @Schema(description = "售后类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "申请原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String applyReason; + + @Schema(description = "补充描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String applyDescription; + + @Schema(description = "补充凭证图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private List applyPicUrls; + + // ========== 交易订单相关 ========== + + @Schema(description = "交易订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long orderId; + + @Schema(description = "交易订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String orderNo; + + @Schema(description = "交易订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long orderItemId; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long spuId; + + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long skuId; + + /** + * 属性数组 + */ + private List properties; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/01.jpg") + private String picUrl; + + @Schema(description = "退货商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + // ========== 审批相关 ========== + + /** + * 审批备注 + * + * 注意,只有审批不通过才会填写 + */ + private String auditReason; + + // ========== 退款相关 ========== + + @Schema(description = "退款金额,单位:分", example = "100") + private Integer refundPrice; + + @Schema(description = "退款时间") + private LocalDateTime refundTime; + + // ========== 退货相关 ========== + + @Schema(description = "退货物流公司编号", example = "1") + private Long logisticsId; + + @Schema(description = "退货物流单号", example = "SF123456789") + private String logisticsNo; + + @Schema(description = "退货时间") + private LocalDateTime deliveryTime; + + @Schema(description = "收货时间") + private LocalDateTime receiveTime; + + @Schema(description = "收货备注") + private String receiveReason; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/package-info.java new file mode 100644 index 0000000..ae73c00 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/package-info.java @@ -0,0 +1,4 @@ +/** + * 基础包,放一些通用的 VO 类 + */ +package com.yunxi.scm.module.trade.controller.app.base; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java new file mode 100644 index 0000000..cc6cc9c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/property/AppProductPropertyValueDetailRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.trade.controller.app.base.property; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 商品属性值的明细 Response VO") +@Data +public class AppProductPropertyValueDetailRespVO { + + @Schema(description = "属性的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long propertyId; + + @Schema(description = "属性的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "颜色") + private String propertyName; + + @Schema(description = "属性值的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long valueId; + + @Schema(description = "属性值的名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "红色") + private String valueName; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/sku/AppProductSkuBaseRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/sku/AppProductSkuBaseRespVO.java new file mode 100644 index 0000000..fce83ca --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/sku/AppProductSkuBaseRespVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.trade.controller.app.base.sku; + +import com.yunxi.scm.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 商品 SKU 基础 Response VO + * + * @author 芋道源码 + */ +@Data +public class AppProductSkuBaseRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "图片地址", example = "https://www.iocoder.cn/xx.png") + private String picUrl; + + @Schema(description = "销售价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer price; + + @Schema(description = "库存", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer stock; + + /** + * 属性数组 + */ + private List properties; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java new file mode 100644 index 0000000..2d7e4b5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/base/spu/AppProductSpuBaseRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.trade.controller.app.base.spu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 商品 SPU 基础 Response VO + * + * @author 芋道源码 + */ +@Data +public class AppProductSpuBaseRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "商品 SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "商品主图地址", example = "https://www.iocoder.cn/xx.png") + private String picUrl; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/TradeCartController.http b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/TradeCartController.http new file mode 100644 index 0000000..b341a48 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/TradeCartController.http @@ -0,0 +1,42 @@ +### 请求 /trade/cart/add 接口 => 成功 +POST {{appApi}}/trade/cart/add +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} +Content-Type: application/json + +{ + "skuId": 1, + "count": 10, + "addStatus": true +} + +### 请求 /trade/cart/update 接口 => 成功 +PUT {{appApi}}/trade/cart/update +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} +Content-Type: application/json + +{ + "id": 35, + "count": 5 +} + +### 请求 /trade/cart/delete 接口 => 成功 +DELETE {{appApi}}/trade/cart/delete?ids=1 +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} + +### 请求 /trade/cart/get-count 接口 => 成功 +GET {{appApi}}/trade/cart/get-count +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} + +### 请求 /trade/cart/get-count-map 接口 => 成功 +GET {{appApi}}/trade/cart/get-count-map +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} + +### 请求 /trade/cart/list 接口 => 成功 +GET {{appApi}}/trade/cart/list +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/TradeCartController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/TradeCartController.java new file mode 100644 index 0000000..7ae408c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/TradeCartController.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.trade.controller.app.cart; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.security.core.annotations.PreAuthenticated; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartAddReqVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartListRespVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartResetReqVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartUpdateReqVO; +import com.yunxi.scm.module.trade.service.cart.TradeCartService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 购物车") +@RestController +@RequestMapping("/trade/cart") +@RequiredArgsConstructor +@Validated +@Slf4j +public class TradeCartController { + + @Resource + private TradeCartService cartService; + + @PostMapping("/add") + @Operation(summary = "添加购物车商品") + @PreAuthenticated + public CommonResult addCart(@Valid @RequestBody AppTradeCartAddReqVO addCountReqVO) { + return success(cartService.addCart(getLoginUserId(), addCountReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新购物车商品") + @PreAuthenticated + public CommonResult updateCart(@Valid @RequestBody AppTradeCartUpdateReqVO updateReqVO) { + cartService.updateCart(getLoginUserId(), updateReqVO); + return success(true); + } + + @PutMapping("/reset") + @Operation(summary = "重置购物车商品") + @PreAuthenticated + public CommonResult resetCart(@Valid @RequestBody AppTradeCartResetReqVO updateReqVO) { + cartService.resetCart(getLoginUserId(), updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除购物车商品") + @Parameter(name = "ids", description = "购物车商品编号", required = true, example = "1024,2048") + @PreAuthenticated + public CommonResult deleteCart(@RequestParam("ids") List ids) { + cartService.deleteCart(getLoginUserId(), ids); + return success(true); + } + + @GetMapping("get-count") + @Operation(summary = "查询用户在购物车中的商品数量") + @PreAuthenticated + public CommonResult getCartCount() { + return success(cartService.getCartCount(getLoginUserId())); + } + + @GetMapping("get-count-map") + @Operation(summary = "查询用户在购物车中的商品 SPU 数量 Map") + @PreAuthenticated + public CommonResult> getCartCountMap() { + return success(cartService.getCartCountMap(getLoginUserId())); + } + + @GetMapping("/list") + @Operation(summary = "查询用户的购物车列表") + @PreAuthenticated + public CommonResult getCartList() { + return success(cartService.getCartList(getLoginUserId())); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartAddReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartAddReqVO.java new file mode 100644 index 0000000..3df5a25 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartAddReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.trade.controller.app.cart.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - 购物车添加购物项 Request VO") +@Data +public class AppTradeCartAddReqVO { + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED,example = "1024") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "新增商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数量不能为空") + private Integer count; + + @Schema(description = "是否添加到购物车", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否添加购物车不能为空") + private Boolean addStatus; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartDetailRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartDetailRespVO.java new file mode 100644 index 0000000..12d8fc9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartDetailRespVO.java @@ -0,0 +1,117 @@ +package com.yunxi.scm.module.trade.controller.app.cart.vo; + +import com.yunxi.scm.module.trade.controller.app.base.sku.AppProductSkuBaseRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 用户的购物车明细 Response VO") +@Data +public class AppTradeCartDetailRespVO { + + /** + * 商品分组数组 + */ + private List itemGroups; + + /** + * 费用 + */ + private Order order; + + @Schema(description = "商品分组") // 多个商品,参加同一个活动,从而形成分组 + @Data + public static class ItemGroup { + + /** + * 商品数组 + */ + private List items; + /** + * 营销活动,订单级别 + */ + private Promotion promotion; + + } + + @Schema(description = "商品 SKU") + @Data + public static class Sku extends AppProductSkuBaseRespVO { + + /** + * SPU 信息 + */ + private AppProductSkuBaseRespVO spu; + + // ========== 购物车相关的字段 ========== + + @Schema(description = "商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + @Schema(description = "是否选中", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean selected; + + // ========== 价格相关的字段,对应 PriceCalculateRespDTO.OrderItem 的属性 ========== + + // TODO 芋艿:后续可以去除一些无用的字段 + + @Schema(description = "商品原价(单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer originalPrice; + @Schema(description = "商品原价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Integer totalOriginalPrice; + @Schema(description = "商品级优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "300") + private Integer totalPromotionPrice; + @Schema(description = "最终购买金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "400") + private Integer totalPresentPrice; + @Schema(description = "最终购买金额(单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "500") + private Integer presentPrice; + @Schema(description = "应付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "600") + private Integer totalPayPrice; + + // ========== 营销相关的字段 ========== + /** + * 营销活动,商品级别 + */ + private Promotion promotion; + + } + + @Schema(description = "订单") // 对应 PriceCalculateRespDTO.Order 类,用于费用(合计) + @Data + public static class Order { + + // TODO 芋艿:后续可以去除一些无用的字段 + + @Schema(description = "商品原价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer skuOriginalPrice; + @Schema(description = "商品优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Integer skuPromotionPrice; + @Schema(description = "订单优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "300") + private Integer orderPromotionPrice; + @Schema(description = "运费金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "400") + private Integer deliveryPrice; + @Schema(description = "应付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "500") + private Integer payPrice; + + } + + @Schema(description = "营销活动") // 对应 PriceCalculateRespDTO.Promotion 类的属性 + @Data + public static class Promotion { + + @Schema(description = "营销编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") // 营销活动的编号、优惠劵的编号 + private Long id; + @Schema(description = "营销名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "xx 活动") + private String name; + @Schema(description = "营销类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + // ========== 匹配情况 ========== + @Schema(description = "是否满足优惠条件", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean meet; + @Schema(description = "满足条件的提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "圣诞价:省 150.00 元") + private String meetTip; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartItemUpdateSelectedReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartItemUpdateSelectedReqVO.java new file mode 100644 index 0000000..b5f9df9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartItemUpdateSelectedReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.trade.controller.app.cart.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collection; + +@Schema(description = "用户 App - 购物车更新是否选中 Request VO") +@Data +public class AppTradeCartItemUpdateSelectedReqVO { + + @Schema(description = "商品 SKU 编号列表", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024,2048") + @NotNull(message = "商品 SKU 编号列表不能为空") + private Collection skuIds; + + @Schema(description = "是否选中", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否选中不能为空") + private Boolean selected; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartListRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartListRespVO.java new file mode 100644 index 0000000..ae14094 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartListRespVO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.trade.controller.app.cart.vo; + +import com.yunxi.scm.module.trade.controller.app.base.sku.AppProductSkuBaseRespVO; +import com.yunxi.scm.module.trade.controller.app.base.spu.AppProductSpuBaseRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 用户的购物列表 Response VO") +@Data +public class AppTradeCartListRespVO { + + /** + * 有效的购物项数组 + */ + private List validList; + + /** + * 无效的购物项数组 + */ + private List invalidList; + + @Schema(description = "购物项") + @Data + public static class Cart { + + @Schema(description = "购物项的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + /** + * 商品 SPU + */ + private AppProductSpuBaseRespVO spu; + /** + * 商品 SKU + */ + private AppProductSkuBaseRespVO sku; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartResetReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartResetReqVO.java new file mode 100644 index 0000000..19eb43d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartResetReqVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.trade.controller.app.cart.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - 购物车重置 Request VO") +@Data +public class AppTradeCartResetReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED,example = "1024") + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + @Schema(description = "商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数量不能为空") + @Min(message = "数量必须大于 0", value = 1L) + private Integer count; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartUpdateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartUpdateReqVO.java new file mode 100644 index 0000000..5cfbf6f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/cart/vo/AppTradeCartUpdateReqVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.trade.controller.app.cart.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 App - 购物车更新 Request VO") +@Data +public class AppTradeCartUpdateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数量不能为空") + @Min(message = "数量必须大于 0", value = 1L) + private Integer count; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/AppDeliverConfigController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/AppDeliverConfigController.java new file mode 100644 index 0000000..f5bc274 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/AppDeliverConfigController.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.trade.controller.app.delivery; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.trade.controller.app.delivery.vo.config.AppDeliveryConfigRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 配送配置") +@RestController +@RequestMapping("/trade/delivery/config") +@Validated +public class AppDeliverConfigController { + + @GetMapping("/get") + @Operation(summary = "获得配送配置") + public CommonResult getDeliveryConfig() { + return success(new AppDeliveryConfigRespVO().setPickUpEnable(true).setTencentLbsKey("123456")); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/AppDeliverPickUpStoreController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/AppDeliverPickUpStoreController.java new file mode 100644 index 0000000..b4a1086 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/AppDeliverPickUpStoreController.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.module.trade.controller.app.delivery; + +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.trade.controller.app.delivery.vo.pickup.AppDeliveryPickUpStoreRespVO; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 自提门店") +@RestController +@RequestMapping("/trade/delivery/pick-up-store") +@Validated +public class AppDeliverPickUpStoreController { + + @GetMapping("/list") + @Operation(summary = "获得自提门店列表") + public CommonResult> getDeliveryPickUpStoreList( + @RequestParam(value = "latitude", required = false) Double latitude, + @RequestParam(value = "longitude", required = false) Double longitude) { + List list = new ArrayList<>(); + Random random = new Random(); + for (int i = 0; i < 10; i++) { + AppDeliveryPickUpStoreRespVO store = new AppDeliveryPickUpStoreRespVO(); + store.setId(random.nextLong()); + store.setName(RandomUtil.randomString(10)); + store.setLogo("https://www.iocoder.cn/" + (i + 1) + ".png"); + store.setPhone("15601691300"); + store.setAreaId(random.nextInt(100000)); + store.setAreaName("上海-" + RandomUtil.randomString(10)); + store.setDetailAddress("普陀区-" + RandomUtil.randomString(10)); + store.setLatitude(random.nextDouble() * 10); + store.setLongitude(random.nextDouble() * 10); + store.setDistance(random.nextInt(1000)); + + list.add(store); + } + + return success(list); + } + + @GetMapping("/get") + @Operation(summary = "获得自提门店") + @Parameter(name = "id", description = "门店编号") + public CommonResult getOrder(@RequestParam("id") Long id) { + AppDeliveryPickUpStoreRespVO store = new AppDeliveryPickUpStoreRespVO(); + Random random = new Random(); + store.setId(random.nextLong()); + store.setName(RandomUtil.randomString(10)); + store.setLogo("https://www.iocoder.cn/" + (1) + ".png"); + store.setPhone("15601691300"); + store.setAreaId(random.nextInt(100000)); + store.setAreaName("上海-" + RandomUtil.randomString(10)); + store.setDetailAddress("普陀区-" + RandomUtil.randomString(10)); + store.setLatitude(random.nextDouble() * 10); + store.setLongitude(random.nextDouble() * 10); + store.setDistance(random.nextInt(1000)); + return success(store); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/vo/config/AppDeliveryConfigRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/vo/config/AppDeliveryConfigRespVO.java new file mode 100644 index 0000000..187a1e1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/vo/config/AppDeliveryConfigRespVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.trade.controller.app.delivery.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +// TODO 芋艿:后续要实现下,配送配置 +@Schema(description = "用户 App - 配送配置 Response VO") +@Data +public class AppDeliveryConfigRespVO { + + @Schema(description = "腾讯地图 KEY", required = true, example = "123456") + private String tencentLbsKey; + + @Schema(description = "是否开启自提", required = true, example = "true") + private Boolean pickUpEnable; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java new file mode 100644 index 0000000..e1a9b5a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/delivery/vo/pickup/AppDeliveryPickUpStoreRespVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.trade.controller.app.delivery.vo.pickup; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 自提门店 Response VO") +@Data +public class AppDeliveryPickUpStoreRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23128") + private Long id; + + @Schema(description = "门店名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "李四") + private String name; + + @Schema(description = "门店 logo", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String logo; + + @Schema(description = "门店手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601892312") + private String phone; + + @Schema(description = "区域编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18733") + private Integer areaId; + + @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区") + private String areaName; + + @Schema(description = "门店详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "复旦大学路 188 号") + private String detailAddress; + + @Schema(description = "纬度", requiredMode = Schema.RequiredMode.REQUIRED, example = "5.88") + private Double latitude; + + @Schema(description = "经度", requiredMode = Schema.RequiredMode.REQUIRED, example = "6.99") + private Double longitude; + + @Schema(description = "距离,单位:米", example = "100") // 只有在用户传递了经纬度时,才进行计算 + private Integer distance; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/AppTradeOrderController.http b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/AppTradeOrderController.http new file mode 100644 index 0000000..1a11d4e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/AppTradeOrderController.http @@ -0,0 +1,37 @@ +### /trade-order/settlement 获得订单结算信息(基于商品) +GET {{appApi}}/trade/order/settlement?type=0&items[0].skuId=1&items[0].count=2&items[1].skuId=2&items[1].count=3&couponId=1 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +### /trade-order/settlement 获得订单结算信息(基于购物车) +GET {{appApi}}/trade/order/settlement?type=0&items[0].cartId=50&couponId=1 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +### /trade-order/create 创建订单(基于商品) +POST {{appApi}}/trade/order/create +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "type": 0, + "addressId": 21, + "items": [ + { + "skuId": 1, + "count": 2 + } + ], + "remark": "我是备注" +} + +### 获得订单交易的分页 +GET {{appApi}}/trade/order/page?pageNo=1&pageSize=10 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +### 获得订单交易的详细 +GET {{appApi}}/trade/order/get-detail?id=21 +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/AppTradeOrderController.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/AppTradeOrderController.java new file mode 100644 index 0000000..e940149 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/AppTradeOrderController.java @@ -0,0 +1,165 @@ +package com.yunxi.scm.module.trade.controller.app.order; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.security.core.annotations.PreAuthenticated; +import com.yunxi.scm.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import com.yunxi.scm.module.product.api.property.ProductPropertyValueApi; +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.trade.controller.app.order.vo.*; +import com.yunxi.scm.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO; +import com.yunxi.scm.module.trade.convert.order.TradeOrderConvert; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.enums.order.TradeOrderStatusEnum; +import com.yunxi.scm.module.trade.framework.order.config.TradeOrderProperties; +import com.yunxi.scm.module.trade.service.order.TradeOrderService; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 交易订单") +@RestController +@RequestMapping("/trade/order") +@Validated +@Slf4j +public class AppTradeOrderController { + + @Resource + private TradeOrderService tradeOrderService; + @Resource + private ProductPropertyValueApi productPropertyValueApi; + @Resource + private TradeOrderProperties tradeOrderProperties; + + @GetMapping("/settlement") + @Operation(summary = "获得订单结算信息") + @PreAuthenticated + public CommonResult settlementOrder(@Valid AppTradeOrderSettlementReqVO settlementReqVO) { + return success(tradeOrderService.settlementOrder(getLoginUserId(), settlementReqVO)); + } + + @PostMapping("/create") + @Operation(summary = "创建订单") + @PreAuthenticated + public CommonResult createOrder(@RequestBody AppTradeOrderCreateReqVO createReqVO) { + TradeOrderDO order = tradeOrderService.createOrder(getLoginUserId(), getClientIP(), createReqVO); + return success(new AppTradeOrderCreateRespVO().setId(order.getId()).setPayOrderId(order.getPayOrderId())); + } + + @PostMapping("/update-paid") + @Operation(summary = "更新订单为已支付") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + public CommonResult updateOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) { + tradeOrderService.updateOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayOrderId()); + return success(true); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得交易订单") + @Parameter(name = "id", description = "交易订单编号") + public CommonResult getOrder(@RequestParam("id") Long id) { + // 查询订单 + TradeOrderDO order = tradeOrderService.getOrder(getLoginUserId(), id); + // 查询订单项 + List orderItems = tradeOrderService.getOrderItemListByOrderId(order.getId()); + // 查询商品属性 + List propertyValueDetails = productPropertyValueApi + .getPropertyValueDetailList(TradeOrderConvert.INSTANCE.convertPropertyValueIds(orderItems)); + // 最终组合 + return success(TradeOrderConvert.INSTANCE.convert02(order, orderItems, + propertyValueDetails, tradeOrderProperties)); + } + + @GetMapping("/page") + @Operation(summary = "获得交易订单分页") + public CommonResult> getOrderPage(AppTradeOrderPageReqVO reqVO) { + // 查询订单 + PageResult pageResult = tradeOrderService.getOrderPage(getLoginUserId(), reqVO); + // 查询订单项 + List orderItems = tradeOrderService.getOrderItemListByOrderId( + convertSet(pageResult.getList(), TradeOrderDO::getId)); + // 查询商品属性 + List propertyValueDetails = productPropertyValueApi + .getPropertyValueDetailList(TradeOrderConvert.INSTANCE.convertPropertyValueIds(orderItems)); + // 最终组合 + return success(TradeOrderConvert.INSTANCE.convertPage02(pageResult, orderItems, propertyValueDetails)); + } + + @GetMapping("/get-count") + @Operation(summary = "获得交易订单数量") + public CommonResult> getOrderCount() { + Map orderCount = Maps.newLinkedHashMapWithExpectedSize(5); + // 全部 + orderCount.put("allCount", tradeOrderService.getOrderCount(getLoginUserId(), null, null)); + // 待付款(未支付) + orderCount.put("unpaidCount", tradeOrderService.getOrderCount(getLoginUserId(), + TradeOrderStatusEnum.UNPAID.getStatus(), null)); + // 待发货 + orderCount.put("undeliveredCount", tradeOrderService.getOrderCount(getLoginUserId(), + TradeOrderStatusEnum.UNDELIVERED.getStatus(), null)); + // 待收货 + orderCount.put("deliveredCount", tradeOrderService.getOrderCount(getLoginUserId(), + TradeOrderStatusEnum.DELIVERED.getStatus(), null)); + // 待评价 + orderCount.put("uncommentedCount", tradeOrderService.getOrderCount(getLoginUserId(), + TradeOrderStatusEnum.COMPLETED.getStatus(), false)); + return success(orderCount); + } + + @PutMapping("/take") + @Operation(summary = "确认交易订单收货") + @Parameter(name = "id", description = "交易订单编号") + public CommonResult takeOrder(@RequestParam("id") Long id) { + // TODO @芋艿:未实现,mock 用 + return success(true); + } + + @DeleteMapping("/cancel") + @Operation(summary = "取消交易订单") + @Parameter(name = "id", description = "交易订单编号") + public CommonResult cancelOrder(@RequestParam("id") Long id) { + // TODO @芋艿:未实现,mock 用 + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除交易订单") + @Parameter(name = "id", description = "交易订单编号") + public CommonResult deleteOrder(@RequestParam("id") Long id) { + // TODO @芋艿:未实现,mock 用 + return success(true); + } + + // ========== 订单项 ========== + + @GetMapping("/item/get") + @Operation(summary = "获得交易订单项") + @Parameter(name = "id", description = "交易订单项编号") + public CommonResult getOrderItem(@RequestParam("id") Long id) { + TradeOrderItemDO item = tradeOrderService.getOrderItem(getLoginUserId(), id); + return success(TradeOrderConvert.INSTANCE.convert03(item)); + } + + @PostMapping("/item/create-comment") + @Operation(summary = "创建交易订单项的评价") + public CommonResult createOrderItemComment(@RequestBody AppTradeOrderItemCommentCreateReqVO createReqVO) { + return success(tradeOrderService.createOrderItemComment(createReqVO)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java new file mode 100644 index 0000000..36ef102 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderCreateReqVO.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 交易订单创建 Request VO") +@Data +public class AppTradeOrderCreateReqVO extends AppTradeOrderSettlementReqVO { + + @Schema(description = "备注", example = "这个是我的订单哟") + private String remark; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderCreateRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderCreateRespVO.java new file mode 100644 index 0000000..6cef92e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderCreateRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "用户 App - 交易订单创建 Response VO") +@Data +public class AppTradeOrderCreateRespVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long payOrderId; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderDetailRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderDetailRespVO.java new file mode 100644 index 0000000..5371ff5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderDetailRespVO.java @@ -0,0 +1,128 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo; + +import com.yunxi.scm.module.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +@Schema(description = "用户 App - 订单交易的明细 Response VO") +@Data +public class AppTradeOrderDetailRespVO { + + // ========== 订单基本信息 ========== + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1146347329394184195") + private String no; + + @Schema(description = "下单时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + + @Schema(description = "用户备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + private String userRemark; + + @Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "购买的商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer productCount; + + @Schema(description = "订单完成时间") + private LocalDateTime finishTime; + + @Schema(description = "订单取消时间") + private LocalDateTime cancelTime; + + @Schema(description = "是否评价", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean commentStatus; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "是否已支付", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean payStatus; + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long payOrderId; + + @Schema(description = "付款时间") + private LocalDateTime payTime; + + @Schema(description = "付款超时时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime payExpireTime; + + @Schema(description = "支付渠道", example = "wx_lite_pay") + private String payChannelCode; + @Schema(description = "支付渠道名", example = "微信小程序支付") + private String payChannelName; + + @Schema(description = "商品原价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer totalPrice; + + @Schema(description = "订单优惠(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer discountPrice; + + @Schema(description = "运费金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer deliveryPrice; + + @Schema(description = "订单调价(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer adjustPrice; + + @Schema(description = "应付金额(总)", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer payPrice; + + // ========== 收件 + 物流基本信息 ========== + + @Schema(description = "配送方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer deliveryType; + + @Schema(description = "发货物流单号", example = "1024") + private String logisticsNo; + + @Schema(description = "发货时间") + private LocalDateTime deliveryTime; + + @Schema(description = "收货时间") + private LocalDateTime receiveTime; + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") + private String receiverName; + + @Schema(description = "收件人手机", requiredMode = Schema.RequiredMode.REQUIRED, example = "13800138000") + private String receiverMobile; + + @Schema(description = "收件人地区编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") + private Integer receiverAreaId; + + @Schema(description = "收件人地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海 上海市 普陀区") + private String receiverAreaName; + + @Schema(description = "收件人详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "中关村大街 1 号") + private String receiverDetailAddress; + + @Schema(description = "自提门店编号", example = "1088") + private Long pickUpStoreId; + + // ========== 售后基本信息 ========== + + // ========== 营销基本信息 ========== + + @Schema(description = "优惠劵编号", example = "1024") + private Long couponId; + + @Schema(description = "优惠劵减免金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer couponPrice; + + @Schema(description = "积分抵扣的金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer pointPrice; + + /** + * 订单项数组 + */ + private List items; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderPageItemRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderPageItemRespVO.java new file mode 100644 index 0000000..a5d92ea --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderPageItemRespVO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo; + +import com.yunxi.scm.module.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; +import java.util.List; + +@Schema(description = "用户 App - 订单交易的分页项 Response VO") +@Data +public class AppTradeOrderPageItemRespVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "订单流水号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1146347329394184195") + private String no; + + @Schema(description = "订单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer type; + + @Schema(description = "订单状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "购买的商品数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer productCount; + + @Schema(description = "是否评价", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean commentStatus; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long payOrderId; + + @Schema(description = "应付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000") + private Integer payPrice; + + // ========== 收件 + 物流基本信息 ========== + + @Schema(description = "配送方式", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer deliveryType; + + /** + * 订单项数组 + */ + private List items; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderPageReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderPageReqVO.java new file mode 100644 index 0000000..81ad778 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderPageReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderStatusEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "交易订单分页 Request VO") +@Data +public class AppTradeOrderPageReqVO extends PageParam { + + @Schema(description = "订单状态", example = "1") + @InEnum(value = TradeOrderStatusEnum.class, message = "订单状态必须是 {value}") + private Integer status; + + @Schema(description = "是否评价", example = "true") + private Boolean commentStatus; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderSettlementReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderSettlementReqVO.java new file mode 100644 index 0000000..18d4564 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderSettlementReqVO.java @@ -0,0 +1,86 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryTypeEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "用户 App - 交易订单结算 Request VO") +@Data +public class AppTradeOrderSettlementReqVO { + + @Schema(description = "商品项数组", requiredMode = Schema.RequiredMode.REQUIRED) + @NotEmpty(message = "商品不能为空") + private List items; + + @Schema(description = "优惠劵编号", example = "1024") + private Long couponId; + + @Schema(description = "是否使用积分", required = true, example = "true") + @NotNull(message = "是否使用积分不能为空") + private Boolean pointStatus; + + // ========== 配送相关相关字段 ========== + @Schema(description = "配送方式", required = true, example = "1") + @InEnum(value = DeliveryTypeEnum.class, message = "配送方式不正确") + private Integer deliveryType; + + @Schema(description = "收件地址编号", example = "1") + private Long addressId; + + @Schema(description = "自提门店编号", example = "1088") + private Long pickUpStoreId; + @Schema(description = "收件人名称", example = "芋艿") // 选择门店自提时,该字段为联系人名 + private String receiverName; + @Schema(description = "收件人手机", example = "15601691300") // 选择门店自提时,该字段为联系人手机 + @Mobile(message = "收件人手机格式不正确") + private String receiverMobile; + + // ========== 秒杀活动相关字段 ========== + @Schema(description = "秒杀活动编号", example = "1024") + private Long seckillActivityId; + + // ========== 拼团活动相关字段 ========== + @Schema(description = "拼团活动编号", example = "1024") + private Long combinationActivityId; + + @Schema(description = "拼团团长编号", example = "2048") + private Long combinationHeadId; + + @Data + @Schema(description = "用户 App - 商品项") + @Valid + public static class Item { + + @Schema(description = "商品 SKU 编号", example = "2048") + private Long skuId; + @Schema(description = "购买数量", example = "1") + @Min(value = 1, message = "购买数量最小值为 {value}") + private Integer count; + + @Schema(description = "购物车项的编号", example = "1024") + private Long cartId; + + @AssertTrue(message = "商品不正确") + @JsonIgnore + public boolean isValid() { + // 组合一:skuId + count 使用商品 SKU + if (skuId != null && count != null) { + return true; + } + // 组合二:cartId 使用购物车项 + return cartId != null; + } + + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java new file mode 100644 index 0000000..b129088 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/AppTradeOrderSettlementRespVO.java @@ -0,0 +1,117 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo; + +import com.yunxi.scm.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "用户 App - 交易订单结算信息 Response VO") +@Data +public class AppTradeOrderSettlementRespVO { + + @Schema(description = "交易类型", required = true, example = "1") // 对应 TradeOrderTypeEnum 枚举 + private Integer type = 1; // TODO 芋艿:改成计算 + + @Schema(description = "购物项数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List items; + + @Schema(description = "费用", requiredMode = Schema.RequiredMode.REQUIRED) + private Price price; + + @Schema(description = "收件地址", requiredMode = Schema.RequiredMode.REQUIRED) + private Address address; + + @Schema(description = "已使用的积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer usedPoint; + + @Schema(description = "总积分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer totalPoint; + + @Schema(description = "购物项") + @Data + public static class Item { + + // ========== SPU 信息 ========== + + @Schema(description = "SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long spuId; + @Schema(description = "SPU 名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "Apple iPhone 12") + private String spuName; + + // ========== SKU 信息 ========== + + @Schema(description = "SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer skuId; + @Schema(description = "价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer price; + @Schema(description = "图片地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "属性数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private List properties; + + // ========== 购物车信息 ========== + + @Schema(description = "购物车编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long cartId; + + @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + } + + @Schema(description = "费用(合计)") + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Price { + + @Schema(description = "商品原价(总),单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "500") + private Integer totalPrice; + + @Schema(description = "运费金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer deliveryPrice; + + @Schema(description = "优惠劵减免金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer couponPrice; + + @Schema(description = "积分抵扣的金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "50") + private Integer pointPrice; + + @Schema(description = "实际支付金额(总),单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "450") + private Integer payPrice; + + } + + @Schema(description = "地址信息") + @Data + public static class Address { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小王") + private String name; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + private String mobile; + + @Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "地区编号不能为空") + private Long areaId; + @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区") + private String areaName; + + @Schema(description = "详细地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "望京悠乐汇 A 座") + private String detailAddress; + + @Schema(description = "是否默认收件地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean defaultStatus; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/item/AppTradeOrderItemCommentCreateReqVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/item/AppTradeOrderItemCommentCreateReqVO.java new file mode 100644 index 0000000..cd70778 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/item/AppTradeOrderItemCommentCreateReqVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo.item; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.List; + +@Schema(description = "用户 App - 商品评价创建 Request VO") +@Data +public class AppTradeOrderItemCommentCreateReqVO { + + @Schema(description = "是否匿名", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否匿名不能为空") + private Boolean anonymous; + + @Schema(description = "交易订单项编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2312312") + @NotNull(message = "交易订单项编号不能为空") + private Long orderItemId; + + @Schema(description = "描述星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "描述星级 1-5 分不能为空") + private Integer descriptionScores; + + @Schema(description = "服务星级 1-5 分", requiredMode = Schema.RequiredMode.REQUIRED, example = "5") + @NotNull(message = "服务星级 1-5 分不能为空") + private Integer benefitScores; + + @Schema(description = "评论内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "穿身上很漂亮诶(*^▽^*)") + @NotNull(message = "评论内容不能为空") + private String content; + + @Schema(description = "评论图片地址数组,以逗号分隔最多上传 9 张", requiredMode = Schema.RequiredMode.REQUIRED, example = "[https://www.iocoder.cn/xx.png]") + @Size(max = 9, message = "评论图片地址数组长度不能超过 9 张") + private List picUrls; + + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/item/AppTradeOrderItemRespVO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/item/AppTradeOrderItemRespVO.java new file mode 100644 index 0000000..f177d95 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/app/order/vo/item/AppTradeOrderItemRespVO.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.trade.controller.app.order.vo.item; + +import com.yunxi.scm.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 订单交易项 Response VO") +@Data +public class AppTradeOrderItemRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "商品 SPU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long spuId; + @Schema(description = "商品 SPU 名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String spuName; + + @Schema(description = "商品 SKU 编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long skuId; + + /** + * 属性数组 + */ + private List properties; + + @Schema(description = "商品图片", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String picUrl; + + @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer count; + + @Schema(description = "是否评价", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean commentStatus; + + // ========== 价格 + 支付基本信息 ========== + + @Schema(description = "商品原价(单)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Integer price; + + // ========== 营销基本信息 ========== + + // TODO 芋艿:在捉摸一下 + + // ========== 售后基本信息 ========== + + @Schema(description = "售后状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer afterSaleStatus; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/package-info.java new file mode 100644 index 0000000..6f19ea8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yunxi-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yunxi-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.yunxi.scm.module.trade.controller; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/aftersale/TradeAfterSaleConvert.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/aftersale/TradeAfterSaleConvert.java new file mode 100644 index 0000000..ab85e87 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/aftersale/TradeAfterSaleConvert.java @@ -0,0 +1,88 @@ +package com.yunxi.scm.module.trade.convert.aftersale; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSaleRespPageItemVO; +import com.yunxi.scm.module.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.yunxi.scm.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSaleCreateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.framework.order.config.TradeOrderProperties; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; + +@Mapper +public interface TradeAfterSaleConvert { + + TradeAfterSaleConvert INSTANCE = Mappers.getMapper(TradeAfterSaleConvert.class); + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(target = "createTime", ignore = true), + @Mapping(target = "updateTime", ignore = true), + @Mapping(target = "creator", ignore = true), + @Mapping(target = "updater", ignore = true), + }) + TradeAfterSaleDO convert(AppTradeAfterSaleCreateReqVO createReqVO, TradeOrderItemDO tradeOrderItem); + + @Mappings({ + @Mapping(source = "afterSale.applyReason", target = "reason"), + @Mapping(source = "afterSale.refundPrice", target = "price") + }) + PayRefundCreateReqDTO convert(String userIp, TradeAfterSaleDO afterSale, + TradeOrderProperties orderProperties); + + MemberUserRespVO convert(MemberUserRespDTO bean); + + PageResult convertPage(PageResult page); + + default PageResult convertPage(PageResult pageResult, + Map memberUsers, List propertyValueDetails) { + PageResult pageVOResult = convertPage(pageResult); + // 处理会员 + 商品属性等关联信息 + Map propertyValueDetailMap = convertMap(propertyValueDetails, ProductPropertyValueDetailRespDTO::getValueId); + for (int i = 0; i < pageResult.getList().size(); i++) { + TradeAfterSaleRespPageItemVO afterSaleVO = pageVOResult.getList().get(i); + TradeAfterSaleDO afterSaleDO = pageResult.getList().get(i); + // 会员 + afterSaleVO.setUser(convert(memberUsers.get(afterSaleDO.getUserId()))); + // 商品属性 + if (CollUtil.isNotEmpty(afterSaleDO.getProperties())) { + afterSaleVO.setProperties(new ArrayList<>(afterSaleDO.getProperties().size())); + // 遍历每个 properties,设置到 TradeOrderPageItemRespVO.Item 中 + afterSaleDO.getProperties().forEach(property -> { + ProductPropertyValueDetailRespDTO propertyValueDetail = propertyValueDetailMap.get(property.getValueId()); + if (propertyValueDetail == null) { + return; + } + afterSaleVO.getProperties().add(convert(propertyValueDetail)); + }); + } + } + return pageVOResult; + } + + ProductPropertyValueDetailRespVO convert(ProductPropertyValueDetailRespDTO bean); + + default Set convertPropertyValueIds(List list) { + if (CollUtil.isEmpty(list)) { + return new HashSet<>(); + } + return list.stream().filter(item -> item.getProperties() != null) + .flatMap(p -> p.getProperties().stream()) // 遍历多个 Property 属性 + .map(TradeOrderItemDO.Property::getValueId) // 将每个 Property 转换成对应的 propertyId,最后形成集合 + .collect(Collectors.toSet()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/cart/TradeCartConvert.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/cart/TradeCartConvert.java new file mode 100644 index 0000000..c2553db --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/cart/TradeCartConvert.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.trade.convert.cart; + +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.yunxi.scm.module.trade.controller.app.base.sku.AppProductSkuBaseRespVO; +import com.yunxi.scm.module.trade.controller.app.base.spu.AppProductSpuBaseRespVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartListRespVO; +import com.yunxi.scm.module.trade.dal.dataobject.cart.TradeCartDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; + +@Mapper +public interface TradeCartConvert { + + TradeCartConvert INSTANCE = Mappers.getMapper(TradeCartConvert.class); + + default AppTradeCartListRespVO convertList(List carts, + List spus, List skus) { + Map spuMap = convertMap(spus, ProductSpuRespDTO::getId); + Map skuMap = convertMap(skus, ProductSkuRespDTO::getId); + // 遍历,开始转换 + List validList = new ArrayList<>(carts.size()); + List invalidList = new ArrayList<>(); + carts.forEach(cart -> { + AppTradeCartListRespVO.Cart cartVO = new AppTradeCartListRespVO.Cart(); + cartVO.setId(cart.getId()).setCount(cart.getCount()); + ProductSpuRespDTO spu = spuMap.get(cart.getSpuId()); + ProductSkuRespDTO sku = skuMap.get(cart.getSkuId()); + cartVO.setSpu(convert(spu)).setSku(convert(sku)); + // 如果 SPU 不存在,或者下架,或者库存不足,说明是无效的 + if (spu == null + || !ProductSpuStatusEnum.isEnable(spu.getStatus()) + || spu.getStock() <= 0) { + invalidList.add(cartVO); + } else { + // 虽然 SKU 可能也会不存在,但是可以通过购物车重新选择 + validList.add(cartVO); + } + }); + return new AppTradeCartListRespVO().setValidList(validList).setInvalidList(invalidList); + } + AppProductSpuBaseRespVO convert(ProductSpuRespDTO spu); + AppProductSkuBaseRespVO convert(ProductSkuRespDTO sku); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryExpressConvert.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryExpressConvert.java new file mode 100644 index 0000000..6c2a1a9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryExpressConvert.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.trade.convert.delivery; + +import java.util.*; + +import com.yunxi.scm.framework.common.pojo.PageResult; + +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressCreateReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressExcelVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressRespVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressUpdateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface DeliveryExpressConvert { + + DeliveryExpressConvert INSTANCE = Mappers.getMapper(DeliveryExpressConvert.class); + + DeliveryExpressDO convert(DeliveryExpressCreateReqVO bean); + + DeliveryExpressDO convert(DeliveryExpressUpdateReqVO bean); + + DeliveryExpressRespVO convert(DeliveryExpressDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryExpressTemplateConvert.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryExpressTemplateConvert.java new file mode 100644 index 0000000..c0b34b2 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryExpressTemplateConvert.java @@ -0,0 +1,96 @@ +package com.yunxi.scm.module.trade.convert.delivery; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.*; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateChargeDO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateFreeDO; +import com.yunxi.scm.module.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; +import com.google.common.collect.Maps; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.findFirst; + +@Mapper +public interface DeliveryExpressTemplateConvert { + + DeliveryExpressTemplateConvert INSTANCE = Mappers.getMapper(DeliveryExpressTemplateConvert.class); + + // ========== Template ========== + + DeliveryExpressTemplateDO convert(DeliveryExpressTemplateCreateReqVO bean); + + DeliveryExpressTemplateDO convert(DeliveryExpressTemplateUpdateReqVO bean); + + DeliveryExpressTemplateRespVO convert(DeliveryExpressTemplateDO bean); + + DeliveryExpressTemplateDetailRespVO convert2(DeliveryExpressTemplateDO bean); + + List convertList(List list); + + List convertList1(List list); + + PageResult convertPage(PageResult page); + + default DeliveryExpressTemplateDetailRespVO convert(DeliveryExpressTemplateDO bean, + List chargeList, + List freeList) { + DeliveryExpressTemplateDetailRespVO respVO = convert2(bean); + respVO.setTemplateCharge(convertTemplateChargeList(chargeList)); + respVO.setTemplateFree(convertTemplateFreeList(freeList)); + return respVO; + } + + // ========== Template Charge ========== + + DeliveryExpressTemplateChargeDO convertTemplateCharge(Long templateId, Integer chargeMode, ExpressTemplateChargeBaseVO vo); + + DeliveryExpressTemplateChargeDO convertTemplateCharge(DeliveryExpressTemplateUpdateReqVO.ExpressTemplateChargeUpdateVO vo); + + DeliveryExpressTemplateRespBO.Charge convertTemplateCharge(DeliveryExpressTemplateChargeDO bean); + + default List convertTemplateChargeList(Long templateId, Integer chargeMode, List list) { + return CollectionUtils.convertList(list, vo -> convertTemplateCharge(templateId, chargeMode, vo)); + } + + // ========== Template Free ========== + + DeliveryExpressTemplateFreeDO convertTemplateFree(Long templateId, ExpressTemplateFreeBaseVO vo); + + DeliveryExpressTemplateFreeDO convertTemplateFree(DeliveryExpressTemplateUpdateReqVO.ExpressTemplateFreeUpdateVO vo); + + DeliveryExpressTemplateRespBO.Free convertTemplateFree(DeliveryExpressTemplateFreeDO bean); + + List convertTemplateChargeList(List list); + + List convertTemplateFreeList(List list); + + default List convertTemplateFreeList(Long templateId, List list) { + return CollectionUtils.convertList(list, vo -> convertTemplateFree(templateId, vo)); + } + + default Map convertMap(Integer areaId, List templateList, + List chargeList, + List freeList) { + Map> templateIdChargeMap = convertMultiMap(chargeList, + DeliveryExpressTemplateChargeDO::getTemplateId); + Map> templateIdFreeMap = convertMultiMap(freeList, + DeliveryExpressTemplateFreeDO::getTemplateId); + // 组合运费模板配置 RespBO + Map result = Maps.newHashMapWithExpectedSize(templateList.size()); + templateList.forEach(template -> { + DeliveryExpressTemplateRespBO bo = new DeliveryExpressTemplateRespBO() + .setChargeMode(template.getChargeMode()) + .setCharge(convertTemplateCharge(findFirst(templateIdChargeMap.get(template.getId()), charge -> charge.getAreaIds().contains(areaId)))) + .setFree(convertTemplateFree(findFirst(templateIdFreeMap.get(template.getId()), free -> free.getAreaIds().contains(areaId)))); + result.put(template.getId(), bo); + }); + return result; + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryPickUpStoreConvert.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryPickUpStoreConvert.java new file mode 100644 index 0000000..5c38171 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/delivery/DeliveryPickUpStoreConvert.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.trade.convert.delivery; + +import java.util.*; + +import com.yunxi.scm.framework.common.pojo.PageResult; + +import com.yunxi.scm.framework.ip.core.utils.AreaUtils; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreCreateReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreExcelVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreRespVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreUpdateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface DeliveryPickUpStoreConvert { + + DeliveryPickUpStoreConvert INSTANCE = Mappers.getMapper(DeliveryPickUpStoreConvert.class); + + DeliveryPickUpStoreDO convert(DeliveryPickUpStoreCreateReqVO bean); + + DeliveryPickUpStoreDO convert(DeliveryPickUpStoreUpdateReqVO bean); + + DeliveryPickUpStoreRespVO convert(DeliveryPickUpStoreDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + @Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToName") + DeliveryPickUpStoreExcelVO convert2(DeliveryPickUpStoreDO bean); + + @Named("convertAreaIdToName") + default String convertAreaIdToName(Integer areaId) { + return AreaUtils.format(areaId); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/order/TradeOrderConvert.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/order/TradeOrderConvert.java new file mode 100644 index 0000000..b4ad6b4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/convert/order/TradeOrderConvert.java @@ -0,0 +1,350 @@ +package com.yunxi.scm.module.trade.convert.order; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.dict.core.util.DictFrameworkUtils; +import com.yunxi.scm.framework.ip.core.utils.AreaUtils; +import com.yunxi.scm.module.member.api.address.dto.AddressRespDTO; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.enums.DictTypeConstants; +import com.yunxi.scm.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; +import com.yunxi.scm.module.promotion.api.combination.dto.CombinationRecordReqDTO; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateReqDTO; +import com.yunxi.scm.module.trade.api.order.dto.TradeOrderRespDTO; +import com.yunxi.scm.module.trade.controller.admin.base.member.user.MemberUserRespVO; +import com.yunxi.scm.module.trade.controller.admin.base.product.property.ProductPropertyValueDetailRespVO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderDetailRespVO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderPageItemRespVO; +import com.yunxi.scm.module.trade.controller.app.base.property.AppProductPropertyValueDetailRespVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.*; +import com.yunxi.scm.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.item.AppTradeOrderItemRespVO; +import com.yunxi.scm.module.trade.dal.dataobject.cart.TradeCartDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDeliveryDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; +import com.yunxi.scm.module.trade.framework.order.config.TradeOrderProperties; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.*; +import java.util.stream.Collectors; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMultiMap; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.addTime; + +@Mapper +public interface TradeOrderConvert { + + TradeOrderConvert INSTANCE = Mappers.getMapper(TradeOrderConvert.class); + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(source = "createReqVO.couponId", target = "couponId"), + @Mapping(target = "remark", ignore = true), + @Mapping(source = "createReqVO.remark", target = "userRemark"), + @Mapping(source = "calculateRespBO.price.totalPrice", target = "totalPrice"), + @Mapping(source = "calculateRespBO.price.discountPrice", target = "discountPrice"), + @Mapping(source = "calculateRespBO.price.deliveryPrice", target = "deliveryPrice"), + @Mapping(source = "calculateRespBO.price.couponPrice", target = "couponPrice"), + @Mapping(source = "calculateRespBO.price.pointPrice", target = "pointPrice"), + @Mapping(source = "calculateRespBO.price.payPrice", target = "payPrice"), + @Mapping(source = "address.name", target = "receiverName"), + @Mapping(source = "address.mobile", target = "receiverMobile"), + @Mapping(source = "address.areaId", target = "receiverAreaId"), + @Mapping(source = "address.detailAddress", target = "receiverDetailAddress"), + }) + TradeOrderDO convert(Long userId, String userIp, AppTradeOrderCreateReqVO createReqVO, + TradePriceCalculateRespBO calculateRespBO, AddressRespDTO address); + + TradeOrderRespDTO convert(TradeOrderDO orderDO); + + default List convertList(TradeOrderDO tradeOrderDO, TradePriceCalculateRespBO calculateRespBO) { + return CollectionUtils.convertList(calculateRespBO.getItems(), item -> { + TradeOrderItemDO orderItem = convert(item); + orderItem.setOrderId(tradeOrderDO.getId()); + orderItem.setUserId(tradeOrderDO.getUserId()); + orderItem.setAfterSaleStatus(TradeOrderItemAfterSaleStatusEnum.NONE.getStatus()); + orderItem.setCommentStatus(false); + return orderItem; + }); + } + + TradeOrderItemDO convert(TradePriceCalculateRespBO.OrderItem item); + + @Mapping(source = "userId", target = "userId") + PriceCalculateReqDTO convert(AppTradeOrderCreateReqVO createReqVO, Long userId); + + @Mappings({ + @Mapping(source = "skuId", target = "id"), + @Mapping(source = "count", target = "incrCount"), + }) + ProductSkuUpdateStockReqDTO.Item convert(TradeOrderItemDO bean); + + List convertList(List list); + + default PayOrderCreateReqDTO convert(TradeOrderDO order, List orderItems, + TradePriceCalculateRespBO calculateRespBO, TradeOrderProperties orderProperties) { + PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO() + .setAppId(orderProperties.getAppId()).setUserIp(order.getUserIp()); + // 商户相关字段 + createReqDTO.setMerchantOrderId(String.valueOf(order.getId())); + String subject = calculateRespBO.getItems().get(0).getSpuName(); + if (calculateRespBO.getItems().size() > 1) { + subject += " 等多件"; + } + createReqDTO.setSubject(subject); + createReqDTO.setBody(subject); // TODO 芋艿:临时写死 + // 订单相关字段 + createReqDTO.setPrice(order.getPayPrice()).setExpireTime(addTime(orderProperties.getExpireTime())); + return createReqDTO; + } + + default Set convertPropertyValueIds(List list) { + if (CollUtil.isEmpty(list)) { + return new HashSet<>(); + } + return list.stream().filter(item -> item.getProperties() != null) + .flatMap(p -> p.getProperties().stream()) // 遍历多个 Property 属性 + .map(TradeOrderItemDO.Property::getValueId) // 将每个 Property 转换成对应的 propertyId,最后形成集合 + .collect(Collectors.toSet()); + } + + // TODO 芋艿:可简化 + default PageResult convertPage(PageResult pageResult, List orderItems, + List propertyValueDetails, + Map memberUserRespDTOMap) { + Map> orderItemMap = convertMultiMap(orderItems, TradeOrderItemDO::getOrderId); + Map propertyValueDetailMap = convertMap(propertyValueDetails, ProductPropertyValueDetailRespDTO::getValueId); + // 转化 List + List orderVOs = CollectionUtils.convertList(pageResult.getList(), order -> { + List xOrderItems = orderItemMap.get(order.getId()); + TradeOrderPageItemRespVO orderVO = convert(order, xOrderItems); + if (CollUtil.isNotEmpty(xOrderItems)) { + // 处理商品属性 + for (int i = 0; i < xOrderItems.size(); i++) { + List properties = xOrderItems.get(i).getProperties(); + if (CollUtil.isEmpty(properties)) { + continue; + } + TradeOrderPageItemRespVO.Item item = orderVO.getItems().get(i); + item.setProperties(new ArrayList<>(properties.size())); + // 遍历每个 properties,设置到 TradeOrderPageItemRespVO.Item 中 + properties.forEach(property -> { + ProductPropertyValueDetailRespDTO propertyValueDetail = propertyValueDetailMap.get(property.getValueId()); + if (propertyValueDetail == null) { + return; + } + item.getProperties().add(convert(propertyValueDetail)); + }); + } + } + // 处理收货地址 + orderVO.setReceiverAreaName(AreaUtils.format(order.getReceiverAreaId())); + // 增加用户昵称 + orderVO.setUser(memberUserRespDTOMap.get(orderVO.getUserId())); + return orderVO; + }); + return new PageResult<>(orderVOs, pageResult.getTotal()); + } + + TradeOrderPageItemRespVO convert(TradeOrderDO order, List items); + + ProductPropertyValueDetailRespVO convert(ProductPropertyValueDetailRespDTO bean); + + // TODO 芋艿:可简化 + default TradeOrderDetailRespVO convert(TradeOrderDO order, List orderItems, + List propertyValueDetails, MemberUserRespDTO user) { + TradeOrderDetailRespVO orderVO = convert2(order, orderItems); + // 处理商品属性 + Map propertyValueDetailMap = convertMap(propertyValueDetails, ProductPropertyValueDetailRespDTO::getValueId); + for (int i = 0; i < orderItems.size(); i++) { + List properties = orderItems.get(i).getProperties(); + if (CollUtil.isEmpty(properties)) { + continue; + } + TradeOrderDetailRespVO.Item item = orderVO.getItems().get(i); + item.setProperties(new ArrayList<>(properties.size())); + // 遍历每个 properties,设置到 TradeOrderPageItemRespVO.Item 中 + properties.forEach(property -> { + ProductPropertyValueDetailRespDTO propertyValueDetail = propertyValueDetailMap.get(property.getValueId()); + if (propertyValueDetail == null) { + return; + } + item.getProperties().add(convert(propertyValueDetail)); + }); + } + // 处理收货地址 + orderVO.setReceiverAreaName(AreaUtils.format(order.getReceiverAreaId())); + // 处理用户信息 + orderVO.setUser(convert(user)); + return orderVO; + } + + TradeOrderDetailRespVO convert2(TradeOrderDO order, List items); + + MemberUserRespVO convert(MemberUserRespDTO bean); + + // TODO 芋艿:可简化 + default PageResult convertPage02(PageResult pageResult, List orderItems, + List propertyValueDetails) { + Map> orderItemMap = convertMultiMap(orderItems, TradeOrderItemDO::getOrderId); + Map propertyValueDetailMap = convertMap(propertyValueDetails, ProductPropertyValueDetailRespDTO::getValueId); + // 转化 List + List orderVOs = CollectionUtils.convertList(pageResult.getList(), order -> { + List xOrderItems = orderItemMap.get(order.getId()); + AppTradeOrderPageItemRespVO orderVO = convert02(order, xOrderItems); + if (CollUtil.isNotEmpty(xOrderItems)) { + // 处理商品属性 + for (int i = 0; i < xOrderItems.size(); i++) { + List properties = xOrderItems.get(i).getProperties(); + if (CollUtil.isEmpty(properties)) { + continue; + } + AppTradeOrderItemRespVO item = orderVO.getItems().get(i); + item.setProperties(new ArrayList<>(properties.size())); + // 遍历每个 properties,设置到 TradeOrderPageItemRespVO.Item 中 + properties.forEach(property -> { + ProductPropertyValueDetailRespDTO propertyValueDetail = propertyValueDetailMap.get(property.getValueId()); + if (propertyValueDetail == null) { + return; + } + item.getProperties().add(convert02(propertyValueDetail)); + }); + } + } + return orderVO; + }); + return new PageResult<>(orderVOs, pageResult.getTotal()); + } + + AppTradeOrderPageItemRespVO convert02(TradeOrderDO order, List items); + + AppProductPropertyValueDetailRespVO convert02(ProductPropertyValueDetailRespDTO bean); + + // TODO 芋艿:可简化 + default AppTradeOrderDetailRespVO convert02(TradeOrderDO order, List orderItems, + List propertyValueDetails, TradeOrderProperties tradeOrderProperties) { + AppTradeOrderDetailRespVO orderVO = convert3(order, orderItems); + orderVO.setPayExpireTime(addTime(tradeOrderProperties.getExpireTime())); + if (StrUtil.isNotEmpty(order.getPayChannelCode())) { + orderVO.setPayChannelName(DictFrameworkUtils.getDictDataLabel(DictTypeConstants.CHANNEL_CODE, order.getPayChannelCode())); + } + // 处理商品属性 + Map propertyValueDetailMap = convertMap(propertyValueDetails, ProductPropertyValueDetailRespDTO::getValueId); + for (int i = 0; i < orderItems.size(); i++) { + List properties = orderItems.get(i).getProperties(); + if (CollUtil.isEmpty(properties)) { + continue; + } + AppTradeOrderItemRespVO item = orderVO.getItems().get(i); + item.setProperties(new ArrayList<>(properties.size())); + // 遍历每个 properties,设置到 TradeOrderPageItemRespVO.Item 中 + properties.forEach(property -> { + ProductPropertyValueDetailRespDTO propertyValueDetail = propertyValueDetailMap.get(property.getValueId()); + if (propertyValueDetail == null) { + return; + } + item.getProperties().add(convert02(propertyValueDetail)); + }); + } + // 处理收货地址 + orderVO.setReceiverAreaName(AreaUtils.format(order.getReceiverAreaId())); + return orderVO; + } + + AppTradeOrderDetailRespVO convert3(TradeOrderDO order, List items); + + AppTradeOrderItemRespVO convert03(TradeOrderItemDO bean); + + @Mappings({ + @Mapping(target = "skuId", source = "tradeOrderItemDO.skuId"), + @Mapping(target = "orderId", source = "tradeOrderItemDO.orderId"), + @Mapping(target = "orderItemId", source = "tradeOrderItemDO.id"), + @Mapping(target = "descriptionScores", source = "createReqVO.descriptionScores"), + @Mapping(target = "benefitScores", source = "createReqVO.benefitScores"), + @Mapping(target = "content", source = "createReqVO.content"), + @Mapping(target = "picUrls", source = "createReqVO.picUrls"), + @Mapping(target = "anonymous", source = "createReqVO.anonymous"), + @Mapping(target = "userId", source = "tradeOrderItemDO.userId") + }) + ProductCommentCreateReqDTO convert04(AppTradeOrderItemCommentCreateReqVO createReqVO, TradeOrderItemDO tradeOrderItemDO); + + default TradePriceCalculateReqBO convert(Long userId, AppTradeOrderSettlementReqVO settlementReqVO, + List cartList) { + TradePriceCalculateReqBO reqBO = new TradePriceCalculateReqBO(); + reqBO.setUserId(userId).setCouponId(settlementReqVO.getCouponId()).setAddressId(settlementReqVO.getAddressId()) + .setItems(new ArrayList<>(settlementReqVO.getItems().size())); + // 商品项的构建 + Map cartMap = convertMap(cartList, TradeCartDO::getId); + for (AppTradeOrderSettlementReqVO.Item item : settlementReqVO.getItems()) { + // 情况一:skuId + count + if (item.getSkuId() != null) { + reqBO.getItems().add(new TradePriceCalculateReqBO.Item().setSkuId(item.getSkuId()).setCount(item.getCount()) + .setSelected(true)); // true 的原因,下单一定选中 + continue; + } + // 情况二:cartId + TradeCartDO cart = cartMap.get(item.getCartId()); + if (cart == null) { + continue; + } + reqBO.getItems().add(new TradePriceCalculateReqBO.Item().setSkuId(cart.getSkuId()).setCount(cart.getCount()) + .setCartId(item.getCartId()).setSelected(true)); // true 的原因,下单一定选中 + } + return reqBO; + } + + default AppTradeOrderSettlementRespVO convert(TradePriceCalculateRespBO calculate, AddressRespDTO address) { + AppTradeOrderSettlementRespVO respVO = convert0(calculate, address); + if (address != null) { + respVO.getAddress().setAreaName(AreaUtils.format(address.getAreaId())); + } + // TODO 芋艿:积分的接入; + respVO.setUsedPoint(1); + respVO.setTotalPoint(100); + return respVO; + } + + AppTradeOrderSettlementRespVO convert0(TradePriceCalculateRespBO calculate, AddressRespDTO address); + + @Mappings({ + @Mapping(target = "activityId", source = "createReqVO.combinationActivityId"), + @Mapping(target = "spuId", source = "orderItem.spuId"), + @Mapping(target = "skuId", source = "orderItem.skuId"), + @Mapping(target = "userId", source = "order.userId"), + @Mapping(target = "orderId", source = "order.id"), + @Mapping(target = "headId", source = "createReqVO.combinationHeadId"), + @Mapping(target = "spuName", source = "orderItem.spuName"), + @Mapping(target = "picUrl", source = "orderItem.picUrl"), + @Mapping(target = "combinationPrice", source = "orderItem.payPrice"), + @Mapping(target = "nickname", source = "user.nickname"), + @Mapping(target = "avatar", source = "user.avatar"), + @Mapping(target = "status", ignore = true) + }) + CombinationRecordReqDTO convert(TradeOrderDO order, TradeOrderItemDO orderItem, AppTradeOrderCreateReqVO createReqVO, MemberUserRespDTO user); + + TradeOrderDeliveryDO covert(Long orderId, Long orderItemId, Long userId, Integer deliveryType, Long logisticsId, String logisticsNo); + + default List covert(TradeOrderDO order, TradeOrderDeliveryReqVO deliveryReqVO) { + ArrayList arrayList = new ArrayList<>(); + deliveryReqVO.getOrderItemIds().forEach(item -> { + arrayList.add(covert(order.getId(), item, order.getUserId(), deliveryReqVO.getType(), + deliveryReqVO.getLogisticsId(), deliveryReqVO.getLogisticsNo())); + }); + return arrayList; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/aftersale/TradeAfterSaleDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/aftersale/TradeAfterSaleDO.java new file mode 100644 index 0000000..e68bd4e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/aftersale/TradeAfterSaleDO.java @@ -0,0 +1,201 @@ +package com.yunxi.scm.module.trade.dal.dataobject.aftersale; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleStatusEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleTypeEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleWayEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 交易售后,用于处理 {@link TradeOrderDO} 交易订单的退款退货流程 + * + * @author 芋道源码 + */ +@TableName(value = "trade_after_sale", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class TradeAfterSaleDO extends BaseDO { + + /** + * 售后编号,主键自增 + */ + private Long id; + /** + * 售后流水号 + * + * 例如说,1146347329394184195 + */ + private String no; + /** + * 退款状态 + * + * 枚举 {@link TradeAfterSaleStatusEnum} + */ + private Integer status; + /** + * 售后方式 + * + * 枚举 {@link TradeAfterSaleWayEnum} + */ + private Integer way; + /** + * 售后类型 + * + * 枚举 {@link TradeAfterSaleTypeEnum} + */ + private Integer type; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 申请原因 + * + * type = 退款,对应 trade_after_sale_refund_reason 类型 + * type = 退货退款,对应 trade_after_sale_refund_and_return_reason 类型 + */ + private String applyReason; + /** + * 补充描述 + */ + private String applyDescription; + /** + * 补充凭证图片 + * + * 数组,以逗号分隔 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List applyPicUrls; + + // ========== 交易订单相关 ========== + /** + * 交易订单编号 + * + * 关联 {@link TradeOrderDO#getId()} + */ + private Long orderId; + /** + * 订单流水号 + * + * 冗余 {@link TradeOrderDO#getNo()} + */ + private String orderNo; + /** + * 交易订单项编号 + * + * 关联 {@link TradeOrderItemDO#getId()} + */ + private Long orderItemId; + /** + * 商品 SPU 编号 + * + * 关联 ProductSpuDO 的 id 字段 + * 冗余 {@link TradeOrderItemDO#getSpuId()} + */ + private Long spuId; + /** + * 商品 SPU 名称 + * + * 关联 ProductSkuDO 的 name 字段 + * 冗余 {@link TradeOrderItemDO#getSpuName()} + */ + private String spuName; + /** + * 商品 SKU 编号 + * + * 关联 ProductSkuDO 的编号 + */ + private Long skuId; + /** + * 属性数组,JSON 格式 + * + * 冗余 {@link TradeOrderItemDO#getProperties()} + */ + @TableField(typeHandler = TradeOrderItemDO.PropertyTypeHandler.class) + private List properties; + /** + * 商品图片 + * + * 冗余 {@link TradeOrderItemDO#getPicUrl()} + */ + private String picUrl; + /** + * 退货商品数量 + */ + private Integer count; + + // ========== 审批相关 ========== + + /** + * 审批时间 + */ + private LocalDateTime auditTime; + /** + * 审批人 + * + * 关联 AdminUserDO 的 id 编号 + */ + private Long auditUserId; + /** + * 审批备注 + * + * 注意,只有审批不通过才会填写 + */ + private String auditReason; + + // ========== 退款相关 ========== + /** + * 退款金额,单位:分。 + */ + private Integer refundPrice; + /** + * 支付退款编号 + * + * 对接 pay-module-biz 支付服务的退款订单编号,即 PayRefundDO 的 id 编号 + */ + private Long payRefundId; + /** + * 退款时间 + */ + private LocalDateTime refundTime; + + // ========== 退货相关 ========== + /** + * 退货物流公司编号 + * + * 关联 LogisticsDO 的 id 编号 + */ + private Long logisticsId; + /** + * 退货物流单号 + */ + private String logisticsNo; + /** + * 退货时间 + */ + private LocalDateTime deliveryTime; + /** + * 收货时间 + */ + private LocalDateTime receiveTime; + /** + * 收货备注 + * + * 注意,只有拒绝收货才会填写 + */ + private String receiveReason; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/aftersale/TradeAfterSaleLogDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/aftersale/TradeAfterSaleLogDO.java new file mode 100644 index 0000000..740efa8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/aftersale/TradeAfterSaleLogDO.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.module.trade.dal.dataobject.aftersale; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.trade.enums.aftersale.AfterSaleOperateTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 交易售后日志 DO + * + * // TODO 可优化:参考淘宝或者有赞:1)增加 action 表示什么操作;2)content 记录每个操作的明细 + * + * @author 芋道源码 + */ +@TableName("trade_after_sale_log") +@KeySequence("trade_after_sale_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeAfterSaleLogDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 1:AdminUserDO 的 id 字段 + * 关联 2:MemberUserDO 的 id 字段 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 售后编号 + * + * 关联 {@link TradeAfterSaleDO#getId()} + */ + private Long afterSaleId; + // todo @CHENCHEN: 改成 Integer 哈;主要未来改文案,不好洗 log 存的字段; + /** + * 操作类型 + * + * 枚举 {@link AfterSaleOperateTypeEnum} + */ + private String operateType; + /** + * 操作明细 + */ + private String content; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/cart/TradeCartDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/cart/TradeCartDO.java new file mode 100644 index 0000000..c5944f5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/cart/TradeCartDO.java @@ -0,0 +1,86 @@ +package com.yunxi.scm.module.trade.dal.dataobject.cart; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * 购物车的商品信息 DO + * + * 每个商品,对应一条记录,通过 {@link #spuId} 和 {@link #skuId} 关联 + * + * @author 芋道源码 + */ +@TableName("trade_cart") +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class TradeCartDO extends BaseDO { + + // ========= 基础字段 BEGIN ========= + + /** + * 编号,唯一自增 + */ + private Long id; + + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + + /** + * 是否添加到购物车 + * + * false - 未添加:用户点击【立即购买】 + * true - 已添加:用户点击【添加购物车】 + * + * 为什么要设计这个字段? + * 配合 orderStatus 字段,可以知道有多少商品,用户点击了【立即购买】,最终多少【确认下单】 + */ + private Boolean addStatus; + /** + * 是否提交订单 + * + * false - 未下单:立即购买,或者添加到购物车,此时设置为 false + * true - 已下单:确认下单,此时设置为 true + */ + private Boolean orderStatus; + + // ========= 基础字段 END ========= + + // ========= 商品信息 BEGIN ========= + + /** + * 商品 SPU 编号 + * + * 关联 ProductSpuDO 的 id 编号 + */ + private Long spuId; + /** + * 商品 SKU 编号 + * + * 关联 ProductSkuDO 的 id 编号 + */ + private Long skuId; + /** + * 商品购买数量 + */ + private Integer count; + + // ========= 商品信息 END ========= + + // ========= 优惠信息 BEGIN ========= + + // TODO 芋艿:combination_id 拼团 ID + // TODO 芋艿:seckill_id 秒杀产品 ID + // TODO 芋艿:bargain_id 砍价 ID + // TODO 芋艿:pinkId 团长拼团 ID + + // ========= 优惠信息 END ========= + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressDO.java new file mode 100644 index 0000000..1ac5a93 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressDO.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.trade.dal.dataobject.delivery; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +/** + * 快递公司 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_express") +@KeySequence("trade_delivery_express_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryExpressDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + + /** + * 快递公司 code + */ + private String code; + + /** + * 快递公司名称 + */ + private String name; + + /** + * 快递公司 logo + */ + private String logo; + + /** + * 排序 + */ + private Integer sort; + + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + // TODO 芋艿:c 和结算相关的字段,后续在看 + // partnerId 是否需要月结账号 + // partnerKey 是否需要月结密码 + // net 是否需要取件网店 + // account 账号 + // password 网点名称 + // isShow 是否显示 +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateChargeDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateChargeDO.java new file mode 100644 index 0000000..3405a0f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateChargeDO.java @@ -0,0 +1,67 @@ +package com.yunxi.scm.module.trade.dal.dataobject.delivery; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.IntegerListTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.List; + +/** + * 快递运费模板计费配置 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_express_template_charge", autoResultMap = true) +@KeySequence("trade_delivery_express_template_charge_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryExpressTemplateChargeDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + + /** + * 配送模板编号 + * + * 关联 {@link DeliveryExpressTemplateDO#getId()} + */ + private Long templateId; + + /** + * 配送区域编号列表 + */ + @TableField(typeHandler = IntegerListTypeHandler.class) + private List areaIds; + + /** + * 配送计费方式 + * + * 冗余 {@link DeliveryExpressTemplateDO#getChargeMode()} + */ + private Integer chargeMode; + + /** + * 首件数量(件数,重量,或体积) + */ + private Double startCount; + /** + * 起步价,单位:分 + */ + private Integer startPrice; + + /** + * 续件数量(件, 重量,或体积) + */ + private Double extraCount; + /** + * 额外价,单位:分 + */ + private Integer extraPrice; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateDO.java new file mode 100644 index 0000000..0343266 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateDO.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.trade.dal.dataobject.delivery; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 快递运费模板 DO + * + * @author jason + */ +@TableName("trade_delivery_express_template") +@KeySequence("trade_delivery_express_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryExpressTemplateDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + + /** + * 模板名称 + */ + private String name; + + /** + * 配送计费方式 + * + * 枚举 {@link DeliveryExpressChargeModeEnum} + */ + private Integer chargeMode; + + /** + * 排序 + */ + private Integer sort; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateFreeDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateFreeDO.java new file mode 100644 index 0000000..127364d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryExpressTemplateFreeDO.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.trade.dal.dataobject.delivery; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.IntegerListTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.util.List; + +/** + * 快递运费模板包邮配置 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_express_template_free", autoResultMap = true) +@KeySequence("trade_delivery_express_template_free_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryExpressTemplateFreeDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 配送模板编号 + * + * 关联 {@link DeliveryExpressTemplateDO#getId()} + */ + private Long templateId; + + + /** + * 配送区域编号列表 + */ + @TableField(typeHandler = IntegerListTypeHandler.class) + private List areaIds; + + /** + * 包邮金额,单位:分 + * + * 订单总金额 > 包邮金额时,才免运费 + */ + private Integer freePrice; + + /** + * 包邮件数 + * + * 订单总件数 > 包邮件数时,才免运费 + */ + private Integer freeCount; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreDO.java new file mode 100644 index 0000000..cdaa3cc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreDO.java @@ -0,0 +1,84 @@ +package com.yunxi.scm.module.trade.dal.dataobject.delivery; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalTime; + +/** + * 自提门店 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_pick_up_store") +@KeySequence("trade_delivery_pick_up_store_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryPickUpStoreDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 门店名称 + */ + private String name; + + /** + * 门店简介 + */ + private String introduction; + + /** + * 门店手机 + */ + private String phone; + + /** + * 区域编号 + */ + private Integer areaId; + + /** + * 门店详细地址 + */ + private String detailAddress; + + /** + * 门店 logo + */ + private String logo; + + /** + * 营业开始时间 + */ + private LocalTime openingTime; + + /** + * 营业结束时间 + */ + private LocalTime closingTime; + + /** + * 纬度 + */ + private Double latitude; + /** + * 经度 + */ + private Double longitude; + + /** + * 门店状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreStaffDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreStaffDO.java new file mode 100644 index 0000000..1d7ea89 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/delivery/DeliveryPickUpStoreStaffDO.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.trade.dal.dataobject.delivery; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +// TODO @芋艿:后续再详细 review 一轮 +// TODO @芋艿:可能改成 DeliveryPickUpStoreUserDO +/** + * 自提门店店员 DO + * + * @author jason + */ +@TableName(value ="trade_delivery_pick_up_store_staff") +@KeySequence("trade_delivery_pick_up_store_staff_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class DeliveryPickUpStoreStaffDO extends BaseDO { + + /** + * 编号,自增 + */ + @TableId + private Long id; + + /** + * 自提门店编号 + * + * 关联 {@link DeliveryPickUpStoreDO#getId()} + */ + private Long storeId; + + /** + * 管理员用户id + * + * 关联 {AdminUserDO#getId()} + */ + private Long adminUserId; + + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderDO.java new file mode 100644 index 0000000..a26d880 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderDO.java @@ -0,0 +1,256 @@ +package com.yunxi.scm.module.trade.dal.dataobject.order; + +import com.yunxi.scm.framework.common.enums.TerminalEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryTypeEnum; +import com.yunxi.scm.module.trade.enums.order.*; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 交易订单 DO + * + * @author 芋道源码 + */ +@TableName("trade_order") +@KeySequence("trade_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeOrderDO extends BaseDO { + + // ========== 订单基本信息 ========== + /** + * 订单编号,主键自增 + */ + private Long id; + /** + * 订单流水号 + * + * 例如说,1146347329394184195 + */ + private String no; + /** + * 订单类型 + * + * 枚举 {@link TradeOrderTypeEnum} + */ + private Integer type; + /** + * 订单来源 + * + * 枚举 {@link TerminalEnum} + */ + private Integer terminal; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 用户 IP + */ + private String userIp; + /** + * 用户备注 + */ + private String userRemark; + /** + * 订单状态 + * + * 枚举 {@link TradeOrderStatusEnum} + */ + private Integer status; + /** + * 购买的商品数量 + */ + private Integer productCount; + /** + * 订单完成时间 + */ + private LocalDateTime finishTime; + /** + * 订单取消时间 + */ + private LocalDateTime cancelTime; + /** + * 取消类型 + * + * 枚举 {@link TradeOrderCancelTypeEnum} + */ + private Integer cancelType; + /** + * 商家备注 + */ + private String remark; + /** + * 是否评价 + * + * true - 已评价 + * false - 未评价 + */ + private Boolean commentStatus; + + // ========== 价格 + 支付基本信息 ========== + + // 价格文档 - 淘宝:https://open.taobao.com/docV3.htm?docId=108471&docType=1 + // 价格文档 - 京东到家:https://openo2o.jddj.com/api/getApiDetail/182/4d1494c5e7ac4679bfdaaed950c5bc7f.htm + // 价格文档 - 有赞:https://doc.youzanyun.com/detail/API/0/906 + + /** + * 支付订单编号 + * + * 对接 pay-module-biz 支付服务的支付订单编号,即 PayOrderDO 的 id 编号 + */ + private Long payOrderId; + /** + * 是否已支付 + * + * true - 已经支付过 + * false - 没有支付过 + */ + private Boolean payStatus; + /** + * 付款时间 + */ + private LocalDateTime payTime; + /** + * 支付渠道 + * + * 对应 PayChannelEnum 枚举 + */ + private String payChannelCode; + + /** + * 商品原价,单位:分 + * + * totalPrice = {@link TradeOrderItemDO#getPrice()} * {@link TradeOrderItemDO#getCount()} 求和 + * + * 对应 taobao 的 trade.total_fee 字段 + */ + private Integer totalPrice; + /** + * 优惠金额,单位:分 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 运费金额,单位:分 + */ + private Integer deliveryPrice; + /** + * 订单调价,单位:分 + * + * 正数,加价;负数,减价 + */ + private Integer adjustPrice; + /** + * 应付金额(总),单位:分 + * + * = {@link #totalPrice} + * - {@link #couponPrice} + * - {@link #pointPrice} + * - {@link #discountPrice} + * + {@link #deliveryPrice} + * + {@link #adjustPrice} + */ + private Integer payPrice; + + // ========== 收件 + 物流基本信息 ========== + /** + * 配送方式 + * + * 枚举 {@link DeliveryTypeEnum} + */ + private Integer deliveryType; + /** + * 发货物流公司编号 + */ + private Long logisticsId; + /** + * 发货物流单号 + */ + private String logisticsNo; + /** + * 发货状态 + * + * 枚举 {@link TradeOrderDeliveryStatusEnum} + */ + private Integer deliveryStatus; + /** + * 发货时间 + */ + private LocalDateTime deliveryTime; + + /** + * 收货时间 + */ + private LocalDateTime receiveTime; + /** + * 收件人名称 + */ + private String receiverName; + /** + * 收件人手机 + */ + private String receiverMobile; + /** + * 收件人地区编号 + */ + private Integer receiverAreaId; + /** + * 收件人详细地址 + */ + private String receiverDetailAddress; + + /** + * 自提门店编号 + * + * 关联 {@link DeliveryPickUpStoreDO#getId()} + */ + private Long pickUpStoreId; + + // ========== 售后基本信息 ========== + /** + * 售后状态 + * + * 枚举 {@link TradeOrderRefundStatusEnum} + */ + private Integer refundStatus; + /** + * 退款金额,单位:分 + * + * 注意,退款并不会影响 {@link #payPrice} 实际支付金额 + * 也就说,一个订单最终产生多少金额的收入 = payPrice - refundPrice + */ + private Integer refundPrice; + + // ========== 营销基本信息 ========== + /** + * 优惠劵编号 + */ + private Long couponId; + /** + * 优惠劵减免金额,单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + // TODO 芋艿:需要记录使用的积分; + /** + * 积分抵扣的金额,单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderDeliveryDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderDeliveryDO.java new file mode 100644 index 0000000..eee2e9e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderDeliveryDO.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.trade.dal.dataobject.order; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +// TODO @puhui999: +/** + * 交易订单发货记录 DO + * + * @author HUIHUI + */ +@TableName("trade_order_delivery") +@KeySequence("trade_order_delivery_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TradeOrderDeliveryDO extends BaseDO { + + /** + * 订单发货记录 id + */ + private Long id; + /** + * 订单 id + */ + private Long orderId; + /** + * 订单项 id TODO 要不要一个发货记录可对应多个订单项? + */ + private Long orderItemId; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 配送方式 + * + * 枚举 {@link DeliveryTypeEnum} + */ + private Integer deliveryType; + /** + * 发货物流公司编号 + */ + private Long logisticsId; + /** + * 发货物流单号 + */ + private String logisticsNo; + + // TODO 同城配送 + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderItemDO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderItemDO.java new file mode 100644 index 0000000..0e97c90 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/dataobject/order/TradeOrderItemDO.java @@ -0,0 +1,207 @@ +package com.yunxi.scm.module.trade.dal.dataobject.order; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleDO; +import com.yunxi.scm.module.trade.dal.dataobject.cart.TradeCartDO; +import com.yunxi.scm.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.util.List; + +/** + * 交易订单项 DO + * + * @author 芋道源码 + */ +@TableName(value = "trade_order_item", autoResultMap = true) +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class TradeOrderItemDO extends BaseDO { + + // ========== 订单项基本信息 ========== + /** + * 编号 + */ + private Long id; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + /** + * 订单编号 + * + * 关联 {@link TradeOrderDO#getId()} + */ + private Long orderId; + /** + * 购物车项编号 + * + * 关联 {@link TradeCartDO#getId()} + */ + private Long cartId; + + // ========== 商品基本信息; 冗余较多字段,减少关联查询 ========== + /** + * 商品 SPU 编号 + * + * 关联 ProductSkuDO 的 spuId 编号 + */ + private Long spuId; + /** + * 商品 SPU 名称 + * + * 冗余 ProductSkuDO 的 spuName 编号 + */ + private String spuName; + /** + * 商品 SKU 编号 + * + * 关联 ProductSkuDO 的 id 编号 + */ + private Long skuId; + /** + * 属性数组,JSON 格式 + * + * 冗余 ProductSkuDO 的 properties 字段 + */ + @TableField(typeHandler = PropertyTypeHandler.class) + private List properties; + /** + * 商品图片 + */ + private String picUrl; + /** + * 购买数量 + */ + private Integer count; + /** + * 是否评价 + * + * true - 已评价 + * false - 未评价 + */ + private Boolean commentStatus; + + // ========== 价格 + 支付基本信息 ========== + + /** + * 商品原价(单),单位:分 + * + * 对应 ProductSkuDO 的 price 字段 + * 对应 taobao 的 order.price 字段 + */ + private Integer price; + /** + * 优惠金额(总),单位:分 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 运费金额(总),单位:分 + */ + private Integer deliveryPrice; + /** + * 订单调价(总),单位:分 + * + * 正数,加价;负数,减价 + */ + private Integer adjustPrice; + /** + * 应付金额(总),单位:分 + * + * = {@link #price} * {@link #count} + * - {@link #couponPrice} + * - {@link #pointPrice} + * - {@link #discountPrice} + * + {@link #deliveryPrice} + * + {@link #adjustPrice} + */ + private Integer payPrice; + + // ========== 营销基本信息 ========== + + /** + * 优惠劵减免金额,单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + /** + * 积分抵扣的金额,单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + + // ========== 售后基本信息 ========== + /** + * 售后状态 + * + * 枚举 {@link TradeOrderItemAfterSaleStatusEnum} + * + * @see TradeAfterSaleDO + */ + private Integer afterSaleStatus; + + /** + * 商品属性 + */ + @Data + public static class Property implements Serializable { + + /** + * 属性编号 + * + * 关联 ProductPropertyDO 的 id 编号 + */ + private Long propertyId; + /** + * 属性名字 + * + * 关联 ProductPropertyDO 的 name 字段 + */ + private String propertyName; + + /** + * 属性值编号 + * + * 关联 ProductPropertyValueDO 的 id 编号 + */ + private Long valueId; + /** + * 属性值名字 + * + * 关联 ProductPropertyValueDO 的 name 字段 + */ + private String valueName; + + } + + // TODO @芋艿:可以找一些新的思路 + public static class PropertyTypeHandler extends AbstractJsonTypeHandler> { + + @Override + protected List parse(String json) { + return JsonUtils.parseArray(json, Property.class); + } + + @Override + protected String toJson(List obj) { + return JsonUtils.toJsonString(obj); + } + + } + +} + diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/aftersale/TradeAfterSaleLogMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/aftersale/TradeAfterSaleLogMapper.java new file mode 100644 index 0000000..8c38c99 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/aftersale/TradeAfterSaleLogMapper.java @@ -0,0 +1,9 @@ +package com.yunxi.scm.module.trade.dal.mysql.aftersale; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleLogDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TradeAfterSaleLogMapper extends BaseMapperX { +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/aftersale/TradeAfterSaleMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/aftersale/TradeAfterSaleMapper.java new file mode 100644 index 0000000..8382545 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/aftersale/TradeAfterSaleMapper.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.trade.dal.mysql.aftersale; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSalePageReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface TradeAfterSaleMapper extends BaseMapperX { + + default PageResult selectPage(TradeAfterSalePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TradeAfterSaleDO::getNo, reqVO.getNo()) + .eqIfPresent(TradeAfterSaleDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TradeAfterSaleDO::getType, reqVO.getType()) + .eqIfPresent(TradeAfterSaleDO::getWay, reqVO.getWay()) + .likeIfPresent(TradeAfterSaleDO::getOrderNo, reqVO.getOrderNo()) + .likeIfPresent(TradeAfterSaleDO::getSpuName, reqVO.getSpuName()) + .betweenIfPresent(TradeAfterSaleDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TradeAfterSaleDO::getId)); + } + + default int updateByIdAndStatus(Long id, Integer status, TradeAfterSaleDO update) { + return update(update, new LambdaUpdateWrapper() + .eq(TradeAfterSaleDO::getId, id).eq(TradeAfterSaleDO::getStatus, status)); + } + + default TradeAfterSaleDO selectByPayRefundId(Long payRefundId) { + return selectOne(TradeAfterSaleDO::getPayRefundId, payRefundId); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/cart/TradeCartMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/cart/TradeCartMapper.java new file mode 100644 index 0000000..35cb4cd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/cart/TradeCartMapper.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.trade.dal.mysql.cart; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.trade.dal.dataobject.cart.TradeCartDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Mapper +public interface TradeCartMapper extends BaseMapperX { + + default TradeCartDO selectByUserIdAndSkuId(Long userId, Long skuId, + Boolean addStatus, Boolean orderStatus) { + return selectOne(new LambdaQueryWrapper().eq(TradeCartDO::getUserId, userId) + .eq(TradeCartDO::getSkuId, skuId) + .eq(TradeCartDO::getAddStatus, addStatus) + .eq(TradeCartDO::getOrderStatus, orderStatus)); + } + + default Integer selectSumByUserId(Long userId) { + // SQL sum 查询 + List> result = selectMaps(new QueryWrapper() + .select("SUM(count) AS sumCount") + .eq("user_id", userId) + .eq("add_status", true) // 只计算添加到购物车中的 + .eq("order_status", false)); // 必须未下单 + // 获得数量 + return CollUtil.getFirst(result) != null ? MapUtil.getInt(result.get(0), "sumCount") : 0; + } + + default Map selectSumMapByUserId(Long userId) { + // SQL sum 查询 + List> result = selectMaps(new QueryWrapper() + .select("spu_id, SUM(count) AS sumCount") + .eq("user_id", userId) + .eq("add_status", true) // 只计算添加到购物车中的 + .eq("order_status", false) // 必须未下单 + .groupBy("spu_id")); + // 获得数量 + return CollectionUtils.convertMap(result, item -> MapUtil.getLong(item, "spu_id"), + item -> MapUtil.getInt(item, "sumCount")); + } + + default TradeCartDO selectById(Long id, Long userId) { + return selectOne(TradeCartDO::getId, id, + TradeCartDO::getUserId, userId); + } + + default List selectListByIds(Collection ids, Long userId) { + return selectList(new LambdaQueryWrapper() + .in(TradeCartDO::getId, ids) + .eq(TradeCartDO::getUserId, userId)); + } + + default List selectListByUserId(Long userId, Boolean addStatus, Boolean orderStatus) { + return selectList(new LambdaQueryWrapper() + .eq(TradeCartDO::getUserId, userId) + .eq(TradeCartDO::getAddStatus, addStatus) + .eq(TradeCartDO::getOrderStatus, orderStatus)); + } + + default void updateByIds(Collection ids, TradeCartDO updateObject) { + update(updateObject, new LambdaQueryWrapper().in(TradeCartDO::getId, ids)); + } + + default List selectListByUserId(Long userId, Set ids) { + return selectList(new LambdaQueryWrapper() + .eq(TradeCartDO::getUserId, userId) + .in(TradeCartDO::getId, ids)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressMapper.java new file mode 100644 index 0000000..053827b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressMapper.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.trade.dal.mysql.delivery; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressExportReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressPageReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface DeliveryExpressMapper extends BaseMapperX { + + default PageResult selectPage(DeliveryExpressPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DeliveryExpressDO::getCode, reqVO.getCode()) + .likeIfPresent(DeliveryExpressDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryExpressDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DeliveryExpressDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(DeliveryExpressDO::getSort)); + } + + default List selectList(DeliveryExpressExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DeliveryExpressDO::getCode, reqVO.getCode()) + .likeIfPresent(DeliveryExpressDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryExpressDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DeliveryExpressDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(DeliveryExpressDO::getSort)); + } + + default DeliveryExpressDO selectByCode(String code) { + return selectOne(new LambdaQueryWrapper() + .eq(DeliveryExpressDO::getCode, code)); + } +} + + + + diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateChargeMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateChargeMapper.java new file mode 100644 index 0000000..9b5ee58 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateChargeMapper.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.trade.dal.mysql.delivery; + + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateChargeDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DeliveryExpressTemplateChargeMapper extends BaseMapperX { + + default List selectListByTemplateId(Long templateId){ + return selectList(new LambdaQueryWrapper() + .eq(DeliveryExpressTemplateChargeDO::getTemplateId, templateId)); + } + + default int deleteByTemplateId(Long templateId){ + return delete(new LambdaQueryWrapper() + .eq(DeliveryExpressTemplateChargeDO::getTemplateId, templateId)); + } + + default List selectByTemplateIds(Collection templateIds) { + return selectList(DeliveryExpressTemplateChargeDO::getTemplateId, templateIds); + } + +} + + + + diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateFreeMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateFreeMapper.java new file mode 100644 index 0000000..849858c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateFreeMapper.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.trade.dal.mysql.delivery; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateFreeDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DeliveryExpressTemplateFreeMapper extends BaseMapperX { + + default List selectListByTemplateId(Long templateId) { + return selectList(new LambdaQueryWrapper() + .eq(DeliveryExpressTemplateFreeDO::getTemplateId, templateId)); + } + + default int deleteByTemplateId(Long templateId) { + return delete(new LambdaQueryWrapper() + .eq(DeliveryExpressTemplateFreeDO::getTemplateId, templateId)); + } + + default List selectListByTemplateIds(Collection templateIds) { + return selectList(DeliveryExpressTemplateFreeDO::getTemplateId, templateIds); + } +} + + + + diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateMapper.java new file mode 100644 index 0000000..8be33f5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryExpressTemplateMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.trade.dal.mysql.delivery; + + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplatePageReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DeliveryExpressTemplateMapper extends BaseMapperX { + + default PageResult selectPage(DeliveryExpressTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DeliveryExpressTemplateDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryExpressTemplateDO::getChargeMode, reqVO.getChargeMode()) + .betweenIfPresent(DeliveryExpressTemplateDO::getCreateTime, reqVO.getCreateTime()) + .orderByAsc(DeliveryExpressTemplateDO::getSort)); + } + + default DeliveryExpressTemplateDO selectByName(String name) { + return selectOne(DeliveryExpressTemplateDO::getName,name); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryPickUpStoreMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryPickUpStoreMapper.java new file mode 100644 index 0000000..9dc8b4b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryPickUpStoreMapper.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.trade.dal.mysql.delivery; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreExportReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStorePageReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface DeliveryPickUpStoreMapper extends BaseMapperX { + + default PageResult selectPage(DeliveryPickUpStorePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DeliveryPickUpStoreDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryPickUpStoreDO::getPhone, reqVO.getPhone()) + .eqIfPresent(DeliveryPickUpStoreDO::getAreaId, reqVO.getAreaId()) + .eqIfPresent(DeliveryPickUpStoreDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DeliveryPickUpStoreDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(DeliveryPickUpStoreDO::getId)); + } + + default List selectList(DeliveryPickUpStoreExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DeliveryPickUpStoreDO::getName, reqVO.getName()) + .eqIfPresent(DeliveryPickUpStoreDO::getPhone, reqVO.getPhone()) + .eqIfPresent(DeliveryPickUpStoreDO::getAreaId, reqVO.getAreaId()) + .eqIfPresent(DeliveryPickUpStoreDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DeliveryPickUpStoreDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(DeliveryPickUpStoreDO::getId)); + } +} + + + + diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryPickUpStoreStaffMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryPickUpStoreStaffMapper.java new file mode 100644 index 0000000..d91d1a3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/delivery/DeliveryPickUpStoreStaffMapper.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.trade.dal.mysql.delivery; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreStaffDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface DeliveryPickUpStoreStaffMapper extends BaseMapperX { + +} + + + + diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderDeliveryMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderDeliveryMapper.java new file mode 100644 index 0000000..8cceb31 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderDeliveryMapper.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.trade.dal.mysql.order; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDeliveryDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +// TODO @puhui999:应该去掉啦 +/** + * 交易订单发货记录 Mapper + * + * @author HUIHUI + */ +@Mapper +public interface TradeOrderDeliveryMapper extends BaseMapperX { + + default List selsectListByOrderIdAndItemIds(Long orderId, List orderItemIds) { + return selectList(new LambdaQueryWrapperX() + .eq(TradeOrderDeliveryDO::getOrderId, orderId).in(TradeOrderDeliveryDO::getOrderItemId, orderItemIds)); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderItemMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderItemMapper.java new file mode 100644 index 0000000..9ae71b2 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderItemMapper.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.trade.dal.mysql.order; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface TradeOrderItemMapper extends BaseMapperX { + + default int updateAfterSaleStatus(Long id, Integer oldAfterSaleStatus, Integer newAfterSaleStatus) { + return update(new TradeOrderItemDO().setAfterSaleStatus(newAfterSaleStatus), + new LambdaUpdateWrapper<>(new TradeOrderItemDO().setId(id).setAfterSaleStatus(oldAfterSaleStatus))); + } + + default List selectListByOrderId(Long orderId) { + return selectList(TradeOrderItemDO::getOrderId, orderId); + } + + default List selectListByOrderId(Collection orderIds) { + return selectList(TradeOrderItemDO::getOrderId, orderIds); + } + + default TradeOrderItemDO selectOrderItemByIdAndUserId(Long orderItemId, Long loginUserId) { + return selectOne(new LambdaQueryWrapperX() + .eq(TradeOrderItemDO::getId, orderItemId) + .eq(TradeOrderItemDO::getUserId, loginUserId)); + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderMapper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderMapper.java new file mode 100644 index 0000000..d207009 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/order/TradeOrderMapper.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.trade.dal.mysql.order; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderPageReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderPageReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Set; + +@Mapper +public interface TradeOrderMapper extends BaseMapperX { + + default int updateByIdAndStatus(Long id, Integer status, TradeOrderDO update) { + return update(update, new LambdaUpdateWrapper() + .eq(TradeOrderDO::getId, id).eq(TradeOrderDO::getStatus, status)); + } + + default TradeOrderDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(TradeOrderDO::getId, id, TradeOrderDO::getUserId, userId); + } + + default PageResult selectPage(TradeOrderPageReqVO reqVO, Set userIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TradeOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(TradeOrderDO::getUserId, reqVO.getUserId()) + .inIfPresent(TradeOrderDO::getUserId, userIds) + .likeIfPresent(TradeOrderDO::getReceiverName, reqVO.getReceiverName()) + .likeIfPresent(TradeOrderDO::getReceiverMobile, reqVO.getReceiverMobile()) + .eqIfPresent(TradeOrderDO::getType, reqVO.getType()) + .eqIfPresent(TradeOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TradeOrderDO::getPayChannelCode, reqVO.getPayChannelCode()) + .eqIfPresent(TradeOrderDO::getTerminal,reqVO.getTerminal()) + .betweenIfPresent(TradeOrderDO::getCreateTime, reqVO.getCreateTime())); + } + + default PageResult selectPage(AppTradeOrderPageReqVO reqVO, Long userId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(TradeOrderDO::getUserId, userId) + .eqIfPresent(TradeOrderDO::getStatus, reqVO.getStatus()) + .eqIfPresent(TradeOrderDO::getCommentStatus, reqVO.getCommentStatus()) + .orderByDesc(TradeOrderDO::getId)); // TODO 芋艿:未来不同的 status,不同的排序 + } + + default Long selectCountByUserIdAndStatus(Long userId, Integer status, Boolean commentStatus) { + return selectCount(new LambdaQueryWrapperX() + .eq(TradeOrderDO::getUserId, userId) + .eqIfPresent(TradeOrderDO::getStatus, status) + .eqIfPresent(TradeOrderDO::getCommentStatus, commentStatus)); + } + + default TradeOrderDO selectOrderByIdAndUserId(Long orderId, Long loginUserId) { + return selectOne(new LambdaQueryWrapperX() + .eq(TradeOrderDO::getId, orderId) + .eq(TradeOrderDO::getUserId, loginUserId)); + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/package-info.java new file mode 100644 index 0000000..bef2362 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/dal/mysql/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 占位 + */ +package com.yunxi.scm.module.trade.dal.mysql; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/config/AfterSaleLogConfiguration.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/config/AfterSaleLogConfiguration.java new file mode 100644 index 0000000..33f080b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/config/AfterSaleLogConfiguration.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.trade.framework.aftersalelog.config; + +import com.yunxi.scm.module.trade.framework.aftersalelog.core.aop.AfterSaleLogAspect; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +// TODO @chenchen:改成 aftersale 好点哈; +/** + * trade 模块的 afterSaleLog 组件的 Configuration + * + * @author 陈賝 + * @since 2023/6/18 11:09 + */ +@Configuration(proxyBeanMethods = false) +public class AfterSaleLogConfiguration { + + @Bean + public AfterSaleLogAspect afterSaleLogAspect() { + return new AfterSaleLogAspect(); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/annotations/AfterSaleLog.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/annotations/AfterSaleLog.java new file mode 100644 index 0000000..76781e5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/annotations/AfterSaleLog.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.trade.framework.aftersalelog.core.annotations; + +import com.yunxi.scm.module.trade.enums.aftersale.AfterSaleOperateTypeEnum; + +import java.lang.annotation.*; + +/** + * 售后日志的注解 + * + * 写在方法上时,会自动记录售后日志 + * + * @author 陈賝 + * @since 2023/6/8 17:04 + * @see com.yunxi.scm.module.trade.framework.aftersalelog.core.aop.AfterSaleLogAspect + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface AfterSaleLog { + + /** + * 售后 ID + */ + String id(); + + /** + * 操作类型 + */ + AfterSaleOperateTypeEnum operateType(); + + /** + * 日志内容 + */ + String content() default ""; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/aop/AfterSaleLogAspect.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/aop/AfterSaleLogAspect.java new file mode 100644 index 0000000..11ba65f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/aop/AfterSaleLogAspect.java @@ -0,0 +1,88 @@ +package com.yunxi.scm.module.trade.framework.aftersalelog.core.aop; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.util.spring.SpringExpressionUtils; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import com.yunxi.scm.module.trade.framework.aftersalelog.core.annotations.AfterSaleLog; +import com.yunxi.scm.module.trade.framework.aftersalelog.core.dto.TradeAfterSaleLogCreateReqDTO; +import com.yunxi.scm.module.trade.framework.aftersalelog.core.service.AfterSaleLogService; +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static java.util.Arrays.asList; + +/** + * 记录售后日志的 AOP 切面 + * + * @author 陈賝 + * @since 2023/6/13 13:54 + */ +@Slf4j +@Aspect +public class AfterSaleLogAspect { + + @Resource + private AfterSaleLogService afterSaleLogService; + + // TODO chenchen: 这个分 3 行把; + private final static String OPERATE_TYPE = "operateType", ID = "id", CONTENT = "content"; + + /** + * 切面存入日志 + */ + @AfterReturning(pointcut = "@annotation(afterSaleLog)", returning = "info") + public void doAfterReturning(JoinPoint joinPoint, AfterSaleLog afterSaleLog, Object info) { + try { + // 日志对象拼接 + Integer userType = WebFrameworkUtils.getLoginUserType(); + Long id = WebFrameworkUtils.getLoginUserId(); + Map formatObj = spelFormat(joinPoint, info); + TradeAfterSaleLogCreateReqDTO dto = new TradeAfterSaleLogCreateReqDTO() + .setUserId(id) + .setUserType(userType) + .setAfterSaleId(MapUtil.getLong(formatObj, ID)) + .setOperateType(MapUtil.getStr(formatObj, OPERATE_TYPE)) + .setContent(MapUtil.getStr(formatObj, CONTENT)); + // 异步存入数据库 + afterSaleLogService.createLog(dto); + } catch (Exception exception) { + log.error("[doAfterReturning][afterSaleLog({}) 日志记录错误]", toJsonString(afterSaleLog), exception); + } + } + + /** + * 获取描述信息 + */ + public static Map spelFormat(JoinPoint joinPoint, Object info) { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + AfterSaleLog afterSaleLogPoint = signature.getMethod().getAnnotation(AfterSaleLog.class); + HashMap result = Maps.newHashMapWithExpectedSize(2); + Map spelMap = SpringExpressionUtils.parseExpression(joinPoint, info, + asList(afterSaleLogPoint.id(), afterSaleLogPoint.content())); + // TODO @chenchen:是不是抽成 3 个方法好点;毕竟 map 太抽象了;; + // 售后ID + String id = MapUtil.getStr(spelMap, afterSaleLogPoint.id()); + result.put(ID, id); + // 操作类型 + String operateType = afterSaleLogPoint.operateType().description(); + result.put(OPERATE_TYPE, operateType); + // 日志内容 + String content = MapUtil.getStr(spelMap, afterSaleLogPoint.content()); + if (ObjectUtil.isNotNull(afterSaleLogPoint.operateType())) { + content += operateType; + } + result.put(CONTENT, content); + return result; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/dto/TradeAfterSaleLogCreateReqDTO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/dto/TradeAfterSaleLogCreateReqDTO.java new file mode 100644 index 0000000..b9440e4 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/dto/TradeAfterSaleLogCreateReqDTO.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.trade.framework.aftersalelog.core.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 售后日志的创建 Request DTO + * + * @author 陈賝 + * @since 2023/6/19 09:54 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TradeAfterSaleLogCreateReqDTO { + + /** + * 编号 + */ + private Long id; + /** + * 用户编号 + * + * 关联 1:AdminUserDO 的 id 字段 + * 关联 2:MemberUserDO 的 id 字段 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 售后编号 + */ + private Long afterSaleId; + /** + * 操作类型 + */ + private String operateType; + /** + * 操作明细 + */ + private String content; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/service/AfterSaleLogService.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/service/AfterSaleLogService.java new file mode 100644 index 0000000..7b1c8d8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/aftersalelog/core/service/AfterSaleLogService.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.trade.framework.aftersalelog.core.service; + + +import com.yunxi.scm.module.trade.framework.aftersalelog.core.dto.TradeAfterSaleLogCreateReqDTO; + +/** + * 交易售后日志 Service 接口 + * + * @author 陈賝 + * @since 2023/6/12 14:18 + */ +public interface AfterSaleLogService { + + /** + * 创建售后日志 + * + * @param logDTO 日志记录 + * @author 陈賝 + * @since 2023/6/12 14:18 + */ + void createLog(TradeAfterSaleLogCreateReqDTO logDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/config/ExpressClientConfig.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/config/ExpressClientConfig.java new file mode 100644 index 0000000..5627e4d --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/config/ExpressClientConfig.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.trade.framework.delivery.config; + +import com.yunxi.scm.module.trade.framework.delivery.core.client.ExpressClient; +import com.yunxi.scm.module.trade.framework.delivery.core.client.ExpressClientFactory; +import com.yunxi.scm.module.trade.framework.delivery.core.client.impl.ExpressClientFactoryImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +/** + * 快递客户端端配置类: + * + * 1. 快递客户端工厂 {@link ExpressClientFactory} + * 2. 默认的快递客户端实现 {@link ExpressClient} + * + * @author jason + */ +@Configuration(proxyBeanMethods = false) +public class ExpressClientConfig { + + @Bean + public ExpressClientFactory expressClientFactory(TradeExpressProperties tradeExpressProperties, + RestTemplate restTemplate) { + return new ExpressClientFactoryImpl(tradeExpressProperties, restTemplate); + } + + @Bean + public ExpressClient defaultExpressClient(ExpressClientFactory expressClientFactory) { + return expressClientFactory.getDefaultExpressClient(); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/config/TradeExpressProperties.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/config/TradeExpressProperties.java new file mode 100644 index 0000000..6d8697a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/config/TradeExpressProperties.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.trade.framework.delivery.config; + +import com.yunxi.scm.module.trade.framework.delivery.core.enums.ExpressClientEnum; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; + +// TODO @芋艿:未来要不要放数据库中?考虑 saas 多租户时,不同租户使用不同的配置? +/** + * 交易运费快递的配置项 + * + * @author jason + */ +@Component +@ConfigurationProperties(prefix = "yunxi.trade.express") +@Data +@Validated +public class TradeExpressProperties { + + /** + * 快递客户端 + * + * 默认不提供,需要提醒用户配置一个快递服务商。 + */ + private ExpressClientEnum client = ExpressClientEnum.NOT_PROVIDE; + + /** + * 快递鸟配置 + */ + @Valid + private KdNiaoConfig kdNiao; + /** + * 快递 100 配置 + */ + @Valid + private Kd100Config kd100; + + /** + * 快递鸟配置项目 + */ + @Data + public static class KdNiaoConfig { + + /** + * 快递鸟用户 ID + */ + @NotEmpty(message = "快递鸟用户 ID 配置项不能为空") + private String businessId; + /** + * 快递鸟 API Key + */ + @NotEmpty(message = "快递鸟 Api Key 配置项不能为空") + private String apiKey; + + } + + /** + * 快递100 配置项 + */ + @Data + public static class Kd100Config { + + /** + * 快递 100 授权码 + */ + @NotEmpty(message = "快递 100 授权码配置项不能为空") + private String customer; + /** + * 快递 100 授权 key + */ + @NotEmpty(message = "快递 100 授权 Key 配置项不能为空") + private String key; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/ExpressClient.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/ExpressClient.java new file mode 100644 index 0000000..5b3a0da --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/ExpressClient.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client; + +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; + +import java.util.List; + +/** + * 快递客户端接口 + * + * @author jason + */ +public interface ExpressClient { + + /** + * 快递实时查询 + * + * @param reqDTO 查询请求参数 + */ + // TODO @jason:返回字段可以参考 https://doc.youzanyun.com/detail/API/0/5 响应的 data + List getExpressTrackList(ExpressTrackQueryReqDTO reqDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/ExpressClientFactory.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/ExpressClientFactory.java new file mode 100644 index 0000000..a423e01 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/ExpressClientFactory.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client; + +import com.yunxi.scm.module.trade.framework.delivery.core.enums.ExpressClientEnum; + +/** + * 快递客户端工厂接口:用于创建和缓存快递客户端 + * + * @author jason + */ +public interface ExpressClientFactory { + + /** + * 获取默认的快递客户端 + */ + ExpressClient getDefaultExpressClient(); + + /** + * 通过枚举获取快递客户端,如果不存在,就创建一个对应快递客户端 + * + * @param clientEnum 快递客户端枚举 + */ + ExpressClient getOrCreateExpressClient(ExpressClientEnum clientEnum); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/convert/ExpressQueryConvert.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/convert/ExpressQueryConvert.java new file mode 100644 index 0000000..f550dd8 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/convert/ExpressQueryConvert.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.convert; + +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kd100.Kd100ExpressQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kd100.Kd100ExpressQueryRespDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kdniao.KdNiaoExpressQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kdniao.KdNiaoExpressQueryRespDTO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface ExpressQueryConvert { + + ExpressQueryConvert INSTANCE = Mappers.getMapper(ExpressQueryConvert.class); + + List convertList(List expressTrackList); + + List convertList2(List expressTrackList); + + KdNiaoExpressQueryReqDTO convert(ExpressTrackQueryReqDTO dto); + + Kd100ExpressQueryReqDTO convert2(ExpressTrackQueryReqDTO dto); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java new file mode 100644 index 0000000..6cbec2c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/ExpressTrackQueryReqDTO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.dto; + +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressDO; +import lombok.Data; + +/** + * 快递轨迹的查询 Req DTO + * + * @author jason + */ +@Data +public class ExpressTrackQueryReqDTO { + + /** + * 快递公司编码 + * + * 对应 {@link DeliveryExpressDO#getCode()} + */ + private String expressCode; + + /** + * 发货快递单号 + */ + private String logisticsNo; + + /** + * 收、寄件人的电话号码 + */ + private String phone; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/ExpressTrackRespDTO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/ExpressTrackRespDTO.java new file mode 100644 index 0000000..016779c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/ExpressTrackRespDTO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.dto; + +import lombok.Data; + +/** + * 快递查询 Resp DTO + * + * @author jason + */ +@Data +public class ExpressTrackRespDTO { + + // TODO @jason:LocalDateTime + /** + * 发生时间 + */ + private String time; + // TODO @jason:其它字段可能要补充下 + /** + * 快递状态 + */ + private String state; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryReqDTO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryReqDTO.java new file mode 100644 index 0000000..f2d31dc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryReqDTO.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kd100; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 快递 100 快递查询 Req DTO + * + * @author jason + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class Kd100ExpressQueryReqDTO { + + /** + * 快递公司编码 + */ + @JsonProperty("com") + private String expressCode; + + /** + * 快递单号 + */ + @JsonProperty("num") + private String logisticsNo; + + /** + * 收、寄件人的电话号码 + */ + private String phone; + /** + * 出发地城市 + */ + private String from; + /** + * 目的地城市,到达目的地后会加大监控频率 + */ + private String to; + + /** + * 返回结果排序 + * + * desc 降序(默认), asc 升序 + */ + private String order; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java new file mode 100644 index 0000000..fa805ca --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kd100; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * 快递 100 实时快递查询 Resp DTO 参见 快递 100 文档 + * + * @author jason + */ +@Data +public class Kd100ExpressQueryRespDTO { + + /** + * 快递公司编码 + */ + @JsonProperty("com") + private String expressCompanyCode; + /** + * 快递单号 + */ + @JsonProperty("nu") + private String logisticsNo; + /** + * 快递单当前状态 + */ + private String state; + + /** + * 查询结果 + * + * 失败返回 "false" + */ + private String result; + /** + * 查询结果失败时的错误信息 + */ + private String message; + + @JsonProperty("data") + private List tracks; + + @Data + public static class ExpressTrack { + /** + * 轨迹发生时间 + */ + @JsonProperty("time") + private String time; + /** + * 轨迹描述 + */ + @JsonProperty("context") + private String state; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java new file mode 100644 index 0000000..97f8135 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryReqDTO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kdniao; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +/** + * 快递鸟快递查询 Req DTO + * + * @author jason + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class KdNiaoExpressQueryReqDTO { + + /** + * 快递公司编码 + */ + @JsonProperty("ShipperCode") + private String expressCode; + /** + * 快递单号 + */ + @JsonProperty("LogisticCode") + private String logisticsNo; + /** + * 订单编号 + */ + @JsonProperty("OrderCode") + private String orderNo; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryRespDTO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryRespDTO.java new file mode 100644 index 0000000..60fa462 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/dto/kdniao/KdNiaoExpressQueryRespDTO.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kdniao; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.List; + +/** + * 快递鸟快递查询 Resp DTO 参见 快递鸟接口文档 + * + * @author jason + */ +@Data +public class KdNiaoExpressQueryRespDTO { + + /** + * 快递公司编码 + */ + @JsonProperty("ShipperCode") + private String expressCompanyCode; + + /** + * 快递单号 + */ + @JsonProperty("LogisticCode") + private String logisticsNo; + + /** + * 订单编号 + */ + @JsonProperty("OrderCode") + private String orderNo; + + @JsonProperty("EBusinessID") + private String businessId; + @JsonProperty("State") + private String state; + /** + * 成功与否 + */ + @JsonProperty("Success") + private Boolean success; + /** + * 失败原因 + */ + @JsonProperty("Reason") + private String reason; + + @JsonProperty("Traces") + private List tracks; + + @Data + public static class ExpressTrack { + /** + * 轨迹发生时间 + */ + @JsonProperty("AcceptTime") + private String time; + /** + * 轨迹描述 + */ + @JsonProperty("AcceptStation") + private String state; + } + +// { +// "EBusinessID": "1237100", +// "Traces": [], +// "State": "0", +// "ShipperCode": "STO", +// "LogisticCode": "638650888018", +// "Success": true, +// "Reason": "暂无轨迹信息" +// } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/ExpressClientFactoryImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/ExpressClientFactoryImpl.java new file mode 100644 index 0000000..912d409 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/ExpressClientFactoryImpl.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.impl; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.module.trade.framework.delivery.config.TradeExpressProperties; +import com.yunxi.scm.module.trade.framework.delivery.core.client.ExpressClient; +import com.yunxi.scm.module.trade.framework.delivery.core.client.impl.kd100.Kd100ExpressClient; +import com.yunxi.scm.module.trade.framework.delivery.core.client.impl.kdniao.KdNiaoExpressClient; +import com.yunxi.scm.module.trade.framework.delivery.core.enums.ExpressClientEnum; +import com.yunxi.scm.module.trade.framework.delivery.core.client.ExpressClientFactory; +import lombok.AllArgsConstructor; +import org.springframework.web.client.RestTemplate; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 快递客户端工厂实现类 + * + * @author jason + */ +@AllArgsConstructor +public class ExpressClientFactoryImpl implements ExpressClientFactory { + + private final Map clientMap = new ConcurrentHashMap<>(8); + + private final TradeExpressProperties tradeExpressProperties; + private final RestTemplate restTemplate; + + @Override + public ExpressClient getDefaultExpressClient() { + ExpressClient defaultClient = getOrCreateExpressClient(tradeExpressProperties.getClient()); + Assert.notNull("默认的快递客户端不能为空"); + return defaultClient; + } + + @Override + public ExpressClient getOrCreateExpressClient(ExpressClientEnum clientEnum) { + return clientMap.computeIfAbsent(clientEnum, + client -> createExpressClient(client, tradeExpressProperties)); + } + + private ExpressClient createExpressClient(ExpressClientEnum queryProviderEnum, + TradeExpressProperties tradeExpressProperties) { + switch (queryProviderEnum) { + case NOT_PROVIDE: + return new NoProvideExpressClient(); + case KD_NIAO: + return new KdNiaoExpressClient(restTemplate, tradeExpressProperties.getKdNiao()); + case KD_100: + return new Kd100ExpressClient(restTemplate, tradeExpressProperties.getKd100()); + } + return null; + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/NoProvideExpressClient.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/NoProvideExpressClient.java new file mode 100644 index 0000000..a1d4777 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/NoProvideExpressClient.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.impl; + +import com.yunxi.scm.module.trade.framework.delivery.core.client.ExpressClient; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; + +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.EXPRESS_CLIENT_NOT_PROVIDE; + +/** + * 未实现的快递客户端,用来提醒用户需要接入快递服务商, + * + * @author jason + */ +public class NoProvideExpressClient implements ExpressClient { + + @Override + public List getExpressTrackList(ExpressTrackQueryReqDTO reqDTO) { + throw exception(EXPRESS_CLIENT_NOT_PROVIDE); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/kd100/Kd100ExpressClient.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/kd100/Kd100ExpressClient.java new file mode 100644 index 0000000..2776a79 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/kd100/Kd100ExpressClient.java @@ -0,0 +1,102 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.impl.kd100; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.HexUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.module.trade.framework.delivery.config.TradeExpressProperties; +import com.yunxi.scm.module.trade.framework.delivery.core.client.ExpressClient; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kd100.Kd100ExpressQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kd100.Kd100ExpressQueryRespDTO; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.EXPRESS_API_QUERY_ERROR; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.EXPRESS_API_QUERY_FAILED; +import static com.yunxi.scm.module.trade.framework.delivery.core.client.convert.ExpressQueryConvert.INSTANCE; + +/** + * 快递 100 客户端 + * + * @author jason + */ +@Slf4j +@AllArgsConstructor +public class Kd100ExpressClient implements ExpressClient { + + private static final String REAL_TIME_QUERY_URL = "https://poll.kuaidi100.com/poll/query.do"; + + private final RestTemplate restTemplate; + private final TradeExpressProperties.Kd100Config config; + + @Override + public List getExpressTrackList(ExpressTrackQueryReqDTO reqDTO) { + // 发起查询 + Kd100ExpressQueryReqDTO kd100ReqParam = INSTANCE.convert2(reqDTO); + kd100ReqParam.setExpressCode(kd100ReqParam.getExpressCode().toLowerCase()); // 快递公司编码需要转成小写 + Kd100ExpressQueryRespDTO respDTO = requestExpressQuery(REAL_TIME_QUERY_URL, kd100ReqParam, + Kd100ExpressQueryRespDTO.class); + log.debug("[getExpressTrackList][快递 100 接口 查询接口返回 {}]", respDTO); + + // 处理结果 + if (Objects.equals("false", respDTO.getResult())) { + log.error("[getExpressTrackList][快递 100 接口 返回失败 {}]", respDTO.getMessage()); + throw exception(EXPRESS_API_QUERY_FAILED, respDTO.getMessage()); + } + if (CollUtil.isEmpty(respDTO.getTracks())) { + return Collections.emptyList(); + } + return INSTANCE.convertList2(respDTO.getTracks()); + } + + /** + * 发送快递 100 实时快递查询请求,可以作为通用快递 100 通用请求接口。 目前没有其它场景需要使用。暂时放这里 + * + * @param url 请求 url + * @param req 对应请求的请求参数 + * @param respClass 对应请求的响应 class + * @param 每个请求的请求结构 Req DTO + * @param 每个请求的响应结构 Resp DTO + */ + private Resp requestExpressQuery(String url, Req req, Class respClass) { + // 请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // 生成签名 + String param = JsonUtils.toJsonString(req); + String sign = generateReqSign(param, config.getKey(), config.getCustomer()); + // 请求体 + MultiValueMap requestBody = new LinkedMultiValueMap<>(); + requestBody.add("customer", config.getCustomer()); + requestBody.add("sign", sign); + requestBody.add("param", param); + log.debug("[sendExpressQueryReq][快递 100 接口的请求参数: {}]", requestBody); + // 发送请求 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + log.debug("[sendExpressQueryReq][快递 100 接口响应结果 {}]", responseEntity); + + // 处理响应 + if (!responseEntity.getStatusCode().is2xxSuccessful()) { + throw exception(EXPRESS_API_QUERY_ERROR); + } + return JsonUtils.parseObject(responseEntity.getBody(), respClass); + } + + private String generateReqSign(String param, String key, String customer) { + String plainText = String.format("%s%s%s", param, key, customer); + return HexUtil.encodeHexStr(DigestUtil.md5(plainText), false); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java new file mode 100644 index 0000000..1c631af --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/kdniao/KdNiaoExpressClient.java @@ -0,0 +1,118 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.impl.kdniao; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.net.URLEncodeUtil; +import cn.hutool.crypto.digest.DigestUtil; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.module.trade.framework.delivery.config.TradeExpressProperties; +import com.yunxi.scm.module.trade.framework.delivery.core.client.ExpressClient; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackRespDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kdniao.KdNiaoExpressQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.kdniao.KdNiaoExpressQueryRespDTO; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.EXPRESS_API_QUERY_FAILED; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.EXPRESS_API_QUERY_ERROR; +import static com.yunxi.scm.module.trade.framework.delivery.core.client.convert.ExpressQueryConvert.INSTANCE; + +/** + * 快递鸟客户端 + * + * @author jason + */ +@Slf4j +@AllArgsConstructor +public class KdNiaoExpressClient implements ExpressClient { + + private static final String REAL_TIME_QUERY_URL = "https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx"; + + /** + * 快递鸟即时查询免费版 RequestType + */ + private static final String REAL_TIME_FREE_REQ_TYPE = "1002"; + private final RestTemplate restTemplate; + private final TradeExpressProperties.KdNiaoConfig config; + + /** + * 快递鸟即时查询免费版本 + * + * @see 快递鸟接口文档 + * @param reqDTO 查询请求参数 + */ + @Override + public List getExpressTrackList(ExpressTrackQueryReqDTO reqDTO) { + KdNiaoExpressQueryReqDTO kdNiaoReqData = INSTANCE.convert(reqDTO); + // 快递公司编码需要转成大写 + kdNiaoReqData.setExpressCode(reqDTO.getExpressCode().toUpperCase()); + KdNiaoExpressQueryRespDTO respDTO = requestKdNiaoApi(REAL_TIME_QUERY_URL, REAL_TIME_FREE_REQ_TYPE, + kdNiaoReqData, KdNiaoExpressQueryRespDTO.class); + log.debug("[getExpressTrackList][快递鸟即时查询接口返回 {}]", respDTO); + + // 处理结果 + if (respDTO == null || !respDTO.getSuccess()) { + throw exception(EXPRESS_API_QUERY_FAILED, respDTO == null ? "" : respDTO.getReason()); + } + if (CollUtil.isNotEmpty(respDTO.getTracks())) { + return Collections.emptyList(); + } + return INSTANCE.convertList(respDTO.getTracks()); + } + + /** + * 快递鸟 通用的 API 请求,暂时没有其他应用场景, 暂时放这里 + * + * @param url 请求 url + * @param requestType 对应的请求指令 (快递鸟的RequestType) + * @param req 对应请求的请求参数 + * @param respClass 对应请求的响应 class + * @param 每个请求的请求结构 Req DTO + * @param 每个请求的响应结构 Resp DTO + */ + private Resp requestKdNiaoApi(String url, String requestType, Req req, + Class respClass){ + // 请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + // 请求体 + String reqData = JsonUtils.toJsonString(req); + String dataSign = generateDataSign(reqData, config.getApiKey()); + MultiValueMap requestBody = new LinkedMultiValueMap<>(); + requestBody.add("RequestData", reqData); + requestBody.add("DataType", "2"); + requestBody.add("EBusinessID", config.getBusinessId()); + requestBody.add("DataSign", dataSign); + requestBody.add("RequestType", requestType); + log.debug("[requestKdNiaoApi][快递鸟接口 RequestType : {}, 的请求参数 {}]", requestType, requestBody); + // 发送请求 + HttpEntity> requestEntity = new HttpEntity<>(requestBody, headers); + ResponseEntity responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); + log.debug("快递鸟接口 RequestType : {}, 的响应结果 {}", requestType, responseEntity); + // 处理响应 + if (!responseEntity.getStatusCode().is2xxSuccessful()) { + throw exception(EXPRESS_API_QUERY_ERROR); + } + return JsonUtils.parseObject(responseEntity.getBody(), respClass); + } + + /** + * 快递鸟生成请求签名 参见 签名说明 + * @param reqData 请求实体 + * @param apiKey api Key + */ + private String generateDataSign(String reqData, String apiKey) { + String plainText = String.format("%s%s", reqData, apiKey); + return URLEncodeUtil.encode(Base64.encode(DigestUtil.md5Hex(plainText))); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/enums/ExpressClientEnum.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/enums/ExpressClientEnum.java new file mode 100644 index 0000000..d4ec746 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/delivery/core/enums/ExpressClientEnum.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 快递客户端枚举 + * + * @author jason + */ +@Getter +@AllArgsConstructor +public enum ExpressClientEnum { + + NOT_PROVIDE("not-provide","未提供"), + KD_NIAO("kd-niao", "快递鸟"), + KD_100("kd-100", "快递100"); + + /** + * 快递服务商唯一编码 + */ + private final String code; + /** + * 快递服务商名称 + */ + private final String name; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/order/config/TradeOrderConfig.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/order/config/TradeOrderConfig.java new file mode 100644 index 0000000..6dff027 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/order/config/TradeOrderConfig.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.trade.framework.order.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +// TODO @LeeYan9: 可以直接给 TradeOrderProperties 一个 @Component生效哈 +/** + * @author LeeYan9 + * @since 2022-09-15 + */ +@Configuration +@EnableConfigurationProperties(TradeOrderProperties.class) +public class TradeOrderConfig { +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/order/config/TradeOrderProperties.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/order/config/TradeOrderProperties.java new file mode 100644 index 0000000..c161b1c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/order/config/TradeOrderProperties.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.trade.framework.order.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +/** + * 交易订单的配置项 + * + * @author LeeYan9 + * @since 2022-09-15 + */ +@ConfigurationProperties(prefix = "yunxi.trade.order") +@Data +@Validated +public class TradeOrderProperties { + + /** + * 应用编号 + */ + @NotNull(message = "应用编号不能为空") + private Long appId; + + /** + * 支付超时时间 + */ + @NotNull(message = "支付超时时间不能为空") + private Duration expireTime; + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/package-info.java new file mode 100644 index 0000000..dd1cc37 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 trade 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.trade.framework; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/web/config/TradeWebConfiguration.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/web/config/TradeWebConfiguration.java new file mode 100644 index 0000000..d54af4e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/web/config/TradeWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.trade.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * trade 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class TradeWebConfiguration { + + /** + * trade 模块的 API 分组 + */ + @Bean + public GroupedOpenApi tradeGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("trade"); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/web/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/web/package-info.java new file mode 100644 index 0000000..46772bf --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * trade 模块的 web 配置 + */ +package com.yunxi.scm.module.trade.framework.web; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/package-info.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/package-info.java new file mode 100644 index 0000000..40ce290 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/package-info.java @@ -0,0 +1,8 @@ +/** + * trade 模块,product 模块,主要实现商品相关功能 + * 例如:品牌、商品分类、spu、sku等功能。 + * + * 1. Controller URL:以 /product/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 product_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.trade; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleService.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleService.java new file mode 100644 index 0000000..87e6e8a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleService.java @@ -0,0 +1,94 @@ +package com.yunxi.scm.module.trade.service.aftersale; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSaleDisagreeReqVO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSalePageReqVO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSaleRefuseReqVO; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSaleCreateReqVO; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSaleDeliveryReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleDO; + +/** + * 交易售后 Service 接口 + * + * @author 芋道源码 + */ +public interface TradeAfterSaleService { + + /** + * 获得交易售后分页 + * + * @param pageReqVO 分页查询 + * @return 交易售后分页 + */ + PageResult getAfterSalePage(TradeAfterSalePageReqVO pageReqVO); + + /** + * 【会员】创建交易售后 + *

+ * 一般是用户发起售后请求 + * + * @param userId 会员用户编号 + * @param createReqVO 创建 Request 信息 + * @return 交易售后编号 + */ + Long createAfterSale(Long userId, AppTradeAfterSaleCreateReqVO createReqVO); + + /** + * 【管理员】同意交易售后 + * + * @param userId 管理员用户编号 + * @param id 交易售后编号 + */ + void agreeAfterSale(Long userId, Long id); + + /** + * 【管理员】拒绝交易售后 + * + * @param userId 管理员用户编号 + * @param auditReqVO 审批 Request 信息 + */ + void disagreeAfterSale(Long userId, TradeAfterSaleDisagreeReqVO auditReqVO); + + /** + * 【会员】退回货物 + * + * @param userId 会员用户编号 + * @param deliveryReqVO 退货 Request 信息 + */ + void deliveryAfterSale(Long userId, AppTradeAfterSaleDeliveryReqVO deliveryReqVO); + + /** + * 【管理员】确认收货 + * + * @param userId 管理员编号 + * @param id 交易售后编号 + */ + void receiveAfterSale(Long userId, Long id); + + /** + * 【管理员】拒绝收货 + * + * @param userId 管理员用户编号 + * @param refuseReqVO 拒绝收货 Request 信息 + */ + void refuseAfterSale(Long userId, TradeAfterSaleRefuseReqVO refuseReqVO); + + /** + * 【管理员】确认退款 + * + * @param userId 管理员用户编号 + * @param userIp 管理员用户 IP + * @param id 售后编号 + */ + void refundAfterSale(Long userId, String userIp, Long id); + + /** + * 【会员】取消售后 + * + * @param userId 会员用户编号 + * @param id 交易售后编号 + */ + void cancelAfterSale(Long userId, Long id); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java new file mode 100644 index 0000000..ce443ee --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleServiceImpl.java @@ -0,0 +1,425 @@ +package com.yunxi.scm.module.trade.service.aftersale; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.module.pay.api.refund.PayRefundApi; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSaleDisagreeReqVO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSalePageReqVO; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSaleRefuseReqVO; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSaleCreateReqVO; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSaleDeliveryReqVO; +import com.yunxi.scm.module.trade.convert.aftersale.TradeAfterSaleConvert; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleDO; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleLogDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.dal.mysql.aftersale.TradeAfterSaleLogMapper; +import com.yunxi.scm.module.trade.dal.mysql.aftersale.TradeAfterSaleMapper; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleStatusEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleTypeEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleWayEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderStatusEnum; +import com.yunxi.scm.module.trade.framework.aftersalelog.core.dto.TradeAfterSaleLogCreateReqDTO; +import com.yunxi.scm.module.trade.framework.aftersalelog.core.service.AfterSaleLogService; +import com.yunxi.scm.module.trade.framework.order.config.TradeOrderProperties; +import com.yunxi.scm.module.trade.service.order.TradeOrderService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.*; + +/** + * 交易售后 Service 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@Service +@Validated +public class TradeAfterSaleServiceImpl implements TradeAfterSaleService, AfterSaleLogService { + + @Resource + private TradeOrderService tradeOrderService; + + @Resource + private TradeAfterSaleMapper tradeAfterSaleMapper; + @Resource + private TradeAfterSaleLogMapper tradeAfterSaleLogMapper; + + @Resource + private PayRefundApi payRefundApi; + + @Resource + private TradeOrderProperties tradeOrderProperties; + + @Override + public PageResult getAfterSalePage(TradeAfterSalePageReqVO pageReqVO) { + return tradeAfterSaleMapper.selectPage(pageReqVO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createAfterSale(Long userId, AppTradeAfterSaleCreateReqVO createReqVO) { + // 第一步,前置校验 + TradeOrderItemDO tradeOrderItem = validateOrderItemApplicable(userId, createReqVO); + + // 第二步,存储交易售后 + TradeAfterSaleDO afterSale = createAfterSale(createReqVO, tradeOrderItem); + return afterSale.getId(); + } + + /** + * 校验交易订单项是否可以申请售后 + * + * @param userId 用户编号 + * @param createReqVO 售后创建信息 + * @return 交易订单项 + */ + private TradeOrderItemDO validateOrderItemApplicable(Long userId, AppTradeAfterSaleCreateReqVO createReqVO) { + // 校验订单项存在 + TradeOrderItemDO orderItem = tradeOrderService.getOrderItem(userId, createReqVO.getOrderItemId()); + if (orderItem == null) { + throw exception(ORDER_ITEM_NOT_FOUND); + } + + // 已申请售后,不允许再发起售后申请 + if (!TradeOrderItemAfterSaleStatusEnum.isNone(orderItem.getAfterSaleStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_ITEM_APPLIED); + } + + // 申请的退款金额,不能超过商品的价格 + if (createReqVO.getRefundPrice() > orderItem.getPayPrice()) { + throw exception(AFTER_SALE_CREATE_FAIL_REFUND_PRICE_ERROR); + } + + // 校验订单存在 + TradeOrderDO order = tradeOrderService.getOrder(userId, orderItem.getOrderId()); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // TODO 芋艿:超过一定时间,不允许售后 + // 已取消,无法发起售后 + if (TradeOrderStatusEnum.isCanceled(order.getStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_CANCELED); + } + // 未支付,无法发起售后 + if (!TradeOrderStatusEnum.havePaid(order.getStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_PAID); + } + // 如果是【退货退款】的情况,需要额外校验是否发货 + if (createReqVO.getWay().equals(TradeAfterSaleWayEnum.RETURN_AND_REFUND.getWay()) + && !TradeOrderStatusEnum.haveDelivered(order.getStatus())) { + throw exception(AFTER_SALE_CREATE_FAIL_ORDER_STATUS_NO_DELIVERED); + } + return orderItem; + } + + private TradeAfterSaleDO createAfterSale(AppTradeAfterSaleCreateReqVO createReqVO, + TradeOrderItemDO orderItem) { + // 创建售后单 + TradeAfterSaleDO afterSale = TradeAfterSaleConvert.INSTANCE.convert(createReqVO, orderItem); + afterSale.setNo(RandomUtil.randomString(10)); // TODO 芋艿:优化 no 生成逻辑 + afterSale.setStatus(TradeAfterSaleStatusEnum.APPLY.getStatus()); + // 标记是售中还是售后 + TradeOrderDO order = tradeOrderService.getOrder(orderItem.getUserId(), orderItem.getOrderId()); + afterSale.setOrderNo(order.getNo()); // 记录 orderNo 订单流水,方便后续检索 + afterSale.setType(TradeOrderStatusEnum.isCompleted(order.getStatus()) + ? TradeAfterSaleTypeEnum.AFTER_SALE.getType() : TradeAfterSaleTypeEnum.IN_SALE.getType()); + // TODO 退还积分 + tradeAfterSaleMapper.insert(afterSale); + + // 更新交易订单项的售后状态 + tradeOrderService.updateOrderItemAfterSaleStatus(orderItem.getId(), + TradeOrderItemAfterSaleStatusEnum.NONE.getStatus(), + TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(), null); + + // 记录售后日志 + createAfterSaleLog(orderItem.getUserId(), UserTypeEnum.MEMBER.getValue(), + afterSale, null, afterSale.getStatus()); + + // TODO 发送售后消息 + return afterSale; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void agreeAfterSale(Long userId, Long id) { + // 校验售后单存在,并状态未审批 + TradeAfterSaleDO afterSale = validateAfterSaleAuditable(id); + + // 更新售后单的状态 + // 情况一:退款:标记为 WAIT_REFUND 状态。后续等退款发起成功后,在标记为 COMPLETE 状态 + // 情况二:退货退款:需要等用户退货后,才能发起退款 + Integer newStatus = afterSale.getType().equals(TradeAfterSaleWayEnum.REFUND.getWay()) ? + TradeAfterSaleStatusEnum.WAIT_REFUND.getStatus() : TradeAfterSaleStatusEnum.SELLER_AGREE.getStatus(); + updateAfterSaleStatus(afterSale.getId(), TradeAfterSaleStatusEnum.APPLY.getStatus(), new TradeAfterSaleDO() + .setStatus(newStatus).setAuditUserId(userId).setAuditTime(LocalDateTime.now())); + + // 记录售后日志 + createAfterSaleLog(userId, UserTypeEnum.ADMIN.getValue(), + afterSale, afterSale.getStatus(), newStatus); + + // TODO 发送售后消息 + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void disagreeAfterSale(Long userId, TradeAfterSaleDisagreeReqVO auditReqVO) { + // 校验售后单存在,并状态未审批 + TradeAfterSaleDO afterSale = validateAfterSaleAuditable(auditReqVO.getId()); + + // 更新售后单的状态 + Integer newStatus = TradeAfterSaleStatusEnum.SELLER_DISAGREE.getStatus(); + updateAfterSaleStatus(afterSale.getId(), TradeAfterSaleStatusEnum.APPLY.getStatus(), new TradeAfterSaleDO() + .setStatus(newStatus).setAuditUserId(userId).setAuditTime(LocalDateTime.now()) + .setAuditReason(auditReqVO.getAuditReason())); + + // 记录售后日志 + createAfterSaleLog(userId, UserTypeEnum.ADMIN.getValue(), + afterSale, afterSale.getStatus(), newStatus); + + // TODO 发送售后消息 + + // 更新交易订单项的售后状态为【未申请】 + tradeOrderService.updateOrderItemAfterSaleStatus(afterSale.getOrderItemId(), + TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(), + TradeOrderItemAfterSaleStatusEnum.NONE.getStatus(), null); + } + + /** + * 校验售后单是否可审批(同意售后、拒绝售后) + * + * @param id 售后编号 + * @return 售后单 + */ + private TradeAfterSaleDO validateAfterSaleAuditable(Long id) { + TradeAfterSaleDO afterSale = tradeAfterSaleMapper.selectById(id); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), TradeAfterSaleStatusEnum.APPLY.getStatus())) { + throw exception(AFTER_SALE_AUDIT_FAIL_STATUS_NOT_APPLY); + } + return afterSale; + } + + private void updateAfterSaleStatus(Long id, Integer status, TradeAfterSaleDO updateObj) { + int updateCount = tradeAfterSaleMapper.updateByIdAndStatus(id, status, updateObj); + if (updateCount == 0) { + throw exception(AFTER_SALE_UPDATE_STATUS_FAIL); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deliveryAfterSale(Long userId, AppTradeAfterSaleDeliveryReqVO deliveryReqVO) { + // 校验售后单存在,并状态未退货 + TradeAfterSaleDO afterSale = tradeAfterSaleMapper.selectById(deliveryReqVO.getId()); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), TradeAfterSaleStatusEnum.SELLER_AGREE.getStatus())) { + throw exception(AFTER_SALE_DELIVERY_FAIL_STATUS_NOT_SELLER_AGREE); + } + + // 更新售后单的物流信息 + updateAfterSaleStatus(afterSale.getId(), TradeAfterSaleStatusEnum.SELLER_AGREE.getStatus(), new TradeAfterSaleDO() + .setStatus(TradeAfterSaleStatusEnum.BUYER_DELIVERY.getStatus()) + .setLogisticsId(deliveryReqVO.getLogisticsId()).setLogisticsNo(deliveryReqVO.getLogisticsNo()) + .setDeliveryTime(deliveryReqVO.getDeliveryTime())); + + // 记录售后日志 + createAfterSaleLog(userId, UserTypeEnum.MEMBER.getValue(), + afterSale, afterSale.getStatus(), TradeAfterSaleStatusEnum.BUYER_DELIVERY.getStatus()); + + // TODO 发送售后消息 + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void receiveAfterSale(Long userId, Long id) { + // 校验售后单存在,并状态为已退货 + TradeAfterSaleDO afterSale = validateAfterSaleReceivable(id); + + // 更新售后单的状态 + updateAfterSaleStatus(afterSale.getId(), TradeAfterSaleStatusEnum.BUYER_DELIVERY.getStatus(), new TradeAfterSaleDO() + .setStatus(TradeAfterSaleStatusEnum.WAIT_REFUND.getStatus()).setReceiveTime(LocalDateTime.now())); + + // 记录售后日志 + createAfterSaleLog(userId, UserTypeEnum.ADMIN.getValue(), + afterSale, afterSale.getStatus(), TradeAfterSaleStatusEnum.WAIT_REFUND.getStatus()); + + // TODO 发送售后消息 + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void refuseAfterSale(Long userId, TradeAfterSaleRefuseReqVO refuseReqVO) { + // 校验售后单存在,并状态为已退货 + TradeAfterSaleDO afterSale = tradeAfterSaleMapper.selectById(refuseReqVO.getId()); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), TradeAfterSaleStatusEnum.BUYER_DELIVERY.getStatus())) { + throw exception(AFTER_SALE_CONFIRM_FAIL_STATUS_NOT_BUYER_DELIVERY); + } + + // 更新售后单的状态 + updateAfterSaleStatus(afterSale.getId(), TradeAfterSaleStatusEnum.BUYER_DELIVERY.getStatus(), new TradeAfterSaleDO() + .setStatus(TradeAfterSaleStatusEnum.SELLER_REFUSE.getStatus()).setReceiveTime(LocalDateTime.now()) + .setReceiveReason(refuseReqVO.getRefuseMemo())); + + // 记录售后日志 + createAfterSaleLog(userId, UserTypeEnum.ADMIN.getValue(), + afterSale, afterSale.getStatus(), TradeAfterSaleStatusEnum.SELLER_REFUSE.getStatus()); + + // TODO 发送售后消息 + + // 更新交易订单项的售后状态为【未申请】 + tradeOrderService.updateOrderItemAfterSaleStatus(afterSale.getOrderItemId(), + TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(), + TradeOrderItemAfterSaleStatusEnum.NONE.getStatus(), null); + } + + /** + * 校验售后单是否可收货,即处于买家已发货 + * + * @param id 售后编号 + * @return 售后单 + */ + private TradeAfterSaleDO validateAfterSaleReceivable(Long id) { + TradeAfterSaleDO afterSale = tradeAfterSaleMapper.selectById(id); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), TradeAfterSaleStatusEnum.BUYER_DELIVERY.getStatus())) { + throw exception(AFTER_SALE_CONFIRM_FAIL_STATUS_NOT_BUYER_DELIVERY); + } + return afterSale; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void refundAfterSale(Long userId, String userIp, Long id) { + // 校验售后单的状态,并状态待退款 + TradeAfterSaleDO afterSale = tradeAfterSaleMapper.selectByPayRefundId(id); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtil.notEqual(afterSale.getStatus(), TradeAfterSaleStatusEnum.WAIT_REFUND.getStatus())) { + throw exception(AFTER_SALE_REFUND_FAIL_STATUS_NOT_WAIT_REFUND); + } + + // 发起退款单。注意,需要在事务提交后,再进行发起,避免重复发起 + createPayRefund(userIp, afterSale); + + // 更新售后单的状态为【已完成】 + updateAfterSaleStatus(afterSale.getId(), TradeAfterSaleStatusEnum.WAIT_REFUND.getStatus(), new TradeAfterSaleDO() + .setStatus(TradeAfterSaleStatusEnum.COMPLETE.getStatus()).setRefundTime(LocalDateTime.now())); + + // 记录售后日志 + createAfterSaleLog(userId, UserTypeEnum.ADMIN.getValue(), + afterSale, afterSale.getStatus(), TradeAfterSaleStatusEnum.COMPLETE.getStatus()); + + // TODO 发送售后消息 + + // 更新交易订单项的售后状态为【已完成】 + tradeOrderService.updateOrderItemAfterSaleStatus(afterSale.getOrderItemId(), + TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(), + TradeOrderItemAfterSaleStatusEnum.SUCCESS.getStatus(), afterSale.getRefundPrice()); + } + + private void createPayRefund(String userIp, TradeAfterSaleDO afterSale) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + + @Override + public void afterCommit() { + // 创建退款单 + PayRefundCreateReqDTO createReqDTO = TradeAfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties); + Long payRefundId = payRefundApi.createRefund(createReqDTO); + // 更新售后单的退款单号 + tradeAfterSaleMapper.updateById(new TradeAfterSaleDO().setId(afterSale.getId()).setPayRefundId(payRefundId)); + } + }); + } + + @Override + public void cancelAfterSale(Long userId, Long id) { + // 校验售后单的状态,并状态待退款 + TradeAfterSaleDO afterSale = tradeAfterSaleMapper.selectByPayRefundId(id); + if (afterSale == null) { + throw exception(AFTER_SALE_NOT_FOUND); + } + if (ObjectUtils.equalsAny(afterSale.getStatus(), TradeAfterSaleStatusEnum.APPLY.getStatus(), + TradeAfterSaleStatusEnum.SELLER_AGREE.getStatus())) { + throw exception(AFTER_SALE_CANCEL_FAIL_STATUS_NOT_APPLY_OR_AGREE); + } + + // 更新售后单的状态为【已取消】 + updateAfterSaleStatus(afterSale.getId(), afterSale.getStatus(), new TradeAfterSaleDO() + .setStatus(TradeAfterSaleStatusEnum.BUYER_CANCEL.getStatus())); + + // 记录售后日志 + createAfterSaleLog(userId, UserTypeEnum.MEMBER.getValue(), + afterSale, afterSale.getStatus(), TradeAfterSaleStatusEnum.BUYER_CANCEL.getStatus()); + + // TODO 发送售后消息 + + // 更新交易订单项的售后状态为【未申请】 + tradeOrderService.updateOrderItemAfterSaleStatus(afterSale.getOrderItemId(), + TradeOrderItemAfterSaleStatusEnum.APPLY.getStatus(), + TradeOrderItemAfterSaleStatusEnum.NONE.getStatus(), null); + } + + @Deprecated + private void createAfterSaleLog(Long userId, Integer userType, TradeAfterSaleDO afterSale, + Integer beforeStatus, Integer afterStatus) { + TradeAfterSaleLogCreateReqDTO logDTO = new TradeAfterSaleLogCreateReqDTO() + .setUserId(userId) + .setUserType(userType) + .setAfterSaleId(afterSale.getId()) + .setOperateType(afterStatus.toString()); + // TODO 废弃,待删除 + this.createLog(logDTO); + } + + // TODO @CHENCHEN:这个注释,写在接口就好了,补充重复写哈;@date 应该是 @since + /** + * 日志记录 + * + * @param logDTO 日志记录 + * @author 陈賝 + * @date 2023/6/12 14:18 + */ + @Override + @Async + public void createLog(TradeAfterSaleLogCreateReqDTO logDTO) { + try { + TradeAfterSaleLogDO afterSaleLog = new TradeAfterSaleLogDO() + .setUserId(logDTO.getUserId()) + .setUserType(logDTO.getUserType()) + .setAfterSaleId(logDTO.getAfterSaleId()) + .setOperateType(logDTO.getOperateType()) + .setContent(logDTO.getContent()); + tradeAfterSaleLogMapper.insert(afterSaleLog); + // TODO @CHENCHEN:代码排版哈;空格要正确 + }catch (Exception exception){ + log.error("[createLog][request({}) 日志记录错误]", toJsonString(logDTO), exception); + } + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/cart/TradeCartService.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/cart/TradeCartService.java new file mode 100644 index 0000000..128aa91 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/cart/TradeCartService.java @@ -0,0 +1,90 @@ +package com.yunxi.scm.module.trade.service.cart; + +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartAddReqVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartListRespVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartResetReqVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartUpdateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.cart.TradeCartDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 购物车 Service 接口 + * + * @author 芋道源码 + */ +public interface TradeCartService { + + /** + * 添加商品到购物车 + * + * @param userId 用户编号 + * @param addReqVO 添加信息 + * @return 购物项的编号 + */ + Long addCart(Long userId, @Valid AppTradeCartAddReqVO addReqVO); + + /** + * 更新购物车商品数量 + * + * @param userId 用户编号 + * @param updateCountReqVO 更新信息 + */ + void updateCart(Long userId, AppTradeCartUpdateReqVO updateCountReqVO); + + /** + * 重置购物车商品 + * + * 使用场景:在一个购物车项对应的商品失效(例如说 SPU 被下架),可以重新选择对应的 SKU + * + * @param userId 用户编号 + * @param updateReqVO 重置信息 + */ + void resetCart(Long userId, AppTradeCartResetReqVO updateReqVO); + + /** + * 删除购物车商品 + * + * @param userId 用户编号 + * @param ids 购物项的编号 + */ + void deleteCart(Long userId, Collection ids); + + /** + * 查询用户在购物车中的商品数量 + * + * @param userId 用户编号 + * @return 商品数量 + */ + Integer getCartCount(Long userId); + + /** + * 查询用户的购物车列表 + * + * @param userId 用户编号 + * @return 购物车列表 + */ + AppTradeCartListRespVO getCartList(Long userId); + + /** + * 查询用户的购物车列表 + * + * @param userId 用户编号 + * @param ids 购物项的编号 + * @return 购物车列表 + */ + List getCartList(Long userId, Set ids); + + /** + * 获得用户的购物车商品 SPU 数量的 Map + * + * @param userId 用户编号 + * @return 购物车商品 SPU 数量的 Map + */ + Map getCartCountMap(Long userId); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/cart/TradeCartServiceImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/cart/TradeCartServiceImpl.java new file mode 100644 index 0000000..89a2e71 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/cart/TradeCartServiceImpl.java @@ -0,0 +1,206 @@ +package com.yunxi.scm.module.trade.service.cart; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.module.product.api.sku.ProductSkuApi; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.spu.ProductSpuApi; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartAddReqVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartListRespVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartResetReqVO; +import com.yunxi.scm.module.trade.controller.app.cart.vo.AppTradeCartUpdateReqVO; +import com.yunxi.scm.module.trade.convert.cart.TradeCartConvert; +import com.yunxi.scm.module.trade.dal.dataobject.cart.TradeCartDO; +import com.yunxi.scm.module.trade.dal.mysql.cart.TradeCartMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SKU_NOT_EXISTS; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.SKU_STOCK_NOT_ENOUGH; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.CARD_ITEM_NOT_FOUND; +import static java.util.Collections.emptyList; + +/** + * 购物车 Service 实现类 + * + * // TODO 芋艿:秒杀、拼团、砍价对购物车的影响 + * // TODO 芋艿:未来优化:购物车的价格计算,支持营销信息 + * + * @author 芋道源码 + */ +@Service +@Validated +public class TradeCartServiceImpl implements TradeCartService { + + @Resource + private TradeCartMapper cartMapper; + + @Resource + private ProductSpuApi productSpuApi; + @Resource + private ProductSkuApi productSkuApi; + + @Override + public Long addCart(Long userId, AppTradeCartAddReqVO addReqVO) { + // 查询 TradeCartDO + TradeCartDO cart = cartMapper.selectByUserIdAndSkuId(userId, addReqVO.getSkuId(), + addReqVO.getAddStatus(), false); + // 校验 SKU + Integer count = cart != null && addReqVO.getAddStatus() ? + cart.getCount() + addReqVO.getCount() : addReqVO.getCount(); + ProductSkuRespDTO sku = checkProductSku(addReqVO.getSkuId(), count); + + // 情况零:特殊,count 小于等于 0,说明前端项目删除 + // 情况一:存在,则进行数量更新 + if (cart != null) { + // 特殊情况,如果 count 小于等于 0,说明前端想要删除 + if (count <= 0) { + cartMapper.deleteById(cart.getId()); + } else { + cartMapper.updateById(new TradeCartDO().setId(cart.getId()).setCount(count)); + } + return cart.getId(); + // 情况二:不存在,则进行插入 + } else { + cart = new TradeCartDO().setUserId(userId) + .setSpuId(sku.getSpuId()).setSkuId(sku.getId()).setCount(count) + .setAddStatus(addReqVO.getAddStatus()).setOrderStatus(false); + cartMapper.insert(cart); + } + return cart.getId(); + } + + @Override + public void updateCart(Long userId, AppTradeCartUpdateReqVO updateReqVO) { + // 校验 TradeCartDO 存在 + TradeCartDO cart = cartMapper.selectById(updateReqVO.getId(), userId); + if (cart == null) { + throw exception(CARD_ITEM_NOT_FOUND); + } + // 校验商品 SKU + checkProductSku(cart.getSkuId(), updateReqVO.getCount()); + + // 更新数量 + cartMapper.updateById(new TradeCartDO().setId(cart.getId()) + .setCount(updateReqVO.getCount())); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void resetCart(Long userId, AppTradeCartResetReqVO resetReqVO) { + // 第一步:删除原本的购物项 + TradeCartDO oldCart = cartMapper.selectById(resetReqVO.getId(), userId); + if (oldCart == null) { + throw exception(CARD_ITEM_NOT_FOUND); + } + cartMapper.deleteById(oldCart.getId()); + + // 第二步:添加新的购物项 + TradeCartDO newCart = cartMapper.selectByUserIdAndSkuId(userId, resetReqVO.getSkuId(), + true, false); + if (newCart != null) { + updateCart(userId, new AppTradeCartUpdateReqVO() + .setId(newCart.getId()).setCount(resetReqVO.getCount())); + } else { + addCart(userId, new AppTradeCartAddReqVO().setAddStatus(true) + .setSkuId(resetReqVO.getSkuId()).setCount(resetReqVO.getCount())); + } + } + + /** + * 购物车删除商品 + * + * @param userId 用户编号 + * @param ids 商品 SKU 编号的数组 + */ + @Override + public void deleteCart(Long userId, Collection ids) { + // 查询 TradeCartDO 列表 + List carts = cartMapper.selectListByIds(ids, userId); + if (CollUtil.isEmpty(carts)) { + return; + } + + // 批量标记删除 + cartMapper.deleteBatchIds(ids); + } + + @Override + public Integer getCartCount(Long userId) { + return cartMapper.selectSumByUserId(userId); + } + + @Override + public Map getCartCountMap(Long userId) { + return cartMapper.selectSumMapByUserId(userId); + } + + @Override + public AppTradeCartListRespVO getCartList(Long userId) { + // 获得购物车的商品,只查询未下单的 + List carts = cartMapper.selectListByUserId(userId, true, false); + carts.sort(Comparator.comparing(TradeCartDO::getId).reversed()); + // 如果未空,则返回空结果 + if (CollUtil.isEmpty(carts)) { + return new AppTradeCartListRespVO().setValidList(emptyList()) + .setInvalidList(emptyList()); + } + + // 查询 SPU、SKU 列表 + List spus = productSpuApi.getSpuList(convertSet(carts, TradeCartDO::getSpuId)); + List skus = productSkuApi.getSkuList(convertSet(carts, TradeCartDO::getSkuId)); + + // 如果 SPU 被删除,则删除购物车对应的商品。延迟删除 + deleteCartIfSpuDeleted(carts, spus); + + // 拼接数据 + return TradeCartConvert.INSTANCE.convertList(carts, spus, skus); + } + + @Override + public List getCartList(Long userId, Set ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return cartMapper.selectListByUserId(userId, ids); + } + + private void deleteCartIfSpuDeleted(List carts, List spus) { + // 如果 SPU 被删除,则删除购物车对应的商品。延迟删除 + carts.removeIf(cart -> { + if (spus.stream().noneMatch(spu -> spu.getId().equals(cart.getSpuId()))) { + cartMapper.deleteById(cart.getId()); + return true; + } + return false; + }); + } + + /** + * 校验商品 SKU 是否合法 + * 1. 是否存在 + * 2. 是否下架 + * 3. 库存不足 + * + * @param skuId 商品 SKU 编号 + * @param count 商品数量 + * @return 商品 SKU + */ + private ProductSkuRespDTO checkProductSku(Long skuId, Integer count) { + ProductSkuRespDTO sku = productSkuApi.getSku(skuId); + if (sku == null) { + throw exception(SKU_NOT_EXISTS); + } + if (count > sku.getStock()) { + throw exception(SKU_STOCK_NOT_ENOUGH); + } + return sku; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressService.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressService.java new file mode 100644 index 0000000..27e5e3e --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressService.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.trade.service.delivery; + +import java.util.*; +import javax.validation.*; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressCreateReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressExportReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressPageReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressUpdateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressDO; + +/** + * 快递公司 Service 接口 + * + * @author jason + */ +public interface DeliveryExpressService { + + /** + * 创建快递公司 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeliveryExpress(@Valid DeliveryExpressCreateReqVO createReqVO); + + /** + * 更新快递公司 + * + * @param updateReqVO 更新信息 + */ + void updateDeliveryExpress(@Valid DeliveryExpressUpdateReqVO updateReqVO); + + /** + * 删除快递公司 + * + * @param id 编号 + */ + void deleteDeliveryExpress(Long id); + + /** + * 获得快递公司 + * + * @param id 编号 + * @return 快递公司 + */ + DeliveryExpressDO getDeliveryExpress(Long id); + + /** + * 获得快递公司列表 + * + * @param ids 编号 + * @return 快递公司列表 + */ + List getDeliveryExpressList(Collection ids); + + /** + * 获得快递公司分页 + * + * @param pageReqVO 分页查询 + * @return 快递公司分页 + */ + PageResult getDeliveryExpressPage(DeliveryExpressPageReqVO pageReqVO); + + /** + * 获得快递公司列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 快递公司列表 + */ + List getDeliveryExpressList(DeliveryExpressExportReqVO exportReqVO); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressServiceImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressServiceImpl.java new file mode 100644 index 0000000..a96b646 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressServiceImpl.java @@ -0,0 +1,102 @@ +package com.yunxi.scm.module.trade.service.delivery; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressCreateReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressExportReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressPageReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.express.DeliveryExpressUpdateReqVO; +import com.yunxi.scm.module.trade.convert.delivery.DeliveryExpressConvert; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.yunxi.scm.module.trade.dal.mysql.delivery.DeliveryExpressMapper; +import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import org.springframework.validation.annotation.Validated; + +import java.util.*; + + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.*; + +/** + * 快递公司 Service 实现类 + * + * @author jason + */ +@Service +@Validated +public class DeliveryExpressServiceImpl implements DeliveryExpressService { + + @Resource + private DeliveryExpressMapper deliveryExpressMapper; + + @Override + public Long createDeliveryExpress(DeliveryExpressCreateReqVO createReqVO) { + //校验编码是否唯一 + validateExpressCodeUnique(createReqVO.getCode(), null); + // 插入 + DeliveryExpressDO deliveryExpress = DeliveryExpressConvert.INSTANCE.convert(createReqVO); + deliveryExpressMapper.insert(deliveryExpress); + // 返回 + return deliveryExpress.getId(); + } + + @Override + public void updateDeliveryExpress(DeliveryExpressUpdateReqVO updateReqVO) { + // 校验存在 + validateDeliveryExpressExists(updateReqVO.getId()); + //校验编码是否唯一 + validateExpressCodeUnique(updateReqVO.getCode(), updateReqVO.getId()); + // 更新 + DeliveryExpressDO updateObj = DeliveryExpressConvert.INSTANCE.convert(updateReqVO); + deliveryExpressMapper.updateById(updateObj); + } + + @Override + public void deleteDeliveryExpress(Long id) { + // 校验存在 + validateDeliveryExpressExists(id); + // 删除 + deliveryExpressMapper.deleteById(id); + } + + private void validateExpressCodeUnique(String code, Long id) { + DeliveryExpressDO express = deliveryExpressMapper.selectByCode(code); + if (express == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的快递公司 + if (id == null) { + throw exception(EXPRESS_CODE_DUPLICATE); + } + if (!express.getId().equals(id)) { + throw exception(EXPRESS_CODE_DUPLICATE); + } + } + private void validateDeliveryExpressExists(Long id) { + if (deliveryExpressMapper.selectById(id) == null) { + throw exception(EXPRESS_NOT_EXISTS); + } + } + + @Override + public DeliveryExpressDO getDeliveryExpress(Long id) { + return deliveryExpressMapper.selectById(id); + } + + @Override + public List getDeliveryExpressList(Collection ids) { + return deliveryExpressMapper.selectBatchIds(ids); + } + + @Override + public PageResult getDeliveryExpressPage(DeliveryExpressPageReqVO pageReqVO) { + return deliveryExpressMapper.selectPage(pageReqVO); + } + + @Override + public List getDeliveryExpressList(DeliveryExpressExportReqVO exportReqVO) { + return deliveryExpressMapper.selectList(exportReqVO); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressTemplateService.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressTemplateService.java new file mode 100644 index 0000000..133fed3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressTemplateService.java @@ -0,0 +1,95 @@ +package com.yunxi.scm.module.trade.service.delivery; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateCreateReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateDetailRespVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplatePageReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateUpdateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import com.yunxi.scm.module.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 快递运费模板 Service 接口 + * + * @author jason + */ +public interface DeliveryExpressTemplateService { + + /** + * 创建快递运费模板 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeliveryExpressTemplate(@Valid DeliveryExpressTemplateCreateReqVO createReqVO); + + /** + * 更新快递运费模板 + * + * @param updateReqVO 更新信息 + */ + void updateDeliveryExpressTemplate(@Valid DeliveryExpressTemplateUpdateReqVO updateReqVO); + + /** + * 删除快递运费模板 + * + * @param id 编号 + */ + void deleteDeliveryExpressTemplate(Long id); + + /** + * 获得快递运费模板 + * + * @param id 编号 + * @return 快递运费模板详情 + */ + DeliveryExpressTemplateDetailRespVO getDeliveryExpressTemplate(Long id); + + /** + * 获得快递运费模板列表 + * + * @param ids 编号 + * @return 快递运费模板列表 + */ + List getDeliveryExpressTemplateList(Collection ids); + + /** + * 获得快递运费模板列表 + * + * @return 快递运费模板列表 + */ + List getDeliveryExpressTemplateList(); + + /** + * 获得快递运费模板分页 + * + * @param pageReqVO 分页查询 + * @return 快递运费模板分页 + */ + PageResult getDeliveryExpressTemplatePage(DeliveryExpressTemplatePageReqVO pageReqVO); + + /** + * 校验快递运费模板 + * + * 如果校验不通过,抛出 {@link com.yunxi.scm.framework.common.exception.ServiceException} 异常 + * + * @param templateId 模板编号 + * @return 快递运费模板 + */ + DeliveryExpressTemplateDO validateDeliveryExpressTemplate(Long templateId); + + /** + * 基于运费模板编号数组和收件人地址区域编号,获取匹配运费模板 + * + * @param ids 编号列表 + * @param areaId 区域编号 + * @return Map (templateId -> 运费模板设置) + */ + Map getExpressTemplateMapByIdsAndArea(Collection ids, Integer areaId); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java new file mode 100644 index 0000000..ea4f5c1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryExpressTemplateServiceImpl.java @@ -0,0 +1,251 @@ +package com.yunxi.scm.module.trade.service.delivery; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateCreateReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateDetailRespVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplatePageReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.expresstemplate.DeliveryExpressTemplateUpdateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateChargeDO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateDO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressTemplateFreeDO; +import com.yunxi.scm.module.trade.dal.mysql.delivery.DeliveryExpressTemplateChargeMapper; +import com.yunxi.scm.module.trade.dal.mysql.delivery.DeliveryExpressTemplateFreeMapper; +import com.yunxi.scm.module.trade.dal.mysql.delivery.DeliveryExpressTemplateMapper; +import com.yunxi.scm.module.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.*; +import static com.yunxi.scm.module.trade.convert.delivery.DeliveryExpressTemplateConvert.INSTANCE; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.EXPRESS_TEMPLATE_NAME_DUPLICATE; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.EXPRESS_TEMPLATE_NOT_EXISTS; + +/** + * 快递运费模板 Service 实现类 + * + * @author jason + */ +@Service +@Validated +public class DeliveryExpressTemplateServiceImpl implements DeliveryExpressTemplateService { + + @Resource + private DeliveryExpressTemplateMapper expressTemplateMapper; + @Resource + private DeliveryExpressTemplateChargeMapper expressTemplateChargeMapper; + @Resource + private DeliveryExpressTemplateFreeMapper expressTemplateFreeMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createDeliveryExpressTemplate(DeliveryExpressTemplateCreateReqVO createReqVO) { + // 校验模板名是否唯一 + validateTemplateNameUnique(createReqVO.getName(), null); + + // 插入 + DeliveryExpressTemplateDO deliveryExpressTemplate = INSTANCE.convert(createReqVO); + expressTemplateMapper.insert(deliveryExpressTemplate); + // 插入运费模板计费表 + if (CollUtil.isNotEmpty(createReqVO.getTemplateCharge())) { + expressTemplateChargeMapper.insertBatch( + INSTANCE.convertTemplateChargeList(deliveryExpressTemplate.getId(), createReqVO.getChargeMode(), createReqVO.getTemplateCharge()) + ); + } + // 插入运费模板包邮表 + if (CollUtil.isNotEmpty(createReqVO.getTemplateFree())) { + expressTemplateFreeMapper.insertBatch( + INSTANCE.convertTemplateFreeList(deliveryExpressTemplate.getId(), createReqVO.getTemplateFree()) + ); + } + return deliveryExpressTemplate.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateDeliveryExpressTemplate(DeliveryExpressTemplateUpdateReqVO updateReqVO) { + // 校验存在 + validateDeliveryExpressTemplateExists(updateReqVO.getId()); + // 校验模板名是否唯一 + validateTemplateNameUnique(updateReqVO.getName(), updateReqVO.getId()); + + // 更新运费从表 + updateExpressTemplateCharge(updateReqVO); + // 更新包邮从表 + updateExpressTemplateFree(updateReqVO); + // 更新模板主表 + DeliveryExpressTemplateDO updateObj = INSTANCE.convert(updateReqVO); + expressTemplateMapper.updateById(updateObj); + } + + private void updateExpressTemplateFree(DeliveryExpressTemplateUpdateReqVO updateReqVO) { + // 1.1 获得新增/修改的区域列表 + List oldFreeList = expressTemplateFreeMapper.selectListByTemplateId(updateReqVO.getId()); + List newFreeList = updateReqVO.getTemplateFree(); + List addFreeList = new ArrayList<>(newFreeList.size()); // 新增包邮区域列表 + List updateFreeList = new ArrayList<>(newFreeList.size()); // 更新包邮区域列表 + for (DeliveryExpressTemplateUpdateReqVO.ExpressTemplateFreeUpdateVO item : newFreeList) { + if (Objects.nonNull(item.getId())) { + updateFreeList.add(INSTANCE.convertTemplateFree(item)); + } else { + item.setTemplateId(updateReqVO.getId()); + addFreeList.add(INSTANCE.convertTemplateFree(item)); + } + } + // 1.2 新增 + if (CollUtil.isNotEmpty(addFreeList)) { + expressTemplateFreeMapper.insertBatch(addFreeList); + } + // 1.3 修改 + if (CollUtil.isNotEmpty(updateFreeList)) { + expressTemplateFreeMapper.updateBatch(updateFreeList); + } + + // 2. 删除 + Set deleteFreeIds = convertSet(oldFreeList, DeliveryExpressTemplateFreeDO::getId); + deleteFreeIds.removeAll(convertSet(updateFreeList, DeliveryExpressTemplateFreeDO::getId)); + if (CollUtil.isNotEmpty(deleteFreeIds)) { + expressTemplateFreeMapper.deleteBatchIds(deleteFreeIds); + } + } + + private void updateExpressTemplateCharge(DeliveryExpressTemplateUpdateReqVO updateReqVO) { + // 1.1 获得新增/修改的区域列表 + List oldChargeList = expressTemplateChargeMapper.selectListByTemplateId(updateReqVO.getId()); + List newChargeList = updateReqVO.getTemplateCharge(); + List addList = new ArrayList<>(newChargeList.size()); // 新增运费区域列表 + List updateList = new ArrayList<>(newChargeList.size()); // 更新运费区域列表 + for (DeliveryExpressTemplateUpdateReqVO.ExpressTemplateChargeUpdateVO item : newChargeList) { + if (item.getId() != null) { + // 计费模式以主表为准 + item.setChargeMode(updateReqVO.getChargeMode()); + updateList.add(INSTANCE.convertTemplateCharge(item)); + } else { + item.setTemplateId(updateReqVO.getId()); + item.setChargeMode(updateReqVO.getChargeMode()); + addList.add(INSTANCE.convertTemplateCharge(item)); + } + } + // 1.2 新增 + if (CollUtil.isNotEmpty(addList)) { + expressTemplateChargeMapper.insertBatch(addList); + } + // 1.3 修改 + if (CollUtil.isNotEmpty(updateList)) { + expressTemplateChargeMapper.updateBatch(updateList); + } + + // 2. 删除 + Set deleteChargeIds = convertSet(oldChargeList, DeliveryExpressTemplateChargeDO::getId); + deleteChargeIds.removeAll(convertSet(updateList, DeliveryExpressTemplateChargeDO::getId)); + if (CollUtil.isNotEmpty(deleteChargeIds)) { + expressTemplateChargeMapper.deleteBatchIds(deleteChargeIds); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDeliveryExpressTemplate(Long id) { + // 校验存在 + validateDeliveryExpressTemplateExists(id); + + // 删除主表 + expressTemplateMapper.deleteById(id); + // 删除运费从表 + expressTemplateChargeMapper.deleteByTemplateId(id); + // 删除包邮从表 + expressTemplateFreeMapper.deleteByTemplateId(id); + } + + /** + * 校验运费模板名是否唯一 + * + * @param name 模板名称 + * @param id 运费模板编号,可以为 null + */ + private void validateTemplateNameUnique(String name, Long id) { + DeliveryExpressTemplateDO template = expressTemplateMapper.selectByName(name); + if (template == null) { + return; + } + // 如果 id 为空 + if (id == null) { + throw exception(EXPRESS_TEMPLATE_NAME_DUPLICATE); + } + if (!template.getId().equals(id)) { + throw exception(EXPRESS_TEMPLATE_NAME_DUPLICATE); + } + } + + private void validateDeliveryExpressTemplateExists(Long id) { + if (expressTemplateMapper.selectById(id) == null) { + throw exception(EXPRESS_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public DeliveryExpressTemplateDetailRespVO getDeliveryExpressTemplate(Long id) { + List chargeList = expressTemplateChargeMapper.selectListByTemplateId(id); + List freeList = expressTemplateFreeMapper.selectListByTemplateId(id); + DeliveryExpressTemplateDO template = expressTemplateMapper.selectById(id); + return INSTANCE.convert(template, chargeList, freeList); + } + + @Override + public List getDeliveryExpressTemplateList(Collection ids) { + return expressTemplateMapper.selectBatchIds(ids); + } + + @Override + public List getDeliveryExpressTemplateList() { + return expressTemplateMapper.selectList(); + } + + @Override + public PageResult getDeliveryExpressTemplatePage(DeliveryExpressTemplatePageReqVO pageReqVO) { + return expressTemplateMapper.selectPage(pageReqVO); + } + + @Override + public DeliveryExpressTemplateDO validateDeliveryExpressTemplate(Long templateId) { + DeliveryExpressTemplateDO template = expressTemplateMapper.selectById(templateId); + if (template == null) { + throw exception(EXPRESS_TEMPLATE_NOT_EXISTS); + } + return template; + } + + @Override + public Map getExpressTemplateMapByIdsAndArea(Collection ids, Integer areaId) { + Assert.notNull(areaId, "区域编号 {} 不能为空", areaId); + // 查询 template 数组 + if (CollUtil.isEmpty(ids)) { + return Collections.emptyMap(); + } + List templateList = expressTemplateMapper.selectBatchIds(ids); + // 查询 templateCharge 数组 + List chargeList = expressTemplateChargeMapper.selectByTemplateIds(ids); + // 查询 templateFree 数组 + List freeList = expressTemplateFreeMapper.selectListByTemplateIds(ids); + + // 组合运费模板配置 RespBO + return INSTANCE.convertMap(areaId, templateList, chargeList, freeList); + } + + private DeliveryExpressTemplateRespBO.Charge findMatchExpressTemplateCharge( + List templateChargeList, Integer areaId) { + return INSTANCE.convertTemplateCharge(findFirst(templateChargeList, item -> item.getAreaIds().contains(areaId))); + } + + private DeliveryExpressTemplateRespBO.Free findMatchExpressTemplateFree( + List templateFreeList, Integer areaId) { + return INSTANCE.convertTemplateFree(findFirst(templateFreeList, item -> item.getAreaIds().contains(areaId))); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryPickUpStoreService.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryPickUpStoreService.java new file mode 100644 index 0000000..a32768b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryPickUpStoreService.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.module.trade.service.delivery; + +import java.util.*; +import javax.validation.*; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreCreateReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreExportReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStorePageReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreUpdateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; + +/** + * 自提门店 Service 接口 + * + * @author jason + */ +public interface DeliveryPickUpStoreService { + + /** + * 创建自提门店 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDeliveryPickUpStore(@Valid DeliveryPickUpStoreCreateReqVO createReqVO); + + /** + * 更新自提门店 + * + * @param updateReqVO 更新信息 + */ + void updateDeliveryPickUpStore(@Valid DeliveryPickUpStoreUpdateReqVO updateReqVO); + + /** + * 删除自提门店 + * + * @param id 编号 + */ + void deleteDeliveryPickUpStore(Long id); + + /** + * 获得自提门店 + * + * @param id 编号 + * @return 自提门店 + */ + DeliveryPickUpStoreDO getDeliveryPickUpStore(Long id); + + /** + * 获得自提门店列表 + * + * @param ids 编号 + * @return 自提门店列表 + */ + List getDeliveryPickUpStoreList(Collection ids); + + /** + * 获得自提门店分页 + * + * @param pageReqVO 分页查询 + * @return 自提门店分页 + */ + PageResult getDeliveryPickUpStorePage(DeliveryPickUpStorePageReqVO pageReqVO); + + /** + * 获得自提门店列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 自提门店列表 + */ + List getDeliveryPickUpStoreList(DeliveryPickUpStoreExportReqVO exportReqVO); +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java new file mode 100644 index 0000000..8edd116 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/DeliveryPickUpStoreServiceImpl.java @@ -0,0 +1,84 @@ +package com.yunxi.scm.module.trade.service.delivery; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreCreateReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreExportReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStorePageReqVO; +import com.yunxi.scm.module.trade.controller.admin.delivery.vo.pickup.DeliveryPickUpStoreUpdateReqVO; +import com.yunxi.scm.module.trade.convert.delivery.DeliveryPickUpStoreConvert; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryPickUpStoreDO; +import com.yunxi.scm.module.trade.dal.mysql.delivery.DeliveryPickUpStoreMapper; +import org.springframework.stereotype.Service; +import javax.annotation.Resource; +import org.springframework.validation.annotation.Validated; + +import java.util.*; + + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.*; + +/** + * 自提门店 Service 实现类 + * + * @author jason + */ +@Service +@Validated +public class DeliveryPickUpStoreServiceImpl implements DeliveryPickUpStoreService { + + @Resource + private DeliveryPickUpStoreMapper deliveryPickUpStoreMapper; + + @Override + public Long createDeliveryPickUpStore(DeliveryPickUpStoreCreateReqVO createReqVO) { + // 插入 + DeliveryPickUpStoreDO deliveryPickUpStore = DeliveryPickUpStoreConvert.INSTANCE.convert(createReqVO); + deliveryPickUpStoreMapper.insert(deliveryPickUpStore); + // 返回 + return deliveryPickUpStore.getId(); + } + + @Override + public void updateDeliveryPickUpStore(DeliveryPickUpStoreUpdateReqVO updateReqVO) { + // 校验存在 + validateDeliveryPickUpStoreExists(updateReqVO.getId()); + // 更新 + DeliveryPickUpStoreDO updateObj = DeliveryPickUpStoreConvert.INSTANCE.convert(updateReqVO); + deliveryPickUpStoreMapper.updateById(updateObj); + } + + @Override + public void deleteDeliveryPickUpStore(Long id) { + // 校验存在 + validateDeliveryPickUpStoreExists(id); + // 删除 + deliveryPickUpStoreMapper.deleteById(id); + } + + private void validateDeliveryPickUpStoreExists(Long id) { + if (deliveryPickUpStoreMapper.selectById(id) == null) { + throw exception(PICK_UP_STORE_NOT_EXISTS); + } + } + + @Override + public DeliveryPickUpStoreDO getDeliveryPickUpStore(Long id) { + return deliveryPickUpStoreMapper.selectById(id); + } + + @Override + public List getDeliveryPickUpStoreList(Collection ids) { + return deliveryPickUpStoreMapper.selectBatchIds(ids); + } + + @Override + public PageResult getDeliveryPickUpStorePage(DeliveryPickUpStorePageReqVO pageReqVO) { + return deliveryPickUpStoreMapper.selectPage(pageReqVO); + } + + @Override + public List getDeliveryPickUpStoreList(DeliveryPickUpStoreExportReqVO exportReqVO) { + return deliveryPickUpStoreMapper.selectList(exportReqVO); + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/bo/DeliveryExpressTemplateRespBO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/bo/DeliveryExpressTemplateRespBO.java new file mode 100644 index 0000000..6c5fcca --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/delivery/bo/DeliveryExpressTemplateRespBO.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.trade.service.delivery.bo; + +import com.yunxi.scm.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; +import lombok.Data; + +/** + * 运费模板配置 Resp BO + * + * @author jason + */ +@Data +public class DeliveryExpressTemplateRespBO { + + /** + * 配送计费方式 + * + * 枚举 {@link DeliveryExpressChargeModeEnum} + */ + private Integer chargeMode; + + /** + * 运费模板快递运费设置 + */ + private Charge charge; + + /** + * 运费模板包邮设置 + */ + private Free free; + + /** + * 快递运费模板费用配置 BO + * + * @author jason + */ + @Data + public static class Charge { + + /** + * 首件数量(件数,重量,或体积) + */ + private Double startCount; + /** + * 起步价,单位:分 + */ + private Integer startPrice; + /** + * 续件数量(件, 重量,或体积) + */ + private Double extraCount; + /** + * 额外价,单位:分 + */ + private Integer extraPrice; + } + + /** + * 快递运费模板包邮配置 BO + * + * @author jason + */ + @Data + public static class Free { + + /** + * 包邮金额,单位:分 + * + * 订单总金额 > 包邮金额时,才免运费 + */ + private Integer freePrice; + + /** + * 包邮件数 + * + * 订单总件数 > 包邮件数时,才免运费 + */ + private Integer freeCount; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/order/TradeOrderService.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/order/TradeOrderService.java new file mode 100644 index 0000000..927b947 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/order/TradeOrderService.java @@ -0,0 +1,190 @@ +package com.yunxi.scm.module.trade.service.order; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderPageReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderPageReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderSettlementReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; + +import java.util.Collection; +import java.util.List; + +import static java.util.Collections.singleton; + +/** + * 交易订单 Service 接口 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface TradeOrderService { + + // =================== Order =================== + + /** + * 获得订单结算信息 + * + * @param userId 登录用户 + * @param settlementReqVO 订单结算请求 + * @return 订单结算结果 + */ + AppTradeOrderSettlementRespVO settlementOrder(Long userId, AppTradeOrderSettlementReqVO settlementReqVO); + + /** + * 【会员】创建交易订单 + * + * @param userId 登录用户 + * @param userIp 用户 IP 地址 + * @param createReqVO 创建交易订单请求模型 + * @return 交易订单的 + */ + TradeOrderDO createOrder(Long userId, String userIp, AppTradeOrderCreateReqVO createReqVO); + + /** + * 更新交易订单已支付 + * + * @param id 交易订单编号 + * @param payOrderId 支付订单编号 + */ + void updateOrderPaid(Long id, Long payOrderId); + + /** + * 【管理员】发货交易订单 + * + * @param userId 管理员编号 + * @param deliveryReqVO 发货请求 + */ + void deliveryOrder(Long userId, TradeOrderDeliveryReqVO deliveryReqVO); + + /** + * 【会员】收货交易订单 + * + * @param userId 用户编号 + * @param id 订单编号 + */ + void receiveOrder(Long userId, Long id); + + /** + * 获得指定编号的交易订单 + * + * @param id 交易订单编号 + * @return 交易订单 + */ + TradeOrderDO getOrder(Long id); + + /** + * 获得指定用户,指定的交易订单 + * + * @param userId 用户编号 + * @param id 交易订单编号 + * @return 交易订单 + */ + TradeOrderDO getOrder(Long userId, Long id); + + /** + * 【管理员】获得交易订单分页 + * + * @param reqVO 分页请求 + * @return 交易订单 + */ + PageResult getOrderPage(TradeOrderPageReqVO reqVO); + + /** + * 【会员】获得交易订单分页 + * + * @param userId 用户编号 + * @param reqVO 分页请求 + * @return 交易订单 + */ + PageResult getOrderPage(Long userId, AppTradeOrderPageReqVO reqVO); + + /** + * 【会员】获得交易订单数量 + * + * @param userId 用户编号 + * @param status 订单状态。如果为空,则不进行筛选 + * @param commonStatus 评价状态。如果为空,则不进行筛选 + * @return 订单数量 + */ + Long getOrderCount(Long userId, Integer status, Boolean commonStatus); + + // =================== Order Item =================== + + /** + * 获得指定用户,指定的交易订单项 + * + * @param userId 用户编号 + * @param itemId 交易订单项编号 + * @return 交易订单项 + */ + TradeOrderItemDO getOrderItem(Long userId, Long itemId); + + /** + * 更新交易订单项的售后状态 + * + * @param id 交易订单项编号 + * @param oldAfterSaleStatus 当前售后状态;如果不符,更新后会抛出异常 + * @param newAfterSaleStatus 目标售后状态 + * @param refundPrice 退款金额;当订单项退款成功时,必须传递该值 + */ + void updateOrderItemAfterSaleStatus(Long id, Integer oldAfterSaleStatus, + Integer newAfterSaleStatus, Integer refundPrice); + + /** + * 根据交易订单项编号数组,查询交易订单项 + * + * @param ids 交易订单项编号数组 + * @return 交易订单项数组 + */ + List getOrderItemList(Collection ids); + + /** + * 根据交易订单编号,查询交易订单项 + * + * @param orderId 交易订单编号 + * @return 交易订单项数组 + */ + default List getOrderItemListByOrderId(Long orderId) { + return getOrderItemListByOrderId(singleton(orderId)); + } + + /** + * 根据交易订单编号数组,查询交易订单项 + * + * @param orderIds 交易订单编号数组 + * @return 交易订单项数组 + */ + List getOrderItemListByOrderId(Collection orderIds); + + /** + * 得到订单项通过 订单项 id 和用户 id + * + * @param orderItemId 订单项 id + * @param loginUserId 登录用户 id + * @return 得到订单项 + */ + TradeOrderItemDO getOrderItemByIdAndUserId(Long orderItemId, Long loginUserId); + + /** + * 得到订单通过 id 和 用户 id + * + * @param orderId 订单 id + * @param loginUserId 登录用户 id + * @return 得到订单 + */ + TradeOrderDO getOrderByIdAndUserId(Long orderId, Long loginUserId); + + /** + * 创建订单项的评论 + * + * @param createReqVO 创建请求 + * @return 得到评价 id + */ + Long createOrderItemComment(AppTradeOrderItemCommentCreateReqVO createReqVO); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/order/TradeOrderServiceImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/order/TradeOrderServiceImpl.java new file mode 100644 index 0000000..5faa350 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/order/TradeOrderServiceImpl.java @@ -0,0 +1,723 @@ +package com.yunxi.scm.module.trade.service.order; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.enums.TerminalEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.module.member.api.address.AddressApi; +import com.yunxi.scm.module.member.api.address.dto.AddressRespDTO; +import com.yunxi.scm.module.member.api.user.MemberUserApi; +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.pay.api.order.PayOrderApi; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderRespDTO; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.yunxi.scm.module.product.api.comment.ProductCommentApi; +import com.yunxi.scm.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import com.yunxi.scm.module.product.api.sku.ProductSkuApi; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuUpdateStockReqDTO; +import com.yunxi.scm.module.promotion.api.combination.CombinationApi; +import com.yunxi.scm.module.promotion.api.coupon.CouponApi; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponUseReqDTO; +import com.yunxi.scm.module.promotion.enums.combination.CombinationRecordStatusEnum; +import com.yunxi.scm.module.system.api.notify.NotifyMessageSendApi; +import com.yunxi.scm.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderPageReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderPageReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderSettlementReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderSettlementRespVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.item.AppTradeOrderItemCommentCreateReqVO; +import com.yunxi.scm.module.trade.convert.order.TradeOrderConvert; +import com.yunxi.scm.module.trade.dal.dataobject.cart.TradeCartDO; +import com.yunxi.scm.module.trade.dal.dataobject.delivery.DeliveryExpressDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDeliveryDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.dal.mysql.order.TradeOrderDeliveryMapper; +import com.yunxi.scm.module.trade.dal.mysql.order.TradeOrderItemMapper; +import com.yunxi.scm.module.trade.dal.mysql.order.TradeOrderMapper; +import com.yunxi.scm.module.trade.enums.ErrorCodeConstants; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryTypeEnum; +import com.yunxi.scm.module.trade.enums.order.*; +import com.yunxi.scm.module.trade.framework.order.config.TradeOrderProperties; +import com.yunxi.scm.module.trade.service.cart.TradeCartService; +import com.yunxi.scm.module.trade.service.delivery.DeliveryExpressService; +import com.yunxi.scm.module.trade.service.price.TradePriceService; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.getSumValue; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.ORDER_NOT_FOUND; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.*; + +/** + * 交易订单 Service 实现类 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +@Service +@Slf4j +public class TradeOrderServiceImpl implements TradeOrderService { + + @Resource + private TradeOrderMapper tradeOrderMapper; + @Resource + private TradeOrderItemMapper tradeOrderItemMapper; + + @Resource + private TradeCartService tradeCartService; + @Resource + private TradePriceService tradePriceService; + @Resource + private DeliveryExpressService deliveryExpressService; + + @Resource + private ProductSkuApi productSkuApi; + @Resource + private PayOrderApi payOrderApi; + @Resource + private AddressApi addressApi; + @Resource + private CouponApi couponApi; + @Resource + private MemberUserApi memberUserApi; + @Resource + private ProductCommentApi productCommentApi; + @Resource + private NotifyMessageSendApi notifyMessageSendApi; + + @Resource + private TradeOrderProperties tradeOrderProperties; + + @Resource + private CombinationApi combinationApi; + @Resource + private TradeOrderDeliveryMapper orderDeliveryMapper; + // =================== Order =================== + + @Override + public AppTradeOrderSettlementRespVO settlementOrder(Long userId, AppTradeOrderSettlementReqVO settlementReqVO) { + // 1. 获得收货地址 + AddressRespDTO address = getAddress(userId, settlementReqVO.getAddressId()); + if (address != null) { + settlementReqVO.setAddressId(address.getId()); + } + + // 2. 计算价格 + TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, settlementReqVO); + + // 3. 拼接返回 + return TradeOrderConvert.INSTANCE.convert(calculateRespBO, address); + } + + /** + * 获得用户地址 + * + * @param userId 用户编号 + * @param addressId 地址编号 + * @return 地址 + */ + private AddressRespDTO getAddress(Long userId, Long addressId) { + if (addressId != null) { + return addressApi.getAddress(addressId, userId); + } + return addressApi.getDefaultAddress(userId); + } + + /** + * 计算订单价格 + * + * @param userId 用户编号 + * @param settlementReqVO 结算信息 + * @return 订单价格 + */ + private TradePriceCalculateRespBO calculatePrice(Long userId, AppTradeOrderSettlementReqVO settlementReqVO) { + // 1. 如果来自购物车,则获得购物车的商品 + List cartList = tradeCartService.getCartList(userId, + convertSet(settlementReqVO.getItems(), AppTradeOrderSettlementReqVO.Item::getCartId)); + + // 2. 计算价格 + TradePriceCalculateReqBO calculateReqBO = TradeOrderConvert.INSTANCE.convert(userId, settlementReqVO, cartList); + calculateReqBO.getItems().forEach(item -> Assert.isTrue(item.getSelected(), // 防御性编程,保证都是选中的 + "商品({}) 未设置为选中", item.getSkuId())); + return tradePriceService.calculatePrice(calculateReqBO); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public TradeOrderDO createOrder(Long userId, String userIp, AppTradeOrderCreateReqVO createReqVO) { + // 2. 价格计算 + TradePriceCalculateRespBO calculateRespBO = calculatePrice(userId, createReqVO); + // 3.1 插入 TradeOrderDO 订单 + TradeOrderDO order = createTradeOrder(userId, userIp, createReqVO, calculateRespBO); + // 3.2 插入 TradeOrderItemDO 订单项 + List orderItems = createTradeOrderItems(order, calculateRespBO); + // 订单创建完后的逻辑 + afterCreateTradeOrder(userId, createReqVO, order, orderItems, calculateRespBO); + // 3.3 校验订单类型 + // 拼团 + if (ObjectUtil.equal(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) { + MemberUserRespDTO user = memberUserApi.getUser(userId); + // TODO 拼团一次应该只能选择一种规格的商品 + combinationApi.createRecord(TradeOrderConvert.INSTANCE.convert(order, orderItems.get(0), createReqVO, user) + .setStatus(CombinationRecordStatusEnum.WAITING.getStatus())); + } + // TODO 秒杀扣减库存是下单就扣除还是等待订单支付成功再扣除 + if (ObjectUtil.equal(TradeOrderTypeEnum.SECKILL.getType(), order.getType())) { + + } + + // TODO @LeeYan9: 是可以思考下, 订单的营销优惠记录, 应该记录在哪里, 微信讨论起来! + return order; + } + + /** + * 校验收件地址是否存在 + * + * @param userId 用户编号 + * @param addressId 收件地址编号 + * @return 收件地址 + */ + private AddressRespDTO validateAddress(Long userId, Long addressId) { + AddressRespDTO address = addressApi.getAddress(addressId, userId); + if (address == null) { + throw exception(ErrorCodeConstants.ORDER_CREATE_ADDRESS_NOT_FOUND); + } + return address; + } + + private TradeOrderDO createTradeOrder(Long userId, String clientIp, AppTradeOrderCreateReqVO createReqVO, + TradePriceCalculateRespBO calculateRespBO) { + // 用户选择物流配送的时候才需要填写收货地址 + AddressRespDTO address = new AddressRespDTO(); + if (ObjectUtil.equal(createReqVO.getDeliveryType(), DeliveryTypeEnum.EXPRESS.getMode())) { + // 用户收件地址的校验 + address = validateAddress(userId, createReqVO.getAddressId()); + } + TradeOrderDO order = TradeOrderConvert.INSTANCE.convert(userId, clientIp, createReqVO, calculateRespBO, address); + order.setType(validateActivity(createReqVO)); + order.setNo(IdUtil.getSnowflakeNextId() + ""); // TODO @LeeYan9: 思考下, 怎么生成好点哈; 这个是会展示给用户的; + order.setStatus(TradeOrderStatusEnum.UNPAID.getStatus()); + order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()); + order.setProductCount(getSumValue(calculateRespBO.getItems(), TradePriceCalculateRespBO.OrderItem::getCount, Integer::sum)); + order.setTerminal(TerminalEnum.H5.getTerminal()); // todo 数据来源? + // 支付信息 + order.setAdjustPrice(0).setPayStatus(false); + // 物流信息 TODO 芋艿:暂时写死物流方式;应该是前端选择的 + order.setDeliveryType(createReqVO.getDeliveryType()).setDeliveryStatus(TradeOrderDeliveryStatusEnum.UNDELIVERED.getStatus()); + // 退款信息 + order.setRefundStatus(TradeOrderRefundStatusEnum.NONE.getStatus()).setRefundPrice(0); + tradeOrderMapper.insert(order); + return order; + } + + /** + * 校验活动,并返回订单类型 + * + * @param createReqVO 请求参数 + * @return 订单类型 + */ + private Integer validateActivity(AppTradeOrderCreateReqVO createReqVO) { + if (createReqVO.getSeckillActivityId() != null) { + return TradeOrderTypeEnum.SECKILL.getType(); + } + if (createReqVO.getCombinationActivityId() != null) { + return TradeOrderTypeEnum.COMBINATION.getType(); + } + // TODO 砍价敬请期待 + return TradeOrderTypeEnum.NORMAL.getType(); + } + + private List createTradeOrderItems(TradeOrderDO tradeOrderDO, TradePriceCalculateRespBO calculateRespBO) { + List orderItems = TradeOrderConvert.INSTANCE.convertList(tradeOrderDO, calculateRespBO); + tradeOrderItemMapper.insertBatch(orderItems); + return orderItems; + } + + /** + * 执行创建完创建完订单后的逻辑 + * + * 例如说:优惠劵的扣减、积分的扣减、支付单的创建等等 + * + * @param userId 用户编号 + * @param createReqVO 创建订单请求 + * @param tradeOrderDO 交易订单 + * @param calculateRespBO 订单价格计算结果 + */ + private void afterCreateTradeOrder(Long userId, AppTradeOrderCreateReqVO createReqVO, + TradeOrderDO tradeOrderDO, List orderItems, + TradePriceCalculateRespBO calculateRespBO) { + // 下单时扣减商品库存 + productSkuApi.updateSkuStock(new ProductSkuUpdateStockReqDTO(TradeOrderConvert.INSTANCE.convertList(orderItems))); + + // 删除购物车商品 TODO 芋艿:待实现 + + // 扣减积分,抵扣金额 TODO 芋艿:待实现 + + // 有使用优惠券时更新 + if (createReqVO.getCouponId() != null) { + couponApi.useCoupon(new CouponUseReqDTO().setId(createReqVO.getCouponId()).setUserId(userId) + .setOrderId(tradeOrderDO.getId())); + } + + // 生成预支付 + createPayOrder(tradeOrderDO, orderItems, calculateRespBO); + + // 增加订单日志 TODO 芋艿:待实现 + } + + private void createPayOrder(TradeOrderDO order, List orderItems, TradePriceCalculateRespBO calculateRespBO) { + // 创建支付单,用于后续的支付 + PayOrderCreateReqDTO payOrderCreateReqDTO = TradeOrderConvert.INSTANCE.convert( + order, orderItems, calculateRespBO, tradeOrderProperties); + Long payOrderId = payOrderApi.createOrder(payOrderCreateReqDTO); + + // 更新到交易单上 + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()).setPayOrderId(payOrderId)); + order.setPayOrderId(payOrderId); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateOrderPaid(Long id, Long payOrderId) { + // 校验并获得交易订单(可支付) + KeyValue orderResult = validateOrderPayable(id, payOrderId); + TradeOrderDO order = orderResult.getKey(); + PayOrderRespDTO payOrder = orderResult.getValue(); + + // 更新 TradeOrderDO 状态为已支付,等待发货 + int updateCount = tradeOrderMapper.updateByIdAndStatus(id, order.getStatus(), + new TradeOrderDO().setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus()).setPayStatus(true) + .setPayTime(LocalDateTime.now()).setPayChannelCode(payOrder.getChannelCode())); + if (updateCount == 0) { + throw exception(ORDER_UPDATE_PAID_STATUS_NOT_UNPAID); + } + // 校验活动 + // 1、拼团活动 + if (ObjectUtil.equal(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) { + // 更新拼团状态 TODO puhui999:订单支付失败或订单支付过期删除这条拼团记录 + combinationApi.updateRecordStatusAndStartTime(order.getUserId(), order.getId(), CombinationRecordStatusEnum.IN_PROGRESS.getStatus()); + } + // TODO 芋艿:发送订单变化的消息 + + // TODO 芋艿:发送站内信 + + // TODO 芋艿:OrderLog + } + + /** + * 校验交易订单满足被支付的条件 + * + * 1. 交易订单未支付 + * 2. 支付单已支付 + * + * @param id 交易订单编号 + * @param payOrderId 支付订单编号 + * @return 交易订单 + */ + private KeyValue validateOrderPayable(Long id, Long payOrderId) { + // 校验订单是否存在 + TradeOrderDO order = tradeOrderMapper.selectById(id); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 校验订单未支付 + if (!TradeOrderStatusEnum.isUnpaid(order.getStatus()) || order.getPayStatus()) { + log.error("[validateOrderPaid][order({}) 不处于待支付状态,请进行处理!order 数据是:{}]", + id, JsonUtils.toJsonString(order)); + throw exception(ORDER_UPDATE_PAID_STATUS_NOT_UNPAID); + } + // 校验支付订单匹配 + if (ObjectUtil.notEqual(order.getPayOrderId(), payOrderId)) { // 支付单号 + log.error("[validateOrderPaid][order({}) 支付单不匹配({}),请进行处理!order 数据是:{}]", + id, payOrderId, JsonUtils.toJsonString(order)); + throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + + // 校验支付单是否存在 + PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId); + if (payOrder == null) { + log.error("[validateOrderPaid][order({}) payOrder({}) 不存在,请进行处理!]", id, payOrderId); + throw exception(ORDER_NOT_FOUND); + } + // 校验支付单已支付 + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + log.error("[validateOrderPaid][order({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", + id, payOrderId, JsonUtils.toJsonString(payOrder)); + throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS); + } + // 校验支付金额一致 + if (ObjectUtil.notEqual(payOrder.getPrice(), order.getPayPrice())) { + log.error("[validateOrderPaid][order({}) payOrder({}) 支付金额不匹配,请进行处理!order 数据是:{},payOrder 数据是:{}]", + id, payOrderId, JsonUtils.toJsonString(order), JsonUtils.toJsonString(payOrder)); + throw exception(ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH); + } + // 校验支付订单匹配(二次) + if (ObjectUtil.notEqual(payOrder.getMerchantOrderId(), id.toString())) { + log.error("[validateOrderPaid][order({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", + id, payOrderId, JsonUtils.toJsonString(payOrder)); + throw exception(ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + return new KeyValue<>(order, payOrder); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deliveryOrder(Long userId, TradeOrderDeliveryReqVO deliveryReqVO) { + // 校验并获得交易订单(可发货) + TradeOrderDO order = validateOrderDeliverable(deliveryReqVO.getId()); + + TradeOrderDO tradeOrderDO = new TradeOrderDO(); + List deliveryDOs = new ArrayList<>(); + /* TODO + * fix: 首先需要店铺设置配送方式如: 自提 、配送、物流-配送、物流-配送-自提、商家配送 + * 1.如果店铺有设置配送方式用户只填写收货地址的情况下店家后台自己选择配送方式 + * 2.如果店铺只支持到店自提那么下单后默认发货不需要物流 + * 3.如果店铺支持 物流-配送-自提 的情况下后台不需要选择配送方式按前端用户选择的配送方式发货即可 + */ + // 判断发货类型 + // 快递发货 + if (ObjectUtil.equal(deliveryReqVO.getType(), DeliveryTypeEnum.EXPRESS.getMode())) { + deliveryDOs = express(order, deliveryReqVO); + tradeOrderDO.setLogisticsId(deliveryReqVO.getLogisticsId()).setLogisticsNo(deliveryReqVO.getLogisticsNo()); + } + // 用户自提 + if (ObjectUtil.equal(deliveryReqVO.getType(), DeliveryTypeEnum.PICK_UP.getMode())) { + deliveryDOs = pickUp(order, deliveryReqVO); + // 重置一下确保快递公司和快递单号为空 + tradeOrderDO.setLogisticsId(null).setLogisticsNo(""); + } + // TODO 芋艿:如果无需发货,需要怎么存储? + if (ObjectUtil.equal(deliveryReqVO.getType(), DeliveryTypeEnum.NULL.getMode())) { + // TODO 情况一:正常走发货逻辑和用户自提有点像 不同点:不需要自提门店只需要用户确认收货 + // TODO 情况二:用户下单付款后直接确认收货或等待用户确认收货 + } + + // 更新 TradeOrderDO 状态为已发货,等待收货 + tradeOrderDO.setStatus(TradeOrderStatusEnum.DELIVERED.getStatus()) + .setDeliveryStatus(TradeOrderDeliveryStatusEnum.DELIVERED.getStatus()).setDeliveryTime(LocalDateTime.now()); + int updateCount = tradeOrderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), tradeOrderDO); + if (updateCount == 0) { + throw exception(ORDER_DELIVERY_FAIL_STATUS_NOT_UNDELIVERED); + } + // 发货成功记录发货表 + orderDeliveryMapper.insertBatch(deliveryDOs); + // TODO 芋艿:发送订单变化的消息 + + // TODO @puhui999:可以抽个 message 包,里面是 Order 所有的 message;类似工作流的 + // 发送站内信 + // 1、构造消息 + Map msgMap = new HashMap<>(); + msgMap.put("orderId", deliveryReqVO.getId()); + msgMap.put("msg", TradeOrderStatusEnum.DELIVERED.getStatus()); + // 2、发送站内信 + notifyMessageSendApi.sendSingleMessageToMember( + new NotifySendSingleToUserReqDTO() + .setUserId(userId) + .setTemplateCode("order_delivery") + .setTemplateParams(msgMap)); + + // TODO 芋艿:OrderLog + // TODO 设计:lili:是不是发货后,才支持售后? + } + + private List express(TradeOrderDO order, TradeOrderDeliveryReqVO deliveryReqVO) { + // 校验快递公司 + DeliveryExpressDO deliveryExpress = deliveryExpressService.getDeliveryExpress(deliveryReqVO.getLogisticsId()); + if (deliveryExpress == null) { + throw exception(EXPRESS_NOT_EXISTS); + } + // 校验发货商品 + validateDeliveryOrderItem(order, deliveryReqVO); + // 创建发货记录 + return TradeOrderConvert.INSTANCE.covert(order, deliveryReqVO); + } + + private List pickUp(TradeOrderDO order, TradeOrderDeliveryReqVO deliveryReqVO) { + // TODO 校验自提门店是否存在 + // 重置一下确保快递公司和快递单号为空 + deliveryReqVO.setLogisticsId(null); + deliveryReqVO.setLogisticsNo(""); + // 校验发货商品 + validateDeliveryOrderItem(order, deliveryReqVO); + // 创建发货记录 + return TradeOrderConvert.INSTANCE.covert(order, deliveryReqVO); + } + + private void validateDeliveryOrderItem(TradeOrderDO order, TradeOrderDeliveryReqVO deliveryReqVO) { + // TODO 设计:like:是否要单独一个 delivery 发货单表??? fix: 多商品可分开单独发货,添加 trade_order_delivery 交易订单发货日志表关联发货所选订单项设置物流单号 + // TODO 设计:niu:要不要支持一个订单下,多个 order item 单独发货,类似有赞 fix + // 校验发货商品 + if (CollUtil.isEmpty(deliveryReqVO.getOrderItemIds())) { + throw exception(ORDER_DELIVERY_FAILED_ITEMS_NOT_EMPTY); + } + // 校验发货商品是否存在 + List orderItemDOs = tradeOrderItemMapper.selectListByOrderId(order.getId()); + Set itemIds = convertSet(orderItemDOs, TradeOrderItemDO::getId); + if (!itemIds.containsAll(deliveryReqVO.getOrderItemIds())) { + throw exception(ORDER_DELIVERY_FAILED_ITEM_NOT_EXISTS); + } + // 校验所选订单项是否存在有已发货的 + List deliveryDOList = orderDeliveryMapper.selsectListByOrderIdAndItemIds(order.getId(), deliveryReqVO.getOrderItemIds()); + if (CollUtil.isNotEmpty(deliveryDOList)) { + HashSet hashSet = CollUtil.newHashSet(deliveryReqVO.getOrderItemIds()); + hashSet.retainAll(convertSet(deliveryDOList, TradeOrderDeliveryDO::getOrderItemId)); + throw exception(ORDER_DELIVERY_FAILED_ITEM_ALREADY_DELIVERY); + } + } + + /** + * 校验交易订单满足被发货的条件 + * + * 1. 交易订单未发货 + * + * @param id 交易订单编号 + * @return 交易订单 + */ + private TradeOrderDO validateOrderDeliverable(Long id) { + // 校验订单是否存在 + TradeOrderDO order = tradeOrderMapper.selectById(id); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 校验订单是否是待发货状态 + if (!TradeOrderStatusEnum.isUndelivered(order.getStatus()) + || ObjectUtil.notEqual(order.getDeliveryStatus(), TradeOrderDeliveryStatusEnum.UNDELIVERED.getStatus())) { + throw exception(ORDER_DELIVERY_FAIL_STATUS_NOT_UNDELIVERED); + } + // 校验订单是否退款 + if (ObjectUtil.notEqual(TradeOrderRefundStatusEnum.NONE.getStatus(), order.getRefundStatus())) { + throw exception(ORDER_DELIVERY_FAIL_REFUND_STATUS_NOT_NONE); + } + // 订单类型:拼团 + if (ObjectUtil.equal(TradeOrderTypeEnum.COMBINATION.getType(), order.getType())) { + // 校验订单拼团是否成功 + // TODO 用户 ID 使用当前登录用户的还是订单保存的? + if (combinationApi.validateRecordStatusIsSuccess(order.getUserId(), order.getId())) { + throw exception(ORDER_DELIVERY_FAIL_COMBINATION_RECORD_STATUS_NOT_SUCCESS); + } + } + // TODO puhui999: 校验订单砍价是否成功 + return order; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void receiveOrder(Long userId, Long id) { + // 校验并获得交易订单(可收货) + TradeOrderDO order = validateOrderReceivable(userId, id); + + // 更新 TradeOrderDO 状态为已完成 + int updateCount = tradeOrderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), + new TradeOrderDO().setStatus(TradeOrderStatusEnum.COMPLETED.getStatus()) + .setDeliveryStatus(TradeOrderDeliveryStatusEnum.RECEIVED.getStatus()).setReceiveTime(LocalDateTime.now())); + if (updateCount == 0) { + throw exception(ORDER_RECEIVE_FAIL_STATUS_NOT_DELIVERED); + } + + // TODO 芋艿:OrderLog + + // TODO 芋艿:lili 发送订单变化的消息 + + // TODO 芋艿:lili 发送商品被购买完成的数据 + } + + @Override + public TradeOrderDO getOrder(Long id) { + return tradeOrderMapper.selectById(id); + } + + /** + * 校验交易订单满足可售货的条件 + * + * 1. 交易订单待收货 + * + * @param userId 用户编号 + * @param id 交易订单编号 + * @return 交易订单 + */ + private TradeOrderDO validateOrderReceivable(Long userId, Long id) { + // 校验订单是否存在 + TradeOrderDO order = tradeOrderMapper.selectByIdAndUserId(id, userId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 校验订单是否是待收货状态 + if (!TradeOrderStatusEnum.isDelivered(order.getStatus()) + || ObjectUtil.notEqual(order.getDeliveryStatus(), TradeOrderDeliveryStatusEnum.DELIVERED.getStatus())) { + throw exception(ORDER_RECEIVE_FAIL_STATUS_NOT_DELIVERED); + } + return order; + } + + @Override + public TradeOrderDO getOrder(Long userId, Long id) { + TradeOrderDO order = tradeOrderMapper.selectById(id); + if (order != null + && ObjectUtil.notEqual(order.getUserId(), userId)) { + return null; + } + return order; + } + + @Override + public PageResult getOrderPage(TradeOrderPageReqVO reqVO) { + // 获得 userId 相关的查询 + Set userIds = new HashSet<>(); + if (StrUtil.isNotEmpty(reqVO.getUserMobile())) { + MemberUserRespDTO user = memberUserApi.getUserByMobile(reqVO.getUserMobile()); + if (user == null) { // 没查询到用户,说明肯定也没他的订单 + return new PageResult<>(); + } + userIds.add(user.getId()); + } + if (StrUtil.isNotEmpty(reqVO.getUserNickname())) { + List users = memberUserApi.getUserListByNickname(reqVO.getUserNickname()); + if (CollUtil.isEmpty(users)) { // 没查询到用户,说明肯定也没他的订单 + return new PageResult<>(); + } + userIds.addAll(convertSet(users, MemberUserRespDTO::getId)); + } + // 分页查询 + return tradeOrderMapper.selectPage(reqVO, userIds); + } + + @Override + public PageResult getOrderPage(Long userId, AppTradeOrderPageReqVO reqVO) { + return tradeOrderMapper.selectPage(reqVO, userId); + } + + @Override + public Long getOrderCount(Long userId, Integer status, Boolean commentStatus) { + return tradeOrderMapper.selectCountByUserIdAndStatus(userId, status, commentStatus); + } + + // =================== Order Item =================== + + @Override + public TradeOrderItemDO getOrderItem(Long userId, Long itemId) { + TradeOrderItemDO orderItem = tradeOrderItemMapper.selectById(itemId); + if (orderItem != null + && ObjectUtil.notEqual(orderItem.getUserId(), userId)) { + return null; + } + return orderItem; + } + + @Override + public void updateOrderItemAfterSaleStatus(Long id, Integer oldAfterSaleStatus, Integer newAfterSaleStatus, Integer refundPrice) { + // 如果退款成功,则 refundPrice 非空 + if (Objects.equals(newAfterSaleStatus, TradeOrderItemAfterSaleStatusEnum.SUCCESS.getStatus()) + && refundPrice == null) { + throw new IllegalArgumentException(StrUtil.format("id({}) 退款成功,退款金额不能为空", id)); + } + + // 更新订单项 + int updateCount = tradeOrderItemMapper.updateAfterSaleStatus(id, oldAfterSaleStatus, newAfterSaleStatus); + if (updateCount <= 0) { + throw exception(ORDER_ITEM_UPDATE_AFTER_SALE_STATUS_FAIL); + } + + // 如果有退款金额,则需要更新订单 + if (refundPrice == null) { + return; + } + // 计算总的退款金额 + TradeOrderDO order = tradeOrderMapper.selectById(tradeOrderItemMapper.selectById(id).getOrderId()); + Integer orderRefundPrice = order.getRefundPrice() + refundPrice; + if (isAllOrderItemAfterSaleSuccess(order.getId())) { // 如果都售后成功,则需要取消订单 + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()) + .setRefundStatus(TradeOrderRefundStatusEnum.ALL.getStatus()).setRefundPrice(orderRefundPrice) + .setCancelType(TradeOrderCancelTypeEnum.AFTER_SALE_CLOSE.getType()).setCancelTime(LocalDateTime.now())); + + // TODO 芋艿:记录订单日志 + + // TODO 芋艿:站内信? + } else { // 如果部分售后,则更新退款金额 + tradeOrderMapper.updateById(new TradeOrderDO().setId(order.getId()) + .setRefundStatus(TradeOrderRefundStatusEnum.PART.getStatus()).setRefundPrice(orderRefundPrice)); + } + + // TODO 芋艿:未来如果有分佣,需要更新相关分佣订单为已失效 + } + + @Override + public List getOrderItemList(Collection ids) { + return tradeOrderItemMapper.selectBatchIds(ids); + } + + @Override + public List getOrderItemListByOrderId(Collection orderIds) { + if (CollUtil.isEmpty(orderIds)) { + return Collections.emptyList(); + } + return tradeOrderItemMapper.selectListByOrderId(orderIds); + } + + @Override + public TradeOrderItemDO getOrderItemByIdAndUserId(Long orderItemId, Long loginUserId) { + return tradeOrderItemMapper.selectOrderItemByIdAndUserId(orderItemId, loginUserId); + } + + @Override + public TradeOrderDO getOrderByIdAndUserId(Long orderId, Long loginUserId) { + return tradeOrderMapper.selectOrderByIdAndUserId(orderId, loginUserId); + } + + @Override + public Long createOrderItemComment(AppTradeOrderItemCommentCreateReqVO createReqVO) { + Long loginUserId = getLoginUserId(); + // 先通过订单项 ID,查询订单项是否存在 + TradeOrderItemDO orderItem = getOrderItemByIdAndUserId(createReqVO.getOrderItemId(), loginUserId); + if (orderItem == null) { + throw exception(ORDER_ITEM_NOT_FOUND); + } + // 校验订单 + TradeOrderDO order = getOrderByIdAndUserId(orderItem.getOrderId(), loginUserId); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + if (ObjectUtil.notEqual(order.getStatus(), TradeOrderStatusEnum.COMPLETED.getStatus())) { + throw exception(ORDER_COMMENT_FAIL_STATUS_NOT_COMPLETED); + } + if (ObjectUtil.notEqual(order.getCommentStatus(), Boolean.FALSE)) { + throw exception(ORDER_COMMENT_STATUS_NOT_FALSE); + } + // TODO @puhui999:是不是评论完,要更新 status、commentStatus;另外,是不是上面 order 可以不校验,直接只判断 orderItem 就够; + // 对于 order 来说,就是评论完,把 order 更新完合理的 status 等字段。 + + ProductCommentCreateReqDTO productCommentCreateReqDTO = TradeOrderConvert.INSTANCE.convert04(createReqVO, orderItem); + return productCommentApi.createComment(productCommentCreateReqDTO); + } + + /** + * 判断指定订单的所有订单项,是不是都售后成功 + * + * @param id 订单编号 + * @return 是否都售后成功 + */ + private boolean isAllOrderItemAfterSaleSuccess(Long id) { + List orderItems = tradeOrderItemMapper.selectListByOrderId(id); + return orderItems.stream().allMatch(orderItem -> Objects.equals(orderItem.getAfterSaleStatus(), + TradeOrderItemAfterSaleStatusEnum.SUCCESS.getStatus())); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/TradePriceService.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/TradePriceService.java new file mode 100644 index 0000000..ea6eecd --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/TradePriceService.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.trade.service.price; + +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; + +import javax.validation.Valid; + +/** + * 价格计算 Service 接口 + * + * @author 芋道源码 + */ +public interface TradePriceService { + + /** + * 价格计算 + * + * @param calculateReqDTO 计算信息 + * @return 计算结果 + */ + TradePriceCalculateRespBO calculatePrice(@Valid TradePriceCalculateReqBO calculateReqDTO); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/TradePriceServiceImpl.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/TradePriceServiceImpl.java new file mode 100644 index 0000000..7eac96a --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/TradePriceServiceImpl.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.module.trade.service.price; + +import com.yunxi.scm.module.product.api.sku.ProductSkuApi; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.spu.ProductSpuApi; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import com.yunxi.scm.module.trade.service.price.calculator.TradePriceCalculator; +import com.yunxi.scm.module.trade.service.price.calculator.TradePriceCalculatorHelper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.product.enums.ErrorCodeConstants.*; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_PAY_PRICE_ILLEGAL; + +/** + * 价格计算 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class TradePriceServiceImpl implements TradePriceService { + + @Resource + private ProductSkuApi productSkuApi; + @Resource + private ProductSpuApi productSpuApi; + + @Resource + private List priceCalculators; + + @Override + public TradePriceCalculateRespBO calculatePrice(TradePriceCalculateReqBO calculateReqBO) { + // 1.1 获得商品 SKU 数组 + List skuList = checkSkuList(calculateReqBO); + // 1.2 获得商品 SPU 数组 + List spuList = checkSpuList(skuList); + + // 2.1 计算价格 + TradePriceCalculateRespBO calculateRespBO = TradePriceCalculatorHelper + .buildCalculateResp(calculateReqBO, spuList, skuList); + priceCalculators.forEach(calculator -> calculator.calculate(calculateReqBO, calculateRespBO)); + // 2.2 如果最终支付金额小于等于 0,则抛出业务异常 + if (calculateRespBO.getPrice().getPayPrice() <= 0) { + log.error("[calculatePrice][价格计算不正确,请求 calculateReqDTO({}),结果 priceCalculate({})]", + calculateReqBO, calculateRespBO); + throw exception(PRICE_CALCULATE_PAY_PRICE_ILLEGAL); + } + return calculateRespBO; + } + + private List checkSkuList(TradePriceCalculateReqBO reqBO) { + // 获得商品 SKU 数组 + Map skuIdCountMap = convertMap(reqBO.getItems(), + TradePriceCalculateReqBO.Item::getSkuId, TradePriceCalculateReqBO.Item::getCount); + List skus = productSkuApi.getSkuList(skuIdCountMap.keySet()); + + // 校验商品 SKU + skus.forEach(sku -> { + Integer count = skuIdCountMap.get(sku.getId()); + if (count == null) { + throw exception(SKU_NOT_EXISTS); + } + if (count > sku.getStock()) { + throw exception(SKU_STOCK_NOT_ENOUGH); + } + }); + return skus; + } + + private List checkSpuList(List skuList) { + // 获得商品 SPU 数组 + List spus = productSpuApi.getSpuList(convertSet(skuList, ProductSkuRespDTO::getSpuId)); + + // 校验商品 SPU + spus.forEach(spu -> { + if (spu == null) { + throw exception(SPU_NOT_EXISTS); + } + if (!ProductSpuStatusEnum.isEnable(spu.getStatus())) { + throw exception(SPU_NOT_ENABLE); + } + }); + return spus; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/bo/TradePriceCalculateReqBO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/bo/TradePriceCalculateReqBO.java new file mode 100644 index 0000000..b833905 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/bo/TradePriceCalculateReqBO.java @@ -0,0 +1,93 @@ +package com.yunxi.scm.module.trade.service.price.bo; + +import com.yunxi.scm.module.trade.enums.delivery.DeliveryTypeEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderTypeEnum; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 价格计算 Request BO + * + * @author yunxi源码 + */ +@Data +public class TradePriceCalculateReqBO { + + /** + * 订单类型 + * + * 枚举 {@link TradeOrderTypeEnum} + */ + private Integer type; + + /** + * 用户编号 + * + * 对应 MemberUserDO 的 id 编号 + */ + private Long userId; + + /** + * 优惠劵编号 + * + * 对应 CouponDO 的 id 编号 + */ + private Long couponId; + + /** + * 收货地址编号 + * + * 对应 MemberAddressDO 的 id 编号 + */ + private Long addressId; + + /** + * 配送方式 + * + * 枚举 {@link DeliveryTypeEnum} + */ + private Integer deliveryType; + + /** + * 商品 SKU 数组 + */ + @NotNull(message = "商品数组不能为空") + private List items; + + /** + * 商品 SKU + */ + @Data + @Valid + public static class Item { + + /** + * SKU 编号 + */ + @NotNull(message = "商品 SKU 编号不能为空") + private Long skuId; + + /** + * SKU 数量 + */ + @NotNull(message = "商品 SKU 数量不能为空") + @Min(value = 0L, message = "商品 SKU 数量必须大于等于 0") + private Integer count; + + /** + * 购物车项的编号 + */ + private Long cartId; + + /** + * 是否选中 + */ + @NotNull(message = "是否选中不能为空") + private Boolean selected; + + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/bo/TradePriceCalculateRespBO.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/bo/TradePriceCalculateRespBO.java new file mode 100644 index 0000000..f443a5f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/bo/TradePriceCalculateRespBO.java @@ -0,0 +1,277 @@ +package com.yunxi.scm.module.trade.service.price.bo; + +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.promotion.enums.common.PromotionTypeEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderTypeEnum; +import lombok.Data; + +import java.util.List; + +/** + * 价格计算 Response BO + * + * 整体设计,参考 taobao 的技术文档: + * 1. 订单管理 + * 2. 常用订单金额说明 + * + * @author 芋道源码 + */ +@Data +public class TradePriceCalculateRespBO { + + /** + * 订单类型 + * + * 枚举 {@link TradeOrderTypeEnum} + */ + private Integer type; + + /** + * 订单价格 + */ + private Price price; + + /** + * 订单项数组 + */ + private List items; + + /** + * 营销活动数组 + * + * 只对应 {@link Price#items} 商品匹配的活动 + */ + private List promotions; + + /** + * 优惠劵编号 + */ + private Long couponId; + + /** + * 订单价格 + */ + @Data + public static class Price { + + /** + * 商品原价(总),单位:分 + * + * 基于 {@link OrderItem#getPrice()} * {@link OrderItem#getCount()} 求和 + * + * 对应 taobao 的 trade.total_fee 字段 + */ + private Integer totalPrice; + /** + * 订单优惠(总),单位:分 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 运费金额,单位:分 + */ + private Integer deliveryPrice; + /** + * 优惠劵减免金额(总),单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + /** + * 积分抵扣的金额,单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + /** + * 最终购买金额(总),单位:分 + * + * = {@link #totalPrice} + * - {@link #couponPrice} + * - {@link #pointPrice} + * - {@link #discountPrice} + * + {@link #deliveryPrice} + */ + private Integer payPrice; + + } + + /** + * 订单商品 SKU + */ + @Data + public static class OrderItem { + + /** + * SPU 编号 + */ + private Long spuId; + /** + * SKU 编号 + */ + private Long skuId; + /** + * 购买数量 + */ + private Integer count; + /** + * 购物车项的编号 + */ + private Long cartId; + /** + * 是否选中 + */ + private Boolean selected; + + /** + * 商品原价(单),单位:分 + * + * 对应 ProductSkuDO 的 price 字段 + * 对应 taobao 的 order.price 字段 + */ + private Integer price; + /** + * 优惠金额(总),单位:分 + * + * 对应 taobao 的 order.discount_fee 字段 + */ + private Integer discountPrice; + /** + * 运费金额(总),单位:分 + */ + private Integer deliveryPrice; + /** + * 优惠劵减免金额,单位:分 + * + * 对应 taobao 的 trade.coupon_fee 字段 + */ + private Integer couponPrice; + /** + * 积分抵扣的金额,单位:分 + * + * 对应 taobao 的 trade.point_fee 字段 + */ + private Integer pointPrice; + /** + * 应付金额(总),单位:分 + * + * = {@link #price} * {@link #count} + * - {@link #couponPrice} + * - {@link #pointPrice} + * - {@link #discountPrice} + * + {@link #deliveryPrice} + */ + private Integer payPrice; + + // ========== 商品 SPU 信息 ========== + /** + * 商品名 + */ + private String spuName; + /** + * 商品图片 + * + * 优先级:SKU.picUrl > SPU.picUrl + */ + private String picUrl; + /** + * 分类编号 + */ + private Long categoryId; + + /** + * 运费模板 Id + */ + private Long deliveryTemplateId; + + // ========== 商品 SKU 信息 ========== + /** + * 商品重量,单位:kg 千克 + */ + private Double weight; + /** + * 商品体积,单位:m^3 平米 + */ + private Double volume; + + /** + * 商品属性数组 + */ + private List properties; + + } + + /** + * 营销明细 + */ + @Data + public static class Promotion { + + /** + * 营销编号 + * + * 例如说:营销活动的编号、优惠劵的编号 + */ + private Long id; + /** + * 营销名字 + */ + private String name; + /** + * 营销类型 + * + * 枚举 {@link PromotionTypeEnum} + */ + private Integer type; + /** + * 计算时的原价(总),单位:分 + */ + private Integer totalPrice; + /** + * 计算时的优惠(总),单位:分 + */ + private Integer discountPrice; + /** + * 匹配的商品 SKU 数组 + */ + private List items; + + // ========== 匹配情况 ========== + + /** + * 是否满足优惠条件 + */ + private Boolean match; + /** + * 满足条件的提示 + * + * 如果 {@link #match} = true 满足,则提示“圣诞价:省 150.00 元” + * 如果 {@link #match} = false 不满足,则提示“购满 85 元,可减 40 元” + */ + private String description; + + } + + /** + * 营销匹配的商品 SKU + */ + @Data + public static class PromotionItem { + + /** + * 商品 SKU 编号 + */ + private Long skuId; + /** + * 计算时的原价(总),单位:分 + */ + private Integer totalPrice; + /** + * 计算时的优惠(总),单位:分 + */ + private Integer discountPrice; + + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeCouponPriceCalculator.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeCouponPriceCalculator.java new file mode 100644 index 0000000..bfea7f9 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeCouponPriceCalculator.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.promotion.api.coupon.CouponApi; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponRespDTO; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponValidReqDTO; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionTypeEnum; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Predicate; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.filterList; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_MIN_PRICE; +import static com.yunxi.scm.module.promotion.enums.ErrorCodeConstants.COUPON_NO_MATCH_SPU; + +/** + * 优惠劵的 {@link TradePriceCalculator} 实现类 + * + * @author 芋道源码 + */ +@Component +@Order(TradePriceCalculator.ORDER_COUPON) +public class TradeCouponPriceCalculator implements TradePriceCalculator { + + @Resource + private CouponApi couponApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 1.1 校验优惠劵 + if (param.getCouponId() == null) { + return; + } + CouponRespDTO coupon = couponApi.validateCoupon(new CouponValidReqDTO() + .setId(param.getCouponId()).setUserId(param.getUserId())); + Assert.notNull(coupon, "校验通过的优惠劵({}),不能为空", param.getCouponId()); + + // 2.1 获得匹配的商品 SKU 数组 + List orderItems = filterMatchCouponOrderItems(result, coupon); + if (CollUtil.isEmpty(orderItems)) { + throw exception(COUPON_NO_MATCH_SPU); + } + // 2.2 计算是否满足优惠劵的使用金额 + Integer totalPayPrice = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + if (totalPayPrice < coupon.getUsePrice()) { + throw exception(COUPON_NO_MATCH_MIN_PRICE); + } + + // 3.1 计算可以优惠的金额 + Integer couponPrice = getCouponPrice(coupon, totalPayPrice); + Assert.isTrue(couponPrice < totalPayPrice, + "优惠劵({}) 的优惠金额({}),不能大于订单总金额({})", coupon.getId(), couponPrice, totalPayPrice); + // 3.2 计算分摊的优惠金额 + List divideCouponPrices = TradePriceCalculatorHelper.dividePrice(orderItems, couponPrice); + + // 4.1 记录使用的优惠劵 + result.setCouponId(param.getCouponId()); + // 4.2 记录优惠明细 + TradePriceCalculatorHelper.addPromotion(result, orderItems, + param.getCouponId(), coupon.getName(), PromotionTypeEnum.COUPON.getType(), + StrUtil.format("优惠劵:省 {} 元", TradePriceCalculatorHelper.formatPrice(couponPrice)), + divideCouponPrices); + // 4.3 更新 SKU 优惠金额 + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + orderItem.setCouponPrice(divideCouponPrices.get(i)); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + } + TradePriceCalculatorHelper.recountAllPrice(result); + } + + private Integer getCouponPrice(CouponRespDTO coupon, Integer totalPayPrice) { + if (PromotionDiscountTypeEnum.PRICE.getType().equals(coupon.getDiscountType())) { // 减价 + return coupon.getDiscountPrice(); + } else if (PromotionDiscountTypeEnum.PERCENT.getType().equals(coupon.getDiscountType())) { // 打折 + int couponPrice = totalPayPrice * coupon.getDiscountPercent() / 100; + return coupon.getDiscountLimitPrice() == null ? couponPrice + : Math.min(couponPrice, coupon.getDiscountLimitPrice()); // 优惠上限 + } + throw new IllegalArgumentException(String.format("优惠劵(%s) 的优惠类型不正确", coupon)); + } + + /** + * 获得优惠劵可使用的订单项(商品)列表 + * + * @param result 计算结果 + * @param coupon 优惠劵 + * @return 订单项(商品)列表 + */ + private List filterMatchCouponOrderItems(TradePriceCalculateRespBO result, + CouponRespDTO coupon) { + Predicate matchPredicate = TradePriceCalculateRespBO.OrderItem::getSelected; + if (PromotionProductScopeEnum.SPU.getScope().equals(coupon.getProductScope())) { + matchPredicate = matchPredicate // 额外加如下条件 + .and(orderItem -> coupon.getProductSpuIds().contains(orderItem.getSpuId())); + } + return filterList(result.getItems(), matchPredicate); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java new file mode 100644 index 0000000..87c46e7 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDeliveryPriceCalculator.java @@ -0,0 +1,223 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.module.member.api.address.AddressApi; +import com.yunxi.scm.module.member.api.address.dto.AddressRespDTO; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryTypeEnum; +import com.yunxi.scm.module.trade.service.delivery.DeliveryExpressTemplateService; +import com.yunxi.scm.module.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO.OrderItem; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.*; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_DELIVERY_PRICE_USER_ADDR_IS_EMPTY; +import static com.yunxi.scm.module.trade.enums.ErrorCodeConstants.PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND; + +/** + * 运费的 {@link TradePriceCalculator} 实现类 + * + * @author jason + */ +@Component +@Order(TradePriceCalculator.ORDER_DELIVERY) +@Slf4j +public class TradeDeliveryPriceCalculator implements TradePriceCalculator { + + @Resource + private AddressApi addressApi; + @Resource + private DeliveryExpressTemplateService deliveryExpressTemplateService; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 1.1 判断配送方式 + if (param.getDeliveryType() == null || DeliveryTypeEnum.PICK_UP.getMode().equals(param.getDeliveryType())) { + return; + } + if (param.getAddressId() == null) { + throw exception(PRICE_CALCULATE_DELIVERY_PRICE_USER_ADDR_IS_EMPTY); + } + // 1.2 得到收件地址区域 + AddressRespDTO address = addressApi.getAddress(param.getAddressId(), param.getUserId()); + Assert.notNull(address, "收件人({})的地址,不能为空", param.getUserId()); + + // 2. 过滤出已选中的商品SKU + List selectedItem = filterList(result.getItems(), OrderItem::getSelected); + Set deliveryTemplateIds = convertSet(selectedItem, OrderItem::getDeliveryTemplateId); + Map expressTemplateMap = + deliveryExpressTemplateService.getExpressTemplateMapByIdsAndArea(deliveryTemplateIds, address.getAreaId()); + // 3. 计算配送费用 + if (CollUtil.isEmpty(expressTemplateMap)) { + log.error("[calculate][找不到商品 templateIds {} areaId{} 对应的运费模板]", deliveryTemplateIds, address.getAreaId()); + throw exception(PRICE_CALCULATE_DELIVERY_PRICE_TEMPLATE_NOT_FOUND); + } + calculateDeliveryPrice(selectedItem, expressTemplateMap, result); + } + + private void calculateDeliveryPrice(List selectedSkus, + Map expressTemplateMap, + TradePriceCalculateRespBO result) { + // 按商品运费模板来计算商品的运费:相同的运费模板可能对应多条订单商品 SKU + Map> tplIdItemMap = convertMultiMap(selectedSkus, OrderItem::getDeliveryTemplateId); + // 依次计算快递运费 + for (Map.Entry> entry : tplIdItemMap.entrySet()) { + Long templateId = entry.getKey(); + List orderItems = entry.getValue(); + DeliveryExpressTemplateRespBO templateBO = expressTemplateMap.get(templateId); + if (templateBO == null) { + log.error("[calculateDeliveryPrice][不能计算快递运费,找不到 templateId({}) 对应的运费模板配置]", templateId); + continue; + } + // 总件数, 总金额, 总重量, 总体积 + int totalCount = 0; + int totalPrice = 0; + double totalWeight = 0; + double totalVolume = 0; + for (OrderItem orderItem : orderItems) { + totalCount += orderItem.getCount(); + totalPrice += orderItem.getPayPrice(); + totalWeight += totalWeight + orderItem.getWeight() * orderItem.getCount(); + totalVolume += totalVolume + orderItem.getVolume() * orderItem.getCount(); + } + // 优先判断是否包邮. 如果包邮不计算快递运费 + if (isExpressFree(templateBO.getChargeMode(), totalCount, totalWeight, + totalVolume, totalPrice, templateBO.getFree())) { + continue; + } + // 计算快递运费 + calculateExpressFeeByChargeMode(totalCount, totalWeight, totalVolume, + templateBO.getChargeMode(), templateBO.getCharge(), orderItems); + + } + TradePriceCalculatorHelper.recountAllPrice(result); + } + + /** + * 按配送方式来计算运费 + * + * @param totalCount 总件数 + * @param totalWeight 总重量 + * @param totalVolume 总体积 + * @param chargeMode 配送计费方式 + * @param templateCharge 快递运费配置 + * @param orderItems SKU 商品项目 + */ + private void calculateExpressFeeByChargeMode(double totalCount, double totalWeight, double totalVolume, + int chargeMode, DeliveryExpressTemplateRespBO.Charge templateCharge, + List orderItems) { + if (templateCharge == null) { + log.error("[calculateExpressFeeByChargeMode][计算快递运费时,找不到 SKU({}) 对应的运费模版]", orderItems); + return; + } + DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); + switch (chargeModeEnum) { + case PIECE: { + calculateExpressFee(totalCount, templateCharge, orderItems); + break; + } + case WEIGHT: { + calculateExpressFee(totalWeight, templateCharge, orderItems); + break; + } + case VOLUME: { + calculateExpressFee(totalVolume, templateCharge, orderItems); + break; + } + } + } + + /** + * 计算 SKU 商品快递费用 + * + * @param total 总件数/总重量/总体积 + * @param templateCharge 快递运费配置 + * @param orderItems SKU 商品项目 + */ + private void calculateExpressFee(double total, DeliveryExpressTemplateRespBO.Charge templateCharge, List orderItems) { + int deliveryPrice; + if (total <= templateCharge.getStartCount()) { + deliveryPrice = templateCharge.getStartPrice(); + } else { + double remainWeight = total - templateCharge.getStartCount(); + // 剩余重量/ 续件 = 续件的次数. 向上取整 + int extraNum = (int) Math.ceil(remainWeight / templateCharge.getExtraCount()); + int extraPrice = templateCharge.getExtraPrice() * extraNum; + deliveryPrice = templateCharge.getStartPrice() + extraPrice; + } + // 分摊快递费用到 SKU. 退费的时候,可能按照 SKU 考虑退费金额 + divideDeliveryPrice(deliveryPrice, orderItems); + } + + /** + * 快递运费分摊到每个 SKU 商品上 + * + * @param deliveryPrice 快递运费 + * @param orderItems SKU 商品 + */ + private void divideDeliveryPrice(int deliveryPrice, List orderItems) { + // TODO @jason:分摊的话,是不是要按照比例呀?重量、价格、数量等等, + // 按比例是不是有点复杂。后面看看是否需要; + // TODO 可以看看别的项目怎么搞的哈。 + int dividePrice = deliveryPrice / orderItems.size(); + for (OrderItem item : orderItems) { + // 更新快递运费 + item.setDeliveryPrice(dividePrice); + TradePriceCalculatorHelper.recountPayPrice(item); + } + } + + /** + * 检查是否包邮 + * + * @param chargeMode 配送计费方式 + * @param totalCount 总件数 + * @param totalWeight 总重量 + * @param totalVolume 总体积 + * @param totalPrice 总金额 + * @param templateFree 包邮配置 + */ + private boolean isExpressFree(Integer chargeMode, int totalCount, double totalWeight, + double totalVolume, int totalPrice, DeliveryExpressTemplateRespBO.Free templateFree) { + if (templateFree == null) { + return false; + } + DeliveryExpressChargeModeEnum chargeModeEnum = DeliveryExpressChargeModeEnum.valueOf(chargeMode); + switch (chargeModeEnum) { + case PIECE: + // 两个条件都满足才包邮 + if (totalCount >= templateFree.getFreeCount() && totalPrice >= templateFree.getFreePrice()) { + return true; + } + break; + case WEIGHT: + // freeCount 是不是应该是 double ?? + // TODO @jason:要不配置的时候,把它的单位和商品对齐?到底是 kg、还是斤 + // TODO @芋艿 目前 包邮 件数/重量/体积 都用的是这个字段 + // TODO @jason:那要不快递模版也改成 kg?这样是不是就不用 double ? + if (totalWeight >= templateFree.getFreeCount() + && totalPrice >= templateFree.getFreePrice()) { + return true; + } + break; + case VOLUME: + if (totalVolume >= templateFree.getFreeCount() + && totalPrice >= templateFree.getFreePrice()) { + return true; + } + break; + } + return false; + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java new file mode 100644 index 0000000..86da8ca --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculator.java @@ -0,0 +1,83 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.promotion.api.discount.DiscountActivityApi; +import com.yunxi.scm.module.promotion.api.discount.dto.DiscountProductRespDTO; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionTypeEnum; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; + +/** + * 限时折扣的 {@link TradePriceCalculator} 实现类 + * + * @author 芋道源码 + */ +@Component +@Order(TradePriceCalculator.ORDER_DISCOUNT_ACTIVITY) +public class TradeDiscountActivityPriceCalculator implements TradePriceCalculator { + + @Resource + private DiscountActivityApi discountActivityApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 获得 SKU 对应的限时折扣活动 + List discountProducts = discountActivityApi.getMatchDiscountProductList( + convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSkuId)); + if (CollUtil.isEmpty(discountProducts)) { + return; + } + Map discountProductMap = convertMap(discountProducts, DiscountProductRespDTO::getSkuId); + + // 处理每个 SKU 的限时折扣 + result.getItems().forEach(orderItem -> { + // 1. 获取该 SKU 的优惠信息 + DiscountProductRespDTO discountProduct = discountProductMap.get(orderItem.getSkuId()); + if (discountProduct == null) { + return; + } + // 2. 计算优惠金额 + Integer newPayPrice = calculatePayPrice(discountProduct, orderItem); + Integer newDiscountPrice = orderItem.getPayPrice() - newPayPrice; + + // 3.1 记录优惠明细 + if (orderItem.getSelected()) { + // 注意,只有在选中的情况下,才会记录到优惠明细。否则仅仅是更新 SKU 优惠金额,用于展示 + TradePriceCalculatorHelper.addPromotion(result, orderItem, + discountProduct.getActivityId(), discountProduct.getActivityName(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType(), + StrUtil.format("限时折扣:省 {} 元", formatPrice(newDiscountPrice)), + newDiscountPrice); + } + // 3.2 更新 SKU 优惠金额 + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + newDiscountPrice); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + }); + TradePriceCalculatorHelper.recountAllPrice(result); + } + + private Integer calculatePayPrice(DiscountProductRespDTO discountProduct, + TradePriceCalculateRespBO.OrderItem orderItem) { + Integer price = orderItem.getPayPrice(); + if (PromotionDiscountTypeEnum.PRICE.getType().equals(discountProduct.getDiscountType())) { // 减价 + price -= discountProduct.getDiscountPrice() * orderItem.getCount(); + } else if (PromotionDiscountTypeEnum.PERCENT.getType().equals(discountProduct.getDiscountType())) { // 打折 + price = price * discountProduct.getDiscountPercent() / 100; + } else { + throw new IllegalArgumentException(String.format("优惠活动的商品(%s) 的优惠类型不正确", discountProduct)); + } + return price; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradePriceCalculator.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradePriceCalculator.java new file mode 100644 index 0000000..108d510 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradePriceCalculator.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; + +/** + * 价格计算的计算器接口 + * + * @author 芋道源码 + */ +public interface TradePriceCalculator { + + int ORDER_DISCOUNT_ACTIVITY = 10; + int ORDER_REWARD_ACTIVITY = 20; + int ORDER_COUPON = 30; + /** + * 快递运费的计算 + * + * 放在各种营销活动、优惠劵后面 TODO + */ + int ORDER_DELIVERY = 40; + + void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result); + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradePriceCalculatorHelper.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradePriceCalculatorHelper.java new file mode 100644 index 0000000..3ba25f5 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradePriceCalculatorHelper.java @@ -0,0 +1,266 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.getSumValue; +import static java.util.Collections.singletonList; + +/** + * {@link TradePriceCalculator} 的工具类 + * + * 主要实现对 {@link TradePriceCalculateRespBO} 计算结果的操作 + * + * @author 芋道源码 + */ +public class TradePriceCalculatorHelper { + + public static TradePriceCalculateRespBO buildCalculateResp(TradePriceCalculateReqBO param, + List spuList, List skuList) { + // 创建 PriceCalculateRespDTO 对象 + TradePriceCalculateRespBO result = new TradePriceCalculateRespBO(); + result.setType(param.getType()); + result.setPromotions(new ArrayList<>()); + + // 创建它的 OrderItem 属性 + result.setItems(new ArrayList<>(param.getItems().size())); + Map spuMap = convertMap(spuList, ProductSpuRespDTO::getId); + Map skuMap = convertMap(skuList, ProductSkuRespDTO::getId); + param.getItems().forEach(item -> { + ProductSkuRespDTO sku = skuMap.get(item.getSkuId()); + if (sku == null) { + return; + } + ProductSpuRespDTO spu = spuMap.get(sku.getSpuId()); + if (spu == null) { + return; + } + // 商品项 + TradePriceCalculateRespBO.OrderItem orderItem = new TradePriceCalculateRespBO.OrderItem(); + result.getItems().add(orderItem); + orderItem.setSpuId(sku.getSpuId()).setSkuId(sku.getId()) + .setCount(item.getCount()).setCartId(item.getCartId()).setSelected(item.getSelected()); + // sku 价格 + orderItem.setPrice(sku.getPrice()).setPayPrice(sku.getPrice() * item.getCount()) + .setDiscountPrice(0).setDeliveryPrice(0).setCouponPrice(0).setPointPrice(0); + // sku 信息 + orderItem.setPicUrl(sku.getPicUrl()).setProperties(sku.getProperties()) + .setWeight(sku.getWeight()).setVolume(sku.getVolume()); + // spu 信息 + orderItem.setSpuName(spu.getName()).setCategoryId(spu.getCategoryId()) + .setDeliveryTemplateId(spu.getDeliveryTemplateId()); + if (orderItem.getPicUrl() == null) { + orderItem.setPicUrl(spu.getPicUrl()); + } + }); + + // 创建它的 Price 属性 + result.setPrice(new TradePriceCalculateRespBO.Price()); + recountAllPrice(result); + return result; + } + + /** + * 基于订单项,重新计算 price 总价 + * + * @param result 计算结果 + */ + public static void recountAllPrice(TradePriceCalculateRespBO result) { + // 先重置 + TradePriceCalculateRespBO.Price price = result.getPrice(); + price.setTotalPrice(0).setDiscountPrice(0).setDeliveryPrice(0) + .setCouponPrice(0).setPointPrice(0).setPayPrice(0); + // 再合计 item + result.getItems().forEach(item -> { + if (!item.getSelected()) { + return; + } + price.setTotalPrice(price.getTotalPrice() + item.getPrice() * item.getCount()); + price.setDiscountPrice(price.getDiscountPrice() + item.getDiscountPrice()); + price.setDeliveryPrice(price.getDeliveryPrice() + item.getDeliveryPrice()); + price.setCouponPrice(price.getCouponPrice() + item.getCouponPrice()); + price.setPointPrice(price.getPointPrice() + item.getPointPrice()); + price.setPayPrice(price.getPayPrice() + item.getPayPrice()); + }); + } + + /** + * 重新计算单个订单项的支付金额 + * + * @param orderItem 订单项 + */ + public static void recountPayPrice(TradePriceCalculateRespBO.OrderItem orderItem) { + orderItem.setPayPrice(orderItem.getPrice()* orderItem.getCount() + - orderItem.getDiscountPrice() + + orderItem.getDeliveryPrice() + - orderItem.getCouponPrice() + - orderItem.getPointPrice()); + } + + /** + * 重新计算每个订单项的支付金额 + * + * 【目前主要是单测使用】 + * + * @param orderItems 订单项数组 + */ + public static void recountPayPrice(List orderItems) { + orderItems.forEach(orderItem -> { + if (orderItem.getDiscountPrice() == null) { + orderItem.setDiscountPrice(0); + } + if (orderItem.getDeliveryPrice() == null) { + orderItem.setDeliveryPrice(0); + } + if (orderItem.getCouponPrice() == null) { + orderItem.setCouponPrice(0); + } + if (orderItem.getPointPrice() == null) { + orderItem.setPointPrice(0); + } + recountPayPrice(orderItem); + }); + } + + /** + * 计算已选中的订单项,总支付金额 + * + * @param orderItems 订单项数组 + * @return 总支付金额 + */ + public static Integer calculateTotalPayPrice(List orderItems) { + return getSumValue(orderItems, + orderItem -> orderItem.getSelected() ? orderItem.getPayPrice() : 0, // 未选中的情况下,不计算支付金额 + Integer::sum); + } + + /** + * 计算已选中的订单项,总商品数 + * + * @param orderItems 订单项数组 + * @return 总商品数 + */ + public static Integer calculateTotalCount(List orderItems) { + return getSumValue(orderItems, + orderItem -> orderItem.getSelected() ? orderItem.getCount() : 0, // 未选中的情况下,不计算数量 + Integer::sum); + } + + /** + * 按照支付金额,返回每个订单项的分摊金额数组 + * + * @param orderItems 订单项数组 + * @param price 金额 + * @return 分摊金额数组,和传入的 orderItems 一一对应 + */ + public static List dividePrice(List orderItems, Integer price) { + Integer total = calculateTotalPayPrice(orderItems); + assert total != null; + // 遍历每一个,进行分摊 + List prices = new ArrayList<>(orderItems.size()); + int remainPrice = price; + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + // 1. 如果是未选中,则分摊为 0 + if (!orderItem.getSelected()) { + prices.add(0); + continue; + } + // 2. 如果选中,则按照百分比,进行分摊 + int partPrice; + if (i < orderItems.size() - 1) { // 减一的原因,是因为拆分时,如果按照比例,可能会出现.所以最后一个,使用反减 + partPrice = (int) (price * (1.0D * orderItem.getPayPrice() / total)); + remainPrice -= partPrice; + } else { + partPrice = remainPrice; + } + Assert.isTrue(partPrice >= 0, "分摊金额必须大于等于 0"); + prices.add(partPrice); + } + return prices; + } + + /** + * 添加【匹配】单个 OrderItem 的营销明细 + * + * @param result 价格计算结果 + * @param orderItem 单个订单商品 SKU + * @param id 营销编号 + * @param name 营销名字 + * @param description 满足条件的提示 + * @param type 营销类型 + * @param discountPrice 单个订单商品 SKU 的优惠价格(总) + */ + public static void addPromotion(TradePriceCalculateRespBO result, TradePriceCalculateRespBO.OrderItem orderItem, + Long id, String name, Integer type, String description, Integer discountPrice) { + addPromotion(result, singletonList(orderItem), id, name, type, description, singletonList(discountPrice)); + } + + /** + * 添加【匹配】多个 OrderItem 的营销明细 + * + * @param result 价格计算结果 + * @param orderItems 多个订单商品 SKU + * @param id 营销编号 + * @param name 营销名字 + * @param description 满足条件的提示 + * @param type 营销类型 + * @param discountPrices 多个订单商品 SKU 的优惠价格(总),和 orderItems 一一对应 + */ + public static void addPromotion(TradePriceCalculateRespBO result, List orderItems, + Long id, String name, Integer type, String description, List discountPrices) { + // 创建营销明细 Item + List promotionItems = new ArrayList<>(discountPrices.size()); + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + promotionItems.add(new TradePriceCalculateRespBO.PromotionItem().setSkuId(orderItem.getSkuId()) + .setTotalPrice(orderItem.getPayPrice()).setDiscountPrice(discountPrices.get(i))); + } + // 创建营销明细 + TradePriceCalculateRespBO.Promotion promotion = new TradePriceCalculateRespBO.Promotion() + .setId(id).setName(name).setType(type) + .setTotalPrice(calculateTotalPayPrice(orderItems)) + .setDiscountPrice(getSumValue(discountPrices, value -> value, Integer::sum)) + .setItems(promotionItems).setMatch(true).setDescription(description); + result.getPromotions().add(promotion); + } + + /** + * 添加【不匹配】多个 OrderItem 的营销明细 + * + * @param result 价格计算结果 + * @param orderItems 多个订单商品 SKU + * @param id 营销编号 + * @param name 营销名字 + * @param description 满足条件的提示 + * @param type 营销类型 + */ + public static void addNotMatchPromotion(TradePriceCalculateRespBO result, List orderItems, + Long id, String name, Integer type, String description) { + // 创建营销明细 Item + List promotionItems = CollectionUtils.convertList(orderItems, + orderItem -> new TradePriceCalculateRespBO.PromotionItem().setSkuId(orderItem.getSkuId()) + .setTotalPrice(orderItem.getPayPrice()).setDiscountPrice(0)); + // 创建营销明细 + TradePriceCalculateRespBO.Promotion promotion = new TradePriceCalculateRespBO.Promotion() + .setId(id).setName(name).setType(type) + .setTotalPrice(calculateTotalPayPrice(orderItems)) + .setDiscountPrice(0) + .setItems(promotionItems).setMatch(false).setDescription(description); + result.getPromotions().add(promotion); + } + + public static String formatPrice(Integer price) { + return String.format("%.2f", price / 100d); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java new file mode 100644 index 0000000..5c31d32 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/main/java/com/yunxi/scm/module/trade/service/price/calculator/TradeRewardActivityPriceCalculator.java @@ -0,0 +1,136 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.promotion.api.reward.RewardActivityApi; +import com.yunxi.scm.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; +import com.yunxi.scm.module.promotion.enums.common.PromotionConditionTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionTypeEnum; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.filterList; +import static com.yunxi.scm.module.trade.service.price.calculator.TradePriceCalculatorHelper.formatPrice; + +/** + * 满减送活动的 {@link TradePriceCalculator} 实现类 + * + * @author 芋道源码 + */ +@Component +@Order(TradePriceCalculator.ORDER_REWARD_ACTIVITY) +public class TradeRewardActivityPriceCalculator implements TradePriceCalculator { + + @Resource + private RewardActivityApi rewardActivityApi; + + @Override + public void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result) { + // 获得 SKU 对应的满减送活动 + List rewardActivities = rewardActivityApi.getMatchRewardActivityList( + convertSet(result.getItems(), TradePriceCalculateRespBO.OrderItem::getSpuId)); + if (CollUtil.isEmpty(rewardActivities)) { + return; + } + + // 处理每个满减送活动 + rewardActivities.forEach(rewardActivity -> calculate(param, result, rewardActivity)); + } + + private void calculate(TradePriceCalculateReqBO param, TradePriceCalculateRespBO result, + RewardActivityMatchRespDTO rewardActivity) { + // 1.1 获得满减送的订单项(商品)列表 + List orderItems = filterMatchCouponOrderItems(result, rewardActivity); + if (CollUtil.isEmpty(orderItems)) { + return; + } + // 1.2 获得最大匹配的满减送活动的规则 + RewardActivityMatchRespDTO.Rule rule = getMaxMatchRewardActivityRule(rewardActivity, orderItems); + if (rule == null) { + TradePriceCalculatorHelper.addNotMatchPromotion(result, orderItems, + rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), + getRewardActivityNotMeetTip(rewardActivity)); + return; + } + + // 2.1 计算可以优惠的金额 + Integer newDiscountPrice = rule.getDiscountPrice(); + // 2.2 计算分摊的优惠金额 + List divideDiscountPrices = TradePriceCalculatorHelper.dividePrice(orderItems, newDiscountPrice); + + // 3.1 记录使用的优惠劵 + result.setCouponId(param.getCouponId()); + // 3.2 记录优惠明细 + TradePriceCalculatorHelper.addPromotion(result, orderItems, + rewardActivity.getId(), rewardActivity.getName(), PromotionTypeEnum.REWARD_ACTIVITY.getType(), + StrUtil.format("满减送:省 {} 元", formatPrice(rule.getDiscountPrice())), + divideDiscountPrices); + // 3.3 更新 SKU 优惠金额 + for (int i = 0; i < orderItems.size(); i++) { + TradePriceCalculateRespBO.OrderItem orderItem = orderItems.get(i); + orderItem.setDiscountPrice(orderItem.getDiscountPrice() + divideDiscountPrices.get(i)); + TradePriceCalculatorHelper.recountPayPrice(orderItem); + } + TradePriceCalculatorHelper.recountAllPrice(result); + } + + /** + * 获得满减送的订单项(商品)列表 + * + * @param result 计算结果 + * @param rewardActivity 满减送活动 + * @return 订单项(商品)列表 + */ + private List filterMatchCouponOrderItems(TradePriceCalculateRespBO result, + RewardActivityMatchRespDTO rewardActivity) { + return filterList(result.getItems(), + orderItem -> CollUtil.contains(rewardActivity.getSpuIds(), orderItem.getSpuId())); + } + + /** + * 获得最大匹配的满减送活动的规则 + * + * @param rewardActivity 满减送活动 + * @param orderItems 商品项 + * @return 匹配的活动规则 + */ + private RewardActivityMatchRespDTO.Rule getMaxMatchRewardActivityRule(RewardActivityMatchRespDTO rewardActivity, + List orderItems) { + // 1. 计算数量和价格 + Integer count = TradePriceCalculatorHelper.calculateTotalCount(orderItems); + Integer price = TradePriceCalculatorHelper.calculateTotalPayPrice(orderItems); + assert count != null && price != null; + + // 2. 倒序找一个最大优惠的规则 + for (int i = rewardActivity.getRules().size() - 1; i >= 0; i--) { + RewardActivityMatchRespDTO.Rule rule = rewardActivity.getRules().get(i); + if (PromotionConditionTypeEnum.PRICE.getType().equals(rewardActivity.getConditionType()) + && price >= rule.getLimit()) { + return rule; + } + if (PromotionConditionTypeEnum.COUNT.getType().equals(rewardActivity.getConditionType()) + && count >= rule.getLimit()) { + return rule; + } + } + return null; + } + + /** + * 获得满减送活动部匹配时的提示 + * + * @param rewardActivity 满减送活动 + * @return 提示 + */ + private String getRewardActivityNotMeetTip(RewardActivityMatchRespDTO rewardActivity) { + // TODO 芋艿:后面再想想;应该找第一个规则,算下还差多少即可。 + return "TODO"; + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/Kd100ExpressClientTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/Kd100ExpressClientTest.java new file mode 100644 index 0000000..357d343 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/Kd100ExpressClientTest.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.impl; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.module.trade.framework.delivery.config.TradeExpressProperties; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.impl.kd100.Kd100ExpressClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import javax.annotation.Resource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +// TODO @jason:可以参考 AliyunSmsClientTest 写,纯 mockito,无需启动 spring 容器 +/** + * @author jason + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = Kd100ExpressClientTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 trade-delivery-query 配置文件 +public class Kd100ExpressClientTest { + + @Resource + private RestTemplateBuilder builder; + @Resource + private TradeExpressProperties expressQueryProperties; + + private Kd100ExpressClient kd100ExpressClient; + + @BeforeEach + public void init(){ + kd100ExpressClient = new Kd100ExpressClient(builder.build(),expressQueryProperties.getKd100()); + } + @Test + @Disabled("需要 授权 key. 暂时忽略") + void testRealTimeQueryExpressFailed() { + ServiceException t = assertThrows(ServiceException.class, () -> { + ExpressTrackQueryReqDTO reqDTO = new ExpressTrackQueryReqDTO(); + reqDTO.setExpressCode("yto"); + reqDTO.setLogisticsNo("YT9383342193097"); + kd100ExpressClient.getExpressTrackList(reqDTO); + }); + assertEquals(1011003005, t.getCode()); + } + + @Import({ + RestTemplateAutoConfiguration.class + }) + @EnableConfigurationProperties(TradeExpressProperties.class) + public static class Application { + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/KdNiaoExpressClientTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/KdNiaoExpressClientTest.java new file mode 100644 index 0000000..b42be37 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/KdNiaoExpressClientTest.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.impl; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.module.trade.framework.delivery.config.TradeExpressProperties; +import com.yunxi.scm.module.trade.framework.delivery.core.client.dto.ExpressTrackQueryReqDTO; +import com.yunxi.scm.module.trade.framework.delivery.core.client.impl.kdniao.KdNiaoExpressClient; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +import javax.annotation.Resource; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +// TODO @jason:可以参考 AliyunSmsClientTest 写,纯 mockito,无需启动 spring 容器 +/** + * {@link KdNiaoExpressClient} 的单元测试 + * + * @author jason + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = KdNiaoExpressClientTest.Application.class) +@ActiveProfiles("unit-test") +public class KdNiaoExpressClientTest { + + @Resource + private RestTemplateBuilder builder; + @Resource + private TradeExpressProperties expressQueryProperties; + + private KdNiaoExpressClient kdNiaoExpressClient; + + @BeforeEach + public void init(){ + kdNiaoExpressClient = new KdNiaoExpressClient(builder.build(),expressQueryProperties.getKdNiao()); + } + @Test + @Disabled("需要 授权 key. 暂时忽略") + void testRealTimeQueryExpressFailed() { + assertThrows(ServiceException.class,() ->{ + ExpressTrackQueryReqDTO reqDTO = new ExpressTrackQueryReqDTO(); + reqDTO.setExpressCode("yy"); + reqDTO.setLogisticsNo("YT9383342193097"); + kdNiaoExpressClient.getExpressTrackList(reqDTO); + }); + } + + @Import({ + RestTemplateAutoConfiguration.class + }) + @EnableConfigurationProperties(TradeExpressProperties.class) + public static class Application { + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/NoProvideExpressClientTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/NoProvideExpressClientTest.java new file mode 100644 index 0000000..cac8bc1 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/framework/delivery/core/client/impl/NoProvideExpressClientTest.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.trade.framework.delivery.core.client.impl; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.module.trade.framework.delivery.config.ExpressClientConfig; +import com.yunxi.scm.module.trade.framework.delivery.config.TradeExpressProperties; +import com.yunxi.scm.module.trade.framework.delivery.core.client.ExpressClient; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.Resource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +// TODO @jason:可以参考 AliyunSmsClientTest 写,纯 mockito,无需启动 spring 容器 +/** + * @author jason + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = NoProvideExpressClientTest.Application.class) +@ActiveProfiles("unit-test") // 设置使用 trade-delivery-query 配置文件 +@Import({ExpressClientConfig.class}) +public class NoProvideExpressClientTest { + + @Resource + private ExpressClient expressClient; + + @Test + void getExpressTrackList() { + ServiceException t = assertThrows(ServiceException.class, () -> { + expressClient.getExpressTrackList(null); + }); + assertEquals(1011003006, t.getCode()); + } + + @Import({ + RestTemplateAutoConfiguration.class, + }) + @EnableConfigurationProperties(TradeExpressProperties.class) + public static class Application { + + @Bean + private RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder.build(); + } + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleServiceTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleServiceTest.java new file mode 100644 index 0000000..737e197 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/aftersale/TradeAfterSaleServiceTest.java @@ -0,0 +1,152 @@ +package com.yunxi.scm.module.trade.service.aftersale; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.pay.api.refund.PayRefundApi; +import com.yunxi.scm.module.trade.controller.admin.aftersale.vo.TradeAfterSalePageReqVO; +import com.yunxi.scm.module.trade.controller.app.aftersale.vo.AppTradeAfterSaleCreateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleDO; +import com.yunxi.scm.module.trade.dal.dataobject.aftersale.TradeAfterSaleLogDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.dal.mysql.aftersale.TradeAfterSaleLogMapper; +import com.yunxi.scm.module.trade.dal.mysql.aftersale.TradeAfterSaleMapper; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleStatusEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleTypeEnum; +import com.yunxi.scm.module.trade.enums.aftersale.TradeAfterSaleWayEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderItemAfterSaleStatusEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderStatusEnum; +import com.yunxi.scm.module.trade.framework.order.config.TradeOrderProperties; +import com.yunxi.scm.module.trade.service.order.TradeOrderService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link TradeAfterSaleService} 的单元测试 + * + * @author 芋道源码 + */ +@Import(TradeAfterSaleServiceImpl.class) +public class TradeAfterSaleServiceTest extends BaseDbUnitTest { + + @Resource + private TradeAfterSaleServiceImpl tradeAfterSaleService; + + @Resource + private TradeAfterSaleMapper tradeAfterSaleMapper; + @Resource + private TradeAfterSaleLogMapper tradeAfterSaleLogMapper; + + @MockBean + private TradeOrderService tradeOrderService; + @MockBean + private PayRefundApi payRefundApi; + + @MockBean + private TradeOrderProperties tradeOrderProperties; + + @Test + public void testCreateAfterSale() { + // 准备参数 + Long userId = 1024L; + AppTradeAfterSaleCreateReqVO createReqVO = new AppTradeAfterSaleCreateReqVO() + .setOrderItemId(1L).setRefundPrice(100).setWay(TradeAfterSaleWayEnum.RETURN_AND_REFUND.getWay()) + .setApplyReason("退钱").setApplyDescription("快退") + .setApplyPicUrls(asList("https://www.baidu.com/1.png", "https://www.baidu.com/2.png")); + // mock 方法(交易订单项) + TradeOrderItemDO orderItem = randomPojo(TradeOrderItemDO.class, o -> { + o.setOrderId(111L).setUserId(userId).setPayPrice(200); + o.setAfterSaleStatus(TradeOrderItemAfterSaleStatusEnum.NONE.getStatus()); + }); + when(tradeOrderService.getOrderItem(eq(1024L), eq(1L))) + .thenReturn(orderItem); + // mock 方法(交易订单) + TradeOrderDO order = randomPojo(TradeOrderDO.class, o -> o.setStatus(TradeOrderStatusEnum.DELIVERED.getStatus()) + .setNo("202211301234")); + when(tradeOrderService.getOrder(eq(1024L), eq(111L))).thenReturn(order); + + // 调用 + Long afterSaleId = tradeAfterSaleService.createAfterSale(userId, createReqVO); + // 断言(TradeAfterSaleDO) + TradeAfterSaleDO afterSale = tradeAfterSaleMapper.selectById(afterSaleId); + assertNotNull(afterSale.getNo()); + assertEquals(afterSale.getStatus(), TradeAfterSaleStatusEnum.APPLY.getStatus()); + assertEquals(afterSale.getType(), TradeAfterSaleTypeEnum.IN_SALE.getType()); + assertPojoEquals(afterSale, createReqVO); + assertEquals(afterSale.getUserId(), 1024L); + assertPojoEquals(afterSale, orderItem, "id", "creator", "createTime", "updater", "updateTime"); + assertEquals(afterSale.getOrderNo(), "202211301234"); + assertNull(afterSale.getPayRefundId()); + assertNull(afterSale.getRefundTime()); + assertNull(afterSale.getLogisticsId()); + assertNull(afterSale.getLogisticsNo()); + assertNull(afterSale.getDeliveryTime()); + assertNull(afterSale.getReceiveReason()); + // 断言(TradeAfterSaleLogDO) + TradeAfterSaleLogDO afterSaleLog = tradeAfterSaleLogMapper.selectList().get(0); + assertEquals(afterSaleLog.getUserId(), userId); + assertEquals(afterSaleLog.getUserType(), UserTypeEnum.MEMBER.getValue()); + assertEquals(afterSaleLog.getAfterSaleId(), afterSaleId); + assertPojoEquals(afterSale, orderItem, "id", "creator", "createTime", "updater", "updateTime"); + assertEquals(afterSaleLog.getContent(), TradeAfterSaleStatusEnum.APPLY.getContent()); + } + + @Test + public void testGetAfterSalePage() { + // mock 数据 + TradeAfterSaleDO dbAfterSale = randomPojo(TradeAfterSaleDO.class, o -> { // 等会查询到 + o.setNo("202211190847450020500077"); + o.setStatus(TradeAfterSaleStatusEnum.APPLY.getStatus()); + o.setWay(TradeAfterSaleWayEnum.RETURN_AND_REFUND.getWay()); + o.setType(TradeAfterSaleTypeEnum.IN_SALE.getType()); + o.setOrderNo("202211190847450020500011"); + o.setSpuName("芋艿"); + o.setCreateTime(buildTime(2022, 1, 15)); + }); + tradeAfterSaleMapper.insert(dbAfterSale); + // 测试 no 不匹配 + tradeAfterSaleMapper.insert(cloneIgnoreId(dbAfterSale, o -> o.setNo("202211190847450020500066"))); + // 测试 status 不匹配 + tradeAfterSaleMapper.insert(cloneIgnoreId(dbAfterSale, o -> o.setStatus(TradeAfterSaleStatusEnum.SELLER_REFUSE.getStatus()))); + // 测试 way 不匹配 + tradeAfterSaleMapper.insert(cloneIgnoreId(dbAfterSale, o -> o.setWay(TradeAfterSaleWayEnum.REFUND.getWay()))); + // 测试 type 不匹配 + tradeAfterSaleMapper.insert(cloneIgnoreId(dbAfterSale, o -> o.setType(TradeAfterSaleTypeEnum.AFTER_SALE.getType()))); + // 测试 orderNo 不匹配 + tradeAfterSaleMapper.insert(cloneIgnoreId(dbAfterSale, o -> o.setOrderNo("202211190847450020500022"))); + // 测试 spuName 不匹配 + tradeAfterSaleMapper.insert(cloneIgnoreId(dbAfterSale, o -> o.setSpuName("土豆"))); + // 测试 createTime 不匹配 + tradeAfterSaleMapper.insert(cloneIgnoreId(dbAfterSale, o -> o.setCreateTime(buildTime(2022, 1, 20)))); + // 准备参数 + TradeAfterSalePageReqVO reqVO = new TradeAfterSalePageReqVO(); + reqVO.setNo("20221119084745002050007"); + reqVO.setStatus(TradeAfterSaleStatusEnum.APPLY.getStatus()); + reqVO.setWay(TradeAfterSaleWayEnum.RETURN_AND_REFUND.getWay()); + reqVO.setType(TradeAfterSaleTypeEnum.IN_SALE.getType()); + reqVO.setOrderNo("20221119084745002050001"); + reqVO.setSpuName("芋"); + reqVO.setCreateTime(new LocalDateTime[]{buildTime(2022, 1, 1), buildTime(2022, 1, 16)}); + + // 调用 + PageResult pageResult = tradeAfterSaleService.getAfterSalePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbAfterSale, pageResult.getList().get(0)); + } +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/order/TradeOrderServiceTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/order/TradeOrderServiceTest.java new file mode 100644 index 0000000..5bc2128 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/order/TradeOrderServiceTest.java @@ -0,0 +1,318 @@ +package com.yunxi.scm.module.trade.service.order; + +import com.yunxi.scm.framework.common.enums.TerminalEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.member.api.address.AddressApi; +import com.yunxi.scm.module.member.api.address.dto.AddressRespDTO; +import com.yunxi.scm.module.member.api.user.MemberUserApi; +import com.yunxi.scm.module.pay.api.order.PayOrderApi; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderRespDTO; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.product.api.sku.ProductSkuApi; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.spu.ProductSpuApi; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.yunxi.scm.module.promotion.api.coupon.CouponApi; +import com.yunxi.scm.module.promotion.api.price.PriceApi; +import com.yunxi.scm.module.promotion.api.price.dto.PriceCalculateRespDTO; +import com.yunxi.scm.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; +import com.yunxi.scm.module.trade.controller.app.order.vo.AppTradeOrderCreateReqVO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderDO; +import com.yunxi.scm.module.trade.dal.dataobject.order.TradeOrderItemDO; +import com.yunxi.scm.module.trade.dal.mysql.order.TradeOrderItemMapper; +import com.yunxi.scm.module.trade.dal.mysql.order.TradeOrderMapper; +import com.yunxi.scm.module.trade.enums.order.*; +import com.yunxi.scm.module.trade.framework.order.config.TradeOrderConfig; +import com.yunxi.scm.module.trade.framework.order.config.TradeOrderProperties; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * {@link TradeOrderServiceImpl} 的单元测试类 + * + * @author LeeYan9 + * @since 2022-09-07 + */ +@Import({TradeOrderServiceImpl.class, TradeOrderConfig.class}) +public class TradeOrderServiceTest extends BaseDbUnitTest { + + @Resource + private TradeOrderServiceImpl tradeOrderService; + + @Resource + private TradeOrderMapper tradeOrderMapper; + @Resource + private TradeOrderItemMapper tradeOrderItemMapper; + + @MockBean + private MemberUserApi memberUserApi; + @MockBean + private ProductSpuApi productSpuApi; + @MockBean + private ProductSkuApi productSkuApi; + @MockBean + private PriceApi priceApi; + @MockBean + private PayOrderApi payOrderApi; + @MockBean + private AddressApi addressApi; + @MockBean + private CouponApi couponApi; + + @MockBean + private TradeOrderProperties tradeOrderProperties; + + @BeforeEach + public void setUp() { + when(tradeOrderProperties.getAppId()).thenReturn(888L); + when(tradeOrderProperties.getExpireTime()).thenReturn(Duration.ofDays(1)); + } + + @Test + public void testCreateTradeOrder_success() { + // 准备参数 + Long userId = 100L; + String userIp = "127.0.0.1"; +// AppTradeOrderCreateReqVO reqVO = new AppTradeOrderCreateReqVO() +// .setAddressId(10L).setCouponId(101L).setRemark("我是备注").setFromCart(true) +// .setItems(Arrays.asList(new AppTradeOrderCreateReqVO.Item().setSkuId(1L).setCount(3), +// new AppTradeOrderCreateReqVO.Item().setSkuId(2L).setCount(4))); + AppTradeOrderCreateReqVO reqVO = null; + // TODO 芋艿:重新高下 + // mock 方法(商品 SKU 检查) + ProductSkuRespDTO sku01 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(1L).setSpuId(11L) + .setPrice(50).setStock(100) + .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(111L).setValueId(222L)))); + ProductSkuRespDTO sku02 = randomPojo(ProductSkuRespDTO.class, o -> o.setId(2L).setSpuId(21L) + .setPrice(20).setStock(50)) + .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(333L).setValueId(444L))); + when(productSkuApi.getSkuList(eq(asSet(1L, 2L)))).thenReturn(Arrays.asList(sku01, sku02)); + // mock 方法(商品 SPU 检查) + ProductSpuRespDTO spu01 = randomPojo(ProductSpuRespDTO.class, o -> o.setId(11L) + .setStatus(ProductSpuStatusEnum.ENABLE.getStatus()).setName("商品 1")); + ProductSpuRespDTO spu02 = randomPojo(ProductSpuRespDTO.class, o -> o.setId(21L) + .setStatus(ProductSpuStatusEnum.ENABLE.getStatus())); + when(productSpuApi.getSpuList(eq(asSet(11L, 21L)))).thenReturn(Arrays.asList(spu01, spu02)); + // mock 方法(用户收件地址的校验) + AddressRespDTO addressRespDTO = new AddressRespDTO().setId(10L).setUserId(userId).setName("芋艿") + .setMobile("15601691300").setAreaId(3306).setDetailAddress("土豆村"); + when(addressApi.getAddress(eq(10L), eq(userId))).thenReturn(addressRespDTO); + // mock 方法(价格计算) + PriceCalculateRespDTO.OrderItem priceOrderItem01 = new PriceCalculateRespDTO.OrderItem() + .setSpuId(11L).setSkuId(1L).setCount(3).setOriginalPrice(150).setOriginalUnitPrice(50) + .setDiscountPrice(20).setPayPrice(130).setOrderPartPrice(7).setOrderDividePrice(35); + PriceCalculateRespDTO.OrderItem priceOrderItem02 = new PriceCalculateRespDTO.OrderItem() + .setSpuId(21L).setSkuId(2L).setCount(4).setOriginalPrice(80).setOriginalUnitPrice(20) + .setDiscountPrice(40).setPayPrice(40).setOrderPartPrice(15).setOrderDividePrice(25); + PriceCalculateRespDTO.Order priceOrder = new PriceCalculateRespDTO.Order() + .setTotalPrice(230).setDiscountPrice(0).setCouponPrice(30) + .setPointPrice(10).setDeliveryPrice(20).setPayPrice(80).setCouponId(101L).setCouponPrice(30) + .setItems(Arrays.asList(priceOrderItem01, priceOrderItem02)); + when(priceApi.calculatePrice(argThat(priceCalculateReqDTO -> { + assertEquals(priceCalculateReqDTO.getUserId(), 100L); + assertEquals(priceCalculateReqDTO.getCouponId(), 101L); + assertEquals(priceCalculateReqDTO.getItems().get(0).getSkuId(), 1L); + assertEquals(priceCalculateReqDTO.getItems().get(0).getCount(), 3); + assertEquals(priceCalculateReqDTO.getItems().get(1).getSkuId(), 2L); + assertEquals(priceCalculateReqDTO.getItems().get(1).getCount(), 4); + return true; + }))).thenReturn(new PriceCalculateRespDTO().setOrder(priceOrder)); + // mock 方法(创建支付单) + when(payOrderApi.createOrder(argThat(createReqDTO -> { + assertEquals(createReqDTO.getAppId(), 888L); + assertEquals(createReqDTO.getUserIp(), userIp); + assertNotNull(createReqDTO.getMerchantOrderId()); // 由于 tradeOrderId 后生成,只能校验非空 + assertEquals(createReqDTO.getSubject(), "商品 1 等多件"); + assertNull(createReqDTO.getBody()); + assertEquals(createReqDTO.getPrice(), 80); + assertNotNull(createReqDTO.getExpireTime()); + return true; + }))).thenReturn(1000L); + + // 调用方法 + TradeOrderDO order = tradeOrderService.createOrder(userId, userIp, reqVO); + // 断言 TradeOrderDO 订单 + List tradeOrderDOs = tradeOrderMapper.selectList(); + assertEquals(tradeOrderDOs.size(), 1); + TradeOrderDO tradeOrderDO = tradeOrderDOs.get(0); + assertEquals(tradeOrderDO.getId(), order.getId()); + assertNotNull(tradeOrderDO.getNo()); + assertEquals(tradeOrderDO.getType(), TradeOrderTypeEnum.NORMAL.getType()); + assertEquals(tradeOrderDO.getTerminal(), TerminalEnum.H5.getTerminal()); + assertEquals(tradeOrderDO.getUserId(), userId); + assertEquals(tradeOrderDO.getUserIp(), userIp); + assertEquals(tradeOrderDO.getStatus(), TradeOrderStatusEnum.UNPAID.getStatus()); + assertEquals(tradeOrderDO.getProductCount(), 7); + assertNull(tradeOrderDO.getFinishTime()); + assertNull(tradeOrderDO.getCancelTime()); + assertNull(tradeOrderDO.getCancelType()); + assertEquals(tradeOrderDO.getUserRemark(), "我是备注"); + assertNull(tradeOrderDO.getRemark()); + assertFalse(tradeOrderDO.getPayStatus()); + assertNull(tradeOrderDO.getPayTime()); + assertEquals(tradeOrderDO.getTotalPrice(), 230); + assertEquals(tradeOrderDO.getDiscountPrice(), 0); + assertEquals(tradeOrderDO.getAdjustPrice(), 0); + assertEquals(tradeOrderDO.getPayPrice(), 80); + assertEquals(tradeOrderDO.getPayOrderId(), 1000L); + assertNull(tradeOrderDO.getPayChannelCode()); + assertNull(tradeOrderDO.getLogisticsId()); + assertEquals(tradeOrderDO.getDeliveryStatus(), TradeOrderDeliveryStatusEnum.UNDELIVERED.getStatus()); + assertNull(tradeOrderDO.getDeliveryTime()); + assertNull(tradeOrderDO.getReceiveTime()); + assertEquals(tradeOrderDO.getReceiverName(), "芋艿"); + assertEquals(tradeOrderDO.getReceiverMobile(), "15601691300"); + assertEquals(tradeOrderDO.getReceiverAreaId(), 3306); + assertEquals(tradeOrderDO.getReceiverDetailAddress(), "土豆村"); + assertEquals(tradeOrderDO.getRefundStatus(), TradeOrderRefundStatusEnum.NONE.getStatus()); + assertEquals(tradeOrderDO.getRefundPrice(), 0); + assertEquals(tradeOrderDO.getCouponPrice(), 30); + assertEquals(tradeOrderDO.getPointPrice(), 10); + // 断言 TradeOrderItemDO 订单(第 1 个) + List tradeOrderItemDOs = tradeOrderItemMapper.selectList(); + assertEquals(tradeOrderItemDOs.size(), 2); + TradeOrderItemDO tradeOrderItemDO01 = tradeOrderItemDOs.get(0); + assertNotNull(tradeOrderItemDO01.getId()); + assertEquals(tradeOrderItemDO01.getUserId(), userId); + assertEquals(tradeOrderItemDO01.getOrderId(), order.getId()); + assertEquals(tradeOrderItemDO01.getSpuId(), 11L); + assertEquals(tradeOrderItemDO01.getSkuId(), 1L); + assertEquals(tradeOrderItemDO01.getProperties().size(), 1); + assertEquals(tradeOrderItemDO01.getProperties().get(0).getPropertyId(), 111L); + assertEquals(tradeOrderItemDO01.getProperties().get(0).getValueId(), 222L); + //assertEquals(tradeOrderItemDO01.getSpuName(), sku01.getSpuName()); TODO 找不到spuName + assertEquals(tradeOrderItemDO01.getPicUrl(), sku01.getPicUrl()); + assertEquals(tradeOrderItemDO01.getCount(), 3); +// assertEquals(tradeOrderItemDO01.getOriginalPrice(), 150); + assertEquals(tradeOrderItemDO01.getPrice(), 50); + assertEquals(tradeOrderItemDO01.getDiscountPrice(), 20); + assertEquals(tradeOrderItemDO01.getPayPrice(), 130); + assertEquals(tradeOrderItemDO01.getAfterSaleStatus(), TradeOrderItemAfterSaleStatusEnum.NONE.getStatus()); + // 断言 TradeOrderItemDO 订单(第 2 个) + TradeOrderItemDO tradeOrderItemDO02 = tradeOrderItemDOs.get(1); + assertNotNull(tradeOrderItemDO02.getId()); + assertEquals(tradeOrderItemDO02.getUserId(), userId); + assertEquals(tradeOrderItemDO02.getOrderId(), order.getId()); + assertEquals(tradeOrderItemDO02.getSpuId(), 21L); + assertEquals(tradeOrderItemDO02.getSkuId(), 2L); + assertEquals(tradeOrderItemDO02.getProperties().size(), 1); + assertEquals(tradeOrderItemDO02.getProperties().get(0).getPropertyId(), 333L); + assertEquals(tradeOrderItemDO02.getProperties().get(0).getValueId(), 444L); + //assertEquals(tradeOrderItemDO02.getSpuName(), sku02.getSpuName()); TODO 找不到spuName + assertEquals(tradeOrderItemDO02.getPicUrl(), sku02.getPicUrl()); + assertEquals(tradeOrderItemDO02.getCount(), 4); +// assertEquals(tradeOrderItemDO02.getOriginalPrice(), 80); + assertEquals(tradeOrderItemDO02.getPrice(), 20); + assertEquals(tradeOrderItemDO02.getDiscountPrice(), 40); + assertEquals(tradeOrderItemDO02.getPayPrice(), 40); + assertEquals(tradeOrderItemDO02.getAfterSaleStatus(), TradeOrderItemAfterSaleStatusEnum.NONE.getStatus()); + // 校验调用 + verify(productSkuApi).updateSkuStock(argThat(updateStockReqDTO -> { + assertEquals(updateStockReqDTO.getItems().size(), 2); + assertEquals(updateStockReqDTO.getItems().get(0).getId(), 1L); + assertEquals(updateStockReqDTO.getItems().get(0).getIncrCount(), 3); + assertEquals(updateStockReqDTO.getItems().get(1).getId(), 2L); + assertEquals(updateStockReqDTO.getItems().get(1).getIncrCount(), 4); + return true; + })); + verify(couponApi).useCoupon(argThat(reqDTO -> { + assertEquals(reqDTO.getId(), reqVO.getCouponId()); + assertEquals(reqDTO.getUserId(), userId); + assertEquals(reqDTO.getOrderId(), order.getId()); + return true; + })); + } + + @Test + public void testUpdateOrderPaid() { + // mock 数据(TradeOrder) + TradeOrderDO order = randomPojo(TradeOrderDO.class, o -> { + o.setId(1L).setStatus(TradeOrderStatusEnum.UNPAID.getStatus()); + o.setPayOrderId(10L).setPayStatus(false).setPayPrice(100).setPayTime(null); + }); + tradeOrderMapper.insert(order); + // 准备参数 + Long id = 1L; + Long payOrderId = 10L; + // mock 方法(支付单) + when(payOrderApi.getOrder(eq(10L))).thenReturn(randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()).setChannelCode("wx_pub") + .setMerchantOrderId("1")).setPrice(100)); + + // 调用 + tradeOrderService.updateOrderPaid(id, payOrderId); + // 断言 + TradeOrderDO dbOrder = tradeOrderMapper.selectById(id); + assertEquals(dbOrder.getStatus(), TradeOrderStatusEnum.UNDELIVERED.getStatus()); + assertTrue(dbOrder.getPayStatus()); + assertNotNull(dbOrder.getPayTime()); + assertEquals(dbOrder.getPayChannelCode(), "wx_pub"); + } + + @Test + public void testDeliveryOrder() { + // mock 数据(TradeOrder) + TradeOrderDO order = randomPojo(TradeOrderDO.class, o -> { + o.setId(1L).setStatus(TradeOrderStatusEnum.UNDELIVERED.getStatus()); + o.setLogisticsId(null).setLogisticsNo(null).setDeliveryTime(null) + .setDeliveryStatus(TradeOrderDeliveryStatusEnum.UNDELIVERED.getStatus()); + }); + tradeOrderMapper.insert(order); + // 准备参数 + TradeOrderDeliveryReqVO deliveryReqVO = new TradeOrderDeliveryReqVO().setId(1L) + .setLogisticsId(10L).setLogisticsNo("100"); + // mock 方法(支付单) + + // 调用 + tradeOrderService.deliveryOrder(randomLongId(), deliveryReqVO); + // 断言 + TradeOrderDO dbOrder = tradeOrderMapper.selectById(1L); + assertEquals(dbOrder.getStatus(), TradeOrderStatusEnum.DELIVERED.getStatus()); + assertEquals(dbOrder.getDeliveryStatus(), TradeOrderDeliveryStatusEnum.DELIVERED.getStatus()); + assertPojoEquals(dbOrder, deliveryReqVO); + assertNotNull(dbOrder.getDeliveryTime()); + } + + @Test + public void testReceiveOrder() { + // mock 数据(TradeOrder) + TradeOrderDO order = randomPojo(TradeOrderDO.class, o -> { + o.setId(1L).setUserId(10L).setStatus(TradeOrderStatusEnum.DELIVERED.getStatus()); + o.setDeliveryStatus(TradeOrderDeliveryStatusEnum.DELIVERED.getStatus()).setReceiveTime(null); + }); + tradeOrderMapper.insert(order); + // 准备参数 + Long id = 1L; + Long userId = 10L; + // mock 方法(支付单) + + // 调用 + tradeOrderService.receiveOrder(userId, id); + // 断言 + TradeOrderDO dbOrder = tradeOrderMapper.selectById(1L); + assertEquals(dbOrder.getStatus(), TradeOrderStatusEnum.COMPLETED.getStatus()); + assertEquals(dbOrder.getDeliveryStatus(), TradeOrderDeliveryStatusEnum.RECEIVED.getStatus()); + assertNotNull(dbOrder.getReceiveTime()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/TradePriceServiceImplTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/TradePriceServiceImplTest.java new file mode 100644 index 0000000..7f73a8c --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/TradePriceServiceImplTest.java @@ -0,0 +1,135 @@ +package com.yunxi.scm.module.trade.service.price; + +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.product.api.property.dto.ProductPropertyValueDetailRespDTO; +import com.yunxi.scm.module.product.api.sku.ProductSkuApi; +import com.yunxi.scm.module.product.api.sku.dto.ProductSkuRespDTO; +import com.yunxi.scm.module.product.api.spu.ProductSpuApi; +import com.yunxi.scm.module.product.api.spu.dto.ProductSpuRespDTO; +import com.yunxi.scm.module.product.enums.spu.ProductSpuStatusEnum; +import com.yunxi.scm.module.trade.enums.order.TradeOrderTypeEnum; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import com.yunxi.scm.module.trade.service.price.calculator.TradePriceCalculator; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; + +import java.util.Arrays; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +/** + * {@link TradePriceServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +public class TradePriceServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private TradePriceServiceImpl tradePriceService; + + @Mock + private ProductSkuApi productSkuApi; + @Mock + private ProductSpuApi productSpuApi; + @Mock + private List priceCalculators; + + @Test + public void testCalculatePrice() { + // 准备参数 + TradePriceCalculateReqBO calculateReqBO = new TradePriceCalculateReqBO() + .setType(TradeOrderTypeEnum.NORMAL.getType()).setUserId(10L) + .setCouponId(20L).setAddressId(30L) + .setItems(Arrays.asList( + new TradePriceCalculateReqBO.Item().setSkuId(100L).setCount(1).setSelected(true), + new TradePriceCalculateReqBO.Item().setSkuId(200L).setCount(3).setSelected(true), + new TradePriceCalculateReqBO.Item().setSkuId(300L).setCount(6).setCartId(233L).setSelected(false) + )); + // mock 方法 + List skuList = Arrays.asList( + new ProductSkuRespDTO().setId(100L).setStock(500).setPrice(1000).setPicUrl("https://t.cn/1.png").setSpuId(1001L) + .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色") + .setValueId(2L).setValueName("红色"))), + new ProductSkuRespDTO().setId(200L).setStock(400).setPrice(2000).setPicUrl("https://t.cn/2.png").setSpuId(1001L) + .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色") + .setValueId(3L).setValueName("黄色"))), + new ProductSkuRespDTO().setId(300L).setStock(600).setPrice(3000).setPicUrl("https://t.cn/3.png").setSpuId(1001L) + .setProperties(singletonList(new ProductPropertyValueDetailRespDTO().setPropertyId(1L).setPropertyName("颜色") + .setValueId(4L).setValueName("黑色"))) + ); + when(productSkuApi.getSkuList(Mockito.eq(asSet(100L, 200L, 300L)))).thenReturn(skuList); + when(productSpuApi.getSpuList(Mockito.eq(asSet(1001L)))) + .thenReturn(singletonList(new ProductSpuRespDTO().setId(1001L).setName("小菜").setCategoryId(666L) + .setStatus(ProductSpuStatusEnum.ENABLE.getStatus()))); + + // 调用 + TradePriceCalculateRespBO calculateRespBO = tradePriceService.calculatePrice(calculateReqBO); + // 断言 + assertEquals(TradeOrderTypeEnum.NORMAL.getType(), calculateRespBO.getType()); + assertEquals(0, calculateRespBO.getPromotions().size()); + assertNull(calculateRespBO.getCouponId()); + // 断言:订单价格 + assertEquals(7000, calculateRespBO.getPrice().getTotalPrice()); + assertEquals(0, calculateRespBO.getPrice().getDiscountPrice()); + assertEquals(0, calculateRespBO.getPrice().getDeliveryPrice()); + assertEquals(0, calculateRespBO.getPrice().getCouponPrice()); + assertEquals(0, calculateRespBO.getPrice().getPointPrice()); + assertEquals(7000, calculateRespBO.getPrice().getPayPrice()); + // 断言:SKU 1 + assertEquals(1001L, calculateRespBO.getItems().get(0).getSpuId()); + assertEquals(100L, calculateRespBO.getItems().get(0).getSkuId()); + assertEquals(1, calculateRespBO.getItems().get(0).getCount()); + assertNull(calculateRespBO.getItems().get(0).getCartId()); + assertTrue(calculateRespBO.getItems().get(0).getSelected()); + assertEquals(1000, calculateRespBO.getItems().get(0).getPrice()); + assertEquals(0, calculateRespBO.getItems().get(0).getDiscountPrice()); + assertEquals(0, calculateRespBO.getItems().get(0).getDeliveryPrice()); + assertEquals(0, calculateRespBO.getItems().get(0).getCouponPrice()); + assertEquals(0, calculateRespBO.getItems().get(0).getPointPrice()); + assertEquals(1000, calculateRespBO.getItems().get(0).getPayPrice()); + assertEquals("小菜", calculateRespBO.getItems().get(0).getSpuName()); + assertEquals("https://t.cn/1.png", calculateRespBO.getItems().get(0).getPicUrl()); + assertEquals(666L, calculateRespBO.getItems().get(0).getCategoryId()); + assertEquals(skuList.get(0).getProperties(), calculateRespBO.getItems().get(0).getProperties()); + // 断言:SKU 2 + assertEquals(1001L, calculateRespBO.getItems().get(1).getSpuId()); + assertEquals(200L, calculateRespBO.getItems().get(1).getSkuId()); + assertEquals(3, calculateRespBO.getItems().get(1).getCount()); + assertNull(calculateRespBO.getItems().get(1).getCartId()); + assertTrue(calculateRespBO.getItems().get(1).getSelected()); + assertEquals(2000, calculateRespBO.getItems().get(1).getPrice()); + assertEquals(0, calculateRespBO.getItems().get(1).getDiscountPrice()); + assertEquals(0, calculateRespBO.getItems().get(1).getDeliveryPrice()); + assertEquals(0, calculateRespBO.getItems().get(1).getCouponPrice()); + assertEquals(0, calculateRespBO.getItems().get(1).getPointPrice()); + assertEquals(6000, calculateRespBO.getItems().get(1).getPayPrice()); + assertEquals("小菜", calculateRespBO.getItems().get(1).getSpuName()); + assertEquals("https://t.cn/2.png", calculateRespBO.getItems().get(1).getPicUrl()); + assertEquals(666L, calculateRespBO.getItems().get(1).getCategoryId()); + assertEquals(skuList.get(1).getProperties(), calculateRespBO.getItems().get(1).getProperties()); + // 断言:SKU 3 + assertEquals(1001L, calculateRespBO.getItems().get(2).getSpuId()); + assertEquals(300L, calculateRespBO.getItems().get(2).getSkuId()); + assertEquals(6, calculateRespBO.getItems().get(2).getCount()); + assertEquals(233L, calculateRespBO.getItems().get(2).getCartId()); + assertFalse(calculateRespBO.getItems().get(2).getSelected()); + assertEquals(3000, calculateRespBO.getItems().get(2).getPrice()); + assertEquals(0, calculateRespBO.getItems().get(2).getDiscountPrice()); + assertEquals(0, calculateRespBO.getItems().get(2).getDeliveryPrice()); + assertEquals(0, calculateRespBO.getItems().get(2).getCouponPrice()); + assertEquals(0, calculateRespBO.getItems().get(2).getPointPrice()); + assertEquals(18000, calculateRespBO.getItems().get(2).getPayPrice()); + assertEquals("小菜", calculateRespBO.getItems().get(2).getSpuName()); + assertEquals("https://t.cn/3.png", calculateRespBO.getItems().get(2).getPicUrl()); + assertEquals(666L, calculateRespBO.getItems().get(2).getCategoryId()); + assertEquals(skuList.get(2).getProperties(), calculateRespBO.getItems().get(2).getProperties()); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java new file mode 100644 index 0000000..eb375db --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeCouponPriceCalculatorTest.java @@ -0,0 +1,144 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.promotion.api.coupon.CouponApi; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponRespDTO; +import com.yunxi.scm.module.promotion.api.coupon.dto.CouponValidReqDTO; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionProductScopeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionTypeEnum; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.ArrayList; + +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link TradeCouponPriceCalculator} 的单元测试类 + * + * @author 芋道源码 + */ +public class TradeCouponPriceCalculatorTest extends BaseMockitoUnitTest { + + @InjectMocks + private TradeCouponPriceCalculator tradeCouponPriceCalculator; + + @Mock + private CouponApi couponApi; + + @Test + public void testCalculate() { + // 准备参数 + TradePriceCalculateReqBO param = new TradePriceCalculateReqBO() + .setUserId(233L).setCouponId(1024L) + .setItems(asList( + new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), // 匹配优惠劵 + new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(true), // 匹配优惠劵 + new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(true), // 不匹配优惠劵 + new TradePriceCalculateReqBO.Item().setSkuId(40L).setCount(5).setSelected(false) // 匹配优惠劵,但是未选中 + )); + TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() + .setPrice(new TradePriceCalculateRespBO.Price()) + .setPromotions(new ArrayList<>()) + .setItems(asList( + new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) + .setPrice(100).setSpuId(1L), + new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(true) + .setPrice(50).setSpuId(2L), + new TradePriceCalculateRespBO.OrderItem().setSkuId(30L).setCount(4).setSelected(true) + .setPrice(30).setSpuId(3L), + new TradePriceCalculateRespBO.OrderItem().setSkuId(40L).setCount(5).setSelected(false) + .setPrice(60).setSpuId(1L) + )); + // 保证价格被初始化上 + TradePriceCalculatorHelper.recountPayPrice(result.getItems()); + TradePriceCalculatorHelper.recountAllPrice(result); + + // mock 方法(优惠劵 Coupon 信息) + CouponRespDTO coupon = randomPojo(CouponRespDTO.class, o -> o.setId(1024L).setName("程序员节") + .setProductScope(PromotionProductScopeEnum.SPU.getScope()).setProductSpuIds(asList(1L, 2L)) + .setUsePrice(350).setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType()) + .setDiscountPercent(50).setDiscountLimitPrice(70)); + when(couponApi.validateCoupon(eq(new CouponValidReqDTO().setId(1024L).setUserId(233L)))).thenReturn(coupon); + + // 调用 + tradeCouponPriceCalculator.calculate(param, result); + // 断言 + assertEquals(result.getCouponId(), 1024L); + // 断言:Price 部分 + TradePriceCalculateRespBO.Price price = result.getPrice(); + assertEquals(price.getTotalPrice(), 470); + assertEquals(price.getDiscountPrice(), 0); + assertEquals(price.getPointPrice(), 0); + assertEquals(price.getDeliveryPrice(), 0); + assertEquals(price.getCouponPrice(), 70); + assertEquals(price.getPayPrice(), 400); + // 断言:SKU 1 + TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0); + assertEquals(orderItem01.getSkuId(), 10L); + assertEquals(orderItem01.getCount(), 2); + assertEquals(orderItem01.getPrice(), 100); + assertEquals(orderItem01.getDiscountPrice(), 0); + assertEquals(orderItem01.getDeliveryPrice(), 0); + assertEquals(orderItem01.getCouponPrice(), 40); + assertEquals(orderItem01.getPointPrice(), 0); + assertEquals(orderItem01.getPayPrice(), 160); + // 断言:SKU 2 + TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1); + assertEquals(orderItem02.getSkuId(), 20L); + assertEquals(orderItem02.getCount(), 3); + assertEquals(orderItem02.getPrice(), 50); + assertEquals(orderItem02.getDiscountPrice(), 0); + assertEquals(orderItem02.getDeliveryPrice(), 0); + assertEquals(orderItem02.getCouponPrice(), 30); + assertEquals(orderItem02.getPointPrice(), 0); + assertEquals(orderItem02.getPayPrice(), 120); + // 断言:SKU 3 + TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2); + assertEquals(orderItem03.getSkuId(), 30L); + assertEquals(orderItem03.getCount(), 4); + assertEquals(orderItem03.getPrice(), 30); + assertEquals(orderItem03.getDiscountPrice(), 0); + assertEquals(orderItem03.getCouponPrice(), 0); + assertEquals(orderItem03.getPointPrice(), 0); + assertEquals(orderItem03.getPayPrice(), 120); + // 断言:SKU 4 + TradePriceCalculateRespBO.OrderItem orderItem04 = result.getItems().get(3); + assertEquals(orderItem04.getSkuId(), 40L); + assertEquals(orderItem04.getCount(), 5); + assertEquals(orderItem04.getPrice(), 60); + assertEquals(orderItem04.getDiscountPrice(), 0); + assertEquals(orderItem04.getCouponPrice(), 0); + assertEquals(orderItem04.getPointPrice(), 0); + assertEquals(orderItem04.getPayPrice(), 300); + // 断言:Promotion 部分 + assertEquals(result.getPromotions().size(), 1); + TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0); + assertEquals(promotion01.getId(), 1024L); + assertEquals(promotion01.getName(), "程序员节"); + assertEquals(promotion01.getType(), PromotionTypeEnum.COUPON.getType()); + assertEquals(promotion01.getTotalPrice(), 350); + assertEquals(promotion01.getDiscountPrice(), 70); + assertTrue(promotion01.getMatch()); + assertEquals(promotion01.getDescription(), "优惠劵:省 0.70 元"); + assertEquals(promotion01.getItems().size(), 2); + TradePriceCalculateRespBO.PromotionItem promotionItem011 = promotion01.getItems().get(0); + assertEquals(promotionItem011.getSkuId(), 10L); + assertEquals(promotionItem011.getTotalPrice(), 200); + assertEquals(promotionItem011.getDiscountPrice(), 40); + TradePriceCalculateRespBO.PromotionItem promotionItem012 = promotion01.getItems().get(1); + assertEquals(promotionItem012.getSkuId(), 20L); + assertEquals(promotionItem012.getTotalPrice(), 150); + assertEquals(promotionItem012.getDiscountPrice(), 30); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java new file mode 100644 index 0000000..55e1f88 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDeliveryPriceCalculatorTest.java @@ -0,0 +1,159 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.member.api.address.AddressApi; +import com.yunxi.scm.module.member.api.address.dto.AddressRespDTO; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryExpressChargeModeEnum; +import com.yunxi.scm.module.trade.enums.delivery.DeliveryTypeEnum; +import com.yunxi.scm.module.trade.service.delivery.DeliveryExpressTemplateService; +import com.yunxi.scm.module.trade.service.delivery.bo.DeliveryExpressTemplateRespBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.ArrayList; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link TradeDeliveryPriceCalculator} 的单元测试 + * + * @author jason + */ +public class TradeDeliveryPriceCalculatorTest extends BaseMockitoUnitTest { + + @InjectMocks + private TradeDeliveryPriceCalculator calculator; + @Mock + private AddressApi addressApi; + @Mock + private DeliveryExpressTemplateService deliveryExpressTemplateService; + + private TradePriceCalculateReqBO reqBO; + private TradePriceCalculateRespBO resultBO; + + private DeliveryExpressTemplateRespBO templateRespBO; + private DeliveryExpressTemplateRespBO.Charge chargeBO; + private DeliveryExpressTemplateRespBO.Free freeBO; + + @BeforeEach + public void init(){ + // 准备参数 + reqBO = new TradePriceCalculateReqBO() + .setDeliveryType(DeliveryTypeEnum.EXPRESS.getMode()) + .setAddressId(10L) + .setUserId(1L) + .setItems(asList( + new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), + new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(10).setSelected(true), + new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(false) // 未选中 + )); + resultBO = new TradePriceCalculateRespBO() + .setPrice(new TradePriceCalculateRespBO.Price()) + .setPromotions(new ArrayList<>()) + .setItems(asList( + new TradePriceCalculateRespBO.OrderItem().setDeliveryTemplateId(1L).setSkuId(10L).setCount(2).setSelected(true) + .setWeight(10d).setVolume(10d).setPrice(100), + new TradePriceCalculateRespBO.OrderItem().setDeliveryTemplateId(1L).setSkuId(20L).setCount(10).setSelected(true) + .setWeight(10d).setVolume(10d).setPrice(200), + new TradePriceCalculateRespBO.OrderItem().setDeliveryTemplateId(1L).setSkuId(30L).setCount(1).setSelected(false) + .setWeight(10d).setVolume(10d).setPrice(300) + )); + // 保证价格被初始化上 + TradePriceCalculatorHelper.recountPayPrice(resultBO.getItems()); + TradePriceCalculatorHelper.recountAllPrice(resultBO); + + // 准备收件地址数据 + AddressRespDTO addressResp = randomPojo(AddressRespDTO.class, item -> item.setAreaId(10)); + when(addressApi.getAddress(eq(10L), eq(1L))).thenReturn(addressResp); + + // 准备运费模板费用配置数据 + chargeBO = randomPojo(DeliveryExpressTemplateRespBO.Charge.class, + item -> item.setStartCount(10D).setStartPrice(1000).setExtraCount(10D).setExtraPrice(2000)); + // 准备运费模板包邮配置数据:订单总件数 < 包邮件数时 12 < 20 + freeBO = randomPojo(DeliveryExpressTemplateRespBO.Free.class, + item -> item.setFreeCount(20).setFreePrice(100)); + // 准备 SP 运费模板数据 + templateRespBO = randomPojo(DeliveryExpressTemplateRespBO.class, + item -> item.setChargeMode(DeliveryExpressChargeModeEnum.PIECE.getType()) + .setCharge(chargeBO).setFree(freeBO)); + } + + @Test + @DisplayName("按件计算运费不包邮的情况") + public void testCalculateByExpressTemplateCharge() { + // SKU 1 : 100 * 2 = 200 + // SKU 2 :200 * 10 = 2000 + // 运费 首件 1000 + 续件 2000 = 3000 + // mock 方法 + when(deliveryExpressTemplateService.getExpressTemplateMapByIdsAndArea(eq(asSet(1L)), eq(10))) + .thenReturn(MapUtil.of(1L, templateRespBO)); + + // 调用 + calculator.calculate(reqBO, resultBO); + // 断言 + TradePriceCalculateRespBO.Price price = resultBO.getPrice(); + assertThat(price) + .extracting("totalPrice","discountPrice","couponPrice","pointPrice","deliveryPrice","payPrice") + .containsExactly(2200, 0, 0, 0, 3000, 5200); + assertThat(resultBO.getItems()).hasSize(3); + // 断言:SKU1 + assertThat(resultBO.getItems().get(0)) + .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") + .containsExactly(100, 2, 0, 0, 0, 1500, 1700); + // 断言:SKU2 + assertThat(resultBO.getItems().get(1)) + .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") + .containsExactly(200, 10, 0, 0, 0, 1500, 3500); + // 断言:SKU3 未选中 + assertThat(resultBO.getItems().get(2)) + .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") + .containsExactly(300, 1, 0, 0, 0, 0, 300); + } + + @Test + @DisplayName("按件计算运费包邮的情况") + public void testCalculateByExpressTemplateFree() { + // SKU 1 : 100 * 2 = 200 + // SKU 2 :200 * 10 = 2000 + // 运费 0 + // mock 方法 + // 准备运费模板包邮配置数据 包邮 订单总件数 > 包邮件数时 12 > 10 + templateRespBO.setFree(randomPojo(DeliveryExpressTemplateRespBO.Free.class, + item -> item.setFreeCount(10).setFreePrice(1000))); + when(deliveryExpressTemplateService.getExpressTemplateMapByIdsAndArea(eq(asSet(1L)), eq(10))) + .thenReturn(MapUtil.of(1L, templateRespBO)); + + // 调用 + calculator.calculate(reqBO, resultBO); + // 断言 + TradePriceCalculateRespBO.Price price = resultBO.getPrice(); + assertThat(price) + .extracting("totalPrice","discountPrice","couponPrice","pointPrice","deliveryPrice","payPrice") + .containsExactly(2200, 0, 0, 0, 0, 2200); + assertThat(resultBO.getItems()).hasSize(3); + // 断言:SKU1 + assertThat(resultBO.getItems().get(0)) + .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") + .containsExactly(100, 2, 0, 0, 0, 0, 200); + // 断言:SKU2 + assertThat(resultBO.getItems().get(1)) + .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") + .containsExactly(200, 10, 0, 0, 0, 0, 2000); + // 断言:SKU3 未选中 + assertThat(resultBO.getItems().get(2)) + .extracting("price", "count","discountPrice" ,"couponPrice", "pointPrice","deliveryPrice","payPrice") + .containsExactly(300, 1, 0, 0, 0, 0, 300); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculatorTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculatorTest.java new file mode 100644 index 0000000..8e2f440 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeDiscountActivityPriceCalculatorTest.java @@ -0,0 +1,118 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.promotion.api.discount.DiscountActivityApi; +import com.yunxi.scm.module.promotion.api.discount.dto.DiscountProductRespDTO; +import com.yunxi.scm.module.promotion.enums.common.PromotionDiscountTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionTypeEnum; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.ArrayList; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link TradeDiscountActivityPriceCalculator} 的单元测试类 + * + * @author 芋道源码 + */ +public class TradeDiscountActivityPriceCalculatorTest extends BaseMockitoUnitTest { + + @InjectMocks + private TradeDiscountActivityPriceCalculator tradeDiscountActivityPriceCalculator; + + @Mock + private DiscountActivityApi discountActivityApi; + + @Test + public void testCalculate() { + // 准备参数 + TradePriceCalculateReqBO param = new TradePriceCalculateReqBO() + .setItems(asList( + new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), // 匹配活动,且已选中 + new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(false) // 匹配活动,但未选中 + )); + TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() + .setPrice(new TradePriceCalculateRespBO.Price()) + .setPromotions(new ArrayList<>()) + .setItems(asList( + new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) + .setPrice(100), + new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(false) + .setPrice(50) + )); + // 保证价格被初始化上 + TradePriceCalculatorHelper.recountPayPrice(result.getItems()); + TradePriceCalculatorHelper.recountAllPrice(result); + + // mock 方法(限时折扣活动) + when(discountActivityApi.getMatchDiscountProductList(eq(asSet(10L, 20L)))).thenReturn(asList( + randomPojo(DiscountProductRespDTO.class, o -> o.setActivityId(1000L) + .setActivityName("活动 1000 号").setSkuId(10L) + .setDiscountType(PromotionDiscountTypeEnum.PRICE.getType()).setDiscountPrice(40)), + randomPojo(DiscountProductRespDTO.class, o -> o.setActivityId(2000L) + .setActivityName("活动 2000 号").setSkuId(20L) + .setDiscountType(PromotionDiscountTypeEnum.PERCENT.getType()).setDiscountPercent(60)) + )); + // 10L: 100 * 2 - 40 * 2 = 120 + // 20L:50 * 3 - 50 * 3 * 0.4 = 90 + + // 调用 + tradeDiscountActivityPriceCalculator.calculate(param, result); + // 断言:Price 部分 + TradePriceCalculateRespBO.Price price = result.getPrice(); + assertEquals(price.getTotalPrice(), 200); + assertEquals(price.getDiscountPrice(), 80); + assertEquals(price.getPointPrice(), 0); + assertEquals(price.getDeliveryPrice(), 0); + assertEquals(price.getCouponPrice(), 0); + assertEquals(price.getPayPrice(), 120); + assertNull(result.getCouponId()); + // 断言:SKU 1 + assertEquals(result.getItems().size(), 2); + TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0); + assertEquals(orderItem01.getSkuId(), 10L); + assertEquals(orderItem01.getCount(), 2); + assertEquals(orderItem01.getPrice(), 100); + assertEquals(orderItem01.getDiscountPrice(), 80); + assertEquals(orderItem01.getDeliveryPrice(), 0); + assertEquals(orderItem01.getCouponPrice(), 0); + assertEquals(orderItem01.getPointPrice(), 0); + assertEquals(orderItem01.getPayPrice(), 120); + // 断言:SKU 2 + TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1); + assertEquals(orderItem02.getSkuId(), 20L); + assertEquals(orderItem02.getCount(), 3); + assertEquals(orderItem02.getPrice(), 50); + assertEquals(orderItem02.getDiscountPrice(), 60); + assertEquals(orderItem02.getDeliveryPrice(), 0); + assertEquals(orderItem02.getCouponPrice(), 0); + assertEquals(orderItem02.getPointPrice(), 0); + assertEquals(orderItem02.getPayPrice(), 90); + // 断言:Promotion 部分 + assertEquals(result.getPromotions().size(), 1); + TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0); + assertEquals(promotion01.getId(), 1000L); + assertEquals(promotion01.getName(), "活动 1000 号"); + assertEquals(promotion01.getType(), PromotionTypeEnum.DISCOUNT_ACTIVITY.getType()); + assertEquals(promotion01.getTotalPrice(), 200); + assertEquals(promotion01.getDiscountPrice(), 80); + assertTrue(promotion01.getMatch()); + assertEquals(promotion01.getDescription(), "限时折扣:省 0.80 元"); + TradePriceCalculateRespBO.PromotionItem promotionItem01 = promotion01.getItems().get(0); + assertEquals(promotion01.getItems().size(), 1); + assertEquals(promotionItem01.getSkuId(), 10L); + assertEquals(promotionItem01.getTotalPrice(), 200); + assertEquals(promotionItem01.getDiscountPrice(), 80); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java new file mode 100644 index 0000000..a71771f --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/java/com/yunxi/scm/module/trade/service/price/calculator/TradeRewardActivityPriceCalculatorTest.java @@ -0,0 +1,232 @@ +package com.yunxi.scm.module.trade.service.price.calculator; + +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.promotion.api.reward.RewardActivityApi; +import com.yunxi.scm.module.promotion.api.reward.dto.RewardActivityMatchRespDTO; +import com.yunxi.scm.module.promotion.enums.common.PromotionConditionTypeEnum; +import com.yunxi.scm.module.promotion.enums.common.PromotionTypeEnum; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateReqBO; +import com.yunxi.scm.module.trade.service.price.bo.TradePriceCalculateRespBO; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.ArrayList; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link TradeRewardActivityPriceCalculator} 的单元测试类 + * + * @author 芋道源码 + */ +public class TradeRewardActivityPriceCalculatorTest extends BaseMockitoUnitTest { + + @InjectMocks + private TradeRewardActivityPriceCalculator tradeRewardActivityPriceCalculator; + + @Mock + private RewardActivityApi rewardActivityApi; + + @Test + public void testCalculate_match() { + // 准备参数 + TradePriceCalculateReqBO param = new TradePriceCalculateReqBO() + .setItems(asList( + new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), // 匹配活动 1 + new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(true), // 匹配活动 1 + new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(true) // 匹配活动 2 + )); + TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() + .setPrice(new TradePriceCalculateRespBO.Price()) + .setPromotions(new ArrayList<>()) + .setItems(asList( + new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) + .setPrice(100).setSpuId(1L), + new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(true) + .setPrice(50).setSpuId(2L), + new TradePriceCalculateRespBO.OrderItem().setSkuId(30L).setCount(4).setSelected(true) + .setPrice(30).setSpuId(3L) + )); + // 保证价格被初始化上 + TradePriceCalculatorHelper.recountPayPrice(result.getItems()); + TradePriceCalculatorHelper.recountAllPrice(result); + + // mock 方法(限时折扣 DiscountActivity 信息) + when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L, 3L)))).thenReturn(asList( + randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") + .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) + .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(200).setDiscountPrice(70)))), + randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(2000L).setName("活动 2000 号") + .setSpuIds(singletonList(3L)).setConditionType(PromotionConditionTypeEnum.COUNT.getType()) + .setRules(asList(new RewardActivityMatchRespDTO.Rule().setLimit(1).setDiscountPrice(10), + new RewardActivityMatchRespDTO.Rule().setLimit(2).setDiscountPrice(60), // 最大可满足,因为是 4 个 + new RewardActivityMatchRespDTO.Rule().setLimit(10).setDiscountPrice(100)))) + )); + + // 调用 + tradeRewardActivityPriceCalculator.calculate(param, result); + // 断言 Order 部分 + TradePriceCalculateRespBO.Price price = result.getPrice(); + assertEquals(price.getTotalPrice(), 470); + assertEquals(price.getDiscountPrice(), 130); + assertEquals(price.getPointPrice(), 0); + assertEquals(price.getDeliveryPrice(), 0); + assertEquals(price.getCouponPrice(), 0); + assertEquals(price.getPayPrice(), 340); + assertNull(result.getCouponId()); + // 断言:SKU 1 + assertEquals(result.getItems().size(), 3); + TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0); + assertEquals(orderItem01.getSkuId(), 10L); + assertEquals(orderItem01.getCount(), 2); + assertEquals(orderItem01.getPrice(), 100); + assertEquals(orderItem01.getDiscountPrice(), 40); + assertEquals(orderItem01.getDeliveryPrice(), 0); + assertEquals(orderItem01.getCouponPrice(), 0); + assertEquals(orderItem01.getPointPrice(), 0); + assertEquals(orderItem01.getPayPrice(), 160); + // 断言:SKU 2 + TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1); + assertEquals(orderItem02.getSkuId(), 20L); + assertEquals(orderItem02.getCount(), 3); + assertEquals(orderItem02.getPrice(), 50); + assertEquals(orderItem02.getDiscountPrice(), 30); + assertEquals(orderItem02.getDeliveryPrice(), 0); + assertEquals(orderItem02.getCouponPrice(), 0); + assertEquals(orderItem02.getPointPrice(), 0); + assertEquals(orderItem02.getPayPrice(), 120); + // 断言:SKU 3 + TradePriceCalculateRespBO.OrderItem orderItem03 = result.getItems().get(2); + assertEquals(orderItem03.getSkuId(), 30L); + assertEquals(orderItem03.getCount(), 4); + assertEquals(orderItem03.getPrice(), 30); + assertEquals(orderItem03.getDiscountPrice(), 60); + assertEquals(orderItem03.getDeliveryPrice(), 0); + assertEquals(orderItem03.getCouponPrice(), 0); + assertEquals(orderItem03.getPointPrice(), 0); + assertEquals(orderItem03.getPayPrice(), 60); + // 断言:Promotion 部分(第一个) + assertEquals(result.getPromotions().size(), 2); + TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0); + assertEquals(promotion01.getId(), 1000L); + assertEquals(promotion01.getName(), "活动 1000 号"); + assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType()); + assertEquals(promotion01.getTotalPrice(), 350); + assertEquals(promotion01.getDiscountPrice(), 70); + assertTrue(promotion01.getMatch()); + assertEquals(promotion01.getDescription(), "满减送:省 0.70 元"); + assertEquals(promotion01.getItems().size(), 2); + TradePriceCalculateRespBO.PromotionItem promotionItem011 = promotion01.getItems().get(0); + assertEquals(promotionItem011.getSkuId(), 10L); + assertEquals(promotionItem011.getTotalPrice(), 200); + assertEquals(promotionItem011.getDiscountPrice(), 40); + TradePriceCalculateRespBO.PromotionItem promotionItem012 = promotion01.getItems().get(1); + assertEquals(promotionItem012.getSkuId(), 20L); + assertEquals(promotionItem012.getTotalPrice(), 150); + assertEquals(promotionItem012.getDiscountPrice(), 30); + // 断言:Promotion 部分(第二个) + TradePriceCalculateRespBO.Promotion promotion02 = result.getPromotions().get(1); + assertEquals(promotion02.getId(), 2000L); + assertEquals(promotion02.getName(), "活动 2000 号"); + assertEquals(promotion02.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType()); + assertEquals(promotion02.getTotalPrice(), 120); + assertEquals(promotion02.getDiscountPrice(), 60); + assertTrue(promotion02.getMatch()); + assertEquals(promotion02.getDescription(), "满减送:省 0.60 元"); + TradePriceCalculateRespBO.PromotionItem promotionItem02 = promotion02.getItems().get(0); + assertEquals(promotion02.getItems().size(), 1); + assertEquals(promotionItem02.getSkuId(), 30L); + assertEquals(promotionItem02.getTotalPrice(), 120); + assertEquals(promotionItem02.getDiscountPrice(), 60); + } + + @Test + public void testCalculate_notMatch() { + // 准备参数 + TradePriceCalculateReqBO param = new TradePriceCalculateReqBO() + .setItems(asList( + new TradePriceCalculateReqBO.Item().setSkuId(10L).setCount(2).setSelected(true), + new TradePriceCalculateReqBO.Item().setSkuId(20L).setCount(3).setSelected(true), + new TradePriceCalculateReqBO.Item().setSkuId(30L).setCount(4).setSelected(true) + )); + TradePriceCalculateRespBO result = new TradePriceCalculateRespBO() + .setPrice(new TradePriceCalculateRespBO.Price()) + .setPromotions(new ArrayList<>()) + .setItems(asList( + new TradePriceCalculateRespBO.OrderItem().setSkuId(10L).setCount(2).setSelected(true) + .setPrice(100).setSpuId(1L), + new TradePriceCalculateRespBO.OrderItem().setSkuId(20L).setCount(3).setSelected(true) + .setPrice(50).setSpuId(2L) + )); + // 保证价格被初始化上 + TradePriceCalculatorHelper.recountPayPrice(result.getItems()); + TradePriceCalculatorHelper.recountAllPrice(result); + + // mock 方法(限时折扣 DiscountActivity 信息) + when(rewardActivityApi.getMatchRewardActivityList(eq(asSet(1L, 2L)))).thenReturn(singletonList( + randomPojo(RewardActivityMatchRespDTO.class, o -> o.setId(1000L).setName("活动 1000 号") + .setSpuIds(asList(1L, 2L)).setConditionType(PromotionConditionTypeEnum.PRICE.getType()) + .setRules(singletonList(new RewardActivityMatchRespDTO.Rule().setLimit(351).setDiscountPrice(70)))) + )); + + // 调用 + tradeRewardActivityPriceCalculator.calculate(param, result); + // 断言 Order 部分 + TradePriceCalculateRespBO.Price price = result.getPrice(); + assertEquals(price.getTotalPrice(), 350); + assertEquals(price.getDiscountPrice(), 0); + assertEquals(price.getPointPrice(), 0); + assertEquals(price.getDeliveryPrice(), 0); + assertEquals(price.getCouponPrice(), 0); + assertEquals(price.getPayPrice(), 350); + assertNull(result.getCouponId()); + // 断言:SKU 1 + assertEquals(result.getItems().size(), 2); + TradePriceCalculateRespBO.OrderItem orderItem01 = result.getItems().get(0); + assertEquals(orderItem01.getSkuId(), 10L); + assertEquals(orderItem01.getCount(), 2); + assertEquals(orderItem01.getPrice(), 100); + assertEquals(orderItem01.getDiscountPrice(), 0); + assertEquals(orderItem01.getDeliveryPrice(), 0); + assertEquals(orderItem01.getCouponPrice(), 0); + assertEquals(orderItem01.getPointPrice(), 0); + assertEquals(orderItem01.getPayPrice(), 200); + // 断言:SKU 2 + TradePriceCalculateRespBO.OrderItem orderItem02 = result.getItems().get(1); + assertEquals(orderItem02.getSkuId(), 20L); + assertEquals(orderItem02.getCount(), 3); + assertEquals(orderItem02.getPrice(), 50); + assertEquals(orderItem02.getDiscountPrice(), 0); + assertEquals(orderItem02.getDeliveryPrice(), 0); + assertEquals(orderItem02.getCouponPrice(), 0); + assertEquals(orderItem02.getPointPrice(), 0); + assertEquals(orderItem02.getPayPrice(), 150); + // 断言 Promotion 部分 + assertEquals(result.getPromotions().size(), 1); + TradePriceCalculateRespBO.Promotion promotion01 = result.getPromotions().get(0); + assertEquals(promotion01.getId(), 1000L); + assertEquals(promotion01.getName(), "活动 1000 号"); + assertEquals(promotion01.getType(), PromotionTypeEnum.REWARD_ACTIVITY.getType()); + assertEquals(promotion01.getTotalPrice(), 350); + assertEquals(promotion01.getDiscountPrice(), 0); + assertFalse(promotion01.getMatch()); + assertEquals(promotion01.getDescription(), "TODO"); // TODO 芋艿:后面再想想 + assertEquals(promotion01.getItems().size(), 2); + TradePriceCalculateRespBO.PromotionItem promotionItem011 = promotion01.getItems().get(0); + assertEquals(promotionItem011.getSkuId(), 10L); + assertEquals(promotionItem011.getTotalPrice(), 200); + assertEquals(promotionItem011.getDiscountPrice(), 0); + TradePriceCalculateRespBO.PromotionItem promotionItem012 = promotion01.getItems().get(1); + assertEquals(promotionItem012.getSkuId(), 20L); + assertEquals(promotionItem012.getTotalPrice(), 150); + assertEquals(promotionItem012.getDiscountPrice(), 0); + } + +} diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..589dad3 --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,61 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm.module + trade: + order: + app-id: 1 + merchant-order-id: 1 + express: + kd-niao: + api-key: xxxx + business-id: xxxxx + kd100: + customer: xxxxx + key: xxxxx + client: not_provide \ No newline at end of file diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/logback.xml b/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/sql/clean.sql b/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..dfa4a5b --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,4 @@ +DELETE FROM trade_order; +DELETE FROM trade_order_item; +DELETE FROM trade_after_sale; +DELETE FROM trade_after_sale_log; diff --git a/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..d4ce5cc --- /dev/null +++ b/yunxi-module-mall/yunxi-module-trade-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,128 @@ +CREATE TABLE IF NOT EXISTS "trade_order" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "no" varchar NOT NULL, + "type" int NOT NULL, + "terminal" int NOT NULL, + "user_id" bigint NOT NULL, + "user_ip" varchar NOT NULL, + "user_remark" varchar, + "status" int NOT NULL, + "product_count" int NOT NULL, + "cancel_type" int, + "remark" varchar, + "pay_status" bit NOT NULL, + "pay_time" datetime, + "finish_time" datetime, + "cancel_time" datetime, + "original_price" int NOT NULL, + "order_price" int NOT NULL, + "discount_price" int NOT NULL, + "delivery_price" int NOT NULL, + "adjust_price" int NOT NULL, + "pay_price" int NOT NULL, + "pay_order_id" bigint, + "pay_channel_code" varchar, + "delivery_template_id" bigint, + "logistics_id" bigint, + "logistics_no" varchar, + "delivery_status" smallint NOT NULL, + "delivery_time" datetime, + "receive_time" datetime, + "receiver_name" varchar NOT NULL, + "receiver_mobile" varchar NOT NULL, + "receiver_area_id" int NOT NULL, + "receiver_post_code" int, + "receiver_detail_address" varchar NOT NULL, + "after_sale_status" int NOT NULL, + "refund_price" int NOT NULL, + "coupon_id" bigint NOT NULL, + "coupon_price" int NOT NULL, + "point_price" int NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '交易订单表'; + +CREATE TABLE IF NOT EXISTS "trade_order_item" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "order_id" bigint NOT NULL, + "spu_id" bigint NOT NULL, + "spu_name" varchar NOT NULL, + "sku_id" bigint NOT NULL, + "properties" varchar, + "pic_url" varchar, + "count" int NOT NULL, + "original_price" int NOT NULL, + "original_unit_price" int NOT NULL, + "discount_price" int NOT NULL, + "pay_price" int NOT NULL, + "order_part_price" int NOT NULL, + "order_divide_price" int NOT NULL, + "after_sale_status" int NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '交易订单明细表'; + +CREATE TABLE IF NOT EXISTS "trade_after_sale" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "no" varchar NOT NULL, + "status" int NOT NULL, + "type" int NOT NULL, + "way" int NOT NULL, + "user_id" bigint NOT NULL, + "apply_reason" varchar NOT NULL, + "apply_description" varchar, + "apply_pic_urls" varchar, + "order_id" bigint NOT NULL, + "order_no" varchar NOT NULL, + "order_item_id" bigint NOT NULL, + "spu_id" bigint NOT NULL, + "spu_name" varchar NOT NULL, + "sku_id" bigint NOT NULL, + "properties" varchar, + "pic_url" varchar, + "count" int NOT NULL, + "audit_time" varchar, + "audit_user_id" bigint, + "audit_reason" varchar, + "refund_price" int NOT NULL, + "pay_refund_id" bigint, + "refund_time" varchar, + "logistics_id" bigint, + "logistics_no" varchar, + "delivery_time" varchar, + "receive_time" varchar, + "receive_reason" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '交易售后表'; + +CREATE TABLE IF NOT EXISTS "trade_after_sale_log" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" int NOT NULL, + "after_sale_id" bigint NOT NULL, + "order_id" bigint NOT NULL, + "order_item_id" bigint NOT NULL, + "before_status" int, + "after_status" int NOT NULL, + "content" varchar NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '交易售后日志'; diff --git a/yunxi-module-member/pom.xml b/yunxi-module-member/pom.xml new file mode 100644 index 0000000..a5d79bf --- /dev/null +++ b/yunxi-module-member/pom.xml @@ -0,0 +1,24 @@ + + + + com.yunxi.scm + yunxi + ${revision} + + 4.0.0 + + yunxi-module-member-api + yunxi-module-member-biz + + yunxi-module-member + pom + + ${project.artifactId} + + member 模块,我们放会员业务。 + 例如说:会员中心等等 + + + diff --git a/yunxi-module-member/yunxi-module-member-api/pom.xml b/yunxi-module-member/yunxi-module-member-api/pom.xml new file mode 100644 index 0000000..72ab8f1 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-api/pom.xml @@ -0,0 +1,26 @@ + + + + com.yunxi.scm + yunxi-module-member + ${revision} + + 4.0.0 + yunxi-module-member-api + jar + + ${project.artifactId} + + member 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + diff --git a/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/address/AddressApi.java b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/address/AddressApi.java new file mode 100644 index 0000000..8c9a612 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/address/AddressApi.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.member.api.address; + +import com.yunxi.scm.module.member.api.address.dto.AddressRespDTO; + +/** + * 用户收件地址 API 接口 + * + * @author 芋道源码 + */ +public interface AddressApi { + + /** + * 获得用户收件地址 + * + * @param id 收件地址编号 + * @param userId 用户编号 + * @return 用户收件地址 + */ + AddressRespDTO getAddress(Long id, Long userId); + + /** + * 获得用户默认收件地址 + * + * @param userId 用户编号 + * @return 用户收件地址 + */ + AddressRespDTO getDefaultAddress(Long userId); + +} diff --git a/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/address/dto/AddressRespDTO.java b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/address/dto/AddressRespDTO.java new file mode 100644 index 0000000..c34dd33 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/address/dto/AddressRespDTO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.member.api.address.dto; + +import lombok.Data; + +/** + * 用户收件地址 Response DTO + * + * @author 芋道源码 + */ +@Data +public class AddressRespDTO { + + /** + * 编号 + */ + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 收件人名称 + */ + private String name; + /** + * 手机号 + */ + private String mobile; + /** + * 地区编号 + */ + private Integer areaId; + /** + * 收件详细地址 + */ + private String detailAddress; + /** + * 是否默认 + */ + private Boolean defaultStatus; + +} diff --git a/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/package-info.java b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/package-info.java new file mode 100644 index 0000000..a85b1e2 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/package-info.java @@ -0,0 +1,4 @@ +/** + * member API 包,定义暴露给其它模块的 API + */ +package com.yunxi.scm.module.member.api; diff --git a/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/user/MemberUserApi.java b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/user/MemberUserApi.java new file mode 100644 index 0000000..fe29200 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/user/MemberUserApi.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.member.api.user; + +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; + +/** + * 会员用户的 API 接口 + * + * @author 芋道源码 + */ +public interface MemberUserApi { + + /** + * 获得会员用户信息 + * + * @param id 用户编号 + * @return 用户信息 + */ + MemberUserRespDTO getUser(Long id); + + /** + * 获得会员用户信息们 + * + * @param ids 用户编号的数组 + * @return 用户信息们 + */ + List getUsers(Collection ids); + + /** + * 获得会员用户 Map + * + * @param ids 用户编号的数组 + * @return 会员用户 Map + */ + default Map getUserMap(Collection ids) { + return convertMap(getUsers(ids), MemberUserRespDTO::getId); + } + + /** + * 基于用户昵称,模糊匹配用户列表 + * + * @param nickname 用户昵称,模糊匹配 + * @return 用户信息的列表 + */ + List getUserListByNickname(String nickname); + + /** + * 基于手机号,精准匹配用户 + * + * @param mobile 手机号 + * @return 用户信息 + */ + MemberUserRespDTO getUserByMobile(String mobile); + +} diff --git a/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/user/dto/MemberUserRespDTO.java b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/user/dto/MemberUserRespDTO.java new file mode 100644 index 0000000..5e1bbb7 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/api/user/dto/MemberUserRespDTO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.member.api.user.dto; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 用户信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class MemberUserRespDTO { + + /** + * 用户ID + */ + private Long id; + /** + * 用户昵称 + */ + private String nickname; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 用户头像 + */ + private String avatar; + /** + * 手机 + */ + private String mobile; + +} diff --git a/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/enums/ErrorCodeConstants.java b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..f3e7e3d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-api/src/main/java/com/yunxi/scm/module/member/enums/ErrorCodeConstants.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.member.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Member 错误码枚举类 + * + * member 系统,使用 1-004-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 用户相关 1004001000============ + ErrorCode USER_NOT_EXISTS = new ErrorCode(1004001000, "用户不存在"); + ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1004001001, "密码校验失败"); + + // ========== AUTH 模块 1004003000 ========== + ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1004003000, "登录失败,账号密码不正确"); + ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1004003001, "登录失败,账号被禁用"); + ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1004003004, "Token 已经过期"); + ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1004003005, "未绑定账号,需要进行绑定"); + ErrorCode AUTH_WEIXIN_MINI_APP_PHONE_CODE_ERROR = new ErrorCode(1004003006, "获得手机号失败"); + + // ========== 用户收件地址 1004004000 ========== + ErrorCode ADDRESS_NOT_EXISTS = new ErrorCode(1004004000, "用户收件地址不存在"); + + //========== 用户积分 1004005000 ========== + + // TODO @xiaqing:错误码要分段;例如说这里,积分配置、积分记录、签到配置、签到记录;分成 4 段; + + ErrorCode SIGN_IN_CONFIG_NOT_EXISTS = new ErrorCode(1004005003, "签到天数规则不存在"); + ErrorCode SIGN_IN_CONFIG_EXISTS = new ErrorCode(1004005004, "签到天数规则已存在"); + + ErrorCode RECORD_NOT_EXISTS = new ErrorCode( 1004005005, "用户积分记录不存在"); + + ErrorCode SIGN_IN_RECORD_NOT_EXISTS = new ErrorCode(1004005006, "用户签到积分不存在"); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/pom.xml b/yunxi-module-member/yunxi-module-member-biz/pom.xml new file mode 100644 index 0000000..c38b202 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/pom.xml @@ -0,0 +1,99 @@ + + + + com.yunxi.scm + yunxi-module-member + ${revision} + + 4.0.0 + yunxi-module-member-biz + jar + + ${project.artifactId} + + member 模块,我们放会员业务。 + 例如说:会员中心等等 + + + + + com.yunxi.scm + yunxi-module-member-api + ${revision} + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + com.yunxi.scm + yunxi-module-infra-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-tenant + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-weixin + + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mq + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-ip + + + + + diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/address/AddressApiImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/address/AddressApiImpl.java new file mode 100644 index 0000000..6d204aa --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/address/AddressApiImpl.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.member.api.address; + +import com.yunxi.scm.module.member.api.address.dto.AddressRespDTO; +import com.yunxi.scm.module.member.convert.address.AddressConvert; +import com.yunxi.scm.module.member.service.address.AddressService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 用户收件地址 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class AddressApiImpl implements AddressApi { + + @Resource + private AddressService addressService; + + @Override + public AddressRespDTO getAddress(Long id, Long userId) { + return AddressConvert.INSTANCE.convert02(addressService.getAddress(userId, id)); + } + + @Override + public AddressRespDTO getDefaultAddress(Long userId) { + return AddressConvert.INSTANCE.convert02(addressService.getDefaultUserAddress(userId)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/package-info.java new file mode 100644 index 0000000..85fb15c --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.member.api; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/user/MemberUserApiImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/user/MemberUserApiImpl.java new file mode 100644 index 0000000..05f8934 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/api/user/MemberUserApiImpl.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.member.api.user; + +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.member.convert.user.UserConvert; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; +import com.yunxi.scm.module.member.service.user.MemberUserService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * 会员用户的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MemberUserApiImpl implements MemberUserApi { + + @Resource + private MemberUserService userService; + + @Override + public MemberUserRespDTO getUser(Long id) { + MemberUserDO user = userService.getUser(id); + return UserConvert.INSTANCE.convert2(user); + } + + @Override + public List getUsers(Collection ids) { + return UserConvert.INSTANCE.convertList2(userService.getUserList(ids)); + } + + @Override + public List getUserListByNickname(String nickname) { + return UserConvert.INSTANCE.convertList2(userService.getUserListByNickname(nickname)); + } + + @Override + public MemberUserRespDTO getUserByMobile(String mobile) { + return UserConvert.INSTANCE.convert2(userService.getUserByMobile(mobile)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/address/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/address/package-info.java new file mode 100644 index 0000000..e2c112d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/address/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.member.controller.admin.address; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/MemberPointConfigController.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/MemberPointConfigController.java new file mode 100644 index 0000000..e16cfd9 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/MemberPointConfigController.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.member.controller.admin.point; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.member.controller.admin.point.vo.config.MemberPointConfigRespVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.config.MemberPointConfigSaveReqVO; +import com.yunxi.scm.module.member.convert.point.MemberPointConfigConvert; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointConfigDO; +import com.yunxi.scm.module.member.service.point.MemberPointConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 会员积分设置") +@RestController +@RequestMapping("/point/config") +@Validated +public class MemberPointConfigController { + + @Resource + private MemberPointConfigService memberPointConfigService; + + @PutMapping("/save") + @Operation(summary = "保存会员积分配置") + @PreAuthorize("@ss.hasPermission('point:config:save')") + public CommonResult updateConfig(@Valid @RequestBody MemberPointConfigSaveReqVO saveReqVO) { + memberPointConfigService.saveConfig(saveReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得会员积分配置") + @PreAuthorize("@ss.hasPermission('point:config:query')") + public CommonResult getConfig() { + MemberPointConfigDO config = memberPointConfigService.getConfig(); + return success(MemberPointConfigConvert.INSTANCE.convert(config)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/MemberPointRecordController.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/MemberPointRecordController.java new file mode 100644 index 0000000..f9c6b04 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/MemberPointRecordController.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.member.controller.admin.point; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordPageReqVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordRespVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordUpdateReqVO; +import com.yunxi.scm.module.member.convert.point.MemberPointRecordConvert; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointRecordDO; +import com.yunxi.scm.module.member.service.point.MemberPointRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 用户积分记录") +@RestController +@RequestMapping("/point/record") +@Validated +public class MemberPointRecordController { + + @Resource + private MemberPointRecordService recordService; + + // TODO @xiaqing:积分应该没有更新操作呀?可以删除哈; + @PutMapping("/update") + @Operation(summary = "更新用户积分记录") + @PreAuthorize("@ss.hasPermission('point:record:update')") + public CommonResult updateRecord(@Valid @RequestBody MemberPointRecordUpdateReqVO updateReqVO) { + recordService.updateRecord(updateReqVO); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得用户积分记录") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('point:record:query')") + public CommonResult getRecord(@RequestParam("id") Long id) { + MemberPointRecordDO record = recordService.getRecord(id); + return success(MemberPointRecordConvert.INSTANCE.convert(record)); + } + + @GetMapping("/page") + @Operation(summary = "获得用户积分记录分页") + @PreAuthorize("@ss.hasPermission('point:record:query')") + public CommonResult> getRecordPage(@Valid MemberPointRecordPageReqVO pageVO) { + PageResult pageResult = recordService.getRecordPage(pageVO); + return success(MemberPointRecordConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigBaseVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigBaseVO.java new file mode 100644 index 0000000..6905a4a --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigBaseVO.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 会员积分配置 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberPointConfigBaseVO { + + private Long id; + + @Schema(description = "积分抵扣开关", required = true, example = "true") + private Boolean tradeDeductEnable; + + @Schema(description = "积分抵扣,单位:分", example = "13506") + private BigDecimal tradeDeductUnitPrice; + + @Schema(description = "积分抵扣最大值", example = "32428") + private Long tradeDeductMaxPrice; + + @Schema(description = "1 元赠送多少分") + private Long tradeGivePoint; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigRespVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigRespVO.java new file mode 100644 index 0000000..1bcad26 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigRespVO.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 会员积分配置 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberPointConfigRespVO extends MemberPointConfigBaseVO { +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigSaveReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigSaveReqVO.java new file mode 100644 index 0000000..794ea96 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/config/MemberPointConfigSaveReqVO.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.config; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 会员积分配置保存 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberPointConfigSaveReqVO extends MemberPointConfigBaseVO { +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordBaseVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordBaseVO.java new file mode 100644 index 0000000..8a9864a --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordBaseVO.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.recrod; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; +import javax.validation.constraints.*; +import org.springframework.format.annotation.DateTimeFormat; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 用户积分记录 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberPointRecordBaseVO { + + @Schema(description = "业务编码", example = "22706") + @NotNull(message = "业务编码不能为空") + private String bizId; + + @Schema(description = "业务类型", example = "1") + @NotNull(message = "业务类型不能为空") + private String bizType; + + @Schema(description = "1增加 0扣减", example = "1") + @NotNull(message = "操作类型不能为空") + private String type; + + @Schema(description = "积分标题") + @NotNull(message = "积分标题不能为空") + private String title; + + @Schema(description = "积分描述", example = "你猜") + private String description; + + @Schema(description = "积分") + @NotNull(message = "操作积分不能为空") + private Integer point; + + @Schema(description = "变动后的积分", requiredMode = Schema.RequiredMode.REQUIRED) +// @NotNull(message = "变动后的积分不能为空") + private Integer totalPoint; + + @Schema(description = "状态:1-订单创建,2-冻结期,3-完成,4-失效(订单退款) ", example = "1") + @NotNull(message = "积分状态不能为空") + private Integer status; + + @Schema(description = "用户id", example = "31169") + @NotNull(message = "用户ID不能为空") + private Integer userId; + + @Schema(description = "冻结时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @NotNull(message = "冻结时间不能为空") + private LocalDateTime freezingTime; + + @Schema(description = "解冻时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @NotNull(message = "解冻时间不能为空") + private LocalDateTime thawingTime; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordCreateReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordCreateReqVO.java new file mode 100644 index 0000000..4147e6a --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordCreateReqVO.java @@ -0,0 +1,12 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.recrod; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "管理后台 - 用户积分记录创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberPointRecordCreateReqVO extends MemberPointRecordBaseVO { + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordExportReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordExportReqVO.java new file mode 100644 index 0000000..e9bb0c0 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordExportReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.recrod; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "管理后台 - 用户积分记录 Excel 导出 Request VO,参数和 PointRecordPageReqVO 是一致的") +@Data +public class MemberPointRecordExportReqVO { + + @Schema(description = "业务编码", example = "22706") + private String bizId; + + @Schema(description = "业务类型", example = "1") + private String bizType; + + @Schema(description = "1增加 0扣减", example = "1") + private String type; + + @Schema(description = "积分标题") + private String title; + + @Schema(description = "状态:1-订单创建,2-冻结期,3-完成,4-失效(订单退款) ", example = "1") + private Integer status; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordPageReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordPageReqVO.java new file mode 100644 index 0000000..1d34029 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordPageReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.recrod; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import com.yunxi.scm.framework.common.pojo.PageParam; + +@Schema(description = "管理后台 - 用户积分记录分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberPointRecordPageReqVO extends PageParam { + + @Schema(description = "业务编码", example = "22706") + private String bizId; + + @Schema(description = "业务类型", example = "1") + private String bizType; + + @Schema(description = "1增加 0扣减", example = "1") + private String type; + + @Schema(description = "积分标题") + private String title; + + @Schema(description = "状态:1-订单创建,2-冻结期,3-完成,4-失效(订单退款) ", example = "1") + private Integer status; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordRespVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordRespVO.java new file mode 100644 index 0000000..443cf54 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.recrod; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户积分记录 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberPointRecordRespVO extends MemberPointRecordBaseVO { + + @Schema(description = "自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "31457") + private Long id; + + @Schema(description = "发生时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordUpdateReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordUpdateReqVO.java new file mode 100644 index 0000000..ffaf3fb --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/point/vo/recrod/MemberPointRecordUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.member.controller.admin.point.vo.recrod; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 用户积分记录更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberPointRecordUpdateReqVO extends MemberPointRecordBaseVO { + + @Schema(description = "自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "31457") + @NotNull(message = "自增主键不能为空") + private Long id; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/MemberSignInConfigController.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/MemberSignInConfigController.java new file mode 100644 index 0000000..12635cf --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/MemberSignInConfigController.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.member.controller.admin.signin; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigCreateReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigPageReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigRespVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigUpdateReqVO; +import com.yunxi.scm.module.member.convert.signin.MemberSignInConfigConvert; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import com.yunxi.scm.module.member.service.signin.MemberSignInConfigService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 积分签到规则") +@RestController +@RequestMapping("/point/sign-in-config") +@Validated +public class MemberSignInConfigController { + + @Resource + private MemberSignInConfigService memberSignInConfigService; + + @PostMapping("/create") + @Operation(summary = "创建积分签到规则") + @PreAuthorize("@ss.hasPermission('point:sign-in-config:create')") + public CommonResult createSignInConfig(@Valid @RequestBody MemberSignInConfigCreateReqVO createReqVO) { + return success(memberSignInConfigService.createSignInConfig(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新积分签到规则") + @PreAuthorize("@ss.hasPermission('point:sign-in-config:update')") + public CommonResult updateSignInConfig(@Valid @RequestBody MemberSignInConfigUpdateReqVO updateReqVO) { + memberSignInConfigService.updateSignInConfig(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除积分签到规则") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('point:sign-in-config:delete')") + public CommonResult deleteSignInConfig(@RequestParam("id") Integer id) { + memberSignInConfigService.deleteSignInConfig(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得积分签到规则") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('point:sign-in-config:query')") + public CommonResult getSignInConfig(@RequestParam("id") Integer id) { + MemberSignInConfigDO signInConfig = memberSignInConfigService.getSignInConfig(id); + return success(MemberSignInConfigConvert.INSTANCE.convert(signInConfig)); + } + + @GetMapping("/page") + @Operation(summary = "获得积分签到规则分页") + @PreAuthorize("@ss.hasPermission('point:sign-in-config:query')") + public CommonResult> getSignInConfigPage(@Valid MemberSignInConfigPageReqVO pageVO) { + PageResult pageResult = memberSignInConfigService.getSignInConfigPage(pageVO); + return success(MemberSignInConfigConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/MemberSignInRecordController.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/MemberSignInRecordController.java new file mode 100644 index 0000000..acdb642 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/MemberSignInRecordController.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.member.controller.admin.signin; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInRecordPageReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInRecordRespVO; +import com.yunxi.scm.module.member.convert.signin.MemberSignInRecordConvert; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import com.yunxi.scm.module.member.service.signin.MemberSignInRecordService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 用户签到积分") +@RestController +@RequestMapping("/point/sign-in-record") +@Validated +public class MemberSignInRecordController { + + @Resource + private MemberSignInRecordService memberSignInRecordService; + + // TODO @xiaqing:签到是不是不用删除? + @DeleteMapping("/delete") + @Operation(summary = "删除用户签到积分") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('point:sign-in-record:delete')") + public CommonResult deleteSignInRecord(@RequestParam("id") Long id) { + memberSignInRecordService.deleteSignInRecord(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得用户签到积分") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('point:sign-in-record:query')") + public CommonResult getSignInRecord(@RequestParam("id") Long id) { + MemberSignInRecordDO signInRecord = memberSignInRecordService.getSignInRecord(id); + return success(MemberSignInRecordConvert.INSTANCE.convert(signInRecord)); + } + + @GetMapping("/page") + @Operation(summary = "获得用户签到积分分页") + @PreAuthorize("@ss.hasPermission('point:sign-in-record:query')") + public CommonResult> getSignInRecordPage(@Valid MemberSignInRecordPageReqVO pageVO) { + PageResult pageResult = memberSignInRecordService.getSignInRecordPage(pageVO); + return success(MemberSignInRecordConvert.INSTANCE.convertPage(pageResult)); + } +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigBaseVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigBaseVO.java new file mode 100644 index 0000000..977fdab --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigBaseVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +/** + * 积分签到规则 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberSignInConfigBaseVO { + + @Schema(description = "签到第 x 天", example = "7") + private Integer day; + + @Schema(description = "签到天数对应分数", example = "10") + private Integer point; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigCreateReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigCreateReqVO.java new file mode 100644 index 0000000..eed7b22 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigCreateReqVO.java @@ -0,0 +1,12 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "管理后台 - 积分签到规则创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInConfigCreateReqVO extends MemberSignInConfigBaseVO { + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigPageReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigPageReqVO.java new file mode 100644 index 0000000..987802e --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigPageReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import com.yunxi.scm.framework.common.pojo.PageParam; + +@Schema(description = "管理后台 - 积分签到规则分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInConfigPageReqVO extends PageParam { + + @Schema(description = "签到第x天", example = "7") + private Integer day; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigRespVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigRespVO.java new file mode 100644 index 0000000..d7a267f --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 积分签到规则 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInConfigRespVO extends MemberSignInConfigBaseVO { + + @Schema(description = "自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "20937") + private Integer id; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigUpdateReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigUpdateReqVO.java new file mode 100644 index 0000000..e6f4225 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInConfigUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 积分签到规则更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInConfigUpdateReqVO extends MemberSignInConfigBaseVO { + + @Schema(description = "规则自增主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "13653") + @NotNull(message = "规则自增主键不能为空") + private Integer id; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordBaseVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordBaseVO.java new file mode 100644 index 0000000..3d6db78 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordBaseVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +/** + * 用户签到积分 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MemberSignInRecordBaseVO { + + @Schema(description = "签到用户", example = "6507") + private Integer userId; + + @Schema(description = "第几天签到") + private Integer day; + + @Schema(description = "签到的分数") + private Integer point; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordCreateReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordCreateReqVO.java new file mode 100644 index 0000000..64a754d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 用户签到积分创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInRecordCreateReqVO extends MemberSignInRecordBaseVO { + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordPageReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordPageReqVO.java new file mode 100644 index 0000000..a5c40ef --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordPageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 用户签到积分分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInRecordPageReqVO extends PageParam { + + @Schema(description = "签到用户", example = "6507") + private Integer userId; + + @Schema(description = "第几天签到") + private Integer day; + + @Schema(description = "签到时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordRespVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordRespVO.java new file mode 100644 index 0000000..c3059a9 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 用户签到积分 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInRecordRespVO extends MemberSignInRecordBaseVO { + + @Schema(description = "签到自增id", requiredMode = Schema.RequiredMode.REQUIRED, example = "11903") + private Long id; + + @Schema(description = "签到时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordUpdateReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordUpdateReqVO.java new file mode 100644 index 0000000..6d03b5f --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/signin/vo/MemberSignInRecordUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.member.controller.admin.signin.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户签到积分更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MemberSignInRecordUpdateReqVO extends MemberSignInRecordBaseVO { + + @Schema(description = "签到自增id", requiredMode = Schema.RequiredMode.REQUIRED, example = "11903") + @NotNull(message = "签到自增id不能为空") + private Long id; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/user/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/user/package-info.java new file mode 100644 index 0000000..0f6970d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/admin/user/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.member.controller.admin.user; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/AppAddressController.http b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/AppAddressController.http new file mode 100644 index 0000000..6bae7c7 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/AppAddressController.http @@ -0,0 +1,54 @@ +### 请求 /create 接口 => 成功 +POST {{appApi}}//member/address/create +Content-Type: application/json +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} + +{ + "name": "yunai", + "mobile": "15601691300", + "areaId": "610632", + "postCode": "200000", + "detailAddress": "芋道源码 233 号 666 室", + "defaulted": true +} + +### 请求 /update 接口 => 成功 +PUT {{appApi}}//member/address/update +Content-Type: application/json +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} + +{ + "id": "1", + "name": "yunai888", + "mobile": "15601691300", + "areaId": "610632", + "postCode": "200000", + "detailAddress": "芋道源码 233 号 666 室", + "defaulted": false +} + +### 请求 /delete 接口 => 成功 +DELETE {{appApi}}//member/address/delete?id=2 +Content-Type: application/json +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} + +### 请求 /get 接口 => 成功 +GET {{appApi}}//member/address/get?id=1 +Content-Type: application/json +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} + +### 请求 /get-default 接口 => 成功 +GET {{appApi}}//member/address/get-default +Content-Type: application/json +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} + +### 请求 /list 接口 => 成功 +GET {{appApi}}//member/address/list +Content-Type: application/json +tenant-id: {{appTenentId}} +Authorization: Bearer {{appToken}} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/AppAddressController.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/AppAddressController.java new file mode 100644 index 0000000..e471d47 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/AppAddressController.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.member.controller.app.address; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressRespVO; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import com.yunxi.scm.module.member.convert.address.AddressConvert; +import com.yunxi.scm.module.member.dal.dataobject.address.MemberAddressDO; +import com.yunxi.scm.module.member.service.address.AddressService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 用户收件地址") +@RestController +@RequestMapping("/member/address") +@Validated +public class AppAddressController { + + @Resource + private AddressService addressService; + + @PostMapping("/create") + @Operation(summary = "创建用户收件地址") + public CommonResult createAddress(@Valid @RequestBody AppAddressCreateReqVO createReqVO) { + return success(addressService.createAddress(getLoginUserId(), createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新用户收件地址") + public CommonResult updateAddress(@Valid @RequestBody AppAddressUpdateReqVO updateReqVO) { + addressService.updateAddress(getLoginUserId(), updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户收件地址") + @Parameter(name = "id", description = "编号", required = true) + public CommonResult deleteAddress(@RequestParam("id") Long id) { + addressService.deleteAddress(getLoginUserId(), id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得用户收件地址") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getAddress(@RequestParam("id") Long id) { + MemberAddressDO address = addressService.getAddress(getLoginUserId(), id); + return success(AddressConvert.INSTANCE.convert(address)); + } + + @GetMapping("/get-default") + @Operation(summary = "获得默认的用户收件地址") + public CommonResult getDefaultUserAddress() { + MemberAddressDO address = addressService.getDefaultUserAddress(getLoginUserId()); + return success(AddressConvert.INSTANCE.convert(address)); + } + + @GetMapping("/list") + @Operation(summary = "获得用户收件地址列表") + public CommonResult> getAddressList() { + List list = addressService.getAddressList(getLoginUserId()); + return success(AddressConvert.INSTANCE.convertList(list)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressBaseVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressBaseVO.java new file mode 100644 index 0000000..d07411b --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressBaseVO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.member.controller.app.address.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +// TODO 芋艿:example 缺失 +/** +* 用户收件地址 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class AppAddressBaseVO { + + @Schema(description = "收件人名称", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "收件人名称不能为空") + private String name; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "手机号不能为空") + private String mobile; + + @Schema(description = "地区编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "地区编号不能为空") + private Long areaId; + + @Schema(description = "收件详细地址", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "收件详细地址不能为空") + private String detailAddress; + + @Schema(description = "是否默认地址", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "是否默认地址不能为空") + private Boolean defaultStatus; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressCreateReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressCreateReqVO.java new file mode 100644 index 0000000..52636fa --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressCreateReqVO.java @@ -0,0 +1,11 @@ +package com.yunxi.scm.module.member.controller.app.address.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "用户 APP - 用户收件地址创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppAddressCreateReqVO extends AppAddressBaseVO { + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressRespVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressRespVO.java new file mode 100644 index 0000000..231259c --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.member.controller.app.address.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "用户 APP - 用户收件地址 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppAddressRespVO extends AppAddressBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "地区名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "上海上海市普陀区") + private String areaName; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressUpdateReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressUpdateReqVO.java new file mode 100644 index 0000000..cd491bd --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/address/vo/AppAddressUpdateReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.member.controller.app.address.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "用户 APP - 用户收件地址更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class AppAddressUpdateReqVO extends AppAddressBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/AppAuthController.http b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/AppAuthController.http new file mode 100644 index 0000000..5125253 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/AppAuthController.http @@ -0,0 +1,51 @@ +### 请求 /login 接口 => 成功 +POST {{appApi}}/member/auth/login +Content-Type: application/json +tenant-id: {{appTenentId}} + +{ + "mobile": "15601691300", + "password": "admin123" +} + +### 请求 /send-sms-code 接口 => 成功 +POST {{appApi}}/member/auth/send-sms-code +Content-Type: application/json +tenant-id: {{appTenentId}} + +{ + "mobile": "15601691399", + "scene": 1 +} + +### 请求 /sms-login 接口 => 成功 +POST {{appApi}}/member/auth/sms-login +Content-Type: application/json +tenant-id: {{appTenentId}} + +{ + "mobile": "15601691301", + "code": 9999 +} + +### 请求 /weixin-mini-app-login 接口 => 成功 +POST {{appApi}}/member/auth/weixin-mini-app-login +Content-Type: application/json +tenant-id: {{appTenentId}} + +{ + "phoneCode": "618e6412e0c728f5b8fc7164497463d0158a923c9e7fd86af8bba393b9decbc5", + "loginCode": "001frTkl21JUf94VGxol2hSlff1frTkR" +} + + +### 请求 /logout 接口 => 成功 +POST {{appApi}}/member/auth/logout +Content-Type: application/json +Authorization: Bearer c1b76bdaf2c146c581caa4d7fd81ee66 +tenant-id: {{appTenentId}} + +### 请求 /auth/refresh-token 接口 => 成功 +POST {{appApi}}/member/auth/refresh-token?refreshToken=bc43d929094849a28b3a69f6e6940d70 +Content-Type: application/json +tenant-id: {{appTenentId}} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/AppAuthController.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/AppAuthController.java new file mode 100644 index 0000000..f084c1b --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/AppAuthController.java @@ -0,0 +1,121 @@ +package com.yunxi.scm.module.member.controller.app.auth; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.framework.security.config.SecurityProperties; +import com.yunxi.scm.framework.security.core.annotations.PreAuthenticated; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.module.member.controller.app.auth.vo.*; +import com.yunxi.scm.module.member.service.auth.MemberAuthService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 APP - 认证") +@RestController +@RequestMapping("/member/auth") +@Validated +@Slf4j +public class AppAuthController { + + @Resource + private MemberAuthService authService; + + @Resource + private SecurityProperties securityProperties; + + @PostMapping("/login") + @Operation(summary = "使用手机 + 密码登录") + public CommonResult login(@RequestBody @Valid AppAuthLoginReqVO reqVO) { + return success(authService.login(reqVO)); + } + + @PostMapping("/logout") + @PermitAll + @Operation(summary = "登出系统") + public CommonResult logout(HttpServletRequest request) { + String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotBlank(token)) { + authService.logout(token); + } + return success(true); + } + + @PostMapping("/refresh-token") + @Operation(summary = "刷新令牌") + @Parameter(name = "refreshToken", description = "刷新令牌", required = true) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return success(authService.refreshToken(refreshToken)); + } + + // ========== 短信登录相关 ========== + + @PostMapping("/sms-login") + @Operation(summary = "使用手机 + 验证码登录") + public CommonResult smsLogin(@RequestBody @Valid AppAuthSmsLoginReqVO reqVO) { + return success(authService.smsLogin(reqVO)); + } + + @PostMapping("/send-sms-code") + @Operation(summary = "发送手机验证码") + public CommonResult sendSmsCode(@RequestBody @Valid AppAuthSmsSendReqVO reqVO) { + authService.sendSmsCode(getLoginUserId(), reqVO); + return success(true); + } + + @PostMapping("/reset-password") + @Operation(summary = "重置密码", description = "用户忘记密码时使用") + @PreAuthenticated + public CommonResult resetPassword(@RequestBody @Valid AppAuthResetPasswordReqVO reqVO) { + authService.resetPassword(reqVO); + return success(true); + } + + @PostMapping("/update-password") + @Operation(summary = "修改用户密码", description = "用户修改密码时使用") + @PreAuthenticated + public CommonResult updatePassword(@RequestBody @Valid AppAuthUpdatePasswordReqVO reqVO) { + authService.updatePassword(getLoginUserId(), reqVO); + return success(true); + } + + // ========== 社交登录相关 ========== + + @GetMapping("/social-auth-redirect") + @Operation(summary = "社交授权的跳转") + @Parameters({ + @Parameter(name = "type", description = "社交类型", required = true), + @Parameter(name = "redirectUri", description = "回调路径") + }) + public CommonResult socialAuthRedirect(@RequestParam("type") Integer type, + @RequestParam("redirectUri") String redirectUri) { + return CommonResult.success(authService.getSocialAuthorizeUrl(type, redirectUri)); + } + + @PostMapping("/social-login") + @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") + public CommonResult socialLogin(@RequestBody @Valid AppAuthSocialLoginReqVO reqVO) { + return success(authService.socialLogin(reqVO)); + } + + @PostMapping("/weixin-mini-app-login") + @Operation(summary = "微信小程序的一键登录") + public CommonResult weixinMiniAppLogin(@RequestBody @Valid AppAuthWeixinMiniAppLoginReqVO reqVO) { + return success(authService.weixinMiniAppLogin(reqVO)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthCheckCodeReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthCheckCodeReqVO.java new file mode 100644 index 0000000..4b58d5b --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthCheckCodeReqVO.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; + +// TODO 芋艿:code review 相关逻辑 +@Schema(description = "用户 APP - 校验验证码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthCheckCodeReqVO { + + @Schema(description = "手机号", example = "15601691234") + @NotBlank(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotBlank(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + @Schema(description = "发送场景,对应 SmsSceneEnum 枚举", example = "1") + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthLoginReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthLoginReqVO.java new file mode 100644 index 0000000..a801acb --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthLoginReqVO.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; + +@Schema(description = "用户 APP - 手机 + 密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthLoginReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + // ========== 绑定社交登录时,需要传递如下参数 ========== + + @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + private Integer socialType; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String socialCode; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + private String socialState; + + @AssertTrue(message = "授权码不能为空") + public boolean isSocialCodeValid() { + return socialType == null || StrUtil.isNotEmpty(socialCode); + } + + @AssertTrue(message = "授权 state 不能为空") + public boolean isSocialState() { + return socialType == null || StrUtil.isNotEmpty(socialState); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java new file mode 100644 index 0000000..844b08c --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthLoginRespVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "用户 APP - 登录 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthLoginRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "happy") + private String accessToken; + + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + private String refreshToken; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime expiresTime; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthResetPasswordReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthResetPasswordReqVO.java new file mode 100644 index 0000000..09a69e8 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthResetPasswordReqVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import com.yunxi.scm.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +// TODO 芋艿:code review 相关逻辑 +@Schema(description = "用户 APP - 重置密码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthResetPasswordReqVO { + + @Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "新密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + @Schema(description = "手机号",requiredMode = Schema.RequiredMode.REQUIRED,example = "15878962356") + @NotBlank(message = "手机号不能为空") + @Mobile + private String mobile; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSmsLoginReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSmsLoginReqVO.java new file mode 100644 index 0000000..edcff10 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSmsLoginReqVO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@Schema(description = "用户 APP - 手机 + 验证码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthSmsLoginReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + // ========== 绑定社交登录时,需要传递如下参数 ========== + + @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + private Integer socialType; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String socialCode; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + private String socialState; + + @AssertTrue(message = "授权码不能为空") + public boolean isSocialCodeValid() { + return socialType == null || StrUtil.isNotEmpty(socialCode); + } + + @AssertTrue(message = "授权 state 不能为空") + public boolean isSocialState() { + return socialType == null || StrUtil.isNotEmpty(socialState); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java new file mode 100644 index 0000000..f1e02fa --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSmsSendReqVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 APP - 发送手机验证码 Request VO") +@Data +@Accessors(chain = true) +public class AppAuthSmsSendReqVO { + + @Schema(description = "手机号", example = "15601691234") + @Mobile + private String mobile; + + @Schema(description = "发送场景,对应 SmsSceneEnum 枚举", example = "1") + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSocialLoginReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSocialLoginReqVO.java new file mode 100644 index 0000000..4a0091e --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthSocialLoginReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 APP - 社交快捷登录 Request VO,使用 code 授权码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthSocialLoginReqVO { + + @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthUpdatePasswordReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthUpdatePasswordReqVO.java new file mode 100644 index 0000000..f0ca678 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthUpdatePasswordReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; + +// TODO 芋艿:code review 相关逻辑 +@Schema(description = "用户 APP - 修改密码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthUpdatePasswordReqVO { + + @Schema(description = "用户旧密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotBlank(message = "旧密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String oldPassword; + + @Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "新密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthWeixinMiniAppLoginReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthWeixinMiniAppLoginReqVO.java new file mode 100644 index 0000000..c8c083d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/auth/vo/AppAuthWeixinMiniAppLoginReqVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.member.controller.app.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "用户 APP - 微信小程序手机登录 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppAuthWeixinMiniAppLoginReqVO { + + @Schema(description = "手机 code,小程序通过 wx.getPhoneNumber 方法获得", requiredMode = Schema.RequiredMode.REQUIRED, example = "hello") + @NotEmpty(message = "手机 code 不能为空") + private String phoneCode; + + @Schema(description = "登录 code,小程序通过 wx.login 方法获得", requiredMode = Schema.RequiredMode.REQUIRED, example = "word") + @NotEmpty(message = "登录 code 不能为空") + private String loginCode; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/AppSocialUserController.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/AppSocialUserController.java new file mode 100644 index 0000000..e61d374 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/AppSocialUserController.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.member.controller.app.social; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.member.controller.app.social.vo.AppSocialUserBindReqVO; +import com.yunxi.scm.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO; +import com.yunxi.scm.module.member.convert.social.SocialUserConvert; +import com.yunxi.scm.module.system.api.social.SocialUserApi; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "用户 App - 社交用户") +@RestController +@RequestMapping("/system/social-user") +@Validated +public class AppSocialUserController { + + @Resource + private SocialUserApi socialUserApi; + + @PostMapping("/bind") + @Operation(summary = "社交绑定,使用 code 授权码") + public CommonResult socialBind(@RequestBody @Valid AppSocialUserBindReqVO reqVO) { + socialUserApi.bindSocialUser(SocialUserConvert.INSTANCE.convert(getLoginUserId(), UserTypeEnum.MEMBER.getValue(), reqVO)); + return CommonResult.success(true); + } + + @DeleteMapping("/unbind") + @Operation(summary = "取消社交绑定") + public CommonResult socialUnbind(@RequestBody AppSocialUserUnbindReqVO reqVO) { + socialUserApi.unbindSocialUser(SocialUserConvert.INSTANCE.convert(getLoginUserId(), UserTypeEnum.MEMBER.getValue(), reqVO)); + return CommonResult.success(true); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/vo/AppSocialUserBindReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/vo/AppSocialUserBindReqVO.java new file mode 100644 index 0000000..d4db86c --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/vo/AppSocialUserBindReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.member.controller.app.social.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 APP - 社交绑定 Request VO,使用 code 授权码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppSocialUserBindReqVO { + + @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/vo/AppSocialUserUnbindReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/vo/AppSocialUserUnbindReqVO.java new file mode 100644 index 0000000..1e3b472 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/social/vo/AppSocialUserUnbindReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.member.controller.app.social.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "用户 APP - 取消社交绑定 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppSocialUserUnbindReqVO { + + @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + @NotEmpty(message = "社交用户的 openid 不能为空") + private String openid; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/AppUserController.http b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/AppUserController.http new file mode 100644 index 0000000..4e4be2a --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/AppUserController.http @@ -0,0 +1,14 @@ +### 请求 /member/user/profile/get 接口 => 没有权限 +GET {{appApi}}/member/user/get +Authorization: Bearer test245 +tenant-id: {{appTenentId}} + +### 请求 /member/user/profile/revise-nickname 接口 成功 +PUT {{appApi}}/member/user/update-nickname?nickname=yunai222 +Authorization: Bearer test245 +tenant-id: {{appTenentId}} + +### 请求 /member/user/get-user-info 接口 成功 +GET {{appApi}}/member/user/get-user-info?id=245 +Authorization: Bearer test245 +tenant-id: {{appTenentId}} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/AppUserController.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/AppUserController.java new file mode 100644 index 0000000..c2ff2dd --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/AppUserController.java @@ -0,0 +1,71 @@ +package com.yunxi.scm.module.member.controller.app.user; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.security.core.annotations.PreAuthenticated; +import com.yunxi.scm.module.member.controller.app.user.vo.AppUserInfoRespVO; +import com.yunxi.scm.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO; +import com.yunxi.scm.module.member.convert.user.UserConvert; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; +import com.yunxi.scm.module.member.service.user.MemberUserService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY; + +@Tag(name = "用户 APP - 用户个人中心") +@RestController +@RequestMapping("/member/user") +@Validated +@Slf4j +public class AppUserController { + + @Resource + private MemberUserService userService; + + @PutMapping("/update-nickname") + @Operation(summary = "修改用户昵称") + @PreAuthenticated + public CommonResult updateUserNickname(@RequestParam("nickname") String nickname) { + userService.updateUserNickname(getLoginUserId(), nickname); + return success(true); + } + + @PostMapping("/update-avatar") + @Operation(summary = "修改用户头像") + @PreAuthenticated + public CommonResult updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception { + if (file.isEmpty()) { + throw exception(FILE_IS_EMPTY); + } + String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream()); + return success(avatar); + } + + @GetMapping("/get") + @Operation(summary = "获得基本信息") + @PreAuthenticated + public CommonResult getUserInfo() { + MemberUserDO user = userService.getUser(getLoginUserId()); + return success(UserConvert.INSTANCE.convert(user)); + } + + @PostMapping("/update-mobile") + @Operation(summary = "修改用户手机") + @PreAuthenticated + public CommonResult updateMobile(@RequestBody @Valid AppUserUpdateMobileReqVO reqVO) { + userService.updateUserMobile(getLoginUserId(), reqVO); + return success(true); + } + +} + diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/vo/AppUserInfoRespVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/vo/AppUserInfoRespVO.java new file mode 100644 index 0000000..427454a --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/vo/AppUserInfoRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.member.controller.app.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "用户 APP - 用户个人信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AppUserInfoRespVO { + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "/infra/file/get/35a12e57-4297-4faa-bf7d-7ed2f211c952") + private String avatar; + + @Schema(description = "用户手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + private String mobile; +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/vo/AppUserUpdateMobileReqVO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/vo/AppUserUpdateMobileReqVO.java new file mode 100644 index 0000000..7223bee --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/app/user/vo/AppUserUpdateMobileReqVO.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.member.controller.app.user.vo; + +import com.yunxi.scm.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@Schema(description = "用户 APP - 修改手机 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AppUserUpdateMobileReqVO { + + @Schema(description = "手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String code; + + @Schema(description = "手机号",requiredMode = Schema.RequiredMode.REQUIRED,example = "15823654487") + @NotBlank(message = "手机号不能为空") + @Length(min = 8, max = 11, message = "手机号码长度为 8-11 位") + @Mobile + private String mobile; + + @Schema(description = "原手机验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "原手机验证码不能为空") + @Length(min = 4, max = 6, message = "手机验证码长度为 4-6 位") + @Pattern(regexp = "^[0-9]+$", message = "手机验证码必须都是数字") + private String oldCode; + + // TODO @芋艿:oldMobile 应该不用传递 + + @Schema(description = "原手机号",requiredMode = Schema.RequiredMode.REQUIRED,example = "15823654487") + @NotBlank(message = "手机号不能为空") + @Length(min = 8, max = 11, message = "手机号码长度为 8-11 位") + @Mobile + private String oldMobile; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/package-info.java new file mode 100644 index 0000000..ef3125e --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yunxi-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yunxi-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.yunxi.scm.module.member.controller; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/address/AddressConvert.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/address/AddressConvert.java new file mode 100644 index 0000000..5656f67 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/address/AddressConvert.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.member.convert.address; + +import com.yunxi.scm.framework.ip.core.utils.AreaUtils; +import com.yunxi.scm.module.member.api.address.dto.AddressRespDTO; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressRespVO; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.address.MemberAddressDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 用户收件地址 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface AddressConvert { + + AddressConvert INSTANCE = Mappers.getMapper(AddressConvert.class); + + MemberAddressDO convert(AppAddressCreateReqVO bean); + + MemberAddressDO convert(AppAddressUpdateReqVO bean); + + @Mapping(source = "areaId", target = "areaName", qualifiedByName = "convertAreaIdToAreaName") + AppAddressRespVO convert(MemberAddressDO bean); + + List convertList(List list); + + AddressRespDTO convert02(MemberAddressDO bean); + + @Named("convertAreaIdToAreaName") + default String convertAreaIdToAreaName(Integer areaId) { + return AreaUtils.format(areaId); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/auth/AuthConvert.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/auth/AuthConvert.java new file mode 100644 index 0000000..f287fd7 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/auth/AuthConvert.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.member.convert.auth; + +import com.yunxi.scm.module.member.controller.app.auth.vo.*; +import com.yunxi.scm.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.api.social.dto.SocialUserUnbindReqDTO; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface AuthConvert { + + AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); + + SocialUserBindReqDTO convert(Long userId, Integer userType, AppAuthSocialLoginReqVO reqVO); + SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO); + + SmsCodeSendReqDTO convert(AppAuthSmsSendReqVO reqVO); + SmsCodeUseReqDTO convert(AppAuthResetPasswordReqVO reqVO, SmsSceneEnum scene, String usedIp); + SmsCodeUseReqDTO convert(AppAuthSmsLoginReqVO reqVO, Integer scene, String usedIp); + + AppAuthLoginRespVO convert(OAuth2AccessTokenRespDTO bean); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/package-info.java new file mode 100644 index 0000000..7c6883d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package com.yunxi.scm.module.member.convert; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/point/MemberPointConfigConvert.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/point/MemberPointConfigConvert.java new file mode 100644 index 0000000..9fc812b --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/point/MemberPointConfigConvert.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.member.convert.point; + +import com.yunxi.scm.module.member.controller.admin.point.vo.config.MemberPointConfigRespVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.config.MemberPointConfigSaveReqVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointConfigDO; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointRecordDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 会员积分配置 Convert + * + * @author QingX + */ +@Mapper +public interface MemberPointConfigConvert { + + MemberPointConfigConvert INSTANCE = Mappers.getMapper(MemberPointConfigConvert.class); + + MemberPointConfigRespVO convert(MemberPointConfigDO bean); + + MemberPointConfigDO convert(MemberPointConfigSaveReqVO bean); + + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/point/MemberPointRecordConvert.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/point/MemberPointRecordConvert.java new file mode 100644 index 0000000..494feb2 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/point/MemberPointRecordConvert.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.member.convert.point; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordCreateReqVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordRespVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointRecordDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 用户积分记录 Convert + * + * @author QingX + */ +@Mapper +public interface MemberPointRecordConvert { + + MemberPointRecordConvert INSTANCE = Mappers.getMapper(MemberPointRecordConvert.class); + + MemberPointRecordDO convert(MemberPointRecordCreateReqVO bean); + + MemberPointRecordDO convert(MemberPointRecordUpdateReqVO bean); + + MemberPointRecordRespVO convert(MemberPointRecordDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/signin/MemberSignInConfigConvert.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/signin/MemberSignInConfigConvert.java new file mode 100644 index 0000000..2916912 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/signin/MemberSignInConfigConvert.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.member.convert.signin; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigCreateReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigRespVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 积分签到规则 Convert + * + * @author QingX + */ +@Mapper +public interface MemberSignInConfigConvert { + + MemberSignInConfigConvert INSTANCE = Mappers.getMapper(MemberSignInConfigConvert.class); + + MemberSignInConfigDO convert(MemberSignInConfigCreateReqVO bean); + + MemberSignInConfigDO convert(MemberSignInConfigUpdateReqVO bean); + + MemberSignInConfigRespVO convert(MemberSignInConfigDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/signin/MemberSignInRecordConvert.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/signin/MemberSignInRecordConvert.java new file mode 100644 index 0000000..2e0bdc0 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/signin/MemberSignInRecordConvert.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.member.convert.signin; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInRecordCreateReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInRecordRespVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInRecordUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 用户签到积分 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface MemberSignInRecordConvert { + + MemberSignInRecordConvert INSTANCE = Mappers.getMapper(MemberSignInRecordConvert.class); + + MemberSignInRecordDO convert(MemberSignInRecordCreateReqVO bean); + + MemberSignInRecordDO convert(MemberSignInRecordUpdateReqVO bean); + + MemberSignInRecordRespVO convert(MemberSignInRecordDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/social/SocialUserConvert.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/social/SocialUserConvert.java new file mode 100644 index 0000000..acc4169 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/social/SocialUserConvert.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.member.convert.social; + +import com.yunxi.scm.module.member.controller.app.social.vo.AppSocialUserBindReqVO; +import com.yunxi.scm.module.member.controller.app.social.vo.AppSocialUserUnbindReqVO; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.api.social.dto.SocialUserUnbindReqDTO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SocialUserConvert { + + SocialUserConvert INSTANCE = Mappers.getMapper(SocialUserConvert.class); + + SocialUserBindReqDTO convert(Long userId, Integer userType, AppSocialUserBindReqVO reqVO); + + SocialUserUnbindReqDTO convert(Long userId, Integer userType, AppSocialUserUnbindReqVO reqVO); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/user/UserConvert.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/user/UserConvert.java new file mode 100644 index 0000000..c4cb5bf --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/user/UserConvert.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.member.convert.user; + +import com.yunxi.scm.module.member.api.user.dto.MemberUserRespDTO; +import com.yunxi.scm.module.member.controller.app.user.vo.AppUserInfoRespVO; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface UserConvert { + + UserConvert INSTANCE = Mappers.getMapper(UserConvert.class); + + AppUserInfoRespVO convert(MemberUserDO bean); + + MemberUserRespDTO convert2(MemberUserDO bean); + + List convertList2(List list); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..19fbece --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/address/MemberAddressDO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/address/MemberAddressDO.java new file mode 100644 index 0000000..437f740 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/address/MemberAddressDO.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.member.dal.dataobject.address; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 用户收件地址 DO + * + * @author 芋道源码 + */ +@TableName("member_address") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberAddressDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 收件人名称 + */ + private String name; + /** + * 手机号 + */ + private String mobile; + /** + * 地区编号 + */ + private Long areaId; + /** + * 收件详细地址 + */ + private String detailAddress; + /** + * 是否默认 + * + * true - 默认收件地址 + */ + private Boolean defaultStatus; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/point/MemberPointConfigDO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/point/MemberPointConfigDO.java new file mode 100644 index 0000000..4fbaf85 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/point/MemberPointConfigDO.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.member.dal.dataobject.point; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.math.BigDecimal; + +/** + * 会员积分配置 DO + * + * @author QingX + */ +@TableName("member_point_config") +@KeySequence("member_point_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberPointConfigDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Integer id; + /** + * 积分抵扣开关 + */ + private Boolean tradeDeductEnable; + /** + * 积分抵扣,单位:分 + * + * 1 积分抵扣多少分 + */ + private BigDecimal tradeDeductUnitPrice; + /** + * 积分抵扣最大值 + */ + private Integer tradeDeductMaxPrice; + /** + * 1 元赠送多少分 + */ + private Integer tradeGivePoint; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/point/MemberPointRecordDO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/point/MemberPointRecordDO.java new file mode 100644 index 0000000..f002bd2 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/point/MemberPointRecordDO.java @@ -0,0 +1,81 @@ +package com.yunxi.scm.module.member.dal.dataobject.point; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 用户积分记录 DO + * + * @author QingX + */ +@TableName("member_point_record") +@KeySequence("member_point_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberPointRecordDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 业务编码 + */ + private String bizId; + /** + * 业务类型 + * + * 枚举 {@link TODO biz_type 对应枚举,然后改成 int 类型哈} + */ + private String bizType; + /** + * 1增加 0扣减 + */ + // TODO @xiaqing:要不把 type 合并到 point 里?增加 point 是正数,减少 point 是负数? + private String type; + /** + * 积分标题 + */ + private String title; + /** + * 积分描述 + */ + private String description; + /** + * 积分 + */ + private Integer point; + /** + * 变动后的积分 + */ + private Integer totalPoint; + /** + * 状态:1-订单创建,2-冻结期,3-完成,4-失效(订单退款) + * + * 枚举 {@link TODO point_status 对应的类} + */ + private Integer status; + /** + * 用户id + */ + private Integer userId; + /** + * 冻结时间 + */ + private LocalDateTime freezingTime; + /** + * 解冻时间 + */ + private LocalDateTime thawingTime; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/signin/MemberSignInConfigDO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/signin/MemberSignInConfigDO.java new file mode 100644 index 0000000..8d7050e --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/signin/MemberSignInConfigDO.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.member.dal.dataobject.signin; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 积分签到规则 DO + * + * @author QingX + */ +@TableName("member_sign_in_config") +@KeySequence("member_sign_in_config_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberSignInConfigDO extends BaseDO { + + /** + * 规则自增主键 + */ + @TableId + private Integer id; + /** + * 签到第x天 + */ + private Integer day; + /** + * 签到天数对应分数 + */ + private Integer point; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/signin/MemberSignInRecordDO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/signin/MemberSignInRecordDO.java new file mode 100644 index 0000000..a147f5f --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/signin/MemberSignInRecordDO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.member.dal.dataobject.signin; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 用户签到积分 DO + * + * @author 芋道源码 + */ +@TableName("member_sign_in_record") +@KeySequence("member_sign_in_record_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberSignInRecordDO extends BaseDO { + + /** + * 签到自增id + */ + @TableId + private Long id; + /** + * 签到用户 + */ + private Integer userId; + /** + * 第几天签到 + */ + private Integer day; + /** + * 签到的分数 + */ + private Integer point; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/user/MemberUserDO.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/user/MemberUserDO.java new file mode 100644 index 0000000..8b9df8f --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/dataobject/user/MemberUserDO.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.member.dal.dataobject.user; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.time.LocalDateTime; + +/** + * 会员用户 DO + * + * uk_mobile 索引:基于 {@link #mobile} 字段 + * + * @author 芋道源码 + */ +@TableName(value = "member_user", autoResultMap = true) +@KeySequence("member_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MemberUserDO extends TenantBaseDO { + + /** + * 用户ID + */ + @TableId + private Long id; + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 手机 + */ + private String mobile; + /** + * 加密后的密码 + * + * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 + */ + private String password; + /** + * 注册 IP + */ + private String registerIp; + /** + * 最后登录IP + */ + private String loginIp; + /** + * 最后登录时间 + */ + private LocalDateTime loginDate; + + // TODO 芋艿:name 真实名字; + // TODO 芋艿:email 邮箱; + // TODO 芋艿:gender 性别; + // TODO 芋艿:score 积分; + // TODO 芋艿:payPassword 支付密码; + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/address/AddressMapper.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/address/AddressMapper.java new file mode 100644 index 0000000..9a7ee54 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/address/AddressMapper.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.member.dal.mysql.address; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.member.dal.dataobject.address.MemberAddressDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface AddressMapper extends BaseMapperX { + + default MemberAddressDO selectByIdAndUserId(Long id, Long userId) { + return selectOne(MemberAddressDO::getId, id, MemberAddressDO::getUserId, userId); + } + + default List selectListByUserIdAndDefaulted(Long userId, Boolean defaulted) { + return selectList(new LambdaQueryWrapperX().eq(MemberAddressDO::getUserId, userId) + .eqIfPresent(MemberAddressDO::getDefaultStatus, defaulted)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/point/MemberPointConfigMapper.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/point/MemberPointConfigMapper.java new file mode 100644 index 0000000..1507e51 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/point/MemberPointConfigMapper.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.member.dal.mysql.point; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointConfigDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 积分设置 Mapper + * + * @author QingX + */ +@Mapper +public interface MemberPointConfigMapper extends BaseMapperX { +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/point/MemberPointRecordMapper.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/point/MemberPointRecordMapper.java new file mode 100644 index 0000000..6f935e9 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/point/MemberPointRecordMapper.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.member.dal.mysql.point; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordPageReqVO; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointRecordDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户积分记录 Mapper + * + * @author QingX + */ +@Mapper +public interface MemberPointRecordMapper extends BaseMapperX { + + default PageResult selectPage(MemberPointRecordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MemberPointRecordDO::getBizId, reqVO.getBizId()) + .eqIfPresent(MemberPointRecordDO::getBizType, reqVO.getBizType()) + .eqIfPresent(MemberPointRecordDO::getType, reqVO.getType()) + .eqIfPresent(MemberPointRecordDO::getTitle, reqVO.getTitle()) + .eqIfPresent(MemberPointRecordDO::getStatus, reqVO.getStatus()) + .orderByDesc(MemberPointRecordDO::getId)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/signin/MemberSignInConfigMapper.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/signin/MemberSignInConfigMapper.java new file mode 100644 index 0000000..047f8a8 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/signin/MemberSignInConfigMapper.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.member.dal.mysql.signin; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigPageReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 积分签到规则 Mapper + * + * @author QingX + */ +@Mapper +public interface MemberSignInConfigMapper extends BaseMapperX { + + default PageResult selectPage(MemberSignInConfigPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MemberSignInConfigDO::getDay, reqVO.getDay()) + .orderByAsc(MemberSignInConfigDO::getDay)); + } + + // + default long selectSameDayNotSelf(MemberSignInConfigUpdateReqVO reqVO){ + return selectCount(new LambdaQueryWrapperX () + .ne(MemberSignInConfigDO::getId, reqVO.getId()) + .eq(MemberSignInConfigDO::getDay,reqVO.getDay()) + ); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/signin/MemberSignInRecordMapper.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/signin/MemberSignInRecordMapper.java new file mode 100644 index 0000000..d8308bd --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/signin/MemberSignInRecordMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.member.dal.mysql.signin; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInRecordPageReqVO; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 用户签到积分 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface MemberSignInRecordMapper extends BaseMapperX { + + default PageResult selectPage(MemberSignInRecordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MemberSignInRecordDO::getUserId, reqVO.getUserId()) + .eqIfPresent(MemberSignInRecordDO::getDay, reqVO.getDay()) + .betweenIfPresent(MemberSignInRecordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MemberSignInRecordDO::getId)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/user/MemberUserMapper.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/user/MemberUserMapper.java new file mode 100644 index 0000000..9e62cde --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/mysql/user/MemberUserMapper.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.member.dal.mysql.user; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 会员 User Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface MemberUserMapper extends BaseMapperX { + + default MemberUserDO selectByMobile(String mobile) { + return selectOne(MemberUserDO::getMobile, mobile); + } + + default List selectListByNicknameLike(String nickname) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(MemberUserDO::getNickname, nickname)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/package-info.java new file mode 100644 index 0000000..57ee9b3 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/package-info.java @@ -0,0 +1,9 @@ +/** + * DAL = Data Access Layer 数据访问层 + * 1. data object:数据对象 + * 2. redis:Redis 的 CRUD 操作 + * 3. mysql:MySQL 的 CRUD 操作 + * + * 其中,MySQL 的表以 member_ 作为前缀 + */ +package com.yunxi.scm.module.member.dal; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/redis/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/redis/package-info.java new file mode 100644 index 0000000..29bd0aa --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/dal/redis/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,后续有类后,可以删除,避免 package 无法提交到 Git 上 + */ +package com.yunxi.scm.module.member.dal.redis; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/package-info.java new file mode 100644 index 0000000..7d82d12 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 member 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.member.framework; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/web/config/MemberWebConfiguration.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/web/config/MemberWebConfiguration.java new file mode 100644 index 0000000..ff9c649 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/web/config/MemberWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.member.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * member 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class MemberWebConfiguration { + + /** + * member 模块的 API 分组 + */ + @Bean + public GroupedOpenApi memberGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("member"); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/web/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/web/package-info.java new file mode 100644 index 0000000..be0614d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * member 模块的 web 配置 + */ +package com.yunxi.scm.module.member.framework.web; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/package-info.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/package-info.java new file mode 100644 index 0000000..8391ca6 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/package-info.java @@ -0,0 +1,8 @@ +/** + * member 模块,我们放会员业务。 + * 例如说:会员中心等等 + * + * 1. Controller URL:以 /member/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 member_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.member; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/address/AddressService.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/address/AddressService.java new file mode 100644 index 0000000..0aa3a28 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/address/AddressService.java @@ -0,0 +1,67 @@ +package com.yunxi.scm.module.member.service.address; + +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.address.MemberAddressDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 用户收件地址 Service 接口 + * + * @author 芋道源码 + */ +public interface AddressService { + + /** + * 创建用户收件地址 + * + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createAddress(Long userId, @Valid AppAddressCreateReqVO createReqVO); + + /** + * 更新用户收件地址 + * + * @param userId 用户编号 + * @param updateReqVO 更新信息 + */ + void updateAddress(Long userId, @Valid AppAddressUpdateReqVO updateReqVO); + + /** + * 删除用户收件地址 + * + * @param userId 用户编号 + * @param id 编号 + */ + void deleteAddress(Long userId, Long id); + + /** + * 获得用户收件地址 + * + * @param id 编号 + * @return 用户收件地址 + */ + MemberAddressDO getAddress(Long userId, Long id); + + /** + * 获得用户收件地址列表 + * + * @param userId 用户编号 + * @return 用户收件地址列表 + */ + List getAddressList(Long userId); + + /** + * 获得用户默认的收件地址 + * + * @param userId 用户编号 + * @return 用户收件地址 + */ + MemberAddressDO getDefaultUserAddress(Long userId); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/address/AddressServiceImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/address/AddressServiceImpl.java new file mode 100644 index 0000000..f253219 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/address/AddressServiceImpl.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.module.member.service.address; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import com.yunxi.scm.module.member.convert.address.AddressConvert; +import com.yunxi.scm.module.member.dal.dataobject.address.MemberAddressDO; +import com.yunxi.scm.module.member.dal.mysql.address.AddressMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.member.enums.ErrorCodeConstants.ADDRESS_NOT_EXISTS; + +/** + * 用户收件地址 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class AddressServiceImpl implements AddressService { + + @Resource + private AddressMapper addressMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createAddress(Long userId, AppAddressCreateReqVO createReqVO) { + // 如果添加的是默认收件地址,则将原默认地址修改为非默认 + if (Boolean.TRUE.equals(createReqVO.getDefaultStatus())) { + List addresses = addressMapper.selectListByUserIdAndDefaulted(userId, true); + addresses.forEach(address -> addressMapper.updateById(new MemberAddressDO().setId(address.getId()).setDefaultStatus(false))); + } + + // 插入 + MemberAddressDO address = AddressConvert.INSTANCE.convert(createReqVO); + address.setUserId(userId); + addressMapper.insert(address); + // 返回 + return address.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateAddress(Long userId, AppAddressUpdateReqVO updateReqVO) { + // 校验存在,校验是否能够操作 + validAddressExists(userId, updateReqVO.getId()); + + // 如果修改的是默认收件地址,则将原默认地址修改为非默认 + if (Boolean.TRUE.equals(updateReqVO.getDefaultStatus())) { + List addresses = addressMapper.selectListByUserIdAndDefaulted(userId, true); + addresses.stream().filter(u -> !u.getId().equals(updateReqVO.getId())) // 排除自己 + .forEach(address -> addressMapper.updateById(new MemberAddressDO().setId(address.getId()).setDefaultStatus(false))); + } + + // 更新 + MemberAddressDO updateObj = AddressConvert.INSTANCE.convert(updateReqVO); + addressMapper.updateById(updateObj); + } + + @Override + public void deleteAddress(Long userId, Long id) { + // 校验存在,校验是否能够操作 + validAddressExists(userId, id); + // 删除 + addressMapper.deleteById(id); + } + + private void validAddressExists(Long userId, Long id) { + MemberAddressDO addressDO = getAddress(userId, id); + if (addressDO == null) { + throw exception(ADDRESS_NOT_EXISTS); + } + } + + @Override + public MemberAddressDO getAddress(Long userId, Long id) { + return addressMapper.selectByIdAndUserId(id, userId); + } + + @Override + public List getAddressList(Long userId) { + return addressMapper.selectListByUserIdAndDefaulted(userId, null); + } + + @Override + public MemberAddressDO getDefaultUserAddress(Long userId) { + List addresses = addressMapper.selectListByUserIdAndDefaulted(userId, true); + return CollUtil.getFirst(addresses); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/auth/MemberAuthService.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/auth/MemberAuthService.java new file mode 100644 index 0000000..547e1bc --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/auth/MemberAuthService.java @@ -0,0 +1,93 @@ +package com.yunxi.scm.module.member.service.auth; + +import com.yunxi.scm.module.member.controller.app.auth.vo.*; + +import javax.validation.Valid; + +/** + * 会员的认证 Service 接口 + * + * 提供用户的账号密码登录、token 的校验等认证相关的功能 + * + * @author 芋道源码 + */ +public interface MemberAuthService { + + /** + * 手机 + 密码登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AppAuthLoginRespVO login(@Valid AppAuthLoginReqVO reqVO); + + /** + * 基于 token 退出登录 + * + * @param token token + */ + void logout(String token); + + /** + * 手机 + 验证码登陆 + * + * @param reqVO 登陆信息 + * @return 登录结果 + */ + AppAuthLoginRespVO smsLogin(@Valid AppAuthSmsLoginReqVO reqVO); + + /** + * 社交登录,使用 code 授权码 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AppAuthLoginRespVO socialLogin(@Valid AppAuthSocialLoginReqVO reqVO); + + /** + * 微信小程序的一键登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO); + + /** + * 获得社交认证 URL + * + * @param type 社交平台类型 + * @param redirectUri 跳转地址 + * @return 认证 URL + */ + String getSocialAuthorizeUrl(Integer type, String redirectUri); + + /** + * 修改用户密码 + * @param userId 用户id + * @param userReqVO 用户请求实体类 + */ + void updatePassword(Long userId, AppAuthUpdatePasswordReqVO userReqVO); + + /** + * 忘记密码 + * @param userReqVO 用户请求实体类 + */ + void resetPassword(AppAuthResetPasswordReqVO userReqVO); + + /** + * 给用户发送短信验证码 + * + * @param userId 用户编号 + * @param reqVO 发送信息 + */ + void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 登录结果 + */ + AppAuthLoginRespVO refreshToken(String refreshToken); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/auth/MemberAuthServiceImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/auth/MemberAuthServiceImpl.java new file mode 100644 index 0000000..374353d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/auth/MemberAuthServiceImpl.java @@ -0,0 +1,302 @@ +package com.yunxi.scm.module.member.service.auth; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.bean.WxMaPhoneNumberInfo; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.util.monitor.TracerUtils; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.module.member.controller.app.auth.vo.*; +import com.yunxi.scm.module.member.convert.auth.AuthConvert; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; +import com.yunxi.scm.module.member.dal.mysql.user.MemberUserMapper; +import com.yunxi.scm.module.member.service.user.MemberUserService; +import com.yunxi.scm.module.system.api.logger.LoginLogApi; +import com.yunxi.scm.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.yunxi.scm.module.system.api.oauth2.OAuth2TokenApi; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; +import com.yunxi.scm.module.system.api.sms.SmsCodeApi; +import com.yunxi.scm.module.system.api.social.SocialUserApi; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.enums.logger.LoginLogTypeEnum; +import com.yunxi.scm.module.system.enums.logger.LoginResultEnum; +import com.yunxi.scm.module.system.enums.oauth2.OAuth2ClientConstants; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.yunxi.scm.module.member.enums.ErrorCodeConstants.*; + +/** + * 会员的认证 Service 接口 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class MemberAuthServiceImpl implements MemberAuthService { + + @Resource + private MemberUserService userService; + @Resource + private SmsCodeApi smsCodeApi; + @Resource + private LoginLogApi loginLogApi; + @Resource + private SocialUserApi socialUserApi; + @Resource + private OAuth2TokenApi oauth2TokenApi; + + @Resource + private WxMaService wxMaService; + + @Resource + private PasswordEncoder passwordEncoder; + @Resource + private MemberUserMapper userMapper; + + @Override + public AppAuthLoginRespVO login(AppAuthLoginReqVO reqVO) { + // 使用手机 + 密码,进行登录。 + MemberUserDO user = login0(reqVO.getMobile(), reqVO.getPassword()); + + // 如果 socialType 非空,说明需要绑定社交用户 + if (reqVO.getSocialType() != null) { + socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); + } + + @Override + @Transactional + public AppAuthLoginRespVO smsLogin(AppAuthSmsLoginReqVO reqVO) { + // 校验验证码 + String userIp = getClientIP(); + smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_LOGIN.getScene(), userIp)); + + // 获得获得注册用户 + MemberUserDO user = userService.createUserIfAbsent(reqVO.getMobile(), userIp); + Assert.notNull(user, "获取用户失败,结果为空"); + + // 如果 socialType 非空,说明需要绑定社交用户 + if (reqVO.getSocialType() != null) { + socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, reqVO.getMobile(), LoginLogTypeEnum.LOGIN_SMS); + } + + @Override + public AppAuthLoginRespVO socialLogin(AppAuthSocialLoginReqVO reqVO) { + // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 + Long userId = socialUserApi.getBindUserId(UserTypeEnum.MEMBER.getValue(), reqVO.getType(), + reqVO.getCode(), reqVO.getState()); + if (userId == null) { + throw exception(AUTH_THIRD_LOGIN_NOT_BIND); + } + + // 自动登录 + MemberUserDO user = userService.getUser(userId); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL); + } + + @Override + public AppAuthLoginRespVO weixinMiniAppLogin(AppAuthWeixinMiniAppLoginReqVO reqVO) { + // 获得对应的手机号信息 + // TODO @芋艿:需要弱化微信小程序的依赖,通过 system 获取手机号 + WxMaPhoneNumberInfo phoneNumberInfo; + try { + phoneNumberInfo = wxMaService.getUserService().getPhoneNoInfo(reqVO.getPhoneCode()); + } catch (Exception exception) { + throw exception(AUTH_WEIXIN_MINI_APP_PHONE_CODE_ERROR); + } + // 获得获得注册用户 + MemberUserDO user = userService.createUserIfAbsent(phoneNumberInfo.getPurePhoneNumber(), getClientIP()); + Assert.notNull(user, "获取用户失败,结果为空"); + + // 绑定社交用户 + socialUserApi.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + SocialTypeEnum.WECHAT_MINI_APP.getType(), reqVO.getLoginCode(), "")); + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user, user.getMobile(), LoginLogTypeEnum.LOGIN_SOCIAL); + } + + private AppAuthLoginRespVO createTokenAfterLoginSuccess(MemberUserDO user, String mobile, LoginLogTypeEnum logType) { + // 插入登陆日志 + createLoginLog(user.getId(), mobile, logType, LoginResultEnum.SUCCESS); + // 创建 Token 令牌 + OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.createAccessToken(new OAuth2AccessTokenCreateReqDTO() + .setUserId(user.getId()).setUserType(getUserType().getValue()) + .setClientId(OAuth2ClientConstants.CLIENT_ID_DEFAULT)); + // 构建返回结果 + return AuthConvert.INSTANCE.convert(accessTokenRespDTO); + } + + @Override + public String getSocialAuthorizeUrl(Integer type, String redirectUri) { + return socialUserApi.getAuthorizeUrl(type, redirectUri); + } + + private MemberUserDO login0(String mobile, String password) { + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_MOBILE; + // 校验账号是否存在 + MemberUserDO user = userService.getUserByMobile(mobile); + if (user == null) { + createLoginLog(null, mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + if (!userService.isPasswordMatch(password, user.getPassword())) { + createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + // 校验是否禁用 + if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + createLoginLog(user.getId(), mobile, logTypeEnum, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + return user; + } + + private void createLoginLog(Long userId, String mobile, LoginLogTypeEnum logType, LoginResultEnum loginResult) { + // 插入登录日志 + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logType.getType()); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(getUserType().getValue()); + reqDTO.setUsername(mobile); + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(getClientIP()); + reqDTO.setResult(loginResult.getResult()); + loginLogApi.createLoginLog(reqDTO); + // 更新最后登录时间 + if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { + userService.updateUserLogin(userId, getClientIP()); + } + } + + @Override + public void logout(String token) { + // 删除访问令牌 + OAuth2AccessTokenRespDTO accessTokenRespDTO = oauth2TokenApi.removeAccessToken(token); + if (accessTokenRespDTO == null) { + return; + } + // 删除成功,则记录登出日志 + createLogoutLog(accessTokenRespDTO.getUserId()); + } + + @Override + public void updatePassword(Long userId, AppAuthUpdatePasswordReqVO reqVO) { + // 检验旧密码 + MemberUserDO userDO = checkOldPassword(userId, reqVO.getOldPassword()); + + // 更新用户密码 + // TODO 芋艿:需要重构到用户模块 + userMapper.updateById(MemberUserDO.builder().id(userDO.getId()) + .password(passwordEncoder.encode(reqVO.getPassword())).build()); + } + + @Override + public void resetPassword(AppAuthResetPasswordReqVO reqVO) { + // 检验用户是否存在 + MemberUserDO userDO = checkUserIfExists(reqVO.getMobile()); + + // 使用验证码 + smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.MEMBER_FORGET_PASSWORD, + getClientIP())); + + // 更新密码 + userMapper.updateById(MemberUserDO.builder().id(userDO.getId()) + .password(passwordEncoder.encode(reqVO.getPassword())).build()); + } + + @Override + public void sendSmsCode(Long userId, AppAuthSmsSendReqVO reqVO) { + // TODO 要根据不同的场景,校验是否有用户 + smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); + } + + @Override + public AppAuthLoginRespVO refreshToken(String refreshToken) { + OAuth2AccessTokenRespDTO accessTokenDO = oauth2TokenApi.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + /** + * 校验旧密码 + * + * @param id 用户 id + * @param oldPassword 旧密码 + * @return MemberUserDO 用户实体 + */ + @VisibleForTesting + public MemberUserDO checkOldPassword(Long id, String oldPassword) { + MemberUserDO user = userMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + // 参数:未加密密码,编码后的密码 + if (!passwordEncoder.matches(oldPassword,user.getPassword())) { + throw exception(USER_PASSWORD_FAILED); + } + return user; + } + + public MemberUserDO checkUserIfExists(String mobile) { + MemberUserDO user = userMapper.selectByMobile(mobile); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + return user; + } + + private void createLogoutLog(Long userId) { + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(LoginLogTypeEnum.LOGOUT_SELF.getType()); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(getUserType().getValue()); + reqDTO.setUsername(getMobile(userId)); + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(getClientIP()); + reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); + loginLogApi.createLoginLog(reqDTO); + } + + private String getMobile(Long userId) { + if (userId == null) { + return null; + } + MemberUserDO user = userService.getUser(userId); + return user != null ? user.getMobile() : null; + } + + private UserTypeEnum getUserType() { + return UserTypeEnum.MEMBER; + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointConfigService.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointConfigService.java new file mode 100644 index 0000000..630067d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointConfigService.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.member.service.point; + +import com.yunxi.scm.module.member.controller.admin.point.vo.config.MemberPointConfigSaveReqVO; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointConfigDO; + +import javax.validation.Valid; + +/** + * 会员积分配置 Service 接口 + * + * @author QingX + */ +public interface MemberPointConfigService { + + /** + * 保存会员积分配置 + * + * @param saveReqVO 更新信息 + */ + void saveConfig(@Valid MemberPointConfigSaveReqVO saveReqVO); + + /** + * 获得会员积分配置 + * + * @return 积分配置 + */ + MemberPointConfigDO getConfig(); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointConfigServiceImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointConfigServiceImpl.java new file mode 100644 index 0000000..ef645a9 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointConfigServiceImpl.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.member.service.point; + +import com.yunxi.scm.module.member.controller.admin.point.vo.config.MemberPointConfigSaveReqVO; +import com.yunxi.scm.module.member.convert.point.MemberPointConfigConvert; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointConfigDO; +import com.yunxi.scm.module.member.dal.mysql.point.MemberPointConfigMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 会员积分配置 Service 实现类 + * + * @author QingX + */ +@Service +@Validated +public class MemberPointConfigServiceImpl implements MemberPointConfigService { + + @Resource + private MemberPointConfigMapper pointConfigMapper; + + @Override + public void saveConfig(MemberPointConfigSaveReqVO saveReqVO) { + // TODO @xiaqing:直接 getConfig() 查询,如果不存在,则插入;存在,则进行更新; + long total = pointConfigMapper.selectCount(); + MemberPointConfigDO pointConfigDO = MemberPointConfigConvert.INSTANCE.convert(saveReqVO); + //大于0存在记录,则更新,否则插入 + if (total > 0) { + pointConfigMapper.updateById(pointConfigDO); + } else { + pointConfigMapper.insert(pointConfigDO); + } + } + + @Override + public MemberPointConfigDO getConfig() { + List list = pointConfigMapper.selectList(); + // TODO @xiaqing:可以使用 CollUtil.getFirst() + return list == null ? null : list.get(0); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointRecordService.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointRecordService.java new file mode 100644 index 0000000..960ee7d --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointRecordService.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.member.service.point; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordPageReqVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointRecordDO; + +import javax.validation.Valid; + +/** + * 用户积分记录 Service 接口 + * + * @author QingX + */ +public interface MemberPointRecordService { + // TODO @xiaqing:方法和方法之间,是空一行哈; + + /** + * 更新用户积分记录 + * + * @param updateReqVO 更新信息 + */ + void updateRecord(@Valid MemberPointRecordUpdateReqVO updateReqVO); + + + /** + * 获得用户积分记录 + * + * @param id 编号 + * @return 用户积分记录 + */ + MemberPointRecordDO getRecord(Long id); + + /** + * 获得用户积分记录分页 + * + * @param pageReqVO 分页查询 + * @return 用户积分记录分页 + */ + PageResult getRecordPage(MemberPointRecordPageReqVO pageReqVO); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointRecordServiceImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointRecordServiceImpl.java new file mode 100644 index 0000000..e23cb0e --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/point/MemberPointRecordServiceImpl.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.member.service.point; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordPageReqVO; +import com.yunxi.scm.module.member.controller.admin.point.vo.recrod.MemberPointRecordUpdateReqVO; +import com.yunxi.scm.module.member.convert.point.MemberPointRecordConvert; +import com.yunxi.scm.module.member.dal.dataobject.point.MemberPointRecordDO; +import com.yunxi.scm.module.member.dal.mysql.point.MemberPointRecordMapper; +import com.yunxi.scm.module.member.enums.ErrorCodeConstants; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; + + +/** + * 用户积分记录 Service 实现类 + * + * @author QingX + */ +@Service +@Validated +public class MemberPointRecordServiceImpl implements MemberPointRecordService { + + @Resource + private MemberPointRecordMapper recordMapper; + + @Override + public void updateRecord(MemberPointRecordUpdateReqVO updateReqVO) { + // 校验存在 + validateRecordExists(updateReqVO.getId()); + // 更新 + MemberPointRecordDO updateObj = MemberPointRecordConvert.INSTANCE.convert(updateReqVO); + recordMapper.updateById(updateObj); + } + + private void validateRecordExists(Long id) { + if (recordMapper.selectById(id) == null) { + throw exception(ErrorCodeConstants.RECORD_NOT_EXISTS); + } + } + + @Override + public MemberPointRecordDO getRecord(Long id) { + return recordMapper.selectById(id); + } + + @Override + public PageResult getRecordPage(MemberPointRecordPageReqVO pageReqVO) { + return recordMapper.selectPage(pageReqVO); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInConfigService.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInConfigService.java new file mode 100644 index 0000000..c8f31c5 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInConfigService.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.module.member.service.signin; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigCreateReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigPageReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInConfigDO; + +import javax.validation.Valid; + +/** + * 积分签到规则 Service 接口 + * + * @author QingX + */ +public interface MemberSignInConfigService { + + /** + * 创建积分签到规则 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Integer createSignInConfig(@Valid MemberSignInConfigCreateReqVO createReqVO); + + /** + * 更新积分签到规则 + * + * @param updateReqVO 更新信息 + */ + void updateSignInConfig(@Valid MemberSignInConfigUpdateReqVO updateReqVO); + + /** + * 删除积分签到规则 + * + * @param id 编号 + */ + void deleteSignInConfig(Integer id); + + /** + * 获得积分签到规则 + * + * @param id 编号 + * @return 积分签到规则 + */ + MemberSignInConfigDO getSignInConfig(Integer id); + + /** + * 获得积分签到规则分页 + * + * @param pageReqVO 分页查询 + * @return 积分签到规则分页 + */ + PageResult getSignInConfigPage(MemberSignInConfigPageReqVO pageReqVO); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInConfigServiceImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInConfigServiceImpl.java new file mode 100644 index 0000000..4596a92 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInConfigServiceImpl.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.module.member.service.signin; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigCreateReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigPageReqVO; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInConfigUpdateReqVO; +import com.yunxi.scm.module.member.convert.signin.MemberSignInConfigConvert; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInConfigDO; +import com.yunxi.scm.module.member.dal.mysql.signin.MemberSignInConfigMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.member.enums.ErrorCodeConstants.SIGN_IN_CONFIG_EXISTS; +import static com.yunxi.scm.module.member.enums.ErrorCodeConstants.SIGN_IN_CONFIG_NOT_EXISTS; + +/** + * 积分签到规则 Service 实现类 + * + * @author QingX + */ +@Service +@Validated +public class MemberSignInConfigServiceImpl implements MemberSignInConfigService { + + @Resource + private MemberSignInConfigMapper memberSignInConfigMapper; + + // TODO @xiaqing:这种写的逻辑,最好按照 校验 - 更新这样的顺序写;类似这里,37 要放到 34 前面;updateSignInConfig 也是一样的思路 + @Override + public Integer createSignInConfig(MemberSignInConfigCreateReqVO createReqVO) { + // 插入 + MemberSignInConfigDO signInConfig = MemberSignInConfigConvert.INSTANCE.convert(createReqVO); + // 判断是否重复插入签到天数 + validateSignInConfigExistsDay(signInConfig.getDay()); + memberSignInConfigMapper.insert(signInConfig); + // 返回 + return signInConfig.getId(); + } + + // TODO @xiaqing:这个逻辑的空行要注意;52 到 53 是没必要的空行;而 49 和 50 之间有个空行会好点,可以区分出是 校验 - 更新这样的逻辑间隔 + @Override + public void updateSignInConfig(MemberSignInConfigUpdateReqVO updateReqVO) { + // 校验存在 + validateSignInConfigExists(updateReqVO.getId()); + //判断是否重复插入签到天数 + validateSignInConfigSameDayNotSelf(updateReqVO); + // 判断更新的 + MemberSignInConfigDO updateObj = MemberSignInConfigConvert.INSTANCE.convert(updateReqVO); + + + memberSignInConfigMapper.updateById(updateObj); + } + + @Override + public void deleteSignInConfig(Integer id) { + // 校验存在 + validateSignInConfigExists(id); + // 删除 + memberSignInConfigMapper.deleteById(id); + } + + private void validateSignInConfigExists(Integer id) { + if (memberSignInConfigMapper.selectById(id) == null) { + throw exception(SIGN_IN_CONFIG_NOT_EXISTS); + } + } + + // TODO @xiaqing:这个唯一判断,也可以参考下别的模块哈; + //根据签到天数判断是否存在一个相同的天数 + private void validateSignInConfigExistsDay(Integer day) { + if (memberSignInConfigMapper.selectCount(MemberSignInConfigDO::getDay,day)>0) { + throw exception(SIGN_IN_CONFIG_EXISTS); + } + } + + // TODO @xiaqing:参考下别的模块,判断唯一,排除自己怎么写的哈; + // 更新天数时判断是否有重复的天数,需要去除自己 + private void validateSignInConfigSameDayNotSelf(MemberSignInConfigUpdateReqVO reqVO) { + if (memberSignInConfigMapper.selectSameDayNotSelf(reqVO)>0) { + throw exception(SIGN_IN_CONFIG_EXISTS); + } + } + + @Override + public MemberSignInConfigDO getSignInConfig(Integer id) { + return memberSignInConfigMapper.selectById(id); + } + + @Override + public PageResult getSignInConfigPage(MemberSignInConfigPageReqVO pageReqVO) { + return memberSignInConfigMapper.selectPage(pageReqVO); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInRecordService.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInRecordService.java new file mode 100644 index 0000000..d8e9471 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInRecordService.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.member.service.signin; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInRecordPageReqVO; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInRecordDO; + +/** + * 用户签到积分 Service 接口 + * + * @author 芋道源码 + */ +public interface MemberSignInRecordService { + + /** + * 删除用户签到积分 + * + * @param id 编号 + */ + void deleteSignInRecord(Long id); + + /** + * 获得用户签到积分 + * + * @param id 编号 + * @return 用户签到积分 + */ + MemberSignInRecordDO getSignInRecord(Long id); + + /** + * 获得用户签到积分分页 + * + * @param pageReqVO 分页查询 + * @return 用户签到积分分页 + */ + PageResult getSignInRecordPage(MemberSignInRecordPageReqVO pageReqVO); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInRecordServiceImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInRecordServiceImpl.java new file mode 100644 index 0000000..c815811 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/signin/MemberSignInRecordServiceImpl.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.module.member.service.signin; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.member.controller.admin.signin.vo.MemberSignInRecordPageReqVO; +import com.yunxi.scm.module.member.dal.dataobject.signin.MemberSignInRecordDO; +import com.yunxi.scm.module.member.dal.mysql.signin.MemberSignInRecordMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.member.enums.ErrorCodeConstants.SIGN_IN_RECORD_NOT_EXISTS; + +/** + * 用户签到积分 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MemberSignInRecordServiceImpl implements MemberSignInRecordService { + + @Resource + private MemberSignInRecordMapper memberSignInRecordMapper; + + @Override + public void deleteSignInRecord(Long id) { + // 校验存在 + validateSignInRecordExists(id); + // 删除 + memberSignInRecordMapper.deleteById(id); + } + + private void validateSignInRecordExists(Long id) { + if (memberSignInRecordMapper.selectById(id) == null) { + throw exception(SIGN_IN_RECORD_NOT_EXISTS); + } + } + + @Override + public MemberSignInRecordDO getSignInRecord(Long id) { + return memberSignInRecordMapper.selectById(id); + } + + @Override + public PageResult getSignInRecordPage(MemberSignInRecordPageReqVO pageReqVO) { + return memberSignInRecordMapper.selectPage(pageReqVO); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/user/MemberUserService.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/user/MemberUserService.java new file mode 100644 index 0000000..9c3f726 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/user/MemberUserService.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.module.member.service.user; + +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; + +import java.io.InputStream; +import java.util.Collection; +import java.util.List; + +/** + * 会员用户 Service 接口 + * + * @author 芋道源码 + */ +public interface MemberUserService { + + /** + * 通过手机查询用户 + * + * @param mobile 手机 + * @return 用户对象 + */ + MemberUserDO getUserByMobile(String mobile); + + /** + * 基于用户昵称,模糊匹配用户列表 + * + * @param nickname 用户昵称,模糊匹配 + * @return 用户信息的列表 + */ + List getUserListByNickname(String nickname); + + /** + * 基于手机号创建用户。 + * 如果用户已经存在,则直接进行返回 + * + * @param mobile 手机号 + * @param registerIp 注册 IP + * @return 用户对象 + */ + MemberUserDO createUserIfAbsent(@Mobile String mobile, String registerIp); + + /** + * 更新用户的最后登陆信息 + * + * @param id 用户编号 + * @param loginIp 登陆 IP + */ + void updateUserLogin(Long id, String loginIp); + + /** + * 通过用户 ID 查询用户 + * + * @param id 用户ID + * @return 用户对象信息 + */ + MemberUserDO getUser(Long id); + + /** + * 通过用户 ID 查询用户们 + * + * @param ids 用户 ID + * @return 用户对象信息数组 + */ + List getUserList(Collection ids); + + /** + * 修改用户昵称 + * @param userId 用户id + * @param nickname 用户新昵称 + */ + void updateUserNickname(Long userId, String nickname); + + /** + * 修改用户头像 + * @param userId 用户id + * @param inputStream 头像文件 + * @return 头像url + */ + String updateUserAvatar(Long userId, InputStream inputStream) throws Exception; + + /** + * 修改手机 + * @param userId 用户id + * @param reqVO 请求实体 + */ + void updateUserMobile(Long userId, AppUserUpdateMobileReqVO reqVO); + + /** + * 判断密码是否匹配 + * + * @param rawPassword 未加密的密码 + * @param encodedPassword 加密后的密码 + * @return 是否匹配 + */ + boolean isPasswordMatch(String rawPassword, String encodedPassword); + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/user/MemberUserServiceImpl.java b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/user/MemberUserServiceImpl.java new file mode 100644 index 0000000..b2f02cf --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/main/java/com/yunxi/scm/module/member/service/user/MemberUserServiceImpl.java @@ -0,0 +1,170 @@ +package com.yunxi.scm.module.member.service.user; + +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.IdUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.module.infra.api.file.FileApi; +import com.yunxi.scm.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; +import com.yunxi.scm.module.member.dal.mysql.user.MemberUserMapper; +import com.yunxi.scm.module.system.api.sms.SmsCodeApi; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.io.InputStream; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.yunxi.scm.module.member.enums.ErrorCodeConstants.USER_NOT_EXISTS; + +/** + * 会员 User Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Valid +@Slf4j +public class MemberUserServiceImpl implements MemberUserService { + + @Resource + private MemberUserMapper memberUserMapper; + + @Resource + private FileApi fileApi; + @Resource + private SmsCodeApi smsCodeApi; + + @Resource + private PasswordEncoder passwordEncoder; + + @Override + public MemberUserDO getUserByMobile(String mobile) { + return memberUserMapper.selectByMobile(mobile); + } + + @Override + public List getUserListByNickname(String nickname) { + return memberUserMapper.selectListByNicknameLike(nickname); + } + + @Override + public MemberUserDO createUserIfAbsent(String mobile, String registerIp) { + // 用户已经存在 + MemberUserDO user = memberUserMapper.selectByMobile(mobile); + if (user != null) { + return user; + } + // 用户不存在,则进行创建 + return this.createUser(mobile, registerIp); + } + + private MemberUserDO createUser(String mobile, String registerIp) { + // 生成密码 + String password = IdUtil.fastSimpleUUID(); + // 插入用户 + MemberUserDO user = new MemberUserDO(); + user.setMobile(mobile); + user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 + user.setPassword(encodePassword(password)); // 加密密码 + user.setRegisterIp(registerIp); + memberUserMapper.insert(user); + return user; + } + + @Override + public void updateUserLogin(Long id, String loginIp) { + memberUserMapper.updateById(new MemberUserDO().setId(id) + .setLoginIp(loginIp).setLoginDate(LocalDateTime.now())); + } + + @Override + public MemberUserDO getUser(Long id) { + return memberUserMapper.selectById(id); + } + + @Override + public List getUserList(Collection ids) { + return memberUserMapper.selectBatchIds(ids); + } + + @Override + public void updateUserNickname(Long userId, String nickname) { + MemberUserDO user = this.checkUserExists(userId); + // 仅当新昵称不等于旧昵称时进行修改 + if (nickname.equals(user.getNickname())){ + return; + } + MemberUserDO userDO = new MemberUserDO(); + userDO.setId(user.getId()); + userDO.setNickname(nickname); + memberUserMapper.updateById(userDO); + } + + @Override + public String updateUserAvatar(Long userId, InputStream avatarFile) throws Exception { + this.checkUserExists(userId); + // 创建文件 + String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile)); + // 更新头像路径 + memberUserMapper.updateById(MemberUserDO.builder().id(userId).avatar(avatar).build()); + return avatar; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUserMobile(Long userId, AppUserUpdateMobileReqVO reqVO) { + // 检测用户是否存在 + checkUserExists(userId); + // TODO 芋艿:oldMobile 应该不用传递 + + // 校验旧手机和旧验证码 + smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getOldMobile()).setCode(reqVO.getOldCode()) + .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); + // 使用新验证码 + smsCodeApi.useSmsCode(new SmsCodeUseReqDTO().setMobile(reqVO.getMobile()).setCode(reqVO.getCode()) + .setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()).setUsedIp(getClientIP())); + + // 更新用户手机 + memberUserMapper.updateById(MemberUserDO.builder().id(userId).mobile(reqVO.getMobile()).build()); + } + + @Override + public boolean isPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 对密码进行加密 + * + * @param password 密码 + * @return 加密后的密码 + */ + private String encodePassword(String password) { + return passwordEncoder.encode(password); + } + + @VisibleForTesting + public MemberUserDO checkUserExists(Long id) { + if (id == null) { + return null; + } + MemberUserDO user = memberUserMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + return user; + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/address/AddressServiceImplTest.java b/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/address/AddressServiceImplTest.java new file mode 100644 index 0000000..3be586f --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/address/AddressServiceImplTest.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.member.service.address; + +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressCreateReqVO; +import com.yunxi.scm.module.member.controller.app.address.vo.AppAddressUpdateReqVO; +import com.yunxi.scm.module.member.dal.dataobject.address.MemberAddressDO; +import com.yunxi.scm.module.member.dal.mysql.address.AddressMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.member.enums.ErrorCodeConstants.ADDRESS_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +/** + * {@link AddressServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(AddressServiceImpl.class) +public class AddressServiceImplTest extends BaseDbUnitTest { + + @Resource + private AddressServiceImpl addressService; + + @Resource + private AddressMapper addressMapper; + + @Test + public void testCreateAddress_success() { + // 准备参数 + AppAddressCreateReqVO reqVO = randomPojo(AppAddressCreateReqVO.class); + + // 调用 + Long addressId = addressService.createAddress(randomLongId(), reqVO); + // 断言 + assertNotNull(addressId); + // 校验记录的属性是否正确 + MemberAddressDO address = addressMapper.selectById(addressId); + assertPojoEquals(reqVO, address); + } + + @Test + public void testUpdateAddress_success() { + // mock 数据 + MemberAddressDO dbAddress = randomPojo(MemberAddressDO.class); + addressMapper.insert(dbAddress);// @Sql: 先插入出一条存在的数据 + // 准备参数 + AppAddressUpdateReqVO reqVO = randomPojo(AppAddressUpdateReqVO.class, o -> { + o.setId(dbAddress.getId()); // 设置更新的 ID + }); + + // 调用 + addressService.updateAddress(dbAddress.getUserId(), reqVO); + // 校验是否更新正确 + MemberAddressDO address = addressMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, address); + } + + @Test + public void testUpdateAddress_notExists() { + // 准备参数 + AppAddressUpdateReqVO reqVO = randomPojo(AppAddressUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> addressService.updateAddress(randomLongId(), reqVO), ADDRESS_NOT_EXISTS); + } + + @Test + public void testDeleteAddress_success() { + // mock 数据 + MemberAddressDO dbAddress = randomPojo(MemberAddressDO.class); + addressMapper.insert(dbAddress);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbAddress.getId(); + + // 调用 + addressService.deleteAddress(dbAddress.getUserId(), id); + // 校验数据不存在了 + assertNull(addressMapper.selectById(id)); + } + + @Test + public void testDeleteAddress_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> addressService.deleteAddress(randomLongId(), id), ADDRESS_NOT_EXISTS); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/auth/MemberAuthServiceTest.java b/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/auth/MemberAuthServiceTest.java new file mode 100644 index 0000000..45aa128 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/auth/MemberAuthServiceTest.java @@ -0,0 +1,126 @@ +package com.yunxi.scm.module.member.service.auth; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import com.yunxi.scm.framework.test.core.ut.BaseDbAndRedisUnitTest; +import com.yunxi.scm.module.member.controller.app.auth.vo.AppAuthResetPasswordReqVO; +import com.yunxi.scm.module.member.controller.app.auth.vo.AppAuthUpdatePasswordReqVO; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; +import com.yunxi.scm.module.member.dal.mysql.user.MemberUserMapper; +import com.yunxi.scm.module.member.service.user.MemberUserService; +import com.yunxi.scm.module.system.api.oauth2.OAuth2TokenApi; +import com.yunxi.scm.module.system.api.logger.LoginLogApi; +import com.yunxi.scm.module.system.api.sms.SmsCodeApi; +import com.yunxi.scm.module.system.api.social.SocialUserApi; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.annotation.Resource; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static cn.hutool.core.util.RandomUtil.randomNumbers; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +// TODO @芋艿:单测的 review,等逻辑都达成一致后 +/** + * {@link MemberAuthService} 的单元测试类 + * + * @author 宋天 + */ +@Import({MemberAuthServiceImpl.class, YunxiRedisAutoConfiguration.class}) +public class MemberAuthServiceTest extends BaseDbAndRedisUnitTest { + + // TODO @芋艿:登录相关的单测,待补全 + + @Resource + private MemberAuthServiceImpl authService; + + @MockBean + private MemberUserService userService; + @MockBean + private SmsCodeApi smsCodeApi; + @MockBean + private LoginLogApi loginLogApi; + @MockBean + private OAuth2TokenApi oauth2TokenApi; + @MockBean + private SocialUserApi socialUserApi; + @MockBean + private WxMaService wxMaService; + @MockBean + private PasswordEncoder passwordEncoder; + + @Resource + private MemberUserMapper memberUserMapper; + + @Test + public void testUpdatePassword_success(){ + // 准备参数 + MemberUserDO userDO = randomUserDO(); + memberUserMapper.insert(userDO); + + // 新密码 + String newPassword = randomString(); + + // 请求实体 + AppAuthUpdatePasswordReqVO reqVO = AppAuthUpdatePasswordReqVO.builder() + .oldPassword(userDO.getPassword()) + .password(newPassword) + .build(); + + // 测试桩 + // 这两个相等是为了返回ture这个结果 + when(passwordEncoder.matches(reqVO.getOldPassword(),reqVO.getOldPassword())).thenReturn(true); + when(passwordEncoder.encode(newPassword)).thenReturn(newPassword); + + // 更新用户密码 + authService.updatePassword(userDO.getId(), reqVO); + assertEquals(memberUserMapper.selectById(userDO.getId()).getPassword(),newPassword); + } + + @Test + public void testResetPassword_success(){ + // 准备参数 + MemberUserDO userDO = randomUserDO(); + memberUserMapper.insert(userDO); + + // 随机密码 + String password = randomNumbers(11); + // 随机验证码 + String code = randomNumbers(4); + + // mock + when(passwordEncoder.encode(password)).thenReturn(password); + + // 更新用户密码 + AppAuthResetPasswordReqVO reqVO = new AppAuthResetPasswordReqVO(); + reqVO.setMobile(userDO.getMobile()); + reqVO.setPassword(password); + reqVO.setCode(code); + + authService.resetPassword(reqVO); + assertEquals(memberUserMapper.selectById(userDO.getId()).getPassword(),password); + } + + + // ========== 随机对象 ========== + + @SafeVarargs + private static MemberUserDO randomUserDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setPassword(randomString()); + }; + return randomPojo(MemberUserDO.class, ArrayUtils.append(consumer, consumers)); + } + + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/user/MemberUserServiceImplTest.java b/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/user/MemberUserServiceImplTest.java new file mode 100644 index 0000000..d657fd4 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/test/java/com/yunxi/scm/module/member/service/user/MemberUserServiceImplTest.java @@ -0,0 +1,137 @@ +package com.yunxi.scm.module.member.service.user; + +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import com.yunxi.scm.framework.test.core.ut.BaseDbAndRedisUnitTest; +import com.yunxi.scm.module.infra.api.file.FileApi; +import com.yunxi.scm.module.member.controller.app.user.vo.AppUserUpdateMobileReqVO; +import com.yunxi.scm.module.member.dal.dataobject.user.MemberUserDO; +import com.yunxi.scm.module.member.dal.mysql.user.MemberUserMapper; +import com.yunxi.scm.module.member.service.auth.MemberAuthServiceImpl; +import com.yunxi.scm.module.system.api.sms.SmsCodeApi; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.annotation.Resource; +import java.io.ByteArrayInputStream; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.*; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.when; + +// TODO @芋艿:单测的 review,等逻辑都达成一致后 +/** + * {@link MemberUserServiceImpl} 的单元测试类 + * + * @author 宋天 + */ +@Import({MemberUserServiceImpl.class, YunxiRedisAutoConfiguration.class}) +public class MemberUserServiceImplTest extends BaseDbAndRedisUnitTest { + + @Resource + private MemberUserServiceImpl memberUserService; + + @Resource + private StringRedisTemplate stringRedisTemplate; + + @Resource + private MemberUserMapper userMapper; + + @MockBean + private MemberAuthServiceImpl authService; + + @MockBean + private PasswordEncoder passwordEncoder; + + @MockBean + private SmsCodeApi smsCodeApi; + @MockBean + private FileApi fileApi; + + @Test + public void testUpdateNickName_success(){ + // mock 数据 + MemberUserDO userDO = randomUserDO(); + userMapper.insert(userDO); + + // 随机昵称 + String newNickName = randomString(); + + // 调用接口修改昵称 + memberUserService.updateUserNickname(userDO.getId(),newNickName); + // 查询新修改后的昵称 + String nickname = memberUserService.getUser(userDO.getId()).getNickname(); + // 断言 + assertEquals(newNickName,nickname); + } + + @Test + public void testUpdateAvatar_success() throws Exception { + // mock 数据 + MemberUserDO dbUser = randomUserDO(); + userMapper.insert(dbUser); + + // 准备参数 + Long userId = dbUser.getId(); + byte[] avatarFileBytes = randomBytes(10); + ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes); + // mock 方法 + String avatar = randomString(); + when(fileApi.createFile(eq(avatarFileBytes))).thenReturn(avatar); + // 调用 + String str = memberUserService.updateUserAvatar(userId, avatarFile); + // 断言 + assertEquals(avatar, str); + } + + @Test + public void updateMobile_success(){ + // mock数据 + String oldMobile = randomNumbers(11); + MemberUserDO userDO = randomUserDO(); + userDO.setMobile(oldMobile); + userMapper.insert(userDO); + + // TODO 芋艿:需要修复该单元测试,重构多模块带来的 + // 旧手机和旧验证码 +// SmsCodeDO codeDO = new SmsCodeDO(); + String oldCode = RandomUtil.randomString(4); +// codeDO.setMobile(userDO.getMobile()); +// codeDO.setCode(oldCode); +// codeDO.setScene(SmsSceneEnum.MEMBER_UPDATE_MOBILE.getScene()); +// codeDO.setUsed(Boolean.FALSE); +// when(smsCodeService.checkCodeIsExpired(codeDO.getMobile(),codeDO.getCode(),codeDO.getScene())).thenReturn(codeDO); + + // 更新手机号 + String newMobile = randomNumbers(11); + String newCode = randomNumbers(4); + AppUserUpdateMobileReqVO reqVO = new AppUserUpdateMobileReqVO(); + reqVO.setMobile(newMobile); + reqVO.setCode(newCode); + reqVO.setOldMobile(oldMobile); + reqVO.setOldCode(oldCode); + memberUserService.updateUserMobile(userDO.getId(),reqVO); + + assertEquals(memberUserService.getUser(userDO.getId()).getMobile(),newMobile); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static MemberUserDO randomUserDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + }; + return randomPojo(MemberUserDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yunxi-module-member/yunxi-module-member-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-member/yunxi-module-member-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..c81dad6 --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,49 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm.module diff --git a/yunxi-module-member/yunxi-module-member-biz/src/test/resources/logback.xml b/yunxi-module-member/yunxi-module-member-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-member/yunxi-module-member-biz/src/test/resources/sql/clean.sql b/yunxi-module-member/yunxi-module-member-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..bb8eddf --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,2 @@ +DELETE FROM "member_user"; +DELETE FROM "member_address"; diff --git a/yunxi-module-member/yunxi-module-member-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-member/yunxi-module-member-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..b60f8fa --- /dev/null +++ b/yunxi-module-member/yunxi-module-member-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,35 @@ +CREATE TABLE IF NOT EXISTS "member_user" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', + "nickname" varchar(30) NOT NULL DEFAULT '' COMMENT '用户昵称', + "avatar" varchar(255) NOT NULL DEFAULT '' COMMENT '头像', + "status" tinyint NOT NULL COMMENT '状态', + "mobile" varchar(11) NOT NULL COMMENT '手机号', + "password" varchar(100) NOT NULL DEFAULT '' COMMENT '密码', + "register_ip" varchar(32) NOT NULL COMMENT '注册 IP', + "login_ip" varchar(50) NULL DEFAULT '' COMMENT '最后登录IP', + "login_date" datetime NULL DEFAULT NULL COMMENT '最后登录时间', + "creator" varchar(64) NULL DEFAULT '' COMMENT '创建者', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + "updater" varchar(64) NULL DEFAULT '' COMMENT '更新者', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + "deleted" bit(1) NOT NULL DEFAULT '0' COMMENT '是否删除', + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '会员表'; + +CREATE TABLE IF NOT EXISTS "member_address" ( + "id" bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint(20) NOT NULL, + "name" varchar(10) NOT NULL, + "mobile" varchar(20) NOT NULL, + "area_id" bigint(20) NOT NULL, + "detail_address" varchar(250) NOT NULL, + "default_status" bit NOT NULL, + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "creator" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "updater" varchar(64) DEFAULT '', + PRIMARY KEY ("id") +) COMMENT '用户收件地址'; + diff --git a/yunxi-module-mp/pom.xml b/yunxi-module-mp/pom.xml new file mode 100644 index 0000000..f1625d5 --- /dev/null +++ b/yunxi-module-mp/pom.xml @@ -0,0 +1,24 @@ + + + + yunxi + com.yunxi.scm + ${revision} + + 4.0.0 + + yunxi-module-mp + pom + + + wechat 模块,主要实现微信平台的相关业务。 + 例如:微信公众号、企业微信 SCRM 等 + + + yunxi-module-mp-api + yunxi-module-mp-biz + + + diff --git a/yunxi-module-mp/yunxi-module-mp-api/pom.xml b/yunxi-module-mp/yunxi-module-mp-api/pom.xml new file mode 100644 index 0000000..2978e9b --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-api/pom.xml @@ -0,0 +1,26 @@ + + + + yunxi-module-mp + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-module-mp-api + jar + + ${project.artifactId} + + mp 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + diff --git a/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/ErrorCodeConstants.java b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..b80ff42 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/ErrorCodeConstants.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.module.mp.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Mp 错误码枚举类 + * + * mp 系统,使用 1-006-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== 公众号账号 1006000000============ + ErrorCode ACCOUNT_NOT_EXISTS = new ErrorCode(1006000000, "公众号账号不存在"); + ErrorCode ACCOUNT_GENERATE_QR_CODE_FAIL = new ErrorCode(1006000001, "生成公众号二维码失败,原因:{}"); + ErrorCode ACCOUNT_CLEAR_QUOTA_FAIL = new ErrorCode(1006000002, "清空公众号的 API 配额失败,原因:{}"); + + // ========== 公众号统计 1006001000============ + ErrorCode STATISTICS_GET_USER_SUMMARY_FAIL = new ErrorCode(1006001000, "获取粉丝增减数据失败,原因:{}"); + ErrorCode STATISTICS_GET_USER_CUMULATE_FAIL = new ErrorCode(1006001001, "获得粉丝累计数据失败,原因:{}"); + ErrorCode STATISTICS_GET_UPSTREAM_MESSAGE_FAIL = new ErrorCode(1006001002, "获得消息发送概况数据失败,原因:{}"); + ErrorCode STATISTICS_GET_INTERFACE_SUMMARY_FAIL = new ErrorCode(1006001003, "获得接口分析数据失败,原因:{}"); + + // ========== 公众号标签 1006002000============ + ErrorCode TAG_NOT_EXISTS = new ErrorCode(1006002000, "标签不存在"); + ErrorCode TAG_CREATE_FAIL = new ErrorCode(1006002001, "创建标签失败,原因:{}"); + ErrorCode TAG_UPDATE_FAIL = new ErrorCode(1006002002, "更新标签失败,原因:{}"); + ErrorCode TAG_DELETE_FAIL = new ErrorCode(1006002003, "删除标签失败,原因:{}"); + ErrorCode TAG_GET_FAIL = new ErrorCode(1006002004, "获得标签失败,原因:{}"); + + // ========== 公众号粉丝 1006003000============ + ErrorCode USER_NOT_EXISTS = new ErrorCode(1006003000, "粉丝不存在"); + ErrorCode USER_UPDATE_TAG_FAIL = new ErrorCode(1006003001, "更新粉丝标签失败,原因:{}"); + + // ========== 公众号素材 1006004000============ + ErrorCode MATERIAL_NOT_EXISTS = new ErrorCode(1006004000, "素材不存在"); + ErrorCode MATERIAL_UPLOAD_FAIL = new ErrorCode(1006004001, "上传素材失败,原因:{}"); + ErrorCode MATERIAL_IMAGE_UPLOAD_FAIL = new ErrorCode(1006004002, "上传图片失败,原因:{}"); + ErrorCode MATERIAL_DELETE_FAIL = new ErrorCode(1006004003, "删除素材失败,原因:{}"); + + // ========== 公众号消息 1006005000============ + ErrorCode MESSAGE_SEND_FAIL = new ErrorCode(1006005000, "发送消息失败,原因:{}"); + + // ========== 公众号发布能力 1006006000============ + ErrorCode FREE_PUBLISH_LIST_FAIL = new ErrorCode(1006006000, "获得已成功发布列表失败,原因:{}"); + ErrorCode FREE_PUBLISH_SUBMIT_FAIL = new ErrorCode(1006006001, "提交发布失败,原因:{}"); + ErrorCode FREE_PUBLISH_DELETE_FAIL = new ErrorCode(1006006002, "删除发布失败,原因:{}"); + + // ========== 公众号草稿 1006007000============ + ErrorCode DRAFT_LIST_FAIL = new ErrorCode(1006007000, "获得草稿列表失败,原因:{}"); + ErrorCode DRAFT_CREATE_FAIL = new ErrorCode(1006007001, "创建草稿失败,原因:{}"); + ErrorCode DRAFT_UPDATE_FAIL = new ErrorCode(1006007002, "更新草稿失败,原因:{}"); + ErrorCode DRAFT_DELETE_FAIL = new ErrorCode(1006007003, "删除草稿失败,原因:{}"); + + // ========== 公众号菜单 1006008000============ + ErrorCode MENU_SAVE_FAIL = new ErrorCode(1006008000, "创建菜单失败,原因:{}"); + ErrorCode MENU_DELETE_FAIL = new ErrorCode(1006008001, "删除菜单失败,原因:{}"); + + // ========== 公众号自动回复 1006009000============ + ErrorCode AUTO_REPLY_NOT_EXISTS = new ErrorCode(1006009000, "自动回复不存在"); + ErrorCode AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS = new ErrorCode(1006009001, "操作失败,原因:已存在关注时的回复"); + ErrorCode AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS = new ErrorCode(1006009002, "操作失败,原因:已存在该消息类型的回复"); + ErrorCode AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS = new ErrorCode(1006009003, "操作失败,原因:已关在该关键字的回复"); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpAutoReplyMatchEnum.java b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpAutoReplyMatchEnum.java new file mode 100644 index 0000000..2b5cda4 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpAutoReplyMatchEnum.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.mp.enums.message; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 公众号消息自动回复的匹配模式 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum MpAutoReplyMatchEnum { + + ALL(1, "完全匹配"), + LIKE(2, "半匹配"), + ; + + /** + * 匹配 + */ + private final Integer match; + /** + * 匹配的名字 + */ + private final String name; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpAutoReplyTypeEnum.java b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpAutoReplyTypeEnum.java new file mode 100644 index 0000000..93efcf8 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpAutoReplyTypeEnum.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.mp.enums.message; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 公众号消息自动回复的类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum MpAutoReplyTypeEnum { + + SUBSCRIBE(1, "关注时回复"), + MESSAGE(2, "收到消息回复"), + KEYWORD(3, "关键词回复"), + ; + + /** + * 来源 + */ + private final Integer type; + /** + * 类型的名字 + */ + private final String name; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpMessageSendFromEnum.java b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpMessageSendFromEnum.java new file mode 100644 index 0000000..6e2a82f --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/enums/message/MpMessageSendFromEnum.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.mp.enums.message; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 微信公众号消息的发送来源 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum MpMessageSendFromEnum { + + USER_TO_MP(1, "粉丝发送给公众号"), + MP_TO_USER(2, "公众号发给粉丝"), + ; + + /** + * 来源 + */ + private final Integer from; + /** + * 来源的名字 + */ + private final String name; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/package-info.java b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/package-info.java new file mode 100644 index 0000000..ca65810 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-api/src/main/java/com/yunxi/scm/module/mp/package-info.java @@ -0,0 +1,8 @@ +/** + * mp 模块,我们放微信微信公众号。 + * 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能 + * + * 1. Controller URL:以 /mp/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 mp_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.mp; diff --git a/yunxi-module-mp/yunxi-module-mp-biz/pom.xml b/yunxi-module-mp/yunxi-module-mp-biz/pom.xml new file mode 100644 index 0000000..bf384bf --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/pom.xml @@ -0,0 +1,94 @@ + + + + yunxi-module-mp + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-module-mp-biz + jar + + ${project.artifactId} + + mp 模块,我们放微信微信公众号。 + 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能 + + + + + com.yunxi.scm + yunxi-module-mp-api + ${revision} + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + com.yunxi.scm + yunxi-module-infra-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-weixin + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-tenant + + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mq + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + + + + + diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/MpAccountController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/MpAccountController.java new file mode 100644 index 0000000..19f5cf5 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/MpAccountController.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.mp.controller.admin.account; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.account.vo.*; +import com.yunxi.scm.module.mp.convert.account.MpAccountConvert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.service.account.MpAccountService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 公众号账号") +@RestController +@RequestMapping("/mp/account") +@Validated +public class MpAccountController { + + @Resource + private MpAccountService mpAccountService; + + @PostMapping("/create") + @Operation(summary = "创建公众号账号") + @PreAuthorize("@ss.hasPermission('mp:account:create')") + public CommonResult createAccount(@Valid @RequestBody MpAccountCreateReqVO createReqVO) { + return success(mpAccountService.createAccount(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新公众号账号") + @PreAuthorize("@ss.hasPermission('mp:account:update')") + public CommonResult updateAccount(@Valid @RequestBody MpAccountUpdateReqVO updateReqVO) { + mpAccountService.updateAccount(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除公众号账号") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('mp:account:delete')") + public CommonResult deleteAccount(@RequestParam("id") Long id) { + mpAccountService.deleteAccount(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得公众号账号") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('mp:account:query')") + public CommonResult getAccount(@RequestParam("id") Long id) { + MpAccountDO wxAccount = mpAccountService.getAccount(id); + return success(MpAccountConvert.INSTANCE.convert(wxAccount)); + } + + @GetMapping("/page") + @Operation(summary = "获得公众号账号分页") + @PreAuthorize("@ss.hasPermission('mp:account:query')") + public CommonResult> getAccountPage(@Valid MpAccountPageReqVO pageVO) { + PageResult pageResult = mpAccountService.getAccountPage(pageVO); + return success(MpAccountConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取公众号账号精简信息列表") + @PreAuthorize("@ss.hasPermission('mp:account:query')") + public CommonResult> getSimpleAccounts() { + List list = mpAccountService.getAccountList(); + return success(MpAccountConvert.INSTANCE.convertList02(list)); + } + + @PutMapping("/generate-qr-code") + @Operation(summary = "生成公众号二维码") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('mp:account:qr-code')") + public CommonResult generateAccountQrCode(@RequestParam("id") Long id) { + mpAccountService.generateAccountQrCode(id); + return success(true); + } + + @PutMapping("/clear-quota") + @Operation(summary = "清空公众号 API 配额") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('mp:account:clear-quota')") + public CommonResult clearAccountQuota(@RequestParam("id") Long id) { + mpAccountService.clearAccountQuota(id); + return success(true); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountBaseVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountBaseVO.java new file mode 100644 index 0000000..fd77e15 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountBaseVO.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.mp.controller.admin.account.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * 公众号账号 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author fengdan + */ +@Data +public class MpAccountBaseVO { + + @Schema(description = "公众号名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + @NotEmpty(message = "公众号名称不能为空") + private String name; + + @Schema(description = "公众号微信号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxiyuanma") + @NotEmpty(message = "公众号微信号不能为空") + private String account; + + @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx5b23ba7a5589ecbb") + @NotEmpty(message = "公众号 appId 不能为空") + private String appId; + + @Schema(description = "公众号密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "3a7b3b20c537e52e74afd395eb85f61f") + @NotEmpty(message = "公众号密钥不能为空") + private String appSecret; + + @Schema(description = "公众号 token", requiredMode = Schema.RequiredMode.REQUIRED, example = "kangdayuzhen") + @NotEmpty(message = "公众号 token 不能为空") + private String token; + + @Schema(description = "加密密钥", example = "gjN+Ksei") + private String aesKey; + + @Schema(description = "备注", example = "请关注芋道源码,学习技术") + private String remark; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java new file mode 100644 index 0000000..50ad5bc --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.mp.controller.admin.account.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 公众号账号创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAccountCreateReqVO extends MpAccountBaseVO { + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java new file mode 100644 index 0000000..5ad1a80 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountPageReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.mp.controller.admin.account.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 公众号账号分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAccountPageReqVO extends PageParam { + + @Schema(name = "公众号名称", description = "模糊匹配") + private String name; + + @Schema(name = "公众号账号", description = "模糊匹配") + private String account; + + @Schema(name = "公众号 appid", description = "模糊匹配") + private String appId; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountRespVO.java new file mode 100644 index 0000000..4a800d6 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.mp.controller.admin.account.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@Schema(description = "管理后台 - 公众号账号 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAccountRespVO extends MpAccountBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "二维码图片URL", example = "https://www.iocoder.cn/1024.png") + private String qrCodeUrl; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java new file mode 100644 index 0000000..586fe44 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountSimpleRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.mp.controller.admin.account.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 公众号账号精简信息 Response VO") +@Data +public class MpAccountSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String name; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java new file mode 100644 index 0000000..0d67396 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/account/vo/MpAccountUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.mp.controller.admin.account.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号账号更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAccountUpdateReqVO extends MpAccountBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/MpMaterialController.http b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/MpMaterialController.http new file mode 100644 index 0000000..74b8f40 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/MpMaterialController.http @@ -0,0 +1,5 @@ +### 请求 /mp/material/page 接口 => 成功 +GET {{baseUrl}}/mp/material/page?permanent=true&pageNo=1&pageSize=10 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/MpMaterialController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/MpMaterialController.java new file mode 100644 index 0000000..3bb46a5 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/MpMaterialController.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.mp.controller.admin.material; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.material.vo.*; +import com.yunxi.scm.module.mp.convert.material.MpMaterialConvert; +import com.yunxi.scm.module.mp.dal.dataobject.material.MpMaterialDO; +import com.yunxi.scm.module.mp.service.material.MpMaterialService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.io.IOException; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 公众号素材") +@RestController +@RequestMapping("/mp/material") +@Validated +public class MpMaterialController { + + @Resource + private MpMaterialService mpMaterialService; + + @Operation(summary = "上传临时素材") + @PostMapping("/upload-temporary") + @PreAuthorize("@ss.hasPermission('mp:material:upload-temporary')") + public CommonResult uploadTemporaryMaterial( + @Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException { + MpMaterialDO material = mpMaterialService.uploadTemporaryMaterial(reqVO); + return success(MpMaterialConvert.INSTANCE.convert(material)); + } + + @Operation(summary = "上传永久素材") + @PostMapping("/upload-permanent") + @PreAuthorize("@ss.hasPermission('mp:material:upload-permanent')") + public CommonResult uploadPermanentMaterial( + @Valid MpMaterialUploadPermanentReqVO reqVO) throws IOException { + MpMaterialDO material = mpMaterialService.uploadPermanentMaterial(reqVO); + return success(MpMaterialConvert.INSTANCE.convert(material)); + } + + @Operation(summary = "删除素材") + @DeleteMapping("/delete-permanent") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('mp:material:delete')") + public CommonResult deleteMaterial(@RequestParam("id") Long id) { + mpMaterialService.deleteMaterial(id); + return success(true); + } + + @Operation(summary = "上传图文内容中的图片") + @PostMapping("/upload-news-image") + @PreAuthorize("@ss.hasPermission('mp:material:upload-news-image')") + public CommonResult uploadNewsImage(@Valid MpMaterialUploadNewsImageReqVO reqVO) + throws IOException { + return success(mpMaterialService.uploadNewsImage(reqVO)); + } + + @Operation(summary = "获得素材分页") + @GetMapping("/page") + @PreAuthorize("@ss.hasPermission('mp:material:query')") + public CommonResult> getMaterialPage(@Valid MpMaterialPageReqVO pageReqVO) { + PageResult pageResult = mpMaterialService.getMaterialPage(pageReqVO); + return success(MpMaterialConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java new file mode 100644 index 0000000..f34ac02 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialPageReqVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.mp.controller.admin.material.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号素材的分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpMaterialPageReqVO extends PageParam { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "是否永久", example = "true") + private Boolean permanent; + + @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", example = "image") + private String type; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialRespVO.java new file mode 100644 index 0000000..378032d --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialRespVO.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.mp.controller.admin.material.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Schema(description = "管理后台 - 公众号素材 Response VO") +@Data +public class MpMaterialRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountId; + @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") + private String appId; + + @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private String mediaId; + + @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image") + private String type; + + @Schema(description = "是否永久 true - 永久;false - 临时", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + private Boolean permanent; + + @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String url; + + + @Schema(description = "名字", example = "yunai.png") + private String name; + + @Schema(description = "公众号文件 URL 只有【永久素材】使用", example = "https://mmbiz.qpic.cn/xxx.mp3") + private String mpUrl; + + @Schema(description = "视频素材的标题 只有【永久素材】使用", example = "我是标题") + private String title; + @Schema(description = "视频素材的描述 只有【永久素材】使用", example = "我是介绍") + private String introduction; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java new file mode 100644 index 0000000..bd71247 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadNewsImageReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.mp.controller.admin.material.vo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号素材上传图文内容中的图片 Request VO") +@Data +public class MpMaterialUploadNewsImageReqVO { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件不能为空") + @JsonIgnore // 避免被操作日志,进行序列化,导致报错 + private MultipartFile file; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java new file mode 100644 index 0000000..38d893d --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadPermanentReqVO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.mp.controller.admin.material.vo; + +import cn.hutool.core.util.ObjectUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号素材上传永久 Request VO") +@Data +public class MpMaterialUploadPermanentReqVO { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image") + @NotEmpty(message = "文件类型不能为空") + private String type; + + @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件不能为空") + @JsonIgnore // 避免被操作日志,进行序列化,导致报错 + private MultipartFile file; + + @Schema(description = "名字 如果 name 为空,则使用 file 文件名", example = "wechat.mp") + private String name; + + @Schema(description = "视频素材的标题 文件类型为 video 时,必填", example = "视频素材的标题") + private String title; + @Schema(description = "视频素材的描述 文件类型为 video 时,必填", example = "视频素材的描述") + private String introduction; + + @AssertTrue(message = "标题不能为空") + public boolean isTitleValid() { + // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的 + return ObjectUtil.notEqual(type, WxConsts.MediaFileType.VIDEO) + || title != null; + } + + @AssertTrue(message = "描述不能为空") + public boolean isIntroductionValid() { + // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的 + return ObjectUtil.notEqual(type, WxConsts.MediaFileType.VIDEO) + || introduction != null; + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java new file mode 100644 index 0000000..f497a87 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.mp.controller.admin.material.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 公众号素材上传结果 Response VO") +@Data +public class MpMaterialUploadRespVO { + + @Schema(description = "素材的 media_id", requiredMode = Schema.RequiredMode.REQUIRED, example = "123") + private String mediaId; + + @Schema(description = "素材的 URL", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png") + private String url; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java new file mode 100644 index 0000000..61b259f --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/material/vo/MpMaterialUploadTemporaryReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.mp.controller.admin.material.vo; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号素材上传临时 Request VO") +@Data +public class MpMaterialUploadTemporaryReqVO { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "文件类型 参见 WxConsts.MediaFileType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "image") + @NotEmpty(message = "文件类型不能为空") + private String type; + + @Schema(description = "文件附件", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "文件不能为空") + @JsonIgnore // 避免被操作日志,进行序列化,导致报错 + private MultipartFile file; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/MpMenuController.http b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/MpMenuController.http new file mode 100644 index 0000000..2276b3b --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/MpMenuController.http @@ -0,0 +1,50 @@ +### 请求 /mp/menu/save 接口 => 成功 +POST {{baseUrl}}/mp/menu/save +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "accountId": "1", + "menus": [ + { + "type":"click", + "name":"今日歌曲", + "menuKey":"V1001_TODAY_MUSIC" + }, + { + "name":"搜索", + "type":"view", + "url":"https://www.soso.com/" + }, + { + "name": "父按钮", + "children": [ + { + "type":"click", + "name":"归去来兮", + "menuKey":"MUSIC" + }, + { + "name":"不说", + "type":"view", + "url":"https://www.soso.com/" + }] + }] +} + +### 请求 /mp/menu/save 接口 => 成功(清空) +POST {{baseUrl}}/mp/menu/save +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "accountId": "1", + "menus": [] +} + +### 请求 /mp/menu/list 接口 => 成功 +GET {{baseUrl}}/mp/menu/list?accountId=1 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/MpMenuController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/MpMenuController.java new file mode 100644 index 0000000..b0f023d --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/MpMenuController.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.mp.controller.admin.menu; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.mp.controller.admin.menu.vo.MpMenuRespVO; +import com.yunxi.scm.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import com.yunxi.scm.module.mp.convert.menu.MpMenuConvert; +import com.yunxi.scm.module.mp.dal.dataobject.menu.MpMenuDO; +import com.yunxi.scm.module.mp.service.menu.MpMenuService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 公众号菜单") +@RestController +@RequestMapping("/mp/menu") +@Validated +public class MpMenuController { + + @Resource + private MpMenuService mpMenuService; + + @PostMapping("/save") + @Operation(summary = "保存公众号菜单") + @PreAuthorize("@ss.hasPermission('mp:menu:save')") + public CommonResult saveMenu(@Valid @RequestBody MpMenuSaveReqVO createReqVO) { + mpMenuService.saveMenu(createReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除公众号菜单") + @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "10") + @PreAuthorize("@ss.hasPermission('mp:menu:delete')") + public CommonResult deleteMenu(@RequestParam("accountId") Long accountId) { + mpMenuService.deleteMenuByAccountId(accountId); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获得公众号菜单列表") + @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "10") + @PreAuthorize("@ss.hasPermission('mp:menu:query')") + public CommonResult> getMenuList(@RequestParam("accountId") Long accountId) { + List list = mpMenuService.getMenuListByAccountId(accountId); + return success(MpMenuConvert.INSTANCE.convertList(list)); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java new file mode 100644 index 0000000..b9f4a32 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuBaseVO.java @@ -0,0 +1,115 @@ +package com.yunxi.scm.module.mp.controller.admin.menu.vo; + +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; +import org.hibernate.validator.constraints.URL; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +import static com.yunxi.scm.module.mp.framework.mp.core.util.MpUtils.*; + +/** + * 公众号菜单 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MpMenuBaseVO { + + /** + * 菜单名称 + */ + private String name; + /** + * 菜单标识 + * + * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错 + */ + private String menuKey; + /** + * 父菜单编号 + */ + private Long parentId; + + // ========== 按钮操作 ========== + + /** + * 按钮类型 + * + * 枚举 {@link WxConsts.MenuButtonType} + */ + private String type; + + @Schema(description = "网页链接", example = "https://www.iocoder.cn/") + @NotEmpty(message = "网页链接不能为空", groups = {ViewButtonGroup.class, MiniProgramButtonGroup.class}) + @URL(message = "网页链接必须是 URL 格式") + private String url; + + @Schema(description = "小程序的 appId", example = "wx1234567890") + @NotEmpty(message = "小程序的 appId 不能为空", groups = MiniProgramButtonGroup.class) + private String miniProgramAppId; + + @Schema(description = "小程序的页面路径", example = "pages/index/index") + @NotEmpty(message = "小程序的页面路径不能为空", groups = MiniProgramButtonGroup.class) + private String miniProgramPagePath; + + @Schema(description ="跳转图文的媒体编号", example = "jCQk93AIIgp8ixClWcW_NXXqBKInNWNmq2XnPeDZl7IMVqWiNeL4FfELtggRXd83") + @NotEmpty(message = "跳转图文的媒体编号不能为空", groups = ViewLimitedButtonGroup.class) + private String articleId; + + // ========== 消息内容 ========== + + @Schema(description = "回复的消息类型 枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC", example = "text") + @NotEmpty(message = "回复的消息类型不能为空", groups = {ClickButtonGroup.class, ScanCodeWaitMsgButtonGroup.class}) + private String replyMessageType; + + @Schema(description = "回复的消息内容", example = "欢迎关注") + @NotEmpty(message = "回复的消息内容不能为空", groups = TextMessageGroup.class) + private String replyContent; + + @Schema(description = "回复的媒体 id", example = "123456") + @NotEmpty(message = "回复的消息 mediaId 不能为空", + groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) + private String replyMediaId; + @Schema(description = "回复的媒体 URL", example = "https://www.iocoder.cn/xxx.jpg") + @NotEmpty(message = "回复的消息 mediaId 不能为空", + groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) + private String replyMediaUrl; + + @Schema(description = "缩略图的媒体 id", example = "123456") + @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class}) + private String replyThumbMediaId; + @Schema(description = "缩略图的媒体 URL",example = "https://www.iocoder.cn/xxx.jpg") + @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class}) + private String replyThumbMediaUrl; + + @Schema(description = "回复的标题", example = "视频标题") + @NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class) + private String replyTitle; + @Schema(description = "回复的描述", example = "视频描述") + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) + private String replyDescription; + + /** + * 回复的图文消息数组 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @NotNull(message = "回复的图文消息不能为空", groups = {NewsMessageGroup.class, ViewLimitedButtonGroup.class}) + @Valid + private List replyArticles; + + @Schema(description = "回复的音乐链接", example = "https://www.iocoder.cn/xxx.mp3") + @NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class) + @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class) + private String replyMusicUrl; + @Schema(description = "高质量音乐链接", example = "https://www.iocoder.cn/xxx.mp3") + @NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class) + @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class) + private String replyHqMusicUrl; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuRespVO.java new file mode 100644 index 0000000..55c0d0a --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuRespVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.mp.controller.admin.menu.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@Schema(description = "管理后台 - 公众号菜单 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpMenuRespVO extends MpMenuBaseVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long accountId; + + @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890ox") + private String appId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java new file mode 100644 index 0000000..45086e4 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/menu/vo/MpMenuSaveReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.mp.controller.admin.menu.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 公众号菜单保存 Request VO") +@Data +public class MpMenuSaveReqVO { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @NotEmpty(message = "菜单不能为空") + @Valid + private List

menus; + + @Schema(description = "管理后台 - 公众号菜单保存时的每个菜单") + @Data + public static class Menu extends MpMenuBaseVO { + + /** + * 子菜单数组 + */ + private List children; + + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpAutoReplyController.http b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpAutoReplyController.http new file mode 100644 index 0000000..dbb3a7b --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpAutoReplyController.http @@ -0,0 +1,5 @@ +### 请求 /mp/message/page 接口 => 成功 +GET {{baseUrl}}/mp/auto-reply/page?accountId=1&pageNo=1&pageSize=10 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpAutoReplyController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpAutoReplyController.java new file mode 100644 index 0000000..c46ccd8 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpAutoReplyController.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.mp.controller.admin.message; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyRespVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import com.yunxi.scm.module.mp.convert.message.MpAutoReplyConvert; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpAutoReplyDO; +import com.yunxi.scm.module.mp.service.message.MpAutoReplyService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 公众号自动回复") +@RestController +@RequestMapping("/mp/auto-reply") +@Validated +public class MpAutoReplyController { + + @Resource + private MpAutoReplyService mpAutoReplyService; + + @GetMapping("/page") + @Operation(summary = "获得公众号自动回复分页") + @PreAuthorize("@ss.hasPermission('mp:auto-reply:query')") + public CommonResult> getAutoReplyPage(@Valid MpMessagePageReqVO pageVO) { + PageResult pageResult = mpAutoReplyService.getAutoReplyPage(pageVO); + return success(MpAutoReplyConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/get") + @Operation(summary = "获得公众号自动回复") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('mp:auto-reply:query')") + public CommonResult getAutoReply(@RequestParam("id") Long id) { + MpAutoReplyDO autoReply = mpAutoReplyService.getAutoReply(id); + return success(MpAutoReplyConvert.INSTANCE.convert(autoReply)); + } + + @PostMapping("/create") + @Operation(summary = "创建公众号自动回复") + @PreAuthorize("@ss.hasPermission('mp:auto-reply:create')") + public CommonResult createAutoReply(@Valid @RequestBody MpAutoReplyCreateReqVO createReqVO) { + return success(mpAutoReplyService.createAutoReply(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新公众号自动回复") + @PreAuthorize("@ss.hasPermission('mp:auto-reply:update')") + public CommonResult updateAutoReply(@Valid @RequestBody MpAutoReplyUpdateReqVO updateReqVO) { + mpAutoReplyService.updateAutoReply(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除公众号自动回复") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('mp:auto-reply:delete')") + public CommonResult deleteAutoReply(@RequestParam("id") Long id) { + mpAutoReplyService.deleteAutoReply(id); + return success(true); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpMessageController.http b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpMessageController.http new file mode 100644 index 0000000..b9f9721 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpMessageController.http @@ -0,0 +1,33 @@ +### 请求 /mp/message/page 接口 => 成功 +GET {{baseUrl}}/mp/message/page?accountId=1&pageNo=1&pageSize=10 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /mp/message/send 接口 => 成功(文本) +POST {{baseUrl}}/mp/message/send +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "userId": 3, + "type": "text", + "content": "测试消息" +} + +### 请求 /mp/message/send 接口 => 成功(音乐) +POST {{baseUrl}}/mp/message/send +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "userId": 3, + "type": "music", + "title": "测试音乐标题", + "description": "测试音乐内容", + "musicUrl": "https://www.iocoder.cn/xx.mp3", + "hqMusicUrl": "https://www.iocoder.cn/xx_high.mp3", + "thumbMediaId": "s98Iveeg9vDVFwa9q0u8-zSfdKe3xIzAm7wCrFE4WKGPIo4d9qAhtC-n6qvnyWyH" +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpMessageController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpMessageController.java new file mode 100644 index 0000000..1bf460c --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/MpMessageController.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.mp.controller.admin.message; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessageRespVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import com.yunxi.scm.module.mp.convert.message.MpMessageConvert; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.yunxi.scm.module.mp.service.message.MpMessageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 公众号消息") +@RestController +@RequestMapping("/mp/message") +@Validated +public class MpMessageController { + + @Resource + private MpMessageService mpMessageService; + + @GetMapping("/page") + @Operation(summary = "获得公众号消息分页") + @PreAuthorize("@ss.hasPermission('mp:message:query')") + public CommonResult> getMessagePage(@Valid MpMessagePageReqVO pageVO) { + PageResult pageResult = mpMessageService.getMessagePage(pageVO); + return success(MpMessageConvert.INSTANCE.convertPage(pageResult)); + } + + @PostMapping("/send") + @Operation(summary = "给粉丝发送消息") + @PreAuthorize("@ss.hasPermission('mp:message:send')") + public CommonResult sendMessage(@Valid @RequestBody MpMessageSendReqVO reqVO) { + MpMessageDO message = mpMessageService.sendKefuMessage(reqVO); + return success(MpMessageConvert.INSTANCE.convert(message)); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java new file mode 100644 index 0000000..96b152c --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyBaseVO.java @@ -0,0 +1,109 @@ +package com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.yunxi.scm.module.mp.enums.message.MpAutoReplyTypeEnum; +import com.yunxi.scm.module.mp.framework.mp.core.util.MpUtils.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; +import org.hibernate.validator.constraints.URL; + +import javax.validation.Valid; +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 公众号自动回复 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MpAutoReplyBaseVO { + + @Schema(description = "回复类型 参见 MpAutoReplyTypeEnum 枚举", example = "1") + @NotNull(message = "回复类型不能为空") + private Integer type; + + // ==================== 请求消息 ==================== + + @Schema(description = "请求的关键字 当 type 为 MpAutoReplyTypeEnum#KEYWORD 时,必填", example = "关键字") + private String requestKeyword; + @Schema(description = "请求的匹配方式 当 type 为 MpAutoReplyTypeEnum#KEYWORD 时,必填", example = "1") + private Integer requestMatch; + + @Schema(description = "请求的消息类型 当 type 为 MpAutoReplyTypeEnum#MESSAGE 时,必填", example = "text") + private String requestMessageType; + + // ==================== 响应消息 ==================== + + @Schema(description = "回复的消息类型 枚举 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC", example = "text") + @NotEmpty(message = "回复的消息类型不能为空") + private String responseMessageType; + + @Schema(description = "回复的消息内容", example = "欢迎关注") + @NotEmpty(message = "回复的消息内容不能为空", groups = TextMessageGroup.class) + private String responseContent; + + @Schema(description = "回复的媒体 id", example = "123456") + @NotEmpty(message = "回复的消息 mediaId 不能为空", + groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) + private String responseMediaId; + @Schema(description = "回复的媒体 URL", example = "https://www.iocoder.cn/xxx.jpg") + @NotEmpty(message = "回复的消息 mediaId 不能为空", + groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) + private String responseMediaUrl; + + @Schema(description = "缩略图的媒体 id", example = "123456") + @NotEmpty(message = "回复的消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class}) + private String responseThumbMediaId; + @Schema(description = "缩略图的媒体 URL",example = "https://www.iocoder.cn/xxx.jpg") + @NotEmpty(message = "回复的消息 thumbMedia 地址不能为空", groups = {MusicMessageGroup.class}) + private String responseThumbMediaUrl; + + @Schema(description = "回复的标题", example = "视频标题") + @NotEmpty(message = "回复的消息标题不能为空", groups = VideoMessageGroup.class) + private String responseTitle; + @Schema(description = "回复的描述", example = "视频描述") + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) + private String responseDescription; + + /** + * 回复的图文消息 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @NotNull(message = "回复的图文消息不能为空", groups = {NewsMessageGroup.class, ViewLimitedButtonGroup.class}) + @Valid + private List responseArticles; + + @Schema(description = "回复的音乐链接", example = "https://www.iocoder.cn/xxx.mp3") + @NotEmpty(message = "回复的音乐链接不能为空", groups = MusicMessageGroup.class) + @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class) + private String responseMusicUrl; + @Schema(description = "高质量音乐链接", example = "https://www.iocoder.cn/xxx.mp3") + @NotEmpty(message = "回复的高质量音乐链接不能为空", groups = MusicMessageGroup.class) + @URL(message = "回复的高质量音乐链接格式不正确", groups = MusicMessageGroup.class) + private String responseHqMusicUrl; + + @AssertTrue(message = "请求的关键字不能为空") + public boolean isRequestKeywordValid() { + return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.KEYWORD) + || requestKeyword != null; + } + + @AssertTrue(message = "请求的关键字的匹配不能为空") + public boolean isRequestMatchValid() { + return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.KEYWORD) + || requestMatch != null; + } + + @AssertTrue(message = "请求的消息类型不能为空") + public boolean isRequestMessageTypeValid() { + return ObjectUtil.notEqual(type, MpAutoReplyTypeEnum.MESSAGE) + || requestMessageType != null; + } + + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java new file mode 100644 index 0000000..f8456ba --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号自动回复的创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAutoReplyCreateReqVO extends MpAutoReplyBaseVO { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java new file mode 100644 index 0000000..17c2975 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyPageReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号自动回复的分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAutoReplyPageReqVO extends PageParam { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java new file mode 100644 index 0000000..3c8a378 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyRespVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@Schema(description = "管理后台 - 公众号自动回复 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAutoReplyRespVO extends MpAutoReplyBaseVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long accountId; + @Schema(description = "公众号 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") + private String appId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java new file mode 100644 index 0000000..8e94240 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/autoreply/MpAutoReplyUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号自动回复的更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAutoReplyUpdateReqVO extends MpAutoReplyBaseVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "主键不能为空") + private Long id; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java new file mode 100644 index 0000000..ef4ae83 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessagePageReqVO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.mp.controller.admin.message.vo.message; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 公众号消息分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpMessagePageReqVO extends PageParam { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "消息类型 参见 WxConsts.XmlMsgType 枚举", example = "text") + private String type; + + @Schema(description = "公众号粉丝标识", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String openid; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java new file mode 100644 index 0000000..c969111 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessageRespVO.java @@ -0,0 +1,101 @@ +package com.yunxi.scm.module.mp.controller.admin.message.vo.message; + +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.baomidou.mybatisplus.annotation.TableField; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; + +import java.util.Date; +import java.util.List; + +@Schema(description = "管理后台 - 公众号消息 Response VO") +@Data +public class MpMessageRespVO { + + @Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Integer id; + + @Schema(description = "微信公众号消息 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "23953173569869169") + private Long msgId; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountId; + @Schema(description = "公众号账号的 appid", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") + private String appId; + + @Schema(description = "公众号粉丝编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long userId; + @Schema(description = "公众号粉丝标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String openid; + + @Schema(description = "消息类型 参见 WxConsts.XmlMsgType 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "text") + private String type; + @Schema(description = "消息来源 参见 MpMessageSendFromEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sendFrom; + + // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html + + @Schema(description = "消息内容 消息类型为 text 时,才有值", example = "你好呀") + private String content; + + @Schema(description = "媒体素材的编号 消息类型为 image、voice、video 时,才有值", example = "1234567890") + private String mediaId; + @Schema(description = "媒体文件的 URL 消息类型为 image、voice、video 时,才有值", example = "https://www.iocoder.cn/xxx.png") + private String mediaUrl; + + @Schema(description = "语音识别后文本 消息类型为 voice 时,才有值", example = "语音识别后文本") + private String recognition; + @Schema(description = "语音格式 消息类型为 voice 时,才有值", example = "amr") + private String format; + + @Schema(description = "标题 消息类型为 video、music、link 时,才有值", example = "我是标题") + private String title; + + @Schema(description = "描述 消息类型为 video、music 时,才有值", example = "我是描述") + private String description; + + @Schema(description = "缩略图的媒体 id 消息类型为 video、music 时,才有值", example = "1234567890") + private String thumbMediaId; + @Schema(description = "缩略图的媒体 URL 消息类型为 video、music 时,才有值", example = "https://www.iocoder.cn/xxx.png") + private String thumbMediaUrl; + + @Schema(description = "点击图文消息跳转链接 消息类型为 link 时,才有值", example = "https://www.iocoder.cn") + private String url; + + @Schema(description = "地理位置维度 消息类型为 location 时,才有值", example = "23.137466") + private Double locationX; + + @Schema(description = "地理位置经度 消息类型为 location 时,才有值", example = "113.352425") + private Double locationY; + + @Schema(description = "地图缩放大小 消息类型为 location 时,才有值", example = "13") + private Double scale; + + @Schema(description = "详细地址 消息类型为 location 时,才有值", example = "杨浦区黄兴路 221-4 号临") + private String label; + + /** + * 图文消息数组 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) + private List articles; + + @Schema(description = "音乐链接 消息类型为 music 时,才有值", example = "https://www.iocoder.cn/xxx.mp3") + private String musicUrl; + @Schema(description = "高质量音乐链接 消息类型为 music 时,才有值", example = "https://www.iocoder.cn/xxx.mp3") + private String hqMusicUrl; + + // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html + + @Schema(description = "事件类型 参见 WxConsts.EventType 枚举", example = "subscribe") + private String event; + @Schema(description = "事件 Key 参见 WxConsts.EventType 枚举", example = "qrscene_123456") + private String eventKey; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java new file mode 100644 index 0000000..153ceeb --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/message/vo/message/MpMessageSendReqVO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.mp.controller.admin.message.vo.message; + +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.yunxi.scm.module.mp.framework.mp.core.util.MpUtils.*; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 公众号消息发送 Request VO") +@Data +public class MpMessageSendReqVO { + + @Schema(description = "公众号粉丝的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "公众号粉丝的编号不能为空") + private Long userId; + + // ========== 消息内容 ========== + + @Schema(description = "消息类型 TEXT/IMAGE/VOICE/VIDEO/NEWS", requiredMode = Schema.RequiredMode.REQUIRED, example = "text") + @NotEmpty(message = "消息类型不能为空") + public String type; + + @Schema(description = "消息内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好呀") + @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class) + private String content; + + @Schema(description = "媒体 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP") + @NotEmpty(message = "消息内容不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) + private String mediaId; + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "没有标题") + @NotEmpty(message = "消息内容不能为空", groups = VideoMessageGroup.class) + private String title; + + @Schema(description = "描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "你猜") + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) + private String description; + + @Schema(description = "缩略图的媒体 id", requiredMode = Schema.RequiredMode.REQUIRED, example = "qqc_2Fot30Jse-HDoZmo5RrUDijz2nGUkP") + @NotEmpty(message = "缩略图的媒体 id 不能为空", groups = MusicMessageGroup.class) + private String thumbMediaId; + + @Schema(description = "图文消息", requiredMode = Schema.RequiredMode.REQUIRED) + @Valid + @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class) + private List articles; + + @Schema(description = "音乐链接 消息类型为 MUSIC 时", example = "https://www.iocoder.cn/music.mp3") + private String musicUrl; + + @Schema(description = "高质量音乐链接 消息类型为 MUSIC 时", example = "https://www.iocoder.cn/music.mp3") + private String hqMusicUrl; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpDraftController.http b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpDraftController.http new file mode 100644 index 0000000..87f9d43 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpDraftController.http @@ -0,0 +1,54 @@ +### 请求 /mp/draft/page 接口 => 成功 +GET {{baseUrl}}/mp/draft/page?accountId=1&pageNo=1&pageSize=10 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /mp/draft/create 接口 => 成功 +POST {{baseUrl}}/mp/draft/create?accountId=1 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "articles": [ + { + "title": "我是标题", + "author": "我是作者", + "digest": "我是摘要", + "content": "我是内容", + "contentSourceUrl": "https://www.iocoder.cn", + "thumbMediaId": "r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn" + }, + { + "title": "我是标题 2", + "author": "我是作者 2", + "digest": "我是摘要 2", + "content": "我是内容 2", + "contentSourceUrl": "https://www.iocoder.cn", + "thumbMediaId": "r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn" + } + ] +} + +### 请求 /mp/draft/create 接口 => 成功 +PUT {{baseUrl}}/mp/draft/update?accountId=1&mediaId=r6ryvl6LrxBU0miaST4Y-q-G9pdsmZw0OYG4FzHQkKfpLfEwIH51wy2bxisx8PvW +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +[{ + "title": "我是标题(OOO)", + "author": "我是作者", + "digest": "我是摘要", + "content": "我是内容", + "contentSourceUrl": "https://www.iocoder.cn", + "thumbMediaId": "r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn" +}, { + "title": "我是标题(XXX)", + "author": "我是作者", + "digest": "我是摘要", + "content": "我是内容", + "contentSourceUrl": "https://www.iocoder.cn", + "thumbMediaId": "r6ryvl6LrxBU0miaST4Y-pIcmK-zAAId-9TGgy-DrSLhjVuWbuT3ZBjk9K1yQ0Dn" +}] diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpDraftController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpDraftController.java new file mode 100644 index 0000000..35bc752 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpDraftController.java @@ -0,0 +1,136 @@ +package com.yunxi.scm.module.mp.controller.admin.news; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.object.PageUtils; +import com.yunxi.scm.module.mp.controller.admin.news.vo.MpDraftPageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.material.MpMaterialDO; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.service.material.MpMaterialService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.draft.*; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.MapUtils.findAndThen; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.*; + +@Tag(name = "管理后台 - 公众号草稿") +@RestController +@RequestMapping("/mp/draft") +@Validated +public class MpDraftController { + + @Resource + private MpServiceFactory mpServiceFactory; + + @Resource + private MpMaterialService mpMaterialService; + + @GetMapping("/page") + @Operation(summary = "获得草稿分页") + @PreAuthorize("@ss.hasPermission('mp:draft:query')") + public CommonResult> getDraftPage(MpDraftPageReqVO reqVO) { + // 从公众号查询草稿箱 + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); + WxMpDraftList draftList; + try { + draftList = mpService.getDraftService().listDraft(PageUtils.getStart(reqVO), reqVO.getPageSize()); + } catch (WxErrorException e) { + throw exception(DRAFT_LIST_FAIL, e.getError().getErrorMsg()); + } + // 查询对应的图片地址。目的:解决公众号的图片链接无法在我们后台展示 + setDraftThumbUrl(draftList.getItems()); + + // 返回分页 + return success(new PageResult<>(draftList.getItems(), draftList.getTotalCount().longValue())); + } + + private void setDraftThumbUrl(List items) { + // 1.1 获得 mediaId 数组 + Set mediaIds = new HashSet<>(); + items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId()))); + if (CollUtil.isEmpty(mediaIds)) { + return; + } + // 1.2 批量查询对应的 Media 素材 + Map materials = CollectionUtils.convertMap(mpMaterialService.getMaterialListByMediaId(mediaIds), + MpMaterialDO::getMediaId); + + // 2. 设置回 WxMpDraftItem 记录 + items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> + findAndThen(materials, newsItem.getThumbMediaId(), material -> newsItem.setThumbUrl(material.getUrl())))); + } + + @PostMapping("/create") + @Operation(summary = "创建草稿") + @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('mp:draft:create')") + public CommonResult deleteDraft(@RequestParam("accountId") Long accountId, + @RequestBody WxMpAddDraft draft) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + String mediaId = mpService.getDraftService().addDraft(draft); + return success(mediaId); + } catch (WxErrorException e) { + throw exception(DRAFT_CREATE_FAIL, e.getError().getErrorMsg()); + } + } + + @PutMapping("/update") + @Operation(summary = "更新草稿") + @Parameters({ + @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"), + @Parameter(name = "mediaId", description = "草稿素材的编号", required = true, example = "xxx") + }) + @PreAuthorize("@ss.hasPermission('mp:draft:update')") + public CommonResult deleteDraft(@RequestParam("accountId") Long accountId, + @RequestParam("mediaId") String mediaId, + @RequestBody List articles) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + for (int i = 0; i < articles.size(); i++) { + WxMpDraftArticles article = articles.get(i); + mpService.getDraftService().updateDraft(new WxMpUpdateDraft(mediaId, i, article)); + } + return success(true); + } catch (WxErrorException e) { + throw exception(DRAFT_UPDATE_FAIL, e.getError().getErrorMsg()); + } + } + + @DeleteMapping("/delete") + @Operation(summary = "删除草稿") + @Parameters({ + @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"), + @Parameter(name = "mediaId", description = "草稿素材的编号", required = true, example = "xxx") + }) + @PreAuthorize("@ss.hasPermission('mp:draft:delete')") + public CommonResult deleteDraft(@RequestParam("accountId") Long accountId, + @RequestParam("mediaId") String mediaId) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + mpService.getDraftService().delDraft(mediaId); + return success(true); + } catch (WxErrorException e) { + throw exception(DRAFT_DELETE_FAIL, e.getError().getErrorMsg()); + } + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpFreePublishController.http b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpFreePublishController.http new file mode 100644 index 0000000..1224132 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpFreePublishController.http @@ -0,0 +1,13 @@ +### 请求 /mp/free-publish/page 接口 => 成功 +GET {{baseUrl}}/mp/free-publish/page?accountId=1&pageNo=1&pageSize=10 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /mp/free-publish/submit 接口 => 成功 +POST {{baseUrl}}/mp/free-publish/submit?accountId=1&mediaId=r6ryvl6LrxBU0miaST4Y-vilmd7iS51D8IPddxflWrau0hIQ2ovY8YanO5jlgUcM +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpFreePublishController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpFreePublishController.java new file mode 100644 index 0000000..23c3dd7 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/MpFreePublishController.java @@ -0,0 +1,119 @@ +package com.yunxi.scm.module.mp.controller.admin.news; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.object.PageUtils; +import com.yunxi.scm.module.mp.controller.admin.news.vo.MpFreePublishPageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.material.MpMaterialDO; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.service.material.MpMaterialService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.tags.Tag; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.freepublish.WxMpFreePublishItem; +import me.chanjar.weixin.mp.bean.freepublish.WxMpFreePublishList; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.MapUtils.findAndThen; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.*; + +@Tag(name = "管理后台 - 公众号发布能力") +@RestController +@RequestMapping("/mp/free-publish") +@Validated +public class MpFreePublishController { + + @Resource + private MpServiceFactory mpServiceFactory; + + @Resource + private MpMaterialService mpMaterialService; + + @GetMapping("/page") + @Operation(summary = "获得已发布的图文分页") + @PreAuthorize("@ss.hasPermission('mp:free-publish:query')") + public CommonResult> getFreePublishPage(MpFreePublishPageReqVO reqVO) { + // 从公众号查询已发布的图文列表 + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); + WxMpFreePublishList publicationRecords; + try { + publicationRecords = mpService.getFreePublishService().getPublicationRecords( + PageUtils.getStart(reqVO), reqVO.getPageSize()); + } catch (WxErrorException e) { + throw exception(FREE_PUBLISH_LIST_FAIL, e.getError().getErrorMsg()); + } + // 查询对应的图片地址。目的:解决公众号的图片链接无法在我们后台展示 + setFreePublishThumbUrl(publicationRecords.getItems()); + + // 返回分页 + return success(new PageResult<>(publicationRecords.getItems(), publicationRecords.getTotalCount().longValue())); + } + + private void setFreePublishThumbUrl(List items) { + // 1.1 获得 mediaId 数组 + Set mediaIds = new HashSet<>(); + items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> mediaIds.add(newsItem.getThumbMediaId()))); + if (CollUtil.isEmpty(mediaIds)) { + return; + } + // 1.2 批量查询对应的 Media 素材 + Map materials = CollectionUtils.convertMap(mpMaterialService.getMaterialListByMediaId(mediaIds), + MpMaterialDO::getMediaId); + + // 2. 设置回 WxMpFreePublishItem 记录 + items.forEach(item -> item.getContent().getNewsItem().forEach(newsItem -> + findAndThen(materials, newsItem.getThumbMediaId(), material -> newsItem.setThumbUrl(material.getUrl())))); + } + + @PostMapping("/submit") + @Operation(summary = "发布草稿") + @Parameters({ + @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"), + @Parameter(name = "mediaId", description = "要发布的草稿的 media_id", required = true, example = "2048") + }) + @PreAuthorize("@ss.hasPermission('mp:free-publish:submit')") + public CommonResult submitFreePublish(@RequestParam("accountId") Long accountId, + @RequestParam("mediaId") String mediaId) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + String publishId = mpService.getFreePublishService().submit(mediaId); + return success(publishId); + } catch (WxErrorException e) { + throw exception(FREE_PUBLISH_SUBMIT_FAIL, e.getError().getErrorMsg()); + } + } + + @DeleteMapping("/delete") + @Operation(summary = "删除草稿") + @Parameters({ + @Parameter(name = "accountId", description = "公众号账号的编号", required = true, example = "1024"), + @Parameter(name = "articleId", description = "发布记录的编号", required = true, example = "2048") + }) + @PreAuthorize("@ss.hasPermission('mp:free-publish:delete')") + public CommonResult deleteFreePublish(@RequestParam("accountId") Long accountId, + @RequestParam("articleId") String articleId) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + mpService.getFreePublishService().deletePushAllArticle(articleId); + return success(true); + } catch (WxErrorException e) { + throw exception(FREE_PUBLISH_DELETE_FAIL, e.getError().getErrorMsg()); + } + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java new file mode 100644 index 0000000..e47cac5 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/vo/MpDraftPageReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.mp.controller.admin.news.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号草稿的分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpDraftPageReqVO extends PageParam { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java new file mode 100644 index 0000000..eb175ca --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/news/vo/MpFreePublishPageReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.mp.controller.admin.news.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号已发布列表的分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpFreePublishPageReqVO extends PageParam { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/MpOpenController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/MpOpenController.java new file mode 100644 index 0000000..5c80c3c --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/MpOpenController.java @@ -0,0 +1,116 @@ +package com.yunxi.scm.module.mp.controller.admin.open; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.framework.tenant.core.util.TenantUtils; +import com.yunxi.scm.module.mp.controller.admin.open.vo.MpOpenCheckSignatureReqVO; +import com.yunxi.scm.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.framework.mp.core.context.MpContextHolder; +import com.yunxi.scm.module.mp.service.account.MpAccountService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.mp.api.WxMpMessageRouter; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.util.Objects; + +@Tag(name = "管理后台 - 公众号回调") +@RestController +@RequestMapping("/mp/open") +@Validated +@Slf4j +public class MpOpenController { + + @Resource + private MpServiceFactory mpServiceFactory; + + @Resource + private MpAccountService mpAccountService; + + /** + * 接收微信公众号的校验签名 + * + * 对应 文档 + */ + @Operation(summary = "校验签名") // 参见 + @GetMapping(value = "/{appId}", produces = "text/plain;charset=utf-8") + public String checkSignature(@PathVariable("appId") String appId, + MpOpenCheckSignatureReqVO reqVO) { + log.info("[checkSignature][appId({}) 接收到来自微信服务器的认证消息({})]", appId, reqVO); + // 校验请求签名 + WxMpService wxMpService = mpServiceFactory.getRequiredMpService(appId); + // 校验通过 + if (wxMpService.checkSignature(reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getSignature())) { + return reqVO.getEchostr(); + } + // 校验不通过 + return "非法请求"; + } + + /** + * 接收微信公众号的消息推送 + * + * 文档 + */ + @Operation(summary = "处理消息") + @PostMapping(value = "/{appId}", produces = "application/xml; charset=UTF-8") + @OperateLog(enable = false) // 回调地址,无需记录操作日志 + public String handleMessage(@PathVariable("appId") String appId, + @RequestBody String content, + MpOpenHandleMessageReqVO reqVO) { + log.info("[handleMessage][appId({}) 推送消息,参数({}) 内容({})]", appId, reqVO, content); + + // 处理 appId + 多租户的上下文 + MpAccountDO account = mpAccountService.getAccountFromCache(appId); + Assert.notNull(account, "公众号 appId({}) 不存在", appId); + try { + MpContextHolder.setAppId(appId); + return TenantUtils.execute(account.getTenantId(), + () -> handleMessage0(appId, content, reqVO)); + } finally { + MpContextHolder.clear(); + } + } + + private String handleMessage0(String appId, String content, MpOpenHandleMessageReqVO reqVO) { + // 校验请求签名 + WxMpService mppService = mpServiceFactory.getRequiredMpService(appId); + Assert.isTrue(mppService.checkSignature(reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getSignature()), + "非法请求"); + + // 第一步,解析消息 + WxMpXmlMessage inMessage = null; + if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式 + inMessage = WxMpXmlMessage.fromXml(content); + } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式 + inMessage = WxMpXmlMessage.fromEncryptedXml(content, mppService.getWxMpConfigStorage(), + reqVO.getTimestamp(), reqVO.getNonce(), reqVO.getMsg_signature()); + } + Assert.notNull(inMessage, "消息解析失败,原因:消息为空"); + + // 第二步,处理消息 + WxMpMessageRouter mpMessageRouter = mpServiceFactory.getRequiredMpMessageRouter(appId); + WxMpXmlOutMessage outMessage = mpMessageRouter.route(inMessage); + if (outMessage == null) { + return ""; + } + + // 第三步,返回消息 + if (StrUtil.isBlank(reqVO.getEncrypt_type())) { // 明文模式 + return outMessage.toXml(); + } else if (Objects.equals(reqVO.getEncrypt_type(), MpOpenHandleMessageReqVO.ENCRYPT_TYPE_AES)) { // AES 加密模式 + return outMessage.toEncryptedXml(mppService.getWxMpConfigStorage()); + } + return ""; + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java new file mode 100644 index 0000000..1823c0b --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/vo/MpOpenCheckSignatureReqVO.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.mp.controller.admin.open.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 公众号校验签名 Request VO") +@Data +public class MpOpenCheckSignatureReqVO { + + @Schema(description = "微信加密签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "490eb57f448b87bd5f20ccef58aa4de46aa1908e") + @NotEmpty(message = "微信加密签名不能为空") + private String signature; + + @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1672587863") + @NotEmpty(message = "时间戳不能为空") + private String timestamp; + + @Schema(description = "随机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1827365808") + @NotEmpty(message = "随机数不能为空") + private String nonce; + + @Schema(description = "随机字符串", requiredMode = Schema.RequiredMode.REQUIRED, example = "2721154047828672511") + @NotEmpty(message = "随机字符串不能为空") + @SuppressWarnings("SpellCheckingInspection") + private String echostr; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java new file mode 100644 index 0000000..e6e68a6 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/open/vo/MpOpenHandleMessageReqVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.mp.controller.admin.open.vo; + + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 公众号处理消息 Request VO") +@Data +public class MpOpenHandleMessageReqVO { + + public static final String ENCRYPT_TYPE_AES = "aes"; + + @Schema(description = "微信加密签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "490eb57f448b87bd5f20ccef58aa4de46aa1908e") + @NotEmpty(message = "微信加密签名不能为空") + private String signature; + + @Schema(description = "时间戳", requiredMode = Schema.RequiredMode.REQUIRED, example = "1672587863") + @NotEmpty(message = "时间戳不能为空") + private String timestamp; + + @Schema(description = "随机数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1827365808") + @NotEmpty(message = "随机数不能为空") + private String nonce; + + @Schema(description = "粉丝 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "oz-Jdtyn-WGm4C4I5Z-nvBMO_ZfY") + @NotEmpty(message = "粉丝 openid 不能为空") + private String openid; + + @Schema(description = "消息加密类型", example = "aes") + private String encrypt_type; + + @Schema(description = "微信签名", example = "QW5kcm9pZCBUaGUgQmFzZTY0IGlzIGEgZ2VuZXJhdGVkIHN0cmluZw==") + private String msg_signature; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/MpStatisticsController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/MpStatisticsController.java new file mode 100644 index 0000000..3a9e3bd --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/MpStatisticsController.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.module.mp.controller.admin.statistics; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.mp.controller.admin.statistics.vo.*; +import com.yunxi.scm.module.mp.convert.statistics.MpStatisticsConvert; +import com.yunxi.scm.module.mp.service.statistics.MpStatisticsService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 公众号统计") +@RestController +@RequestMapping("/mp/statistics") +@Validated +public class MpStatisticsController { + + @Resource + private MpStatisticsService mpStatisticsService; + + @GetMapping("/user-summary") + @Operation(summary = "获得粉丝增减数据") + @PreAuthorize("@ss.hasPermission('mp:statistics:query')") + public CommonResult> getUserSummary(MpStatisticsGetReqVO getReqVO) { + List list = mpStatisticsService.getUserSummary( + getReqVO.getAccountId(), getReqVO.getDate()); + return success(MpStatisticsConvert.INSTANCE.convertList01(list)); + } + + @GetMapping("/user-cumulate") + @Operation(summary = "获得粉丝累计数据") + @PreAuthorize("@ss.hasPermission('mp:statistics:query')") + public CommonResult> getUserCumulate(MpStatisticsGetReqVO getReqVO) { + List list = mpStatisticsService.getUserCumulate( + getReqVO.getAccountId(), getReqVO.getDate()); + return success(MpStatisticsConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/upstream-message") + @Operation(summary = "获取消息发送概况数据") + @PreAuthorize("@ss.hasPermission('mp:statistics:query')") + public CommonResult> getUpstreamMessage(MpStatisticsGetReqVO getReqVO) { + List list = mpStatisticsService.getUpstreamMessage( + getReqVO.getAccountId(), getReqVO.getDate()); + return success(MpStatisticsConvert.INSTANCE.convertList03(list)); + } + + @GetMapping("/interface-summary") + @Operation(summary = "获取消息发送概况数据") + @PreAuthorize("@ss.hasPermission('mp:statistics:query')") + public CommonResult> getInterfaceSummary(MpStatisticsGetReqVO getReqVO) { + List list = mpStatisticsService.getInterfaceSummary( + getReqVO.getAccountId(), getReqVO.getDate()); + return success(MpStatisticsConvert.INSTANCE.convertList04(list)); + } +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java new file mode 100644 index 0000000..4d9a253 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsGetReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 获得统计数据 Request VO") +@Data +public class MpStatisticsGetReqVO { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "查询时间范围") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @NotNull(message = "查询时间范围不能为空") + private LocalDateTime[] date; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java new file mode 100644 index 0000000..fe61359 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsInterfaceSummaryRespVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Schema(description = "管理后台 - 某一天的接口分析数据 Response VO") +@Data +public class MpStatisticsInterfaceSummaryRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private Date refDate; + + @Schema(description = "通过服务器配置地址获得消息后,被动回复粉丝消息的次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer callbackCount; + + @Schema(description = "上述动作的失败次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer failCount; + + @Schema(description = "总耗时,除以 callback_count 即为平均耗时", requiredMode = Schema.RequiredMode.REQUIRED, example = "30") + private Integer totalTimeCost; + + @Schema(description = "最大耗时", requiredMode = Schema.RequiredMode.REQUIRED, example = "40") + private Integer maxTimeCost; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java new file mode 100644 index 0000000..16f2295 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUpstreamMessageRespVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO") +@Data +public class MpStatisticsUpstreamMessageRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private Date refDate; + + @Schema(description = "上行发送了(向公众号发送了)消息的粉丝数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer messageUser; + + @Schema(description = "上行发送了消息的消息总数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer messageCount; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java new file mode 100644 index 0000000..f0a8129 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUserCumulateRespVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Schema(description = "管理后台 - 某一天的消息发送概况数据 Response VO") +@Data +public class MpStatisticsUserCumulateRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private Date refDate; + + @Schema(description = "累计粉丝量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer cumulateUser; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java new file mode 100644 index 0000000..ae2f171 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/statistics/vo/MpStatisticsUserSummaryRespVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.mp.controller.admin.statistics.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.Date; + +@Schema(description = "管理后台 - 某一天的粉丝增减数据 Response VO") +@Data +public class MpStatisticsUserSummaryRespVO { + + @Schema(description = "日期", requiredMode = Schema.RequiredMode.REQUIRED) + private Date refDate; + + @Schema(description = "粉丝来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer userSource; + + @Schema(description = "新关注的粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer newUser; + + @Schema(description = "取消关注的粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Integer cancelUser; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/MpTagController.http b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/MpTagController.http new file mode 100644 index 0000000..fe79105 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/MpTagController.http @@ -0,0 +1,39 @@ +### 请求 /mp/tag/create 接口 => 成功 +POST {{baseUrl}}/mp/tag/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "accountId": "1", + "name": "测试" +} + +### 请求 /mp/tag/update 接口 => 成功 +PUT {{baseUrl}}/mp/tag/update +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "id": "3", + "name": "测试标签啦" +} + +### 请求 /mp/tag/delete 接口 => 成功 +DELETE {{baseUrl}}/mp/tag/delete?id=3 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /mp/tag/page 接口 => 成功 +GET {{baseUrl}}/mp/tag/page?accountId=1&pageNo=1&pageSize=10 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /mp/tag/sync 接口 => 成功 +POST {{baseUrl}}/mp/tag/sync?accountId=1 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/MpTagController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/MpTagController.java new file mode 100644 index 0000000..78c65cd --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/MpTagController.java @@ -0,0 +1,88 @@ +package com.yunxi.scm.module.mp.controller.admin.tag; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.*; +import com.yunxi.scm.module.mp.convert.tag.MpTagConvert; +import com.yunxi.scm.module.mp.dal.dataobject.tag.MpTagDO; +import com.yunxi.scm.module.mp.service.tag.MpTagService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 公众号标签") +@RestController +@RequestMapping("/mp/tag") +@Validated +public class MpTagController { + + @Resource + private MpTagService mpTagService; + + @PostMapping("/create") + @Operation(summary = "创建公众号标签") + @PreAuthorize("@ss.hasPermission('mp:tag:create')") + public CommonResult createTag(@Valid @RequestBody MpTagCreateReqVO createReqVO) { + return success(mpTagService.createTag(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新公众号标签") + @PreAuthorize("@ss.hasPermission('mp:tag:update')") + public CommonResult updateTag(@Valid @RequestBody MpTagUpdateReqVO updateReqVO) { + mpTagService.updateTag(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除公众号标签") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('mp:tag:delete')") + public CommonResult deleteTag(@RequestParam("id") Long id) { + mpTagService.deleteTag(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获取公众号标签详情") + @PreAuthorize("@ss.hasPermission('mp:tag:query')") + public CommonResult get(@RequestParam("id") Long id) { + MpTagDO mpTagDO = mpTagService.get(id); + return success(MpTagConvert.INSTANCE.convert(mpTagDO)); + } + + @GetMapping("/page") + @Operation(summary = "获取公众号标签分页") + @PreAuthorize("@ss.hasPermission('mp:tag:query')") + public CommonResult> getTagPage(MpTagPageReqVO pageReqVO) { + PageResult pageResult = mpTagService.getTagPage(pageReqVO); + return success(MpTagConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取公众号账号精简信息列表") + @PreAuthorize("@ss.hasPermission('mp:account:query')") + public CommonResult> getSimpleTags() { + List list = mpTagService.getTagList(); + return success(MpTagConvert.INSTANCE.convertList02(list)); + } + + @PostMapping("/sync") + @Operation(summary = "同步公众号标签") + @Parameter(name = "accountId", description = "公众号账号的编号", required = true) + @PreAuthorize("@ss.hasPermission('mp:tag:sync')") + public CommonResult syncTag(@RequestParam("accountId") Long accountId) { + mpTagService.syncTag(accountId); + return success(true); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagBaseVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagBaseVO.java new file mode 100644 index 0000000..279daca --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagBaseVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.mp.controller.admin.tag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +/** + * 公众号标签 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author fengdan + */ +@Data +public class MpTagBaseVO { + + @Schema(description = "标签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + @NotEmpty(message = "标签名不能为空") + private String name; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java new file mode 100644 index 0000000..36b137f --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.mp.controller.admin.tag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号标签创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpTagCreateReqVO extends MpTagBaseVO { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java new file mode 100644 index 0000000..bd2a581 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagPageReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.mp.controller.admin.tag.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 公众号标签分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpTagPageReqVO extends PageParam { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotEmpty(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "标签名,模糊匹配", example = "哈哈") + private String name; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagRespVO.java new file mode 100644 index 0000000..3f2e749 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.mp.controller.admin.tag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Date; + +@Schema(description = "管理后台 - 公众号标签 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpTagRespVO extends MpTagBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "此标签下粉丝数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer count; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java new file mode 100644 index 0000000..090d5a1 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagSimpleRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.mp.controller.admin.tag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 公众号标签精简信息 Response VO") +@Data +public class MpTagSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号的标签编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long tagId; + + @Schema(description = "标签名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "快乐") + private String name; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java new file mode 100644 index 0000000..c09c4c3 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/tag/vo/MpTagUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.mp.controller.admin.tag.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号标签更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpTagUpdateReqVO extends MpTagBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/MpUserController.http b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/MpUserController.http new file mode 100644 index 0000000..7c61581 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/MpUserController.http @@ -0,0 +1,18 @@ +### 请求 /mp/user/sync 接口 => 成功 +POST {{baseUrl}}/mp/user/sync?accountId=1 +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /mp/user/update 接口 => 成功 +PUT {{baseUrl}}/mp/user/update +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "id": "3", + "nickname": "test", + "remark": "测试备注", + "tagIds": [103, 104] +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/MpUserController.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/MpUserController.java new file mode 100644 index 0000000..fb467d3 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/MpUserController.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.mp.controller.admin.user; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserRespVO; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; +import com.yunxi.scm.module.mp.convert.user.MpUserConvert; +import com.yunxi.scm.module.mp.dal.dataobject.user.MpUserDO; +import com.yunxi.scm.module.mp.service.user.MpUserService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 公众号粉丝") +@RestController +@RequestMapping("/mp/user") +@Validated +public class MpUserController { + + @Resource + private MpUserService mpUserService; + + @GetMapping("/page") + @Operation(summary = "获得公众号粉丝分页") + @PreAuthorize("@ss.hasPermission('mp:user:query')") + public CommonResult> getUserPage(@Valid MpUserPageReqVO pageVO) { + PageResult pageResult = mpUserService.getUserPage(pageVO); + return success(MpUserConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/get") + @Operation(summary = "获得公众号粉丝") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('mp:user:query')") + public CommonResult getUser(@RequestParam("id") Long id) { + return success(MpUserConvert.INSTANCE.convert(mpUserService.getUser(id))); + } + + @PutMapping("/update") + @Operation(summary = "更新公众号粉丝") + @PreAuthorize("@ss.hasPermission('mp:user:update')") + public CommonResult updateUser(@Valid @RequestBody MpUserUpdateReqVO updateReqVO) { + mpUserService.updateUser(updateReqVO); + return success(true); + } + + @PostMapping("/sync") + @Operation(summary = "同步公众号粉丝") + @Parameter(name = "accountId", description = "公众号账号的编号", required = true) + @PreAuthorize("@ss.hasPermission('mp:user:sync')") + public CommonResult syncUser(@RequestParam("accountId") Long accountId) { + mpUserService.syncUser(accountId); + return success(true); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserPageReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserPageReqVO.java new file mode 100644 index 0000000..7a721c1 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserPageReqVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.mp.controller.admin.user.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 公众号粉丝分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpUserPageReqVO extends PageParam { + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + @NotNull(message = "公众号账号的编号不能为空") + private Long accountId; + + @Schema(description = "公众号粉丝标识,模糊匹配", example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String openid; + + @Schema(description = "公众号粉丝昵称,模糊匹配", example = "芋艿") + private String nickname; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserRespVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserRespVO.java new file mode 100644 index 0000000..13bb7a3 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserRespVO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.mp.controller.admin.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +@Schema(description = "管理后台 - 公众号粉丝 Response VO") +@Data +public class MpUserRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "公众号粉丝标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "o6_bmjrPTlm6_2sgVt7hMZOPfL2M") + private String openid; + + @Schema(description = "关注状态 参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer subscribeStatus; + @Schema(description = "关注时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime subscribeTime; + @Schema(description = "取消关注时间") + private LocalDateTime unsubscribeTime; + + @Schema(description = "昵称", example = "芋道") + private String nickname; + @Schema(description = "头像地址", example = "https://www.iocoder.cn/1.png") + private String headImageUrl; + @Schema(description = "语言", example = "zh_CN") + private String language; + @Schema(description = "国家", example = "中国") + private String country; + @Schema(description = "省份", example = "广东省") + private String province; + @Schema(description = "城市", example = "广州市") + private String city; + @Schema(description = "备注", example = "你是一个芋头嘛") + private String remark; + + @Schema(description = "标签编号数组", example = "1,2,3") + private List tagIds; + + @Schema(description = "公众号账号的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long accountId; + @Schema(description = "公众号账号的 appId", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx1234567890") + private String appId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java new file mode 100644 index 0000000..bb9087a --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/admin/user/vo/MpUserUpdateReqVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.mp.controller.admin.user.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +@Schema(description = "管理后台 - 公众号粉丝更新 Request VO") +@Data +public class MpUserUpdateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "昵称", example = "芋道") + private String nickname; + + @Schema(description = "备注", example = "你是一个芋头嘛") + private String remark; + + @Schema(description = "标签编号数组", example = "1,2,3") + private List tagIds; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/package-info.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/package-info.java new file mode 100644 index 0000000..0126378 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yunxi-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yunxi-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.yunxi.scm.module.mp.controller; diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/account/MpAccountConvert.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/account/MpAccountConvert.java new file mode 100644 index 0000000..cd83b9f --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/account/MpAccountConvert.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.mp.convert.account; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountRespVO; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountSimpleRespVO; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MpAccountConvert { + + MpAccountConvert INSTANCE = Mappers.getMapper(MpAccountConvert.class); + + MpAccountDO convert(MpAccountCreateReqVO bean); + + MpAccountDO convert(MpAccountUpdateReqVO bean); + + MpAccountRespVO convert(MpAccountDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/material/MpMaterialConvert.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/material/MpMaterialConvert.java new file mode 100644 index 0000000..55ad414 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/material/MpMaterialConvert.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.mp.convert.material; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialRespVO; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialUploadRespVO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.material.MpMaterialDO; +import me.chanjar.weixin.mp.bean.material.WxMpMaterial; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.io.File; + +@Mapper +public interface MpMaterialConvert { + + MpMaterialConvert INSTANCE = Mappers.getMapper(MpMaterialConvert.class); + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(source = "account.id", target = "accountId"), + @Mapping(source = "account.appId", target = "appId"), + @Mapping(source = "name", target = "name") + }) + MpMaterialDO convert(String mediaId, String type, String url, MpAccountDO account, + String name); + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(source = "account.id", target = "accountId"), + @Mapping(source = "account.appId", target = "appId"), + @Mapping(source = "name", target = "name") + }) + MpMaterialDO convert(String mediaId, String type, String url, MpAccountDO account, + String name, String title, String introduction, String mpUrl); + + MpMaterialUploadRespVO convert(MpMaterialDO bean); + + default WxMpMaterial convert(String name, File file, String title, String introduction) { + return new WxMpMaterial(name, file, title, introduction); + } + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/menu/MpMenuConvert.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/menu/MpMenuConvert.java new file mode 100644 index 0000000..3b6fa53 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/menu/MpMenuConvert.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.mp.convert.menu; + +import com.yunxi.scm.module.mp.controller.admin.menu.vo.MpMenuRespVO; +import com.yunxi.scm.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.menu.MpMenuDO; +import com.yunxi.scm.module.mp.service.message.bo.MpMessageSendOutReqBO; +import me.chanjar.weixin.common.bean.menu.WxMenuButton; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MpMenuConvert { + + MpMenuConvert INSTANCE = Mappers.getMapper(MpMenuConvert.class); + + MpMenuRespVO convert(MpMenuDO bean); + + List convertList(List list); + + @Mappings({ + @Mapping(source = "menu.appId", target = "appId"), + @Mapping(source = "menu.replyMessageType", target = "type"), + @Mapping(source = "menu.replyContent", target = "content"), + @Mapping(source = "menu.replyMediaId", target = "mediaId"), + @Mapping(source = "menu.replyThumbMediaId", target = "thumbMediaId"), + @Mapping(source = "menu.replyTitle", target = "title"), + @Mapping(source = "menu.replyDescription", target = "description"), + @Mapping(source = "menu.replyArticles", target = "articles"), + @Mapping(source = "menu.replyMusicUrl", target = "musicUrl"), + @Mapping(source = "menu.replyHqMusicUrl", target = "hqMusicUrl"), + }) + MpMessageSendOutReqBO convert(String openid, MpMenuDO menu); + + List convert(List list); + + @Mappings({ + @Mapping(source = "menuKey", target = "key"), + @Mapping(source = "children", target = "subButtons"), + }) + WxMenuButton convert(MpMenuSaveReqVO.Menu bean); + + MpMenuDO convert02(MpMenuSaveReqVO.Menu menu); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/message/MpAutoReplyConvert.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/message/MpAutoReplyConvert.java new file mode 100644 index 0000000..49bbadd --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/message/MpAutoReplyConvert.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.mp.convert.message; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyRespVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpAutoReplyDO; +import com.yunxi.scm.module.mp.service.message.bo.MpMessageSendOutReqBO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MpAutoReplyConvert { + + MpAutoReplyConvert INSTANCE = Mappers.getMapper(MpAutoReplyConvert.class); + + @Mappings({ + @Mapping(source = "reply.appId", target = "appId"), + @Mapping(source = "reply.responseMessageType", target = "type"), + @Mapping(source = "reply.responseContent", target = "content"), + @Mapping(source = "reply.responseMediaId", target = "mediaId"), + @Mapping(source = "reply.responseTitle", target = "title"), + @Mapping(source = "reply.responseDescription", target = "description"), + @Mapping(source = "reply.responseArticles", target = "articles"), + }) + MpMessageSendOutReqBO convert(String openid, MpAutoReplyDO reply); + + PageResult convertPage(PageResult page); + + MpAutoReplyRespVO convert(MpAutoReplyDO bean); + + MpAutoReplyDO convert(MpAutoReplyCreateReqVO bean); + + MpAutoReplyDO convert(MpAutoReplyUpdateReqVO bean); +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/message/MpMessageConvert.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/message/MpMessageConvert.java new file mode 100644 index 0000000..7b00108 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/message/MpMessageConvert.java @@ -0,0 +1,172 @@ +package com.yunxi.scm.module.mp.convert.message; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessageRespVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.yunxi.scm.module.mp.dal.dataobject.user.MpUserDO; +import com.yunxi.scm.module.mp.service.message.bo.MpMessageSendOutReqBO; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutNewsMessage; +import me.chanjar.weixin.mp.builder.outxml.BaseBuilder; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MpMessageConvert { + + MpMessageConvert INSTANCE = Mappers.getMapper(MpMessageConvert.class); + + MpMessageRespVO convert(MpMessageDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default MpMessageDO convert(WxMpXmlMessage wxMessage, MpAccountDO account, MpUserDO user) { + MpMessageDO message = convert(wxMessage); + if (account != null) { + message.setAccountId(account.getId()).setAppId(account.getAppId()); + } + if (user != null) { + message.setUserId(user.getId()).setOpenid(user.getOpenid()); + } + return message; + } + @Mappings(value = { + @Mapping(source = "msgType", target = "type"), + @Mapping(target = "createTime", ignore = true), + }) + MpMessageDO convert(WxMpXmlMessage bean); + + default MpMessageDO convert(MpMessageSendOutReqBO sendReqBO, MpAccountDO account, MpUserDO user) { + // 构建消息 + MpMessageDO message = new MpMessageDO(); + message.setType(sendReqBO.getType()); + switch (sendReqBO.getType()) { + case WxConsts.XmlMsgType.TEXT: // 1. 文本 + message.setContent(sendReqBO.getContent()); + break; + case WxConsts.XmlMsgType.IMAGE: // 2. 图片 + case WxConsts.XmlMsgType.VOICE: // 3. 语音 + message.setMediaId(sendReqBO.getMediaId()); + break; + case WxConsts.XmlMsgType.VIDEO: // 4. 视频 + message.setMediaId(sendReqBO.getMediaId()) + .setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()); + break; + case WxConsts.XmlMsgType.NEWS: // 5. 图文 + message.setArticles(sendReqBO.getArticles()); + case WxConsts.XmlMsgType.MUSIC: // 6. 音乐 + message.setTitle(sendReqBO.getTitle()).setDescription(sendReqBO.getDescription()) + .setMusicUrl(sendReqBO.getMusicUrl()).setHqMusicUrl(sendReqBO.getHqMusicUrl()) + .setThumbMediaId(sendReqBO.getThumbMediaId()); + break; + default: + throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); + } + + // 其它字段 + if (account != null) { + message.setAccountId(account.getId()).setAppId(account.getAppId()); + } + if (user != null) { + message.setUserId(user.getId()).setOpenid(user.getOpenid()); + } + return message; + } + + default WxMpXmlOutMessage convert02(MpMessageDO message, MpAccountDO account) { + BaseBuilder builder; + // 个性化字段 + switch (message.getType()) { + case WxConsts.XmlMsgType.TEXT: + builder = WxMpXmlOutMessage.TEXT().content(message.getContent()); + break; + case WxConsts.XmlMsgType.IMAGE: + builder = WxMpXmlOutMessage.IMAGE().mediaId(message.getMediaId()); + break; + case WxConsts.XmlMsgType.VOICE: + builder = WxMpXmlOutMessage.VOICE().mediaId(message.getMediaId()); + break; + case WxConsts.XmlMsgType.VIDEO: + builder = WxMpXmlOutMessage.VIDEO().mediaId(message.getMediaId()) + .title(message.getTitle()).description(message.getDescription()); + break; + case WxConsts.XmlMsgType.NEWS: + builder = WxMpXmlOutMessage.NEWS().articles(convertList02(message.getArticles())); + break; + case WxConsts.XmlMsgType.MUSIC: + builder = WxMpXmlOutMessage.MUSIC().title(message.getTitle()).description(message.getDescription()) + .musicUrl(message.getMusicUrl()).hqMusicUrl(message.getHqMusicUrl()) + .thumbMediaId(message.getThumbMediaId()); + break; + default: + throw new IllegalArgumentException("不支持的消息类型:" + message.getType()); + } + // 通用字段 + builder.fromUser(account.getAccount()); + builder.toUser(message.getOpenid()); + return builder.build(); + } + List convertList02(List list); + + default WxMpKefuMessage convert(MpMessageSendReqVO sendReqVO, MpUserDO user) { + me.chanjar.weixin.mp.builder.kefu.BaseBuilder builder; + // 个性化字段 + switch (sendReqVO.getType()) { + case WxConsts.KefuMsgType.TEXT: + builder = WxMpKefuMessage.TEXT().content(sendReqVO.getContent()); + break; + case WxConsts.KefuMsgType.IMAGE: + builder = WxMpKefuMessage.IMAGE().mediaId(sendReqVO.getMediaId()); + break; + case WxConsts.KefuMsgType.VOICE: + builder = WxMpKefuMessage.VOICE().mediaId(sendReqVO.getMediaId()); + break; + case WxConsts.KefuMsgType.VIDEO: + builder = WxMpKefuMessage.VIDEO().mediaId(sendReqVO.getMediaId()) + .title(sendReqVO.getTitle()).description(sendReqVO.getDescription()); + break; + case WxConsts.KefuMsgType.NEWS: + builder = WxMpKefuMessage.NEWS().articles(convertList03(sendReqVO.getArticles())); + break; + case WxConsts.KefuMsgType.MUSIC: + builder = WxMpKefuMessage.MUSIC().title(sendReqVO.getTitle()).description(sendReqVO.getDescription()) + .thumbMediaId(sendReqVO.getThumbMediaId()) + .musicUrl(sendReqVO.getMusicUrl()).hqMusicUrl(sendReqVO.getHqMusicUrl()); + break; + default: + throw new IllegalArgumentException("不支持的消息类型:" + sendReqVO.getType()); + } + // 通用字段 + builder.toUser(user.getOpenid()); + return builder.build(); + } + List convertList03(List list); + + default MpMessageDO convert(WxMpKefuMessage wxMessage, MpAccountDO account, MpUserDO user) { + MpMessageDO message = convert(wxMessage); + if (account != null) { + message.setAccountId(account.getId()).setAppId(account.getAppId()); + } + if (user != null) { + message.setUserId(user.getId()).setOpenid(user.getOpenid()); + } + return message; + } + @Mappings(value = { + @Mapping(source = "msgType", target = "type"), + @Mapping(target = "createTime", ignore = true), + }) + MpMessageDO convert(WxMpKefuMessage bean); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/statistics/MpStatisticsConvert.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/statistics/MpStatisticsConvert.java new file mode 100644 index 0000000..74971c3 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/statistics/MpStatisticsConvert.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.mp.convert.statistics; + +import com.yunxi.scm.module.mp.controller.admin.statistics.vo.MpStatisticsInterfaceSummaryRespVO; +import com.yunxi.scm.module.mp.controller.admin.statistics.vo.MpStatisticsUpstreamMessageRespVO; +import com.yunxi.scm.module.mp.controller.admin.statistics.vo.MpStatisticsUserCumulateRespVO; +import com.yunxi.scm.module.mp.controller.admin.statistics.vo.MpStatisticsUserSummaryRespVO; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MpStatisticsConvert { + + MpStatisticsConvert INSTANCE = Mappers.getMapper(MpStatisticsConvert.class); + + List convertList01(List list); + + List convertList02(List list); + + List convertList03(List list); + + @Mappings({ + @Mapping(source = "refDate", target = "refDate", dateFormat = "yyyy-MM-dd"), + @Mapping(source = "msgUser", target = "messageUser"), + @Mapping(source = "msgCount", target = "messageCount"), + }) + MpStatisticsUpstreamMessageRespVO convert(WxDataCubeMsgResult bean); + + List convertList04(List list); + + @Mapping(source = "refDate", target = "refDate", dateFormat = "yyyy-MM-dd") + MpStatisticsInterfaceSummaryRespVO convert(WxDataCubeInterfaceResult bean); +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/tag/MpTagConvert.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/tag/MpTagConvert.java new file mode 100644 index 0000000..16e3d85 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/tag/MpTagConvert.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.mp.convert.tag; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagRespVO; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagSimpleRespVO; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.tag.MpTagDO; +import me.chanjar.weixin.mp.bean.tag.WxUserTag; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MpTagConvert { + + MpTagConvert INSTANCE = Mappers.getMapper(MpTagConvert.class); + + WxUserTag convert(MpTagUpdateReqVO bean); + + MpTagRespVO convert(WxUserTag bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + @Mappings({ + @Mapping(target = "id", ignore = true), + @Mapping(source = "tag.id", target = "tagId"), + @Mapping(source = "tag.name", target = "name"), + @Mapping(source = "tag.count", target = "count"), + @Mapping(source = "account.id", target = "accountId"), + @Mapping(source = "account.appId", target = "appId"), + }) + MpTagDO convert(WxUserTag tag, MpAccountDO account); + + MpTagRespVO convert(MpTagDO mpTagDO); + + List convertList02(List list); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/user/MpUserConvert.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/user/MpUserConvert.java new file mode 100644 index 0000000..47fb7a7 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/convert/user/MpUserConvert.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.mp.convert.user; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserRespVO; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.user.MpUserDO; +import me.chanjar.weixin.mp.bean.result.WxMpUser; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MpUserConvert { + + MpUserConvert INSTANCE = Mappers.getMapper(MpUserConvert.class); + + MpUserRespVO convert(MpUserDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + @Mappings(value = { + @Mapping(source = "openId", target = "openid"), + @Mapping(source = "headImgUrl", target = "headImageUrl"), + @Mapping(target = "subscribeTime", ignore = true), // 单独转换 + }) + MpUserDO convert(WxMpUser wxMpUser); + + default MpUserDO convert(MpAccountDO account, WxMpUser wxMpUser) { + MpUserDO user = convert(wxMpUser); + user.setSubscribeStatus(wxMpUser.getSubscribe() ? CommonStatusEnum.ENABLE.getStatus() + : CommonStatusEnum.DISABLE.getStatus()); + user.setSubscribeTime(LocalDateTimeUtil.of(wxMpUser.getSubscribeTime() * 1000L)); + if (account != null) { + user.setAccountId(account.getId()); + user.setAppId(account.getAppId()); + } + return user; + } + + default List convertList(MpAccountDO account, List wxUsers) { + return CollectionUtils.convertList(wxUsers, wxUser -> convert(account, wxUser)); + } + + MpUserDO convert(MpUserUpdateReqVO bean); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/account/MpAccountDO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/account/MpAccountDO.java new file mode 100644 index 0000000..020fe03 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/account/MpAccountDO.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.module.mp.dal.dataobject.account; + +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 公众号账号 DO + * + * @author 芋道源码 + */ +@TableName("mp_account") +@KeySequence("mp_account_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MpAccountDO extends TenantBaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 公众号名称 + */ + private String name; + /** + * 公众号账号 + */ + private String account; + /** + * 公众号 appid + */ + private String appId; + /** + * 公众号密钥 + */ + private String appSecret; + /** + * 公众号token + */ + private String token; + /** + * 消息加解密密钥 + */ + private String aesKey; + /** + * 二维码图片 URL + */ + private String qrCodeUrl; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/material/MpMaterialDO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/material/MpMaterialDO.java new file mode 100644 index 0000000..40eabe1 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/material/MpMaterialDO.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.module.mp.dal.dataobject.material; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; +import me.chanjar.weixin.common.api.WxConsts; + +/** + * 公众号素材 DO + * + * 1. 临时素材 + * 2. 永久素材 + * + * @author 芋道源码 + */ +@TableName("mp_material") +@KeySequence("mp_material_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MpMaterialDO extends BaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + + /** + * 公众号素材 id + */ + private String mediaId; + /** + * 文件类型 + * + * 枚举 {@link WxConsts.MediaFileType} + */ + private String type; + /** + * 是否永久 + * + * true - 永久素材 + * false - 临时素材 + */ + private Boolean permanent; + /** + * 文件服务器的 URL + */ + private String url; + + /** + * 名字 + * + * 永久素材:非空 + * 临时素材:可能为空。 + * 1. 为空的情况:粉丝主动发送的图片、语音等 + * 2. 非空的情况:主动发送给粉丝的图片、语音等 + */ + private String name; + + /** + * 公众号文件 URL + * + * 只有【永久素材】使用 + */ + private String mpUrl; + + /** + * 视频素材的标题 + * + * 只有【永久素材】使用 + */ + private String title; + /** + * 视频素材的描述 + * + * 只有【永久素材】使用 + */ + private String introduction; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/menu/MpMenuDO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/menu/MpMenuDO.java new file mode 100644 index 0000000..860b186 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/menu/MpMenuDO.java @@ -0,0 +1,184 @@ +package com.yunxi.scm.module.mp.dal.dataobject.menu; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.api.WxConsts.MenuButtonType; + +import java.util.List; + +/** + * 公众号菜单 DO + * + * @author 芋道源码 + */ +@TableName(value = "mp_menu", autoResultMap = true) +@KeySequence("mp_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpMenuDO extends BaseDO { + + /** + * 编号 - 顶级菜单 + */ + public static final Long ID_ROOT = 0L; + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + + /** + * 菜单名称 + */ + private String name; + /** + * 菜单标识 + * + * 支持多 DB 类型时,无法直接使用 key + @TableField("menuKey") 来实现转换,原因是 "menuKey" AS key 而存在报错 + */ + private String menuKey; + /** + * 父菜单编号 + */ + private Long parentId; + + // ========== 按钮操作 ========== + + /** + * 按钮类型 + * + * 枚举 {@link MenuButtonType} + */ + private String type; + + /** + * 网页链接 + * + * 粉丝点击菜单可打开链接,不超过 1024 字节 + * + * 类型为 {@link WxConsts.XmlMsgType} 的 VIEW、MINIPROGRAM + */ + private String url; + + /** + * 小程序的 appId + * + * 类型为 {@link MenuButtonType} 的 MINIPROGRAM + */ + private String miniProgramAppId; + /** + * 小程序的页面路径 + * + * 类型为 {@link MenuButtonType} 的 MINIPROGRAM + */ + private String miniProgramPagePath; + + /** + * 跳转图文的媒体编号 + */ + private String articleId; + + // ========== 消息内容 ========== + + /** + * 消息类型 + * + * 当 {@link #type} 为 CLICK、SCANCODE_WAITMSG + * + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC + */ + private String replyMessageType; + + /** + * 回复的消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + private String replyContent; + + /** + * 回复的媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String replyMediaId; + /** + * 回复的媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String replyMediaUrl; + + /** + * 回复的标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String replyTitle; + /** + * 回复的描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String replyDescription; + + /** + * 回复的缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String replyThumbMediaId; + /** + * 回复的缩略图的媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String replyThumbMediaUrl; + + /** + * 回复的图文消息数组 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) + private List replyArticles; + + /** + * 回复的音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String replyMusicUrl; + /** + * 回复的高质量音乐链接 + * + * WIFI 环境优先使用该链接播放音乐 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String replyHqMusicUrl; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/message/MpAutoReplyDO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/message/MpAutoReplyDO.java new file mode 100644 index 0000000..d30c9e9 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/message/MpAutoReplyDO.java @@ -0,0 +1,164 @@ +package com.yunxi.scm.module.mp.dal.dataobject.message; + +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.enums.message.MpAutoReplyMatchEnum; +import com.yunxi.scm.module.mp.enums.message.MpAutoReplyTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.api.WxConsts.XmlMsgType; + +import java.util.List; +import java.util.Set; + +/** + * 公众号消息自动回复 DO + * + * @author 芋道源码 + */ +@TableName(value = "mp_auto_reply", autoResultMap = true) +@KeySequence("mp_auto_reply_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpAutoReplyDO extends BaseDO { + + public static Set REQUEST_MESSAGE_TYPE = SetUtils.asSet(WxConsts.XmlMsgType.TEXT, WxConsts.XmlMsgType.IMAGE, + WxConsts.XmlMsgType.VOICE, WxConsts.XmlMsgType.VIDEO, WxConsts.XmlMsgType.SHORTVIDEO, + WxConsts.XmlMsgType.LOCATION, WxConsts.XmlMsgType.LINK); + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + + /** + * 回复类型 + * + * 枚举 {@link MpAutoReplyTypeEnum} + */ + private Integer type; + + // ==================== 请求消息 ==================== + + /** + * 请求的关键字 + * + * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#KEYWORD} + */ + private String requestKeyword; + /** + * 请求的关键字的匹配 + * + * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#KEYWORD} + * + * 枚举 {@link MpAutoReplyMatchEnum} + */ + private Integer requestMatch; + + /** + * 请求的消息类型 + * + * 当 {@link #type} 为 {@link MpAutoReplyTypeEnum#MESSAGE} + * + * 枚举 {@link XmlMsgType} 中的 {@link #REQUEST_MESSAGE_TYPE} + */ + private String requestMessageType; + + // ==================== 响应消息 ==================== + + /** + * 回复的消息类型 + * + * 枚举 {@link XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS + */ + private String responseMessageType; + + /** + * 回复的消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + private String responseContent; + + /** + * 回复的媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String responseMediaId; + /** + * 回复的媒体 URL + */ + private String responseMediaUrl; + + /** + * 回复的标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String responseTitle; + /** + * 回复的描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + private String responseDescription; + + /** + * 回复的缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String responseThumbMediaId; + /** + * 回复的缩略图的媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String responseThumbMediaUrl; + + /** + * 回复的图文消息 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @TableField(typeHandler = MpMessageDO.ArticleTypeHandler.class) + private List responseArticles; + + /** + * 回复的音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String responseMusicUrl; + /** + * 回复的高质量音乐链接 + * + * WIFI 环境优先使用该链接播放音乐 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String responseHqMusicUrl; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/message/MpMessageDO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/message/MpMessageDO.java new file mode 100644 index 0000000..c422a8b --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/message/MpMessageDO.java @@ -0,0 +1,255 @@ +package com.yunxi.scm.module.mp.dal.dataobject.message; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.user.MpUserDO; +import com.yunxi.scm.module.mp.enums.message.MpMessageSendFromEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.AbstractJsonTypeHandler; +import lombok.*; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.mp.builder.kefu.NewsBuilder; + +import javax.validation.constraints.NotEmpty; +import java.io.Serializable; +import java.util.List; + +/** + * 公众号消息 DO + * + * @author 芋道源码 + */ +@TableName(value = "mp_message", autoResultMap = true) +@KeySequence("mp_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MpMessageDO extends BaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 微信公众号消息 id + */ + private Long msgId; + /** + * 公众号账号的 ID + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appid + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + /** + * 公众号粉丝的编号 + * + * 关联 {@link MpUserDO#getId()} + */ + private Long userId; + /** + * 公众号粉丝标志 + * + * 冗余 {@link MpUserDO#getOpenid()} + */ + private String openid; + + /** + * 消息类型 + * + * 枚举 {@link WxConsts.XmlMsgType} + */ + private String type; + /** + * 消息来源 + * + * 枚举 {@link MpMessageSendFromEnum} + */ + private Integer sendFrom; + + // ========= 普通消息内容 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html + + /** + * 消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + private String content; + + /** + * 媒体文件的编号 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + private String mediaId; + /** + * 媒体文件的 URL + */ + private String mediaUrl; + /** + * 语音识别后文本 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE + */ + private String recognition; + /** + * 语音格式,如 amr,speex 等 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VOICE + */ + private String format; + /** + * 标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC、LINK + */ + private String title; + /** + * 描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC + */ + private String description; + + /** + * 缩略图的媒体 id,通过素材管理中的接口上传多媒体文件,得到的 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String thumbMediaId; + /** + * 缩略图的媒体 URL + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC、VIDEO + */ + private String thumbMediaUrl; + + /** + * 点击图文消息跳转链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LINK + */ + private String url; + + /** + * 地理位置维度 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION + */ + private Double locationX; + /** + * 地理位置经度 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION + */ + private Double locationY; + /** + * 地图缩放大小 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION + */ + private Double scale; + /** + * 详细地址 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 LOCATION + * + * 例如说杨浦区黄兴路 221-4 号临 + */ + private String label; + + /** + * 图文消息数组 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @TableField(typeHandler = ArticleTypeHandler.class) + private List
articles; + + /** + * 音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String musicUrl; + /** + * 高质量音乐链接 + * + * WIFI 环境优先使用该链接播放音乐 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + private String hqMusicUrl; + + // ========= 事件推送 https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html + + /** + * 事件类型 + * + * 枚举 {@link WxConsts.EventType} + */ + private String event; + /** + * 事件 Key + * + * 1. {@link WxConsts.EventType} 的 SCAN:qrscene_ 为前缀,后面为二维码的参数值 + * 2. {@link WxConsts.EventType} 的 CLICK:与自定义菜单接口中 KEY 值对应 + */ + private String eventKey; + + /** + * 文章 + */ + @Data + public static class Article implements Serializable { + + /** + * 图文消息标题 + */ + @NotEmpty(message = "图文消息标题不能为空", groups = NewsBuilder.class) + private String title; + /** + * 图文消息描述 + */ + @NotEmpty(message = "图文消息描述不能为空", groups = NewsBuilder.class) + private String description; + /** + * 图片链接 + * + * 支持 JPG、PNG 格式,较好的效果为大图 360*200,小图 200*200 + */ + @NotEmpty(message = "图片链接不能为空", groups = NewsBuilder.class) + private String picUrl; + /** + * 点击图文消息跳转链接 + */ + @NotEmpty(message = "点击图文消息跳转链接不能为空", groups = NewsBuilder.class) + private String url; + + } + + // TODO @芋艿:可以找一些新的思路 + public static class ArticleTypeHandler extends AbstractJsonTypeHandler> { + + @Override + protected List
parse(String json) { + return JsonUtils.parseArray(json, Article.class); + } + + @Override + protected String toJson(List
obj) { + return JsonUtils.toJsonString(obj); + } + + } +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/tag/MpTagDO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/tag/MpTagDO.java new file mode 100644 index 0000000..0cd2bbf --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/tag/MpTagDO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.mp.dal.dataobject.tag; + +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import lombok.*; + +import com.baomidou.mybatisplus.annotation.*; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import me.chanjar.weixin.mp.bean.tag.WxUserTag; + +/** + * 公众号标签 DO + * + * @author 芋道源码 + */ +@TableName("mp_tag") +@KeySequence("mp_tag_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MpTagDO extends BaseDO { + + /** + * 主键 + */ + @TableId(type = IdType.INPUT) + private Long id; + /** + * 公众号标签 id + */ + private Long tagId; + /** + * 标签名 + */ + private String name; + /** + * 此标签下粉丝数 + * + * 冗余:{@link WxUserTag#getCount()} 字段,需要管理员点击【同步】后,更新该字段 + */ + private Integer count; + + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/user/MpUserDO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/user/MpUserDO.java new file mode 100644 index 0000000..dc42b79 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/dataobject/user/MpUserDO.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.module.mp.dal.dataobject.user; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.LongListTypeHandler; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.tag.MpTagDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 微信公众号粉丝 DO + * + * @author 芋道源码 + */ +@TableName(value = "mp_user", autoResultMap = true) +@KeySequence("mp_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MpUserDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 粉丝标识 + */ + private String openid; + /** + * 关注状态 + * + * 枚举 {@link CommonStatusEnum} + * 1. 开启 - 已关注 + * 2. 禁用 - 取消关注 + */ + private Integer subscribeStatus; + /** + * 关注时间 + */ + private LocalDateTime subscribeTime; + /** + * 取消关注时间 + */ + private LocalDateTime unsubscribeTime; + /** + * 昵称 + * + * 注意,2021-12-27 公众号接口不再返回头像和昵称,只能通过微信公众号的网页登录获取 + */ + private String nickname; + /** + * 头像地址 + * + * 注意,2021-12-27 公众号接口不再返回头像和昵称,只能通过微信公众号的网页登录获取 + */ + private String headImageUrl; + /** + * 语言 + */ + private String language; + /** + * 国家 + */ + private String country; + /** + * 省份 + */ + private String province; + /** + * 城市 + */ + private String city; + /** + * 备注 + */ + private String remark; + /** + * 标签编号数组 + * + * 注意,对应的是 {@link MpTagDO#getTagId()} 字段 + */ + @TableField(typeHandler = LongListTypeHandler.class) + private List tagIds; + + /** + * 公众号账号的编号 + * + * 关联 {@link MpAccountDO#getId()} + */ + private Long accountId; + /** + * 公众号 appId + * + * 冗余 {@link MpAccountDO#getAppId()} + */ + private String appId; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/account/MpAccountMapper.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/account/MpAccountMapper.java new file mode 100644 index 0000000..e712fb1 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/account/MpAccountMapper.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.mp.dal.mysql.account; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountPageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; + +@Mapper +public interface MpAccountMapper extends BaseMapperX { + + default PageResult selectPage(MpAccountPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(MpAccountDO::getName, reqVO.getName()) + .likeIfPresent(MpAccountDO::getAccount, reqVO.getAccount()) + .likeIfPresent(MpAccountDO::getAppId, reqVO.getAppId()) + .orderByDesc(MpAccountDO::getId)); + } + + default MpAccountDO selectByAppId(String appId) { + return selectOne(MpAccountDO::getAppId, appId); + } + + @Select("SELECT COUNT(*) FROM mp_account WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/material/MpMaterialMapper.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/material/MpMaterialMapper.java new file mode 100644 index 0000000..8271690 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/material/MpMaterialMapper.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.mp.dal.mysql.material; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.material.MpMaterialDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface MpMaterialMapper extends BaseMapperX { + + default MpMaterialDO selectByAccountIdAndMediaId(Long accountId, String mediaId) { + return selectOne(MpMaterialDO::getAccountId, accountId, + MpMaterialDO::getMediaId, mediaId); + } + + default PageResult selectPage(MpMaterialPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .eq(MpMaterialDO::getAccountId, pageReqVO.getAccountId()) + .eqIfPresent(MpMaterialDO::getPermanent, pageReqVO.getPermanent()) + .eqIfPresent(MpMaterialDO::getType, pageReqVO.getType()) + .orderByDesc(MpMaterialDO::getId)); + } + + default List selectListByMediaId(Collection mediaIds) { + return selectList(MpMaterialDO::getMediaId, mediaIds); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/menu/MpMenuMapper.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/menu/MpMenuMapper.java new file mode 100644 index 0000000..6ead41c --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/menu/MpMenuMapper.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.mp.dal.mysql.menu; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.mp.dal.dataobject.menu.MpMenuDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MpMenuMapper extends BaseMapperX { + + default MpMenuDO selectByAppIdAndMenuKey(String appId, String menuKey) { + return selectOne(MpMenuDO::getAppId, appId, + MpMenuDO::getMenuKey, menuKey); + } + + default List selectListByAccountId(Long accountId) { + return selectList(MpMenuDO::getAccountId, accountId); + } + + default void deleteByAccountId(Long accountId) { + delete(new LambdaQueryWrapperX().eq(MpMenuDO::getAccountId, accountId)); + } +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/message/MpAutoReplyMapper.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/message/MpAutoReplyMapper.java new file mode 100644 index 0000000..26aaeb0 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/message/MpAutoReplyMapper.java @@ -0,0 +1,70 @@ +package com.yunxi.scm.module.mp.dal.mysql.message; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpAutoReplyDO; +import com.yunxi.scm.module.mp.enums.message.MpAutoReplyMatchEnum; +import com.yunxi.scm.module.mp.enums.message.MpAutoReplyTypeEnum; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MpAutoReplyMapper extends BaseMapperX { + + default PageResult selectPage(MpMessagePageReqVO pageVO) { + return selectPage(pageVO, new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAccountId, pageVO.getAccountId()) + .eqIfPresent(MpAutoReplyDO::getType, pageVO.getType())); + } + + default List selectListByAppIdAndKeywordAll(String appId, String requestKeyword) { + return selectList(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAppId, appId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) + .eq(MpAutoReplyDO::getRequestMatch, MpAutoReplyMatchEnum.ALL.getMatch()) + .eq(MpAutoReplyDO::getRequestKeyword, requestKeyword)); + } + + default List selectListByAppIdAndKeywordLike(String appId, String requestKeyword) { + return selectList(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAppId, appId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) + .eq(MpAutoReplyDO::getRequestMatch, MpAutoReplyMatchEnum.LIKE.getMatch()) + .like(MpAutoReplyDO::getRequestKeyword, requestKeyword)); + } + + default List selectListByAppIdAndMessage(String appId, String requestMessageType) { + return selectList(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAppId, appId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.MESSAGE.getType()) + .eq(MpAutoReplyDO::getRequestMessageType, requestMessageType)); + } + + default List selectListByAppIdAndSubscribe(String appId) { + return selectList(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAppId, appId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.SUBSCRIBE.getType())); + } + + default MpAutoReplyDO selectByAccountIdAndSubscribe(Long accountId) { + return selectOne(MpAutoReplyDO::getAccountId, accountId, + MpAutoReplyDO::getType, MpAutoReplyTypeEnum.SUBSCRIBE.getType()); + } + + default MpAutoReplyDO selectByAccountIdAndMessage(Long accountId, String requestMessageType) { + return selectOne(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAccountId, accountId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.MESSAGE.getType()) + .eq(MpAutoReplyDO::getRequestMessageType, requestMessageType)); + } + + default MpAutoReplyDO selectByAccountIdAndKeyword(Long accountId, String requestKeyword) { + return selectOne(new LambdaQueryWrapperX() + .eq(MpAutoReplyDO::getAccountId, accountId) + .eq(MpAutoReplyDO::getType, MpAutoReplyTypeEnum.KEYWORD.getType()) + .eq(MpAutoReplyDO::getRequestKeyword, requestKeyword)); + } +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/message/MpMessageMapper.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/message/MpMessageMapper.java new file mode 100644 index 0000000..a2c4d76 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/message/MpMessageMapper.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.mp.dal.mysql.message; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MpMessageMapper extends BaseMapperX { + + default PageResult selectPage(MpMessagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MpMessageDO::getAccountId, reqVO.getAccountId()) + .eqIfPresent(MpMessageDO::getType, reqVO.getType()) + .eqIfPresent(MpMessageDO::getOpenid, reqVO.getOpenid()) + .betweenIfPresent(MpMessageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(MpMessageDO::getId)); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/tag/MpTagMapper.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/tag/MpTagMapper.java new file mode 100644 index 0000000..cb158bf --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/tag/MpTagMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.mp.dal.mysql.tag; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagPageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.tag.MpTagDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MpTagMapper extends BaseMapperX { + + default PageResult selectPage(MpTagPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MpTagDO::getAccountId, reqVO.getAccountId()) + .likeIfPresent(MpTagDO::getName, reqVO.getName()) + .orderByDesc(MpTagDO::getId)); + } + + default List selectListByAccountId(Long accountId) { + return selectList(MpTagDO::getAccountId, accountId); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/user/MpUserMapper.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/user/MpUserMapper.java new file mode 100644 index 0000000..f6eaea1 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/dal/mysql/user/MpUserMapper.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.mp.dal.mysql.user; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserPageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.user.MpUserDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MpUserMapper extends BaseMapperX { + + default PageResult selectPage(MpUserPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(MpUserDO::getOpenid, reqVO.getOpenid()) + .likeIfPresent(MpUserDO::getNickname, reqVO.getNickname()) + .eqIfPresent(MpUserDO::getAccountId, reqVO.getAccountId()) + .orderByDesc(MpUserDO::getId)); + } + + default MpUserDO selectByAppIdAndOpenid(String appId, String openid) { + return selectOne(MpUserDO::getAppId, appId, + MpUserDO::getOpenid, openid); + } + + default List selectListByAppIdAndOpenid(String appId, List openids) { + return selectList(new LambdaQueryWrapperX() + .eq(MpUserDO::getAppId, appId) + .in(MpUserDO::getOpenid, openids)); + + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/config/MpConfiguration.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/config/MpConfiguration.java new file mode 100644 index 0000000..d94d045 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/config/MpConfiguration.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.mp.framework.mp.config; + +import com.yunxi.scm.module.mp.framework.mp.core.DefaultMpServiceFactory; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.service.handler.menu.MenuHandler; +import com.yunxi.scm.module.mp.service.handler.message.MessageReceiveHandler; +import com.yunxi.scm.module.mp.service.handler.message.MessageAutoReplyHandler; +import com.yunxi.scm.module.mp.service.handler.other.KfSessionHandler; +import com.yunxi.scm.module.mp.service.handler.other.NullHandler; +import com.yunxi.scm.module.mp.service.handler.other.ScanHandler; +import com.yunxi.scm.module.mp.service.handler.other.StoreCheckNotifyHandler; +import com.yunxi.scm.module.mp.service.handler.user.LocationHandler; +import com.yunxi.scm.module.mp.service.handler.user.SubscribeHandler; +import com.yunxi.scm.module.mp.service.handler.user.UnsubscribeHandler; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 微信公众号的配置类 + * + * @author 芋道源码 + */ +@Configuration +public class MpConfiguration { + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public RedisTemplateWxRedisOps redisTemplateWxRedisOps(StringRedisTemplate stringRedisTemplate) { + return new RedisTemplateWxRedisOps(stringRedisTemplate); + } + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public MpServiceFactory mpServiceFactory(RedisTemplateWxRedisOps redisTemplateWxRedisOps, + WxMpProperties wxMpProperties, + MessageReceiveHandler messageReceiveHandler, + KfSessionHandler kfSessionHandler, + StoreCheckNotifyHandler storeCheckNotifyHandler, + MenuHandler menuHandler, + NullHandler nullHandler, + SubscribeHandler subscribeHandler, + UnsubscribeHandler unsubscribeHandler, + LocationHandler locationHandler, + ScanHandler scanHandler, + MessageAutoReplyHandler messageAutoReplyHandler) { + return new DefaultMpServiceFactory(redisTemplateWxRedisOps, wxMpProperties, + messageReceiveHandler, kfSessionHandler, storeCheckNotifyHandler, menuHandler, + nullHandler, subscribeHandler, unsubscribeHandler, locationHandler, scanHandler, messageAutoReplyHandler); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/DefaultMpServiceFactory.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/DefaultMpServiceFactory.java new file mode 100644 index 0000000..463705c --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/DefaultMpServiceFactory.java @@ -0,0 +1,177 @@ +package com.yunxi.scm.module.mp.framework.mp.core; + +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.service.handler.menu.MenuHandler; +import com.yunxi.scm.module.mp.service.handler.message.MessageReceiveHandler; +import com.yunxi.scm.module.mp.service.handler.message.MessageAutoReplyHandler; +import com.yunxi.scm.module.mp.service.handler.other.KfSessionHandler; +import com.yunxi.scm.module.mp.service.handler.other.NullHandler; +import com.yunxi.scm.module.mp.service.handler.other.ScanHandler; +import com.yunxi.scm.module.mp.service.handler.other.StoreCheckNotifyHandler; +import com.yunxi.scm.module.mp.service.handler.user.LocationHandler; +import com.yunxi.scm.module.mp.service.handler.user.SubscribeHandler; +import com.yunxi.scm.module.mp.service.handler.user.UnsubscribeHandler; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import com.google.common.collect.Maps; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import me.chanjar.weixin.mp.api.WxMpMessageRouter; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import me.chanjar.weixin.mp.constant.WxMpEventConstants; + +import java.util.List; +import java.util.Map; + +/** + * 默认的 {@link MpServiceFactory} 实现类 + * + * @author 芋道源码 + */ +@Slf4j +@RequiredArgsConstructor +public class DefaultMpServiceFactory implements MpServiceFactory { + + /** + * 微信 appId 与 WxMpService 的映射 + */ + private volatile Map appId2MpServices; + /** + * 公众号账号 id 与 WxMpService 的映射 + */ + private volatile Map id2MpServices; + /** + * 微信 appId 与 WxMpMessageRouter 的映射 + */ + private volatile Map mpMessageRouters; + + private final RedisTemplateWxRedisOps redisTemplateWxRedisOps; + private final WxMpProperties mpProperties; + + // ========== 各种 Handler ========== + + private final MessageReceiveHandler messageReceiveHandler; + private final KfSessionHandler kfSessionHandler; + private final StoreCheckNotifyHandler storeCheckNotifyHandler; + private final MenuHandler menuHandler; + private final NullHandler nullHandler; + private final SubscribeHandler subscribeHandler; + private final UnsubscribeHandler unsubscribeHandler; + private final LocationHandler locationHandler; + private final ScanHandler scanHandler; + private final MessageAutoReplyHandler messageAutoReplyHandler; + + @Override + public void init(List list) { + Map appId2MpServices = Maps.newHashMap(); + Map id2MpServices = Maps.newHashMap(); + Map mpMessageRouters = Maps.newHashMap(); + // 处理 list + list.forEach(account -> { + // 构建 WxMpService 对象 + WxMpService mpService = buildMpService(account); + appId2MpServices.put(account.getAppId(), mpService); + id2MpServices.put(account.getId(), mpService); + // 构建 WxMpMessageRouter 对象 + WxMpMessageRouter mpMessageRouter = buildMpMessageRouter(mpService); + mpMessageRouters.put(account.getAppId(), mpMessageRouter); + }); + + // 设置到缓存 + this.appId2MpServices = appId2MpServices; + this.id2MpServices = id2MpServices; + this.mpMessageRouters = mpMessageRouters; + } + + @Override + public WxMpService getMpService(Long id) { + return id2MpServices.get(id); + } + + @Override + public WxMpService getMpService(String appId) { + return appId2MpServices.get(appId); + } + + @Override + public WxMpMessageRouter getMpMessageRouter(String appId) { + return mpMessageRouters.get(appId); + } + + private WxMpService buildMpService(MpAccountDO account) { + // 第一步,创建 WxMpRedisConfigImpl 对象 + WxMpRedisConfigImpl configStorage = new WxMpRedisConfigImpl( + redisTemplateWxRedisOps, mpProperties.getConfigStorage().getKeyPrefix()); + configStorage.setAppId(account.getAppId()); + configStorage.setSecret(account.getAppSecret()); + configStorage.setToken(account.getToken()); + configStorage.setAesKey(account.getAesKey()); + + // 第二步,创建 WxMpService 对象 + WxMpService service = new WxMpServiceImpl(); + service.setWxMpConfigStorage(configStorage); + return service; + } + + private WxMpMessageRouter buildMpMessageRouter(WxMpService mpService) { + WxMpMessageRouter router = new WxMpMessageRouter(mpService); + // 记录所有事件的日志(异步执行) + router.rule().handler(messageReceiveHandler).next(); + + // 接收客服会话管理事件 + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxMpEventConstants.CustomerService.KF_CREATE_SESSION) + .handler(kfSessionHandler).end(); + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxMpEventConstants.CustomerService.KF_CLOSE_SESSION) + .handler(kfSessionHandler) + .end(); + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxMpEventConstants.CustomerService.KF_SWITCH_SESSION) + .handler(kfSessionHandler).end(); + + // 门店审核事件 + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxMpEventConstants.POI_CHECK_NOTIFY) + .handler(storeCheckNotifyHandler).end(); + + // 自定义菜单事件 + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.MenuButtonType.CLICK).handler(menuHandler).end(); + + // 点击菜单连接事件 + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.MenuButtonType.VIEW).handler(nullHandler).end(); + + // 关注事件 + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.SUBSCRIBE).handler(subscribeHandler) + .end(); + + // 取消关注事件 + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.UNSUBSCRIBE) + .handler(unsubscribeHandler).end(); + + // 上报地理位置事件 + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.LOCATION).handler(locationHandler) + .end(); + + // 接收地理位置消息 + router.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION) + .handler(locationHandler).end(); + + // 扫码事件 + router.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.SCAN).handler(scanHandler).end(); + + // 默认 + router.rule().async(false).handler(messageAutoReplyHandler).end(); + return router; + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/MpServiceFactory.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/MpServiceFactory.java new file mode 100644 index 0000000..8bc24c9 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/MpServiceFactory.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.module.mp.framework.mp.core; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import me.chanjar.weixin.mp.api.WxMpMessageRouter; +import me.chanjar.weixin.mp.api.WxMpService; + +import java.util.List; + +/** + * {@link WxMpService} 工厂接口 + * + * @author 芋道源码 + */ +public interface MpServiceFactory { + + /** + * 基于微信公众号的账号,初始化对应的 WxMpService 与 WxMpMessageRouter 实例 + * + * @param list 公众号的账号列表 + */ + void init(List list); + + /** + * 获得 id 对应的 WxMpService 实例 + * + * @param id 微信公众号的编号 + * @return WxMpService 实例 + */ + WxMpService getMpService(Long id); + + default WxMpService getRequiredMpService(Long id) { + WxMpService wxMpService = getMpService(id); + Assert.notNull(wxMpService, "找到对应 id({}) 的 WxMpService,请核实!", id); + return wxMpService; + } + + /** + * 获得 appId 对应的 WxMpService 实例 + * + * @param appId 微信公众号 appId + * @return WxMpService 实例 + */ + WxMpService getMpService(String appId); + + default WxMpService getRequiredMpService(String appId) { + WxMpService wxMpService = getMpService(appId); + Assert.notNull(wxMpService, "找到对应 appId({}) 的 WxMpService,请核实!", appId); + return wxMpService; + } + + /** + * 获得 appId 对应的 WxMpMessageRouter 实例 + * + * @param appId 微信公众号 appId + * @return WxMpMessageRouter 实例 + */ + WxMpMessageRouter getMpMessageRouter(String appId); + + default WxMpMessageRouter getRequiredMpMessageRouter(String appId) { + WxMpMessageRouter wxMpMessageRouter = getMpMessageRouter(appId); + Assert.notNull(wxMpMessageRouter, "找到对应 appId({}) 的 WxMpMessageRouter,请核实!", appId); + return wxMpMessageRouter; + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/context/MpContextHolder.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/context/MpContextHolder.java new file mode 100644 index 0000000..6354aae --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/context/MpContextHolder.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2018-2025, lengleng All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * Neither the name of the pig4cloud.com developer nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * Author: lengleng (wangiegie@gmail.com) + */ + +package com.yunxi.scm.module.mp.framework.mp.core.context; + +import com.yunxi.scm.module.mp.controller.admin.open.vo.MpOpenHandleMessageReqVO; +import com.alibaba.ttl.TransmittableThreadLocal; +import lombok.experimental.UtilityClass; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; + +/** + * 微信上下文 Context + * + * 目的:解决微信多公众号的问题,在 {@link WxMpMessageHandler} 实现类中,可以通过 {@link #getAppId()} 获取到当前的 appId + * + * @see com.yunxi.scm.module.mp.controller.admin.open.MpOpenController#handleMessage(String, String, MpOpenHandleMessageReqVO) + * + * @author 芋道源码 + */ +public class MpContextHolder { + + /** + * 微信公众号的 appId 上下文 + */ + private static final ThreadLocal APPID = new TransmittableThreadLocal<>(); + + public static void setAppId(String appId) { + APPID.set(appId); + } + + public static String getAppId() { + return APPID.get(); + } + + public static void clear() { + APPID.remove(); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/util/MpUtils.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/util/MpUtils.java new file mode 100644 index 0000000..eecf55c --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/mp/core/util/MpUtils.java @@ -0,0 +1,167 @@ +package com.yunxi.scm.module.mp.framework.mp.core.util; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; + +import javax.validation.Validator; + +/** + * 公众号工具类 + * + * @author 芋道源码 + */ +@Slf4j +public class MpUtils { + + /** + * 校验消息的格式是否符合要求 + * + * @param type 类型 + * @param message 消息 + */ + public static void validateMessage(Validator validator, String type, Object message) { + // 获得对应的校验 group + Class group; + switch (type) { + case WxConsts.XmlMsgType.TEXT: + group = TextMessageGroup.class; + break; + case WxConsts.XmlMsgType.IMAGE: + group = ImageMessageGroup.class; + break; + case WxConsts.XmlMsgType.VOICE: + group = VoiceMessageGroup.class; + break; + case WxConsts.XmlMsgType.VIDEO: + group = VideoMessageGroup.class; + break; + case WxConsts.XmlMsgType.NEWS: + group = NewsMessageGroup.class; + break; + case WxConsts.XmlMsgType.MUSIC: + group = MusicMessageGroup.class; + break; + default: + log.error("[validateMessage][未知的消息类型({})]", message); + throw new IllegalArgumentException("不支持的消息类型:" + type); + } + // 执行校验 + ValidationUtils.validate(validator, message, group); + } + + public static void validateButton(Validator validator, String type, String messageType, Object button) { + if (StrUtil.isBlank(type)) { + return; + } + // 获得对应的校验 group + Class group; + switch (type) { + case WxConsts.MenuButtonType.CLICK: + group = ClickButtonGroup.class; + validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式 + break; + case WxConsts.MenuButtonType.VIEW: + group = ViewButtonGroup.class; + break; + case WxConsts.MenuButtonType.MINIPROGRAM: + group = MiniProgramButtonGroup.class; + break; + case WxConsts.MenuButtonType.SCANCODE_WAITMSG: + group = ScanCodeWaitMsgButtonGroup.class; + validateMessage(validator, messageType, button); // 需要额外校验回复的消息格式 + break; + case "article_" + WxConsts.MenuButtonType.VIEW_LIMITED: + group = ViewLimitedButtonGroup.class; + break; + case WxConsts.MenuButtonType.SCANCODE_PUSH: // 不用校验,直接 return 即可 + case WxConsts.MenuButtonType.PIC_SYSPHOTO: + case WxConsts.MenuButtonType.PIC_PHOTO_OR_ALBUM: + case WxConsts.MenuButtonType.PIC_WEIXIN: + case WxConsts.MenuButtonType.LOCATION_SELECT: + return; + default: + log.error("[validateButton][未知的按钮({})]", button); + throw new IllegalArgumentException("不支持的按钮类型:" + type); + } + // 执行校验 + ValidationUtils.validate(validator, button, group); + } + + /** + * 根据消息类型,获得对应的媒体文件类型 + * + * 注意,不会返回 WxConsts.MediaFileType.THUMB,因为该类型会有明确标注 + * + * @param messageType 消息类型 {@link WxConsts.XmlMsgType} + * @return 媒体文件类型 {@link WxConsts.MediaFileType} + */ + public static String getMediaFileType(String messageType) { + switch (messageType) { + case WxConsts.XmlMsgType.IMAGE: + return WxConsts.MediaFileType.IMAGE; + case WxConsts.XmlMsgType.VOICE: + return WxConsts.MediaFileType.VOICE; + case WxConsts.XmlMsgType.VIDEO: + return WxConsts.MediaFileType.VIDEO; + default: + return WxConsts.MediaFileType.FILE; + } + } + + /** + * Text 类型的消息,参数校验 Group + */ + public interface TextMessageGroup {} + + /** + * Image 类型的消息,参数校验 Group + */ + public interface ImageMessageGroup {} + + /** + * Voice 类型的消息,参数校验 Group + */ + public interface VoiceMessageGroup {} + + /** + * Video 类型的消息,参数校验 Group + */ + public interface VideoMessageGroup {} + + /** + * News 类型的消息,参数校验 Group + */ + public interface NewsMessageGroup {} + + /** + * Music 类型的消息,参数校验 Group + */ + public interface MusicMessageGroup {} + + /** + * Click 类型的按钮,参数校验 Group + */ + public interface ClickButtonGroup {} + + /** + * View 类型的按钮,参数校验 Group + */ + public interface ViewButtonGroup {} + + /** + * MiniProgram 类型的按钮,参数校验 Group + */ + public interface MiniProgramButtonGroup {} + + /** + * SCANCODE_WAITMSG 类型的按钮,参数校验 Group + */ + public interface ScanCodeWaitMsgButtonGroup {} + + /** + * VIEW_LIMITED 类型的按钮,参数校验 Group + */ + public interface ViewLimitedButtonGroup {} +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/package-info.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/package-info.java new file mode 100644 index 0000000..049ac85 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 mp 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.mp.framework; diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/web/config/MpWebConfiguration.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/web/config/MpWebConfiguration.java new file mode 100644 index 0000000..d1f6f08 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/web/config/MpWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.mp.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * mp 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class MpWebConfiguration { + + /** + * mp 模块的 API 分组 + */ + @Bean + public GroupedOpenApi mpGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("mp"); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/web/package-info.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/web/package-info.java new file mode 100644 index 0000000..c6bec13 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * mp 模块的 web 配置 + */ +package com.yunxi.scm.module.mp.framework.web; diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/package-info.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/package-info.java new file mode 100644 index 0000000..ca65810 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/package-info.java @@ -0,0 +1,8 @@ +/** + * mp 模块,我们放微信微信公众号。 + * 例如说:提供微信公众号的账号、菜单、粉丝、标签、消息、自动回复、素材、模板通知、运营数据等功能 + * + * 1. Controller URL:以 /mp/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 mp_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.mp; diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/account/MpAccountService.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/account/MpAccountService.java new file mode 100644 index 0000000..0842268 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/account/MpAccountService.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.module.mp.service.account; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; + +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.ACCOUNT_NOT_EXISTS; + +/** + * 公众号账号 Service 接口 + * + * @author 芋道源码 + */ +public interface MpAccountService { + + /** + * 初始化缓存 + */ + void initLocalCache(); + + /** + * 创建公众号账号 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createAccount(@Valid MpAccountCreateReqVO createReqVO); + + /** + * 更新公众号账号 + * + * @param updateReqVO 更新信息 + */ + void updateAccount(@Valid MpAccountUpdateReqVO updateReqVO); + + /** + * 删除公众号账号 + * + * @param id 编号 + */ + void deleteAccount(Long id); + + /** + * 获得公众号账号 + * + * @param id 编号 + * @return 公众号账号 + */ + MpAccountDO getAccount(Long id); + + /** + * 获得公众号账号。若不存在,则抛出业务异常 + * + * @param id 编号 + * @return 公众号账号 + */ + default MpAccountDO getRequiredAccount(Long id) { + MpAccountDO account = getAccount(id); + if (account == null) { + throw exception(ACCOUNT_NOT_EXISTS); + } + return account; + } + + /** + * 从缓存中,获得公众号账号 + * + * @param appId 微信公众号 appId + * @return 公众号账号 + */ + MpAccountDO getAccountFromCache(String appId); + + /** + * 获得公众号账号分页 + * + * @param pageReqVO 分页查询 + * @return 公众号账号分页 + */ + PageResult getAccountPage(MpAccountPageReqVO pageReqVO); + + /** + * 获得公众号账号列表 + * + * @return 公众号账号列表 + */ + List getAccountList(); + + /** + * 生成公众号账号的二维码 + * + * @param id 编号 + */ + void generateAccountQrCode(Long id); + + /** + * 清空公众号账号的 API 配额 + * + * 参考文档:接口调用频次限制说明 + * + * @param id 编号 + */ + void clearAccountQuota(Long id); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/account/MpAccountServiceImpl.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/account/MpAccountServiceImpl.java new file mode 100644 index 0000000..e194957 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/account/MpAccountServiceImpl.java @@ -0,0 +1,229 @@ +package com.yunxi.scm.module.mp.service.account; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjUtil; +import com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.tenant.core.util.TenantUtils; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.account.vo.MpAccountUpdateReqVO; +import com.yunxi.scm.module.mp.convert.account.MpAccountConvert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.mysql.account.MpAccountMapper; +import com.yunxi.scm.module.mp.enums.ErrorCodeConstants; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.google.common.annotations.VisibleForTesting; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.getMaxValue; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.USER_USERNAME_EXISTS; + +/** + * 公众号账号 Service 实现类 + * + * @author fengdan + */ +@Slf4j +@Service +@Validated +public class MpAccountServiceImpl implements MpAccountService { + + /** + * 账号缓存 + * key:账号编号 {@link MpAccountDO#getAppId()} + * + * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + private volatile Map accountCache; + + @Resource + private MpAccountMapper mpAccountMapper; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Override + @PostConstruct + public void initLocalCache() { + // 注意:忽略自动多租户,因为要全局初始化缓存 + TenantUtils.executeIgnore(() -> { + // 第一步:查询数据 + List accounts = Collections.emptyList(); + try { + accounts = mpAccountMapper.selectList(); + } catch (Throwable ex) { + if (!ex.getMessage().contains("doesn't exist")) { + throw ex; + } + log.error("[微信公众号 yunxi-module-mp - 表结构未导入][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + log.info("[initLocalCacheIfUpdate][缓存公众号账号,数量为:{}]", accounts.size()); + + // 第二步:构建缓存。创建或更新支付 Client + mpServiceFactory.init(accounts); + accountCache = convertMap(accounts, MpAccountDO::getAppId); + }); + } + + /** + * 通过定时任务轮询,刷新缓存 + * + * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 + */ + @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) + public void refreshLocalCache() { + // 注意:忽略自动多租户,因为要全局初始化缓存 + TenantUtils.executeIgnore(() -> { + // 情况一:如果缓存里没有数据,则直接刷新缓存 + if (CollUtil.isEmpty(accountCache)) { + initLocalCache(); + return; + } + + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 + LocalDateTime maxTime = getMaxValue(accountCache.values(), MpAccountDO::getUpdateTime); + if (mpAccountMapper.selectCountByUpdateTimeGt(maxTime) > 0) { + initLocalCache(); + } + }); + } + + @Override + public Long createAccount(MpAccountCreateReqVO createReqVO) { + // 校验 appId 唯一 + validateAppIdUnique(null, createReqVO.getAppId()); + + // 插入 + MpAccountDO account = MpAccountConvert.INSTANCE.convert(createReqVO); + mpAccountMapper.insert(account); + + // 刷新缓存 + initLocalCache(); + return account.getId(); + } + + @Override + public void updateAccount(MpAccountUpdateReqVO updateReqVO) { + // 校验存在 + validateAccountExists(updateReqVO.getId()); + // 校验 appId 唯一 + validateAppIdUnique(updateReqVO.getId(), updateReqVO.getAppId()); + + // 更新 + MpAccountDO updateObj = MpAccountConvert.INSTANCE.convert(updateReqVO); + mpAccountMapper.updateById(updateObj); + + // 刷新缓存 + initLocalCache(); + } + + @Override + public void deleteAccount(Long id) { + // 校验存在 + validateAccountExists(id); + // 删除 + mpAccountMapper.deleteById(id); + + // 刷新缓存 + initLocalCache(); + } + + private MpAccountDO validateAccountExists(Long id) { + MpAccountDO account = mpAccountMapper.selectById(id); + if (account == null) { + throw ServiceExceptionUtil.exception(ErrorCodeConstants.ACCOUNT_NOT_EXISTS); + } + return account; + } + + @VisibleForTesting + public void validateAppIdUnique(Long id, String appId) { + // 多个租户,appId 是不能重复,否则公众号回调会无法识别 + TenantUtils.executeIgnore(() -> { + MpAccountDO account = mpAccountMapper.selectByAppId(appId); + if (account == null) { + return; + } + // 存在 account 记录的情况下 + if (id == null // 新增时,说明重复 + || ObjUtil.notEqual(id, account.getId())) { // 更新时,如果 id 不一致,说明重复 + throw exception(USER_USERNAME_EXISTS); + } + }); + } + + @Override + public MpAccountDO getAccount(Long id) { + return mpAccountMapper.selectById(id); + } + + @Override + public MpAccountDO getAccountFromCache(String appId) { + return accountCache.get(appId); + } + + @Override + public PageResult getAccountPage(MpAccountPageReqVO pageReqVO) { + return mpAccountMapper.selectPage(pageReqVO); + } + + @Override + public List getAccountList() { + return mpAccountMapper.selectList(); + } + + @Override + public void generateAccountQrCode(Long id) { + // 校验存在 + MpAccountDO account = validateAccountExists(id); + + // 生成二维码 + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getAppId()); + String qrCodeUrl; + try { + WxMpQrCodeTicket qrCodeTicket = mpService.getQrcodeService().qrCodeCreateLastTicket("default"); + qrCodeUrl = mpService.getQrcodeService().qrCodePictureUrl(qrCodeTicket.getTicket()); + } catch (WxErrorException e) { + throw exception(ErrorCodeConstants.ACCOUNT_GENERATE_QR_CODE_FAIL, e.getError().getErrorMsg()); + } + + // 保存二维码 + mpAccountMapper.updateById(new MpAccountDO().setId(id).setQrCodeUrl(qrCodeUrl)); + } + + @Override + public void clearAccountQuota(Long id) { + // 校验存在 + MpAccountDO account = validateAccountExists(id); + + // 生成二维码 + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getAppId()); + try { + mpService.clearQuota(account.getAppId()); + } catch (WxErrorException e) { + throw exception(ErrorCodeConstants.ACCOUNT_CLEAR_QUOTA_FAIL, e.getError().getErrorMsg()); + } + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/menu/MenuHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/menu/MenuHandler.java new file mode 100644 index 0000000..23f3ae9 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/menu/MenuHandler.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.mp.service.handler.menu; + +import com.yunxi.scm.module.mp.framework.mp.core.context.MpContextHolder; +import com.yunxi.scm.module.mp.service.menu.MpMenuService; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMenuService; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +import static me.chanjar.weixin.common.api.WxConsts.MenuButtonType; + +/** + * 自定义菜单的事件处理器 + * + * 逻辑:粉丝点击菜单时,触发对应的回复 + * + * @author 芋道源码 + */ +@Component +public class MenuHandler implements WxMpMessageHandler { + + @Resource + private MpMenuService mpMenuService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService weixinService, WxSessionManager sessionManager) { + return mpMenuService.reply(MpContextHolder.getAppId(), wxMessage.getEventKey(), wxMessage.getFromUser()); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/message/MessageAutoReplyHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/message/MessageAutoReplyHandler.java new file mode 100644 index 0000000..2f52792 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/message/MessageAutoReplyHandler.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.mp.service.handler.message; + +import com.yunxi.scm.module.mp.dal.dataobject.message.MpAutoReplyDO; +import com.yunxi.scm.module.mp.framework.mp.core.context.MpContextHolder; +import com.yunxi.scm.module.mp.service.message.MpAutoReplyService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 自动回复消息的事件处理器 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class MessageAutoReplyHandler implements WxMpMessageHandler { + + @Resource + private MpAutoReplyService mpAutoReplyService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService weixinService, WxSessionManager sessionManager) { + // 只处理指定类型的消息 + if (!MpAutoReplyDO.REQUEST_MESSAGE_TYPE.contains(wxMessage.getMsgType())) { + return null; + } + + // 自动回复 + return mpAutoReplyService.replyForMessage(MpContextHolder.getAppId(), wxMessage); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/message/MessageReceiveHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/message/MessageReceiveHandler.java new file mode 100644 index 0000000..f5c25dc --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/message/MessageReceiveHandler.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.mp.service.handler.message; + +import com.yunxi.scm.module.mp.framework.mp.core.context.MpContextHolder; +import com.yunxi.scm.module.mp.service.message.MpMessageService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 保存微信消息的事件处理器 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class MessageReceiveHandler implements WxMpMessageHandler { + + @Resource + private MpMessageService mpMessageService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + log.info("[handle][接收到请求消息,内容:{}]", wxMessage); + mpMessageService.receiveMessage(MpContextHolder.getAppId(), wxMessage); + return null; + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/KfSessionHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/KfSessionHandler.java new file mode 100644 index 0000000..52e598c --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/KfSessionHandler.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.mp.service.handler.other; + +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 接收客服会话管理的事件处理器 + * + * @author 芋道源码 + */ +@Component +public class KfSessionHandler implements WxMpMessageHandler { + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + throw new UnsupportedOperationException("未实现该处理,请自行重写"); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/NullHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/NullHandler.java new file mode 100644 index 0000000..38d70d9 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/NullHandler.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.mp.service.handler.other; + +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 点击菜单连接的事件处理器 + */ +@Component +public class NullHandler implements WxMpMessageHandler { + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + throw new UnsupportedOperationException("未实现该处理,请自行重写"); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/ScanHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/ScanHandler.java new file mode 100644 index 0000000..751ac4f --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/ScanHandler.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.mp.service.handler.other; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 扫码的事件处理器 + */ +@Component +public class ScanHandler implements WxMpMessageHandler { + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map context, + WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException { + throw new UnsupportedOperationException("未实现该处理,请自行重写"); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/StoreCheckNotifyHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/StoreCheckNotifyHandler.java new file mode 100644 index 0000000..6669ed8 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/StoreCheckNotifyHandler.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.mp.service.handler.other; + +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * 门店审核事件的事件处理器 + */ +@Component +public class StoreCheckNotifyHandler implements WxMpMessageHandler { + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + throw new UnsupportedOperationException("未实现该处理,请自行重写"); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/package-info.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/package-info.java new file mode 100644 index 0000000..a3474c1 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/other/package-info.java @@ -0,0 +1,4 @@ +/** + * 本包内的 handler 都是一些不重要的,所以放在 other 其它里 + */ +package com.yunxi.scm.module.mp.service.handler.other; diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/LocationHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/LocationHandler.java new file mode 100644 index 0000000..43ccb42 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/LocationHandler.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.mp.service.handler.user; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.module.mp.framework.mp.core.context.MpContextHolder; +import com.yunxi.scm.module.mp.service.message.MpAutoReplyService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 上报地理位置的事件处理器 + * + * 触发操作:打开微信公众号 -> 点击 + 号 -> 选择「语音」 + * + * 逻辑:粉丝上传地理位置时,也可以触发自动回复 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class LocationHandler implements WxMpMessageHandler { + + @Resource + private MpAutoReplyService mpAutoReplyService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService wxMpService, WxSessionManager sessionManager) { + // 防御性编程:必须是 LOCATION 消息 + if (ObjectUtil.notEqual(wxMessage.getMsgType(), WxConsts.XmlMsgType.LOCATION)) { + return null; + } + log.info("[handle][上报地理位置,纬度({})、经度({})、精度({})", wxMessage.getLatitude(), + wxMessage.getLongitude(), wxMessage.getPrecision()); + + // 自动回复 + return mpAutoReplyService.replyForMessage(MpContextHolder.getAppId(), wxMessage); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/SubscribeHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/SubscribeHandler.java new file mode 100644 index 0000000..730011e --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/SubscribeHandler.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.mp.service.handler.user; + +import com.yunxi.scm.module.mp.framework.mp.core.context.MpContextHolder; +import com.yunxi.scm.module.mp.service.message.MpAutoReplyService; +import com.yunxi.scm.module.mp.service.user.MpUserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import me.chanjar.weixin.mp.bean.result.WxMpUser; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 关注的事件处理器 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SubscribeHandler implements WxMpMessageHandler { + + @Resource + private MpUserService mpUserService; + @Resource + private MpAutoReplyService mpAutoReplyService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, Map context, + WxMpService weixinService, WxSessionManager sessionManager) throws WxErrorException { + // 第一步,从公众号平台,获取粉丝信息 + log.info("[handle][粉丝({}) 关注]", wxMessage.getFromUser()); + WxMpUser wxMpUser = null; + try { + wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser()); + } catch (WxErrorException e) { + log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e); + } + + // 第二步,保存粉丝信息 + mpUserService.saveUser(MpContextHolder.getAppId(), wxMpUser); + + // 第三步,回复关注的欢迎语 + return mpAutoReplyService.replyForSubscribe(MpContextHolder.getAppId(), wxMessage); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/UnsubscribeHandler.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/UnsubscribeHandler.java new file mode 100644 index 0000000..7ca1519 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/handler/user/UnsubscribeHandler.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.mp.service.handler.user; + +import com.yunxi.scm.module.mp.framework.mp.core.context.MpContextHolder; +import com.yunxi.scm.module.mp.service.user.MpUserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.session.WxSessionManager; +import me.chanjar.weixin.mp.api.WxMpMessageHandler; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.Map; + +/** + * 取消关注的事件处理器 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class UnsubscribeHandler implements WxMpMessageHandler { + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpUserService mpUserService; + + @Override + public WxMpXmlOutMessage handle(WxMpXmlMessage wxMessage, + Map context, WxMpService wxMpService, + WxSessionManager sessionManager) { + log.info("[handle][粉丝({}) 取消关注]", wxMessage.getFromUser()); + mpUserService.updateUserUnsubscribe(MpContextHolder.getAppId(), wxMessage.getFromUser()); + return null; + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/material/MpMaterialService.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/material/MpMaterialService.java new file mode 100644 index 0000000..af263fb --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/material/MpMaterialService.java @@ -0,0 +1,84 @@ +package com.yunxi.scm.module.mp.service.material; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.material.MpMaterialDO; +import me.chanjar.weixin.common.api.WxConsts; + +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +/** + * 公众号素材 Service 接口 + * + * @author 芋道源码 + */ +public interface MpMaterialService { + + /** + * 获得素材的 URL + * + * 该 URL 来自我们自己的文件服务器存储的 URL,不是公众号存储的 URL + * + * @param accountId 公众号账号编号 + * @param mediaId 公众号素材 id + * @param type 文件类型 {@link WxConsts.MediaFileType} + * @return 素材的 URL + */ + String downloadMaterialUrl(Long accountId, String mediaId, String type); + + /** + * 上传临时素材 + * + * @param reqVO 请求 + * @return 素材 + * @throws IOException 文件操作发生异常 + */ + MpMaterialDO uploadTemporaryMaterial(@Valid MpMaterialUploadTemporaryReqVO reqVO) throws IOException; + + /** + * 上传永久素材 + * + * @param reqVO 请求 + * @return 素材 + * @throws IOException 文件操作发生异常 + */ + MpMaterialDO uploadPermanentMaterial(@Valid MpMaterialUploadPermanentReqVO reqVO) throws IOException; + + /** + * 上传图文内容中的图片 + * + * @param reqVO 上传请求 + * @return 图片地址 + */ + String uploadNewsImage(MpMaterialUploadNewsImageReqVO reqVO) throws IOException; + + /** + * 获得素材分页 + * + * @param pageReqVO 分页请求 + * @return 素材分页 + */ + PageResult getMaterialPage(MpMaterialPageReqVO pageReqVO); + + /** + * 获得素材列表 + * + * @param mediaIds 素材 mediaId 列表 + * @return 素材列表 + */ + List getMaterialListByMediaId(Collection mediaIds); + + /** + * 删除素材 + * + * @param id 编号 + */ + void deleteMaterial(Long id); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/material/MpMaterialServiceImpl.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/material/MpMaterialServiceImpl.java new file mode 100644 index 0000000..008f815 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/material/MpMaterialServiceImpl.java @@ -0,0 +1,224 @@ +package com.yunxi.scm.module.mp.service.material; + +import cn.hutool.core.io.FileTypeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.infra.api.file.FileApi; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialUploadNewsImageReqVO; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialUploadPermanentReqVO; +import com.yunxi.scm.module.mp.controller.admin.material.vo.MpMaterialUploadTemporaryReqVO; +import com.yunxi.scm.module.mp.convert.material.MpMaterialConvert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.material.MpMaterialDO; +import com.yunxi.scm.module.mp.dal.mysql.material.MpMaterialMapper; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.service.account.MpAccountService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.*; + +/** + * 公众号素材 Service 接口 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class MpMaterialServiceImpl implements MpMaterialService { + + @Resource + private MpMaterialMapper mpMaterialMapper; + + @Resource + private FileApi fileApi; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpAccountService mpAccountService; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Override + public String downloadMaterialUrl(Long accountId, String mediaId, String type) { + // 第一步,直接从数据库查询。如果已经下载,直接返回 + MpMaterialDO material = mpMaterialMapper.selectByAccountIdAndMediaId(accountId, mediaId); + if (material != null) { + return material.getUrl(); + } + + // 第二步,尝试从临时素材中下载 + String url = downloadMedia(accountId, mediaId); + if (url == null) { + return null; + } + MpAccountDO account = mpAccountService.getRequiredAccount(accountId); + material = MpMaterialConvert.INSTANCE.convert(mediaId, type, url, account, null) + .setPermanent(false); + mpMaterialMapper.insert(material); + + // 不考虑下载永久素材,因为上传的时候已经保存 + return url; + } + + @Override + public MpMaterialDO uploadTemporaryMaterial(MpMaterialUploadTemporaryReqVO reqVO) throws IOException { + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); + // 第一步,上传到公众号 + File file = null; + WxMediaUploadResult result; + String mediaId; + String url; + try { + // 写入到临时文件 + file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); + reqVO.getFile().transferTo(file); + // 上传到公众号 + result = mpService.getMaterialService().mediaUpload(reqVO.getType(), file); + // 上传到文件服务 + mediaId = ObjUtil.defaultIfNull(result.getMediaId(), result.getThumbMediaId()); + url = uploadFile(mediaId, file); + } catch (WxErrorException e) { + throw exception(MATERIAL_UPLOAD_FAIL, e.getError().getErrorMsg()); + } finally { + FileUtil.del(file); + } + + // 第二步,存储到数据库 + MpAccountDO account = mpAccountService.getRequiredAccount(reqVO.getAccountId()); + MpMaterialDO material = MpMaterialConvert.INSTANCE.convert(mediaId, reqVO.getType(), url, account, + reqVO.getFile().getName()).setPermanent(false); + mpMaterialMapper.insert(material); + return material; + } + + @Override + public MpMaterialDO uploadPermanentMaterial(MpMaterialUploadPermanentReqVO reqVO) throws IOException { + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); + // 第一步,上传到公众号 + String name = StrUtil.blankToDefault(reqVO.getName(), reqVO.getFile().getName()); + File file = null; + WxMpMaterialUploadResult result; + String mediaId; + String url; + try { + // 写入到临时文件 + file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); + reqVO.getFile().transferTo(file); + // 上传到公众号 + result = mpService.getMaterialService().materialFileUpload(reqVO.getType(), + MpMaterialConvert.INSTANCE.convert(name, file, reqVO.getTitle(), reqVO.getIntroduction())); + // 上传到文件服务 + mediaId = ObjUtil.defaultIfNull(result.getMediaId(), result.getMediaId()); + url = uploadFile(mediaId, file); + } catch (WxErrorException e) { + throw exception(MATERIAL_UPLOAD_FAIL, e.getError().getErrorMsg()); + } finally { + FileUtil.del(file); + } + + // 第二步,存储到数据库 + MpAccountDO account = mpAccountService.getRequiredAccount(reqVO.getAccountId()); + MpMaterialDO material = MpMaterialConvert.INSTANCE.convert(mediaId, reqVO.getType(), url, account, + name, reqVO.getTitle(), reqVO.getIntroduction(), result.getUrl()).setPermanent(true); + mpMaterialMapper.insert(material); + return material; + } + + @Override + public String uploadNewsImage(MpMaterialUploadNewsImageReqVO reqVO) throws IOException { + WxMpService mpService = mpServiceFactory.getRequiredMpService(reqVO.getAccountId()); + File file = null; + try { + // 写入到临时文件 + file = FileUtil.newFile(FileUtil.getTmpDirPath() + reqVO.getFile().getOriginalFilename()); + reqVO.getFile().transferTo(file); + // 上传到公众号 + return mpService.getMaterialService().mediaImgUpload(file).getUrl(); + } catch (WxErrorException e) { + throw exception(MATERIAL_IMAGE_UPLOAD_FAIL, e.getError().getErrorMsg()); + } finally { + FileUtil.del(file); + } + } + + @Override + public PageResult getMaterialPage(MpMaterialPageReqVO pageReqVO) { + return mpMaterialMapper.selectPage(pageReqVO); + } + + @Override + public List getMaterialListByMediaId(Collection mediaIds) { + return mpMaterialMapper.selectListByMediaId(mediaIds); + } + + @Override + public void deleteMaterial(Long id) { + MpMaterialDO material = mpMaterialMapper.selectById(id); + if (material == null) { + throw exception(MATERIAL_NOT_EXISTS); + } + + // 第一步,从公众号删除 + if (material.getPermanent()) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(material.getAppId()); + try { + mpService.getMaterialService().materialDelete(material.getMediaId()); + } catch (WxErrorException e) { + throw exception(MATERIAL_DELETE_FAIL, e.getError().getErrorMsg()); + } + } + + // 第二步,从数据库中删除 + mpMaterialMapper.deleteById(id); + } + + /** + * 下载微信媒体文件的内容,并上传到文件服务 + * + * 为什么要下载?媒体文件在微信后台保存时间为 3 天,即 3 天后 media_id 失效。 + * + * @param accountId 公众号账号的编号 + * @param mediaId 媒体文件编号 + * @return 上传后的 URL + */ + public String downloadMedia(Long accountId, String mediaId) { + WxMpService mpService = mpServiceFactory.getMpService(accountId); + for (int i = 0; i < 3; i++) { + try { + // 第一步,从公众号下载媒体文件 + File file = mpService.getMaterialService().mediaDownload(mediaId); + // 第二步,上传到文件服务 + return uploadFile(mediaId, file); + } catch (WxErrorException e) { + log.error("[mediaDownload][media({}) 第 ({}) 次下载失败]", mediaId, i); + } + } + return null; + } + + private String uploadFile(String mediaId, File file) { + String path = mediaId + "." + FileTypeUtil.getType(file); + return fileApi.createFile(path, FileUtil.readBytes(file)); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/menu/MpMenuService.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/menu/MpMenuService.java new file mode 100644 index 0000000..548da85 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/menu/MpMenuService.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.mp.service.menu; + +import com.yunxi.scm.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.menu.MpMenuDO; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; + +import javax.validation.Valid; +import java.util.List; + +/** + * 公众号菜单 Service 接口 + * + * @author 芋道源码 + */ +public interface MpMenuService { + + /** + * 保存公众号菜单 + * + * @param createReqVO 创建信息 + */ + void saveMenu(@Valid MpMenuSaveReqVO createReqVO); + + /** + * 删除公众号菜单 + * + * @param accountId 公众号账号的编号 + */ + void deleteMenuByAccountId(Long accountId); + + /** + * 粉丝点击菜单按钮时,回复对应的消息 + * + * @param appId 公众号 AppId + * @param key 菜单按钮的标识 + * @param openid 粉丝的 openid + * @return 消息 + */ + WxMpXmlOutMessage reply(String appId, String key, String openid); + + /** + * 获得公众号菜单列表 + * + * @param accountId 公众号账号的编号 + * @return 公众号菜单列表 + */ + List getMenuListByAccountId(Long accountId); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/menu/MpMenuServiceImpl.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/menu/MpMenuServiceImpl.java new file mode 100644 index 0000000..89fe3ee --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/menu/MpMenuServiceImpl.java @@ -0,0 +1,171 @@ +package com.yunxi.scm.module.mp.service.menu; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.module.mp.controller.admin.menu.vo.MpMenuSaveReqVO; +import com.yunxi.scm.module.mp.convert.menu.MpMenuConvert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.menu.MpMenuDO; +import com.yunxi.scm.module.mp.dal.mysql.menu.MpMenuMapper; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.framework.mp.core.util.MpUtils; +import com.yunxi.scm.module.mp.service.account.MpAccountService; +import com.yunxi.scm.module.mp.service.message.MpMessageService; +import com.yunxi.scm.module.mp.service.message.bo.MpMessageSendOutReqBO; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.menu.WxMenu; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.MENU_DELETE_FAIL; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.MENU_SAVE_FAIL; + +/** + * 公众号菜单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class MpMenuServiceImpl implements MpMenuService { + + @Resource + private MpMessageService mpMessageService; + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private MpAccountService mpAccountService; + + @Resource + @Lazy // 延迟加载,避免循环引用报错 + private MpServiceFactory mpServiceFactory; + + @Resource + private Validator validator; + + @Resource + private MpMenuMapper mpMenuMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public void saveMenu(MpMenuSaveReqVO createReqVO) { + MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); + WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); + + // 参数校验 + createReqVO.getMenus().forEach(this::validateMenu); + + // 第一步,同步公众号 + WxMenu wxMenu = new WxMenu(); + wxMenu.setButtons(MpMenuConvert.INSTANCE.convert(createReqVO.getMenus())); + try { + mpService.getMenuService().menuCreate(wxMenu); + } catch (WxErrorException e) { + throw exception(MENU_SAVE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,存储到数据库 + mpMenuMapper.deleteByAccountId(createReqVO.getAccountId()); + createReqVO.getMenus().forEach(menu -> { + // 先保存顶级菜单 + MpMenuDO menuDO = createMenu(menu, null, account); + // 再保存子菜单 + if (CollUtil.isEmpty(menu.getChildren())) { + return; + } + menu.getChildren().forEach(childMenu -> createMenu(childMenu, menuDO, account)); + }); + } + + /** + * 校验菜单的格式是否正确 + * + * @param menu 菜单 + */ + private void validateMenu(MpMenuSaveReqVO.Menu menu) { + MpUtils.validateButton(validator, menu.getType(), menu.getReplyMessageType(), menu); + // 子菜单 + if (CollUtil.isEmpty(menu.getChildren())) { + return; + } + menu.getChildren().forEach(this::validateMenu); + } + + /** + * 创建菜单,并存储到数据库 + * + * @param wxMenu 菜单信息 + * @param parentMenu 父菜单 + * @param account 公众号账号 + * @return 创建后的菜单 + */ + private MpMenuDO createMenu(MpMenuSaveReqVO.Menu wxMenu, MpMenuDO parentMenu, MpAccountDO account) { + // 创建菜单 + MpMenuDO menu = CollUtil.isNotEmpty(wxMenu.getChildren()) + ? new MpMenuDO().setName(wxMenu.getName()) + : MpMenuConvert.INSTANCE.convert02(wxMenu); + // 设置菜单的公众号账号信息 + if (account != null) { + menu.setAccountId(account.getId()).setAppId(account.getAppId()); + } + // 设置父编号 + if (parentMenu != null) { + menu.setParentId(parentMenu.getId()); + } else { + menu.setParentId(MpMenuDO.ID_ROOT); + } + + // 插入到数据库 + mpMenuMapper.insert(menu); + return menu; + } + + @Override + public void deleteMenuByAccountId(Long accountId) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + // 第一步,同步公众号 + try { + mpService.getMenuService().menuDelete(); + } catch (WxErrorException e) { + throw exception(MENU_DELETE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,存储到数据库 + mpMenuMapper.deleteByAccountId(accountId); + } + + @Override + public WxMpXmlOutMessage reply(String appId, String key, String openid) { + // 第一步,获得菜单 + MpMenuDO menu = mpMenuMapper.selectByAppIdAndMenuKey(appId, key); + if (menu == null) { + log.error("[reply][appId({}) key({}) 找不到对应的菜单]", appId, key); + return null; + } + // 按钮必须要有消息类型,不然后续无法回复消息 + if (StrUtil.isEmpty(menu.getReplyMessageType())) { + log.error("[reply][menu({}) 不存在对应的消息类型]", menu); + return null; + } + + // 第二步,回复消息 + MpMessageSendOutReqBO sendReqBO = MpMenuConvert.INSTANCE.convert(openid, menu); + return mpMessageService.sendOutMessage(sendReqBO); + } + + @Override + public List getMenuListByAccountId(Long accountId) { + return mpMenuMapper.selectListByAccountId(accountId); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpAutoReplyService.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpAutoReplyService.java new file mode 100644 index 0000000..52407f8 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpAutoReplyService.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.mp.service.message; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpAutoReplyDO; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; + +/** + * 公众号的自动回复 Service 接口 + * + * @author 芋道源码 + */ +public interface MpAutoReplyService { + + /** + * 获得公众号自动回复分页 + * + * @param pageVO 分页请求 + * @return 自动回复分页结果 + */ + PageResult getAutoReplyPage(MpMessagePageReqVO pageVO); + + /** + * 获得公众号自动回复 + * + * @param id 编号 + * @return 自动回复 + */ + MpAutoReplyDO getAutoReply(Long id); + + + /** + * 创建公众号自动回复 + * + * @param createReqVO 创建请求 + * @return 自动回复的编号 + */ + Long createAutoReply(MpAutoReplyCreateReqVO createReqVO); + + /** + * 更新公众号自动回复 + * + * @param updateReqVO 更新请求 + */ + void updateAutoReply(MpAutoReplyUpdateReqVO updateReqVO); + + /** + * 删除公众号自动回复 + * + * @param id 自动回复的编号 + */ + void deleteAutoReply(Long id); + + /** + * 当收到消息时,自动回复 + * + * @param appId 微信公众号 appId + * @param wxMessage 消息 + * @return 回复的消息 + */ + WxMpXmlOutMessage replyForMessage(String appId, WxMpXmlMessage wxMessage); + + /** + * 当粉丝关注时,自动回复 + * + * @param appId 微信公众号 appId + * @param wxMessage 消息 + * @return 回复的消息 + */ + WxMpXmlOutMessage replyForSubscribe(String appId, WxMpXmlMessage wxMessage); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpAutoReplyServiceImpl.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpAutoReplyServiceImpl.java new file mode 100644 index 0000000..7f73f9b --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpAutoReplyServiceImpl.java @@ -0,0 +1,202 @@ +package com.yunxi.scm.module.mp.service.message; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjUtil; +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.autoreply.MpAutoReplyUpdateReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import com.yunxi.scm.module.mp.convert.message.MpAutoReplyConvert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpAutoReplyDO; +import com.yunxi.scm.module.mp.dal.mysql.message.MpAutoReplyMapper; +import com.yunxi.scm.module.mp.enums.message.MpAutoReplyTypeEnum; +import com.yunxi.scm.module.mp.framework.mp.core.util.MpUtils; +import com.yunxi.scm.module.mp.service.account.MpAccountService; +import com.yunxi.scm.module.mp.service.message.bo.MpMessageSendOutReqBO; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.*; + +/** + * 公众号的自动回复 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class MpAutoReplyServiceImpl implements MpAutoReplyService { + + @Resource + private MpMessageService mpMessageService; + @Resource + @Lazy // 延迟加载,避免循环依赖 + private MpAccountService mpAccountService; + + @Resource + private Validator validator; + + @Resource + private MpAutoReplyMapper mpAutoReplyMapper; + + @Override + public PageResult getAutoReplyPage(MpMessagePageReqVO pageVO) { + return mpAutoReplyMapper.selectPage(pageVO); + } + + @Override + public MpAutoReplyDO getAutoReply(Long id) { + return mpAutoReplyMapper.selectById(id); + } + + @Override + public Long createAutoReply(MpAutoReplyCreateReqVO createReqVO) { + // 第一步,校验数据 + if (createReqVO.getResponseMessageType() != null) { + MpUtils.validateMessage(validator, createReqVO.getResponseMessageType(), createReqVO); + } + validateAutoReplyConflict(null, createReqVO.getAccountId(), createReqVO.getType(), + createReqVO.getRequestKeyword(), createReqVO.getRequestMessageType()); + + // 第二步,插入数据 + MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); + MpAutoReplyDO autoReply = MpAutoReplyConvert.INSTANCE.convert(createReqVO) + .setAppId(account.getAppId()); + mpAutoReplyMapper.insert(autoReply); + return autoReply.getId(); + } + + @Override + public void updateAutoReply(MpAutoReplyUpdateReqVO updateReqVO) { + // 第一步,校验数据 + if (updateReqVO.getResponseMessageType() != null) { + MpUtils.validateMessage(validator, updateReqVO.getResponseMessageType(), updateReqVO); + } + MpAutoReplyDO autoReply = validateAutoReplyExists(updateReqVO.getId()); + validateAutoReplyConflict(updateReqVO.getId(), autoReply.getAccountId(), updateReqVO.getType(), + updateReqVO.getRequestKeyword(), updateReqVO.getRequestMessageType()); + + // 第二步,更新数据 + MpAutoReplyDO updateObj = MpAutoReplyConvert.INSTANCE.convert(updateReqVO) + .setAccountId(null).setAppId(null); // 避免前端传递,更新着两个字段 + mpAutoReplyMapper.updateById(updateObj); + } + + /** + * 校验自动回复是否冲突 + * + * 不同的 type,会有不同的逻辑: + * 1. type = SUBSCRIBE 时,不允许有其他的自动回复 + * 2. type = MESSAGE 时,校验 requestMessageType 已经存在自动回复 + * 3. type = KEYWORD 时,校验 keyword 已经存在自动回复 + * + * @param id 自动回复编号 + * @param accountId 公众号账号的编号 + * @param type 类型 + * @param requestKeyword 请求关键词 + * @param requestMessageType 请求消息类型 + */ + private void validateAutoReplyConflict(Long id, Long accountId, Integer type, + String requestKeyword, String requestMessageType) { + // 获得已经存在的自动回复 + MpAutoReplyDO autoReply = null; + ErrorCode errorCode = null; + if (MpAutoReplyTypeEnum.SUBSCRIBE.getType().equals(type)) { + autoReply = mpAutoReplyMapper.selectByAccountIdAndSubscribe(accountId); + errorCode = AUTO_REPLY_ADD_SUBSCRIBE_FAIL_EXISTS; + } else if (MpAutoReplyTypeEnum.MESSAGE.getType().equals(type)) { + autoReply = mpAutoReplyMapper.selectByAccountIdAndMessage(accountId, requestMessageType); + errorCode = AUTO_REPLY_ADD_MESSAGE_FAIL_EXISTS; + } else if (MpAutoReplyTypeEnum.KEYWORD.getType().equals(type)) { + autoReply = mpAutoReplyMapper.selectByAccountIdAndKeyword(accountId, requestKeyword); + errorCode = AUTO_REPLY_ADD_KEYWORD_FAIL_EXISTS; + } + if (autoReply == null) { + return; + } + + // 存在冲突,抛出业务异常 + if (id == null // 情况一,新增(id == null),存在记录,说明冲突 + || ObjUtil.notEqual(id, autoReply.getId())) { // 情况二,修改(id != null),id 不匹配,说明冲突 + throw exception(errorCode); + } + } + + @Override + public void deleteAutoReply(Long id) { + // 校验粉丝存在 + validateAutoReplyExists(id); + + // 删除自动回复 + mpAutoReplyMapper.deleteById(id); + } + + private MpAutoReplyDO validateAutoReplyExists(Long id) { + MpAutoReplyDO autoReply = mpAutoReplyMapper.selectById(id); + if (autoReply == null) { + throw exception(AUTO_REPLY_NOT_EXISTS); + } + return autoReply; + } + + @Override + public WxMpXmlOutMessage replyForMessage(String appId, WxMpXmlMessage wxMessage) { + // 第一步,匹配自动回复 + List replies = null; + // 1.1 关键字 + if (wxMessage.getMsgType().equals(WxConsts.XmlMsgType.TEXT)) { + // 完全匹配 + replies = mpAutoReplyMapper.selectListByAppIdAndKeywordAll(appId, wxMessage.getContent()); + if (CollUtil.isEmpty(replies)) { + // 模糊匹配 + replies = mpAutoReplyMapper.selectListByAppIdAndKeywordLike(appId, wxMessage.getContent()); + } + } + // 1.2 消息类型 + if (CollUtil.isEmpty(replies)) { + replies = mpAutoReplyMapper.selectListByAppIdAndMessage(appId, wxMessage.getMsgType()); + } + if (CollUtil.isEmpty(replies)) { + return null; + } + MpAutoReplyDO reply = CollUtil.getFirst(replies); + + // 第二步,基于自动回复,创建消息 + MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); + return mpMessageService.sendOutMessage(sendReqBO); + } + + @Override + public WxMpXmlOutMessage replyForSubscribe(String appId, WxMpXmlMessage wxMessage) { + // 第一步,匹配自动回复 + List replies = mpAutoReplyMapper.selectListByAppIdAndSubscribe(appId); + MpAutoReplyDO reply = CollUtil.isNotEmpty(replies) ? CollUtil.getFirst(replies) + : buildDefaultSubscribeAutoReply(appId); // 如果不存在,提供一个默认末班 + + // 第二步,基于自动回复,创建消息 + MpMessageSendOutReqBO sendReqBO = MpAutoReplyConvert.INSTANCE.convert(wxMessage.getFromUser(), reply); + return mpMessageService.sendOutMessage(sendReqBO); + } + + private MpAutoReplyDO buildDefaultSubscribeAutoReply(String appId) { + MpAccountDO account = mpAccountService.getAccountFromCache(appId); + Assert.notNull(account, "公众号账号({}) 不存在", appId); + // 构建默认的【关注】自动回复 + return new MpAutoReplyDO().setAppId(appId).setAccountId(account.getId()) + .setType(MpAutoReplyTypeEnum.SUBSCRIBE.getType()) + .setResponseMessageType(WxConsts.XmlMsgType.TEXT).setResponseContent("感谢关注"); + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpMessageService.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpMessageService.java new file mode 100644 index 0000000..25ca6cd --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpMessageService.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.mp.service.message; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.yunxi.scm.module.mp.service.message.bo.MpMessageSendOutReqBO; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; + +import javax.validation.Valid; + +/** + * 公众号消息 Service 接口 + * + * @author 芋道源码 + */ +public interface MpMessageService { + + /** + * 获得公众号消息分页 + * + * @param pageReqVO 分页查询 + * @return 公众号消息分页 + */ + PageResult getMessagePage(MpMessagePageReqVO pageReqVO); + + /** + * 从公众号,接收到粉丝消息 + * + * @param appId 微信公众号 appId + * @param wxMessage 消息 + */ + void receiveMessage(String appId, WxMpXmlMessage wxMessage); + + /** + * 使用公众号,给粉丝回复消息 + * + * 例如说:自动回复、客服消息、菜单回复消息等场景 + * + * 注意,该方法只是返回 WxMpXmlOutMessage 对象,不会真的发送消息 + * + * @param sendReqBO 消息内容 + * @return 微信回复消息 XML + */ + WxMpXmlOutMessage sendOutMessage(@Valid MpMessageSendOutReqBO sendReqBO); + + /** + * 使用公众号,给粉丝发送【客服】消息 + * + * 注意,该方法会真实发送消息 + * + * @param sendReqVO 消息内容 + * @return 消息 + */ + MpMessageDO sendKefuMessage(MpMessageSendReqVO sendReqVO); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpMessageServiceImpl.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpMessageServiceImpl.java new file mode 100644 index 0000000..534788b --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/MpMessageServiceImpl.java @@ -0,0 +1,148 @@ +package com.yunxi.scm.module.mp.service.message; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessagePageReqVO; +import com.yunxi.scm.module.mp.controller.admin.message.vo.message.MpMessageSendReqVO; +import com.yunxi.scm.module.mp.convert.message.MpMessageConvert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.yunxi.scm.module.mp.dal.dataobject.user.MpUserDO; +import com.yunxi.scm.module.mp.dal.mysql.message.MpMessageMapper; +import com.yunxi.scm.module.mp.enums.message.MpMessageSendFromEnum; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.framework.mp.core.util.MpUtils; +import com.yunxi.scm.module.mp.service.account.MpAccountService; +import com.yunxi.scm.module.mp.service.material.MpMaterialService; +import com.yunxi.scm.module.mp.service.message.bo.MpMessageSendOutReqBO; +import com.yunxi.scm.module.mp.service.user.MpUserService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.kefu.WxMpKefuMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlMessage; +import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Validator; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.MESSAGE_SEND_FAIL; + +/** + * 粉丝消息 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class MpMessageServiceImpl implements MpMessageService { + + @Resource + @Lazy // 延迟加载,避免循环依赖 + private MpAccountService mpAccountService; + @Resource + private MpUserService mpUserService; + @Resource + private MpMaterialService mpMaterialService; + + @Resource + private MpMessageMapper mpMessageMapper; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Resource + private Validator validator; + + @Override + public PageResult getMessagePage(MpMessagePageReqVO pageReqVO) { + return mpMessageMapper.selectPage(pageReqVO); + } + + @Override + public void receiveMessage(String appId, WxMpXmlMessage wxMessage) { + // 获得关联信息 + MpAccountDO account = mpAccountService.getAccountFromCache(appId); + Assert.notNull(account, "公众号账号({}) 不存在", appId); + MpUserDO user = mpUserService.getUser(appId, wxMessage.getFromUser()); + Assert.notNull(user, "公众号粉丝({}/{}) 不存在", appId, wxMessage.getFromUser()); + + // 记录消息 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user) + .setSendFrom(MpMessageSendFromEnum.USER_TO_MP.getFrom()); + downloadMessageMedia(message); + mpMessageMapper.insert(message); + } + + @Override + public WxMpXmlOutMessage sendOutMessage(MpMessageSendOutReqBO sendReqBO) { + // 校验消息格式 + MpUtils.validateMessage(validator, sendReqBO.getType(), sendReqBO); + + // 获得关联信息 + MpAccountDO account = mpAccountService.getAccountFromCache(sendReqBO.getAppId()); + Assert.notNull(account, "公众号账号({}) 不存在", sendReqBO.getAppId()); + MpUserDO user = mpUserService.getUser(sendReqBO.getAppId(), sendReqBO.getOpenid()); + Assert.notNull(user, "公众号粉丝({}/{}) 不存在", sendReqBO.getAppId(), sendReqBO.getOpenid()); + + // 记录消息 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(sendReqBO, account, user). + setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); + downloadMessageMedia(message); + mpMessageMapper.insert(message); + + // 转换返回 WxMpXmlOutMessage 对象 + return MpMessageConvert.INSTANCE.convert02(message, account); + } + + @Override + public MpMessageDO sendKefuMessage(MpMessageSendReqVO sendReqVO) { + // 校验消息格式 + MpUtils.validateMessage(validator, sendReqVO.getType(), sendReqVO); + + // 获得关联信息 + MpUserDO user = mpUserService.getRequiredUser(sendReqVO.getUserId()); + MpAccountDO account = mpAccountService.getRequiredAccount(user.getAccountId()); + + // 发送客服消息 + WxMpKefuMessage wxMessage = MpMessageConvert.INSTANCE.convert(sendReqVO, user); + WxMpService mpService = mpServiceFactory.getRequiredMpService(user.getAppId()); + try { + mpService.getKefuService().sendKefuMessageWithResponse(wxMessage); + } catch (WxErrorException e) { + throw exception(MESSAGE_SEND_FAIL, e.getError().getErrorMsg()); + } + + // 记录消息 + MpMessageDO message = MpMessageConvert.INSTANCE.convert(wxMessage, account, user) + .setSendFrom(MpMessageSendFromEnum.MP_TO_USER.getFrom()); + downloadMessageMedia(message); + mpMessageMapper.insert(message); + return message; + } + + /** + * 下载消息使用到的媒体文件,并上传到文件服务 + * + * @param message 消息 + */ + private void downloadMessageMedia(MpMessageDO message) { + if (StrUtil.isNotEmpty(message.getMediaId())) { + message.setMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(), + message.getMediaId(), MpUtils.getMediaFileType(message.getType()))); + } + if (StrUtil.isNotEmpty(message.getThumbMediaId())) { + message.setThumbMediaUrl(mpMaterialService.downloadMaterialUrl(message.getAccountId(), + message.getThumbMediaId(), WxConsts.MediaFileType.THUMB)); + } + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/bo/MpMessageSendOutReqBO.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/bo/MpMessageSendOutReqBO.java new file mode 100644 index 0000000..2edfb63 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/message/bo/MpMessageSendOutReqBO.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.module.mp.service.message.bo; + +import com.yunxi.scm.module.mp.dal.dataobject.message.MpMessageDO; +import com.yunxi.scm.module.mp.framework.mp.core.util.MpUtils.*; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; +import org.hibernate.validator.constraints.URL; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 公众号消息发送 Request BO + * + * 为什么要有该 BO 呢?在自动回复、客服消息、菜单回复消息等场景,都涉及到 MP 给粉丝发送消息,所以使用该 BO 统一承接 + * + * @author 芋道源码 + */ +@Data +public class MpMessageSendOutReqBO { + + /** + * 公众号 appId + */ + @NotEmpty(message = "公众号 appId 不能为空") + private String appId; + /** + * 公众号粉丝 openid + */ + @NotEmpty(message = "公众号粉丝 openid 不能为空") + private String openid; + + // ========== 消息内容 ========== + /** + * 消息类型 + * + * 枚举 {@link WxConsts.XmlMsgType} 中的 TEXT、IMAGE、VOICE、VIDEO、NEWS、MUSIC + */ + @NotEmpty(message = "消息类型不能为空") + public String type; + + /** + * 消息内容 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 TEXT + */ + @NotEmpty(message = "消息内容不能为空", groups = TextMessageGroup.class) + private String content; + + /** + * 媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 IMAGE、VOICE、VIDEO + */ + @NotEmpty(message = "消息 mediaId 不能为空", groups = {ImageMessageGroup.class, VoiceMessageGroup.class, VideoMessageGroup.class}) + private String mediaId; + + /** + * 缩略图的媒体 id + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO、MUSIC + */ + @NotEmpty(message = "消息 thumbMediaId 不能为空", groups = {MusicMessageGroup.class}) + private String thumbMediaId; + + /** + * 标题 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + @NotEmpty(message = "消息标题不能为空", groups = VideoMessageGroup.class) + private String title; + /** + * 描述 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 VIDEO + */ + @NotEmpty(message = "消息描述不能为空", groups = VideoMessageGroup.class) + private String description; + + /** + * 图文消息 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 NEWS + */ + @Valid + @NotNull(message = "图文消息不能为空", groups = NewsMessageGroup.class) + private List articles; + + /** + * 音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + @NotEmpty(message = "音乐链接不能为空", groups = MusicMessageGroup.class) + @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class) + private String musicUrl; + + /** + * 高质量音乐链接 + * + * 消息类型为 {@link WxConsts.XmlMsgType} 的 MUSIC + */ + @NotEmpty(message = "高质量音乐链接不能为空", groups = MusicMessageGroup.class) + @URL(message = "高质量音乐链接格式不正确", groups = MusicMessageGroup.class) + private String hqMusicUrl; + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/statistics/MpStatisticsService.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/statistics/MpStatisticsService.java new file mode 100644 index 0000000..a3f358a --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/statistics/MpStatisticsService.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.mp.service.statistics; + +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 公众号统计 Service 接口 + * + * @author 芋道源码 + */ +public interface MpStatisticsService { + + /** + * 获取粉丝增减数据 + * + * @param accountId 公众号账号编号 + * @param date 时间区间 + * @return 粉丝增减数据 + */ + List getUserSummary(Long accountId, LocalDateTime[] date); + + /** + * 获取粉丝累计数据 + * + * @param accountId 公众号账号编号 + * @param date 时间区间 + * @return 粉丝累计数据 + */ + List getUserCumulate(Long accountId, LocalDateTime[] date); + + /** + * 获取消息发送概况数据 + * + * @param accountId 公众号账号编号 + * @param date 时间区间 + * @return 消息发送概况数据 + */ + List getUpstreamMessage(Long accountId, LocalDateTime[] date); + + /** + * 获取接口分析数据 + * + * @param accountId 公众号账号编号 + * @param date 时间区间 + * @return 接口分析数据 + */ + List getInterfaceSummary(Long accountId, LocalDateTime[] date); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/statistics/MpStatisticsServiceImpl.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/statistics/MpStatisticsServiceImpl.java new file mode 100644 index 0000000..c80ab0c --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/statistics/MpStatisticsServiceImpl.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.module.mp.service.statistics; + +import cn.hutool.core.date.DateUtil; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeInterfaceResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeMsgResult; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserCumulate; +import me.chanjar.weixin.mp.bean.datacube.WxDataCubeUserSummary; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.*; + +/** + * 公众号统计 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class MpStatisticsServiceImpl implements MpStatisticsService { + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Override + public List getUserSummary(Long accountId, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + return mpService.getDataCubeService().getUserSummary( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_USER_SUMMARY_FAIL, e.getError().getErrorMsg()); + } + } + + @Override + public List getUserCumulate(Long accountId, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + return mpService.getDataCubeService().getUserCumulate( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_USER_CUMULATE_FAIL, e.getError().getErrorMsg()); + } + } + + @Override + public List getUpstreamMessage(Long accountId, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + return mpService.getDataCubeService().getUpstreamMsg( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_UPSTREAM_MESSAGE_FAIL, e.getError().getErrorMsg()); + } + } + + @Override + public List getInterfaceSummary(Long accountId, LocalDateTime[] date) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + try { + return mpService.getDataCubeService().getInterfaceSummary( + DateUtil.date(date[0]), DateUtil.date(date[1])); + } catch (WxErrorException e) { + throw exception(STATISTICS_GET_INTERFACE_SUMMARY_FAIL, e.getError().getErrorMsg()); + } + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/tag/MpTagService.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/tag/MpTagService.java new file mode 100644 index 0000000..d49a148 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/tag/MpTagService.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.mp.service.tag; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.tag.MpTagDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 公众号标签 Service 接口 + * + * @author fengdan + */ +public interface MpTagService { + + /** + * 创建公众号标签 + * + * @param createReqVO 创建标签信息 + * @return 标签编号 + */ + Long createTag(@Valid MpTagCreateReqVO createReqVO); + + /** + * 更新公众号标签 + * + * @param updateReqVO 更新标签信息 + */ + void updateTag(@Valid MpTagUpdateReqVO updateReqVO); + + /** + * 删除公众号标签 + * + * @param id 编号 + */ + void deleteTag(Long id); + + /** + * 获得公众号标签分页 + * + * @param pageReqVO 分页查询 + * @return 公众号标签分页 + */ + PageResult getTagPage(MpTagPageReqVO pageReqVO); + + /** + * 获得公众号标签详情 + * @param id id查询 + * @return 公众号标签详情 + */ + MpTagDO get(Long id); + + List getTagList(); + + /** + * 同步公众号标签 + * + * @param accountId 公众号账号的编号 + */ + void syncTag(Long accountId); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/tag/MpTagServiceImpl.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/tag/MpTagServiceImpl.java new file mode 100644 index 0000000..bc6b638 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/tag/MpTagServiceImpl.java @@ -0,0 +1,164 @@ +package com.yunxi.scm.module.mp.service.tag; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagCreateReqVO; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.tag.vo.MpTagUpdateReqVO; +import com.yunxi.scm.module.mp.convert.tag.MpTagConvert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.tag.MpTagDO; +import com.yunxi.scm.module.mp.dal.mysql.tag.MpTagMapper; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.service.account.MpAccountService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.tag.WxUserTag; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.*; + +/** + * 公众号标签 Service 实现类 + * + * @author fengdan + */ +@Slf4j +@Service +@Validated +public class MpTagServiceImpl implements MpTagService { + + @Resource + private MpTagMapper mpTagMapper; + + @Resource + private MpAccountService mpAccountService; + + @Resource + @Lazy // 延迟加载,为了解决延迟加载 + private MpServiceFactory mpServiceFactory; + + @Override + public Long createTag(MpTagCreateReqVO createReqVO) { + // 获得公众号账号 + MpAccountDO account = mpAccountService.getRequiredAccount(createReqVO.getAccountId()); + + // 第一步,新增标签到公众号平台。标签名的唯一,交给公众号平台 + WxMpService mpService = mpServiceFactory.getRequiredMpService(createReqVO.getAccountId()); + WxUserTag wxTag; + try { + wxTag = mpService.getUserTagService().tagCreate(createReqVO.getName()); + } catch (WxErrorException e) { + throw exception(TAG_CREATE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,新增标签到数据库 + MpTagDO tag = MpTagConvert.INSTANCE.convert(wxTag, account); + mpTagMapper.insert(tag); + return tag.getId(); + } + + @Override + public void updateTag(MpTagUpdateReqVO updateReqVO) { + // 校验标签存在 + MpTagDO tag = validateTagExists(updateReqVO.getId()); + + // 第一步,更新标签到公众号平台。标签名的唯一,交给公众号平台 + WxMpService mpService = mpServiceFactory.getRequiredMpService(tag.getAccountId()); + try { + mpService.getUserTagService().tagUpdate(tag.getTagId(), updateReqVO.getName()); + } catch (WxErrorException e) { + throw exception(TAG_UPDATE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,更新标签到数据库 + mpTagMapper.updateById(new MpTagDO().setId(tag.getId()).setName(updateReqVO.getName())); + } + + @Override + public void deleteTag(Long id) { + // 校验标签存在 + MpTagDO tag = validateTagExists(id); + + // 第一步,删除标签到公众号平台。 + WxMpService mpService = mpServiceFactory.getRequiredMpService(tag.getAccountId()); + try { + mpService.getUserTagService().tagDelete(tag.getTagId()); + } catch (WxErrorException e) { + throw exception(TAG_DELETE_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,删除标签到数据库 + mpTagMapper.deleteById(tag.getId()); + } + + private MpTagDO validateTagExists(Long id) { + MpTagDO tag = mpTagMapper.selectById(id); + if (tag == null) { + throw exception(TAG_NOT_EXISTS); + } + return tag; + } + + @Override + public PageResult getTagPage(MpTagPageReqVO pageReqVO) { + return mpTagMapper.selectPage(pageReqVO); + } + + @Override + public MpTagDO get(Long id) { + return mpTagMapper.selectById(id); + } + + @Override + public List getTagList() { + return mpTagMapper.selectList(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void syncTag(Long accountId) { + MpAccountDO account = mpAccountService.getRequiredAccount(accountId); + + // 第一步,从公众号平台获取最新的标签列表 + WxMpService mpService = mpServiceFactory.getRequiredMpService(accountId); + List wxTags; + try { + wxTags = mpService.getUserTagService().tagGet(); + } catch (WxErrorException e) { + throw exception(TAG_GET_FAIL, e.getError().getErrorMsg()); + } + + // 第二步,合并更新回自己的数据库;由于标签只有 100 个,所以直接 for 循环操作 + Map tagMap = convertMap(mpTagMapper.selectListByAccountId(accountId), + MpTagDO::getTagId); + wxTags.forEach(wxTag -> { + MpTagDO tag = tagMap.remove(wxTag.getId()); + // 情况一,不存在,新增 + if (tag == null) { + tag = MpTagConvert.INSTANCE.convert(wxTag, account); + mpTagMapper.insert(tag); + return; + } + // 情况二,存在,则更新 + mpTagMapper.updateById(new MpTagDO().setId(tag.getId()) + .setName(wxTag.getName()).setCount(wxTag.getCount())); + }); + // 情况三,部分标签已经不存在了,删除 + if (CollUtil.isNotEmpty(tagMap)) { + mpTagMapper.deleteBatchIds(convertList(tagMap.values(), MpTagDO::getId)); + } + } + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/user/MpUserService.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/user/MpUserService.java new file mode 100644 index 0000000..ede8a21 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/user/MpUserService.java @@ -0,0 +1,102 @@ +package com.yunxi.scm.module.mp.service.user; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; +import com.yunxi.scm.module.mp.dal.dataobject.user.MpUserDO; +import me.chanjar.weixin.mp.bean.result.WxMpUser; + +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS; + +/** + * 公众号粉丝 Service 接口 + * + * @author 芋道源码 + */ +public interface MpUserService { + + /** + * 获得公众号粉丝 + * + * @param id 编号 + * @return 公众号粉丝 + */ + MpUserDO getUser(Long id); + + /** + * 使用 appId + openId,获得公众号粉丝 + * + * @param appId 公众号 appId + * @param openId 公众号 openId + * @return 公众号粉丝 + */ + MpUserDO getUser(String appId, String openId); + + /** + * 获得公众号粉丝 + * + * @param id 编号 + * @return 公众号粉丝 + */ + default MpUserDO getRequiredUser(Long id) { + MpUserDO user = getUser(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + return user; + } + + /** + * 获得公众号粉丝列表 + * + * @param ids 编号 + * @return 公众号粉丝列表 + */ + List getUserList(Collection ids); + + /** + * 获得公众号粉丝分页 + * + * @param pageReqVO 分页查询 + * @return 公众号粉丝分页 + */ + PageResult getUserPage(MpUserPageReqVO pageReqVO); + + /** + * 保存公众号粉丝 + * + * 新增或更新,根据是否存在数据库中 + * + * @param appId 公众号 appId + * @param wxMpUser 公众号粉丝的信息 + * @return 公众号粉丝 + */ + MpUserDO saveUser(String appId, WxMpUser wxMpUser); + + /** + * 同步一个公众号粉丝 + * + * @param accountId 公众号账号的编号 + */ + void syncUser(Long accountId); + + /** + * 更新公众号粉丝,取消关注 + * + * @param appId 公众号 appId + * @param openId 公众号粉丝的 openid + */ + void updateUserUnsubscribe(String appId, String openId); + + /** + * 更新公众号粉丝 + * + * @param updateReqVO 更新信息 + */ + void updateUser(MpUserUpdateReqVO updateReqVO); + +} diff --git a/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/user/MpUserServiceImpl.java b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/user/MpUserServiceImpl.java new file mode 100644 index 0000000..799d9f3 --- /dev/null +++ b/yunxi-module-mp/yunxi-module-mp-biz/src/main/java/com/yunxi/scm/module/mp/service/user/MpUserServiceImpl.java @@ -0,0 +1,215 @@ +package com.yunxi.scm.module.mp.service.user; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserPageReqVO; +import com.yunxi.scm.module.mp.controller.admin.user.vo.MpUserUpdateReqVO; +import com.yunxi.scm.module.mp.convert.user.MpUserConvert; +import com.yunxi.scm.module.mp.dal.dataobject.account.MpAccountDO; +import com.yunxi.scm.module.mp.dal.dataobject.user.MpUserDO; +import com.yunxi.scm.module.mp.dal.mysql.user.MpUserMapper; +import com.yunxi.scm.module.mp.framework.mp.core.MpServiceFactory; +import com.yunxi.scm.module.mp.service.account.MpAccountService; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.bean.result.WxMpUser; +import me.chanjar.weixin.mp.bean.result.WxMpUserList; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.USER_NOT_EXISTS; +import static com.yunxi.scm.module.mp.enums.ErrorCodeConstants.USER_UPDATE_TAG_FAIL; + +/** + * 微信公众号粉丝 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class MpUserServiceImpl implements MpUserService { + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpAccountService mpAccountService; + + @Resource + @Lazy // 延迟加载,解决循环依赖的问题 + private MpServiceFactory mpServiceFactory; + + @Resource + private MpUserMapper mpUserMapper; + + @Override + public MpUserDO getUser(Long id) { + return mpUserMapper.selectById(id); + } + + @Override + public MpUserDO getUser(String appId, String openId) { + return mpUserMapper.selectByAppIdAndOpenid(appId, openId); + } + + @Override + public List getUserList(Collection ids) { + return mpUserMapper.selectBatchIds(ids); + } + + @Override + public PageResult getUserPage(MpUserPageReqVO pageReqVO) { + return mpUserMapper.selectPage(pageReqVO); + } + + @Override + public MpUserDO saveUser(String appId, WxMpUser wxMpUser) { + // 构建保存的 MpUserDO 对象 + MpAccountDO account = mpAccountService.getAccountFromCache(appId); + MpUserDO user = MpUserConvert.INSTANCE.convert(account, wxMpUser); + + // 根据情况,插入或更新 + MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, wxMpUser.getOpenId()); + if (dbUser == null) { + mpUserMapper.insert(user); + } else { + user.setId(dbUser.getId()); + mpUserMapper.updateById(user); + } + return user; + } + + @Override + @Async + public void syncUser(Long accountId) { + MpAccountDO account = mpAccountService.getRequiredAccount(accountId); + // for 循环,避免递归出意外问题,导致死循环 + String nextOpenid = null; + for (int i = 0; i < Short.MAX_VALUE; i++) { + log.info("[syncUser][第({}) 次加载公众号粉丝列表,nextOpenid({})]", i, nextOpenid); + try { + nextOpenid = syncUser0(account, nextOpenid); + } catch (WxErrorException e) { + log.error("[syncUser][第({}) 次同步粉丝异常]", i, e); + break; + } + // 如果 nextOpenid 为空,表示已经同步完毕 + if (StrUtil.isEmpty(nextOpenid)) { + break; + } + } + } + + private String syncUser0(MpAccountDO account, String nextOpenid) throws WxErrorException { + // 第一步,从公众号流式加载粉丝 + WxMpService mpService = mpServiceFactory.getRequiredMpService(account.getId()); + WxMpUserList wxUserList = mpService.getUserService().userList(nextOpenid); + if (CollUtil.isEmpty(wxUserList.getOpenids())) { + return null; + } + + // 第二步,分批加载粉丝信息 + List> openidsList = CollUtil.split(wxUserList.getOpenids(), 100); + for (List openids : openidsList) { + log.info("[syncUser][批量加载粉丝信息,openids({})]", openids); + List wxUsers = mpService.getUserService().userInfoList(openids); + batchSaveUser(account, wxUsers); + } + + // 返回下一次的 nextOpenId + return wxUserList.getNextOpenid(); + } + + private void batchSaveUser(MpAccountDO account, List wxUsers) { + if (CollUtil.isEmpty(wxUsers)) { + return; + } + // 1. 获得数据库已保存的粉丝列表 + List dbUsers = mpUserMapper.selectListByAppIdAndOpenid(account.getAppId(), + CollectionUtils.convertList(wxUsers, WxMpUser::getOpenId)); + Map openId2Users = CollectionUtils.convertMap(dbUsers, MpUserDO::getOpenid); + + // 2.1 根据情况,插入或更新 + List users = MpUserConvert.INSTANCE.convertList(account, wxUsers); + List newUsers = new ArrayList<>(); + for (MpUserDO user : users) { + MpUserDO dbUser = openId2Users.get(user.getOpenid()); + if (dbUser == null) { // 新增:稍后批量插入 + newUsers.add(user); + } else { // 更新:直接执行更新 + user.setId(dbUser.getId()); + mpUserMapper.updateById(user); + } + } + // 2.2 批量插入 + if (CollUtil.isNotEmpty(newUsers)) { + mpUserMapper.insertBatch(newUsers); + } + } + + @Override + public void updateUserUnsubscribe(String appId, String openid) { + MpUserDO dbUser = mpUserMapper.selectByAppIdAndOpenid(appId, openid); + if (dbUser == null) { + log.error("[updateUserUnsubscribe][微信公众号粉丝 appId({}) openid({}) 不存在]", appId, openid); + return; + } + mpUserMapper.updateById(new MpUserDO().setId(dbUser.getId()).setSubscribeStatus(CommonStatusEnum.DISABLE.getStatus()) + .setUnsubscribeTime(LocalDateTime.now())); + } + + @Override + public void updateUser(MpUserUpdateReqVO updateReqVO) { + // 校验存在 + MpUserDO user = validateUserExists(updateReqVO.getId()); + + // 第一步,更新标签到公众号 + updateUserTag(user.getAppId(), user.getOpenid(), updateReqVO.getTagIds()); + + // 第二步,更新基本信息到数据库 + MpUserDO updateObj = MpUserConvert.INSTANCE.convert(updateReqVO).setId(user.getId()); + mpUserMapper.updateById(updateObj); + } + + private MpUserDO validateUserExists(Long id) { + MpUserDO user = mpUserMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + return user; + } + + private void updateUserTag(String appId, String openid, List tagIds) { + WxMpService mpService = mpServiceFactory.getRequiredMpService(appId); + try { + // 第一步,先取消原来的标签 + List oldTagIds = mpService.getUserTagService().userTagList(openid); + for (Long tagId : oldTagIds) { + mpService.getUserTagService().batchUntagging(tagId, new String[]{openid}); + } + // 第二步,再设置新的标签 + if (CollUtil.isEmpty(tagIds)) { + return; + } + for (Long tagId: tagIds) { + mpService.getUserTagService().batchTagging(tagId, new String[]{openid}); + } + } catch (WxErrorException e) { + throw exception(USER_UPDATE_TAG_FAIL, e.getError().getErrorMsg()); + } + } + +} diff --git a/yunxi-module-pay/pom.xml b/yunxi-module-pay/pom.xml new file mode 100644 index 0000000..286adb2 --- /dev/null +++ b/yunxi-module-pay/pom.xml @@ -0,0 +1,25 @@ + + + + com.yunxi.scm + yunxi + ${revision} + + 4.0.0 + yunxi-module-pay + pom + + yunxi-module-pay-api + yunxi-module-pay-biz + + + ${project.artifactId} + + pay 模块,我们放支付业务,提供业务的支付能力。 + 例如说:商户、应用、支付、退款等等 + + + + diff --git a/yunxi-module-pay/yunxi-module-pay-api/pom.xml b/yunxi-module-pay/yunxi-module-pay-api/pom.xml new file mode 100644 index 0000000..5b7ed17 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/pom.xml @@ -0,0 +1,33 @@ + + + + yunxi-module-pay + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-module-pay-api + jar + + ${project.artifactId} + + pay 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java new file mode 100644 index 0000000..5da1e57 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/dto/PayOrderNotifyReqDTO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.pay.api.notify.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 支付单的通知 Request DTO + * + * @author 芋道源码 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderNotifyReqDTO { + + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单号不能为空") + private String merchantOrderId; + + /** + * 支付订单编号 + */ + @NotNull(message = "支付订单编号不能为空") + private Long payOrderId; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java new file mode 100644 index 0000000..555336c --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/dto/PayRefundNotifyReqDTO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.pay.api.notify.dto; + +import com.yunxi.scm.module.pay.enums.refund.PayRefundStatusEnum; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 退款单的通知 Request DTO + * + * @author 芋道源码 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayRefundNotifyReqDTO { + + /** + * 商户退款单编号 + */ + @NotEmpty(message = "商户退款单编号不能为空") + private String merchantOrderId; + + /** + * 支付退款编号 + */ + @NotNull(message = "支付退款编号不能为空") + private Long payRefundId; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/package-info.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/package-info.java new file mode 100644 index 0000000..9fff824 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/notify/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位符,无特殊作用 + */ +package com.yunxi.scm.module.pay.api.notify; diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/PayOrderApi.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/PayOrderApi.java new file mode 100644 index 0000000..1a8a286 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/PayOrderApi.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.pay.api.order; + +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderRespDTO; + +import javax.validation.Valid; + +/** + * 支付单 API 接口 + * + * @author LeeYan9 + * @since 2022-08-26 + */ +public interface PayOrderApi { + + /** + * 创建支付单 + * + * @param reqDTO 创建请求 + * @return 支付单编号 + */ + Long createOrder(@Valid PayOrderCreateReqDTO reqDTO); + + /** + * 获得支付单 + * + * @param id 支付单编号 + * @return 支付单 + */ + PayOrderRespDTO getOrder(Long id); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/dto/PayOrderCreateReqDTO.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/dto/PayOrderCreateReqDTO.java new file mode 100644 index 0000000..18d3c47 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/dto/PayOrderCreateReqDTO.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.pay.api.order.dto; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 支付单创建 Request DTO + */ +@Data +public class PayOrderCreateReqDTO implements Serializable { + + /** + * 应用编号 + */ + @NotNull(message = "应用编号不能为空") + private Long appId; + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + /** + * 商品标题 + */ + @NotEmpty(message = "商品标题不能为空") + @Length(max = 32, message = "商品标题不能超过 32") + private String subject; + /** + * 商品描述 + */ + @Length(max = 128, message = "商品描述信息长度不能超过128") + private String body; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + @NotNull(message = "支付金额不能为空") + @DecimalMin(value = "0", inclusive = false, message = "支付金额必须大于零") + private Integer price; + + /** + * 支付过期时间 + */ + @NotNull(message = "支付过期时间不能为空") + private LocalDateTime expireTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/dto/PayOrderRespDTO.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/dto/PayOrderRespDTO.java new file mode 100644 index 0000000..8f490b7 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/order/dto/PayOrderRespDTO.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.pay.api.order.dto; + +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import lombok.Data; + +/** + * 支付单信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayOrderRespDTO { + + /** + * 订单编号,数据库自增 + */ + private Long id; + /** + * 渠道编码 + * + * 枚举 PayChannelEnum + */ + private String channelCode; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + * 例如说,内部系统 A 的订单号。需要保证每个 PayMerchantDO 唯一 + */ + private String merchantOrderId; + + // ========== 订单相关字段 ========== + /** + * 支付金额,单位:分 + */ + private Integer price; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + + // ========== 渠道相关字段 ========== + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/PayRefundApi.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/PayRefundApi.java new file mode 100644 index 0000000..bcba37a --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/PayRefundApi.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.pay.api.refund; + +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundRespDTO; + +import javax.validation.Valid; + +/** + * 退款单 API 接口 + * + * @author 芋道源码 + */ +public interface PayRefundApi { + + /** + * 创建退款单 + * + * @param reqDTO 创建请求 + * @return 退款单编号 + */ + Long createRefund(@Valid PayRefundCreateReqDTO reqDTO); + + /** + * 获得退款单 + * + * @param id 退款单编号 + * @return 退款单 + */ + PayRefundRespDTO getRefund(Long id); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/dto/PayRefundCreateReqDTO.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/dto/PayRefundCreateReqDTO.java new file mode 100644 index 0000000..88ecaf3 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/dto/PayRefundCreateReqDTO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.pay.api.refund.dto; + +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Min; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 退款单创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class PayRefundCreateReqDTO { + + /** + * 应用编号 + */ + @NotNull(message = "应用编号不能为空") + private Long appId; + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + */ + @NotEmpty(message = "商户订单编号不能为空") + private String merchantOrderId; + + /** + * 商户退款编号 + */ + @NotEmpty(message = "商户退款编号不能为空") + private String merchantRefundId; + + /** + * 退款描述 + */ + @NotEmpty(message = "退款描述不能为空") + @Length(max = 128, message = "退款描述长度不能超过 128") + private String reason; + + // ========== 订单相关字段 ========== + + /** + * 退款金额,单位:分 + */ + @NotNull(message = "退款金额不能为空") + @Min(value = 1, message = "退款金额必须大于零") + private Integer price; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/dto/PayRefundRespDTO.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/dto/PayRefundRespDTO.java new file mode 100644 index 0000000..8f846e3 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/api/refund/dto/PayRefundRespDTO.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.pay.api.refund.dto; + +import com.yunxi.scm.module.pay.enums.refund.PayRefundStatusEnum; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 退款单信息 Response DTO + * + * @author 芋道源码 + */ +@Data +public class PayRefundRespDTO { + + /** + * 退款单编号 + */ + private Long id; + + // ========== 退款相关字段 ========== + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer status; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + */ + private String merchantOrderId; + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/DictTypeConstants.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/DictTypeConstants.java new file mode 100644 index 0000000..49a5fee --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/DictTypeConstants.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.pay.enums; + +/** + * Pay 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String CHANNEL_CODE = "pay_channel_code"; // 支付渠道编码 + + String ORDER_STATUS = "pay_order_status"; // 支付渠道 + + String REFUND_STATUS = "pay_order_status"; // 退款状态 + + String NOTIFY_STATUS = "pay_notify_status"; // 回调状态 + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/ErrorCodeConstants.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..4304965 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/ErrorCodeConstants.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.module.pay.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Pay 错误码 Core 枚举类 + * + * pay 系统,使用 1-007-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== APP 模块 1007000000 ========== + ErrorCode APP_NOT_FOUND = new ErrorCode(1007000000, "App 不存在"); + ErrorCode APP_IS_DISABLE = new ErrorCode(1007000002, "App 已经被禁用"); + ErrorCode APP_EXIST_ORDER_CANT_DELETE = new ErrorCode(1007000003, "支付应用存在支付订单,无法删除"); + ErrorCode APP_EXIST_REFUND_CANT_DELETE = new ErrorCode(1007000004, "支付应用存在退款订单,无法删除"); + + // ========== CHANNEL 模块 1007001000 ========== + ErrorCode CHANNEL_NOT_FOUND = new ErrorCode(1007001000, "支付渠道的配置不存在"); + ErrorCode CHANNEL_IS_DISABLE = new ErrorCode(1007001001, "支付渠道已经禁用"); + ErrorCode CHANNEL_EXIST_SAME_CHANNEL_ERROR = new ErrorCode(1007001004, "已存在相同的渠道"); + + // ========== ORDER 模块 1007002000 ========== + ErrorCode ORDER_NOT_FOUND = new ErrorCode(1007002000, "支付订单不存在"); + ErrorCode ORDER_STATUS_IS_NOT_WAITING = new ErrorCode(1007002001, "支付订单不处于待支付"); + ErrorCode ORDER_STATUS_IS_SUCCESS = new ErrorCode(1007002002, "订单已支付,请刷新页面"); + ErrorCode ORDER_IS_EXPIRED = new ErrorCode(1007002003, "支付订单已经过期"); + ErrorCode ORDER_SUBMIT_CHANNEL_ERROR = new ErrorCode(1007002004, "发起支付报错,错误码:{},错误提示:{}"); + ErrorCode ORDER_REFUND_FAIL_STATUS_ERROR = new ErrorCode(1007002005, "支付订单退款失败,原因:状态不是已支付或已退款"); + + // ========== ORDER 模块(拓展单) 1007003000 ========== + ErrorCode ORDER_EXTENSION_NOT_FOUND = new ErrorCode(1007003000, "支付交易拓展单不存在"); + ErrorCode ORDER_EXTENSION_STATUS_IS_NOT_WAITING = new ErrorCode(1007003001, "支付交易拓展单不处于待支付"); + ErrorCode ORDER_EXTENSION_IS_PAID = new ErrorCode(1007003002, "订单已支付,请等待支付结果"); + + // ========== 支付模块(退款) 1007006000 ========== + ErrorCode REFUND_PRICE_EXCEED = new ErrorCode(1007006000, "退款金额超过订单可退款金额"); + ErrorCode REFUND_HAS_REFUNDING = new ErrorCode(1007006002, "已经有退款在处理中"); + ErrorCode REFUND_EXISTS = new ErrorCode(1007006003, "已经存在退款单"); + ErrorCode REFUND_NOT_FOUND = new ErrorCode(1007006004, "支付退款单不存在"); + ErrorCode REFUND_STATUS_IS_NOT_WAITING = new ErrorCode(1007006005, "支付退款单不处于待退款"); + + // ========== 示例订单 1007900000 ========== + ErrorCode DEMO_ORDER_NOT_FOUND = new ErrorCode(1007900000, "示例订单不存在"); + ErrorCode DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID = new ErrorCode(1007900001, "示例订单更新支付状态失败,订单不是【未支付】状态"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR = new ErrorCode(1007900002, "示例订单更新支付状态失败,支付单编号不匹配"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS = new ErrorCode(1007900003, "示例订单更新支付状态失败,支付单状态不是【支付成功】状态"); + ErrorCode DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH = new ErrorCode(1007900004, "示例订单更新支付状态失败,支付单金额不匹配"); + ErrorCode DEMO_ORDER_REFUND_FAIL_NOT_PAID = new ErrorCode(1007900005, "发起退款失败,示例订单未支付"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUNDED = new ErrorCode(1007900006, "发起退款失败,示例订单已退款"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND = new ErrorCode(1007900007, "发起退款失败,退款订单不存在"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS = new ErrorCode(1007900008, "发起退款失败,退款订单未退款成功"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR = new ErrorCode(1007900009, "发起退款失败,退款单编号不匹配"); + ErrorCode DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH = new ErrorCode(1007900010, "发起退款失败,退款单金额不匹配"); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/member/WalletOperateTypeEnum.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/member/WalletOperateTypeEnum.java new file mode 100644 index 0000000..cf4cd6f --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/member/WalletOperateTypeEnum.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.pay.enums.member; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 钱包操作类型枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum WalletOperateTypeEnum { + TOP_UP_INC(1, "充值增加"), + ORDER_DEC(2, "订单消费扣除"); + // TODO 其它类型 + + private final Integer type; + + private final String desc; +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/member/WalletTransactionGategoryEnum.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/member/WalletTransactionGategoryEnum.java new file mode 100644 index 0000000..b0061eb --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/member/WalletTransactionGategoryEnum.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.pay.enums.member; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 钱包交易大类枚举 + * + * @author jason + */ +@AllArgsConstructor +@Getter +public enum WalletTransactionGategoryEnum { + TOP_UP(1, "充值"), + SPENDING(2, "支出"); + + /** + * 分类 + */ + private final Integer category; + + /** + * 说明 + */ + private final String desc; +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/notify/PayNotifyStatusEnum.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/notify/PayNotifyStatusEnum.java new file mode 100644 index 0000000..ba113da --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/notify/PayNotifyStatusEnum.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.pay.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付通知状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayNotifyStatusEnum { + + WAITING(0, "等待通知"), + SUCCESS(10, "通知成功"), + FAILURE(20, "通知失败"), // 多次尝试,彻底失败 + REQUEST_SUCCESS(21, "请求成功,但是结果失败"), + REQUEST_FAILURE(22, "请求失败"), + + ; + + /** + * 状态 + */ + private final Integer status; + /** + * 名字 + */ + private final String name; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/notify/PayNotifyTypeEnum.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/notify/PayNotifyTypeEnum.java new file mode 100644 index 0000000..1268ce5 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/notify/PayNotifyTypeEnum.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.pay.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 支付通知类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayNotifyTypeEnum { + + ORDER(1, "支付单"), + REFUND(2, "退款单"), + ; + + /** + * 类型 + */ + private final Integer type; + /** + * 名字 + */ + private final String name; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/order/PayOrderStatusEnum.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/order/PayOrderStatusEnum.java new file mode 100644 index 0000000..19706d8 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/order/PayOrderStatusEnum.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.module.pay.enums.order; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 支付订单的状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayOrderStatusEnum implements IntArrayValuable { + + WAITING(0, "未支付"), + SUCCESS(10, "支付成功"), + REFUND(20, "已退款"), + CLOSED(30, "支付关闭"), // 注意:全部退款后,还是 REFUND 状态 + ; + + private final Integer status; + private final String name; + + @Override + public int[] array() { + return new int[0]; + } + + /** + * 判断是否支付成功 + * + * @param status 状态 + * @return 是否支付成功 + */ + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + /** + * 判断是否支付成功或者已退款 + * + * @param status 状态 + * @return 是否支付成功或者已退款 + */ + public static boolean isSuccessOrRefund(Integer status) { + return ObjectUtils.equalsAny(status, + SUCCESS.getStatus(), REFUND.getStatus()); + } + + /** + * 判断是否支付关闭 + * + * @param status 状态 + * @return 是否支付关闭 + */ + public static boolean isClosed(Integer status) { + return Objects.equals(status, CLOSED.getStatus()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/refund/PayRefundStatusEnum.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/refund/PayRefundStatusEnum.java new file mode 100644 index 0000000..80d74f1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/enums/refund/PayRefundStatusEnum.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.pay.enums.refund; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; + +/** + * 渠道的退款状态枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum PayRefundStatusEnum { + + WAITING(0, "未退款"), + SUCCESS(10, "退款成功"), + FAILURE(20, "退款失败"); + + private final Integer status; + private final String name; + + public static boolean isSuccess(Integer status) { + return Objects.equals(status, SUCCESS.getStatus()); + } + + public static boolean isFailure(Integer status) { + return Objects.equals(status, FAILURE.getStatus()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/package-info.java b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/package-info.java new file mode 100644 index 0000000..2df1647 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-api/src/main/java/com/yunxi/scm/module/pay/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.pay; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/pom.xml b/yunxi-module-pay/yunxi-module-pay-biz/pom.xml new file mode 100644 index 0000000..6ce2a49 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/pom.xml @@ -0,0 +1,79 @@ + + + + yunxi-module-pay + com.yunxi.scm + ${revision} + + 4.0.0 + yunxi-module-pay-biz + jar + + ${project.artifactId} + + pay 模块,我们放支付业务,提供业务的支付能力。 + 例如说:商户、应用、支付、退款等等 + + + + + com.yunxi.scm + yunxi-module-pay-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-pay + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-tenant + + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-job + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + + + + + diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/api/order/PayOrderApiImpl.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/api/order/PayOrderApiImpl.java new file mode 100644 index 0000000..1927cad --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/api/order/PayOrderApiImpl.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.pay.api.order; + +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderRespDTO; +import com.yunxi.scm.module.pay.convert.order.PayOrderConvert; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 支付单 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PayOrderApiImpl implements PayOrderApi { + + @Resource + private PayOrderService payOrderService; + + @Override + public Long createOrder(PayOrderCreateReqDTO reqDTO) { + return payOrderService.createOrder(reqDTO); + } + + @Override + public PayOrderRespDTO getOrder(Long id) { + PayOrderDO order = payOrderService.getOrder(id); + return PayOrderConvert.INSTANCE.convert2(order); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/api/refund/PayRefundApiImpl.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/api/refund/PayRefundApiImpl.java new file mode 100644 index 0000000..0ae36a2 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/api/refund/PayRefundApiImpl.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.pay.api.refund; + +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundRespDTO; +import com.yunxi.scm.module.pay.convert.refund.PayRefundConvert; +import com.yunxi.scm.module.pay.service.refund.PayRefundService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 退款单 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PayRefundApiImpl implements PayRefundApi { + + @Resource + private PayRefundService payRefundService; + + @Override + public Long createRefund(PayRefundCreateReqDTO reqDTO) { + return payRefundService.createPayRefund(reqDTO); + } + + @Override + public PayRefundRespDTO getRefund(Long id) { + return PayRefundConvert.INSTANCE.convert02(payRefundService.getRefund(id)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/PayAppController.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/PayAppController.java new file mode 100644 index 0000000..ddea9b1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/PayAppController.java @@ -0,0 +1,108 @@ +package com.yunxi.scm.module.pay.controller.admin.app; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.module.pay.controller.admin.app.vo.*; +import com.yunxi.scm.module.pay.convert.app.PayAppConvert; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.service.app.PayAppService; +import com.yunxi.scm.module.pay.service.channel.PayChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.*; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; + +@Slf4j +@Tag(name = "管理后台 - 支付应用信息") +@RestController +@RequestMapping("/pay/app") +@Validated +public class PayAppController { + + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + + @PostMapping("/create") + @Operation(summary = "创建支付应用信息") + @PreAuthorize("@ss.hasPermission('pay:app:create')") + public CommonResult createApp(@Valid @RequestBody PayAppCreateReqVO createReqVO) { + return success(appService.createApp(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新支付应用信息") + @PreAuthorize("@ss.hasPermission('pay:app:update')") + public CommonResult updateApp(@Valid @RequestBody PayAppUpdateReqVO updateReqVO) { + appService.updateApp(updateReqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "更新支付应用状态") + @PreAuthorize("@ss.hasPermission('pay:app:update')") + public CommonResult updateAppStatus(@Valid @RequestBody PayAppUpdateStatusReqVO updateReqVO) { + appService.updateAppStatus(updateReqVO.getId(), updateReqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除支付应用信息") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('pay:app:delete')") + public CommonResult deleteApp(@RequestParam("id") Long id) { + appService.deleteApp(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得支付应用信息") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:app:query')") + public CommonResult getApp(@RequestParam("id") Long id) { + PayAppDO app = appService.getApp(id); + return success(PayAppConvert.INSTANCE.convert(app)); + } + + @GetMapping("/page") + @Operation(summary = "获得支付应用信息分页") + @PreAuthorize("@ss.hasPermission('pay:app:query')") + public CommonResult> getAppPage(@Valid PayAppPageReqVO pageVO) { + // 得到应用分页列表 + PageResult pageResult = appService.getAppPage(pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + + // 得到所有的应用编号,查出所有的渠道 + Collection appIds = convertList(pageResult.getList(), PayAppDO::getId); + List channels = channelService.getChannelListByAppIds(appIds); + + // 拼接后返回 + return success(PayAppConvert.INSTANCE.convertPage(pageResult, channels)); + } + + @GetMapping("/list") + @Operation(summary = "获得应用列表") + @PreAuthorize("@ss.hasPermission('pay:merchant:query')") + public CommonResult> getAppList() { + List appListDO = appService.getAppList(); + return success(PayAppConvert.INSTANCE.convertList(appListDO)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppBaseVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppBaseVO.java new file mode 100644 index 0000000..739f466 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppBaseVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.pay.controller.admin.app.vo; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.*; + +/** +* 支付应用信息 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayAppBaseVO { + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "小豆") + @NotNull(message = "应用名不能为空") + private String name; + + @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + @NotNull(message = "开启状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "备注", example = "我是一个测试应用") + private String remark; + + @Schema(description = "支付结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay-callback") + @NotNull(message = "支付结果的回调地址不能为空") + @URL(message = "支付结果的回调地址必须为 URL 格式") + private String orderNotifyUrl; + + @Schema(description = "退款结果的回调地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/refund-callback") + @NotNull(message = "退款结果的回调地址不能为空") + @URL(message = "退款结果的回调地址必须为 URL 格式") + private String refundNotifyUrl; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java new file mode 100644 index 0000000..3e1fb3d --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppCreateReqVO.java @@ -0,0 +1,11 @@ +package com.yunxi.scm.module.pay.controller.admin.app.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "管理后台 - 支付应用信息创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppCreateReqVO extends PayAppBaseVO { + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java new file mode 100644 index 0000000..208cf64 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppPageItemRespVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.Set; + +@Schema(description = "管理后台 - 支付应用信息分页查询 Response VO,相比于支付信息,还会多出应用渠道的开关信息") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppPageItemRespVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "已配置的支付渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "[alipay_pc, alipay_wap]") + private Set channelCodes; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppPageReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppPageReqVO.java new file mode 100644 index 0000000..527c45a --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppPageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.pay.controller.admin.app.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 支付应用信息分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppPageReqVO extends PageParam { + + @Schema(description = "应用名", example = "小豆") + private String name; + + @Schema(description = "开启状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppRespVO.java new file mode 100644 index 0000000..e7f9679 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.pay.controller.admin.app.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付应用信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppRespVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java new file mode 100644 index 0000000..2baf4f2 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppUpdateReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.pay.controller.admin.app.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 支付应用信息更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayAppUpdateReqVO extends PayAppBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long id; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java new file mode 100644 index 0000000..8e3ea13 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/app/vo/PayAppUpdateStatusReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.pay.controller.admin.app.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 应用更新状态 Request VO") +@Data +public class PayAppUpdateStatusReqVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long id; + + @Schema(description = "状态,见 SysCommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/PayChannelController.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/PayChannelController.java new file mode 100644 index 0000000..d9f4ebd --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/PayChannelController.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.pay.controller.admin.channel; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelRespVO; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.yunxi.scm.module.pay.convert.channel.PayChannelConvert; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.service.channel.PayChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "管理后台 - 支付渠道") +@RestController +@RequestMapping("/pay/channel") +@Validated +public class PayChannelController { + + @Resource + private PayChannelService channelService; + + @PostMapping("/create") + @Operation(summary = "创建支付渠道 ") + @PreAuthorize("@ss.hasPermission('pay:channel:create')") + public CommonResult createChannel(@Valid @RequestBody PayChannelCreateReqVO createReqVO) { + return success(channelService.createChannel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新支付渠道 ") + @PreAuthorize("@ss.hasPermission('pay:channel:update')") + public CommonResult updateChannel(@Valid @RequestBody PayChannelUpdateReqVO updateReqVO) { + channelService.updateChannel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除支付渠道 ") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('pay:channel:delete')") + public CommonResult deleteChannel(@RequestParam("id") Long id) { + channelService.deleteChannel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得支付渠道") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:channel:query')") + public CommonResult getChannel(@RequestParam(value = "id", required = false) Long id, + @RequestParam(value = "appId", required = false) Long appId, + @RequestParam(value = "code", required = false) String code) { + PayChannelDO channel = null; + if (id != null) { + channel = channelService.getChannel(id); + } else if (appId != null && code != null) { + channel = channelService.getChannelByAppIdAndCode(appId, code); + } + return success(PayChannelConvert.INSTANCE.convert(channel)); + } + + @GetMapping("/get-enable-code-list") + @Operation(summary = "获得指定应用的开启的支付渠道编码列表") + @Parameter(name = "appId", description = "应用编号", required = true, example = "1") + public CommonResult> getEnableChannelCodeList(@RequestParam("appId") Long appId) { + List channels = channelService.getEnableChannelList(appId); + return success(convertSet(channels, PayChannelDO::getCode)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java new file mode 100644 index 0000000..293fc12 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelBaseVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.pay.controller.admin.channel.vo; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +/** +* 支付渠道 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayChannelBaseVO { + + @Schema(description = "开启状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "开启状态不能为空") + @InEnum(CommonStatusEnum.class) + private Integer status; + + @Schema(description = "备注", example = "我是小备注") + private String remark; + + @Schema(description = "渠道费率,单位:百分比", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "渠道费率,单位:百分比不能为空") + private Double feeRate; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long appId; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java new file mode 100644 index 0000000..f4975b2 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelCreateReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.pay.controller.admin.channel.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 支付渠道 创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelCreateReqVO extends PayChannelBaseVO { + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc") + @NotNull(message = "渠道编码不能为空") + private String code; + + @Schema(description = "渠道配置的 json 字符串") + @NotBlank(message = "渠道配置不能为空") + private String config; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelRespVO.java new file mode 100644 index 0000000..e7034fa --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.pay.controller.admin.channel.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付渠道 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelRespVO extends PayChannelBaseVO { + + @Schema(description = "商户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private LocalDateTime createTime; + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "alipay_pc") + private String code; + + @Schema(description = "配置", requiredMode = Schema.RequiredMode.REQUIRED) + private String config; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java new file mode 100644 index 0000000..7772547 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/channel/vo/PayChannelUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.pay.controller.admin.channel.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 支付渠道 更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayChannelUpdateReqVO extends PayChannelBaseVO { + + @Schema(description = "商户编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "商户编号不能为空") + private Long id; + + @Schema(description = "渠道配置的json字符串") + @NotBlank(message = "渠道配置不能为空") + private String config; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/PayDemoOrderController.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/PayDemoOrderController.java new file mode 100644 index 0000000..b4ceac5 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/PayDemoOrderController.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.pay.controller.admin.demo; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import com.yunxi.scm.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import com.yunxi.scm.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO; +import com.yunxi.scm.module.pay.convert.demo.PayDemoOrderConvert; +import com.yunxi.scm.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import com.yunxi.scm.module.pay.service.demo.PayDemoOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 示例订单") +@RestController +@RequestMapping("/pay/demo-order") +@Validated +public class PayDemoOrderController { + + @Resource + private PayDemoOrderService payDemoOrderService; + + @PostMapping("/create") + @Operation(summary = "创建示例订单") + public CommonResult createDemoOrder(@Valid @RequestBody PayDemoOrderCreateReqVO createReqVO) { + return success(payDemoOrderService.createDemoOrder(getLoginUserId(), createReqVO)); + } + + @GetMapping("/page") + @Operation(summary = "获得示例订单分页") + public CommonResult> getDemoOrderPage(@Valid PageParam pageVO) { + PageResult pageResult = payDemoOrderService.getDemoOrderPage(pageVO); + return success(PayDemoOrderConvert.INSTANCE.convertPage(pageResult)); + } + + @PostMapping("/update-paid") + @Operation(summary = "更新示例订单为已支付") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现 + @OperateLog(enable = false) // 禁用操作日志,因为没有操作人 + public CommonResult updateDemoOrderPaid(@RequestBody PayOrderNotifyReqDTO notifyReqDTO) { + payDemoOrderService.updateDemoOrderPaid(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayOrderId()); + return success(true); + } + + @PutMapping("/refund") + @Operation(summary = "发起示例订单的退款") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult refundDemoOrder(@RequestParam("id") Long id) { + payDemoOrderService.refundDemoOrder(id, getClientIP()); + return success(true); + } + + @PostMapping("/update-refunded") + @Operation(summary = "更新示例订单为已退款") // 由 pay-module 支付服务,进行回调,可见 PayNotifyJob + @PermitAll // 无需登录,安全由 PayDemoOrderService 内部校验实现 + @OperateLog(enable = false) // 禁用操作日志,因为没有操作人 + public CommonResult updateDemoOrderRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { + payDemoOrderService.updateDemoOrderRefunded(Long.valueOf(notifyReqDTO.getMerchantOrderId()), + notifyReqDTO.getPayRefundId()); + return success(true); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java new file mode 100644 index 0000000..65ab961 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/vo/PayDemoOrderCreateReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.pay.controller.admin.demo.vo; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 示例订单创建 Request VO") +@Data +public class PayDemoOrderCreateReqVO { + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17682") + @NotNull(message = "商品编号不能为空") + private Long spuId; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java new file mode 100644 index 0000000..30201b6 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/demo/vo/PayDemoOrderRespVO.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.pay.controller.admin.demo.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +/** +* 示例订单 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayDemoOrderRespVO { + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23199") + private Long userId; + + @Schema(description = "商品编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17682") + private Long spuId; + + @Schema(description = "商家备注", example = "李四") + private String spuName; + + @Schema(description = "价格,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "30381") + private Integer price; + + @Schema(description = "是否已支付", requiredMode = Schema.RequiredMode.REQUIRED) + private Boolean payStatus; + + @Schema(description = "支付订单编号", example = "16863") + private Long payOrderId; + + @Schema(description = "订单支付时间") + private LocalDateTime payTime; + + @Schema(description = "支付渠道", example = "alipay_qr") + private String payChannelCode; + + @Schema(description = "支付退款编号", example = "23366") + private Long payRefundId; + + @Schema(description = "退款金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "14039") + private Integer refundPrice; + + @Schema(description = "退款时间") + private LocalDateTime refundTime; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/PayNotifyController.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/PayNotifyController.java new file mode 100644 index 0000000..46db35d --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/PayNotifyController.java @@ -0,0 +1,130 @@ +package com.yunxi.scm.module.pay.controller.admin.notify; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.framework.pay.core.client.PayClient; +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO; +import com.yunxi.scm.module.pay.convert.notify.PayNotifyTaskConvert; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import com.yunxi.scm.module.pay.service.app.PayAppService; +import com.yunxi.scm.module.pay.service.notify.PayNotifyService; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import com.yunxi.scm.module.pay.service.refund.PayRefundService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.CHANNEL_NOT_FOUND; + +@Tag(name = "管理后台 - 回调通知") +@RestController +@RequestMapping("/pay/notify") +@Validated +@Slf4j +public class PayNotifyController { + + @Resource + private PayOrderService orderService; + @Resource + private PayRefundService refundService; + @Resource + private PayNotifyService notifyService; + @Resource + private PayAppService appService; + + @Resource + private PayClientFactory payClientFactory; + + @PostMapping(value = "/order/{channelId}") + @Operation(summary = "支付渠道的统一【支付】回调") + @PermitAll + @OperateLog(enable = false) // 回调地址,无需记录操作日志 + public String notifyOrder(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { + log.info("[notifyOrder][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = payClientFactory.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayOrderRespDTO notify = payClient.parseOrderNotify(params, body); + orderService.notifyOrder(channelId, notify); + return "success"; + } + + @PostMapping(value = "/refund/{channelId}") + @Operation(summary = "支付渠道的统一【退款】回调") + @PermitAll + @OperateLog(enable = false) // 回调地址,无需记录操作日志 + public String notifyRefund(@PathVariable("channelId") Long channelId, + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { + log.info("[notifyRefund][channelId({}) 回调数据({}/{})]", channelId, params, body); + // 1. 校验支付渠道是否存在 + PayClient payClient = payClientFactory.getPayClient(channelId); + if (payClient == null) { + log.error("[notifyCallback][渠道编号({}) 找不到对应的支付客户端]", channelId); + throw exception(CHANNEL_NOT_FOUND); + } + + // 2. 解析通知数据 + PayRefundRespDTO notify = payClient.parseRefundNotify(params, body); + refundService.notifyRefund(channelId, notify); + return "success"; + } + + @GetMapping("/get-detail") + @Operation(summary = "获得回调通知的明细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:notify:query')") + public CommonResult getNotifyTaskDetail(@RequestParam("id") Long id) { + PayNotifyTaskDO task = notifyService.getNotifyTask(id); + if (task == null) { + return success(null); + } + // 拼接返回 + PayAppDO app = appService.getApp(task.getAppId()); + List logs = notifyService.getNotifyLogList(id); + return success(PayNotifyTaskConvert.INSTANCE.convert(task, app, logs)); + } + + @GetMapping("/page") + @Operation(summary = "获得回调通知分页") + @PreAuthorize("@ss.hasPermission('pay:notify:query')") + public CommonResult> getNotifyTaskPage(@Valid PayNotifyTaskPageReqVO pageVO) { + PageResult pageResult = notifyService.getNotifyTaskPage(pageVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(PageResult.empty()); + } + // 拼接返回 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayNotifyTaskDO::getAppId)); + return success(PayNotifyTaskConvert.INSTANCE.convertPage(pageResult, appMap)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java new file mode 100644 index 0000000..8be1f9b --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskBaseVO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.pay.controller.admin.notify.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 回调通知 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class PayNotifyTaskBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10636") + private Long appId; + + @Schema(description = "通知类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Byte type; + + @Schema(description = "数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6722") + private Long dataId; + + @Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Byte status; + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "26697") + private String merchantOrderId; + + @Schema(description = "下一次通知时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime nextNotifyTime; + + @Schema(description = "最后一次执行时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime lastExecuteTime; + + @Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte notifyTimes; + + @Schema(description = "最大可通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte maxNotifyTimes; + + @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") + private String notifyUrl; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java new file mode 100644 index 0000000..fbb6981 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskDetailRespVO.java @@ -0,0 +1,54 @@ + +package com.yunxi.scm.module.pay.controller.admin.notify.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 回调通知的明细 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayNotifyTaskDetailRespVO extends PayNotifyTaskBaseVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3380") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime updateTime; + + @Schema(description = "应用名称", example = "wx_pay") + private String appName; + + @Schema(description = "回调日志列表") + private List logs; + + @Schema(description = "管理后台 - 回调日志") + @Data + public static class Log { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8848") + private Long id; + + @Schema(description = "通知状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Byte status; + + @Schema(description = "当前通知次数", requiredMode = Schema.RequiredMode.REQUIRED) + private Byte notifyTimes; + + @Schema(description = "HTTP 响应结果", requiredMode = Schema.RequiredMode.REQUIRED) + private String response; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java new file mode 100644 index 0000000..6208e09 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskPageReqVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.pay.controller.admin.notify.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 回调通知分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayNotifyTaskPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "10636") + private Long appId; + + @Schema(description = "通知类型", example = "2") + private Integer type; + + @Schema(description = "数据编号", example = "6722") + private Long dataId; + + @Schema(description = "通知状态", example = "1") + private Integer status; + + @Schema(description = "商户订单编号", example = "26697") + private String merchantOrderId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java new file mode 100644 index 0000000..7f9fcf1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/notify/vo/PayNotifyTaskRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.pay.controller.admin.notify.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 回调通知 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayNotifyTaskRespVO extends PayNotifyTaskBaseVO { + + @Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "3380") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "应用名称", example = "wx_pay") + private String appName; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/PayOrderController.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/PayOrderController.java new file mode 100644 index 0000000..d02dc6e --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/PayOrderController.java @@ -0,0 +1,111 @@ +package com.yunxi.scm.module.pay.controller.admin.order; + +import cn.hutool.core.collection.CollectionUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.pay.controller.admin.order.vo.*; +import com.yunxi.scm.module.pay.convert.order.PayOrderConvert; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.yunxi.scm.module.pay.service.app.PayAppService; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 支付订单") +@RestController +@RequestMapping("/pay/order") +@Validated +public class PayOrderController { + + @Resource + private PayOrderService orderService; + @Resource + private PayAppService appService; + + @GetMapping("/get") + @Operation(summary = "获得支付订单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult getOrder(@RequestParam("id") Long id) { + return success(PayOrderConvert.INSTANCE.convert(orderService.getOrder(id))); + } + + @GetMapping("/get-detail") + @Operation(summary = "获得支付订单详情") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult getOrderDetail(@RequestParam("id") Long id) { + PayOrderDO order = orderService.getOrder(id); + if (order == null) { + return success(null); + } + + // 拼接返回 + PayAppDO app = appService.getApp(order.getAppId()); + PayOrderExtensionDO orderExtension = orderService.getOrderExtension(order.getExtensionId()); + return success(PayOrderConvert.INSTANCE.convert(order, orderExtension, app)); + } + + @PostMapping("/submit") + @Operation(summary = "提交支付订单") + public CommonResult submitPayOrder(@RequestBody PayOrderSubmitReqVO reqVO) { + PayOrderSubmitRespVO respVO = orderService.submitOrder(reqVO, getClientIP()); + return success(respVO); + } + + @GetMapping("/page") + @Operation(summary = "获得支付订单分页") + @PreAuthorize("@ss.hasPermission('pay:order:query')") + public CommonResult> getOrderPage(@Valid PayOrderPageReqVO pageVO) { + PageResult pageResult = orderService.getOrderPage(pageVO); + if (CollectionUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); + } + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayOrderDO::getAppId)); + return success(PayOrderConvert.INSTANCE.convertPage(pageResult, appMap)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出支付订单 Excel") + @PreAuthorize("@ss.hasPermission('pay:order:export')") + @OperateLog(type = EXPORT) + public void exportOrderExcel(@Valid PayOrderExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = orderService.getOrderList(exportReqVO); + if (CollectionUtil.isEmpty(list)) { + ExcelUtils.write(response, "支付订单.xls", "数据", + PayOrderExcelVO.class, new ArrayList<>()); + return; + } + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(list, PayOrderDO::getAppId)); + List excelList = PayOrderConvert.INSTANCE.convertList(list, appMap); + // 导出 Excel + ExcelUtils.write(response, "支付订单.xls", "数据", PayOrderExcelVO.class, excelList); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderBaseVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderBaseVO.java new file mode 100644 index 0000000..10c83be --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderBaseVO.java @@ -0,0 +1,89 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 支付订单 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + * + * @author aquan + */ +@Data +public class PayOrderBaseVO { + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "应用编号不能为空") + private Long appId; + + @Schema(description = "渠道编号", example = "2048") + private Long channelId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "888") + @NotNull(message = "商户订单编号不能为空") + private String merchantOrderId; + + @Schema(description = "商品标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + @NotNull(message = "商品标题不能为空") + private String subject; + + @Schema(description = "商品描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是土豆") + @NotNull(message = "商品描述不能为空") + private String body; + + @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://127.0.0.1:48080/pay/notify") + @NotNull(message = "异步通知地址不能为空") + private String notifyUrl; + + @Schema(description = "支付金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "支付金额,单位:分不能为空") + private Long price; + + @Schema(description = "渠道手续费,单位:百分比", example = "10") + private Double channelFeeRate; + + @Schema(description = "渠道手续金额,单位:分", example = "100") + private Integer channelFeePrice; + + @Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "支付状态不能为空") + private Integer status; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotNull(message = "用户 IP不能为空") + private String userIp; + + @Schema(description = "订单失效时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "订单失效时间不能为空") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime expireTime; + + @Schema(description = "订单支付成功时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime successTime; + + @Schema(description = "支付成功的订单拓展单编号", example = "50") + private Long extensionId; + + @Schema(description = "支付订单号", example = "2048888") + private String no; + + @Schema(description = "退款总金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "退款总金额,单位:分不能为空") + private Long refundPrice; + + @Schema(description = "渠道用户编号", example = "2048") + private String channelUserId; + + @Schema(description = "渠道订单号", example = "4096") + private String channelOrderNo; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java new file mode 100644 index 0000000..55ea3ca --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderDetailsRespVO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单详细信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderDetailsRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", required = true, example = "1024") + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String appName; + + @Schema(description = "创建时间", required = true) + private LocalDateTime createTime; + + @Schema(description = "更新时间", required = true) + private LocalDateTime updateTime; + + /** + * 支付订单扩展 + */ + private PayOrderExtension extension; + + @Data + @Schema(description = "支付订单扩展") + public static class PayOrderExtension { + + @Schema(description = "支付订单号", required = true, example = "1024") + private String no; + + @Schema(description = "支付异步通知的内容") + private String channelNotifyData; + + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderExcelVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderExcelVO.java new file mode 100644 index 0000000..0772dff --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderExcelVO.java @@ -0,0 +1,67 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.framework.excel.core.convert.MoneyConvert; +import com.yunxi.scm.module.pay.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 支付订单 Excel VO + * + * @author aquan + */ +@Data +public class PayOrderExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @ExcelProperty(value = "支付金额", converter = MoneyConvert.class) + private Integer price; + + @ExcelProperty(value = "退款金额", converter = MoneyConvert.class) + private Integer refundPrice; + + @ExcelProperty(value = "手续金额", converter = MoneyConvert.class) + private Integer channelFeePrice; + + @ExcelProperty("商户单号") + private String merchantOrderId; + + @ExcelProperty(value = "支付单号") + private String no; + + @ExcelProperty("渠道单号") + private String channelOrderNo; + + @ExcelProperty(value = "支付状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.ORDER_STATUS) + private Integer status; + + @ExcelProperty(value = "渠道编号名称", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("订单支付成功时间") + private LocalDateTime successTime; + + @ExcelProperty("订单失效时间") + private LocalDateTime expireTime; + + @ExcelProperty(value = "应用名称") + private String appName; + + @ExcelProperty("商品标题") + private String subject; + + @ExcelProperty("商品描述") + private String body; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java new file mode 100644 index 0000000..d72eed7 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderExportReqVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 支付订单 Excel 导出 Request VO,参数和 PayOrderPageReqVO 是一致的") +@Data +public class PayOrderExportReqVO { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", example = "4096") + private String merchantOrderId; + + @Schema(description = "渠道编号", example = "1888") + private String channelOrderNo; + + @Schema(description = "支付单号", example = "2014888") + private String no; + + @Schema(description = "支付状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java new file mode 100644 index 0000000..5dbfd41 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderPageItemRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderPageItemRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "应用名称", example = "wx_pay") + private String appName; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java new file mode 100644 index 0000000..8405476 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderPageReqVO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 支付订单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户订单编号", example = "4096") + private String merchantOrderId; + + @Schema(description = "渠道编号", example = "1888") + private String channelOrderNo; + + @Schema(description = "支付单号", example = "2014888") + private String no; + + @Schema(description = "支付状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderRespVO.java new file mode 100644 index 0000000..67f579e --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 支付订单 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayOrderRespVO extends PayOrderBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java new file mode 100644 index 0000000..22fc35a --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderSubmitReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 支付订单提交 Request VO") +@Data +public class PayOrderSubmitReqVO { + + @Schema(description = "支付单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "支付单编号不能为空") + private Long id; + + @Schema(description = "支付渠道", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_pub") + @NotEmpty(message = "支付渠道不能为空") + private String channelCode; + + @Schema(description = "支付渠道的额外参数,例如说,微信公众号需要传递 openid 参数") + private Map channelExtras; + + @Schema(description = "展示模式", example = "url") // 参见 {@link PayDisplayModeEnum} 枚举。如果不传递,则每个支付渠道使用默认的方式 + private String displayMode; + + @Schema(description = "回跳地址") + @URL(message = "回跳地址的格式必须是 URL") + private String returnUrl; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java new file mode 100644 index 0000000..40993f8 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/order/vo/PayOrderSubmitRespVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.pay.controller.admin.order.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 支付订单提交 Response VO") +@Data +public class PayOrderSubmitRespVO { + + @Schema(description = "支付状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") // 参见 PayOrderStatusEnum 枚举 + private Integer status; + + @Schema(description = "展示模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "url") // 参见 PayDisplayModeEnum 枚举 + private String displayMode; + @Schema(description = "展示内容", requiredMode = Schema.RequiredMode.REQUIRED) + private String displayContent; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/PayRefundController.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/PayRefundController.java new file mode 100644 index 0000000..4ba988b --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/PayRefundController.java @@ -0,0 +1,96 @@ +package com.yunxi.scm.module.pay.controller.admin.refund; + +import cn.hutool.core.collection.CollectionUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.*; +import com.yunxi.scm.module.pay.convert.refund.PayRefundConvert; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; +import com.yunxi.scm.module.pay.service.app.PayAppService; +import com.yunxi.scm.module.pay.service.refund.PayRefundService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 退款订单") +@RestController +@RequestMapping("/pay/refund") +@Validated +public class PayRefundController { + + @Resource + private PayRefundService refundService; + @Resource + private PayAppService appService; + + @GetMapping("/get") + @Operation(summary = "获得退款订单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('pay:refund:query')") + public CommonResult getRefund(@RequestParam("id") Long id) { + PayRefundDO refund = refundService.getRefund(id); + if (refund == null) { + return success(new PayRefundDetailsRespVO()); + } + + // 拼接数据 + PayAppDO app = appService.getApp(refund.getAppId()); + return success(PayRefundConvert.INSTANCE.convert(refund, app)); + } + + @GetMapping("/page") + @Operation(summary = "获得退款订单分页") + @PreAuthorize("@ss.hasPermission('pay:refund:query')") + public CommonResult> getRefundPage(@Valid PayRefundPageReqVO pageVO) { + PageResult pageResult = refundService.getRefundPage(pageVO); + if (CollectionUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); + } + + // 处理应用ID数据 + Map appMap = appService.getAppMap(convertList(pageResult.getList(), PayRefundDO::getAppId)); + return success(PayRefundConvert.INSTANCE.convertPage(pageResult, appMap)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出退款订单 Excel") + @PreAuthorize("@ss.hasPermission('pay:refund:export')") + @OperateLog(type = EXPORT) + public void exportRefundExcel(@Valid PayRefundExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = refundService.getRefundList(exportReqVO); + if (CollectionUtil.isEmpty(list)) { + ExcelUtils.write(response, "退款订单.xls", "数据", + PayRefundExcelVO.class, new ArrayList<>()); + return; + } + + // 拼接返回 + Map appMap = appService.getAppMap(convertList(list, PayRefundDO::getAppId)); + List excelList = PayRefundConvert.INSTANCE.convertList(list, appMap); + // 导出 Excel + ExcelUtils.write(response, "退款订单.xls", "数据", PayRefundExcelVO.class, excelList); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java new file mode 100644 index 0000000..40e67a2 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundBaseVO.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; + +/** +* 退款订单 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class PayRefundBaseVO { + + @Schema(description = "外部退款号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110") + private String no; + + @Schema(description = "应用编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long appId; + + @Schema(description = "渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048") + private Long channelId; + + @Schema(description = "渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "wx_app") + private String channelCode; + + @Schema(description = "订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long orderId; + + // ========== 商户相关字段 ========== + + @Schema(description = "商户订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "225") + private String merchantOrderId; + + @Schema(description = "商户退款订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "512") + private String merchantRefundId; + + @Schema(description = "异步通知地址", requiredMode = Schema.RequiredMode.REQUIRED) + private String notifyUrl; + + // ========== 退款相关字段 ========== + + @Schema(description = "退款状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer status; + + @Schema(description = "支付金额", requiredMode = Schema.RequiredMode.REQUIRED, example = "100") + private Long payPrice; + + @Schema(description = "退款金额,单位分", requiredMode = Schema.RequiredMode.REQUIRED, example = "200") + private Long refundPrice; + + @Schema(description = "退款原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "我要退了") + private String reason; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + private String userIp; + + // ========== 渠道相关字段 ========== + + @Schema(description = "渠道订单号", requiredMode = Schema.RequiredMode.REQUIRED, example = "233") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "2022") + private String channelRefundNo; + + @Schema(description = "退款成功时间") + private LocalDateTime successTime; + + @Schema(description = "调用渠道的错误码") + private String channelErrorCode; + + @Schema(description = "调用渠道的错误提示") + private String channelErrorMsg; + + @Schema(description = "支付渠道的额外参数") + private String channelNotifyData; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java new file mode 100644 index 0000000..fef19b8 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundDetailsRespVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 退款订单详情 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayRefundDetailsRespVO extends PayRefundBaseVO { + + @Schema(description = "支付退款编号", requiredMode = Schema.RequiredMode.REQUIRED) + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是芋艿") + private String appName; + + @Schema(description = "支付订单", requiredMode = Schema.RequiredMode.REQUIRED) + private Order order; + + @Schema(description = "创建时间") + private LocalDateTime createTime; + + @Schema(description = "更新时间") + private LocalDateTime updateTime; + + @Schema(description = "管理后台 - 支付订单") + @Data + public static class Order { + + @Schema(description = "商品标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + private String subject; + + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java new file mode 100644 index 0000000..473a0e6 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java @@ -0,0 +1,61 @@ +package com.yunxi.scm.module.pay.controller.admin.refund.vo; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.framework.excel.core.convert.MoneyConvert; +import com.yunxi.scm.module.pay.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 退款订单 Excel VO + * + * @author aquan + */ +@Data +public class PayRefundExcelVO { + + @ExcelProperty("支付退款编号") + private Long id; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + + @ExcelProperty(value = "支付金额", converter = MoneyConvert.class) + private Integer payPrice; + + @ExcelProperty(value = "退款金额", converter = MoneyConvert.class) + private Integer refundPrice; + + @ExcelProperty("商户退款单号") + private String merchantRefundId; + @ExcelProperty("退款单号") + private String no; + @ExcelProperty("渠道退款单号") + private String channelRefundNo; + + @ExcelProperty("商户支付单号") + private String merchantOrderId; + @ExcelProperty("渠道支付单号") + private String channelOrderNo; + + @ExcelProperty(value = "退款状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.REFUND_STATUS) + private Integer status; + + @ExcelProperty(value = "退款渠道", converter = DictConvert.class) + @DictFormat(DictTypeConstants.CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("成功时间") + private LocalDateTime successTime; + + @ExcelProperty(value = "支付应用") + private String appName; + + @ExcelProperty("退款原因") + private String reason; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java new file mode 100644 index 0000000..cf65511 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundExportReqVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 退款订单 Excel 导出 Request VO,参数和 PayRefundPageReqVO 是一致的") +@Data +public class PayRefundExportReqVO { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户支付单号", example = "10") + private String merchantOrderId; + + @Schema(description = "商户退款单号", example = "20") + private String merchantRefundId; + + @Schema(description = "渠道支付单号", example = "30") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "40") + private String channelRefundNo; + + @Schema(description = "退款状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java new file mode 100644 index 0000000..5d0ed27 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundPageItemRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.pay.controller.admin.refund.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 退款订单分页查询 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayRefundPageItemRespVO extends PayRefundBaseVO { + + @Schema(description = "支付订单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "应用名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是芋艿") + private String appName; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java new file mode 100644 index 0000000..b7b1a6d --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/admin/refund/vo/PayRefundPageReqVO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.pay.controller.admin.refund.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 退款订单分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class PayRefundPageReqVO extends PageParam { + + @Schema(description = "应用编号", example = "1024") + private Long appId; + + @Schema(description = "渠道编码", example = "wx_app") + private String channelCode; + + @Schema(description = "商户支付单号", example = "10") + private String merchantOrderId; + + @Schema(description = "商户退款单号", example = "20") + private String merchantRefundId; + + @Schema(description = "渠道支付单号", example = "30") + private String channelOrderNo; + + @Schema(description = "渠道退款单号", example = "40") + private String channelRefundNo; + + @Schema(description = "退款状态", example = "0") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/channel/AppPayChannelController.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/channel/AppPayChannelController.java new file mode 100644 index 0000000..0f34b34 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/channel/AppPayChannelController.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.pay.controller.app.channel; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.service.channel.PayChannelService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; + +@Tag(name = "用户 App - 支付渠道") +@RestController +@RequestMapping("/pay/channel") +@Validated +public class AppPayChannelController { + + @Resource + private PayChannelService channelService; + + @GetMapping("/get-enable-code-list") + @Operation(summary = "获得指定应用的开启的支付渠道编码列表") + @Parameter(name = "appId", description = "应用编号", required = true, example = "1") + public CommonResult> getEnableChannelCodeList(@RequestParam("appId") Long appId) { + List channels = channelService.getEnableChannelList(appId); + return success(convertSet(channels, PayChannelDO::getCode)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/AppPayOrderController.http b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/AppPayOrderController.http new file mode 100644 index 0000000..14ce54e --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/AppPayOrderController.http @@ -0,0 +1,63 @@ +### /pay/create 提交支付订单【alipay_pc】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 174, + "channelCode": "alipay_pc" +} + +### /pay/create 提交支付订单【wx_bar】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 202, + "channelCode": "wx_bar", + "channelExtras": { + "authCode": "134042110834344848" + } +} + +### /pay/create 提交支付订单【wx_pub】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 202, + "channelCode": "wx_pub", + "channelExtras": { + "openid": "ockUAwIZ-0OeMZl9ogcZ4ILrGba0" + } +} + +### /pay/create 提交支付订单【wx_lite】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 202, + "channelCode": "wx_lite", + "channelExtras": { + "openid": "oLefc4g5GjKWHJjLjMSXB3wX0fD0" + } +} + +### /pay/create 提交支付订单【wx_native】 +POST {{appApi}}/pay/order/submit +Content-Type: application/json +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} + +{ + "id": 202, + "channelCode": "wx_native" +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/AppPayOrderController.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/AppPayOrderController.java new file mode 100644 index 0000000..ae06660 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/AppPayOrderController.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.pay.controller.app.order; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderRespVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import com.yunxi.scm.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO; +import com.yunxi.scm.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; +import com.yunxi.scm.module.pay.convert.order.PayOrderConvert; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; + +@Tag(name = "用户 APP - 支付订单") +@RestController +@RequestMapping("/pay/order") +@Validated +@Slf4j +public class AppPayOrderController { + + @Resource + private PayOrderService payOrderService; + + // TODO 芋艿:临时 demo,技术打样。 + @GetMapping("/get") + @Operation(summary = "获得支付订单") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + public CommonResult getOrder(@RequestParam("id") Long id) { + return success(PayOrderConvert.INSTANCE.convert(payOrderService.getOrder(id))); + } + + @PostMapping("/submit") + @Operation(summary = "提交支付订单") + public CommonResult submitPayOrder(@RequestBody AppPayOrderSubmitReqVO reqVO) { + PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP()); + return success(PayOrderConvert.INSTANCE.convert3(respVO)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java new file mode 100644 index 0000000..c914d42 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/vo/AppPayOrderSubmitReqVO.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.pay.controller.app.order.vo; + +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "用户 APP - 支付订单提交 Request VO") +@Data +public class AppPayOrderSubmitReqVO extends PayOrderSubmitReqVO { +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java new file mode 100644 index 0000000..1a5885c --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/order/vo/AppPayOrderSubmitRespVO.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.pay.controller.app.order.vo; + +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +@Schema(description = "用户 APP - 支付订单提交 Response VO") +@Data +public class AppPayOrderSubmitRespVO extends PayOrderSubmitRespVO { + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/refund/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/refund/package-info.java new file mode 100644 index 0000000..12e6a10 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/app/refund/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占个位置,没啥用 + */ +package com.yunxi.scm.module.pay.controller.app.refund; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/package-info.java new file mode 100644 index 0000000..9539777 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yunxi-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yunxi-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.yunxi.scm.module.pay.controller; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/app/PayAppConvert.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/app/PayAppConvert.java new file mode 100644 index 0000000..26ec029 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/app/PayAppConvert.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.pay.convert.app; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppPageItemRespVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppRespVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +/** + * 支付应用信息 Convert + * + * @author 芋艿 + */ +@Mapper +public interface PayAppConvert { + + PayAppConvert INSTANCE = Mappers.getMapper(PayAppConvert.class); + + PayAppPageItemRespVO pageConvert (PayAppDO bean); + + PayAppDO convert(PayAppCreateReqVO bean); + + PayAppDO convert(PayAppUpdateReqVO bean); + + PayAppRespVO convert(PayAppDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + default PageResult convertPage(PageResult pageResult, List channels) { + PageResult voPageResult = convertPage(pageResult); + // 处理 channel 关系 + Map> appIdChannelMap = CollectionUtils.convertMultiMap2(channels, PayChannelDO::getAppId, PayChannelDO::getCode); + voPageResult.getList().forEach(app -> app.setChannelCodes(appIdChannelMap.get(app.getId()))); + return voPageResult; + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/channel/PayChannelConvert.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/channel/PayChannelConvert.java new file mode 100644 index 0000000..522a30f --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/channel/PayChannelConvert.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.pay.convert.channel; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelRespVO; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PayChannelConvert { + + PayChannelConvert INSTANCE = Mappers.getMapper(PayChannelConvert.class); + + @Mapping(target = "config",ignore = true) + PayChannelDO convert(PayChannelCreateReqVO bean); + + @Mapping(target = "config",ignore = true) + PayChannelDO convert(PayChannelUpdateReqVO bean); + + @Mapping(target = "config",expression = "java(com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString(bean.getConfig()))") + PayChannelRespVO convert(PayChannelDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/demo/PayDemoOrderConvert.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/demo/PayDemoOrderConvert.java new file mode 100644 index 0000000..323ff4a --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/demo/PayDemoOrderConvert.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.pay.convert.demo; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.demo.vo.PayDemoOrderRespVO; +import com.yunxi.scm.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +/** + * 示例订单 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface PayDemoOrderConvert { + + PayDemoOrderConvert INSTANCE = Mappers.getMapper(PayDemoOrderConvert.class); + + PayDemoOrderDO convert(PayDemoOrderCreateReqVO bean); + + PayDemoOrderRespVO convert(PayDemoOrderDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/notify/PayNotifyTaskConvert.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/notify/PayNotifyTaskConvert.java new file mode 100644 index 0000000..8e897e8 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/notify/PayNotifyTaskConvert.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.pay.convert.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskDetailRespVO; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskRespVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +/** + * 支付通知 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface PayNotifyTaskConvert { + + PayNotifyTaskConvert INSTANCE = Mappers.getMapper(PayNotifyTaskConvert.class); + + PayNotifyTaskRespVO convert(PayNotifyTaskDO bean); + + default PageResult convertPage(PageResult page, Map appMap){ + PageResult result = convertPage(page); + result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName()))); + return result; + } + PageResult convertPage(PageResult page); + + default PayNotifyTaskDetailRespVO convert(PayNotifyTaskDO task, PayAppDO app, List logs) { + PayNotifyTaskDetailRespVO respVO = convert(task, logs); + if (app != null) { + respVO.setAppName(app.getName()); + } + return respVO; + } + PayNotifyTaskDetailRespVO convert(PayNotifyTaskDO task, List logs); +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/order/PayOrderConvert.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/order/PayOrderConvert.java new file mode 100644 index 0000000..d4438bc --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/order/PayOrderConvert.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.pay.convert.order; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderRespDTO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.*; +import com.yunxi.scm.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +/** + * 支付订单 Convert + * + * @author aquan + */ +@Mapper +public interface PayOrderConvert { + + PayOrderConvert INSTANCE = Mappers.getMapper(PayOrderConvert.class); + + PayOrderRespVO convert(PayOrderDO bean); + + PayOrderRespDTO convert2(PayOrderDO order); + + default PayOrderDetailsRespVO convert(PayOrderDO order, PayOrderExtensionDO orderExtension, PayAppDO app) { + PayOrderDetailsRespVO respVO = convertDetail(order); + respVO.setExtension(convert(orderExtension)); + if (app != null) { + respVO.setAppName(app.getName()); + } + return respVO; + } + PayOrderDetailsRespVO convertDetail(PayOrderDO bean); + PayOrderDetailsRespVO.PayOrderExtension convert(PayOrderExtensionDO bean); + + default PageResult convertPage(PageResult page, Map appMap) { + PageResult result = convertPage(page); + result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName()))); + return result; + } + PageResult convertPage(PageResult page); + + default List convertList(List list, Map appMap) { + return CollectionUtils.convertList(list, order -> { + PayOrderExcelVO excelVO = convertExcel(order); + MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName())); + return excelVO; + }); + } + PayOrderExcelVO convertExcel(PayOrderDO bean); + + PayOrderDO convert(PayOrderCreateReqDTO bean); + + @Mapping(target = "id", ignore = true) + PayOrderExtensionDO convert(PayOrderSubmitReqVO bean, String userIp); + + PayOrderUnifiedReqDTO convert2(PayOrderSubmitReqVO reqVO, String userIp); + + @Mapping(source = "order.status", target = "status") + PayOrderSubmitRespVO convert(PayOrderDO order, com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO); + + AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/package-info.java new file mode 100644 index 0000000..98191d3 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package com.yunxi.scm.module.pay.convert; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/refund/PayRefundConvert.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/refund/PayRefundConvert.java new file mode 100644 index 0000000..f3ce3fb --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/refund/PayRefundConvert.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.module.pay.convert.refund; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundRespDTO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundDetailsRespVO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundExcelVO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundPageItemRespVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface PayRefundConvert { + + PayRefundConvert INSTANCE = Mappers.getMapper(PayRefundConvert.class); + + + default PayRefundDetailsRespVO convert(PayRefundDO refund, PayAppDO app) { + PayRefundDetailsRespVO respVO = convert(refund); + if (app != null) { + respVO.setAppName(app.getName()); + } + return respVO; + } + PayRefundDetailsRespVO convert(PayRefundDO bean); + PayRefundDetailsRespVO.Order convert(PayOrderDO bean); + + default PageResult convertPage(PageResult page, Map appMap) { + PageResult result = convertPage(page); + result.getList().forEach(order -> MapUtils.findAndThen(appMap, order.getAppId(), app -> order.setAppName(app.getName()))); + return result; + } + PageResult convertPage(PageResult page); + + PayRefundDO convert(PayRefundCreateReqDTO bean); + + PayRefundRespDTO convert02(PayRefundDO bean); + + default List convertList(List list, Map appMap) { + return CollectionUtils.convertList(list, order -> { + PayRefundExcelVO excelVO = convertExcel(order); + MapUtils.findAndThen(appMap, order.getAppId(), app -> excelVO.setAppName(app.getName())); + return excelVO; + }); + } + PayRefundExcelVO convertExcel(PayRefundDO bean); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..19fbece --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/app/PayAppDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/app/PayAppDO.java new file mode 100644 index 0000000..4d86214 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/app/PayAppDO.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.pay.dal.dataobject.app; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 支付应用 DO + * 一个商户下,可能会有多个支付应用。例如说,京东有京东商城、京东到家等等 + * 不过一般来说,一个商户,只有一个应用哈~ + * + * 即 PayMerchantDO : PayAppDO = 1 : n + * + * @author 芋道源码 + */ +@TableName("pay_app") +@KeySequence("pay_app_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayAppDO extends BaseDO { + + /** + * 应用编号,数据库自增 + */ + @TableId + private Long id; + /** + * 应用名 + */ + private String name; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 支付结果的回调地址 + */ + private String orderNotifyUrl; + /** + * 退款结果的回调地址 + */ + private String refundNotifyUrl; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/channel/PayChannelDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/channel/PayChannelDO.java new file mode 100644 index 0000000..e960417 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/channel/PayChannelDO.java @@ -0,0 +1,69 @@ +package com.yunxi.scm.module.pay.dal.dataobject.channel; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.pay.core.client.PayClientConfig; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +/** + * 支付渠道 DO + * 一个应用下,会有多种支付渠道,例如说微信支付、支付宝支付等等 + * + * 即 PayAppDO : PayChannelDO = 1 : n + * + * @author 芋道源码 + */ +@TableName(value = "pay_channel", autoResultMap = true) +@KeySequence("pay_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayChannelDO extends TenantBaseDO { + + /** + * 渠道编号,数据库自增 + */ + private Long id; + /** + * 渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String code; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 渠道费率,单位:百分比 + */ + private Double feeRate; + /** + * 备注 + */ + private String remark; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 支付渠道配置 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private PayClientConfig config; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/demo/PayDemoOrderDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/demo/PayDemoOrderDO.java new file mode 100644 index 0000000..7921b07 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/demo/PayDemoOrderDO.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.module.pay.dal.dataobject.demo; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 示例订单 + * + * 演示业务系统的订单,如何接入 pay 系统的支付与退款 + * + * @author 芋道源码 + */ +@TableName("pay_demo_order") +@KeySequence("pay_demo_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayDemoOrderDO extends BaseDO { + + /** + * 订单编号,自增 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 商品编号 + */ + private Long spuId; + /** + * 商品名称 + */ + private String spuName; + /** + * 价格,单位:分 + */ + private Integer price; + + // ========== 支付相关字段 ========== + + /** + * 是否支付 + */ + private Boolean payStatus; + /** + * 支付订单编号 + * + * 对接 pay-module-biz 支付服务的支付订单编号,即 PayOrderDO 的 id 编号 + */ + private Long payOrderId; + /** + * 付款时间 + */ + private LocalDateTime payTime; + /** + * 支付渠道 + * + * 对应 PayChannelEnum 枚举 + */ + private String payChannelCode; + + // ========== 退款相关字段 ========== + /** + * 支付退款单号 + */ + private Long payRefundId; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + /** + * 退款完成时间 + */ + private LocalDateTime refundTime; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/member/MemberWalletDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/member/MemberWalletDO.java new file mode 100644 index 0000000..1ec63e8 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/member/MemberWalletDO.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.pay.dal.dataobject.member; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +// TODO @jason:修改 MemberWalletDO 为 PayWalletDO +/** + * 支付 - 会员钱包 DO + * + * @author jason + */ +@TableName(value ="pay_member_wallet") +@KeySequence("pay_member_wallet_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class MemberWalletDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + // TODO @jaosn:增加 userType 字段; + /** + * 用户 id + * + * 关联 MemberUserDO 的 id 编号 + * 关联 AdminUserDO 的 id 编号 + */ + private Long userId; + + /** + * 余额, 单位分 + */ + private Integer balance; + + /** + * 累计支出, 单位分 + */ + private Integer totalSpending; + + /** + * 累计充值, 单位分 + */ + private Integer totalTopUp; +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/member/MemberWalletTransactionDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/member/MemberWalletTransactionDO.java new file mode 100644 index 0000000..ed15436 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/member/MemberWalletTransactionDO.java @@ -0,0 +1,86 @@ +package com.yunxi.scm.module.pay.dal.dataobject.member; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.pay.enums.member.WalletOperateTypeEnum; +import com.yunxi.scm.module.pay.enums.member.WalletTransactionGategoryEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 支付-会员钱包明细 DO + * + * @author jason + */ +@TableName(value ="pay_member_wallet_transaction") +@KeySequence("pay_member_wallet_transaction_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +public class MemberWalletTransactionDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + + /** + * 会员钱包 id + * + * 关联 {@link MemberWalletDO#getId()} + */ + private Long walletId; + + /** + * 用户 id + * + * 关联 MemberUserDO 的 id 编号 + */ + private Long userId; + + /** + * 交易单号 @芋艿 这里是关联交易单号, 还是订单号 , 退款单号! ?? + */ + private String tradeNo; + + /** + * 交易分类 + * + * 枚举 {@link WalletTransactionGategoryEnum#getCategory()} + */ + private Integer category; + + /** + * 操作分类 + * + * 枚举 {@link WalletOperateTypeEnum#getType()} + */ + private Integer operateType; + + /** + * 操作详情 + */ + private String operateDesc; + + /** + * 交易金额, 单位分 + */ + private Integer price; + + /** + * 余额, 单位分 + */ + private Integer balance; + + /** + * 备注 + */ + private String mark; + + /** + * 交易时间 + */ + private LocalDateTime transactionTime; +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/notify/PayNotifyLogDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/notify/PayNotifyLogDO.java new file mode 100644 index 0000000..32c89d1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/notify/PayNotifyLogDO.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.module.pay.dal.dataobject.notify; + +import com.yunxi.scm.module.pay.enums.notify.PayNotifyStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 商户支付、退款等的通知 Log + * 每次通知时,都会在该表中,记录一次 Log,方便排查问题 + * + * @author 芋道源码 + */ +@TableName("pay_notify_log") +@KeySequence("pay_notify_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayNotifyLogDO extends BaseDO { + + /** + * 日志编号,自增 + */ + private Long id; + /** + * 通知任务编号 + * + * 关联 {@link PayNotifyTaskDO#getId()} + */ + private Long taskId; + /** + * 第几次被通知 + * + * 对应到 {@link PayNotifyTaskDO#getNotifyTimes()} + */ + private Integer notifyTimes; + /** + * HTTP 响应结果 + */ + private String response; + /** + * 支付通知状态 + * + * 外键 {@link PayNotifyStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java new file mode 100644 index 0000000..f62f33b --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/notify/PayNotifyTaskDO.java @@ -0,0 +1,96 @@ +package com.yunxi.scm.module.pay.dal.dataobject.notify; + +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyStatusEnum; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; + +/** + * 支付通知 + * 在支付系统收到支付渠道的支付、退款的结果后,需要不断的通知到业务系统,直到成功。 + * + * @author 芋道源码 + */ +@TableName("pay_notify_task") +@KeySequence("pay_notify_task_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class PayNotifyTaskDO extends TenantBaseDO { + + /** + * 通知频率,单位为秒。 + * + * 算上首次的通知,实际是一共 1 + 8 = 9 次。 + */ + public static final Integer[] NOTIFY_FREQUENCY = new Integer[]{ + 15, 15, 30, 180, + 1800, 1800, 1800, 3600 + }; + + /** + * 编号,自增 + */ + @TableId + private Long id; + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 通知类型 + * + * 外键 {@link PayNotifyTypeEnum} + */ + private Integer type; + /** + * 数据编号,根据不同 type 进行关联: + * + * 1. {@link PayNotifyTypeEnum#ORDER} 时,关联 {@link PayOrderDO#getId()} + * 2. {@link PayNotifyTypeEnum#REFUND} 时,关联 {@link PayRefundDO#getId()} + */ + private Long dataId; + /** + * 商户订单编号 + */ + private String merchantOrderId; + /** + * 通知状态 + * + * 外键 {@link PayNotifyStatusEnum} + */ + private Integer status; + /** + * 下一次通知时间 + */ + private LocalDateTime nextNotifyTime; + /** + * 最后一次执行时间 + */ + private LocalDateTime lastExecuteTime; + /** + * 当前通知次数 + */ + private Integer notifyTimes; + /** + * 最大可通知次数 + */ + private Integer maxNotifyTimes; + /** + * 通知地址 + */ + private String notifyUrl; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/order/PayOrderDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/order/PayOrderDO.java new file mode 100644 index 0000000..e45a39c --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/order/PayOrderDO.java @@ -0,0 +1,138 @@ +package com.yunxi.scm.module.pay.dal.dataobject.order; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 支付订单 DO + * + * @author 芋道源码 + */ +@TableName("pay_order") +@KeySequence("pay_order_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderDO extends BaseDO { + + /** + * 订单编号,数据库自增 + */ + private Long id; + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 渠道编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + + // ========== 商户相关字段 ========== + + /** + * 商户订单编号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantOrderId; + /** + * 商品标题 + */ + private String subject; + /** + * 商品描述信息 + */ + private String body; + /** + * 异步通知地址 + */ + private String notifyUrl; + + // ========== 订单相关字段 ========== + + /** + * 支付金额,单位:分 + */ + private Integer price; + /** + * 渠道手续费,单位:百分比 + * + * 冗余 {@link PayChannelDO#getFeeRate()} + */ + private Double channelFeeRate; + /** + * 渠道手续金额,单位:分 + */ + private Integer channelFeePrice; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + /** + * 用户 IP + */ + private String userIp; + /** + * 订单失效时间 + */ + private LocalDateTime expireTime; + /** + * 订单支付成功时间 + */ + private LocalDateTime successTime; + /** + * 支付成功的订单拓展单编号 + * + * 关联 {@link PayOrderExtensionDO#getId()} + */ + private Long extensionId; + /** + * 支付成功的外部订单号 + * + * 关联 {@link PayOrderExtensionDO#getNo()} + */ + private String no; + + // ========== 退款相关字段 ========== + /** + * 退款总金额,单位:分 + */ + private Integer refundPrice; + + // ========== 渠道相关字段 ========== + /** + * 渠道用户编号 + * + * 例如说,微信 openid、支付宝账号 + */ + private String channelUserId; + /** + * 渠道订单号 + */ + private String channelOrderNo; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/order/PayOrderExtensionDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/order/PayOrderExtensionDO.java new file mode 100644 index 0000000..f24e1c2 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/order/PayOrderExtensionDO.java @@ -0,0 +1,96 @@ +package com.yunxi.scm.module.pay.dal.dataobject.order; + +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.Map; + +/** + * 支付订单拓展 DO + * + * 每次调用支付渠道,都会生成一条对应记录 + * + * @author 芋道源码 + */ +@TableName(value = "pay_order_extension",autoResultMap = true) +@KeySequence("pay_order_extension_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayOrderExtensionDO extends BaseDO { + + /** + * 订单拓展编号,数据库自增 + */ + private Long id; + /** + * 外部订单号,根据规则生成 + * + * 调用支付渠道时,使用该字段作为对接的订单号: + * 1. 微信支付:对应 JSAPI 支付 的 out_trade_no 字段 + * 2. 支付宝支付:对应 电脑网站支付 的 out_trade_no 字段 + * + * 例如说,P202110132239124200055 + */ + private String no; + /** + * 订单号 + * + * 关联 {@link PayOrderDO#getId()} + */ + private Long orderId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 渠道编码 + */ + private String channelCode; + /** + * 用户 IP + */ + private String userIp; + /** + * 支付状态 + * + * 枚举 {@link PayOrderStatusEnum} + */ + private Integer status; + /** + * 支付渠道的额外参数 + * + * 参见 参数说明 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map channelExtras; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道报错时,错误信息 + */ + private String channelErrorMsg; + + /** + * 支付渠道的同步/异步通知的内容 + * + * 对应 {@link PayOrderRespDTO#getRawData()} + */ + private String channelNotifyData; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/refund/PayRefundDO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/refund/PayRefundDO.java new file mode 100644 index 0000000..5aac20b --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/dataobject/refund/PayRefundDO.java @@ -0,0 +1,160 @@ +package com.yunxi.scm.module.pay.dal.dataobject.refund; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.enums.refund.PayRefundStatusEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 支付退款单 DO + * 一个支付订单,可以拥有多个支付退款单 + * + * 即 PayOrderDO : PayRefundDO = 1 : n + * + * @author 芋道源码 + */ +@TableName("pay_refund") +@KeySequence("pay_refund_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayRefundDO extends BaseDO { + + /** + * 退款单编号,数据库自增 + */ + @TableId + private Long id; + /** + * 外部退款号,根据规则生成 + * + * 调用支付渠道时,使用该字段作为对接的退款号: + * 1. 微信退款:对应 申请退款 的 out_refund_no 字段 + * 2. 支付宝退款:对应 的 out_request_no 字段 + */ + private String no; + + /** + * 应用编号 + * + * 关联 {@link PayAppDO#getId()} + */ + private Long appId; + /** + * 渠道编号 + * + * 关联 {@link PayChannelDO#getId()} + */ + private Long channelId; + /** + * 商户编码 + * + * 枚举 {@link PayChannelEnum} + */ + private String channelCode; + /** + * 订单编号 + * + * 关联 {@link PayOrderDO#getId()} + */ + private Long orderId; + /** + * 支付订单编号 + * + * 冗余 {@link PayOrderDO#getNo()} + */ + private String orderNo; + + // ========== 商户相关字段 ========== + /** + * 商户订单编号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantOrderId; + /** + * 商户退款订单号 + * + * 例如说,内部系统 A 的订单号,需要保证每个 PayAppDO 唯一 + */ + private String merchantRefundId; + /** + * 异步通知地址 + */ + private String notifyUrl; + + // ========== 退款相关字段 ========== + /** + * 退款状态 + * + * 枚举 {@link PayRefundStatusEnum} + */ + private Integer status; + + /** + * 支付金额,单位:分 + */ + private Integer payPrice; + /** + * 退款金额,单位:分 + */ + private Integer refundPrice; + + /** + * 退款原因 + */ + private String reason; + + /** + * 用户 IP + */ + private String userIp; + + // ========== 渠道相关字段 ========== + /** + * 渠道订单号 + * + * 冗余 {@link PayOrderDO#getChannelOrderNo()} + */ + private String channelOrderNo; + /** + * 渠道退款单号 + * + * 1. 微信退款:对应 申请退款 的 refund_id 字段 + * 2. 支付宝退款:没有字段 + */ + private String channelRefundNo; + /** + * 退款成功时间 + */ + private LocalDateTime successTime; + + /** + * 调用渠道的错误码 + */ + private String channelErrorCode; + /** + * 调用渠道的错误提示 + */ + private String channelErrorMsg; + + /** + * 支付渠道的同步/异步通知的内容 + * + * 对应 {@link PayRefundRespDTO#getRawData()} + */ + private String channelNotifyData; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/app/PayAppMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/app/PayAppMapper.java new file mode 100644 index 0000000..bfe5f51 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/app/PayAppMapper.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.pay.dal.mysql.app; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.framework.mybatis.core.query.QueryWrapperX; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PayAppMapper extends BaseMapperX { + + default PageResult selectPage(PayAppPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(PayAppDO::getName, reqVO.getName()) + .eqIfPresent(PayAppDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayAppDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayAppDO::getId)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/channel/PayChannelMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/channel/PayChannelMapper.java new file mode 100644 index 0000000..85c6c03 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/channel/PayChannelMapper.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.pay.dal.mysql.channel; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PayChannelMapper extends BaseMapperX { + + default PayChannelDO selectByAppIdAndCode(Long appId, String code) { + return selectOne(PayChannelDO::getAppId, appId, PayChannelDO::getCode, code); + } + + default List selectListByAppIds(Collection appIds){ + return selectList(PayChannelDO::getAppId, appIds); + } + + default List selectListByAppId(Long appId, Integer status) { + return selectList(new LambdaQueryWrapperX() + .eq(PayChannelDO::getAppId, appId) + .eq(PayChannelDO::getStatus, status)); + } + + @Select("SELECT COUNT(*) FROM pay_channel WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxUpdateTime); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/demo/PayDemoOrderMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/demo/PayDemoOrderMapper.java new file mode 100644 index 0000000..e127bb1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/demo/PayDemoOrderMapper.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.pay.dal.mysql.demo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 示例订单 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface PayDemoOrderMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .orderByDesc(PayDemoOrderDO::getId)); + } + + default int updateByIdAndPayed(Long id, boolean wherePayed, PayDemoOrderDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(PayDemoOrderDO::getId, id).eq(PayDemoOrderDO::getPayStatus, wherePayed)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/member/MemberWalletMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/member/MemberWalletMapper.java new file mode 100644 index 0000000..c763d97 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/member/MemberWalletMapper.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.pay.dal.mysql.member; + + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.pay.dal.dataobject.member.MemberWalletDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MemberWalletMapper extends BaseMapperX { + +} + + + + diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/member/MemberWalletTransactionMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/member/MemberWalletTransactionMapper.java new file mode 100644 index 0000000..97adf9d --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/member/MemberWalletTransactionMapper.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.pay.dal.mysql.member; + + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.pay.dal.dataobject.member.MemberWalletTransactionDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MemberWalletTransactionMapper extends BaseMapperX { + +} + + + + diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/notify/PayNotifyLogMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/notify/PayNotifyLogMapper.java new file mode 100644 index 0000000..842074c --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/notify/PayNotifyLogMapper.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.pay.dal.mysql.notify; + +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayNotifyLogMapper extends BaseMapperX { + + default List selectListByTaskId(Long taskId) { + return selectList(PayNotifyLogDO::getTaskId, taskId); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java new file mode 100644 index 0000000..5833a66 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/notify/PayNotifyTaskMapper.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.pay.dal.mysql.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyStatusEnum; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayNotifyTaskMapper extends BaseMapperX { + + /** + * 获得需要通知的 PayNotifyTaskDO 记录。需要满足如下条件: + * + * 1. status 非成功 + * 2. nextNotifyTime 小于当前时间 + * + * @return PayTransactionNotifyTaskDO 数组 + */ + default List selectListByNotify() { + return selectList(new LambdaQueryWrapper() + .in(PayNotifyTaskDO::getStatus, PayNotifyStatusEnum.WAITING.getStatus(), + PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()) + .le(PayNotifyTaskDO::getNextNotifyTime, LocalDateTime.now())); + } + + default PageResult selectPage(PayNotifyTaskPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayNotifyTaskDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayNotifyTaskDO::getType, reqVO.getType()) + .eqIfPresent(PayNotifyTaskDO::getDataId, reqVO.getDataId()) + .eqIfPresent(PayNotifyTaskDO::getStatus, reqVO.getStatus()) + .eqIfPresent(PayNotifyTaskDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .betweenIfPresent(PayNotifyTaskDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayNotifyTaskDO::getId)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/order/PayOrderExtensionMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/order/PayOrderExtensionMapper.java new file mode 100644 index 0000000..a954c07 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/order/PayOrderExtensionMapper.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.pay.dal.mysql.order; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayOrderExtensionMapper extends BaseMapperX { + + default PayOrderExtensionDO selectByNo(String no) { + return selectOne(PayOrderExtensionDO::getNo, no); + } + + default int updateByIdAndStatus(Long id, Integer status, PayOrderExtensionDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayOrderExtensionDO::getId, id).eq(PayOrderExtensionDO::getStatus, status)); + } + + default List selectListByOrderId(Long orderId) { + return selectList(PayOrderExtensionDO::getOrderId, orderId); + } + + default List selectListByStatusAndCreateTimeGe(Integer status, LocalDateTime minCreateTime) { + return selectList(new LambdaQueryWrapper() + .eq(PayOrderExtensionDO::getStatus, status) + .ge(PayOrderExtensionDO::getCreateTime, minCreateTime)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/order/PayOrderMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/order/PayOrderMapper.java new file mode 100644 index 0000000..118f4ac --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/order/PayOrderMapper.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.module.pay.dal.mysql.order; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface PayOrderMapper extends BaseMapperX { + + default PageResult selectPage(PayOrderPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayOrderDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayOrderDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayOrderDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayOrderDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(PayOrderDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayOrderDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayOrderDO::getId)); + } + + default List selectList(PayOrderExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(PayOrderDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayOrderDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayOrderDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayOrderDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayOrderDO::getNo, reqVO.getNo()) + .eqIfPresent(PayOrderDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayOrderDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayOrderDO::getId)); + } + + default Long selectCountByAppId(Long appId) { + return selectCount(PayOrderDO::getAppId, appId); + } + + default PayOrderDO selectByAppIdAndMerchantOrderId(Long appId, String merchantOrderId) { + return selectOne(PayOrderDO::getAppId, appId, + PayOrderDO::getMerchantOrderId, merchantOrderId); + } + + default int updateByIdAndStatus(Long id, Integer status, PayOrderDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayOrderDO::getId, id).eq(PayOrderDO::getStatus, status)); + } + + default List selectListByStatusAndExpireTimeLt(Integer status, LocalDateTime expireTime) { + return selectList(new LambdaQueryWrapper() + .eq(PayOrderDO::getStatus, status) + .lt(PayOrderDO::getExpireTime, expireTime)); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/refund/PayRefundMapper.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/refund/PayRefundMapper.java new file mode 100644 index 0000000..97d43a5 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/mysql/refund/PayRefundMapper.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.pay.dal.mysql.refund; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PayRefundMapper extends BaseMapperX { + + default Long selectCountByAppId(Long appId) { + return selectCount(PayRefundDO::getAppId, appId); + } + + default PayRefundDO selectByAppIdAndMerchantRefundId(Long appId, String merchantRefundId) { + return selectOne(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getMerchantRefundId, merchantRefundId)); + } + + default Long selectCountByAppIdAndOrderId(Long appId, Long orderId, Integer status) { + return selectCount(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getOrderId, orderId) + .eq(PayRefundDO::getStatus, status)); + } + + default PayRefundDO selectByAppIdAndNo(Long appId, String no) { + return selectOne(new LambdaQueryWrapperX() + .eq(PayRefundDO::getAppId, appId) + .eq(PayRefundDO::getNo, no)); + } + + default int updateByIdAndStatus(Long id, Integer status, PayRefundDO update) { + return update(update, new LambdaQueryWrapper() + .eq(PayRefundDO::getId, id).eq(PayRefundDO::getStatus, status)); + } + + default PageResult selectPage(PayRefundPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(PayRefundDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayRefundDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayRefundDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayRefundDO::getMerchantRefundId, reqVO.getMerchantRefundId()) + .likeIfPresent(PayRefundDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayRefundDO::getChannelRefundNo, reqVO.getChannelRefundNo()) + .eqIfPresent(PayRefundDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayRefundDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayRefundDO::getId)); + } + + default List selectList(PayRefundExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(PayRefundDO::getAppId, reqVO.getAppId()) + .eqIfPresent(PayRefundDO::getChannelCode, reqVO.getChannelCode()) + .likeIfPresent(PayRefundDO::getMerchantOrderId, reqVO.getMerchantOrderId()) + .likeIfPresent(PayRefundDO::getMerchantRefundId, reqVO.getMerchantRefundId()) + .likeIfPresent(PayRefundDO::getChannelOrderNo, reqVO.getChannelOrderNo()) + .likeIfPresent(PayRefundDO::getChannelRefundNo, reqVO.getChannelRefundNo()) + .eqIfPresent(PayRefundDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(PayRefundDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(PayRefundDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(PayRefundDO::getStatus, status); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/RedisKeyConstants.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/RedisKeyConstants.java new file mode 100644 index 0000000..ccf7c79 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/RedisKeyConstants.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.pay.dal.redis; + +/** + * 支付 Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 通知任务的分布式锁 + * + * KEY 格式:pay_notify:lock:%d // 参数来自 DefaultLockKeyBuilder 类 + * VALUE 数据格式:HASH // RLock.class:Redisson 的 Lock 锁,使用 Hash 数据结构 + * 过期时间:不固定 + */ + String PAY_NOTIFY_LOCK = "pay_notify:lock:%d"; + + /** + * 支付序号的缓存 + * + * KEY 格式:pay_no:{prefix} + * VALUE 数据格式:编号自增 + */ + String PAY_NO = "pay_no"; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/no/PayNoRedisDAO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/no/PayNoRedisDAO.java new file mode 100644 index 0000000..b4d4098 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/no/PayNoRedisDAO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.pay.dal.redis.no; + +import cn.hutool.core.date.DatePattern;import cn.hutool.core.date.DateUtil;import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource;import java.time.LocalDateTime; + +/** + * 支付序号的 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayNoRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 生成序号 + * + * @param prefix 前缀 + * @return 序号 + */ + public String generate(String prefix) { + String noPrefix = prefix + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_PATTERN); + Long no = stringRedisTemplate.opsForValue().increment(noPrefix); + return noPrefix + no; + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java new file mode 100644 index 0000000..fcf35ac --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/dal/redis/notify/PayNotifyLockRedisDAO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.pay.dal.redis.notify; + +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.module.pay.dal.redis.RedisKeyConstants.PAY_NOTIFY_LOCK; + +/** + * 支付通知的锁 Redis DAO + * + * @author 芋道源码 + */ +@Repository +public class PayNotifyLockRedisDAO { + + @Resource + private RedissonClient redissonClient; + + public void lock(Long id, Long timeoutMillis, Runnable runnable) { + String lockKey = formatKey(id); + RLock lock = redissonClient.getLock(lockKey); + try { + lock.lock(timeoutMillis, TimeUnit.MILLISECONDS); + // 执行逻辑 + runnable.run(); + } finally { + lock.unlock(); + } + } + + private static String formatKey(Long id) { + return String.format(PAY_NOTIFY_LOCK, id); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/job/config/PayJobConfiguration.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/job/config/PayJobConfiguration.java new file mode 100644 index 0000000..1c75664 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/job/config/PayJobConfiguration.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.pay.framework.job.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.ThreadPoolExecutor; + +@Configuration(proxyBeanMethods = false) +public class PayJobConfiguration { + + public static final String NOTIFY_THREAD_POOL_TASK_EXECUTOR = "NOTIFY_THREAD_POOL_TASK_EXECUTOR"; + + @Bean(NOTIFY_THREAD_POOL_TASK_EXECUTOR) + public ThreadPoolTaskExecutor notifyThreadPoolTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(8); // 设置核心线程数 + executor.setMaxPoolSize(16); // 设置最大线程数 + executor.setKeepAliveSeconds(60); // 设置空闲时间 + executor.setQueueCapacity(100); // 设置队列大小 + executor.setThreadNamePrefix("notify-task-"); // 配置线程池的前缀 + executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); + // 进行加载 + executor.initialize(); + return executor; + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/job/core/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/job/core/package-info.java new file mode 100644 index 0000000..ad93bdd --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/job/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.pay.framework.job.core; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/package-info.java new file mode 100644 index 0000000..bb4421b --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 pay 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.pay.framework; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/config/PayConfiguration.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/config/PayConfiguration.java new file mode 100644 index 0000000..d3b72a8 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/config/PayConfiguration.java @@ -0,0 +1,9 @@ +package com.yunxi.scm.module.pay.framework.pay.config; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(PayProperties.class) +public class PayConfiguration { +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/config/PayProperties.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/config/PayProperties.java new file mode 100644 index 0000000..df1d2c7 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/config/PayProperties.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.pay.framework.pay.config; + +import lombok.Data; +import org.hibernate.validator.constraints.URL; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotEmpty; + +@ConfigurationProperties(prefix = "yunxi.pay") +@Validated +@Data +public class PayProperties { + + private static final String ORDER_NO_PREFIX = "P"; + private static final String REFUND_NO_PREFIX = "R"; + + /** + * 支付回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyOrder 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => yunxi-module-pay 的 orderNotifyUrl 地址 => 业务的 PayAppDO.orderNotifyUrl 地址 + */ + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String orderNotifyUrl; + + /** + * 退款回调地址 + * + * 实际上,对应的 PayNotifyController 的 notifyRefund 方法的 URL + * + * 回调顺序:支付渠道(支付宝支付、微信支付) => yunxi-module-pay 的 refundNotifyUrl 地址 => 业务的 PayAppDO.notifyRefundUrl 地址 + */ + @NotEmpty(message = "支付回调地址不能为空") + @URL(message = "支付回调地址的格式必须是 URL") + private String refundNotifyUrl; + + /** + * 支付订单 no 的前缀 + */ + @NotEmpty(message = "支付订单 no 的前缀不能为空") + private String orderNoPrefix = ORDER_NO_PREFIX; + + /** + * 退款订单 no 的前缀 + */ + @NotEmpty(message = "退款订单 no 的前缀不能为空") + private String refundNoPrefix = REFUND_NO_PREFIX; + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/core/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/core/package-info.java new file mode 100644 index 0000000..c4afdb1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/pay/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,无实际作用 + */ +package com.yunxi.scm.module.pay.framework.pay.core; \ No newline at end of file diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/web/config/PayWebConfiguration.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/web/config/PayWebConfiguration.java new file mode 100644 index 0000000..78787aa --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/web/config/PayWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.pay.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * pay 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class PayWebConfiguration { + + /** + * pay 模块的 API 分组 + */ + @Bean + public GroupedOpenApi payGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("pay"); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/web/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/web/package-info.java new file mode 100644 index 0000000..f0f0e96 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * pay 模块的 web 配置 + */ +package com.yunxi.scm.module.pay.framework.web; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/notify/PayNotifyJob.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/notify/PayNotifyJob.java new file mode 100644 index 0000000..5d0aade --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/notify/PayNotifyJob.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.pay.job.notify; + +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.framework.tenant.core.job.TenantJob; +import com.yunxi.scm.module.pay.service.notify.PayNotifyService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 支付通知 Job + * 通过不断扫描待通知的 PayNotifyTaskDO 记录,回调业务线的回调接口 + * + * @author 芋道源码 + */ +@Component +@TenantJob // 多租户 +@Slf4j +public class PayNotifyJob implements JobHandler { + + @Resource + private PayNotifyService payNotifyService; + + @Override + public String execute(String param) throws Exception { + int notifyCount = payNotifyService.executeNotify(); + return String.format("执行支付通知 %s 个", notifyCount); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/order/PayOrderExpireJob.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/order/PayOrderExpireJob.java new file mode 100644 index 0000000..a036ae1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/order/PayOrderExpireJob.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.pay.job.order; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.framework.tenant.core.job.TenantJob; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 支付订单的过期 Job + * + * 支付超过过期时间时,支付渠道是不会通知进行过期,所以需要定时进行过期关闭。 + * + * @author 芋道源码 + */ +@Component +@TenantJob +public class PayOrderExpireJob implements JobHandler { + + @Resource + private PayOrderService orderService; + + @Override + public String execute(String param) { + int count = orderService.expireOrder(); + return StrUtil.format("支付过期 {} 个", count); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/order/PayOrderSyncJob.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/order/PayOrderSyncJob.java new file mode 100644 index 0000000..da59b78 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/order/PayOrderSyncJob.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.pay.job.order; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.framework.tenant.core.job.TenantJob; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; + +/** + * 支付订单的同步 Job + * + * 由于支付订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author 芋道源码 + */ +@Component +@TenantJob +public class PayOrderSyncJob implements JobHandler { + + /** + * 同步创建时间在 N 分钟之前的订单 + * + * 为什么同步 10 分钟之前的订单? + * 因为一个订单发起支付,到支付成功,大多数在 10 分钟内,需要保证轮询到。 + * 如果设置为 30、60 或者更大时间范围,会导致轮询的订单太多,影响性能。当然,你也可以根据自己的业务情况来处理。 + */ + private static final Duration CREATE_TIME_DURATION_BEFORE = Duration.ofMinutes(10); + + @Resource + private PayOrderService orderService; + + @Override + public String execute(String param) { + LocalDateTime minCreateTime = LocalDateTime.now().minus(CREATE_TIME_DURATION_BEFORE); + int count = orderService.syncOrder(minCreateTime); + return StrUtil.format("同步支付订单 {} 个", count); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/refund/PayRefundSyncJob.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/refund/PayRefundSyncJob.java new file mode 100644 index 0000000..026e4a2 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/job/refund/PayRefundSyncJob.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.pay.job.refund; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.framework.tenant.core.job.TenantJob; +import com.yunxi.scm.module.pay.service.refund.PayRefundService; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 退款订单的同步 Job + * + * 由于退款订单的状态,是由支付渠道异步通知进行同步,考虑到异步通知可能会失败(小概率),所以需要定时进行同步。 + * + * @author 芋道源码 + */ +@Component +@TenantJob +public class PayRefundSyncJob implements JobHandler { + + @Resource + private PayRefundService refundService; + + @Override + public String execute(String param) { + int count = refundService.syncRefund(); + return StrUtil.format("同步退款订单 {} 个", count); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/package-info.java new file mode 100644 index 0000000..f271833 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/package-info.java @@ -0,0 +1,10 @@ +/** + * pay 模块,我们放支付业务,提供业务的支付能力。 + * 例如说:商户、应用、支付、退款等等 + * + * 1. Controller URL:以 /pay/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 pay_ 开头,方便在数据库中区分 + * + * 注意,由于 Pay 模块和 Trade 模块,容易重名,所以类名都加载 Pay 的前缀~ + */ +package com.yunxi.scm.module.pay; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/app/PayAppService.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/app/PayAppService.java new file mode 100644 index 0000000..28a7607 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/app/PayAppService.java @@ -0,0 +1,105 @@ +package com.yunxi.scm.module.pay.service.app; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 支付应用 Service 接口 + * + * @author 芋艿 + */ +public interface PayAppService { + + /** + * 创建支付应用 + * + * @param createReqVO 创建 + * @return 编号 + */ + Long createApp(@Valid PayAppCreateReqVO createReqVO); + + /** + * 更新支付应用 + * + * @param updateReqVO 更新 + */ + void updateApp(@Valid PayAppUpdateReqVO updateReqVO); + + /** + * 修改应用状态 + * + * @param id 应用编号 + * @param status 状态 + */ + void updateAppStatus(Long id, Integer status); + + /** + * 删除支付应用 + * + * @param id 编号 + */ + void deleteApp(Long id); + + /** + * 获得支付应用 + * + * @param id 编号 + * @return 支付应用 + */ + PayAppDO getApp(Long id); + + /** + * 获得支付应用列表 + * + * @param ids 编号 + * @return 支付应用列表 + */ + List getAppList(Collection ids); + + /** + * 获得支付应用列表 + * + * @return 支付应用列表 + */ + List getAppList(); + + /** + * 获得支付应用分页 + * + * @param pageReqVO 分页查询 + * @return 支付应用分页 + */ + PageResult getAppPage(PayAppPageReqVO pageReqVO); + + /** + * 获得指定编号的商户 Map + * + * @param ids 应用编号集合 + * @return 商户 Map + */ + default Map getAppMap(Collection ids) { + List list = getAppList(ids); + return CollectionUtils.convertMap(list, PayAppDO::getId); + } + + /** + * 支付应用的合法性 + * + * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param id 应用编号 + * @return 应用 + */ + PayAppDO validPayApp(Long id); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/app/PayAppServiceImpl.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/app/PayAppServiceImpl.java new file mode 100644 index 0000000..13a6272 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/app/PayAppServiceImpl.java @@ -0,0 +1,126 @@ +package com.yunxi.scm.module.pay.service.app; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import com.yunxi.scm.module.pay.convert.app.PayAppConvert; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.mysql.app.PayAppMapper; +import com.yunxi.scm.module.pay.enums.ErrorCodeConstants; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import com.yunxi.scm.module.pay.service.refund.PayRefundService; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付应用 Service 实现类 + * + * @author aquan + */ +@Service +@Validated +public class PayAppServiceImpl implements PayAppService { + + @Resource + private PayAppMapper appMapper; + + @Resource + @Lazy // 延迟加载,避免循环依赖报错 + private PayOrderService orderService; + @Resource + @Lazy // 延迟加载,避免循环依赖报错 + private PayRefundService refundService; + + @Override + public Long createApp(PayAppCreateReqVO createReqVO) { + // 插入 + PayAppDO app = PayAppConvert.INSTANCE.convert(createReqVO); + appMapper.insert(app); + // 返回 + return app.getId(); + } + + @Override + public void updateApp(PayAppUpdateReqVO updateReqVO) { + // 校验存在 + validateAppExists(updateReqVO.getId()); + // 更新 + PayAppDO updateObj = PayAppConvert.INSTANCE.convert(updateReqVO); + appMapper.updateById(updateObj); + } + + @Override + public void updateAppStatus(Long id, Integer status) { + // 校验商户存在 + validateAppExists(id); + // 更新状态 + appMapper.updateById(new PayAppDO().setId(id).setStatus(status)); + } + + @Override + public void deleteApp(Long id) { + // 校验存在 + validateAppExists(id); + // 校验关联数据是否存在 + if (orderService.getOrderCountByAppId(id) > 0) { + throw exception(APP_EXIST_ORDER_CANT_DELETE); + } + if (refundService.getRefundCountByAppId(id) > 0) { + throw exception(APP_EXIST_REFUND_CANT_DELETE); + } + + // 删除 + appMapper.deleteById(id); + } + + private void validateAppExists(Long id) { + if (appMapper.selectById(id) == null) { + throw exception(APP_NOT_FOUND); + } + } + + @Override + public PayAppDO getApp(Long id) { + return appMapper.selectById(id); + } + + @Override + public List getAppList(Collection ids) { + return appMapper.selectBatchIds(ids); + } + + @Override + public List getAppList() { + return appMapper.selectList(); + } + + @Override + public PageResult getAppPage(PayAppPageReqVO pageReqVO) { + return appMapper.selectPage(pageReqVO); + } + + @Override + public PayAppDO validPayApp(Long id) { + PayAppDO app = appMapper.selectById(id); + // 校验是否存在 + if (app == null) { + throw exception(ErrorCodeConstants.APP_NOT_FOUND); + } + // 校验是否禁用 + if (CommonStatusEnum.DISABLE.getStatus().equals(app.getStatus())) { + throw exception(ErrorCodeConstants.APP_IS_DISABLE); + } + return app; + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/channel/PayChannelService.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/channel/PayChannelService.java new file mode 100644 index 0000000..beccf20 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/channel/PayChannelService.java @@ -0,0 +1,95 @@ +package com.yunxi.scm.module.pay.service.channel; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; + +/** + * 支付渠道 Service 接口 + * + * @author aquan + */ +public interface PayChannelService { + + /** + * 创建支付渠道 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createChannel(@Valid PayChannelCreateReqVO createReqVO); + + /** + * 更新支付渠道 + * + * @param updateReqVO 更新信息 + */ + void updateChannel(@Valid PayChannelUpdateReqVO updateReqVO); + + /** + * 删除支付渠道 + * + * @param id 编号 + */ + void deleteChannel(Long id); + + /** + * 获得支付渠道 + * + * @param id 编号 + * @return 支付渠道 + */ + PayChannelDO getChannel(Long id); + + /** + * 根据支付应用 ID 集合,获得支付渠道列表 + * + * @param appIds 应用编号集合 + * @return 支付渠道列表 + */ + List getChannelListByAppIds(Collection appIds); + + /** + * 根据条件获取渠道 + * + * @param appId 应用编号 + * @param code 渠道编码 + * @return 数量 + */ + PayChannelDO getChannelByAppIdAndCode(Long appId, String code); + + /** + * 支付渠道的合法性 + * + * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param id 渠道编号 + * @return 渠道信息 + */ + PayChannelDO validPayChannel(Long id); + + /** + * 支付渠道的合法性 + * + * 如果不合法,抛出 {@link ServiceException} 业务异常 + * + * @param appId 应用编号 + * @param code 支付渠道 + * @return 渠道信息 + */ + PayChannelDO validPayChannel(Long appId, String code); + + /** + * 获得指定应用的开启的渠道列表 + * + * @param appId 应用编号 + * @return 渠道列表 + */ + List getEnableChannelList(Long appId); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/channel/PayChannelServiceImpl.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/channel/PayChannelServiceImpl.java new file mode 100644 index 0000000..839bf38 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/channel/PayChannelServiceImpl.java @@ -0,0 +1,225 @@ +package com.yunxi.scm.module.pay.service.channel; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.pay.core.client.PayClientConfig; +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.tenant.core.util.TenantUtils; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.yunxi.scm.module.pay.convert.channel.PayChannelConvert; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.dal.mysql.channel.PayChannelMapper; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import javax.validation.Validator; +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付渠道 Service 实现类 + * + * @author aquan + */ +@Service +@Slf4j +@Validated +public class PayChannelServiceImpl implements PayChannelService { + + @Getter // 为了方便测试,这里提供 getter 方法 + @Setter + private volatile List channelCache; + + @Resource + private PayClientFactory payClientFactory; + + @Resource + private PayChannelMapper channelMapper; + + @Resource + private Validator validator; + + /** + * 初始化 {@link #payClientFactory} 缓存 + */ + @PostConstruct + public void initLocalCache() { + // 注意:忽略自动多租户,因为要全局初始化缓存 + TenantUtils.executeIgnore(() -> { + // 第一步:查询数据 + List channels = Collections.emptyList(); + try { + channels = channelMapper.selectList(); + } catch (Throwable ex) { + if (!ex.getMessage().contains("doesn't exist")) { + throw ex; + } + log.error("[支付模块 yunxi-module-pay - 表结构未导入][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + } + log.info("[initLocalCache][缓存支付渠道,数量为:{}]", channels.size()); + + // 第二步:构建缓存:创建或更新支付 Client + channels.forEach(payChannel -> payClientFactory.createOrUpdatePayClient(payChannel.getId(), + payChannel.getCode(), payChannel.getConfig())); + this.channelCache = channels; + }); + } + + /** + * 通过定时任务轮询,刷新缓存 + * + * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 + */ + @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) + public void refreshLocalCache() { + // 注意:忽略自动多租户,因为要全局初始化缓存 + TenantUtils.executeIgnore(() -> { + // 情况一:如果缓存里没有数据,则直接刷新缓存 + if (CollUtil.isEmpty(channelCache)) { + initLocalCache(); + return; + } + + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 + LocalDateTime maxTime = CollectionUtils.getMaxValue(channelCache, PayChannelDO::getUpdateTime); + if (channelMapper.selectCountByUpdateTimeGt(maxTime) > 0) { + initLocalCache(); + } + }); + } + + @Override + public Long createChannel(PayChannelCreateReqVO reqVO) { + // 断言是否有重复的 + PayChannelDO dbChannel = getChannelByAppIdAndCode(reqVO.getAppId(), reqVO.getCode()); + if (dbChannel != null) { + throw exception(CHANNEL_EXIST_SAME_CHANNEL_ERROR); + } + + // 新增渠道 + PayChannelDO channel = PayChannelConvert.INSTANCE.convert(reqVO) + .setConfig(parseConfig(reqVO.getCode(), reqVO.getConfig())); + channelMapper.insert(channel); + + // 刷新缓存 + initLocalCache(); + return channel.getId(); + } + + @Override + public void updateChannel(PayChannelUpdateReqVO updateReqVO) { + // 校验存在 + PayChannelDO dbChannel = validateChannelExists(updateReqVO.getId()); + + // 更新 + PayChannelDO channel = PayChannelConvert.INSTANCE.convert(updateReqVO) + .setConfig(parseConfig(dbChannel.getCode(), updateReqVO.getConfig())); + channelMapper.updateById(channel); + + // 刷新缓存 + initLocalCache(); + } + + /** + * 解析并校验配置 + * + * @param code 渠道编码 + * @param configStr 配置 + * @return 支付配置 + */ + private PayClientConfig parseConfig(String code, String configStr) { + // 解析配置 + Class payClass = PayChannelEnum.getByCode(code).getConfigClass(); + if (ObjectUtil.isNull(payClass)) { + throw exception(CHANNEL_NOT_FOUND); + } + PayClientConfig config = JsonUtils.parseObject2(configStr, payClass); + Assert.notNull(config); + + // 验证参数 + config.validate(validator); + return config; + } + + @Override + public void deleteChannel(Long id) { + // 校验存在 + validateChannelExists(id); + + // 删除 + channelMapper.deleteById(id); + + // 刷新缓存 + initLocalCache(); + } + + private PayChannelDO validateChannelExists(Long id) { + PayChannelDO channel = channelMapper.selectById(id); + if (channel == null) { + throw exception(CHANNEL_NOT_FOUND); + } + return channel; + } + + @Override + public PayChannelDO getChannel(Long id) { + return channelMapper.selectById(id); + } + + @Override + public List getChannelListByAppIds(Collection appIds) { + return channelMapper.selectListByAppIds(appIds); + } + + @Override + public PayChannelDO getChannelByAppIdAndCode(Long appId, String code) { + return channelMapper.selectByAppIdAndCode(appId, code); + } + + @Override + public PayChannelDO validPayChannel(Long id) { + PayChannelDO channel = channelMapper.selectById(id); + validPayChannel(channel); + return channel; + } + + @Override + public PayChannelDO validPayChannel(Long appId, String code) { + PayChannelDO channel = channelMapper.selectByAppIdAndCode(appId, code); + validPayChannel(channel); + return channel; + } + + private void validPayChannel(PayChannelDO channel) { + if (channel == null) { + throw exception(CHANNEL_NOT_FOUND); + } + if (CommonStatusEnum.DISABLE.getStatus().equals(channel.getStatus())) { + throw exception(CHANNEL_IS_DISABLE); + } + } + + @Override + public List getEnableChannelList(Long appId) { + return channelMapper.selectListByAppId(appId, CommonStatusEnum.ENABLE.getStatus()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/demo/PayDemoOrderService.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/demo/PayDemoOrderService.java new file mode 100644 index 0000000..b6afc84 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/demo/PayDemoOrderService.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.module.pay.service.demo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.demo.PayDemoOrderDO; + +import javax.validation.Valid; + +/** + * 示例订单 Service 接口 + * + * @author 芋道源码 + */ +public interface PayDemoOrderService { + + /** + * 创建示例订单 + * + * @param userId 用户编号 + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createDemoOrder(Long userId, @Valid PayDemoOrderCreateReqVO createReqVO); + + /** + * 获得示例订单 + * + * @param id 编号 + * @return 示例订单 + */ + PayDemoOrderDO getDemoOrder(Long id); + + /** + * 获得示例订单分页 + * + * @param pageReqVO 分页查询 + * @return 示例订单分页 + */ + PageResult getDemoOrderPage(PageParam pageReqVO); + + /** + * 更新示例订单为已支付 + * + * @param id 编号 + * @param payOrderId 支付订单号 + */ + void updateDemoOrderPaid(Long id, Long payOrderId); + + /** + * 发起示例订单的退款 + * + * @param id 编号 + * @param userIp 用户编号 + */ + void refundDemoOrder(Long id, String userIp); + + /** + * 更新示例订单为已退款 + * + * @param id 编号 + * @param payRefundId 退款订单号 + */ + void updateDemoOrderRefunded(Long id, Long payRefundId); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/demo/PayDemoOrderServiceImpl.java new file mode 100644 index 0000000..f07ead8 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -0,0 +1,265 @@ +package com.yunxi.scm.module.pay.service.demo; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.pay.api.order.PayOrderApi; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderRespDTO; +import com.yunxi.scm.module.pay.api.refund.PayRefundApi; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundRespDTO; +import com.yunxi.scm.module.pay.controller.admin.demo.vo.PayDemoOrderCreateReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.demo.PayDemoOrderDO; +import com.yunxi.scm.module.pay.dal.mysql.demo.PayDemoOrderMapper; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.yunxi.scm.module.pay.enums.refund.PayRefundStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import static cn.hutool.core.util.ObjectUtil.*; +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.addTime; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; + +/** + * 示例订单 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class PayDemoOrderServiceImpl implements PayDemoOrderService { + + /** + * 接入的实力应用编号 + * + * 从 [支付管理 -> 应用信息] 里添加 + */ + private static final Long PAY_APP_ID = 7L; + + /** + * 商品信息 Map + * + * key:商品编号 + * value:[商品名、商品价格] + */ + private final Map spuNames = new HashMap<>(); + + @Resource + private PayOrderApi payOrderApi; + @Resource + private PayRefundApi payRefundApi; + + @Resource + private PayDemoOrderMapper payDemoOrderMapper; + + public PayDemoOrderServiceImpl() { + spuNames.put(1L, new Object[]{"华为手机", 1}); + spuNames.put(2L, new Object[]{"小米电视", 10}); + spuNames.put(3L, new Object[]{"苹果手表", 100}); + spuNames.put(4L, new Object[]{"华硕笔记本", 1000}); + spuNames.put(5L, new Object[]{"蔚来汽车", 200000}); + } + + @Override + public Long createDemoOrder(Long userId, PayDemoOrderCreateReqVO createReqVO) { + // 1.1 获得商品 + Object[] spu = spuNames.get(createReqVO.getSpuId()); + Assert.notNull(spu, "商品({}) 不存在", createReqVO.getSpuId()); + String spuName = (String) spu[0]; + Integer price = (Integer) spu[1]; + // 1.2 插入 demo 订单 + PayDemoOrderDO demoOrder = new PayDemoOrderDO().setUserId(userId) + .setSpuId(createReqVO.getSpuId()).setSpuName(spuName) + .setPrice(price).setPayStatus(false).setRefundPrice(0); + payDemoOrderMapper.insert(demoOrder); + + // 2.1 创建支付单 + Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO() + .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 + .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号 + .setSubject(spuName).setBody("").setPrice(price) // 价格信息 + .setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间 + // 2.2 更新支付单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(demoOrder.getId()) + .setPayOrderId(payOrderId)); + // 返回 + return demoOrder.getId(); + } + + @Override + public PayDemoOrderDO getDemoOrder(Long id) { + return payDemoOrderMapper.selectById(id); + } + + @Override + public PageResult getDemoOrderPage(PageParam pageReqVO) { + return payDemoOrderMapper.selectPage(pageReqVO); + } + + @Override + public void updateDemoOrderPaid(Long id, Long payOrderId) { + // 校验并获得支付订单(可支付) + PayOrderRespDTO payOrder = validateDemoOrderCanPaid(id, payOrderId); + + // 更新 PayDemoOrderDO 状态为已支付 + int updateCount = payDemoOrderMapper.updateByIdAndPayed(id, false, + new PayDemoOrderDO().setPayStatus(true).setPayTime(LocalDateTime.now()) + .setPayChannelCode(payOrder.getChannelCode())); + if (updateCount == 0) { + throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID); + } + } + + /** + * 校验交易订单满足被支付的条件 + * + * 1. 交易订单未支付 + * 2. 支付单已支付 + * + * @param id 交易订单编号 + * @param payOrderId 支付订单编号 + * @return 交易订单 + */ + private PayOrderRespDTO validateDemoOrderCanPaid(Long id, Long payOrderId) { + // 1.1 校验订单是否存在 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 1.2 校验订单未支付 + if (order.getPayStatus()) { + log.error("[validateDemoOrderCanPaid][order({}) 不处于待支付状态,请进行处理!order 数据是:{}]", + id, toJsonString(order)); + throw exception(DEMO_ORDER_UPDATE_PAID_STATUS_NOT_UNPAID); + } + // 1.3 校验支付订单匹配 + if (notEqual(order.getPayOrderId(), payOrderId)) { // 支付单号 + log.error("[validateDemoOrderCanPaid][order({}) 支付单不匹配({}),请进行处理!order 数据是:{}]", + id, payOrderId, toJsonString(order)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + + // 2.1 校验支付单是否存在 + PayOrderRespDTO payOrder = payOrderApi.getOrder(payOrderId); + if (payOrder == null) { + log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 不存在,请进行处理!]", id, payOrderId); + throw exception(ORDER_NOT_FOUND); + } + // 2.2 校验支付单已支付 + if (!PayOrderStatusEnum.isSuccess(payOrder.getStatus())) { + log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 未支付,请进行处理!payOrder 数据是:{}]", + id, payOrderId, toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_STATUS_NOT_SUCCESS); + } + // 2.3 校验支付金额一致 + if (notEqual(payOrder.getPrice(), order.getPrice())) { + log.error("[validateDemoOrderCanPaid][order({}) payOrder({}) 支付金额不匹配,请进行处理!order 数据是:{},payOrder 数据是:{}]", + id, payOrderId, toJsonString(order), toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_PRICE_NOT_MATCH); + } + // 2.4 校验支付订单匹配(二次) + if (notEqual(payOrder.getMerchantOrderId(), id.toString())) { + log.error("[validateDemoOrderCanPaid][order({}) 支付单不匹配({}),请进行处理!payOrder 数据是:{}]", + id, payOrderId, toJsonString(payOrder)); + throw exception(DEMO_ORDER_UPDATE_PAID_FAIL_PAY_ORDER_ID_ERROR); + } + return payOrder; + } + + @Override + public void refundDemoOrder(Long id, String userIp) { + // 1. 校验订单是否可以退款 + PayDemoOrderDO order = validateDemoOrderCanRefund(id); + + // 2.1 生成退款单号 + // 一般来说,用户发起退款的时候,都会单独插入一个售后维权表,然后使用该表的 id 作为 refundId + // 这里我们是个简单的 demo,所以没有售后维权表,直接使用订单 id + "-refund" 来演示 + String refundId = order.getId() + "-refund"; + // 2.2 创建退款单 + Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() + .setAppId(PAY_APP_ID).setUserIp(getClientIP()) // 支付应用 + .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 + .setMerchantRefundId(refundId) + .setReason("想退钱").setPrice(order.getPrice()));// 价格信息 + // 2.3 更新退款单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id) + .setPayRefundId(payRefundId).setRefundPrice(order.getPrice())); + } + + private PayDemoOrderDO validateDemoOrderCanRefund(Long id) { + // 校验订单是否存在 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 校验订单是否支付 + if (!order.getPayStatus()) { + throw exception(DEMO_ORDER_REFUND_FAIL_NOT_PAID); + } + // 校验订单是否已退款 + if (order.getPayRefundId() != null) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUNDED); + } + return order; + } + + @Override + public void updateDemoOrderRefunded(Long id, Long payRefundId) { + // 1. 校验并获得退款订单(可退款) + PayRefundRespDTO payRefund = validateDemoOrderCanRefunded(id, payRefundId); + // 2.2 更新退款单到 demo 订单 + payDemoOrderMapper.updateById(new PayDemoOrderDO().setId(id) + .setRefundTime(payRefund.getSuccessTime())); + } + + private PayRefundRespDTO validateDemoOrderCanRefunded(Long id, Long payRefundId) { + // 1.1 校验示例订单 + PayDemoOrderDO order = payDemoOrderMapper.selectById(id); + if (order == null) { + throw exception(DEMO_ORDER_NOT_FOUND); + } + // 1.2 校验退款订单匹配 + if (Objects.equals(order.getPayOrderId(), payRefundId)) { + log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({}),请进行处理!order 数据是:{}]", + id, payRefundId, toJsonString(order)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + + // 2.1 校验退款订单 + PayRefundRespDTO payRefund = payRefundApi.getRefund(payRefundId); + if (payRefund == null) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_FOUND); + } + // 2.2 + if (!PayRefundStatusEnum.isSuccess(payRefund.getStatus())) { + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_NOT_SUCCESS); + } + // 2.3 校验退款金额一致 + if (notEqual(payRefund.getRefundPrice(), order.getPrice())) { + log.error("[validateDemoOrderCanRefunded][order({}) payRefund({}) 退款金额不匹配,请进行处理!order 数据是:{},payRefund 数据是:{}]", + id, payRefundId, toJsonString(order), toJsonString(payRefund)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_PRICE_NOT_MATCH); + } + // 2.4 校验退款订单匹配(二次) + if (notEqual(payRefund.getMerchantOrderId(), id.toString())) { + log.error("[validateDemoOrderCanRefunded][order({}) 退款单不匹配({}),请进行处理!payRefund 数据是:{}]", + id, payRefundId, toJsonString(payRefund)); + throw exception(DEMO_ORDER_REFUND_FAIL_REFUND_ORDER_ID_ERROR); + } + return payRefund; + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/notify/PayNotifyService.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/notify/PayNotifyService.java new file mode 100644 index 0000000..4a61520 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/notify/PayNotifyService.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.pay.service.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyTaskDO; + +import java.util.List; + +/** + * 回调通知 Service 接口 + * + * @author 芋道源码 + */ +public interface PayNotifyService { + + /** + * 创建回调通知任务 + * + * @param type 类型 + * @param dataId 数据编号 + */ + void createPayNotifyTask(Integer type, Long dataId); + + /** + * 执行回调通知 + * + * 注意,该方法提供给定时任务调用。目前是 yunxi-server 进行调用 + * @return 通知数量 + */ + int executeNotify() throws InterruptedException; + + /** + * 获得回调通知 + * + * @param id 编号 + * @return 回调通知 + */ + PayNotifyTaskDO getNotifyTask(Long id); + + /** + * 获得回调通知分页 + * + * @param pageReqVO 分页查询 + * @return 回调通知分页 + */ + PageResult getNotifyTaskPage(PayNotifyTaskPageReqVO pageReqVO); + + /** + * 获得回调日志列表 + * + * @param taskId 任务编号 + * @return 日志列表 + */ + List getNotifyLogList(Long taskId); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/notify/PayNotifyServiceImpl.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/notify/PayNotifyServiceImpl.java new file mode 100644 index 0000000..af614b1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/notify/PayNotifyServiceImpl.java @@ -0,0 +1,294 @@ +package com.yunxi.scm.module.pay.service.notify; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.tenant.core.util.TenantUtils; +import com.yunxi.scm.module.pay.api.notify.dto.PayOrderNotifyReqDTO; +import com.yunxi.scm.module.pay.api.notify.dto.PayRefundNotifyReqDTO; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; +import com.yunxi.scm.module.pay.dal.mysql.notify.PayNotifyLogMapper; +import com.yunxi.scm.module.pay.dal.mysql.notify.PayNotifyTaskMapper; +import com.yunxi.scm.module.pay.dal.redis.notify.PayNotifyLockRedisDAO; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyStatusEnum; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyTypeEnum; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import com.yunxi.scm.module.pay.service.refund.PayRefundService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Lazy; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.addTime; +import static com.yunxi.scm.module.pay.framework.job.config.PayJobConfiguration.NOTIFY_THREAD_POOL_TASK_EXECUTOR; + +/** + * 支付通知 Core Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Valid +@Slf4j +public class PayNotifyServiceImpl implements PayNotifyService { + + /** + * 通知超时时间,单位:秒 + */ + public static final int NOTIFY_TIMEOUT = 120; + /** + * {@link #NOTIFY_TIMEOUT} 的毫秒 + */ + public static final long NOTIFY_TIMEOUT_MILLIS = 120 * DateUtils.SECOND_MILLIS; + + @Resource + @Lazy // 循环依赖,避免报错 + private PayOrderService orderService; + @Resource + @Lazy // 循环依赖,避免报错 + private PayRefundService refundService; + + @Resource + private PayNotifyTaskMapper notifyTaskMapper; + @Resource + private PayNotifyLogMapper notifyLogMapper; + + @Resource(name = NOTIFY_THREAD_POOL_TASK_EXECUTOR) + private ThreadPoolTaskExecutor threadPoolTaskExecutor; + + @Resource + private PayNotifyLockRedisDAO notifyLockCoreRedisDAO; + + @Override + @Transactional(rollbackFor = Exception.class) + public void createPayNotifyTask(Integer type, Long dataId) { + PayNotifyTaskDO task = new PayNotifyTaskDO().setType(type).setDataId(dataId); + task.setStatus(PayNotifyStatusEnum.WAITING.getStatus()).setNextNotifyTime(LocalDateTime.now()) + .setNotifyTimes(0).setMaxNotifyTimes(PayNotifyTaskDO.NOTIFY_FREQUENCY.length + 1); + // 补充 appId + notifyUrl 字段 + if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) { + PayOrderDO order = orderService.getOrder(task.getDataId()); // 不进行非空判断,有问题直接异常 + task.setAppId(order.getAppId()). + setMerchantOrderId(order.getMerchantOrderId()).setNotifyUrl(order.getNotifyUrl()); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) { + PayRefundDO refundDO = refundService.getRefund(task.getDataId()); + task.setAppId(refundDO.getAppId()) + .setMerchantOrderId(refundDO.getMerchantOrderId()).setNotifyUrl(refundDO.getNotifyUrl()); + } + + // 执行插入 + notifyTaskMapper.insert(task); + + // 必须在事务提交后,在发起任务,否则 PayNotifyTaskDO 还没入库,就提前回调接入的业务 + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + executeNotify(task); + } + }); + } + + @Override + public int executeNotify() throws InterruptedException { + // 获得需要通知的任务 + List tasks = notifyTaskMapper.selectListByNotify(); + if (CollUtil.isEmpty(tasks)) { + return 0; + } + + // 遍历,逐个通知 + CountDownLatch latch = new CountDownLatch(tasks.size()); + tasks.forEach(task -> threadPoolTaskExecutor.execute(() -> { + try { + executeNotify(task); + } finally { + latch.countDown(); + } + })); + // 等待完成 + awaitExecuteNotify(latch); + // 返回执行完成的任务数(成功 + 失败) + return tasks.size(); + } + + /** + * 等待全部支付通知的完成 + * 每 1 秒会打印一次剩余任务数量 + * + * @param latch Latch + * @throws InterruptedException 如果被打断 + */ + private void awaitExecuteNotify(CountDownLatch latch) throws InterruptedException { + long size = latch.getCount(); + for (int i = 0; i < NOTIFY_TIMEOUT; i++) { + if (latch.await(1L, TimeUnit.SECONDS)) { + return; + } + log.info("[awaitExecuteNotify][任务处理中, 总任务数({}) 剩余任务数({})]", size, latch.getCount()); + } + log.error("[awaitExecuteNotify][任务未处理完,总任务数({}) 剩余任务数({})]", size, latch.getCount()); + } + + /** + * 同步执行单个支付通知 + * + * @param task 通知任务 + */ + public void executeNotify(PayNotifyTaskDO task) { + // 分布式锁,避免并发问题 + notifyLockCoreRedisDAO.lock(task.getId(), NOTIFY_TIMEOUT_MILLIS, () -> { + // 校验,当前任务是否已经被通知过 + // 虽然已经通过分布式加锁,但是可能同时满足通知的条件,然后都去获得锁。此时,第一个执行完后,第二个还是能拿到锁,然后会再执行一次。 + // 因此,此处我们通过第 notifyTimes 通知次数是否匹配来判断 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + if (ObjectUtil.notEqual(task.getNotifyTimes(), dbTask.getNotifyTimes())) { + log.warn("[executeNotifySync][task({}) 任务被忽略,原因是它的通知不是第 ({}) 次,可能是因为并发执行了]", + JsonUtils.toJsonString(task), dbTask.getNotifyTimes()); + return; + } + + // 执行通知 + getSelf().executeNotify0(dbTask); + }); + } + + @Transactional(rollbackFor = Exception.class) + public void executeNotify0(PayNotifyTaskDO task) { + // 发起回调 + CommonResult invokeResult = null; + Throwable invokeException = null; + try { + invokeResult = executeNotifyInvoke(task); + } catch (Throwable e) { + invokeException = e; + } + + // 处理结果 + Integer newStatus = processNotifyResult(task, invokeResult, invokeException); + + // 记录 PayNotifyLog 日志 + String response = invokeException != null ? ExceptionUtil.getRootCauseMessage(invokeException) : + JsonUtils.toJsonString(invokeResult); + notifyLogMapper.insert(PayNotifyLogDO.builder().taskId(task.getId()) + .notifyTimes(task.getNotifyTimes() + 1).status(newStatus).response(response).build()); + } + + /** + * 执行单个支付任务的 HTTP 调用 + * + * @param task 通知任务 + * @return HTTP 响应 + */ + private CommonResult executeNotifyInvoke(PayNotifyTaskDO task) { + // 拼接 body 参数 + Object request; + if (Objects.equals(task.getType(), PayNotifyTypeEnum.ORDER.getType())) { + request = PayOrderNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId()) + .payOrderId(task.getDataId()).build(); + } else if (Objects.equals(task.getType(), PayNotifyTypeEnum.REFUND.getType())) { + request = PayRefundNotifyReqDTO.builder().merchantOrderId(task.getMerchantOrderId()) + .payRefundId(task.getDataId()).build(); + } else { + throw new RuntimeException("未知的通知任务类型:" + JsonUtils.toJsonString(task)); + } + // 拼接 header 参数 + Map headers = new HashMap<>(); + TenantUtils.addTenantHeader(headers, task.getTenantId()); + + // 发起请求 + try (HttpResponse response = HttpUtil.createPost(task.getNotifyUrl()) + .body(JsonUtils.toJsonString(request)).addHeaders(headers) + .timeout((int) NOTIFY_TIMEOUT_MILLIS).execute()) { + // 解析结果 + return JsonUtils.parseObject(response.body(), CommonResult.class); + } + } + + /** + * 处理并更新通知结果 + * + * @param task 通知任务 + * @param invokeResult 通知结果 + * @param invokeException 通知异常 + * @return 最终任务的状态 + */ + @VisibleForTesting + Integer processNotifyResult(PayNotifyTaskDO task, CommonResult invokeResult, Throwable invokeException) { + // 设置通用的更新 PayNotifyTaskDO 的字段 + PayNotifyTaskDO updateTask = new PayNotifyTaskDO() + .setId(task.getId()) + .setLastExecuteTime(LocalDateTime.now()) + .setNotifyTimes(task.getNotifyTimes() + 1); + + // 情况一:调用成功 + if (invokeResult != null && invokeResult.isSuccess()) { + updateTask.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + // 情况二:调用失败、调用异常 + // 2.1 超过最大回调次数 + if (updateTask.getNotifyTimes() >= PayNotifyTaskDO.NOTIFY_FREQUENCY.length) { + updateTask.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + // 2.2 未超过最大回调次数 + updateTask.setNextNotifyTime(addTime(Duration.ofSeconds(PayNotifyTaskDO.NOTIFY_FREQUENCY[updateTask.getNotifyTimes()]))); + updateTask.setStatus(invokeException != null ? PayNotifyStatusEnum.REQUEST_FAILURE.getStatus() + : PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()); + notifyTaskMapper.updateById(updateTask); + return updateTask.getStatus(); + } + + @Override + public PayNotifyTaskDO getNotifyTask(Long id) { + return notifyTaskMapper.selectById(id); + } + + @Override + public PageResult getNotifyTaskPage(PayNotifyTaskPageReqVO pageReqVO) { + return notifyTaskMapper.selectPage(pageReqVO); + } + + @Override + public List getNotifyLogList(Long taskId) { + return notifyLogMapper.selectListByTaskId(taskId); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayNotifyServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/order/PayOrderService.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/order/PayOrderService.java new file mode 100644 index 0000000..6cb9c01 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/order/PayOrderService.java @@ -0,0 +1,124 @@ +package com.yunxi.scm.module.pay.service.order; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderExtensionDO; + +import javax.validation.Valid; +import javax.validation.constraints.NotEmpty; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 支付订单 Service 接口 + * + * @author aquan + */ +public interface PayOrderService { + + /** + * 获得支付订单 + * + * @param id 编号 + * @return 支付订单 + */ + PayOrderDO getOrder(Long id); + + /** + * 获得支付订单 + * + * @param appId 应用编号 + * @param merchantOrderId 商户订单编号 + * @return 支付订单 + */ + PayOrderDO getOrder(Long appId, String merchantOrderId); + + /** + * 获得指定应用的订单数量 + * + * @param appId 应用编号 + * @return 订单数量 + */ + Long getOrderCountByAppId(Long appId); + + /** + * 获得支付订单分页 + * + * @param pageReqVO 分页查询 + * @return 支付订单分页 + */ + PageResult getOrderPage(PayOrderPageReqVO pageReqVO); + + /** + * 获得支付订单列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 支付订单列表 + */ + List getOrderList(PayOrderExportReqVO exportReqVO); + + /** + * 创建支付单 + * + * @param reqDTO 创建请求 + * @return 支付单编号 + */ + Long createOrder(@Valid PayOrderCreateReqDTO reqDTO); + + /** + * 提交支付 + * 此时,会发起支付渠道的调用 + * + * @param reqVO 提交请求 + * @param userIp 提交 IP + * @return 提交结果 + */ + PayOrderSubmitRespVO submitOrder(@Valid PayOrderSubmitReqVO reqVO, + @NotEmpty(message = "提交 IP 不能为空") String userIp); + + /** + * 通知支付单成功 + * + * @param channelId 渠道编号 + * @param notify 通知 + */ + void notifyOrder(Long channelId, PayOrderRespDTO notify); + + /** + * 更新支付订单的退款金额 + * + * @param id 编号 + * @param incrRefundPrice 增加的退款金额 + */ + void updateOrderRefundPrice(Long id, Integer incrRefundPrice); + + /** + * 获得支付订单 + * + * @param id 编号 + * @return 支付订单 + */ + PayOrderExtensionDO getOrderExtension(Long id); + + /** + * 同步订单的支付状态 + * + * @param minCreateTime 最小创建时间 + * @return 同步到已支付的订单数量 + */ + int syncOrder(LocalDateTime minCreateTime); + + /** + * 将已过期的订单,状态修改为已关闭 + * + * @return 过期的订单数量 + */ + int expireOrder(); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceImpl.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceImpl.java new file mode 100644 index 0000000..da89be7 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceImpl.java @@ -0,0 +1,549 @@ +package com.yunxi.scm.module.pay.service.order; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.framework.pay.core.client.PayClient; +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import com.yunxi.scm.framework.tenant.core.util.TenantUtils; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import com.yunxi.scm.module.pay.convert.order.PayOrderConvert; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.yunxi.scm.module.pay.dal.mysql.order.PayOrderExtensionMapper; +import com.yunxi.scm.module.pay.dal.mysql.order.PayOrderMapper; +import com.yunxi.scm.module.pay.dal.redis.no.PayNoRedisDAO; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyTypeEnum; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.yunxi.scm.module.pay.framework.pay.config.PayProperties; +import com.yunxi.scm.module.pay.service.app.PayAppService; +import com.yunxi.scm.module.pay.service.channel.PayChannelService; +import com.yunxi.scm.module.pay.service.notify.PayNotifyService; +import com.yunxi.scm.module.pay.util.MoneyUtils; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; + +/** + * 支付订单 Service 实现类 + * + * @author aquan + */ +@Service +@Validated +@Slf4j +public class PayOrderServiceImpl implements PayOrderService { + + @Resource + private PayProperties payProperties; + + @Resource + private PayClientFactory payClientFactory; + + @Resource + private PayOrderMapper orderMapper; + @Resource + private PayOrderExtensionMapper orderExtensionMapper; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + @Resource + private PayNotifyService notifyService; + + @Override + public PayOrderDO getOrder(Long id) { + return orderMapper.selectById(id); + } + + @Override + public PayOrderDO getOrder(Long appId, String merchantOrderId) { + return orderMapper.selectByAppIdAndMerchantOrderId(appId, merchantOrderId); + } + + @Override + public Long getOrderCountByAppId(Long appId) { + return orderMapper.selectCountByAppId(appId); + } + + @Override + public PageResult getOrderPage(PayOrderPageReqVO pageReqVO) { + return orderMapper.selectPage(pageReqVO); + } + + @Override + public List getOrderList(PayOrderExportReqVO exportReqVO) { + return orderMapper.selectList(exportReqVO); + } + + @Override + public Long createOrder(PayOrderCreateReqDTO reqDTO) { + // 校验 App + PayAppDO app = appService.validPayApp(reqDTO.getAppId()); + + // 查询对应的支付交易单是否已经存在。如果是,则直接返回 + PayOrderDO order = orderMapper.selectByAppIdAndMerchantOrderId( + reqDTO.getAppId(), reqDTO.getMerchantOrderId()); + if (order != null) { + log.warn("[createOrder][appId({}) merchantOrderId({}) 已经存在对应的支付单({})]", order.getAppId(), + order.getMerchantOrderId(), toJsonString(order)); // 理论来说,不会出现这个情况 + return order.getId(); + } + + // 创建支付交易单 + order = PayOrderConvert.INSTANCE.convert(reqDTO).setAppId(app.getId()) + // 商户相关字段 + .setNotifyUrl(app.getOrderNotifyUrl()) + // 订单相关字段 + .setStatus(PayOrderStatusEnum.WAITING.getStatus()) + // 退款相关字段 + .setRefundPrice(0); + orderMapper.insert(order); + return order.getId(); + } + + @Override // 注意,这里不能添加事务注解,避免调用支付渠道失败时,将 PayOrderExtensionDO 回滚了 + public PayOrderSubmitRespVO submitOrder(PayOrderSubmitReqVO reqVO, String userIp) { + // 1.1 获得 PayOrderDO ,并校验其是否存在 + PayOrderDO order = validateOrderCanSubmit(reqVO.getId()); + // 1.32 校验支付渠道是否有效 + PayChannelDO channel = validateChannelCanSubmit(order.getAppId(), reqVO.getChannelCode()); + PayClient client = payClientFactory.getPayClient(channel.getId()); + + // 2. 插入 PayOrderExtensionDO + String no = noRedisDAO.generate(payProperties.getOrderNoPrefix()); + PayOrderExtensionDO orderExtension = PayOrderConvert.INSTANCE.convert(reqVO, userIp) + .setOrderId(order.getId()).setNo(no) + .setChannelId(channel.getId()).setChannelCode(channel.getCode()) + .setStatus(PayOrderStatusEnum.WAITING.getStatus()); + orderExtensionMapper.insert(orderExtension); + + // 3. 调用三方接口 + PayOrderUnifiedReqDTO unifiedOrderReqDTO = PayOrderConvert.INSTANCE.convert2(reqVO, userIp) + // 商户相关的字段 + .setOutTradeNo(orderExtension.getNo()) // 注意,此处使用的是 PayOrderExtensionDO.no 属性! + .setSubject(order.getSubject()).setBody(order.getBody()) + .setNotifyUrl(genChannelOrderNotifyUrl(channel)) + .setReturnUrl(reqVO.getReturnUrl()) + // 订单相关字段 + .setPrice(order.getPrice()).setExpireTime(order.getExpireTime()); + PayOrderRespDTO unifiedOrderResp = client.unifiedOrder(unifiedOrderReqDTO); + + // 4. 如果调用直接支付成功,则直接更新支付单状态为成功。例如说:付款码支付,免密支付时,就直接验证支付成功 + if (unifiedOrderResp != null) { + getSelf().notifyOrder(channel, unifiedOrderResp); + // 如有渠道错误码,则抛出业务异常,提示用户 + if (StrUtil.isNotEmpty(unifiedOrderResp.getChannelErrorCode())) { + throw exception(ORDER_SUBMIT_CHANNEL_ERROR, unifiedOrderResp.getChannelErrorCode(), + unifiedOrderResp.getChannelErrorMsg()); + } + // 此处需要读取最新的状态 + order = orderMapper.selectById(order.getId()); + } + return PayOrderConvert.INSTANCE.convert(order, unifiedOrderResp); + } + + private PayOrderDO validateOrderCanSubmit(Long id) { + PayOrderDO order = orderMapper.selectById(id); + if (order == null) { // 是否存在 + throw exception(ORDER_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(order.getStatus())) { // 校验状态,发现已支付 + throw exception(ORDER_STATUS_IS_SUCCESS); + } + if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 + throw exception(ORDER_STATUS_IS_NOT_WAITING); + } + if (LocalDateTimeUtils.beforeNow(order.getExpireTime())) { // 校验是否过期 + throw exception(ORDER_IS_EXPIRED); + } + + // 【重要】校验是否支付拓展单已支付,只是没有回调、或者数据不正常 + validateOrderActuallyPaid(id); + return order; + } + + /** + * 校验支付订单实际已支付 + * + * @param id 支付编号 + */ + @VisibleForTesting + void validateOrderActuallyPaid(Long id) { + List orderExtensions = orderExtensionMapper.selectListByOrderId(id); + orderExtensions.forEach(orderExtension -> { + // 情况一:校验数据库中的 orderExtension 是不是已支付 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.warn("[validateOrderCanSubmit][order({}) 的 extension({}) 已支付,可能是数据不一致]", + id, orderExtension.getId()); + throw exception(ORDER_EXTENSION_IS_PAID); + } + // 情况二:调用三方接口,查询支付单状态,是不是已支付 + PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[validateOrderCanSubmit][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + if (respDTO != null && PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) { + log.warn("[validateOrderCanSubmit][order({}) 的 PayOrderRespDTO({}) 已支付,可能是回调延迟]", + id, toJsonString(respDTO)); + throw exception(ORDER_EXTENSION_IS_PAID); + } + }); + } + + private PayChannelDO validateChannelCanSubmit(Long appId, String channelCode) { + // 校验 App + appService.validPayApp(appId); + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(appId, channelCode); + PayClient client = payClientFactory.getPayClient(channel.getId()); + if (client == null) { + log.error("[validatePayChannelCanSubmit][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(CHANNEL_NOT_FOUND); + } + return channel; + } + + /** + * 根据支付渠道的编码,生成支付渠道的回调地址 + * + * @param channel 支付渠道 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id + */ + private String genChannelOrderNotifyUrl(PayChannelDO channel) { + return payProperties.getOrderNotifyUrl() + "/" + channel.getId(); + } + + @Override + public void notifyOrder(Long channelId, PayOrderRespDTO notify) { + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(channelId); + // 更新支付订单为已支付 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyOrder(channel, notify)); + } + + /** + * 通知并更新订单的支付结果 + * + * @param channel 支付渠道 + * @param notify 通知 + */ + @Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyPayOrder(channel, notify) 调用,否则事务不生效 + public void notifyOrder(PayChannelDO channel, PayOrderRespDTO notify) { + // 情况一:支付成功的回调 + if (PayOrderStatusRespEnum.isSuccess(notify.getStatus())) { + notifyOrderSuccess(channel, notify); + return; + } + // 情况二:支付失败的回调 + if (PayOrderStatusRespEnum.isClosed(notify.getStatus())) { + notifyOrderClosed(channel, notify); + } + // 情况三:WAITING:无需处理 + // 情况四:REFUND:通过退款回调处理 + } + + private void notifyOrderSuccess(PayChannelDO channel, PayOrderRespDTO notify) { + // 1. 更新 PayOrderExtensionDO 支付成功 + PayOrderExtensionDO orderExtension = updateOrderSuccess(notify); + // 2. 更新 PayOrderDO 支付成功 + Boolean paid = updateOrderSuccess(channel, orderExtension, notify); + if (paid) { // 如果之前已经成功回调,则直接返回,不用重复记录支付通知记录;例如说:支付平台重复回调 + return; + } + + // 3. 插入支付通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.ORDER.getType(), + orderExtension.getOrderId()); + } + + /** + * 更新 PayOrderExtensionDO 支付成功 + * + * @param notify 通知 + * @return PayOrderExtensionDO 对象 + */ + private PayOrderExtensionDO updateOrderSuccess(PayOrderRespDTO notify) { + // 1. 查询 PayOrderExtensionDO + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo()); + if (orderExtension == null) { + throw exception(ORDER_EXTENSION_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[updateOrderExtensionSuccess][orderExtension({}) 已经是已支付,无需更新]", orderExtension.getId()); + return orderExtension; + } + if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 + throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderExtensionDO + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(), + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()).channelNotifyData(toJsonString(notify)).build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionSuccess][orderExtension({}) 更新为已支付]", orderExtension.getId()); + return orderExtension; + } + + /** + * 更新 PayOrderDO 支付成功 + * + * @param channel 支付渠道 + * @param orderExtension 支付拓展单 + * @param notify 通知回调 + * @return 是否之前已经成功回调 + */ + private Boolean updateOrderSuccess(PayChannelDO channel, PayOrderExtensionDO orderExtension, + PayOrderRespDTO notify) { + // 1. 判断 PayOrderDO 是否处于待支付 + PayOrderDO order = orderMapper.selectById(orderExtension.getOrderId()); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + if (PayOrderStatusEnum.isSuccess(order.getStatus()) // 如果已经是成功,直接返回,不用重复更新 + && Objects.equals(order.getExtensionId(), orderExtension.getId())) { + log.info("[updateOrderExtensionSuccess][order({}) 已经是已支付,无需更新]", order.getId()); + return true; + } + if (!PayOrderStatusEnum.WAITING.getStatus().equals(order.getStatus())) { // 校验状态,必须是待支付 + throw exception(ORDER_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderDO + int updateCounts = orderMapper.updateByIdAndStatus(order.getId(), PayOrderStatusEnum.WAITING.getStatus(), + PayOrderDO.builder().status(PayOrderStatusEnum.SUCCESS.getStatus()) + .channelId(channel.getId()).channelCode(channel.getCode()) + .successTime(notify.getSuccessTime()).extensionId(orderExtension.getId()).no(orderExtension.getNo()) + .channelOrderNo(notify.getChannelOrderNo()).channelUserId(notify.getChannelUserId()) + .channelFeeRate(channel.getFeeRate()).channelFeePrice(MoneyUtils.calculateRatePrice(order.getPrice(), channel.getFeeRate())) + .build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(ORDER_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionSuccess][order({}) 更新为已支付]", order.getId()); + return false; + } + + private void notifyOrderClosed(PayChannelDO channel, PayOrderRespDTO notify) { + updateOrderExtensionClosed(channel, notify); + } + + private void updateOrderExtensionClosed(PayChannelDO channel, PayOrderRespDTO notify) { + // 1. 查询 PayOrderExtensionDO + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectByNo(notify.getOutTradeNo()); + if (orderExtension == null) { + throw exception(ORDER_EXTENSION_NOT_FOUND); + } + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { // 如果已经是关闭,直接返回,不用重复更新 + log.info("[updateOrderExtensionClosed][orderExtension({}) 已经是支付关闭,无需更新]", orderExtension.getId()); + return; + } + // 一般出现先是支付成功,然后支付关闭,都是全部退款导致关闭的场景。这个情况,我们不更新支付拓展单,只通过退款流程,更新支付单 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.info("[updateOrderExtensionClosed][orderExtension({}) 是已支付,无需更新为支付关闭]", orderExtension.getId()); + return; + } + if (ObjectUtil.notEqual(orderExtension.getStatus(), PayOrderStatusEnum.WAITING.getStatus())) { // 校验状态,必须是待支付 + throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + // 2. 更新 PayOrderExtensionDO + int updateCounts = orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), orderExtension.getStatus(), + PayOrderExtensionDO.builder().status(PayOrderStatusEnum.CLOSED.getStatus()).channelNotifyData(toJsonString(notify)) + .channelErrorCode(notify.getChannelErrorCode()).channelErrorMsg(notify.getChannelErrorMsg()).build()); + if (updateCounts == 0) { // 校验状态,必须是待支付 + throw exception(ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + log.info("[updateOrderExtensionClosed][orderExtension({}) 更新为支付关闭]", orderExtension.getId()); + } + + @Override + public void updateOrderRefundPrice(Long id, Integer incrRefundPrice) { + PayOrderDO order = orderMapper.selectById(id); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(ORDER_REFUND_FAIL_STATUS_ERROR); + } + if (order.getRefundPrice() + incrRefundPrice > order.getPrice()) { + throw exception(REFUND_PRICE_EXCEED); + } + + // 更新订单 + PayOrderDO updateObj = new PayOrderDO() + .setRefundPrice(order.getRefundPrice() + incrRefundPrice) + .setStatus(PayOrderStatusEnum.REFUND.getStatus()); + int updateCount = orderMapper.updateByIdAndStatus(id, order.getStatus(), updateObj); + if (updateCount == 0) { + throw exception(ORDER_REFUND_FAIL_STATUS_ERROR); + } + } + + @Override + public PayOrderExtensionDO getOrderExtension(Long id) { + return orderExtensionMapper.selectById(id); + } + + @Override + public int syncOrder(LocalDateTime minCreateTime) { + // 1. 查询指定创建时间内的待支付订单 + List orderExtensions = orderExtensionMapper.selectListByStatusAndCreateTimeGe( + PayOrderStatusEnum.WAITING.getStatus(), minCreateTime); + if (CollUtil.isEmpty(orderExtensions)) { + return 0; + } + // 2. 遍历执行 + int count = 0; + for (PayOrderExtensionDO orderExtension : orderExtensions) { + count += syncOrder(orderExtension) ? 1 : 0; + } + return count; + } + + /** + * 同步单个支付拓展单 + * + * @param orderExtension 支付拓展单 + * @return 是否已支付 + */ + private boolean syncOrder(PayOrderExtensionDO orderExtension) { + try { + // 1.1 查询支付订单信息 + PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[syncOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return false; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + // 1.2 回调支付结果 + notifyOrder(orderExtension.getChannelId(), respDTO); + + // 2. 如果是已支付,则返回 true + return PayOrderStatusRespEnum.isSuccess(respDTO.getStatus()); + } catch (Throwable e) { + log.error("[syncOrder][orderExtension({}) 同步支付状态异常]", orderExtension.getId(), e); + return false; + } + } + + @Override + public int expireOrder() { + // 1. 查询过期的待支付订单 + List orders = orderMapper.selectListByStatusAndExpireTimeLt( + PayOrderStatusEnum.WAITING.getStatus(), LocalDateTime.now()); + if (CollUtil.isEmpty(orders)) { + return 0; + } + + // 2. 遍历执行 + int count = 0; + for (PayOrderDO order : orders) { + count += expireOrder(order) ? 1 : 0; + } + return count; + } + + /** + * 同步单个支付单 + * + * @param order 支付单 + * @return 是否已过期 + */ + private boolean expireOrder(PayOrderDO order) { + try { + // 1. 需要先处理关联的支付拓展单,避免错误的过期已支付 or 已退款的订单 + List orderExtensions = orderExtensionMapper.selectListByOrderId(order.getId()); + for (PayOrderExtensionDO orderExtension : orderExtensions) { + if (PayOrderStatusEnum.isClosed(orderExtension.getStatus())) { + continue; + } + // 情况一:校验数据库中的 orderExtension 是不是已支付 + if (PayOrderStatusEnum.isSuccess(orderExtension.getStatus())) { + log.error("[expireOrder][order({}) 的 extension({}) 已支付,可能是数据不一致]", + order.getId(), orderExtension.getId()); + return false; + } + // 情况二:调用三方接口,查询支付单状态,是不是已支付/已退款 + PayClient payClient = payClientFactory.getPayClient(orderExtension.getChannelId()); + if (payClient == null) { + log.error("[expireOrder][渠道编号({}) 找不到对应的支付客户端]", orderExtension.getChannelId()); + return false; + } + PayOrderRespDTO respDTO = payClient.getOrder(orderExtension.getNo()); + if (PayOrderStatusRespEnum.isRefund(respDTO.getStatus())) { + // 补充说明:按道理,应该是 WAITING => SUCCESS => REFUND 状态,如果直接 WAITING => REFUND 状态,说明中间丢了过程 + // 此时,需要人工介入,手工补齐数据,保持 WAITING => SUCCESS => REFUND 的过程 + log.error("[expireOrder][extension({}) 的 PayOrderRespDTO({}) 已退款,可能是回调延迟]", + orderExtension.getId(), toJsonString(respDTO)); + return false; + } + if (PayOrderStatusRespEnum.isSuccess(respDTO.getStatus())) { + notifyOrder(orderExtension.getChannelId(), respDTO); + return false; + } + // 兜底逻辑:将支付拓展单更新为已关闭 + PayOrderExtensionDO updateObj = new PayOrderExtensionDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setChannelNotifyData(toJsonString(respDTO)); + if (orderExtensionMapper.updateByIdAndStatus(orderExtension.getId(), PayOrderStatusEnum.WAITING.getStatus(), + updateObj) == 0) { + log.error("[expireOrder][extension({}) 更新为支付关闭失败]", orderExtension.getId()); + return false; + } + log.info("[expireOrder][extension({}) 更新为支付关闭成功]", orderExtension.getId()); + } + + // 2. 都没有上述情况,可以安心更新为已关闭 + PayOrderDO updateObj = new PayOrderDO().setStatus(PayOrderStatusEnum.CLOSED.getStatus()); + if (orderMapper.updateByIdAndStatus(order.getId(), order.getStatus(), updateObj) == 0) { + log.error("[expireOrder][order({}) 更新为支付关闭失败]", order.getId()); + return false; + } + log.info("[expireOrder][order({}) 更新为支付关闭失败]", order.getId()); + return true; + } catch (Throwable e) { + log.error("[expireOrder][order({}) 过期订单异常]", order.getId(), e); + return false; + } + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayOrderServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/refund/PayRefundService.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/refund/PayRefundService.java new file mode 100644 index 0000000..cd1e478 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/refund/PayRefundService.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.pay.service.refund; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; + +import java.util.List; + +/** + * 退款订单 Service 接口 + * + * @author aquan + */ +public interface PayRefundService { + + /** + * 获得退款订单 + * + * @param id 编号 + * @return 退款订单 + */ + PayRefundDO getRefund(Long id); + + /** + * 获得指定应用的退款数量 + * + * @param appId 应用编号 + * @return 退款数量 + */ + Long getRefundCountByAppId(Long appId); + + /** + * 获得退款订单分页 + * + * @param pageReqVO 分页查询 + * @return 退款订单分页 + */ + PageResult getRefundPage(PayRefundPageReqVO pageReqVO); + + /** + * 获得退款订单列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 退款订单列表 + */ + List getRefundList(PayRefundExportReqVO exportReqVO); + + /** + * 创建退款申请 + * + * @param reqDTO 退款申请信息 + * @return 退款单号 + */ + Long createPayRefund(PayRefundCreateReqDTO reqDTO); + + /** + * 渠道的退款通知 + * + * @param channelId 渠道编号 + * @param notify 通知 + */ + void notifyRefund(Long channelId, PayRefundRespDTO notify); + + /** + * 同步渠道退款的退款状态 + * + * @return 同步到状态的退款数量,包括退款成功、退款失败 + */ + int syncRefund(); + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/refund/PayRefundServiceImpl.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/refund/PayRefundServiceImpl.java new file mode 100644 index 0000000..bb9fad9 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/service/refund/PayRefundServiceImpl.java @@ -0,0 +1,330 @@ +package com.yunxi.scm.module.pay.service.refund; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.pay.core.client.PayClient; +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.refund.PayRefundStatusRespEnum; +import com.yunxi.scm.framework.tenant.core.util.TenantUtils; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import com.yunxi.scm.module.pay.convert.refund.PayRefundConvert; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; +import com.yunxi.scm.module.pay.dal.mysql.refund.PayRefundMapper; +import com.yunxi.scm.module.pay.dal.redis.no.PayNoRedisDAO; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyTypeEnum; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.yunxi.scm.module.pay.enums.refund.PayRefundStatusEnum; +import com.yunxi.scm.module.pay.framework.pay.config.PayProperties; +import com.yunxi.scm.module.pay.service.app.PayAppService; +import com.yunxi.scm.module.pay.service.channel.PayChannelService; +import com.yunxi.scm.module.pay.service.notify.PayNotifyService; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; + +/** + * 退款订单 Service 实现类 + * + * @author jason + */ +@Service +@Slf4j +@Validated +public class PayRefundServiceImpl implements PayRefundService { + + @Resource + private PayProperties payProperties; + + @Resource + private PayClientFactory payClientFactory; + + @Resource + private PayRefundMapper refundMapper; + @Resource + private PayNoRedisDAO noRedisDAO; + + @Resource + private PayOrderService orderService; + @Resource + private PayAppService appService; + @Resource + private PayChannelService channelService; + @Resource + private PayNotifyService notifyService; + + @Override + public PayRefundDO getRefund(Long id) { + return refundMapper.selectById(id); + } + + @Override + public Long getRefundCountByAppId(Long appId) { + return refundMapper.selectCountByAppId(appId); + } + + @Override + public PageResult getRefundPage(PayRefundPageReqVO pageReqVO) { + return refundMapper.selectPage(pageReqVO); + } + + @Override + public List getRefundList(PayRefundExportReqVO exportReqVO) { + return refundMapper.selectList(exportReqVO); + } + + @Override + public Long createPayRefund(PayRefundCreateReqDTO reqDTO) { + // 1.1 校验 App + PayAppDO app = appService.validPayApp(reqDTO.getAppId()); + // 1.2 校验支付订单 + PayOrderDO order = validatePayOrderCanRefund(reqDTO); + // 1.3 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(order.getChannelId()); + PayClient client = payClientFactory.getPayClient(channel.getId()); + if (client == null) { + log.error("[refund][渠道编号({}) 找不到对应的支付客户端]", channel.getId()); + throw exception(CHANNEL_NOT_FOUND); + } + // 1.4 校验退款订单是否已经存在 + PayRefundDO refund = refundMapper.selectByAppIdAndMerchantRefundId( + app.getId(), reqDTO.getMerchantRefundId()); + if (refund != null) { + throw exception(REFUND_EXISTS); + } + + // 2.1 插入退款单 + String no = noRedisDAO.generate(payProperties.getRefundNoPrefix()); + refund = PayRefundConvert.INSTANCE.convert(reqDTO) + .setNo(no).setOrderId(order.getId()).setOrderNo(order.getNo()) + .setChannelId(order.getChannelId()).setChannelCode(order.getChannelCode()) + // 商户相关的字段 + .setNotifyUrl(app.getRefundNotifyUrl()) + // 渠道相关字段 + .setChannelOrderNo(order.getChannelOrderNo()) + // 退款相关字段 + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setPayPrice(order.getPrice()).setRefundPrice(reqDTO.getPrice()); + refundMapper.insert(refund); + try { + // 2.2 向渠道发起退款申请 + PayRefundUnifiedReqDTO unifiedReqDTO = new PayRefundUnifiedReqDTO() + .setPayPrice(order.getPrice()) + .setRefundPrice(reqDTO.getPrice()) + .setOutTradeNo(order.getNo()) + .setOutRefundNo(refund.getNo()) + .setNotifyUrl(genChannelRefundNotifyUrl(channel)) + .setReason(reqDTO.getReason()); + PayRefundRespDTO refundRespDTO = client.unifiedRefund(unifiedReqDTO); + // 2.3 处理退款返回 + getSelf().notifyRefund(channel, refundRespDTO); + } catch (Throwable e) { + // 注意:这里仅打印异常,不进行抛出。 + // 原因是:虽然调用支付渠道进行退款发生异常(网络请求超时),实际退款成功。这个结果,后续通过退款回调、或者退款轮询补偿可以拿到。 + // 最终,在异常的情况下,支付中心会异步回调业务的退款回调接口,提供退款结果 + log.error("[createPayRefund][退款 id({}) requestDTO({}) 发生异常]", + refund.getId(), reqDTO, e); + } + + // 返回退款编号 + return refund.getId(); + } + + /** + * 校验支付订单是否可以退款 + * + * @param reqDTO 退款申请信息 + * @return 支付订单 + */ + private PayOrderDO validatePayOrderCanRefund(PayRefundCreateReqDTO reqDTO) { + PayOrderDO order = orderService.getOrder(reqDTO.getAppId(), reqDTO.getMerchantOrderId()); + if (order == null) { + throw exception(ORDER_NOT_FOUND); + } + // 校验状态,必须是已支付、或者已退款 + if (!PayOrderStatusEnum.isSuccessOrRefund(order.getStatus())) { + throw exception(ORDER_REFUND_FAIL_STATUS_ERROR); + } + + // 校验金额,退款金额不能大于原定的金额 + if (reqDTO.getPrice() + order.getRefundPrice() > order.getPrice()){ + throw exception(REFUND_PRICE_EXCEED); + } + // 是否有退款中的订单 + if (refundMapper.selectCountByAppIdAndOrderId(reqDTO.getAppId(), order.getId(), + PayRefundStatusEnum.WAITING.getStatus()) > 0) { + throw exception(REFUND_HAS_REFUNDING); + } + return order; + } + + /** + * 根据支付渠道的编码,生成支付渠道的回调地址 + * + * @param channel 支付渠道 + * @return 支付渠道的回调地址 配置地址 + "/" + channel id + */ + private String genChannelRefundNotifyUrl(PayChannelDO channel) { + return payProperties.getRefundNotifyUrl() + "/" + channel.getId(); + } + + @Override + public void notifyRefund(Long channelId, PayRefundRespDTO notify) { + // 校验支付渠道是否有效 + PayChannelDO channel = channelService.validPayChannel(channelId); + // 更新退款订单 + TenantUtils.execute(channel.getTenantId(), () -> getSelf().notifyRefund(channel, notify)); + } + + /** + * 通知并更新订单的退款结果 + * + * @param channel 支付渠道 + * @param notify 通知 + */ + @Transactional(rollbackFor = Exception.class) // 注意,如果是方法内调用该方法,需要通过 getSelf().notifyRefund(channel, notify) 调用,否则事务不生效 + public void notifyRefund(PayChannelDO channel, PayRefundRespDTO notify) { + // 情况一:退款成功 + if (PayRefundStatusRespEnum.isSuccess(notify.getStatus())) { + notifyRefundSuccess(channel, notify); + return; + } + // 情况二:退款失败 + if (PayRefundStatusRespEnum.isFailure(notify.getStatus())) { + notifyRefundFailure(channel, notify); + } + } + + private void notifyRefundSuccess(PayChannelDO channel, PayRefundRespDTO notify) { + // 1.1 查询 PayRefundDO + PayRefundDO refund = refundMapper.selectByAppIdAndNo( + channel.getAppId(), notify.getOutRefundNo()); + if (refund == null) { + throw exception(REFUND_NOT_FOUND); + } + if (PayRefundStatusEnum.isSuccess(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[notifyRefundSuccess][退款订单({}) 已经是退款成功,无需更新]", refund.getId()); + return; + } + if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) { + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + // 1.2 更新 PayRefundDO + PayRefundDO updateRefundObj = new PayRefundDO() + .setSuccessTime(notify.getSuccessTime()) + .setChannelRefundNo(notify.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(notify)); + int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj); + if (updateCounts == 0) { // 校验状态,必须是等待状态 + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + log.info("[notifyRefundSuccess][退款订单({}) 更新为退款成功]", refund.getId()); + + // 2. 更新订单 + orderService.updateOrderRefundPrice(refund.getOrderId(), refund.getRefundPrice()); + + // 3. 插入退款通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(), + refund.getId()); + } + + private void notifyRefundFailure(PayChannelDO channel, PayRefundRespDTO notify) { + // 1.1 查询 PayRefundDO + PayRefundDO refund = refundMapper.selectByAppIdAndNo( + channel.getAppId(), notify.getOutRefundNo()); + if (refund == null) { + throw exception(REFUND_NOT_FOUND); + } + if (PayRefundStatusEnum.isFailure(refund.getStatus())) { // 如果已经是成功,直接返回,不用重复更新 + log.info("[notifyRefundSuccess][退款订单({}) 已经是退款关闭,无需更新]", refund.getId()); + return; + } + if (!PayRefundStatusEnum.WAITING.getStatus().equals(refund.getStatus())) { + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + // 1.2 更新 PayRefundDO + PayRefundDO updateRefundObj = new PayRefundDO() + .setChannelRefundNo(notify.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.FAILURE.getStatus()) + .setChannelNotifyData(toJsonString(notify)) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()); + int updateCounts = refundMapper.updateByIdAndStatus(refund.getId(), refund.getStatus(), updateRefundObj); + if (updateCounts == 0) { // 校验状态,必须是等待状态 + throw exception(REFUND_STATUS_IS_NOT_WAITING); + } + log.info("[notifyRefundFailure][退款订单({}) 更新为退款失败]", refund.getId()); + + // 2. 插入退款通知记录 + notifyService.createPayNotifyTask(PayNotifyTypeEnum.REFUND.getType(), + refund.getId()); + } + + @Override + public int syncRefund() { + // 1. 查询指定创建时间内的待退款订单 + List refunds = refundMapper.selectListByStatus(PayRefundStatusEnum.WAITING.getStatus()); + if (CollUtil.isEmpty(refunds)) { + return 0; + } + // 2. 遍历执行 + int count = 0; + for (PayRefundDO refund : refunds) { + count += syncRefund(refund) ? 1 : 0; + } + return count; + } + + /** + * 同步单个退款订单 + * + * @param refund 退款订单 + * @return 是否同步到 + */ + private boolean syncRefund(PayRefundDO refund) { + try { + // 1.1 查询退款订单信息 + PayClient payClient = payClientFactory.getPayClient(refund.getChannelId()); + if (payClient == null) { + log.error("[syncRefund][渠道编号({}) 找不到对应的支付客户端]", refund.getChannelId()); + return false; + } + PayRefundRespDTO respDTO = payClient.getRefund(refund.getOrderNo(), refund.getNo()); + // 1.2 回调退款结果 + notifyRefund(refund.getChannelId(), respDTO); + + // 2. 如果同步到,则返回 true + return PayRefundStatusEnum.isSuccess(respDTO.getStatus()) + || PayRefundStatusEnum.isFailure(respDTO.getStatus()); + } catch (Throwable e) { + log.error("[syncRefund][refund({}) 同步退款状态异常]", refund.getId(), e); + return false; + } + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PayRefundServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/util/MoneyUtils.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/util/MoneyUtils.java new file mode 100644 index 0000000..39d07c2 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/util/MoneyUtils.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.pay.util; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +/** + * 金额工具类 + * + * @author 芋道源码 + */ +public class MoneyUtils { + + /** + * 计算百分比金额,四舍五入 + * + * @param price 金额 + * @param rate 百分比,例如说 56.77% 则传入 56.77 + * @return 百分比金额 + */ + public static Integer calculateRatePrice(Integer price, Double rate) { + return new BigDecimal(price) + .multiply(BigDecimal.valueOf(rate)) // 乘以 + .setScale(0, RoundingMode.HALF_UP) // 四舍五入 + .intValue(); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/util/PaySeqUtils.java b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/util/PaySeqUtils.java new file mode 100644 index 0000000..15665dc --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/main/java/com/yunxi/scm/module/pay/util/PaySeqUtils.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.pay.util; + +import cn.hutool.core.date.DatePattern; +import cn.hutool.core.date.DateUtil; + +import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 支付相关编号的生产 + */ +// TODO @jason:需要改造,基于 db; +public class PaySeqUtils { + + private static final AtomicLong REFUND_REQ_NO_SEQ = new AtomicLong(0L); + + private static final AtomicLong MER_REFUND_NO_SEQ = new AtomicLong(0L); + + private static final AtomicLong MER_ORDER_NO_SEQ = new AtomicLong(0L); + + // TODO 芋艿:需要看看 + /** + * 生成商户退款单号,用于测试,应该由商户系统生成 + * @return 商户退款单 + */ + public static String genMerchantRefundNo() { + return String.format("%s%s%04d", "MR", + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_MS_PATTERN), + (int) MER_REFUND_NO_SEQ.getAndIncrement() % 10000); + } + + // TODO 芋艿:需要看看 + + /** + * 生成退款请求号 + * @return 退款请求号 + */ + public static String genRefundReqNo() { + return String.format("%s%s%04d", "RR", + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_MS_PATTERN), + (int) REFUND_REQ_NO_SEQ.getAndIncrement() % 10000); + } + + /** + * 生成商户订单编号号 用于测试,应该由商户系统生成 + * @return 商户订单编号 + */ + public static String genMerchantOrderNo() { + return String.format("%s%s%04d", "MO", + DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_MS_PATTERN), + (int) MER_ORDER_NO_SEQ.getAndIncrement() % 10000); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/dal/dataobject/merchant/PayChannelDOTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/dal/dataobject/merchant/PayChannelDOTest.java new file mode 100644 index 0000000..19d6319 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/dal/dataobject/merchant/PayChannelDOTest.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.pay.dal.dataobject.merchant; + +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.pay.core.client.impl.weixin.WXPayClientConfig; +import org.junit.jupiter.api.Test; + +public class PayChannelDOTest { + + @Test + public void testSerialization() { + PayChannelDO payChannelDO = new PayChannelDO(); + // 创建配置 + WXPayClientConfig config = new WXPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WXPayClientConfig.API_VERSION_V2); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + payChannelDO.setConfig(config); + + // 序列化 + String text = JsonUtils.toJsonString(payChannelDO); + System.out.println(text); + + // 反序列化 + payChannelDO = JsonUtils.parseObject(text, PayChannelDO.class); + System.out.println(payChannelDO.getConfig().getClass()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/dal/mysql/merchant/PayChannelMapperIntegrationTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/dal/mysql/merchant/PayChannelMapperIntegrationTest.java new file mode 100644 index 0000000..4542654 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/dal/mysql/merchant/PayChannelMapperIntegrationTest.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.pay.dal.mysql.merchant; + +import cn.hutool.core.io.IoUtil; +import com.yunxi.scm.module.pay.dal.dataobject.merchant.PayChannelDO; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import com.yunxi.scm.framework.pay.core.client.impl.weixin.WXPayClientConfig; +import com.yunxi.scm.framework.pay.core.enums.PayChannelEnum; +import com.yunxi.scm.module.pay.test.BaseDbIntegrationTest; +import org.junit.jupiter.api.Test; + +import javax.annotation.Resource; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.List; + +@Resource +public class PayChannelMapperIntegrationTest extends BaseDbIntegrationTest { + + @Resource + private PayChannelMapper payChannelMapper; + + /** + * 插入 {@link PayChannelEnum#WX_PUB} 初始配置 + */ + @Test + public void testInsertWxPub() throws FileNotFoundException { + PayChannelDO payChannelDO = new PayChannelDO(); + payChannelDO.setCode(PayChannelEnum.WX_PUB.getCode()); + payChannelDO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + payChannelDO.setFeeRate(1D); + payChannelDO.setAppId(6L); + // 配置 + WXPayClientConfig config = new WXPayClientConfig(); + config.setAppId("wx041349c6f39b268b"); + config.setMchId("1545083881"); + config.setApiVersion(WXPayClientConfig.API_VERSION_V2); + config.setMchKey("0alL64UDQdlCwiKZ73ib7ypaIjMns06p"); + config.setPrivateKeyContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_key.pem"))); + config.setPrivateCertContent(IoUtil.readUtf8(new FileInputStream("/Users/yunai/Downloads/wx_pay/apiclient_cert.pem"))); + config.setApiV3Key("joerVi8y5DJ3o4ttA0o1uH47Xz1u2Ase"); + payChannelDO.setConfig(config); + // 执行插入 + payChannelMapper.insert(payChannelDO); + } + + // TODO @ouyang:Zfb 改成 AlipayQr + /** + * 插入 {@link PayChannelEnum#ALIPAY_QR} 初始配置 + */ + @Test + public void testInsertZfb() { + PayChannelDO payChannelDO = new PayChannelDO(); + payChannelDO.setCode(PayChannelEnum.ALIPAY_QR.getCode()); + payChannelDO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + payChannelDO.setFeeRate(1D); + payChannelDO.setAppId(6L); + // 配置 + AlipayPayClientConfig config = new AlipayPayClientConfig(); + config.setAppId("2021000118634035"); + config.setServerUrl(AlipayPayClientConfig.SERVER_URL_SANDBOX); + config.setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT); + config.setPrivateKey("MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCHsEV1cDupwJv890x84qbppUtRIfhaKSwSVN0thCcsDCaAsGR5MZslDkO8NCT9V4r2SVXjyY7eJUZlZd1M0C8T01Tg4UOx5LUbic0O3A1uJMy6V1n9IyYwbAW3AEZhBd5bSbPgrqvmv3NeWSTQT6Anxnllf+2iDH6zyA2fPl7cYyQtbZoDJQFGqr4F+cGh2R6akzRKNoBkAeMYwoY6es2lX8sJxCVPWUmxNUoL3tScwlSpd7Bxw0q9c/X01jMwuQ0+Va358zgFiGERTE6yD01eu40OBDXOYO3z++y+TAYHlQQ2toMO63trepo88X3xV3R44/1DH+k2pAm2IF5ixiLrAgMBAAECggEAPx3SoXcseaD7rmcGcE0p4SMfbsUDdkUSmBBbtfF0GzwnqNLkWa+mgE0rWt9SmXngTQH97vByAYmLPl1s3G82ht1V7Sk7yQMe74lhFllr8eEyTjeVx3dTK1EEM4TwN+936DTXdFsr4TELJEcJJdD0KaxcCcfBLRDs2wnitEFZ9N+GoZybVmY8w0e0MI7PLObUZ2l0X4RurQnfG9ZxjXjC7PkeMVv7cGGylpNFi3BbvkRhdhLPDC2E6wqnr9e7zk+hiENivAezXrtxtwKovzCtnWJ1r0IO14Rh47H509Ic0wFnj+o5YyUL4LdmpL7yaaH6fM7zcSLFjNZPHvZCKPwYcQKBgQDQFho98QvnL8ex4v6cry4VitGpjSXm1qP3vmMQk4rTsn8iPWtcxPjqGEqOQJjdi4Mi0VZKQOLFwlH0kl95wNrD/isJ4O1yeYfX7YAXApzHqYNINzM79HemO3Yx1qLMW3okRFJ9pPRzbQ9qkTpsaegsmyX316zOBhzGRYjKbutTYwKBgQCm7phr9XdFW5Vh+XR90mVs483nrLmMiDKg7YKxSLJ8amiDjzPejCn7i95Hah08P+2MIZLIPbh2VLacczR6ltRRzN5bg5etFuqSgfkuHyxpoDmpjbe08+Q2h8JBYqcC5Nhv1AKU4iOUhVLHo/FBAQliMcGc/J3eiYTFC7EsNx382QKBgClb20doe7cttgFTXswBvaUmfFm45kmla924B7SpvrQpDD/f+VDtDZRp05fGmxuduSjYdtA3aVtpLiTwWu22OUUvZZqHDGruYOO4Hvdz23mL5b4ayqImCwoNU4bAZIc9v18p/UNf3/55NNE3oGcf/bev9rH2OjCQ4nM+Ktwhg8CFAoGACSgvbkShzUkv0ZcIf9ppu+ZnJh1AdGgINvGwaJ8vQ0nm/8h8NOoFZ4oNoGc+wU5Ubops7dUM6FjPR5e+OjdJ4E7Xp7d5O4J1TaIZlCEbo5OpdhaTDDcQvrkFu+Z4eN0qzj+YAKjDAOOrXc4tbr5q0FsgXscwtcNfaBuzFVTUrUkCgYEAwzPnMNhWG3zOWLUs2QFA2GP4Y+J8cpUYfj6pbKKzeLwyG9qBwF1NJpN8m+q9q7V9P2LY+9Lp9e1mGsGeqt5HMEA3P6vIpcqLJLqE/4PBLLRzfccTcmqb1m71+erxTRhHBRkGS+I7dZEb3olQfnS1Y1tpMBxiwYwR3LW4oXuJwj8="); + config.setAlipayPublicKey("MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnq90KnF4dTnlzzmxpujbI05OYqi5WxAS6cL0gnZFv2gK51HExF8v/BaP7P979PhFMgWTqmOOI+Dtno5s+yD09XTY1WkshbLk6i4g2Xlr8fyW9ODnkU88RI2w9UdPhQU4cPPwBNlrsYhKkVK2OxwM3kFqjoBBY0CZoZCsSQ3LDH5WeZqPArlsS6xa2zqJBuuoKjMrdpELl3eXSjP8K54eDJCbeetCZNKWLL3DPahTPB7LZikfYmslb0QUvCgGapD0xkS7eVq70NaL1G57MWABs4tbfWgxike4Daj3EfUrzIVspQxj7w8HEj9WozJPgL88kSJSits0pqD3n5r8HSuseQIDAQAB"); + // 创建客户端 + payChannelDO.setConfig(config); + // 执行插入 + payChannelMapper.insert(payChannelDO); + } + + /** + * 查询所有支付配置,看看是否都是 ok 的 + */ + @Test + public void testSelectList() { + List payChannels = payChannelMapper.selectList(); + System.out.println(payChannels.size()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceIntegrationTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceIntegrationTest.java new file mode 100644 index 0000000..209975b --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceIntegrationTest.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.module.pay.service.order; + +import com.yunxi.scm.module.pay.service.channel.PayAppServiceImpl; +import com.yunxi.scm.module.pay.service.channel.PayChannelServiceImpl; +import com.yunxi.scm.module.pay.service.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.service.order.dto.PayOrderSubmitReqDTO; +import com.yunxi.scm.module.pay.test.BaseDbIntegrationTest; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.pay.config.YunxiPayAutoConfiguration; +import com.yunxi.scm.framework.pay.core.enums.PayChannelEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; + +@Import({PayOrderServiceImpl.class, PayAppServiceImpl.class, + PayChannelServiceImpl.class, YunxiPayAutoConfiguration.class}) +public class PayOrderServiceIntegrationTest extends BaseDbIntegrationTest { + + @Resource + private PayOrderService payOrderService; + + @Test + public void testCreatePayOrder() { + // 构造请求 + PayOrderCreateReqDTO reqDTO = new PayOrderCreateReqDTO(); + reqDTO.setAppId(6L); + reqDTO.setUserIp("127.0.0.1"); + reqDTO.setMerchantOrderId(String.valueOf(System.currentTimeMillis())); + reqDTO.setSubject("标题"); + reqDTO.setBody("内容"); + reqDTO.setPrice(100); + reqDTO.setExpireTime(DateUtils.addTime(Duration.ofDays(1))); + // 发起请求 + payOrderService.createPayOrder(reqDTO); + } + + @Test + public void testSubmitPayOrder() { + // 构造请求 + PayOrderSubmitReqDTO reqDTO = new PayOrderSubmitReqDTO(); + reqDTO.setId(10L); + reqDTO.setAppId(6L); + reqDTO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqDTO.setUserIp("127.0.0.1"); + // 发起请求 + payOrderService.submitPayOrder(reqDTO); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/service/package-info.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/service/package-info.java new file mode 100644 index 0000000..fdf1adb --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/service/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.pay.service; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseDbAndRedisIntegrationTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseDbAndRedisIntegrationTest.java new file mode 100644 index 0000000..e6579ba --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseDbAndRedisIntegrationTest.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.pay.test; + +import com.yunxi.scm.framework.datasource.config.YunxiDataSourceAutoConfiguration; +import com.yunxi.scm.framework.mybatis.config.YunxiMybatisAutoConfiguration; +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseDbAndRedisIntegrationTest { + + @Import({ + // DB 配置类 + DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类 + YunxiDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + // MyBatis 配置类 + YunxiMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YunxiRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseDbIntegrationTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseDbIntegrationTest.java new file mode 100644 index 0000000..b7c922e --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseDbIntegrationTest.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.pay.test; + +import com.yunxi.scm.framework.datasource.config.YunxiDataSourceAutoConfiguration; +import com.yunxi.scm.framework.mybatis.config.YunxiMybatisAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseDbIntegrationTest { + + @Import({ + // DB 配置类 + DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类 + YunxiDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + // MyBatis 配置类 + YunxiMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseRedisIntegrationTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseRedisIntegrationTest.java new file mode 100644 index 0000000..413922d --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/java/com/yunxi/scm/module/pay/test/BaseRedisIntegrationTest.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.pay.test; + +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseRedisIntegrationTest { + + @Import({ + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YunxiRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/resources/application-integration-test.yaml b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/resources/application-integration-test.yaml new file mode 100644 index 0000000..d9d9d39 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test-integration/resources/application-integration-test.yaml @@ -0,0 +1,93 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志 + global-config: + db-config: + id-type: AUTO # 自增 ID + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + mapper-locations: classpath*:mapper/*.xml + type-aliases-package: ${yunxi.info.base-package}.module.*.dal.dataobject + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +yunxi: + info: + version: 1.0.0 + base-package: com.yunxi.scm.module + pay: + pay-notify-url: http://niubi.natapp1.cc/api/pay/order/notify + refund-notify-url: http://niubi.natapp1.cc/api/pay/refund/notify diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/app/PayAppServiceTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/app/PayAppServiceTest.java new file mode 100644 index 0000000..3b095f1 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/app/PayAppServiceTest.java @@ -0,0 +1,258 @@ +package com.yunxi.scm.module.pay.service.app; + +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppPageReqVO; +import com.yunxi.scm.module.pay.controller.admin.app.vo.PayAppUpdateReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.mysql.app.PayAppMapper; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import com.yunxi.scm.module.pay.service.refund.PayRefundService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link PayAppServiceImpl} 的单元测试 + * + * @author aquan + */ +@Import(PayAppServiceImpl.class) +public class PayAppServiceTest extends BaseDbUnitTest { + + @Resource + private PayAppServiceImpl appService; + + @Resource + private PayAppMapper appMapper; + + @MockBean + private PayOrderService orderService; + @MockBean + private PayRefundService refundService; + + @Test + public void testCreateApp_success() { + // 准备参数 + PayAppCreateReqVO reqVO = randomPojo(PayAppCreateReqVO.class, o -> + o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus())) + .setOrderNotifyUrl(randomURL()) + .setRefundNotifyUrl(randomURL())); + + // 调用 + Long appId = appService.createApp(reqVO); + // 断言 + assertNotNull(appId); + PayAppDO app = appMapper.selectById(appId); + assertPojoEquals(reqVO, app); + } + + @Test + public void testUpdateApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setOrderNotifyUrl(randomURL()).setRefundNotifyUrl(randomURL()); + o.setId(dbApp.getId()); // 设置更新的 ID + }); + + // 调用 + appService.updateApp(reqVO); + // 校验是否更新正确 + PayAppDO app = appMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, app); + } + + @Test + public void testUpdateApp_notExists() { + // 准备参数 + PayAppUpdateReqVO reqVO = randomPojo(PayAppUpdateReqVO.class, o -> + o.setStatus((RandomUtil.randomEle(CommonStatusEnum.values()).getStatus()))); + // 调用, 并断言异常 + assertServiceException(() -> appService.updateApp(reqVO), APP_NOT_FOUND); + } + + @Test + public void testUpdateAppStatus() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, o -> + o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + + // 准备参数 + Long id = dbApp.getId(); + Integer status = CommonStatusEnum.ENABLE.getStatus(); + // 调用 + appService.updateAppStatus(id, status); + // 断言 + PayAppDO app = appMapper.selectById(id); // 获取最新的 + assertEquals(status, app.getStatus()); + } + + @Test + public void testDeleteApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + appService.deleteApp(id); + // 校验数据不存在了 + assertNull(appMapper.selectById(id)); + } + + @Test + public void testDeleteApp_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_NOT_FOUND); + } + + @Test + public void testDeleteApp_existOrder() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + // mock 订单有订单 + when(orderService.getOrderCountByAppId(eq(id))).thenReturn(10L); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_EXIST_ORDER_CANT_DELETE); + } + + @Test + public void testDeleteApp_existRefund() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + // mock 订单有订单 + when(refundService.getRefundCountByAppId(eq(id))).thenReturn(10L); + + // 调用, 并断言异常 + assertServiceException(() -> appService.deleteApp(id), APP_EXIST_REFUND_CANT_DELETE); + } + + @Test + public void testApp() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + PayAppDO app = appService.getApp(id); + // 校验数据一致 + assertPojoEquals(app, dbApp); + } + + @Test + public void testAppMap() { + // mock 数据 + PayAppDO dbApp01 = randomPojo(PayAppDO.class); + appMapper.insert(dbApp01);// @Sql: 先插入出一条存在的数据 + PayAppDO dbApp02 = randomPojo(PayAppDO.class); + appMapper.insert(dbApp02);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp01.getId(); + + // 调用 + Map appMap = appService.getAppMap(singleton(id)); + // 校验数据一致 + assertEquals(1, appMap.size()); + assertPojoEquals(dbApp01, appMap.get(id)); + } + + @Test + public void testGetAppPage() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, o -> { // 等会查询到 + o.setName("灿灿姐的杂货铺"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021,11,20)); + }); + + appMapper.insert(dbApp); + // 测试 name 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setName("敏敏姐的杂货铺"))); + // 测试 status 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + appMapper.insert(cloneIgnoreId(dbApp, o -> o.setCreateTime(buildTime(2021,12,21)))); + // 准备参数 + PayAppPageReqVO reqVO = new PayAppPageReqVO(); + reqVO.setName("灿灿姐的杂货铺"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2021, 11, 19, 2021, 11, 21)); + + // 调用 + PageResult pageResult = appService.getAppPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbApp, pageResult.getList().get(0)); + } + + @Test + public void testValidPayApp_success() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, + o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用 + PayAppDO app = appService.validPayApp(id); + // 校验数据一致 + assertPojoEquals(app, dbApp); + } + + @Test + public void testValidPayApp_notFound() { + assertServiceException(() -> appService.validPayApp(randomLongId()), APP_NOT_FOUND); + } + + @Test + public void testValidPayApp_disable() { + // mock 数据 + PayAppDO dbApp = randomPojo(PayAppDO.class, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + appMapper.insert(dbApp);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbApp.getId(); + + // 调用,并断言异常 + assertServiceException(() -> appService.validPayApp(id), APP_IS_DISABLE); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/channel/PayChannelServiceTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/channel/PayChannelServiceTest.java new file mode 100644 index 0000000..e2cd5f6 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/channel/PayChannelServiceTest.java @@ -0,0 +1,365 @@ +package com.yunxi.scm.module.pay.service.channel; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.client.impl.alipay.AlipayPayClientConfig; +import com.yunxi.scm.framework.pay.core.client.impl.weixin.WxPayClientConfig; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelCreateReqVO; +import com.yunxi.scm.module.pay.controller.admin.channel.vo.PayChannelUpdateReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.dal.mysql.channel.PayChannelMapper; +import com.alibaba.fastjson.JSON; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import javax.validation.Validator; + +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.addTime; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; + +@Import({PayChannelServiceImpl.class}) +public class PayChannelServiceTest extends BaseDbUnitTest { + + private static final String ALIPAY_SERVER_URL = "https://openapi.alipay.com/gateway.do"; + + @Resource + private PayChannelServiceImpl channelService; + + @Resource + private PayChannelMapper channelMapper; + + @MockBean + private PayClientFactory payClientFactory; + @MockBean + private Validator validator; + + @BeforeEach + public void setUp() { + channelService.setChannelCache(null); + } + + @Test + public void testInitLocalCache() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig())); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + + // 调用 + channelService.initLocalCache(); + // 校验缓存 + assertEquals(1, channelService.getChannelCache().size()); + assertEquals(dbChannel, channelService.getChannelCache().get(0)); + } + + @Test + public void testRefreshLocalCache() { + // mock 数据 01 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig()).setUpdateTime(addTime(Duration.ofMinutes(-2)))); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + channelService.initLocalCache(); + // mock 数据 02 + PayChannelDO dbChannel02 = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig())); + channelMapper.insert(dbChannel02);// @Sql: 先插入出一条存在的数据 + + // 调用 + channelService.refreshLocalCache(); + // 校验缓存 + assertEquals(2, channelService.getChannelCache().size()); + assertEquals(dbChannel, channelService.getChannelCache().get(0)); + assertEquals(dbChannel02, channelService.getChannelCache().get(1)); + } + + @Test + public void testCreateChannel_success() { + // 准备参数 + WxPayClientConfig config = randomWxPayClientConfig(); + PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> { + o.setStatus(randomCommonStatus()); + o.setCode(PayChannelEnum.WX_PUB.getCode()); + o.setConfig(JsonUtils.toJsonString(config)); + }); + + // 调用 + Long channelId = channelService.createChannel(reqVO); + // 校验记录的属性是否正确 + PayChannelDO channel = channelMapper.selectById(channelId); + assertPojoEquals(reqVO, channel, "config"); + assertPojoEquals(config, channel.getConfig()); + // 校验缓存 + assertEquals(1, channelService.getChannelCache().size()); + assertEquals(channel, channelService.getChannelCache().get(0)); + } + + @Test + public void testCreateChannel_exists() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, + o -> o.setConfig(randomWxPayClientConfig())); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PayChannelCreateReqVO reqVO = randomPojo(PayChannelCreateReqVO.class, o -> { + o.setAppId(dbChannel.getAppId()); + o.setCode(dbChannel.getCode()); + }); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.createChannel(reqVO), CHANNEL_EXIST_SAME_CHANNEL_ERROR); + } + + @Test + public void testUpdateChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + AlipayPayClientConfig config = randomAlipayPayClientConfig(); + PayChannelUpdateReqVO reqVO = randomPojo(PayChannelUpdateReqVO.class, o -> { + o.setId(dbChannel.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + o.setConfig(JsonUtils.toJsonString(config)); + }); + + // 调用 + channelService.updateChannel(reqVO); + // 校验是否更新正确 + PayChannelDO channel = channelMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, channel, "config"); + assertPojoEquals(config, channel.getConfig()); + // 校验缓存 + assertEquals(1, channelService.getChannelCache().size()); + assertEquals(channel, channelService.getChannelCache().get(0)); + } + + @Test + public void testUpdateChannel_notExists() { + // 准备参数 + AlipayPayClientConfig payClientPublicKeyConfig = randomAlipayPayClientConfig(); + PayChannelUpdateReqVO reqVO = randomPojo(PayChannelUpdateReqVO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setConfig(JSON.toJSONString(payClientPublicKeyConfig)); + }); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.updateChannel(reqVO), CHANNEL_NOT_FOUND); + } + + @Test + public void testDeleteChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + channelService.deleteChannel(id); + // 校验数据不存在了 + assertNull(channelMapper.selectById(id)); + // 校验缓存 + assertEquals(0, channelService.getChannelCache().size()); + } + + @Test + public void testDeleteChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.deleteChannel(id), CHANNEL_NOT_FOUND); + } + + @Test + public void testGetChannel() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + PayChannelDO channel = channelService.getChannel(id); + // 校验是否更新正确 + assertPojoEquals(dbChannel, channel); + } + + @Test + public void testGetChannelListByAppIds() { + // mock 数据 + PayChannelDO dbChannel01 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel01);// @Sql: 先插入出一条存在的数据 + PayChannelDO dbChannel02 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.WX_PUB.getCode()); + o.setConfig(randomWxPayClientConfig()); + }); + channelMapper.insert(dbChannel02);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel01.getAppId(); + + // 调用 + List channels = channelService.getChannelListByAppIds(Collections.singleton(appId)); + // 校验是否更新正确 + assertEquals(1, channels.size()); + assertPojoEquals(dbChannel01, channels.get(0)); + } + + @Test + public void testGetChannelByAppIdAndCode() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel.getAppId(); + String code = dbChannel.getCode();; + + // 调用 + PayChannelDO channel = channelService.getChannelByAppIdAndCode(appId, code); + // 断言 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testValidPayChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.validPayChannel(id), CHANNEL_NOT_FOUND); + } + + @Test + public void testValidPayChannel_isDisable() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> channelService.validPayChannel(id), CHANNEL_IS_DISABLE); + } + + @Test + public void testValidPayChannel_success() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbChannel.getId(); + + // 调用 + PayChannelDO channel = channelService.validPayChannel(id); + // 断言异常 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testValidPayChannel_appIdAndCode() { + // mock 数据 + PayChannelDO dbChannel = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long appId = dbChannel.getAppId(); + String code = dbChannel.getCode(); + + // 调用 + PayChannelDO channel = channelService.validPayChannel(appId, code); + // 断言异常 + assertPojoEquals(channel, dbChannel); + } + + @Test + public void testGetEnableChannelList() { + // 准备参数 + Long appId = randomLongId(); + // mock 数据 01(enable 不匹配) + PayChannelDO dbChannel01 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + }); + channelMapper.insert(dbChannel01);// @Sql: 先插入出一条存在的数据 + // mock 数据 02(appId 不匹配) + PayChannelDO dbChannel02 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel02);// @Sql: 先插入出一条存在的数据 + // mock 数据 03 + PayChannelDO dbChannel03 = randomPojo(PayChannelDO.class, o -> { + o.setCode(PayChannelEnum.ALIPAY_APP.getCode()); + o.setConfig(randomAlipayPayClientConfig()); + o.setAppId(appId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + channelMapper.insert(dbChannel03);// @Sql: 先插入出一条存在的数据 + + // 调用 + List channel = channelService.getEnableChannelList(appId); + // 断言异常 + assertPojoEquals(channel, dbChannel03); + } + + public WxPayClientConfig randomWxPayClientConfig() { + return new WxPayClientConfig() + .setAppId(randomString()) + .setMchId(randomString()) + .setApiVersion(WxPayClientConfig.API_VERSION_V2) + .setMchKey(randomString()); + } + + public AlipayPayClientConfig randomAlipayPayClientConfig() { + return new AlipayPayClientConfig() + .setServerUrl(randomURL()) + .setAppId(randomString()) + .setSignType(AlipayPayClientConfig.SIGN_TYPE_DEFAULT) + .setMode(AlipayPayClientConfig.MODE_PUBLIC_KEY) + .setPrivateKey(randomString()) + .setAlipayPublicKey(randomString()); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/notify/PayNotifyServiceTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/notify/PayNotifyServiceTest.java new file mode 100644 index 0000000..cbc65e6 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/notify/PayNotifyServiceTest.java @@ -0,0 +1,351 @@ +package com.yunxi.scm.module.pay.service.notify; + +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.pay.controller.admin.notify.vo.PayNotifyTaskPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyLogDO; +import com.yunxi.scm.module.pay.dal.dataobject.notify.PayNotifyTaskDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; +import com.yunxi.scm.module.pay.dal.mysql.notify.PayNotifyLogMapper; +import com.yunxi.scm.module.pay.dal.mysql.notify.PayNotifyTaskMapper; +import com.yunxi.scm.module.pay.dal.redis.notify.PayNotifyLockRedisDAO; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyStatusEnum; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyTypeEnum; +import com.yunxi.scm.module.pay.framework.job.config.PayJobConfiguration; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import com.yunxi.scm.module.pay.service.refund.PayRefundService; +import com.yunxi.scm.module.pay.service.refund.PayRefundServiceImpl; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.*; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link PayRefundServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayJobConfiguration.class, PayNotifyServiceImpl.class, PayNotifyLockRedisDAO.class}) +public class PayNotifyServiceTest extends BaseDbUnitTest { + + @Resource + private PayNotifyServiceImpl notifyService; + + @MockBean + private PayOrderService orderService; + @MockBean + private PayRefundService refundService; + + @Resource + private PayNotifyTaskMapper notifyTaskMapper; + @Resource + private PayNotifyLogMapper notifyLogMapper; + + @MockBean + private RedissonClient redissonClient; + + @Test + public void testCreatePayNotifyTask_order() { + PayNotifyServiceImpl payNotifyService = mock(PayNotifyServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayNotifyServiceImpl.class))) + .thenReturn(payNotifyService); + + // 准备参数 + Integer type = PayNotifyTypeEnum.ORDER.getType(); + Long dataId = 1L; + // mock 方法(order) + PayOrderDO order = randomPojo(PayOrderDO.class); + when(orderService.getOrder(eq(1L))).thenReturn(order); + // mock 方法(lock) + mockLock(null); // null 的原因,是咱没办法拿到 taskId 新增 + + // 调用 + notifyService.createPayNotifyTask(type, dataId); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectOne(null); + assertNotNull(dbTask.getNextNotifyTime()); + assertThat(dbTask) + .extracting("type", "dataId", "status", "notifyTimes", "maxNotifyTimes", + "appId", "merchantOrderId", "notifyUrl") + .containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9, + order.getAppId(), order.getMerchantOrderId(), order.getNotifyUrl()); + // 断言,调用 + verify(payNotifyService).executeNotify0(eq(dbTask)); + } + } + + @Test + public void testCreatePayNotifyTask_refund() { + PayNotifyServiceImpl payNotifyService = mock(PayNotifyServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayNotifyServiceImpl.class))) + .thenReturn(payNotifyService); + + // 准备参数 + Integer type = PayNotifyTypeEnum.REFUND.getType(); + Long dataId = 1L; + // mock 方法(refund) + PayRefundDO refund = randomPojo(PayRefundDO.class); + when(refundService.getRefund(eq(1L))).thenReturn(refund); + // mock 方法(lock) + mockLock(null); // null 的原因,是咱没办法拿到 taskId 新增 + + // 调用 + notifyService.createPayNotifyTask(type, dataId); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectOne(null); + assertNotNull(dbTask.getNextNotifyTime()); + assertThat(dbTask) + .extracting("type", "dataId", "status", "notifyTimes", "maxNotifyTimes", + "appId", "merchantOrderId", "notifyUrl") + .containsExactly(type, dataId, PayNotifyStatusEnum.WAITING.getStatus(), 0, 9, + refund.getAppId(), refund.getMerchantOrderId(), refund.getNotifyUrl()); + // 断言,调用 + verify(payNotifyService).executeNotify0(eq(dbTask)); + } + } + + @Test + public void testExecuteNotify() throws InterruptedException { + // mock 数据(notify) + PayNotifyTaskDO dbTask01 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.WAITING.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask01); + PayNotifyTaskDO dbTask02 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask02); + PayNotifyTaskDO dbTask03 = randomPojo(PayNotifyTaskDO.class, + o -> o.setStatus(PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask03); + PayNotifyTaskDO dbTask04 = randomPojo(PayNotifyTaskDO.class, // 不满足状态 + o -> o.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask04); + PayNotifyTaskDO dbTask05 = randomPojo(PayNotifyTaskDO.class, // 不满足状态 + o -> o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(-1)))); + notifyTaskMapper.insert(dbTask05); + PayNotifyTaskDO dbTask06 = randomPojo(PayNotifyTaskDO.class, // 不满足时间 + o -> o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()) + .setNextNotifyTime(addTime(Duration.ofMinutes(1)))); + notifyTaskMapper.insert(dbTask06); + // mock 方法(lock) + mockLock(dbTask01.getId()); + mockLock(dbTask02.getId()); + mockLock(dbTask03.getId()); + + // 调用 + int count = notifyService.executeNotify(); + // 断言,数量 + assertEquals(count, 3); + } + + @Test // 由于 HttpUtil 不好 mock,所以只测试异常的情况 + public void testExecuteNotify0_exception() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, o -> o.setType(-1) + .setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + + // 调用 + notifyService.executeNotify0(task); + // 断言,task + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + // 断言,log + PayNotifyLogDO dbLog = notifyLogMapper.selectOne(null); + assertEquals(dbLog.getTaskId(), task.getId()); + assertEquals(dbLog.getNotifyTimes(), 1); + assertTrue(dbLog.getResponse().contains("未知的通知任务类型:")); + assertEquals(dbLog.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + } + + @Test + public void testProcessNotifyResult_success() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.success(randomString()); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.SUCCESS.getStatus()); + } + + @Test + public void testProcessNotifyResult_failure() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(8).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 9); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.FAILURE.getStatus()); + } + + @Test + public void testProcessNotifyResult_requestFailure() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, null); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_SUCCESS.getStatus()); + } + + @Test + public void testProcessNotifyResult_requestSuccess() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class, + o -> o.setNotifyTimes(0).setMaxNotifyTimes(9)); + notifyTaskMapper.insert(task); + // 准备参数 + CommonResult invokeResult = CommonResult.error(BAD_REQUEST); + RuntimeException invokeException = new RuntimeException(); + + // 调用 + notifyService.processNotifyResult(task, invokeResult, invokeException); + // 断言 + PayNotifyTaskDO dbTask = notifyTaskMapper.selectById(task.getId()); + assertNotEquals(task.getNextNotifyTime(), dbTask.getNextNotifyTime()); + assertNotEquals(task.getLastExecuteTime(), dbTask.getNextNotifyTime()); + assertEquals(dbTask.getNotifyTimes(), 1); + assertEquals(dbTask.getStatus(), PayNotifyStatusEnum.REQUEST_FAILURE.getStatus()); + } + + @Test + public void testGetNotifyTask() { + // mock 数据(task) + PayNotifyTaskDO task = randomPojo(PayNotifyTaskDO.class); + notifyTaskMapper.insert(task); + // 准备参数 + Long id = task.getId(); + + // 调用 + PayNotifyTaskDO dbTask = notifyService.getNotifyTask(id); + // 断言 + assertPojoEquals(dbTask, task); + } + + @Test + public void testGetNotifyTaskPage() { + // mock 数据 + PayNotifyTaskDO dbTask = randomPojo(PayNotifyTaskDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setType(PayNotifyTypeEnum.REFUND.getType()); + o.setDataId(100L); + o.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + o.setMerchantOrderId("P110"); + o.setCreateTime(buildTime(2023, 2, 3)); + }); + notifyTaskMapper.insert(dbTask); + // 测试 appId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setAppId(2L))); + // 测试 type 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setType(PayNotifyTypeEnum.ORDER.getType()))); + // 测试 dataId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setDataId(200L))); + // 测试 status 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setStatus(PayNotifyStatusEnum.FAILURE.getStatus()))); + // 测试 merchantOrderId 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setMerchantOrderId(randomString()))); + // 测试 createTime 不匹配 + notifyTaskMapper.insert(cloneIgnoreId(dbTask, o -> o.setCreateTime(buildTime(2023, 1, 1)))); + // 准备参数 + PayNotifyTaskPageReqVO reqVO = new PayNotifyTaskPageReqVO(); + reqVO.setAppId(1L); + reqVO.setType(PayNotifyTypeEnum.REFUND.getType()); + reqVO.setDataId(100L); + reqVO.setStatus(PayNotifyStatusEnum.SUCCESS.getStatus()); + reqVO.setMerchantOrderId("P110"); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28)); + + // 调用 + PageResult pageResult = notifyService.getNotifyTaskPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTask, pageResult.getList().get(0)); + } + + @Test + public void testGetNotifyLogList() { + // mock 数据 + PayNotifyLogDO dbLog = randomPojo(PayNotifyLogDO.class); + notifyLogMapper.insert(dbLog); + PayNotifyLogDO dbLog02 = randomPojo(PayNotifyLogDO.class); + notifyLogMapper.insert(dbLog02); + // 准备参数 + Long taskId = dbLog.getTaskId(); + + // 调用 + List logList = notifyService.getNotifyLogList(taskId); + // 断言 + assertEquals(logList.size(), 1); + assertPojoEquals(dbLog, logList.get(0)); + } + + private void mockLock(Long id) { + RLock lock = mock(RLock.class); + if (id == null) { + when(redissonClient.getLock(anyString())) + .thenReturn(lock); + } else { + when(redissonClient.getLock(eq("pay_notify:lock:" + id))) + .thenReturn(lock); + } + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceTest.java new file mode 100644 index 0000000..e0792ba --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/order/PayOrderServiceTest.java @@ -0,0 +1,1105 @@ +package com.yunxi.scm.module.pay.service.order; + +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.pay.core.client.PayClient; +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.client.dto.order.PayOrderRespDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderDisplayModeEnum; +import com.yunxi.scm.framework.pay.core.enums.order.PayOrderStatusRespEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbAndRedisUnitTest; +import com.yunxi.scm.module.pay.api.order.dto.PayOrderCreateReqDTO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderExportReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderPageReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitReqVO; +import com.yunxi.scm.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import com.yunxi.scm.module.pay.dal.mysql.order.PayOrderExtensionMapper; +import com.yunxi.scm.module.pay.dal.mysql.order.PayOrderMapper; +import com.yunxi.scm.module.pay.dal.redis.no.PayNoRedisDAO; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyTypeEnum; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.yunxi.scm.module.pay.framework.pay.config.PayProperties; +import com.yunxi.scm.module.pay.service.app.PayAppService; +import com.yunxi.scm.module.pay.service.channel.PayChannelService; +import com.yunxi.scm.module.pay.service.notify.PayNotifyService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.*; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * {@link PayOrderServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayOrderServiceImpl.class, PayNoRedisDAO.class}) +public class PayOrderServiceTest extends BaseDbAndRedisUnitTest { + + @Resource + private PayOrderServiceImpl orderService; + + @Resource + private PayOrderMapper orderMapper; + @Resource + private PayOrderExtensionMapper orderExtensionMapper; + + @MockBean + private PayClientFactory payClientFactory; + @MockBean + private PayProperties properties; + @MockBean + private PayAppService appService; + @MockBean + private PayChannelService channelService; + @MockBean + private PayNotifyService notifyService; + + @BeforeEach + public void setUp() { + when(properties.getOrderNotifyUrl()).thenReturn("http://127.0.0.1"); + } + + @Test + public void testGetOrder_id() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + + // 调用 + PayOrderDO dbOrder = orderService.getOrder(id); + // 断言 + assertPojoEquals(dbOrder, order); + } + + @Test + public void testGetOrder_appIdAndMerchantOrderId() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class); + orderMapper.insert(order); + // 准备参数 + Long appId = order.getAppId(); + String merchantOrderId = order.getMerchantOrderId(); + + // 调用 + PayOrderDO dbOrder = orderService.getOrder(appId, merchantOrderId); + // 断言 + assertPojoEquals(dbOrder, order); + } + + @Test + public void testGetOrderCountByAppId() { + // mock 数据(PayOrderDO) + PayOrderDO order01 = randomPojo(PayOrderDO.class); + orderMapper.insert(order01); + PayOrderDO order02 = randomPojo(PayOrderDO.class); + orderMapper.insert(order02); + // 准备参数 + Long appId = order01.getAppId(); + + // 调用 + Long count = orderService.getOrderCountByAppId(appId); + // 断言 + assertEquals(count, 1L); + } + + @Test + public void testGetOrderPage() { + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("110"); + o.setChannelOrderNo("220"); + o.setNo("330"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setCreateTime(buildTime(2018, 1, 15)); + }); + orderMapper.insert(dbOrder); + // 测试 appId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setMerchantOrderId(randomString()))); + // 测试 channelOrderNo 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelOrderNo(randomString()))); + // 测试 no 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setNo(randomString()))); + // 测试 status 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); + // 测试 createTime 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(buildTime(2019, 1, 1)))); + // 准备参数 + PayOrderPageReqVO reqVO = new PayOrderPageReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("11"); + reqVO.setChannelOrderNo("22"); + reqVO.setNo("33"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2018, 1, 10, 2018, 1, 30)); + + // 调用 + PageResult pageResult = orderService.getOrderPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbOrder, pageResult.getList().get(0)); + } + + @Test + public void testGetOrderList() { + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("110"); + o.setChannelOrderNo("220"); + o.setNo("330"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setCreateTime(buildTime(2018, 1, 15)); + }); + orderMapper.insert(dbOrder); + // 测试 appId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setMerchantOrderId(randomString()))); + // 测试 channelOrderNo 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setChannelOrderNo(randomString()))); + // 测试 no 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setNo(randomString()))); + // 测试 status 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()))); + // 测试 createTime 不匹配 + orderMapper.insert(cloneIgnoreId(dbOrder, o -> o.setCreateTime(buildTime(2019, 1, 1)))); + // 准备参数 + PayOrderExportReqVO reqVO = new PayOrderExportReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("11"); + reqVO.setChannelOrderNo("22"); + reqVO.setNo("33"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2018, 1, 10, 2018, 1, 30)); + + // 调用 + List list = orderService.getOrderList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbOrder, list.get(0)); + } + + @Test + public void testCreateOrder_success() { + // mock 参数 + PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("10") + .setSubject(randomString()).setBody(randomString())); + // mock 方法 + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L).setOrderNotifyUrl("http://127.0.0.1")); + when(appService.validPayApp(eq(reqDTO.getAppId()))).thenReturn(app); + + // 调用 + Long orderId = orderService.createOrder(reqDTO); + // 断言 + PayOrderDO order = orderMapper.selectById(orderId); + assertPojoEquals(order, reqDTO); + assertEquals(order.getAppId(), 1L); + assertEquals(order.getNotifyUrl(), "http://127.0.0.1"); + assertEquals(order.getStatus(), PayOrderStatusEnum.WAITING.getStatus()); + assertEquals(order.getRefundPrice(), 0); + } + + @Test + public void testCreateOrder_exists() { + // mock 参数 + PayOrderCreateReqDTO reqDTO = randomPojo(PayOrderCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("10")); + // mock 数据 + PayOrderDO dbOrder = randomPojo(PayOrderDO.class, o -> o.setAppId(1L).setMerchantOrderId("10")); + orderMapper.insert(dbOrder); + + // 调用 + Long orderId = orderService.createOrder(reqDTO); + // 断言 + PayOrderDO order = orderMapper.selectById(orderId); + assertPojoEquals(dbOrder, order); + } + + @Test + public void testSubmitOrder_notFound() { + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_NOT_FOUND); + } + + @Test + public void testSubmitOrder_notWaiting() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus())); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_NOT_WAITING); + } + + @Test + public void testSubmitOrder_isSuccess() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_STATUS_IS_SUCCESS); + } + + @Test + public void testSubmitOrder_expired() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofDays(-1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId())); + String userIp = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), ORDER_IS_EXPIRED); + } + + @Test + public void testSubmitOrder_channelNotFound() { + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + + // 调用, 并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), CHANNEL_NOT_FOUND); + } + + @Test // 调用 unifiedOrder 接口,返回存在渠道错误 + public void testSubmitOrder_channelError() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法() + PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> + o.setChannelErrorCode("001").setChannelErrorMsg("模拟异常")); + when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> { + assertNotNull(payOrderUnifiedReqDTO.getOutTradeNo()); + assertThat(payOrderUnifiedReqDTO) + .extracting("subject", "body", "notifyUrl", "returnUrl", "price", "expireTime") + .containsExactly(order.getSubject(), order.getBody(), "http://127.0.0.1/10", + reqVO.getReturnUrl(), order.getPrice(), order.getExpireTime()); + return true; + }))).thenReturn(unifiedOrderResp); + + // 调用,并断言异常 + assertServiceException(() -> orderService.submitOrder(reqVO, userIp), + ORDER_SUBMIT_CHANNEL_ERROR, "001", "模拟异常"); + // 断言,数据记录(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null); + assertNotNull(orderExtension); + assertThat(orderExtension).extracting("no", "orderId").isNotNull(); + assertThat(orderExtension) + .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras", + "channelErrorCode", "channelErrorMsg", "channelNotifyData") + .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp, + PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(), + null, null, null); + } + } + + @Test + public void testSubmitOrder_success() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setAppId(1L).setExpireTime(addTime(Duration.ofDays(1)))); + orderMapper.insert(order); + // 准备参数 + PayOrderSubmitReqVO reqVO = randomPojo(PayOrderSubmitReqVO.class, o -> o.setId(order.getId()) + .setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + String userIp = randomString(); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L), eq(PayChannelEnum.ALIPAY_APP.getCode()))) + .thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(支付渠道的调用) + PayOrderRespDTO unifiedOrderResp = randomPojo(PayOrderRespDTO.class, o -> o.setChannelErrorCode(null).setChannelErrorMsg(null) + .setDisplayMode(PayOrderDisplayModeEnum.URL.getMode()).setDisplayContent("tudou")); + when(client.unifiedOrder(argThat(payOrderUnifiedReqDTO -> { + assertNotNull(payOrderUnifiedReqDTO.getOutTradeNo()); + assertThat(payOrderUnifiedReqDTO) + .extracting("subject", "body", "notifyUrl", "returnUrl", "price", "expireTime") + .containsExactly(order.getSubject(), order.getBody(), "http://127.0.0.1/10", + reqVO.getReturnUrl(), order.getPrice(), order.getExpireTime()); + return true; + }))).thenReturn(unifiedOrderResp); + + // 调用 + PayOrderSubmitRespVO result = orderService.submitOrder(reqVO, userIp); + // 断言,数据记录(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = orderExtensionMapper.selectOne(null); + assertNotNull(orderExtension); + assertThat(orderExtension).extracting("no", "orderId").isNotNull(); + assertThat(orderExtension) + .extracting("channelId", "channelCode","userIp" ,"status", "channelExtras", + "channelErrorCode", "channelErrorMsg", "channelNotifyData") + .containsExactly(10L, PayChannelEnum.ALIPAY_APP.getCode(), userIp, + PayOrderStatusEnum.WAITING.getStatus(), reqVO.getChannelExtras(), + null, null, null); + // 断言,返回(PayOrderSubmitRespVO) + assertThat(result) + .extracting("status", "displayMode", "displayContent") + .containsExactly(PayOrderStatusEnum.WAITING.getStatus(), PayOrderDisplayModeEnum.URL.getMode(), "tudou"); + // 断言,调用 + verify(payOrderServiceImpl).notifyOrder(same(channel), same(unifiedOrderResp)); + } + } + + @Test + public void testValidateOrderActuallyPaid_dbPaid() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderExtensionMapper.insert(orderExtension); + + // 调用,并断言异常 + assertServiceException(() -> orderService.validateOrderActuallyPaid(id), + ORDER_EXTENSION_IS_PAID); + } + + @Test + public void testValidateOrderActuallyPaid_remotePaid() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient 已支付) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client); + when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()))); + + // 调用,并断言异常 + assertServiceException(() -> orderService.validateOrderActuallyPaid(id), + ORDER_EXTENSION_IS_PAID); + } + + @Test + public void testValidateOrderActuallyPaid_success() { + // 准备参数 + Long id = randomLongId(); + // mock 方法(OrderExtension 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setOrderId(id).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient 已支付) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(orderExtension.getChannelId()))).thenReturn(client); + when(client.getOrder(eq(orderExtension.getNo()))).thenReturn(randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + + // 调用,并断言异常 + orderService.validateOrderActuallyPaid(id); + } + + @Test + public void testNotifyOrder_channelId() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + // 准备参数 + Long channelId = 10L; + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + orderService.notifyOrder(channelId, notify); + // 断言 + verify(payOrderServiceImpl).notifyOrder(same(channel), same(notify)); + } + } + + @Test + public void testNotifyOrderSuccess_orderExtension_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_NOT_FOUND); + } + + @Test + public void testNotifyOrderSuccess_orderExtension_closed() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyOrderSuccess_order_notFound() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_NOT_FOUND); + // 断言 PayOrderExtensionDO :数据更新被回滚 + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderSuccess_order_closed() { + testNotifyOrderSuccess_order_closedOrRefund(PayOrderStatusEnum.CLOSED.getStatus()); + } + + @Test + public void testNotifyOrderSuccess_order_refund() { + testNotifyOrderSuccess_order_closedOrRefund(PayOrderStatusEnum.REFUND.getStatus()); + } + + private void testNotifyOrderSuccess_order_closedOrRefund(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status)); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_STATUS_IS_NOT_WAITING); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderSuccess_order_paid() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 重要:需要将 order 的 extensionId 更新下 + order.setExtensionId(orderExtension.getId()); + orderMapper.updateById(order); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + // 断言 PayOrderDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(order, orderMapper.selectOne(null)); + // 断言,调用 + verify(notifyService, never()).createPayNotifyTask(anyInt(), anyLong()); + } + + @Test + public void testNotifyOrderSuccess_order_waiting() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setPrice(10)); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setNo("P110") + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setFeeRate(0.1D)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.SUCCESS.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + orderExtension.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(notify)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + // 断言 PayOrderDO :数据未更新,因为它是 SUCCESS + order.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setChannelId(10L).setChannelCode(channel.getCode()) + .setSuccessTime(notify.getSuccessTime()).setExtensionId(orderExtension.getId()).setNo(orderExtension.getNo()) + .setChannelOrderNo(notify.getChannelOrderNo()).setChannelUserId(notify.getChannelUserId()) + .setChannelFeeRate(0.1D).setChannelFeePrice(1); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + // 断言,调用 + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.ORDER.getType()), + eq(orderExtension.getOrderId())); + } + + @Test + public void testNotifyOrderClosed_orderExtension_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus())); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_NOT_FOUND); + } + + @Test + public void testNotifyOrderClosed_orderExtension_closed() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 CLOSED + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderClosed_orderExtension_paid() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO :数据未更新,因为它是 SUCCESS + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null)); + } + + @Test + public void testNotifyOrderClosed_orderExtension_refund() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用,并断言异常 + assertServiceException(() -> orderService.notifyOrder(channel, notify), + ORDER_EXTENSION_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyOrderClosed_orderExtension_waiting() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setNo("P110")); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + PayOrderRespDTO notify = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusRespEnum.CLOSED.getStatus()) + .setOutTradeNo("P110")); + + // 调用 + orderService.notifyOrder(channel, notify); + // 断言 PayOrderExtensionDO + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()).setChannelNotifyData(toJsonString(notify)) + .setChannelErrorCode(notify.getChannelErrorCode()).setChannelErrorMsg(notify.getChannelErrorMsg()); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + } + + @Test + public void testUpdateOrderRefundPrice_notFound() { + // 准备参数 + Long id = randomLongId(); + Integer incrRefundPrice = randomInteger(); + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + ORDER_NOT_FOUND); + } + + @Test + public void testUpdateOrderRefundPrice_waiting() { + testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.WAITING.getStatus()); + } + + @Test + public void testUpdateOrderRefundPrice_closed() { + testUpdateOrderRefundPrice_waitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus()); + } + + private void testUpdateOrderRefundPrice_waitingOrClosed(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(status)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = randomInteger(); + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testUpdateOrderRefundPrice_priceExceed() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setRefundPrice(1).setPrice(10)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = 10; + + // 调用,并断言异常 + assertServiceException(() -> orderService.updateOrderRefundPrice(id, incrRefundPrice), + REFUND_PRICE_EXCEED); + } + + @Test + public void testUpdateOrderRefundPrice_refund() { + testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.REFUND.getStatus()); + } + + @Test + public void testUpdateOrderRefundPrice_success() { + testUpdateOrderRefundPrice_refundOrSuccess(PayOrderStatusEnum.SUCCESS.getStatus()); + } + + private void testUpdateOrderRefundPrice_refundOrSuccess(Integer status) { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(status).setRefundPrice(1).setPrice(10)); + orderMapper.insert(order); + // 准备参数 + Long id = order.getId(); + Integer incrRefundPrice = 8; + + // 调用 + orderService.updateOrderRefundPrice(id, incrRefundPrice); + // 断言 + order.setRefundPrice(9).setStatus(PayOrderStatusEnum.REFUND.getStatus()); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + } + + @Test + public void testGetOrderExtension() { + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class); + orderExtensionMapper.insert(orderExtension); + // 准备参数 + Long id = orderExtension.getId(); + + // 调用 + PayOrderExtensionDO dbOrderExtension = orderService.getOrderExtension(id); + // 断言 + assertPojoEquals(dbOrderExtension, orderExtension); + } + + @Test + public void testSyncOrder_payClientNotFound() { + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncOrder_exception() { + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L) + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 异常) + when(client.getOrder(any())).thenThrow(new RuntimeException()); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncOrder_orderSuccess() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L).setNo("P110") + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 1); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testSyncOrder_orderClosed() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // 准备参数 + LocalDateTime minCreateTime = LocalDateTime.now().minus(Duration.ofMinutes(10)); + // mock 数据(PayOrderExtensionDO) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setChannelId(10L).setNo("P110") + .setCreateTime(LocalDateTime.now())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.syncOrder(minCreateTime); + // 断言 + assertEquals(count, 0); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testExpireOrder_orderExtension_isSuccess() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 已支付) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()) + .setOrderId(order.getId())); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_payClient_notFound() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()) + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_getOrder_isRefund() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 退款返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.REFUND.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + } + + @Test + public void testExpireOrder_getOrder_isSuccess() { + PayOrderServiceImpl payOrderServiceImpl = mock(PayOrderServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayOrderServiceImpl.class))) + .thenReturn(payOrderServiceImpl); + + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 成功返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + // mock 方法(PayChannelDO) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 0); + // 断言 order 没有变化,因为没更新 + assertPojoEquals(order, orderMapper.selectOne(null)); + verify(payOrderServiceImpl).notifyOrder(same(channel), same(respDTO)); + } + } + + @Test + public void testExpireOrder_success() { + // mock 数据(PayOrderDO) + PayOrderDO order = randomPojo(PayOrderDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setExpireTime(addTime(Duration.ofMinutes(-1)))); + orderMapper.insert(order); + // mock 数据(PayOrderExtensionDO 等待中) + PayOrderExtensionDO orderExtension = randomPojo(PayOrderExtensionDO.class, + o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()) + .setOrderId(order.getId()).setNo("P110") + .setChannelId(10L)); + orderExtensionMapper.insert(orderExtension); + // mock 方法(PayClient) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(PayClient 关闭返回) + PayOrderRespDTO respDTO = randomPojo(PayOrderRespDTO.class, + o -> o.setStatus(PayOrderStatusEnum.CLOSED.getStatus())); + when(client.getOrder(eq("P110"))).thenReturn(respDTO); + + // 调用 + int count = orderService.expireOrder(); + // 断言 + assertEquals(count, 1); + // 断言 extension 变化 + orderExtension.setStatus(PayOrderStatusEnum.CLOSED.getStatus()) + .setChannelNotifyData(toJsonString(respDTO)); + assertPojoEquals(orderExtension, orderExtensionMapper.selectOne(null), + "updateTime", "updater"); + // 断言 order 变化 + order.setStatus(PayOrderStatusEnum.CLOSED.getStatus()); + assertPojoEquals(order, orderMapper.selectOne(null), + "updateTime", "updater"); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/refund/PayRefundServiceTest.java b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/refund/PayRefundServiceTest.java new file mode 100644 index 0000000..c662b43 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/java/com/yunxi/scm/module/pay/service/refund/PayRefundServiceTest.java @@ -0,0 +1,703 @@ +package com.yunxi.scm.module.pay.service.refund; + +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.pay.core.client.PayClient; +import com.yunxi.scm.framework.pay.core.client.PayClientFactory; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundRespDTO; +import com.yunxi.scm.framework.pay.core.client.dto.refund.PayRefundUnifiedReqDTO; +import com.yunxi.scm.framework.pay.core.enums.channel.PayChannelEnum; +import com.yunxi.scm.framework.pay.core.enums.refund.PayRefundStatusRespEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbAndRedisUnitTest; +import com.yunxi.scm.module.pay.api.refund.dto.PayRefundCreateReqDTO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundExportReqVO; +import com.yunxi.scm.module.pay.controller.admin.refund.vo.PayRefundPageReqVO; +import com.yunxi.scm.module.pay.dal.dataobject.app.PayAppDO; +import com.yunxi.scm.module.pay.dal.dataobject.channel.PayChannelDO; +import com.yunxi.scm.module.pay.dal.dataobject.order.PayOrderDO; +import com.yunxi.scm.module.pay.dal.dataobject.refund.PayRefundDO; +import com.yunxi.scm.module.pay.dal.mysql.refund.PayRefundMapper; +import com.yunxi.scm.module.pay.dal.redis.no.PayNoRedisDAO; +import com.yunxi.scm.module.pay.enums.notify.PayNotifyTypeEnum; +import com.yunxi.scm.module.pay.enums.order.PayOrderStatusEnum; +import com.yunxi.scm.module.pay.enums.refund.PayRefundStatusEnum; +import com.yunxi.scm.module.pay.framework.pay.config.PayProperties; +import com.yunxi.scm.module.pay.service.app.PayAppService; +import com.yunxi.scm.module.pay.service.channel.PayChannelService; +import com.yunxi.scm.module.pay.service.notify.PayNotifyService; +import com.yunxi.scm.module.pay.service.order.PayOrderService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static com.yunxi.scm.module.pay.enums.ErrorCodeConstants.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +/** + * {@link PayRefundServiceImpl} 的单元测试类 + * + * @author 芋艿 + */ +@Import({PayRefundServiceImpl.class, PayNoRedisDAO.class}) +public class PayRefundServiceTest extends BaseDbAndRedisUnitTest { + + @Resource + private PayRefundServiceImpl refundService; + + @Resource + private PayRefundMapper refundMapper; + + @MockBean + private PayProperties payProperties; + @MockBean + private PayClientFactory payClientFactory; + @MockBean + private PayOrderService orderService; + @MockBean + private PayAppService appService; + @MockBean + private PayChannelService channelService; + @MockBean + private PayNotifyService notifyService; + + @BeforeEach + public void setUp() { + when(payProperties.getRefundNotifyUrl()).thenReturn("http://127.0.0.1"); + } + + @Test + public void testGetRefund() { + // mock 数据 + PayRefundDO refund = randomPojo(PayRefundDO.class); + refundMapper.insert(refund); + // 准备参数 + Long id = refund.getId(); + + // 调用 + PayRefundDO dbRefund = refundService.getRefund(id); + // 断言 + assertPojoEquals(dbRefund, refund); + } + + @Test + public void testGetRefundCountByAppId() { + // mock 数据 + PayRefundDO refund01 = randomPojo(PayRefundDO.class); + refundMapper.insert(refund01); + PayRefundDO refund02 = randomPojo(PayRefundDO.class); + refundMapper.insert(refund02); + // 准备参数 + Long appId = refund01.getAppId(); + + // 调用 + Long count = refundService.getRefundCountByAppId(appId); + // 断言 + assertEquals(count, 1); + } + + @Test + public void testGetRefundPage() { + // mock 数据 + PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("MOT0000001"); + o.setMerchantRefundId("MRF0000001"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setChannelOrderNo("CH0000001"); + o.setChannelRefundNo("CHR0000001"); + o.setCreateTime(buildTime(2021, 1, 10)); + }); + refundMapper.insert(dbRefund); + // 测试 appId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString()))); + // 测试 merchantRefundId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString()))); + // 测试 channelOrderNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString()))); + // 测试 channelRefundNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString()))); + // 测试 status 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + // 测试 createTime 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + PayRefundPageReqVO reqVO = new PayRefundPageReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("MOT0000001"); + reqVO.setMerchantRefundId("MRF0000001"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setChannelOrderNo("CH0000001"); + reqVO.setChannelRefundNo("CHR0000001"); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11)); + + // 调用 + PageResult pageResult = refundService.getRefundPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbRefund, pageResult.getList().get(0)); + } + + @Test + public void testGetRefundList() { + // mock 数据 + PayRefundDO dbRefund = randomPojo(PayRefundDO.class, o -> { // 等会查询到 + o.setAppId(1L); + o.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + o.setMerchantOrderId("MOT0000001"); + o.setMerchantRefundId("MRF0000001"); + o.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + o.setChannelOrderNo("CH0000001"); + o.setChannelRefundNo("CHR0000001"); + o.setCreateTime(buildTime(2021, 1, 10)); + }); + refundMapper.insert(dbRefund); + // 测试 appId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setAppId(2L))); + // 测试 channelCode 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelCode(PayChannelEnum.ALIPAY_APP.getCode()))); + // 测试 merchantOrderId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantOrderId(randomString()))); + // 测试 merchantRefundId 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setMerchantRefundId(randomString()))); + // 测试 channelOrderNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelOrderNo(randomString()))); + // 测试 channelRefundNo 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setChannelRefundNo(randomString()))); + // 测试 status 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setStatus(PayOrderStatusEnum.WAITING.getStatus()))); + // 测试 createTime 不匹配 + refundMapper.insert(cloneIgnoreId(dbRefund, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + PayRefundExportReqVO reqVO = new PayRefundExportReqVO(); + reqVO.setAppId(1L); + reqVO.setChannelCode(PayChannelEnum.WX_PUB.getCode()); + reqVO.setMerchantOrderId("MOT0000001"); + reqVO.setMerchantRefundId("MRF0000001"); + reqVO.setStatus(PayOrderStatusEnum.SUCCESS.getStatus()); + reqVO.setChannelOrderNo("CH0000001"); + reqVO.setChannelRefundNo("CHR0000001"); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 9, 2021, 1, 11)); + + // 调用 + List list = refundService.getRefundList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbRefund, list.get(0)); + } + + @Test + public void testCreateRefund_orderNotFound() { + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + ORDER_NOT_FOUND); + } + + @Test + public void testCreateRefund_orderWaiting() { + testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.WAITING.getStatus()); + } + + @Test + public void testCreateRefund_orderClosed() { + testCreateRefund_orderWaitingOrClosed(PayOrderStatusEnum.CLOSED.getStatus()); + } + + private void testCreateRefund_orderWaitingOrClosed(Integer status) { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> o.setStatus(status)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + ORDER_REFUND_FAIL_STATUS_ERROR); + } + + @Test + public void testCreateRefund_refundPriceExceed() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_PRICE_EXCEED); + } + + @Test + public void testCreateRefund_orderHasRefunding() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(10)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1)); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 数据(refund 在退款中) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> + o.setOrderId(order.getId()).setStatus(PayOrderStatusEnum.WAITING.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_PRICE_EXCEED); + } + + @Test + public void testCreateRefund_channelNotFound() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9)); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L))).thenReturn(channel); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + CHANNEL_NOT_FOUND); + } + + @Test + public void testCreateRefund_refundExists() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(1L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(1L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 数据(refund 已存在) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> + o.setAppId(1L).setMerchantRefundId("200")); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.createPayRefund(reqDTO), + REFUND_EXISTS); + } + + @Test + public void testCreateRefund_invokeException() { + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(10L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 调用发生异常) + when(client.unifiedRefund(any(PayRefundUnifiedReqDTO.class))).thenThrow(new RuntimeException()); + + // 调用 + Long refundId = refundService.createPayRefund(reqDTO); + // 断言 + PayRefundDO refundDO = refundMapper.selectById(refundId); + assertPojoEquals(reqDTO, refundDO); + assertNotNull(refundDO.getNo()); + assertThat(refundDO) + .extracting("orderId", "orderNo", "channelId", "channelCode", + "notifyUrl", "channelOrderNo", "status", "payPrice", "refundPrice") + .containsExactly(order.getId(), order.getNo(), channel.getId(), channel.getCode(), + app.getRefundNotifyUrl(), order.getChannelOrderNo(), PayRefundStatusEnum.WAITING.getStatus(), + order.getPrice(), reqDTO.getPrice()); + } + + @Test + public void testCreateRefund_invokeSuccess() { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + PayRefundCreateReqDTO reqDTO = randomPojo(PayRefundCreateReqDTO.class, + o -> o.setAppId(1L).setMerchantOrderId("100").setPrice(9) + .setMerchantRefundId("200").setReason("测试退款")); + // mock 方法(app) + PayAppDO app = randomPojo(PayAppDO.class, o -> o.setId(1L)); + when(appService.validPayApp(eq(1L))).thenReturn(app); + // mock 数据(order) + PayOrderDO order = randomPojo(PayOrderDO.class, o -> + o.setStatus(PayOrderStatusEnum.REFUND.getStatus()) + .setPrice(10).setRefundPrice(1) + .setChannelId(10L).setChannelCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(orderService.getOrder(eq(1L), eq("100"))).thenReturn(order); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L) + .setCode(PayChannelEnum.ALIPAY_APP.getCode())); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 成功) + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class); + when(client.unifiedRefund(argThat(unifiedReqDTO -> { + assertNotNull(unifiedReqDTO.getOutRefundNo()); + assertThat(unifiedReqDTO) + .extracting("payPrice", "refundPrice", "outTradeNo", + "notifyUrl", "reason") + .containsExactly(order.getPrice(), reqDTO.getPrice(), order.getNo(), + "http://127.0.0.1/10", reqDTO.getReason()); + return true; + }))).thenReturn(refundRespDTO); + + // 调用 + Long refundId = refundService.createPayRefund(reqDTO); + // 断言 + PayRefundDO refundDO = refundMapper.selectById(refundId); + assertPojoEquals(reqDTO, refundDO); + assertNotNull(refundDO.getNo()); + assertThat(refundDO) + .extracting("orderId", "orderNo", "channelId", "channelCode", + "notifyUrl", "channelOrderNo", "status", "payPrice", "refundPrice") + .containsExactly(order.getId(), order.getNo(), channel.getId(), channel.getCode(), + app.getRefundNotifyUrl(), order.getChannelOrderNo(), PayRefundStatusEnum.WAITING.getStatus(), + order.getPrice(), reqDTO.getPrice()); + // 断言调用 + verify(payRefundServiceImpl).notifyRefund(same(channel), same(refundRespDTO)); + } + } + + @Test + public void testNotifyRefund() { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + Long channelId = 10L; + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + refundService.notifyRefund(channelId, refundRespDTO); + // 断言 + verify(payRefundServiceImpl).notifyRefund(same(channel), same(refundRespDTO)); + } + } + + @Test + public void testNotifyRefundSuccess_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_NOT_FOUND); + } + + @Test + public void testNotifyRefundSuccess_isSuccess() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund 没有更新,因为已经退款成功 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundSuccess_failure() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.FAILURE.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyRefundSuccess_updateOrderException() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + // mock 方法(order + 更新异常) + doThrow(new RuntimeException()).when(orderService) + .updateOrderRefundPrice(eq(100L), eq(23)); + + // 调用,并断言异常 + assertThrows(RuntimeException.class, () -> refundService.notifyRefund(channel, refundRespDTO)); + // 断言,refund 没有更新,因为事务回滚了 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundSuccess_success() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.SUCCESS.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund + refund.setSuccessTime(refundRespDTO.getSuccessTime()) + .setChannelRefundNo(refundRespDTO.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus()) + .setChannelNotifyData(toJsonString(refundRespDTO)); + assertPojoEquals(refund, refundMapper.selectById(refund.getId()), + "updateTime", "updater"); + // 断言,调用 + verify(orderService).updateOrderRefundPrice(eq(100L), eq(23)); + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.REFUND.getType()), + eq(refund.getId())); + } + + @Test + public void testNotifyRefundFailure_notFound() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_NOT_FOUND); + } + + @Test + public void testNotifyRefundFailure_isFailure() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 退款失败) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.FAILURE.getStatus())); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund 没有更新,因为已经退款失败 + assertPojoEquals(refund, refundMapper.selectById(refund.getId())); + } + + @Test + public void testNotifyRefundFailure_isSuccess() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.SUCCESS.getStatus())); + refundMapper.insert(refund); + + // 调用,并断言异常 + assertServiceException(() -> refundService.notifyRefund(channel, refundRespDTO), + REFUND_STATUS_IS_NOT_WAITING); + } + + @Test + public void testNotifyRefundFailure_success() { + // 准备参数 + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L).setAppId(1L)); + PayRefundRespDTO refundRespDTO = randomPojo(PayRefundRespDTO.class, + o -> o.setStatus(PayRefundStatusRespEnum.FAILURE.getStatus()).setOutRefundNo("R100")); + // mock 数据(refund + 已支付) + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setNo("R100") + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderId(100L).setRefundPrice(23)); + refundMapper.insert(refund); + + // 调用 + refundService.notifyRefund(channel, refundRespDTO); + // 断言,refund + refund.setChannelRefundNo(refundRespDTO.getChannelRefundNo()) + .setStatus(PayRefundStatusEnum.FAILURE.getStatus()) + .setChannelNotifyData(toJsonString(refundRespDTO)) + .setChannelErrorCode(refundRespDTO.getChannelErrorCode()) + .setChannelErrorMsg(refundRespDTO.getChannelErrorMsg()); + assertPojoEquals(refund, refundMapper.selectById(refund.getId()), + "updateTime", "updater"); + // 断言,调用 + verify(notifyService).createPayNotifyTask(eq(PayNotifyTypeEnum.REFUND.getType()), + eq(refund.getId())); + } + + @Test + public void testSyncRefund_notFound() { + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus())); + refundMapper.insert(refund); + + // 调用 + int count = refundService.syncRefund(); + // 断言 + assertEquals(count, 0); + } + + @Test + public void testSyncRefund_waiting() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.WAITING.getStatus()), 0); + } + + @Test + public void testSyncRefund_success() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.SUCCESS.getStatus()), 1); + } + + @Test + public void testSyncRefund_failure() { + assertEquals(testSyncRefund_waitingOrSuccessOrFailure(PayRefundStatusRespEnum.FAILURE.getStatus()), 1); + } + + private int testSyncRefund_waitingOrSuccessOrFailure(Integer status) { + PayRefundServiceImpl payRefundServiceImpl = mock(PayRefundServiceImpl.class); + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PayRefundServiceImpl.class))) + .thenReturn(payRefundServiceImpl); + + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setChannelId(10L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderNo("P110").setNo("R220")); + refundMapper.insert(refund); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 返回指定状态) + PayRefundRespDTO respDTO = randomPojo(PayRefundRespDTO.class, o -> o.setStatus(status)); + when(client.getRefund(eq("P110"), eq("R220"))).thenReturn(respDTO); + // mock 方法(channel) + PayChannelDO channel = randomPojo(PayChannelDO.class, o -> o.setId(10L)); + when(channelService.validPayChannel(eq(10L))).thenReturn(channel); + + // 调用 + return refundService.syncRefund(); + } + } + + @Test + public void testSyncRefund_exception() { + // 准备参数 + PayRefundDO refund = randomPojo(PayRefundDO.class, o -> o.setAppId(1L).setChannelId(10L) + .setStatus(PayRefundStatusEnum.WAITING.getStatus()) + .setOrderNo("P110").setNo("R220")); + refundMapper.insert(refund); + // mock 方法(client) + PayClient client = mock(PayClient.class); + when(payClientFactory.getPayClient(eq(10L))).thenReturn(client); + // mock 方法(client 抛出异常) + when(client.getRefund(eq("P110"), eq("R220"))).thenThrow(new RuntimeException()); + + // 调用 + int count = refundService.syncRefund(); + // 断言 + assertEquals(count, 0); + } + +} diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..c81dad6 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,49 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm.module diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/logback.xml b/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/sql/clean.sql b/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..91fff0c --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,7 @@ +DELETE FROM pay_app; +DELETE FROM pay_channel; +DELETE FROM pay_order; +DELETE FROM pay_order_extension; +DELETE FROM pay_refund; +DELETE FROM pay_notify_task; +DELETE FROM pay_notify_log; diff --git a/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..6ae2ce2 --- /dev/null +++ b/yunxi-module-pay/yunxi-module-pay-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,146 @@ +CREATE TABLE IF NOT EXISTS "pay_app" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(64) NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(255) DEFAULT NULL, + `order_notify_url` varchar(1024) NOT NULL, + `refund_notify_url` varchar(1024) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '支付应用'; + +CREATE TABLE IF NOT EXISTS "pay_channel" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(32) NOT NULL, + "status" tinyint(4) NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "fee_rate" double NOT NULL DEFAULT 0, + "app_id" bigint(20) NOT NULL, + "config" varchar(10240) NOT NULL, + "creator" varchar(64) NULL DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) NULL DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit(1) NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT = '支付渠道'; + +CREATE TABLE IF NOT EXISTS `pay_order` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) DEFAULT NULL, + `channel_code` varchar(32) DEFAULT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `subject` varchar(32) NOT NULL, + `body` varchar(128) NOT NULL, + `notify_url` varchar(1024) NOT NULL, + `price` bigint(20) NOT NULL, + `channel_fee_rate` double DEFAULT 0, + `channel_fee_price` bigint(20) DEFAULT 0, + `status` tinyint(4) NOT NULL, + `user_ip` varchar(50) NOT NULL, + `expire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `success_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, + `notify_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, + `extension_id` bigint(20) DEFAULT NULL, + `no` varchar(64) NULL, + `refund_price` bigint(20) NOT NULL, + `channel_user_id` varchar(255) DEFAULT NULL, + `channel_order_no` varchar(64) DEFAULT NULL, + `creator` varchar(64) DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '支付订单'; + +CREATE TABLE IF NOT EXISTS `pay_order_extension` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `order_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `user_ip` varchar(50) NULL DEFAULT NULL, + `status` tinyint(4) NOT NULL, + `channel_extras` varchar(1024) NULL DEFAULT NULL, + `channel_error_code` varchar(64) NULL, + `channel_error_msg` varchar(64) NULL, + `channel_notify_data` varchar(1024) NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '支付订单拓展'; + +CREATE TABLE IF NOT EXISTS `pay_refund` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `order_id` bigint(20) NOT NULL, + `order_no` varchar(64) NOT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `merchant_refund_id` varchar(64) NOT NULL, + `notify_url` varchar(1024) NOT NULL, + `status` tinyint(4) NOT NULL, + `pay_price` bigint(20) NOT NULL, + `refund_price` bigint(20) NOT NULL, + `reason` varchar(256) NOT NULL, + `user_ip` varchar(50) NULL DEFAULT NULL, + `channel_order_no` varchar(64) NOT NULL, + `channel_refund_no` varchar(64) NULL DEFAULT NULL, + `success_time` datetime(0) NULL DEFAULT NULL, + `channel_error_code` varchar(128) NULL DEFAULT NULL, + `channel_error_msg` varchar(256) NULL DEFAULT NULL, + `channel_notify_data` varchar(1024) NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '退款订单'; + +CREATE TABLE IF NOT EXISTS `pay_notify_task` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `app_id` bigint(20) NOT NULL, + `type` tinyint(4) NOT NULL, + `data_id` bigint(20) NOT NULL, + `merchant_order_id` varchar(64) NOT NULL, + `status` tinyint(4) NOT NULL, + `next_notify_time` datetime(0) NULL DEFAULT NULL, + `last_execute_time` datetime(0) NULL DEFAULT NULL, + `notify_times` int NOT NULL, + `max_notify_times` int NOT NULL, + `notify_url` varchar(1024) NOT NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + `tenant_id` bigint(20) NOT NULL DEFAULT 0, + PRIMARY KEY ("id") +) COMMENT = '支付通知任务'; + +CREATE TABLE IF NOT EXISTS `pay_notify_log` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `task_id` bigint(20) NOT NULL, + `notify_times` int NOT NULL, + `response` varchar(1024) NOT NULL, + `status` tinyint(4) NOT NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '支付通知日志'; diff --git a/yunxi-module-report/pom.xml b/yunxi-module-report/pom.xml new file mode 100644 index 0000000..8bce094 --- /dev/null +++ b/yunxi-module-report/pom.xml @@ -0,0 +1,23 @@ + + + + com.yunxi.scm + yunxi + ${revision} + + 4.0.0 + + yunxi-module-report-api + yunxi-module-report-biz + + yunxi-module-report + pom + + ${project.artifactId} + + report 模块,主要实现数据可视化报表等功能。 + + + diff --git a/yunxi-module-report/yunxi-module-report-api/pom.xml b/yunxi-module-report/yunxi-module-report-api/pom.xml new file mode 100644 index 0000000..6808292 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-api/pom.xml @@ -0,0 +1,26 @@ + + + + com.yunxi.scm + yunxi-module-report + ${revision} + + 4.0.0 + + yunxi-module-report-api + jar + + ${project.artifactId} + + report 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + diff --git a/yunxi-module-report/yunxi-module-report-api/src/main/java/com/yunxi/scm/module/report/api/package-info.java b/yunxi-module-report/yunxi-module-report-api/src/main/java/com/yunxi/scm/module/report/api/package-info.java new file mode 100644 index 0000000..f6f8391 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-api/src/main/java/com/yunxi/scm/module/report/api/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,避免 api 目录无文件时,git 无法提交 + */ +package com.yunxi.scm.module.report.api; diff --git a/yunxi-module-report/yunxi-module-report-api/src/main/java/com/yunxi/scm/module/report/enums/ErrorCodeConstants.java b/yunxi-module-report/yunxi-module-report-api/src/main/java/com/yunxi/scm/module/report/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..faf5ea0 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-api/src/main/java/com/yunxi/scm/module/report/enums/ErrorCodeConstants.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.report.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * Report 错误码枚举类 + * + * report 系统,使用 1-003-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== AUTH 模块 1003000000 ========== + ErrorCode GO_VIEW_PROJECT_NOT_EXISTS = new ErrorCode(1003000000, "GoView 项目不存在"); + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/pom.xml b/yunxi-module-report/yunxi-module-report-biz/pom.xml new file mode 100644 index 0000000..296d9b1 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/pom.xml @@ -0,0 +1,78 @@ + + + + com.yunxi.scm + yunxi-module-report + ${revision} + + 4.0.0 + + yunxi-module-report-biz + jar + + ${project.artifactId} + + report 模块,主要实现数据可视化报表等功能: + 1. 基于「积木报表」实现,打印设计、报表设计、图形设计、大屏设计等。 + + + + com.yunxi.scm + yunxi-module-report-api + ${revision} + + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-tenant + + + + + com.yunxi.scm + yunxi-spring-boot-starter-web + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + + + + + org.jeecgframework.jimureport + jimureport-spring-boot-starter + + + + xerces + xercesImpl + + + + diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/ajreport/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/ajreport/package-info.java new file mode 100644 index 0000000..d16b8b5 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/ajreport/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.report.controller.admin.ajreport; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/GoViewDataController.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/GoViewDataController.java new file mode 100644 index 0000000..8361862 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/GoViewDataController.java @@ -0,0 +1,66 @@ +package com.yunxi.scm.module.report.controller.admin.goview; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.report.controller.admin.goview.vo.data.GoViewDataGetBySqlReqVO; +import com.yunxi.scm.module.report.controller.admin.goview.vo.data.GoViewDataRespVO; +import com.yunxi.scm.module.report.service.goview.GoViewDataService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import java.util.*; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - GoView 数据", description = "提供 SQL、HTTP 等数据查询的能力") +@RestController +@RequestMapping("/report/go-view/data") +@Validated +public class GoViewDataController { + + @Resource + private GoViewDataService goViewDataService; + + @RequestMapping("/get-by-sql") + @Operation(summary = "使用 SQL 查询数据") + @PreAuthorize("@ss.hasPermission('report:go-view-data:get-by-sql')") + @OperateLog(enable = false) // 不记录操作日志,因为不需要 + public CommonResult getDataBySQL(@Valid @RequestBody GoViewDataGetBySqlReqVO reqVO) { + return success(goViewDataService.getDataBySQL(reqVO.getSql())); + } + + @RequestMapping("/get-by-http") + @Operation(summary = "使用 HTTP 查询数据", description = "这个只是示例接口,实际应该每个查询,都要写一个接口") + @PreAuthorize("@ss.hasPermission('report:go-view-data:get-by-http')") + @OperateLog(enable = false) // 不记录操作日志,因为不需要 + public CommonResult getDataByHttp( + @RequestParam(required = false) Map params, + @RequestBody(required = false) String body) { // params、body 按照需要去接收,这里仅仅是示例 + GoViewDataRespVO respVO = new GoViewDataRespVO(); + // 1. 数据维度 + respVO.setDimensions(Arrays.asList("日期", "PV", "UV")); // PV 是每天访问次数;UV 是每天访问人数 + // 2. 明细数据列表 + // 目前通过随机的方式生成。一般来说,这里你可以写逻辑来实现数据的返回 + respVO.setSource(new LinkedList<>()); + for (int i = 1; i <= 12; i++) { + String date = "2021-" + (i < 10 ? "0" + i : i); + Integer pv = RandomUtil.randomInt(1000, 10000); + Integer uv = RandomUtil.randomInt(100, 1000); + respVO.getSource().add(MapUtil.builder().put("日期", date) + .put("PV", pv).put("UV", uv).build()); + } + return success(respVO); + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/GoViewProjectController.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/GoViewProjectController.java new file mode 100644 index 0000000..8b80e93 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/GoViewProjectController.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.module.report.controller.admin.goview; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectRespVO; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import com.yunxi.scm.module.report.convert.goview.GoViewProjectConvert; +import com.yunxi.scm.module.report.dal.dataobject.goview.GoViewProjectDO; +import com.yunxi.scm.module.report.service.goview.GoViewProjectService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - GoView 项目") +@RestController +@RequestMapping("/report/go-view/project") +@Validated +public class GoViewProjectController { + + @Resource + private GoViewProjectService goViewProjectService; + + @PostMapping("/create") + @Operation(summary = "创建项目") + @PreAuthorize("@ss.hasPermission('report:go-view-project:create')") + public CommonResult createProject(@Valid @RequestBody GoViewProjectCreateReqVO createReqVO) { + return success(goViewProjectService.createProject(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新项目") + @PreAuthorize("@ss.hasPermission('report:go-view-project:update')") + public CommonResult updateProject(@Valid @RequestBody GoViewProjectUpdateReqVO updateReqVO) { + goViewProjectService.updateProject(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除 GoView 项目") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('report:go-view-project:delete')") + public CommonResult deleteProject(@RequestParam("id") Long id) { + goViewProjectService.deleteProject(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得项目") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('report:go-view-project:query')") + public CommonResult getProject(@RequestParam("id") Long id) { + GoViewProjectDO project = goViewProjectService.getProject(id); + return success(GoViewProjectConvert.INSTANCE.convert(project)); + } + + @GetMapping("/my-page") + @Operation(summary = "获得我的项目分页") + @PreAuthorize("@ss.hasPermission('report:go-view-project:query')") + public CommonResult> getMyProjectPage(@Valid PageParam pageVO) { + PageResult pageResult = goViewProjectService.getMyProjectPage( + pageVO, getLoginUserId()); + return success(GoViewProjectConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/data/GoViewDataGetBySqlReqVO.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/data/GoViewDataGetBySqlReqVO.java new file mode 100644 index 0000000..e806855 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/data/GoViewDataGetBySqlReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.report.controller.admin.goview.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - GoView 使用 SQL 查询数据 Request VO") +@Data +public class GoViewDataGetBySqlReqVO { + + @Schema(description = "SQL 语句", requiredMode = Schema.RequiredMode.REQUIRED, example = "SELECT * FROM user") + @NotEmpty(message = "SQL 语句不能为空") + private String sql; + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/data/GoViewDataRespVO.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/data/GoViewDataRespVO.java new file mode 100644 index 0000000..8d6c993 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/data/GoViewDataRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.report.controller.admin.goview.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - GoView 数据 Response VO") +@Data +public class GoViewDataRespVO { + + @Schema(description = "数据维度", requiredMode = Schema.RequiredMode.REQUIRED, example = "['product', 'data1', 'data2']") + private List dimensions; + + @Schema(description = "数据明细列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List> source; + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectCreateReqVO.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectCreateReqVO.java new file mode 100644 index 0000000..f51decf --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectCreateReqVO.java @@ -0,0 +1,15 @@ +package com.yunxi.scm.module.report.controller.admin.goview.vo.project; + +import lombok.*; +import io.swagger.v3.oas.annotations.media.Schema; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - GoView 项目创建 Request VO") +@Data +public class GoViewProjectCreateReqVO { + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + @NotEmpty(message = "项目名称不能为空") + private String name; + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectRespVO.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectRespVO.java new file mode 100644 index 0000000..7c791ab --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectRespVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.report.controller.admin.goview.vo.project; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - GoView 项目 Response VO") +@Data +public class GoViewProjectRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18993") + private Long id; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "发布状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "报表内容") // JSON 格式 + private String content; + + @Schema(description = "预览图片 URL", example = "https://www.iocoder.cn") + private String picUrl; + + @Schema(description = "项目备注", example = "你猜") + private String remark; + + @Schema(description = "创建人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String creator; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectUpdateReqVO.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectUpdateReqVO.java new file mode 100644 index 0000000..3f84cee --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/admin/goview/vo/project/GoViewProjectUpdateReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.report.controller.admin.goview.vo.project; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - GoView 项目更新 Request VO") +@Data +public class GoViewProjectUpdateReqVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18993") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "项目名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五") + private String name; + + @Schema(description = "发布状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @InEnum(value = CommonStatusEnum.class, message = "发布状态必须是 {value}") + private Integer status; + + @Schema(description = "报表内容") // JSON 格式 + private String content; + + @Schema(description = "预览图片 URL", example = "https://www.iocoder.cn") + private String picUrl; + + @Schema(description = "项目备注", example = "你猜") + private String remark; + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/package-info.java new file mode 100644 index 0000000..123bdf1 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yunxi-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yunxi-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.yunxi.scm.module.report.controller; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/convert/ajreport/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/convert/ajreport/package-info.java new file mode 100644 index 0000000..4915e4a --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/convert/ajreport/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 占位,后续删除 + */ +package com.yunxi.scm.module.report.convert.ajreport; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/convert/goview/GoViewProjectConvert.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/convert/goview/GoViewProjectConvert.java new file mode 100644 index 0000000..7f7f829 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/convert/goview/GoViewProjectConvert.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.report.convert.goview; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectRespVO; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import com.yunxi.scm.module.report.dal.dataobject.goview.GoViewProjectDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface GoViewProjectConvert { + + GoViewProjectConvert INSTANCE = Mappers.getMapper(GoViewProjectConvert.class); + + GoViewProjectDO convert(GoViewProjectCreateReqVO bean); + + GoViewProjectDO convert(GoViewProjectUpdateReqVO bean); + + GoViewProjectRespVO convert(GoViewProjectDO bean); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/dataobject/ajreport/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/dataobject/ajreport/package-info.java new file mode 100644 index 0000000..28714b7 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/dataobject/ajreport/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占位,待删除 + */ +package com.yunxi.scm.module.report.dal.dataobject.ajreport; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/dataobject/goview/GoViewProjectDO.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/dataobject/goview/GoViewProjectDO.java new file mode 100644 index 0000000..6c46e54 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/dataobject/goview/GoViewProjectDO.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.report.dal.dataobject.goview; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * GoView 项目表 + * + * 每个大屏图标,对应一个项目 + * + * @author 芋道源码 + */ +@TableName(value = "report_go_view_project", autoResultMap = true) // 由于 SQL Server 的 system_user 是关键字,所以使用 system_users +@KeySequence("report_go_view_project_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GoViewProjectDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId + private Long id; + /** + * 项目名称 + */ + private String name; + /** + * 预览图片 URL + */ + private String picUrl; + /** + * 报表内容 + * + * JSON 配置,使用字符串存储 + */ + private String content; + /** + * 发布状态 + * + * 0 - 已发布 + * 1 - 未发布 + * + * 枚举 {@link com.yunxi.scm.framework.common.enums.CommonStatusEnum} + */ + private Integer status; + /** + * 项目备注 + */ + private String remark; +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/mysql/ajreport/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/mysql/ajreport/package-info.java new file mode 100644 index 0000000..0cf1b90 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/mysql/ajreport/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占位,待删除 + */ +package com.yunxi.scm.module.report.dal.mysql.ajreport; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/mysql/goview/GoViewProjectMapper.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/mysql/goview/GoViewProjectMapper.java new file mode 100644 index 0000000..cba0270 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/dal/mysql/goview/GoViewProjectMapper.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.report.dal.mysql.goview; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.report.dal.dataobject.goview.GoViewProjectDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface GoViewProjectMapper extends BaseMapperX { + + default PageResult selectPage(PageParam reqVO, Long userId) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eq(GoViewProjectDO::getCreator, userId) + .orderByDesc(GoViewProjectDO::getId)); + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/config/JmReportConfiguration.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/config/JmReportConfiguration.java new file mode 100644 index 0000000..f44b0ef --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/config/JmReportConfiguration.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.report.framework.jmreport.config; + +import com.yunxi.scm.framework.security.config.SecurityProperties; +import com.yunxi.scm.module.system.api.oauth2.OAuth2TokenApi; +import com.yunxi.scm.module.report.framework.jmreport.core.service.JmReportTokenServiceImpl; +import org.jeecg.modules.jmreport.api.JmReportTokenServiceI; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +/** + * 积木报表的配置类 + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +@ComponentScan(basePackages = "org.jeecg.modules.jmreport") // 扫描积木报表的包 +public class JmReportConfiguration { + + @Bean + @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") + public JmReportTokenServiceI jmReportTokenService(OAuth2TokenApi oAuth2TokenApi, SecurityProperties securityProperties) { + return new JmReportTokenServiceImpl(oAuth2TokenApi, securityProperties); + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java new file mode 100644 index 0000000..9f5dac3 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/core/service/JmReportTokenServiceImpl.java @@ -0,0 +1,131 @@ +package com.yunxi.scm.module.report.framework.jmreport.core.service; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.security.config.SecurityProperties; +import com.yunxi.scm.framework.security.core.LoginUser; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.web.core.util.WebFrameworkUtils; +import com.yunxi.scm.module.system.api.oauth2.OAuth2TokenApi; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import lombok.RequiredArgsConstructor; +import org.jeecg.modules.jmreport.api.JmReportTokenServiceI; +import org.springframework.http.HttpHeaders; + +import javax.servlet.http.HttpServletRequest; +import java.util.Objects; + +/** + * {@link JmReportTokenServiceI} 实现类,提供积木报表的 Token 校验、用户信息的查询等功能 + * + * @author 随心 + */ +@RequiredArgsConstructor +public class JmReportTokenServiceImpl implements JmReportTokenServiceI { + + /** + * 积木 token head 头 + */ + private static final String JM_TOKEN_HEADER = "X-Access-Token"; + /** + * auth 相关格式 + */ + private static final String AUTHORIZATION_FORMAT = SecurityFrameworkUtils.AUTHORIZATION_BEARER + " %s"; + + private final OAuth2TokenApi oauth2TokenApi; + + private final SecurityProperties securityProperties; + + /** + * 自定义 API 数据集appian自定义 Header,解决 Token 传递。 + * 参考 api数据集token机制详解 文档 + * + * @return 新 head + */ + @Override + public HttpHeaders customApiHeader() { + // 读取积木标标系统的 token + HttpServletRequest request = ServletUtils.getRequest(); + String token = request.getHeader(JM_TOKEN_HEADER); + + // 设置到 yunxi 系统的 token + HttpHeaders headers = new HttpHeaders(); + headers.add(securityProperties.getTokenHeader(), String.format(AUTHORIZATION_FORMAT, token)); + return headers; + } + + /** + * 校验 Token 是否有效,即验证通过 + * + * @param token JmReport 前端传递的 token + * @return 是否认证通过 + */ + @Override + public Boolean verifyToken(String token) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (!Objects.isNull(userId)) { + return true; + } + return buildLoginUserByToken(token) != null; + } + + /** + * 获得用户编号 + *

+ * 虽然方法名获得的是 username,实际对应到项目中是用户编号 + * + * @param token JmReport 前端传递的 token + * @return 用户编号 + */ + @Override + public String getUsername(String token) { + Long userId = SecurityFrameworkUtils.getLoginUserId(); + if (ObjectUtil.isNotNull(userId)) { + return String.valueOf(userId); + } + LoginUser user = buildLoginUserByToken(token); + return user == null ? null : String.valueOf(user.getId()); + } + + /** + * 基于 token 构建登录用户 + * + * @param token token + * @return 返回 token 对应的用户信息 + */ + private LoginUser buildLoginUserByToken(String token) { + if (StrUtil.isEmpty(token)) { + return null; + } + // TODO 如下的实现不算特别优雅,主要咱是不想搞的太复杂,所以参考对应的 Filter 先实现了 + + // ① 参考 TokenAuthenticationFilter 的认证逻辑(Security 的上下文清理,交给 Spring Security 完成) + // 目的:实现基于 JmReport 前端传递的 token,实现认证 + TenantContextHolder.setIgnore(true); // 忽略租户,保证可查询到 token 信息 + LoginUser user = null; + try { + OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token); + if (accessToken == null) { + return null; + } + user = new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType()) + .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes()); + } catch (ServiceException ignored) { + // do nothing:如果报错,说明认证失败,则返回 false 即可 + } + if (user == null) { + return null; + } + SecurityFrameworkUtils.setLoginUser(user, WebFrameworkUtils.getRequest()); + + // ② 参考 TenantContextWebFilter 实现(Tenant 的上下文清理,交给 TenantContextWebFilter 完成) + // 目的:基于 LoginUser 获得到的租户编号,设置到 Tenant 上下文,避免查询数据库时的报错 + TenantContextHolder.setIgnore(false); + TenantContextHolder.setTenantId(user.getTenantId()); + return user; + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/core/web/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/core/web/package-info.java new file mode 100644 index 0000000..70ddfb5 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/jmreport/core/web/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,后续会基于 Filter 实现积木报表的认证等功能,替代 {@link com.yunxi.scm.module.report.framework.jmreport.core.service.JmReportTokenServiceImpl} + */ +package com.yunxi.scm.module.report.framework.jmreport.core.web; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/package-info.java new file mode 100644 index 0000000..9f7dde4 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 report 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.report.framework; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/security/config/SecurityConfiguration.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/security/config/SecurityConfiguration.java new file mode 100644 index 0000000..b28c36f --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/security/config/SecurityConfiguration.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.report.framework.security.config; + +import com.yunxi.scm.framework.security.config.AuthorizeRequestsCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; + +/** + * Report 模块的 Security 配置 + */ +@Configuration("reportSecurityConfiguration") +public class SecurityConfiguration { + + @Bean("reportAuthorizeRequestsCustomizer") + public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() { + return new AuthorizeRequestsCustomizer() { + + @Override + public void customize(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry) { + //积木报表 + registry.antMatchers("/jmreport/**").permitAll(); + } + + }; + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/security/core/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/security/core/package-info.java new file mode 100644 index 0000000..8e93e2d --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/security/core/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.report.framework.security.core; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/ureport/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/ureport/package-info.java new file mode 100644 index 0000000..ba5206e --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/framework/ureport/package-info.java @@ -0,0 +1,7 @@ +/** + * ureport2:https://github.com/youseries/ureport + * + * ureport2 和 jimurepot 是相同类型的产品,不过停更了,最好发布时间是 2018 年。 + * 它们之间的功能对比,可见 https://juejin.cn/post/6939836480269320200 地址 + */ +package com.yunxi.scm.module.report.framework.ureport; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/package-info.java new file mode 100644 index 0000000..77bf6e8 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/package-info.java @@ -0,0 +1,9 @@ +/** + * report 模块,主要实现数据可视化报表等功能: + * 1. 基于「积木报表」实现,打印设计、报表设计、图形设计、大屏设计等。URL 前缀是 /jmreport,表名前缀是 jimu_ + * + * 由于「积木报表」的大屏设计器需要收费,后续会自研,对应的是: + * 1. Controller URL:以 /report/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 report_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.report; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/ajreport/package-info.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/ajreport/package-info.java new file mode 100644 index 0000000..45af7aa --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/ajreport/package-info.java @@ -0,0 +1,4 @@ +/** + * TODO 芋艿:占位,待删除 + */ +package com.yunxi.scm.module.report.service.ajreport; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewDataService.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewDataService.java new file mode 100644 index 0000000..d8da834 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewDataService.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.report.service.goview; + +import com.yunxi.scm.module.report.controller.admin.goview.vo.data.GoViewDataRespVO; + +/** + * GoView 数据 Service 接口 + * + * @author 芋道源码 + */ +public interface GoViewDataService { + + /** + * 使用 SQL 查询数据 + * + * @param sql SQL 语句 + * @return 数据 + */ + GoViewDataRespVO getDataBySQL(String sql); + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewDataServiceImpl.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewDataServiceImpl.java new file mode 100644 index 0000000..06cacdf --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewDataServiceImpl.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.report.service.goview; + +import com.yunxi.scm.module.report.controller.admin.goview.vo.data.GoViewDataRespVO; +import com.google.common.collect.Maps; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.jdbc.support.rowset.SqlRowSetMetaData; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.Map; + +/** + * GoView 数据 Service 实现类 + * + * 补充说明: + * 1. 目前默认使用 jdbcTemplate 查询项目配置的数据源。如果你想查询其它数据源,可以新建对应数据源的 jdbcTemplate 来实现。 + * 2. 默认数据源是 MySQL 关系数据源,可能数据量比较大的情况下,会比较慢,可以考虑后续使用 Click House 等等。 + * + * @author 芋道源码 + */ +@Service +@Validated +public class GoViewDataServiceImpl implements GoViewDataService { + + @Resource + private JdbcTemplate jdbcTemplate; + + @Override + public GoViewDataRespVO getDataBySQL(String sql) { + // 1. 执行查询 + SqlRowSet sqlRowSet = jdbcTemplate.queryForRowSet(sql); + + // 2. 构建返回结果 + GoViewDataRespVO respVO = new GoViewDataRespVO(); + // 2.1 解析元数据 + SqlRowSetMetaData metaData = sqlRowSet.getMetaData(); + String[] columnNames = metaData.getColumnNames(); + respVO.setDimensions(Arrays.asList(columnNames)); + // 2.2 解析数据明细 + respVO.setSource(new LinkedList<>()); // 由于数据量不确认,使用 LinkedList 虽然内存占用大一点,但是不存在扩容复制的问题 + while (sqlRowSet.next()) { + Map data = Maps.newHashMapWithExpectedSize(columnNames.length); + for (String columnName : columnNames) { + data.put(columnName, sqlRowSet.getObject(columnName)); + } + respVO.getSource().add(data); + } + return respVO; + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewProjectService.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewProjectService.java new file mode 100644 index 0000000..d3e078d --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewProjectService.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.report.service.goview; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import com.yunxi.scm.module.report.dal.dataobject.goview.GoViewProjectDO; + +import javax.validation.Valid; + +/** + * GoView 项目 Service 接口 + * + * @author 芋道源码 + */ +public interface GoViewProjectService { + + /** + * 创建项目 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createProject(@Valid GoViewProjectCreateReqVO createReqVO); + + /** + * 更新项目 + * + * @param updateReqVO 更新信息 + */ + void updateProject(@Valid GoViewProjectUpdateReqVO updateReqVO); + + /** + * 删除项目 + * + * @param id 编号 + */ + void deleteProject(Long id); + + /** + * 获得项目 + * + * @param id 编号 + * @return 项目 + */ + GoViewProjectDO getProject(Long id); + + /** + * 获得我的项目分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @return GoView 项目分页 + */ + PageResult getMyProjectPage(PageParam pageReqVO, Long userId); + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewProjectServiceImpl.java b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewProjectServiceImpl.java new file mode 100644 index 0000000..08cd461 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/main/java/com/yunxi/scm/module/report/service/goview/GoViewProjectServiceImpl.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.report.service.goview; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import com.yunxi.scm.module.report.convert.goview.GoViewProjectConvert; +import com.yunxi.scm.module.report.dal.dataobject.goview.GoViewProjectDO; +import com.yunxi.scm.module.report.dal.mysql.goview.GoViewProjectMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.report.enums.ErrorCodeConstants.GO_VIEW_PROJECT_NOT_EXISTS; + +/** + * GoView 项目 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class GoViewProjectServiceImpl implements GoViewProjectService { + + @Resource + private GoViewProjectMapper goViewProjectMapper; + + @Override + public Long createProject(GoViewProjectCreateReqVO createReqVO) { + // 插入 + GoViewProjectDO goViewProject = GoViewProjectConvert.INSTANCE.convert(createReqVO) + .setStatus(CommonStatusEnum.DISABLE.getStatus()); + goViewProjectMapper.insert(goViewProject); + // 返回 + return goViewProject.getId(); + } + + @Override + public void updateProject(GoViewProjectUpdateReqVO updateReqVO) { + // 校验存在 + validateProjectExists(updateReqVO.getId()); + // 更新 + GoViewProjectDO updateObj = GoViewProjectConvert.INSTANCE.convert(updateReqVO); + goViewProjectMapper.updateById(updateObj); + } + + @Override + public void deleteProject(Long id) { + // 校验存在 + validateProjectExists(id); + // 删除 + goViewProjectMapper.deleteById(id); + } + + private void validateProjectExists(Long id) { + if (goViewProjectMapper.selectById(id) == null) { + throw exception(GO_VIEW_PROJECT_NOT_EXISTS); + } + } + + @Override + public GoViewProjectDO getProject(Long id) { + return goViewProjectMapper.selectById(id); + } + + @Override + public PageResult getMyProjectPage(PageParam pageReqVO, Long userId) { + return goViewProjectMapper.selectPage(pageReqVO, userId); + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/test/java/com/yunxi/scm/module/report/service/goview/GoViewDataServiceImplTest.java b/yunxi-module-report/yunxi-module-report-biz/src/test/java/com/yunxi/scm/module/report/service/goview/GoViewDataServiceImplTest.java new file mode 100644 index 0000000..e98b483 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/test/java/com/yunxi/scm/module/report/service/goview/GoViewDataServiceImplTest.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.report.service.goview; + +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.report.controller.admin.goview.vo.data.GoViewDataRespVO; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.jdbc.support.rowset.SqlRowSetMetaData; + +import javax.annotation.Resource; +import java.util.Arrays; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Import(GoViewDataServiceImpl.class) +public class GoViewDataServiceImplTest extends BaseDbUnitTest { + + @Resource + private GoViewDataServiceImpl goViewDataService; + + @MockBean + private JdbcTemplate jdbcTemplate; + + @Test + public void testGetDataBySQL() { + // 准备参数 + String sql = "SELECT id, name FROM system_users"; + // mock 方法 + SqlRowSet sqlRowSet = mock(SqlRowSet.class); + when(jdbcTemplate.queryForRowSet(eq(sql))).thenReturn(sqlRowSet); + // mock 元数据 + SqlRowSetMetaData metaData = mock(SqlRowSetMetaData.class); + when(sqlRowSet.getMetaData()).thenReturn(metaData); + when(metaData.getColumnNames()).thenReturn(new String[]{"id", "name"}); + // mock 数据明细 + when(sqlRowSet.next()).thenReturn(true).thenReturn(true).thenReturn(false); + when(sqlRowSet.getObject("id")).thenReturn(1L).thenReturn(2L); + when(sqlRowSet.getObject("name")).thenReturn("芋道源码").thenReturn("芋道"); + + // 调用 + GoViewDataRespVO dataBySQL = goViewDataService.getDataBySQL(sql); + // 断言 + assertEquals(Arrays.asList("id", "name"), dataBySQL.getDimensions()); + assertEquals(2, dataBySQL.getDimensions().size()); + assertEquals(2, dataBySQL.getSource().get(0).size()); + assertEquals(1L, dataBySQL.getSource().get(0).get("id")); + assertEquals("芋道源码", dataBySQL.getSource().get(0).get("name")); + assertEquals(2, dataBySQL.getSource().get(1).size()); + assertEquals(2L, dataBySQL.getSource().get(1).get("id")); + assertEquals("芋道", dataBySQL.getSource().get(1).get("name")); + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/test/java/com/yunxi/scm/module/report/service/goview/GoViewProjectServiceImplTest.java b/yunxi-module-report/yunxi-module-report-biz/src/test/java/com/yunxi/scm/module/report/service/goview/GoViewProjectServiceImplTest.java new file mode 100644 index 0000000..a2228ed --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/test/java/com/yunxi/scm/module/report/service/goview/GoViewProjectServiceImplTest.java @@ -0,0 +1,135 @@ +package com.yunxi.scm.module.report.service.goview; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectCreateReqVO; +import com.yunxi.scm.module.report.controller.admin.goview.vo.project.GoViewProjectUpdateReqVO; +import com.yunxi.scm.module.report.dal.dataobject.goview.GoViewProjectDO; +import com.yunxi.scm.module.report.dal.mysql.goview.GoViewProjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.report.enums.ErrorCodeConstants.GO_VIEW_PROJECT_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link GoViewProjectServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(GoViewProjectServiceImpl.class) +public class GoViewProjectServiceImplTest extends BaseDbUnitTest { + + @Resource + private GoViewProjectServiceImpl goViewProjectService; + + @Resource + private GoViewProjectMapper goViewProjectMapper; + + @Test + public void testCreateProject_success() { + // 准备参数 + GoViewProjectCreateReqVO reqVO = randomPojo(GoViewProjectCreateReqVO.class); + + // 调用 + Long goViewProjectId = goViewProjectService.createProject(reqVO); + // 断言 + assertNotNull(goViewProjectId); + // 校验记录的属性是否正确 + GoViewProjectDO goViewProject = goViewProjectMapper.selectById(goViewProjectId); + assertPojoEquals(reqVO, goViewProject); + } + + @Test + public void testUpdateProject_success() { + // mock 数据 + GoViewProjectDO dbGoViewProject = randomPojo(GoViewProjectDO.class); + goViewProjectMapper.insert(dbGoViewProject);// @Sql: 先插入出一条存在的数据 + // 准备参数 + GoViewProjectUpdateReqVO reqVO = randomPojo(GoViewProjectUpdateReqVO.class, o -> { + o.setId(dbGoViewProject.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + }); + + // 调用 + goViewProjectService.updateProject(reqVO); + // 校验是否更新正确 + GoViewProjectDO goViewProject = goViewProjectMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, goViewProject); + } + + @Test + public void testUpdateProject_notExists() { + // 准备参数 + GoViewProjectUpdateReqVO reqVO = randomPojo(GoViewProjectUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> goViewProjectService.updateProject(reqVO), GO_VIEW_PROJECT_NOT_EXISTS); + } + + @Test + public void testDeleteProject_success() { + // mock 数据 + GoViewProjectDO dbGoViewProject = randomPojo(GoViewProjectDO.class); + goViewProjectMapper.insert(dbGoViewProject);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbGoViewProject.getId(); + + // 调用 + goViewProjectService.deleteProject(id); + // 校验数据不存在了 + assertNull(goViewProjectMapper.selectById(id)); + } + + @Test + public void testDeleteProject_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> goViewProjectService.deleteProject(id), GO_VIEW_PROJECT_NOT_EXISTS); + } + + @Test + public void testGetProject() { + // mock 数据 + GoViewProjectDO dbGoViewProject = randomPojo(GoViewProjectDO.class); + goViewProjectMapper.insert(dbGoViewProject);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbGoViewProject.getId(); + + // 调用 + GoViewProjectDO goViewProject = goViewProjectService.getProject(id); + // 断言 + assertPojoEquals(dbGoViewProject, goViewProject); + } + + @Test + public void testGetMyGoViewProjectPage() { + // mock 数据 + GoViewProjectDO dbGoViewProject = randomPojo(GoViewProjectDO.class, o -> { // 等会查询到 + o.setCreator("1"); + }); + goViewProjectMapper.insert(dbGoViewProject); + // 测试 userId 不匹配 + goViewProjectMapper.insert(cloneIgnoreId(dbGoViewProject, o -> o.setCreator("2"))); + // 准备参数 + PageParam reqVO = new PageParam(); + Long userId = 1L; + + // 调用 + PageResult pageResult = goViewProjectService.getMyProjectPage(reqVO, userId); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbGoViewProject, pageResult.getList().get(0)); + } + +} diff --git a/yunxi-module-report/yunxi-module-report-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-report/yunxi-module-report-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..b65a147 --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,55 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm.module + captcha: + timeout: 5m + width: 160 + height: 60 + enable: true diff --git a/yunxi-module-report/yunxi-module-report-biz/src/test/resources/logback.xml b/yunxi-module-report/yunxi-module-report-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-report/yunxi-module-report-biz/src/test/resources/sql/clean.sql b/yunxi-module-report/yunxi-module-report-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..4a0268a --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/test/resources/sql/clean.sql @@ -0,0 +1 @@ +DELETE FROM "report_go_view_project"; diff --git a/yunxi-module-report/yunxi-module-report-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-report/yunxi-module-report-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..a77397f --- /dev/null +++ b/yunxi-module-report/yunxi-module-report-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS "report_go_view_project" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "pic_url" varchar, + "content" varchar, + "status" varchar NOT NULL, + "remark" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'GoView 项目表'; diff --git a/yunxi-module-system/pom.xml b/yunxi-module-system/pom.xml new file mode 100644 index 0000000..cc6f0eb --- /dev/null +++ b/yunxi-module-system/pom.xml @@ -0,0 +1,24 @@ + + + + com.yunxi.scm + yunxi + ${revision} + + 4.0.0 + + yunxi-module-system-api + yunxi-module-system-biz + + yunxi-module-system + pom + + ${project.artifactId} + + system 模块下,我们放通用业务,支撑上层的核心业务。 + 例如说:用户、部门、权限、数据字典等等 + + + diff --git a/yunxi-module-system/yunxi-module-system-api/pom.xml b/yunxi-module-system/yunxi-module-system-api/pom.xml new file mode 100644 index 0000000..6a4c9f8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/pom.xml @@ -0,0 +1,34 @@ + + + + com.yunxi.scm + yunxi-module-system + ${revision} + + 4.0.0 + yunxi-module-system-api + jar + + ${project.artifactId} + + system 模块 API,暴露给其它模块调用 + + + + + com.yunxi.scm + yunxi-common + + + + + org.springframework.boot + spring-boot-starter-validation + true + + + + + diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/DeptApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/DeptApi.java new file mode 100644 index 0000000..bc9cfed --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/DeptApi.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.system.api.dept; + +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 部门 API 接口 + * + * @author 芋道源码 + */ +public interface DeptApi { + + /** + * 获得部门信息 + * + * @param id 部门编号 + * @return 部门信息 + */ + DeptRespDTO getDept(Long id); + + /** + * 获得部门信息数组 + * + * @param ids 部门编号数组 + * @return 部门信息数组 + */ + List getDeptList(Collection ids); + + /** + * 校验部门们是否有效。如下情况,视为无效: + * 1. 部门编号不存在 + * 2. 部门被禁用 + * + * @param ids 角色编号数组 + */ + void validateDeptList(Collection ids); + + /** + * 获得指定编号的部门 Map + * + * @param ids 部门编号数组 + * @return 部门 Map + */ + default Map getDeptMap(Set ids) { + List list = getDeptList(ids); + return CollectionUtils.convertMap(list, DeptRespDTO::getId); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/PostApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/PostApi.java new file mode 100644 index 0000000..4f377a0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/PostApi.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.api.dept; + +import java.util.Collection; + +/** + * 岗位 API 接口 + * + * @author 芋道源码 + */ +public interface PostApi { + + /** + * 校验岗位们是否有效。如下情况,视为无效: + * 1. 岗位编号不存在 + * 2. 岗位被禁用 + * + * @param ids 岗位编号数组 + */ + void validPostList(Collection ids); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/dto/DeptRespDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/dto/DeptRespDTO.java new file mode 100644 index 0000000..d4a5236 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dept/dto/DeptRespDTO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.api.dept.dto; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 部门 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DeptRespDTO { + + /** + * 部门编号 + */ + private Long id; + /** + * 部门名称 + */ + private String name; + /** + * 父部门编号 + */ + private Long parentId; + /** + * 负责人的用户编号 + */ + private Long leaderUserId; + /** + * 部门状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dict/DictDataApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dict/DictDataApi.java new file mode 100644 index 0000000..2d9512a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dict/DictDataApi.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.api.dict; + +import com.yunxi.scm.module.system.api.dict.dto.DictDataRespDTO; + +import java.util.Collection; + +/** + * 字典数据 API 接口 + * + * @author 芋道源码 + */ +public interface DictDataApi { + + /** + * 校验字典数据们是否有效。如下情况,视为无效: + * 1. 字典数据不存在 + * 2. 字典数据被禁用 + * + * @param dictType 字典类型 + * @param values 字典数据值的数组 + */ + void validateDictDataList(String dictType, Collection values); + + /** + * 获得指定的字典数据,从缓存中 + * + * @param type 字典类型 + * @param value 字典数据值 + * @return 字典数据 + */ + DictDataRespDTO getDictData(String type, String value); + + /** + * 解析获得指定的字典数据,从缓存中 + * + * @param type 字典类型 + * @param label 字典数据标签 + * @return 字典数据 + */ + DictDataRespDTO parseDictData(String type, String label); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dict/dto/DictDataRespDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dict/dto/DictDataRespDTO.java new file mode 100644 index 0000000..af600af --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/dict/dto/DictDataRespDTO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.api.dict.dto; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +/** + * 字典数据 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DictDataRespDTO { + + /** + * 字典标签 + */ + private String label; + /** + * 字典值 + */ + private String value; + /** + * 字典类型 + */ + private String dictType; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/ErrorCodeApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/ErrorCodeApi.java new file mode 100644 index 0000000..7b5c6a2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/ErrorCodeApi.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.system.api.errorcode; + +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeRespDTO; + +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 错误码 Api 接口 + * + * @author 芋道源码 + */ +public interface ErrorCodeApi { + + /** + * 自动创建错误码 + * + * @param autoGenerateDTOs 错误码信息 + */ + void autoGenerateErrorCodeList(@Valid List autoGenerateDTOs); + + /** + * 增量获得错误码数组 + * + * 如果 minUpdateTime 为空时,则获取所有错误码 + * + * @param applicationName 应用名 + * @param minUpdateTime 最小更新时间 + * @return 错误码数组 + */ + List getErrorCodeList(String applicationName, LocalDateTime minUpdateTime); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/dto/ErrorCodeAutoGenerateReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/dto/ErrorCodeAutoGenerateReqDTO.java new file mode 100644 index 0000000..0f398d7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/dto/ErrorCodeAutoGenerateReqDTO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.api.errorcode.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 错误码自动生成 DTO + * + * @author dylan + */ +@Data +@Accessors(chain = true) +public class ErrorCodeAutoGenerateReqDTO { + + /** + * 应用名 + */ + @NotNull(message = "应用名不能为空") + private String applicationName; + /** + * 错误码编码 + */ + @NotNull(message = "错误码编码不能为空") + private Integer code; + /** + * 错误码错误提示 + */ + @NotEmpty(message = "错误码错误提示不能为空") + private String message; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/dto/ErrorCodeRespDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/dto/ErrorCodeRespDTO.java new file mode 100644 index 0000000..afe470e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/errorcode/dto/ErrorCodeRespDTO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.api.errorcode.dto; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 错误码的 Response DTO + * + * @author 芋道源码 + */ +@Data +public class ErrorCodeRespDTO { + + /** + * 错误码编码 + */ + private Integer code; + /** + * 错误码错误提示 + */ + private String message; + /** + * 更新时间 + */ + private LocalDateTime updateTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/LoginLogApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/LoginLogApi.java new file mode 100644 index 0000000..42feff7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/LoginLogApi.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.api.logger; + +import com.yunxi.scm.module.system.api.logger.dto.LoginLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * 登录日志的 API 接口 + * + * @author 芋道源码 + */ +public interface LoginLogApi { + + /** + * 创建登录日志 + * + * @param reqDTO 日志信息 + */ + void createLoginLog(@Valid LoginLogCreateReqDTO reqDTO); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/OperateLogApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/OperateLogApi.java new file mode 100644 index 0000000..6fea691 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/OperateLogApi.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.api.logger; + +import com.yunxi.scm.module.system.api.logger.dto.OperateLogCreateReqDTO; + +import javax.validation.Valid; + +/** + * 操作日志 API 接口 + * + * @author 芋道源码 + */ +public interface OperateLogApi { + + /** + * 创建操作日志 + * + * @param createReqDTO 请求 + */ + void createOperateLog(@Valid OperateLogCreateReqDTO createReqDTO); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/dto/LoginLogCreateReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/dto/LoginLogCreateReqDTO.java new file mode 100644 index 0000000..0018067 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/dto/LoginLogCreateReqDTO.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.module.system.api.logger.dto; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 登录日志创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class LoginLogCreateReqDTO { + + /** + * 日志类型 + */ + @NotNull(message = "日志类型不能为空") + private Integer logType; + /** + * 链路追踪编号 + */ + private String traceId; + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + /** + * 用户账号 + */ + @NotBlank(message = "用户账号不能为空") + @Size(max = 30, message = "用户账号长度不能超过30个字符") + private String username; + + /** + * 登录结果 + */ + @NotNull(message = "登录结果不能为空") + private Integer result; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + /** + * 浏览器 UserAgent + * + * 允许空,原因:Job 过期登出时,是无法传递 UserAgent 的 + */ + private String userAgent; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/dto/OperateLogCreateReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/dto/OperateLogCreateReqDTO.java new file mode 100644 index 0000000..dc3c15e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/logger/dto/OperateLogCreateReqDTO.java @@ -0,0 +1,123 @@ +package com.yunxi.scm.module.system.api.logger.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 操作日志创建 Request DTO + */ +@Data +public class OperateLogCreateReqDTO { + + /** + * 链路追踪编号 + */ + private String traceId; + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 操作模块 + */ + @NotEmpty(message = "操作模块不能为空") + private String module; + + /** + * 操作名 + */ + @NotEmpty(message = "操作名") + private String name; + + /** + * 操作分类 + */ + @NotNull(message = "操作分类不能为空") + private Integer type; + + /** + * 操作明细 + */ + private String content; + + /** + * 拓展字段 + */ + private Map exts; + + /** + * 请求方法名 + */ + @NotEmpty(message = "请求方法名不能为空") + private String requestMethod; + + /** + * 请求地址 + */ + @NotEmpty(message = "请求地址不能为空") + private String requestUrl; + + /** + * 用户 IP + */ + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + /** + * 浏览器 UserAgent + */ + @NotEmpty(message = "浏览器 UserAgent 不能为空") + private String userAgent; + + /** + * Java 方法名 + */ + @NotEmpty(message = "Java 方法名不能为空") + private String javaMethod; + + /** + * Java 方法的参数 + */ + private String javaMethodArgs; + + /** + * 开始时间 + */ + @NotNull(message = "开始时间不能为空") + private LocalDateTime startTime; + + /** + * 执行时长,单位:毫秒 + */ + @NotNull(message = "执行时长不能为空") + private Integer duration; + + /** + * 结果码 + */ + @NotNull(message = "结果码不能为空") + private Integer resultCode; + + /** + * 结果提示 + */ + private String resultMsg; + + /** + * 结果数据 + */ + private String resultData; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/mail/MailSendApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/mail/MailSendApi.java new file mode 100644 index 0000000..aa6e0a9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/mail/MailSendApi.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.api.mail; + +import com.yunxi.scm.module.system.api.mail.dto.MailSendSingleToUserReqDTO; + +import javax.validation.Valid; + +/** + * 邮箱发送 API 接口 + * + * @author 芋道源码 + */ +public interface MailSendApi { + + /** + * 发送单条邮箱给 Admin 用户 + * + * 在 mail 为空时,使用 userId 加载对应 Admin 的邮箱 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleMailToAdmin(@Valid MailSendSingleToUserReqDTO reqDTO); + + /** + * 发送单条邮箱给 Member 用户 + * + * 在 mail 为空时,使用 userId 加载对应 Member 的邮箱 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleMailToMember(@Valid MailSendSingleToUserReqDTO reqDTO); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java new file mode 100644 index 0000000..58dc372 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/mail/dto/MailSendSingleToUserReqDTO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.api.mail.dto; + +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; +import java.util.Map; + +/** + * 邮件发送 Request DTO + * + * @author wangjingqi + */ +@Data +public class MailSendSingleToUserReqDTO { + + /** + * 用户编号 + */ + private Long userId; + /** + * 邮箱 + */ + @Email + private String mail; + + /** + * 邮件模板编号 + */ + @NotNull(message = "邮件模板编号不能为空") + private String templateCode; + /** + * 邮件模板参数 + */ + private Map templateParams; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/NotifyMessageSendApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/NotifyMessageSendApi.java new file mode 100644 index 0000000..47c657d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/NotifyMessageSendApi.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.system.api.notify; + +import com.yunxi.scm.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import com.yunxi.scm.module.system.api.notify.dto.NotifyTemplateReqDTO; + +import javax.validation.Valid; + +/** + * 站内信发送 API 接口 + * + * @author xrcoder + */ +public interface NotifyMessageSendApi { + + /** + * 发送单条站内信给 Admin 用户 + * + * @param reqDTO 发送请求 + * @return 发送消息 ID + */ + Long sendSingleMessageToAdmin(@Valid NotifySendSingleToUserReqDTO reqDTO); + + /** + * 发送单条站内信给 Member 用户 + * + * @param reqDTO 发送请求 + * @return 发送消息 ID + */ + Long sendSingleMessageToMember(@Valid NotifySendSingleToUserReqDTO reqDTO); + + + boolean validateNotifyTemplate(String orderDelivery); + + void createNotifyTemplate(NotifyTemplateReqDTO templateReqDTO); +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/dto/NotifySendSingleToUserReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/dto/NotifySendSingleToUserReqDTO.java new file mode 100644 index 0000000..3fc44ea --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/dto/NotifySendSingleToUserReqDTO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.api.notify.dto; + +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +/** + * 站内信发送给 Admin 或者 Member 用户 + * + * @author xrcoder + */ +@Data +public class NotifySendSingleToUserReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + + /** + * 站内信模板编号 + */ + @NotEmpty(message = "站内信模板编号不能为空") + private String templateCode; + + /** + * 站内信模板参数 + */ + private Map templateParams; +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/dto/NotifyTemplateReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/dto/NotifyTemplateReqDTO.java new file mode 100644 index 0000000..c5d08d8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/notify/dto/NotifyTemplateReqDTO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.api.notify.dto; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Data +public class NotifyTemplateReqDTO { + + @NotEmpty(message = "模版名称不能为空") + private String name; + + @NotNull(message = "模版编码不能为空") + private String code; + + @NotNull(message = "模版类型不能为空") + private Integer type; + + @NotEmpty(message = "发送人名称不能为空") + private String nickname; + + @NotEmpty(message = "模版内容不能为空") + private String content; + + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/OAuth2TokenApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/OAuth2TokenApi.java new file mode 100644 index 0000000..e973dda --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/OAuth2TokenApi.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.system.api.oauth2; + +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; + +import javax.validation.Valid; + +/** + * OAuth2.0 Token API 接口 + * + * @author 芋道源码 + */ +public interface OAuth2TokenApi { + + /** + * 创建访问令牌 + * + * @param reqDTO 访问令牌的创建信息 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO createAccessToken(@Valid OAuth2AccessTokenCreateReqDTO reqDTO); + + /** + * 校验访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken); + + /** + * 移除访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO removeAccessToken(String accessToken); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java new file mode 100644 index 0000000..1a13600 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenCheckRespDTO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.api.oauth2.dto; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * OAuth2.0 访问令牌的校验 Response DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCheckRespDTO implements Serializable { + + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 租户编号 + */ + private Long tenantId; + /** + * 授权范围的数组 + */ + private List scopes; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java new file mode 100644 index 0000000..5b82e78 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenCreateReqDTO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.api.oauth2.dto; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.List; + +/** + * OAuth2.0 访问令牌创建 Request DTO + * + * @author 芋道源码 + */ +@Data +public class OAuth2AccessTokenCreateReqDTO implements Serializable { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @NotNull(message = "用户类型不能为空") + @InEnum(value = UserTypeEnum.class, message = "用户类型必须是 {value}") + private Integer userType; + /** + * 客户端编号 + */ + @NotNull(message = "客户端编号不能为空") + private String clientId; + /** + * 授权范围 + */ + private List scopes; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenRespDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenRespDTO.java new file mode 100644 index 0000000..8f05d55 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/oauth2/dto/OAuth2AccessTokenRespDTO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.system.api.oauth2.dto; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * OAuth2.0 访问令牌的信息 Response DTO + * + * @author 芋道源码 + */ +@Data +@Accessors(chain = true) +public class OAuth2AccessTokenRespDTO implements Serializable { + + /** + * 访问令牌 + */ + private String accessToken; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/package-info.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/package-info.java new file mode 100644 index 0000000..7bbade7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/package-info.java @@ -0,0 +1,4 @@ +/** + * System API 包,定义暴露给其它模块的 API + */ +package com.yunxi.scm.module.system.api; diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/PermissionApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/PermissionApi.java new file mode 100644 index 0000000..4d66ce0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/PermissionApi.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.system.api.permission; + +import com.yunxi.scm.module.system.api.permission.dto.DeptDataPermissionRespDTO; + +import java.util.Collection; +import java.util.Set; + +/** + * 权限 API 接口 + * + * @author 芋道源码 + */ +public interface PermissionApi { + + /** + * 获得拥有多个角色的用户编号集合 + * + * @param roleIds 角色编号集合 + * @return 用户编号集合 + */ + Set getUserRoleIdListByRoleIds(Collection roleIds); + + /** + * 判断是否有权限,任一一个即可 + * + * @param userId 用户编号 + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(Long userId, String... permissions); + + /** + * 判断是否有角色,任一一个即可 + * + * @param userId 用户编号 + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(Long userId, String... roles); + + /** + * 获得登陆用户的部门数据权限 + * + * @param userId 用户编号 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/RoleApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/RoleApi.java new file mode 100644 index 0000000..877ad62 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/RoleApi.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.api.permission; + +import java.util.Collection; + +/** + * 角色 API 接口 + * + * @author 芋道源码 + */ +public interface RoleApi { + + /** + * 校验角色们是否有效。如下情况,视为无效: + * 1. 角色编号不存在 + * 2. 角色被禁用 + * + * @param ids 角色编号数组 + */ + void validRoleList(Collection ids); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/dto/DeptDataPermissionRespDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/dto/DeptDataPermissionRespDTO.java new file mode 100644 index 0000000..c83b216 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/permission/dto/DeptDataPermissionRespDTO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.system.api.permission.dto; + +import lombok.Data; + +import java.util.HashSet; +import java.util.Set; + +/** + * 部门的数据权限 Response DTO + * + * @author 芋道源码 + */ +@Data +public class DeptDataPermissionRespDTO { + + /** + * 是否可查看全部数据 + */ + private Boolean all; + /** + * 是否可查看自己的数据 + */ + private Boolean self; + /** + * 可查看的部门编号数组 + */ + private Set deptIds; + + public DeptDataPermissionRespDTO() { + this.all = false; + this.self = false; + this.deptIds = new HashSet<>(); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sensitiveword/SensitiveWordApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sensitiveword/SensitiveWordApi.java new file mode 100644 index 0000000..90a4c6d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sensitiveword/SensitiveWordApi.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.api.sensitiveword; + +import java.util.List; + +/** + * 敏感词 API 接口 + * + * @author 永不言败 + */ +public interface SensitiveWordApi { + + /** + * 获得文本所包含的不合法的敏感词数组 + * + * @param text 文本 + * @param tags 标签数组 + * @return 不合法的敏感词数组 + */ + List validateText(String text, List tags); + + /** + * 判断文本是否包含敏感词 + * + * @param text 文本 + * @param tags 表述数组 + * @return 是否包含 + */ + boolean isTextValid(String text, List tags); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/SmsCodeApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/SmsCodeApi.java new file mode 100644 index 0000000..ffff7a5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/SmsCodeApi.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.api.sms; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeUseReqDTO; + +import javax.validation.Valid; + +/** + * 短信验证码 API 接口 + * + * @author 芋道源码 + */ +public interface SmsCodeApi { + + /** + * 创建短信验证码,并进行发送 + * + * @param reqDTO 发送请求 + */ + void sendSmsCode(@Valid SmsCodeSendReqDTO reqDTO); + + /** + * 验证短信验证码,并进行使用 + * 如果正确,则将验证码标记成已使用 + * 如果错误,则抛出 {@link ServiceException} 异常 + * + * @param reqDTO 使用请求 + */ + void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO); + + /** + * 检查验证码是否有效 + * + * @param reqDTO 校验请求 + */ + void validateSmsCode(@Valid SmsCodeValidateReqDTO reqDTO); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/SmsSendApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/SmsSendApi.java new file mode 100644 index 0000000..8778f21 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/SmsSendApi.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.api.sms; + +import com.yunxi.scm.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; + +import javax.validation.Valid; + +/** + * 短信发送 API 接口 + * + * @author 芋道源码 + */ +public interface SmsSendApi { + + /** + * 发送单条短信给 Admin 用户 + * + * 在 mobile 为空时,使用 userId 加载对应 Admin 的手机号 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleSmsToAdmin(@Valid SmsSendSingleToUserReqDTO reqDTO); + + /** + * 发送单条短信给 Member 用户 + * + * 在 mobile 为空时,使用 userId 加载对应 Member 的手机号 + * + * @param reqDTO 发送请求 + * @return 发送日志编号 + */ + Long sendSingleSmsToMember(@Valid SmsSendSingleToUserReqDTO reqDTO); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java new file mode 100644 index 0000000..7e86165 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeSendReqDTO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.api.sms.dto.code; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的发送 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeSendReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 发送 IP + */ + @NotEmpty(message = "发送 IP 不能为空") + private String createIp; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java new file mode 100644 index 0000000..3949f32 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeUseReqDTO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.api.sms.dto.code; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的使用 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeUseReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 验证码 + */ + @NotEmpty(message = "验证码") + private String code; + /** + * 使用 IP + */ + @NotEmpty(message = "使用 IP 不能为空") + private String usedIp; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java new file mode 100644 index 0000000..1ed0a30 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/code/SmsCodeValidateReqDTO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.api.sms.dto.code; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 短信验证码的校验 Request DTO + * + * @author 芋道源码 + */ +@Data +public class SmsCodeValidateReqDTO { + + /** + * 手机号 + */ + @Mobile + @NotEmpty(message = "手机号不能为空") + private String mobile; + /** + * 发送场景 + */ + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + /** + * 验证码 + */ + @NotEmpty(message = "验证码") + private String code; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java new file mode 100644 index 0000000..8725777 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/sms/dto/send/SmsSendSingleToUserReqDTO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.system.api.sms.dto.send; + +import com.yunxi.scm.framework.common.validation.Mobile; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import java.util.Map; + +/** + * 短信发送给 Admin 或者 Member 用户 + * + * @author 芋道源码 + */ +@Data +public class SmsSendSingleToUserReqDTO { + + /** + * 用户编号 + */ + private Long userId; + /** + * 手机号 + */ + @Mobile + private String mobile; + /** + * 短信模板编号 + */ + @NotEmpty(message = "短信模板编号不能为空") + private String templateCode; + /** + * 短信模板参数 + */ + private Map templateParams; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/SocialUserApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/SocialUserApi.java new file mode 100644 index 0000000..eb19d64 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/SocialUserApi.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.system.api.social; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.api.social.dto.SocialUserUnbindReqDTO; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; + +import javax.validation.Valid; + +/** + * 社交用户的 API 接口 + * + * @author 芋道源码 + */ +public interface SocialUserApi { + + /** + * 获得社交平台的授权 URL + * + * @param type 社交平台的类型 {@link SocialTypeEnum} + * @param redirectUri 重定向 URL + * @return 社交平台的授权 URL + */ + String getAuthorizeUrl(Integer type, String redirectUri); + + /** + * 绑定社交用户 + * + * @param reqDTO 绑定信息 + */ + void bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); + + /** + * 取消绑定社交用户 + * + * @param reqDTO 解绑 + */ + void unbindSocialUser(@Valid SocialUserUnbindReqDTO reqDTO); + + /** + * 获得社交用户的绑定用户编号 + * 注意,返回的是 MemberUser 或者 AdminUser 的 id 编号! + * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 + * + * @param userType 用户类型 + * @param type 社交平台的类型 + * @param code 授权码 + * @param state state + * @return 绑定用户编号 + */ + Long getBindUserId(Integer userType, Integer type, String code, String state); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/dto/SocialUserBindReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/dto/SocialUserBindReqDTO.java new file mode 100644 index 0000000..037ba84 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/dto/SocialUserBindReqDTO.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.system.api.social.dto; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 取消绑定社交用户 Request DTO + * + * @author 芋道源码 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserBindReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 社交平台的类型 + */ + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + /** + * 授权码 + */ + @NotEmpty(message = "授权码不能为空") + private String code; + /** + * state + */ + @NotNull(message = "state 不能为空") + private String state; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/dto/SocialUserUnbindReqDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/dto/SocialUserUnbindReqDTO.java new file mode 100644 index 0000000..5297a12 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/social/dto/SocialUserUnbindReqDTO.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.system.api.social.dto; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 社交绑定 Request DTO,使用 code 授权码 + * + * @author 芋道源码 + */ +@Data +public class SocialUserUnbindReqDTO { + + /** + * 用户编号 + */ + @NotNull(message = "用户编号不能为空") + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + @NotNull(message = "用户类型不能为空") + private Integer userType; + + /** + * 社交平台的类型 + */ + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + /** + * 社交平台的 unionId + */ + @NotEmpty(message = "社交平台的 unionId 不能为空") + private String unionId; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/tenant/TenantApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/tenant/TenantApi.java new file mode 100644 index 0000000..3594d67 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/tenant/TenantApi.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.api.tenant; + +import java.util.List; + +/** + * 多租户的 API 接口 + * + * @author 芋道源码 + */ +public interface TenantApi { + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIdList(); + + /** + * 校验租户是否合法 + * + * @param id 租户编号 + */ + void validateTenant(Long id); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/user/AdminUserApi.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/user/AdminUserApi.java new file mode 100644 index 0000000..4373896 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/user/AdminUserApi.java @@ -0,0 +1,69 @@ +package com.yunxi.scm.module.system.api.user; + +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Admin 用户 API 接口 + * + * @author 芋道源码 + */ +public interface AdminUserApi { + + /** + * 通过用户 ID 查询用户 + * + * @param id 用户ID + * @return 用户对象信息 + */ + AdminUserRespDTO getUser(Long id); + + /** + * 通过用户 ID 查询用户们 + * + * @param ids 用户 ID 们 + * @return 用户对象信息 + */ + List getUserList(Collection ids); + + /** + * 获得指定部门的用户数组 + * + * @param deptIds 部门数组 + * @return 用户数组 + */ + List getUserListByDeptIds(Collection deptIds); + + /** + * 获得指定岗位的用户数组 + * + * @param postIds 岗位数组 + * @return 用户数组 + */ + List getUserListByPostIds(Collection postIds); + + /** + * 获得用户 Map + * + * @param ids 用户编号数组 + * @return 用户 Map + */ + default Map getUserMap(Collection ids) { + List users = getUserList(ids); + return CollectionUtils.convertMap(users, AdminUserRespDTO::getId); + } + + /** + * 校验用户们是否有效。如下情况,视为无效: + * 1. 用户编号不存在 + * 2. 用户被禁用 + * + * @param ids 用户编号数组 + */ + void validateUserList(Collection ids); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/user/dto/AdminUserRespDTO.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/user/dto/AdminUserRespDTO.java new file mode 100644 index 0000000..71ba9d5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/api/user/dto/AdminUserRespDTO.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.system.api.user.dto; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import lombok.Data; + +import java.util.Set; + +/** + * Admin 用户 Response DTO + * + * @author 芋道源码 + */ +@Data +public class AdminUserRespDTO { + + /** + * 用户ID + */ + private Long id; + /** + * 用户昵称 + */ + private String nickname; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + + /** + * 部门ID + */ + private Long deptId; + /** + * 岗位编号数组 + */ + private Set postIds; + /** + * 手机号码 + */ + private String mobile; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/DictTypeConstants.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/DictTypeConstants.java new file mode 100644 index 0000000..b9ee471 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/DictTypeConstants.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.system.enums; + +/** + * System 字典类型的枚举类 + * + * @author 芋道源码 + */ +public interface DictTypeConstants { + + String USER_TYPE = "user_type"; // 用户类型 + String COMMON_STATUS = "common_status"; // 系统状态 + + // ========== SYSTEM 模块 ========== + + String USER_SEX = "system_user_sex"; // 用户性别 + + String OPERATE_TYPE = "system_operate_type"; // 操作类型 + + String LOGIN_TYPE = "system_login_type"; // 登录日志的类型 + String LOGIN_RESULT = "system_login_result"; // 登录结果 + + String ERROR_CODE_TYPE = "system_error_code_type"; // 错误码的类型枚举 + + String SMS_CHANNEL_CODE = "system_sms_channel_code"; // 短信渠道编码 + String SMS_TEMPLATE_TYPE = "system_sms_template_type"; // 短信模板类型 + String SMS_SEND_STATUS = "system_sms_send_status"; // 短信发送状态 + String SMS_RECEIVE_STATUS = "system_sms_receive_status"; // 短信接收状态 + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/ErrorCodeConstants.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/ErrorCodeConstants.java new file mode 100644 index 0000000..7af04b7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/ErrorCodeConstants.java @@ -0,0 +1,166 @@ +package com.yunxi.scm.module.system.enums; + +import com.yunxi.scm.framework.common.exception.ErrorCode; + +/** + * System 错误码枚举类 + * + * system 系统,使用 1-002-000-000 段 + */ +public interface ErrorCodeConstants { + + // ========== AUTH 模块 1002000000 ========== + ErrorCode AUTH_LOGIN_BAD_CREDENTIALS = new ErrorCode(1002000000, "登录失败,账号密码不正确"); + ErrorCode AUTH_LOGIN_USER_DISABLED = new ErrorCode(1002000001, "登录失败,账号被禁用"); + ErrorCode AUTH_LOGIN_CAPTCHA_CODE_ERROR = new ErrorCode(1002000004, "验证码不正确,原因:{}"); + ErrorCode AUTH_THIRD_LOGIN_NOT_BIND = new ErrorCode(1002000005, "未绑定账号,需要进行绑定"); + ErrorCode AUTH_TOKEN_EXPIRED = new ErrorCode(1002000006, "Token 已经过期"); + ErrorCode AUTH_MOBILE_NOT_EXISTS = new ErrorCode(1002000007, "手机号不存在"); + + // ========== 菜单模块 1002001000 ========== + ErrorCode MENU_NAME_DUPLICATE = new ErrorCode(1002001000, "已经存在该名字的菜单"); + ErrorCode MENU_PARENT_NOT_EXISTS = new ErrorCode(1002001001, "父菜单不存在"); + ErrorCode MENU_PARENT_ERROR = new ErrorCode(1002001002, "不能设置自己为父菜单"); + ErrorCode MENU_NOT_EXISTS = new ErrorCode(1002001003, "菜单不存在"); + ErrorCode MENU_EXISTS_CHILDREN = new ErrorCode(1002001004, "存在子菜单,无法删除"); + ErrorCode MENU_PARENT_NOT_DIR_OR_MENU = new ErrorCode(1002001005, "父菜单的类型必须是目录或者菜单"); + + // ========== 角色模块 1002002000 ========== + ErrorCode ROLE_NOT_EXISTS = new ErrorCode(1002002000, "角色不存在"); + ErrorCode ROLE_NAME_DUPLICATE = new ErrorCode(1002002001, "已经存在名为【{}】的角色"); + ErrorCode ROLE_CODE_DUPLICATE = new ErrorCode(1002002002, "已经存在编码为【{}】的角色"); + ErrorCode ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE = new ErrorCode(1002002003, "不能操作类型为系统内置的角色"); + ErrorCode ROLE_IS_DISABLE = new ErrorCode(1002002004, "名字为【{}】的角色已被禁用"); + ErrorCode ROLE_ADMIN_CODE_ERROR = new ErrorCode(1002002005, "编码【{}】不能使用"); + + // ========== 用户模块 1002003000 ========== + ErrorCode USER_USERNAME_EXISTS = new ErrorCode(1002003000, "用户账号已经存在"); + ErrorCode USER_MOBILE_EXISTS = new ErrorCode(1002003001, "手机号已经存在"); + ErrorCode USER_EMAIL_EXISTS = new ErrorCode(1002003002, "邮箱已经存在"); + ErrorCode USER_NOT_EXISTS = new ErrorCode(1002003003, "用户不存在"); + ErrorCode USER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1002003004, "导入用户数据不能为空!"); + ErrorCode USER_PASSWORD_FAILED = new ErrorCode(1002003005, "用户密码校验失败"); + ErrorCode USER_IS_DISABLE = new ErrorCode(1002003006, "名字为【{}】的用户已被禁用"); + ErrorCode USER_COUNT_MAX = new ErrorCode(1002003008, "创建用户失败,原因:超过租户最大租户配额({})!"); + + // ========== 部门模块 1002004000 ========== + ErrorCode DEPT_NAME_DUPLICATE = new ErrorCode(1002004000, "已经存在该名字的部门"); + ErrorCode DEPT_PARENT_NOT_EXITS = new ErrorCode(1002004001,"父级部门不存在"); + ErrorCode DEPT_NOT_FOUND = new ErrorCode(1002004002, "当前部门不存在"); + ErrorCode DEPT_EXITS_CHILDREN = new ErrorCode(1002004003, "存在子部门,无法删除"); + ErrorCode DEPT_PARENT_ERROR = new ErrorCode(1002004004, "不能设置自己为父部门"); + ErrorCode DEPT_EXISTS_USER = new ErrorCode(1002004005, "部门中存在员工,无法删除"); + ErrorCode DEPT_NOT_ENABLE = new ErrorCode(1002004006, "部门({})不处于开启状态,不允许选择"); + ErrorCode DEPT_PARENT_IS_CHILD = new ErrorCode(1002004007, "不能设置自己的子部门为父部门"); + + // ========== 岗位模块 1002005000 ========== + ErrorCode POST_NOT_FOUND = new ErrorCode(1002005000, "当前岗位不存在"); + ErrorCode POST_NOT_ENABLE = new ErrorCode(1002005001, "岗位({}) 不处于开启状态,不允许选择"); + ErrorCode POST_NAME_DUPLICATE = new ErrorCode(1002005002, "已经存在该名字的岗位"); + ErrorCode POST_CODE_DUPLICATE = new ErrorCode(1002005003, "已经存在该标识的岗位"); + + // ========== 字典类型 1002006000 ========== + ErrorCode DICT_TYPE_NOT_EXISTS = new ErrorCode(1002006001, "当前字典类型不存在"); + ErrorCode DICT_TYPE_NOT_ENABLE = new ErrorCode(1002006002, "字典类型不处于开启状态,不允许选择"); + ErrorCode DICT_TYPE_NAME_DUPLICATE = new ErrorCode(1002006003, "已经存在该名字的字典类型"); + ErrorCode DICT_TYPE_TYPE_DUPLICATE = new ErrorCode(1002006004, "已经存在该类型的字典类型"); + ErrorCode DICT_TYPE_HAS_CHILDREN = new ErrorCode(1002006005, "无法删除,该字典类型还有字典数据"); + + // ========== 字典数据 1002007000 ========== + ErrorCode DICT_DATA_NOT_EXISTS = new ErrorCode(1002007001, "当前字典数据不存在"); + ErrorCode DICT_DATA_NOT_ENABLE = new ErrorCode(1002007002, "字典数据({})不处于开启状态,不允许选择"); + ErrorCode DICT_DATA_VALUE_DUPLICATE = new ErrorCode(1002007003, "已经存在该值的字典数据"); + + // ========== 通知公告 1002008000 ========== + ErrorCode NOTICE_NOT_FOUND = new ErrorCode(1002008001, "当前通知公告不存在"); + + // ========== 短信渠道 1002011000 ========== + ErrorCode SMS_CHANNEL_NOT_EXISTS = new ErrorCode(1002011000, "短信渠道不存在"); + ErrorCode SMS_CHANNEL_DISABLE = new ErrorCode(1002011001, "短信渠道不处于开启状态,不允许选择"); + ErrorCode SMS_CHANNEL_HAS_CHILDREN = new ErrorCode(1002011002, "无法删除,该短信渠道还有短信模板"); + + // ========== 短信模板 1002012000 ========== + ErrorCode SMS_TEMPLATE_NOT_EXISTS = new ErrorCode(1002012000, "短信模板不存在"); + ErrorCode SMS_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1002012001, "已经存在编码为【{}】的短信模板"); + + // ========== 短信发送 1002013000 ========== + ErrorCode SMS_SEND_MOBILE_NOT_EXISTS = new ErrorCode(1002013000, "手机号不存在"); + ErrorCode SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS = new ErrorCode(1002013001, "模板参数({})缺失"); + ErrorCode SMS_SEND_TEMPLATE_NOT_EXISTS = new ErrorCode(1002013002, "短信模板不存在"); + + // ========== 短信验证码 1002014000 ========== + ErrorCode SMS_CODE_NOT_FOUND = new ErrorCode(1002014000, "验证码不存在"); + ErrorCode SMS_CODE_EXPIRED = new ErrorCode(1002014001, "验证码已过期"); + ErrorCode SMS_CODE_USED = new ErrorCode(1002014002, "验证码已使用"); + ErrorCode SMS_CODE_NOT_CORRECT = new ErrorCode(1002014003, "验证码不正确"); + ErrorCode SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY = new ErrorCode(1002014004, "超过每日短信发送数量"); + ErrorCode SMS_CODE_SEND_TOO_FAST = new ErrorCode(1002014005, "短信发送过于频率"); + ErrorCode SMS_CODE_IS_EXISTS = new ErrorCode(1002014006, "手机号已被使用"); + ErrorCode SMS_CODE_IS_UNUSED = new ErrorCode(1002014007, "验证码未被使用"); + + // ========== 租户信息 1002015000 ========== + ErrorCode TENANT_NOT_EXISTS = new ErrorCode(1002015000, "租户不存在"); + ErrorCode TENANT_DISABLE = new ErrorCode(1002015001, "名字为【{}】的租户已被禁用"); + ErrorCode TENANT_EXPIRE = new ErrorCode(1002015002, "名字为【{}】的租户已过期"); + ErrorCode TENANT_CAN_NOT_UPDATE_SYSTEM = new ErrorCode(1002015003, "系统租户不能进行修改、删除等操作!"); + ErrorCode TENANT_NAME_DUPLICATE = new ErrorCode(1002015004, "名字为【{}】的租户已存在"); + + // ========== 租户套餐 1002016000 ========== + ErrorCode TENANT_PACKAGE_NOT_EXISTS = new ErrorCode(1002016000, "租户套餐不存在"); + ErrorCode TENANT_PACKAGE_USED = new ErrorCode(1002016001, "租户正在使用该套餐,请给租户重新设置套餐后再尝试删除"); + ErrorCode TENANT_PACKAGE_DISABLE = new ErrorCode(1002016002, "名字为【{}】的租户套餐已被禁用"); + + // ========== 错误码模块 1002017000 ========== + ErrorCode ERROR_CODE_NOT_EXISTS = new ErrorCode(1002017000, "错误码不存在"); + ErrorCode ERROR_CODE_DUPLICATE = new ErrorCode(1002017001, "已经存在编码为【{}】的错误码"); + + // ========== 社交用户 1002018000 ========== + ErrorCode SOCIAL_USER_AUTH_FAILURE = new ErrorCode(1002018000, "社交授权失败,原因是:{}"); + ErrorCode SOCIAL_USER_UNBIND_NOT_SELF = new ErrorCode(1002018001, "社交解绑失败,非当前用户绑定"); + ErrorCode SOCIAL_USER_NOT_FOUND = new ErrorCode(1002018002, "社交授权失败,找不到对应的用户"); + + // ========== 系统敏感词 1002019000 ========= + ErrorCode SENSITIVE_WORD_NOT_EXISTS = new ErrorCode(1002019000, "系统敏感词在所有标签中都不存在"); + ErrorCode SENSITIVE_WORD_EXISTS = new ErrorCode(1002019001, "系统敏感词已在标签中存在"); + + // ========== OAuth2 客户端 1002020000 ========= + ErrorCode OAUTH2_CLIENT_NOT_EXISTS = new ErrorCode(1002020000, "OAuth2 客户端不存在"); + ErrorCode OAUTH2_CLIENT_EXISTS = new ErrorCode(1002020001, "OAuth2 客户端编号已存在"); + ErrorCode OAUTH2_CLIENT_DISABLE = new ErrorCode(1002020002, "OAuth2 客户端已禁用"); + ErrorCode OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS = new ErrorCode(1002020003, "不支持该授权类型"); + ErrorCode OAUTH2_CLIENT_SCOPE_OVER = new ErrorCode(1002020004, "授权范围过大"); + ErrorCode OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH = new ErrorCode(1002020005, "无效 redirect_uri: {}"); + ErrorCode OAUTH2_CLIENT_CLIENT_SECRET_ERROR = new ErrorCode(1002020006, "无效 client_secret: {}"); + + // ========== OAuth2 授权 1002021000 ========= + ErrorCode OAUTH2_GRANT_CLIENT_ID_MISMATCH = new ErrorCode(1002021000, "client_id 不匹配"); + ErrorCode OAUTH2_GRANT_REDIRECT_URI_MISMATCH = new ErrorCode(1002021001, "redirect_uri 不匹配"); + ErrorCode OAUTH2_GRANT_STATE_MISMATCH = new ErrorCode(1002021002, "state 不匹配"); + ErrorCode OAUTH2_GRANT_CODE_NOT_EXISTS = new ErrorCode(1002021003, "code 不存在"); + + // ========== OAuth2 授权 1002022000 ========= + ErrorCode OAUTH2_CODE_NOT_EXISTS = new ErrorCode(1002022000, "code 不存在"); + ErrorCode OAUTH2_CODE_EXPIRE = new ErrorCode(1002022001, "code 已过期"); + + // ========== 邮箱账号 1002023000 ========== + ErrorCode MAIL_ACCOUNT_NOT_EXISTS = new ErrorCode(1002023000, "邮箱账号不存在"); + ErrorCode MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS = new ErrorCode(1002023001, "无法删除,该邮箱账号还有邮件模板"); + + // ========== 邮件模版 1002024000 ========== + ErrorCode MAIL_TEMPLATE_NOT_EXISTS = new ErrorCode(1002024000, "邮件模版不存在"); + ErrorCode MAIL_TEMPLATE_CODE_EXISTS = new ErrorCode(1002024001, "邮件模版 code({}) 已存在"); + + // ========== 邮件发送 1002025000 ========== + ErrorCode MAIL_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1002025000, "模板参数({})缺失"); + ErrorCode MAIL_SEND_MAIL_NOT_EXISTS = new ErrorCode(1002025001, "邮箱不存在"); + + // ========== 站内信模版 1002026000 ========== + ErrorCode NOTIFY_TEMPLATE_NOT_EXISTS = new ErrorCode(1002026000, "站内信模版不存在"); + ErrorCode NOTIFY_TEMPLATE_CODE_DUPLICATE = new ErrorCode(1002026001, "已经存在编码为【{}】的站内信模板"); + + // ========== 站内信模版 1002027000 ========== + + // ========== 站内信发送 1002028000 ========== + ErrorCode NOTIFY_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1002028000, "模板参数({})缺失"); + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/common/SexEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/common/SexEnum.java new file mode 100644 index 0000000..d714eff --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/common/SexEnum.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.enums.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 性别的枚举值 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SexEnum { + + /** 男 */ + MALE(1), + /** 女 */ + FEMALE(2), + /* 未知 */ + UNKNOWN(3); + + /** + * 性别 + */ + private final Integer sex; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/dept/DeptIdEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/dept/DeptIdEnum.java new file mode 100644 index 0000000..94b3132 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/dept/DeptIdEnum.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.enums.dept; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 部门编号枚举 + */ +@Getter +@AllArgsConstructor +public enum DeptIdEnum { + + /** + * 根节点 + */ + ROOT(0L); + + private final Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/errorcode/ErrorCodeTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/errorcode/ErrorCodeTypeEnum.java new file mode 100644 index 0000000..36b421b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/errorcode/ErrorCodeTypeEnum.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.system.enums.errorcode; + +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 错误码的类型枚举 + * + * @author dylan + */ +@AllArgsConstructor +@Getter +public enum ErrorCodeTypeEnum implements IntArrayValuable { + + /** + * 自动生成 + */ + AUTO_GENERATION(1), + /** + * 手动编辑 + */ + MANUAL_OPERATION(2); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(ErrorCodeTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + + @Override + public int[] array() { + return ARRAYS; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/logger/LoginLogTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/logger/LoginLogTypeEnum.java new file mode 100644 index 0000000..8802500 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/logger/LoginLogTypeEnum.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 登录日志的类型枚举 + */ +@Getter +@AllArgsConstructor +public enum LoginLogTypeEnum { + + LOGIN_USERNAME(100), // 使用账号登录 + LOGIN_SOCIAL(101), // 使用社交登录 + LOGIN_MOBILE(103), // 使用手机登陆 + LOGIN_SMS(104), // 使用短信登陆 + + LOGOUT_SELF(200), // 自己主动登出 + LOGOUT_DELETE(202), // 强制退出 + ; + + /** + * 日志类型 + */ + private final Integer type; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/logger/LoginResultEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/logger/LoginResultEnum.java new file mode 100644 index 0000000..e98afeb --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/logger/LoginResultEnum.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.enums.logger; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 登录结果的枚举类 + */ +@Getter +@AllArgsConstructor +public enum LoginResultEnum { + + SUCCESS(0), // 成功 + BAD_CREDENTIALS(10), // 账号或密码不正确 + USER_DISABLED(20), // 用户被禁用 + CAPTCHA_NOT_FOUND(30), // 图片验证码不存在 + CAPTCHA_CODE_ERROR(31), // 图片验证码不正确 + + ; + + /** + * 结果 + */ + private final Integer result; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/mail/MailSendStatusEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/mail/MailSendStatusEnum.java new file mode 100644 index 0000000..cefe8f8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/mail/MailSendStatusEnum.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.enums.mail; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 邮件的发送状态枚举 + * + * @author wangjingyi + * @since 2022/4/10 13:39 + */ +@Getter +@AllArgsConstructor +public enum MailSendStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 发送成功 + FAILURE(20), // 发送失败 + IGNORE(30), // 忽略,即不发送 + ; + + private final int status; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/notice/NoticeTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/notice/NoticeTypeEnum.java new file mode 100644 index 0000000..1d88fb6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/notice/NoticeTypeEnum.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.enums.notice; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 通知类型 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum NoticeTypeEnum { + + NOTICE(1), + ANNOUNCEMENT(2); + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/notify/NotifyTemplateTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/notify/NotifyTemplateTypeEnum.java new file mode 100644 index 0000000..d0917d0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/notify/NotifyTemplateTypeEnum.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.enums.notify; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 通知模板类型枚举 + * + * @author HUIHUI + */ +@Getter +@AllArgsConstructor +public enum NotifyTemplateTypeEnum { + + /** + * 系统消息 + */ + SYSTEM_MESSAGE(2), + /** + * 通知消息 + */ + NOTIFICATION_MESSAGE(1); + + private final Integer type; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/oauth2/OAuth2ClientConstants.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/oauth2/OAuth2ClientConstants.java new file mode 100644 index 0000000..8230adf --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/oauth2/OAuth2ClientConstants.java @@ -0,0 +1,12 @@ +package com.yunxi.scm.module.system.enums.oauth2; + +/** + * OAuth2.0 客户端的通用枚举 + * + * @author 芋道源码 + */ +public interface OAuth2ClientConstants { + + String CLIENT_ID_DEFAULT = "default"; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/oauth2/OAuth2GrantTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/oauth2/OAuth2GrantTypeEnum.java new file mode 100644 index 0000000..6cf70e0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/oauth2/OAuth2GrantTypeEnum.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.system.enums.oauth2; + +import cn.hutool.core.util.ArrayUtil; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * OAuth2 授权类型(模式)的枚举 + * + * @author 芋道源码 + */ +@AllArgsConstructor +@Getter +public enum OAuth2GrantTypeEnum { + + PASSWORD("password"), // 密码模式 + AUTHORIZATION_CODE("authorization_code"), // 授权码模式 + IMPLICIT("implicit"), // 简化模式 + CLIENT_CREDENTIALS("client_credentials"), // 客户端模式 + REFRESH_TOKEN("refresh_token"), // 刷新模式 + ; + + private final String grantType; + + public static OAuth2GrantTypeEnum getByGranType(String grantType) { + return ArrayUtil.firstMatch(o -> o.getGrantType().equals(grantType), values()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/DataScopeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/DataScopeEnum.java new file mode 100644 index 0000000..0346c51 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/DataScopeEnum.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 数据范围枚举类 + * + * 用于实现数据级别的权限 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum DataScopeEnum { + + ALL(1), // 全部数据权限 + + DEPT_CUSTOM(2), // 指定部门数据权限 + DEPT_ONLY(3), // 部门数据权限 + DEPT_AND_CHILD(4), // 部门及以下数据权限 + + SELF(5); // 仅本人数据权限 + + /** + * 范围 + */ + private final Integer scope; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/MenuTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/MenuTypeEnum.java new file mode 100644 index 0000000..d4e5af4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/MenuTypeEnum.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 菜单类型枚举类 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum MenuTypeEnum { + + DIR(1), // 目录 + MENU(2), // 菜单 + BUTTON(3) // 按钮 + ; + + /** + * 类型 + */ + private final Integer type; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/RoleCodeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/RoleCodeEnum.java new file mode 100644 index 0000000..5178fd5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/RoleCodeEnum.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.enums.permission; + +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 角色标识枚举 + */ +@Getter +@AllArgsConstructor +public enum RoleCodeEnum { + + SUPER_ADMIN("super_admin", "超级管理员"), + TENANT_ADMIN("tenant_admin", "租户管理员"), + ; + + /** + * 角色编码 + */ + private final String code; + /** + * 名字 + */ + private final String name; + + public static boolean isSuperAdmin(String code) { + return ObjectUtils.equalsAny(code, SUPER_ADMIN.getCode()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/RoleTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/RoleTypeEnum.java new file mode 100644 index 0000000..222a239 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/permission/RoleTypeEnum.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.enums.permission; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum RoleTypeEnum { + + /** + * 内置角色 + */ + SYSTEM(1), + /** + * 自定义角色 + */ + CUSTOM(2); + + private final Integer type; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsReceiveStatusEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsReceiveStatusEnum.java new file mode 100644 index 0000000..d35ae5a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsReceiveStatusEnum.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的接收状态枚举 + * + * @author 芋道源码 + * @date 2021/2/1 13:39 + */ +@Getter +@AllArgsConstructor +public enum SmsReceiveStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 接收成功 + FAILURE(20), // 接收失败 + ; + + private final int status; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsSceneEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsSceneEnum.java new file mode 100644 index 0000000..ed0c5c6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsSceneEnum.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.system.enums.sms; + +import cn.hutool.core.util.ArrayUtil; +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 用户短信验证码发送场景的枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SmsSceneEnum implements IntArrayValuable { + + MEMBER_LOGIN(1, "user-sms-login", "会员用户 - 手机号登陆"), + MEMBER_UPDATE_MOBILE(2, "user-sms-reset-password", "会员用户 - 修改手机"), + MEMBER_FORGET_PASSWORD(3, "user-sms-update-mobile", "会员用户 - 忘记密码"), + + ADMIN_MEMBER_LOGIN(21, "admin-sms-login", "后台用户 - 手机号登录"); + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SmsSceneEnum::getScene).toArray(); + + /** + * 验证场景的编号 + */ + private final Integer scene; + /** + * 模版编码 + */ + private final String templateCode; + /** + * 描述 + */ + private final String description; + + @Override + public int[] array() { + return ARRAYS; + } + + public static SmsSceneEnum getCodeByScene(Integer scene) { + return ArrayUtil.firstMatch(sceneEnum -> sceneEnum.getScene().equals(scene), + values()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsSendStatusEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsSendStatusEnum.java new file mode 100644 index 0000000..8f6225a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsSendStatusEnum.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的发送状态枚举 + * + * @author zzf + * @date 2021/2/1 13:39 + */ +@Getter +@AllArgsConstructor +public enum SmsSendStatusEnum { + + INIT(0), // 初始化 + SUCCESS(10), // 发送成功 + FAILURE(20), // 发送失败 + IGNORE(30), // 忽略,即不发送 + ; + + private final int status; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsTemplateTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsTemplateTypeEnum.java new file mode 100644 index 0000000..f06cfd8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/sms/SmsTemplateTypeEnum.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.enums.sms; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 短信的模板类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SmsTemplateTypeEnum { + + VERIFICATION_CODE(1), // 验证码 + NOTICE(2), // 通知 + PROMOTION(3), // 营销 + ; + + /** + * 类型 + */ + private final int type; + +} diff --git a/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/social/SocialTypeEnum.java b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/social/SocialTypeEnum.java new file mode 100644 index 0000000..bbc8043 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-api/src/main/java/com/yunxi/scm/module/system/enums/social/SocialTypeEnum.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.module.system.enums.social; + +import cn.hutool.core.util.ArrayUtil; +import com.yunxi.scm.framework.common.core.IntArrayValuable; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Arrays; + +/** + * 社交平台的类型枚举 + * + * @author 芋道源码 + */ +@Getter +@AllArgsConstructor +public enum SocialTypeEnum implements IntArrayValuable { + + /** + * Gitee + * 文档链接:https://gitee.com/api/v5/oauth_doc#/ + */ + GITEE(10, "GITEE"), + /** + * 钉钉 + * 文档链接:https://developers.dingtalk.com/document/app/obtain-identity-credentials + */ + DINGTALK(20, "DINGTALK"), + + /** + * 企业微信 + * 文档链接:https://xkcoding.com/2019/08/06/use-justauth-integration-wechat-enterprise.html + */ + WECHAT_ENTERPRISE(30, "WECHAT_ENTERPRISE"), + /** + * 微信公众平台 - 移动端 H5 + * 文档链接:https://www.cnblogs.com/juewuzhe/p/11905461.html + */ + WECHAT_MP(31, "WECHAT_MP"), + /** + * 微信开放平台 - 网站应用 PC 端扫码授权登录 + * 文档链接:https://justauth.wiki/guide/oauth/wechat_open/#_2-申请开发者资质认证 + */ + WECHAT_OPEN(32, "WECHAT_OPEN"), + /** + * 微信小程序 + * 文档链接:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html + */ + WECHAT_MINI_APP(34, "WECHAT_MINI_APP"), + ; + + public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(SocialTypeEnum::getType).toArray(); + + /** + * 类型 + */ + private final Integer type; + /** + * 类型的标识 + */ + private final String source; + + @Override + public int[] array() { + return ARRAYS; + } + + public static SocialTypeEnum valueOfType(Integer type) { + return ArrayUtil.firstMatch(o -> o.getType().equals(type), values()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/pom.xml b/yunxi-module-system/yunxi-module-system-biz/pom.xml new file mode 100644 index 0000000..7cff411 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/pom.xml @@ -0,0 +1,124 @@ + + + + com.yunxi.scm + yunxi-module-system + ${revision} + + 4.0.0 + yunxi-module-system-biz + jar + + ${project.artifactId} + + system 模块下,我们放通用业务,支撑上层的核心业务。 + 例如说:用户、部门、权限、数据字典等等 + + + + + com.yunxi.scm + yunxi-module-system-api + ${revision} + + + com.yunxi.scm + yunxi-module-infra-api + ${revision} + + + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-operatelog + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-sms + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-dict + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-data-permission + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-social + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-tenant + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-ip + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-weixin + + + + + com.yunxi.scm + yunxi-spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-validation + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mybatis + + + + com.yunxi.scm + yunxi-spring-boot-starter-redis + + + + + com.yunxi.scm + yunxi-spring-boot-starter-job + + + + + com.yunxi.scm + yunxi-spring-boot-starter-mq + + + + + com.yunxi.scm + yunxi-spring-boot-starter-test + test + + + + + com.yunxi.scm + yunxi-spring-boot-starter-excel + + + + com.yunxi.scm + yunxi-spring-boot-starter-captcha + + + + org.springframework.boot + spring-boot-starter-mail + + + + diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dept/DeptApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dept/DeptApiImpl.java new file mode 100644 index 0000000..31be8ed --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dept/DeptApiImpl.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.system.api.dept; + +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.convert.dept.DeptConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.service.dept.DeptService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * 部门 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class DeptApiImpl implements DeptApi { + + @Resource + private DeptService deptService; + + @Override + public DeptRespDTO getDept(Long id) { + DeptDO dept = deptService.getDept(id); + return DeptConvert.INSTANCE.convert03(dept); + } + + @Override + public List getDeptList(Collection ids) { + List depts = deptService.getDeptList(ids); + return DeptConvert.INSTANCE.convertList03(depts); + } + + @Override + public void validateDeptList(Collection ids) { + deptService.validateDeptList(ids); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dept/PostApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dept/PostApiImpl.java new file mode 100644 index 0000000..ab772d5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dept/PostApiImpl.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.api.dept; + +import com.yunxi.scm.module.system.service.dept.PostService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; + +/** + * 岗位 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PostApiImpl implements PostApi { + + @Resource + private PostService postService; + + @Override + public void validPostList(Collection ids) { + postService.validatePostList(ids); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dict/DictDataApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dict/DictDataApiImpl.java new file mode 100644 index 0000000..a220224 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/dict/DictDataApiImpl.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.api.dict; + +import com.yunxi.scm.module.system.api.dict.dto.DictDataRespDTO; +import com.yunxi.scm.module.system.convert.dict.DictDataConvert; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictDataDO; +import com.yunxi.scm.module.system.service.dict.DictDataService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; + +/** + * 字典数据 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class DictDataApiImpl implements DictDataApi { + + @Resource + private DictDataService dictDataService; + + @Override + public void validateDictDataList(String dictType, Collection values) { + dictDataService.validateDictDataList(dictType, values); + } + + @Override + public DictDataRespDTO getDictData(String dictType, String value) { + DictDataDO dictData = dictDataService.getDictData(dictType, value); + return DictDataConvert.INSTANCE.convert02(dictData); + } + + @Override + public DictDataRespDTO parseDictData(String dictType, String label) { + DictDataDO dictData = dictDataService.parseDictData(dictType, label); + return DictDataConvert.INSTANCE.convert02(dictData); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/errorcode/ErrorCodeApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/errorcode/ErrorCodeApiImpl.java new file mode 100644 index 0000000..fbcd70f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/errorcode/ErrorCodeApiImpl.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.api.errorcode; + +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import com.yunxi.scm.module.system.service.errorcode.ErrorCodeService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 错误码 Api 实现类 + * + * @author 芋道源码 + */ +@Service +public class ErrorCodeApiImpl implements ErrorCodeApi { + + @Resource + private ErrorCodeService errorCodeService; + + @Override + public void autoGenerateErrorCodeList(List autoGenerateDTOs) { + errorCodeService.autoGenerateErrorCodes(autoGenerateDTOs); + } + + @Override + public List getErrorCodeList(String applicationName, LocalDateTime minUpdateTime) { + return errorCodeService.getErrorCodeList(applicationName, minUpdateTime); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/logger/LoginLogApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/logger/LoginLogApiImpl.java new file mode 100644 index 0000000..0e6e0ae --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/logger/LoginLogApiImpl.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.api.logger; + +import com.yunxi.scm.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.yunxi.scm.module.system.service.logger.LoginLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 登录日志的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class LoginLogApiImpl implements LoginLogApi { + + @Resource + private LoginLogService loginLogService; + + @Override + public void createLoginLog(LoginLogCreateReqDTO reqDTO) { + loginLogService.createLoginLog(reqDTO); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/logger/OperateLogApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/logger/OperateLogApiImpl.java new file mode 100644 index 0000000..7778cb7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/logger/OperateLogApiImpl.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.api.logger; + +import com.yunxi.scm.module.system.api.logger.dto.OperateLogCreateReqDTO; +import com.yunxi.scm.module.system.service.logger.OperateLogService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 操作日志 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class OperateLogApiImpl implements OperateLogApi { + + @Resource + private OperateLogService operateLogService; + + @Override + public void createOperateLog(OperateLogCreateReqDTO createReqDTO) { + operateLogService.createOperateLog(createReqDTO); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/mail/MailSendApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/mail/MailSendApiImpl.java new file mode 100644 index 0000000..f0aeb50 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/mail/MailSendApiImpl.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.api.mail; + +import com.yunxi.scm.module.system.api.mail.dto.MailSendSingleToUserReqDTO; +import com.yunxi.scm.module.system.service.mail.MailSendService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 邮件发送 API 实现类 + * + * @author wangjingyi + */ +@Service +@Validated +public class MailSendApiImpl implements MailSendApi { + + @Resource + private MailSendService mailSendService; + + @Override + public Long sendSingleMailToAdmin(MailSendSingleToUserReqDTO reqDTO) { + return mailSendService.sendSingleMailToAdmin(reqDTO.getMail(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + + @Override + public Long sendSingleMailToMember(MailSendSingleToUserReqDTO reqDTO) { + return mailSendService.sendSingleMailToMember(reqDTO.getMail(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/notify/NotifyMessageSendApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/notify/NotifyMessageSendApiImpl.java new file mode 100644 index 0000000..dd2bc3e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/notify/NotifyMessageSendApiImpl.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.system.api.notify; + +import com.yunxi.scm.module.system.api.notify.dto.NotifySendSingleToUserReqDTO; +import com.yunxi.scm.module.system.api.notify.dto.NotifyTemplateReqDTO; +import com.yunxi.scm.module.system.service.notify.NotifySendService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * 站内信发送 API 实现类 + * + * @author xrcoder + */ +@Service +public class NotifyMessageSendApiImpl implements NotifyMessageSendApi { + + @Resource + private NotifySendService notifySendService; + + @Override + public Long sendSingleMessageToAdmin(NotifySendSingleToUserReqDTO reqDTO) { + return notifySendService.sendSingleNotifyToAdmin(reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + + @Override + public Long sendSingleMessageToMember(NotifySendSingleToUserReqDTO reqDTO) { + return notifySendService.sendSingleNotifyToMember(reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + + @Override + public boolean validateNotifyTemplate(String orderDelivery) { + return false; + } + + @Override + public void createNotifyTemplate(NotifyTemplateReqDTO templateReqDTO) { + + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/oauth2/OAuth2TokenApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/oauth2/OAuth2TokenApiImpl.java new file mode 100644 index 0000000..f9e128a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/oauth2/OAuth2TokenApiImpl.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.system.api.oauth2; + +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenCreateReqDTO; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; +import com.yunxi.scm.module.system.convert.auth.OAuth2TokenConvert; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.service.oauth2.OAuth2TokenService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +/** + * OAuth2.0 Token API 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2TokenApiImpl implements OAuth2TokenApi { + + @Resource + private OAuth2TokenService oauth2TokenService; + + @Override + public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken( + reqDTO.getUserId(), reqDTO.getUserType(), reqDTO.getClientId(), reqDTO.getScopes()); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + + @Override + public OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken) { + return OAuth2TokenConvert.INSTANCE.convert(oauth2TokenService.checkAccessToken(accessToken)); + } + + @Override + public OAuth2AccessTokenRespDTO removeAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(accessToken); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + + @Override + public OAuth2AccessTokenRespDTO refreshAccessToken(String refreshToken, String clientId) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); + return OAuth2TokenConvert.INSTANCE.convert2(accessTokenDO); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/permission/PermissionApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/permission/PermissionApiImpl.java new file mode 100644 index 0000000..31188a2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/permission/PermissionApiImpl.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.api.permission; + +import com.yunxi.scm.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.yunxi.scm.module.system.service.permission.PermissionService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Set; + +/** + * 权限 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class PermissionApiImpl implements PermissionApi { + + @Resource + private PermissionService permissionService; + + @Override + public Set getUserRoleIdListByRoleIds(Collection roleIds) { + return permissionService.getUserRoleIdListByRoleId(roleIds); + } + + @Override + public boolean hasAnyPermissions(Long userId, String... permissions) { + return permissionService.hasAnyPermissions(userId, permissions); + } + + @Override + public boolean hasAnyRoles(Long userId, String... roles) { + return permissionService.hasAnyRoles(userId, roles); + } + + @Override + public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { + return permissionService.getDeptDataPermission(userId); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/permission/RoleApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/permission/RoleApiImpl.java new file mode 100644 index 0000000..16a64c2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/permission/RoleApiImpl.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.api.permission; + +import com.yunxi.scm.module.system.service.permission.RoleService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; + +/** + * 角色 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class RoleApiImpl implements RoleApi { + + @Resource + private RoleService roleService; + + @Override + public void validRoleList(Collection ids) { + roleService.validateRoleList(ids); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sensitiveword/SensitiveWordApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sensitiveword/SensitiveWordApiImpl.java new file mode 100644 index 0000000..86242f4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sensitiveword/SensitiveWordApiImpl.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.system.api.sensitiveword; + +import com.yunxi.scm.module.system.service.sensitiveword.SensitiveWordService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 敏感词 API 实现类 + * + * @author 永不言败 + */ +@Service +public class SensitiveWordApiImpl implements SensitiveWordApi { + + @Resource + private SensitiveWordService sensitiveWordService; + + @Override + public List validateText(String text, List tags) { + return sensitiveWordService.validateText(text, tags); + } + + @Override + public boolean isTextValid(String text, List tags) { + return sensitiveWordService.isTextValid(text, tags); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sms/SmsCodeApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sms/SmsCodeApiImpl.java new file mode 100644 index 0000000..7bce985 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sms/SmsCodeApiImpl.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.system.api.sms; + +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.yunxi.scm.module.system.service.sms.SmsCodeService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 短信验证码 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsCodeApiImpl implements SmsCodeApi { + + @Resource + private SmsCodeService smsCodeService; + + @Override + public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { + smsCodeService.sendSmsCode(reqDTO); + } + + @Override + public void useSmsCode(SmsCodeUseReqDTO reqDTO) { + smsCodeService.useSmsCode(reqDTO); + } + + @Override + public void validateSmsCode(SmsCodeValidateReqDTO reqDTO) { + smsCodeService.validateSmsCode(reqDTO); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sms/SmsSendApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sms/SmsSendApiImpl.java new file mode 100644 index 0000000..8bc8963 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/sms/SmsSendApiImpl.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.api.sms; + +import com.yunxi.scm.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO; +import com.yunxi.scm.module.system.service.sms.SmsSendService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 短信发送 API 接口 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsSendApiImpl implements SmsSendApi { + + @Resource + private SmsSendService smsSendService; + + @Override + public Long sendSingleSmsToAdmin(SmsSendSingleToUserReqDTO reqDTO) { + return smsSendService.sendSingleSmsToAdmin(reqDTO.getMobile(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + + @Override + public Long sendSingleSmsToMember(SmsSendSingleToUserReqDTO reqDTO) { + return smsSendService.sendSingleSmsToMember(reqDTO.getMobile(), reqDTO.getUserId(), + reqDTO.getTemplateCode(), reqDTO.getTemplateParams()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/social/SocialUserApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/social/SocialUserApiImpl.java new file mode 100644 index 0000000..e06373e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/social/SocialUserApiImpl.java @@ -0,0 +1,44 @@ +package com.yunxi.scm.module.system.api.social; + +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.api.social.dto.SocialUserUnbindReqDTO; +import com.yunxi.scm.module.system.service.social.SocialUserService; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; + +/** + * 社交用户的 API 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SocialUserApiImpl implements SocialUserApi { + + @Resource + private SocialUserService socialUserService; + + @Override + public String getAuthorizeUrl(Integer type, String redirectUri) { + return socialUserService.getAuthorizeUrl(type, redirectUri); + } + + @Override + public void bindSocialUser(SocialUserBindReqDTO reqDTO) { + socialUserService.bindSocialUser(reqDTO); + } + + @Override + public void unbindSocialUser(SocialUserUnbindReqDTO reqDTO) { + socialUserService.unbindSocialUser(reqDTO.getUserId(), reqDTO.getUserType(), + reqDTO.getType(), reqDTO.getUnionId()); + } + + @Override + public Long getBindUserId(Integer userType, Integer type, String code, String state) { + return socialUserService.getBindUserId(userType, type, code, state); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/tenant/TenantApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/tenant/TenantApiImpl.java new file mode 100644 index 0000000..f1cb9e0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/tenant/TenantApiImpl.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.api.tenant; + +import com.yunxi.scm.module.system.service.tenant.TenantService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 多租户的 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class TenantApiImpl implements TenantApi { + + @Resource + private TenantService tenantService; + + @Override + public List getTenantIdList() { + return tenantService.getTenantIdList(); + } + + @Override + public void validateTenant(Long id) { + tenantService.validTenant(id); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/user/AdminUserApiImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/user/AdminUserApiImpl.java new file mode 100644 index 0000000..a0182bd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/api/user/AdminUserApiImpl.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.system.api.user; + +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import com.yunxi.scm.module.system.convert.user.UserConvert; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +/** + * Admin 用户 API 实现类 + * + * @author 芋道源码 + */ +@Service +public class AdminUserApiImpl implements AdminUserApi { + + @Resource + private AdminUserService userService; + + @Override + public AdminUserRespDTO getUser(Long id) { + AdminUserDO user = userService.getUser(id); + return UserConvert.INSTANCE.convert4(user); + } + + @Override + public List getUserList(Collection ids) { + List users = userService.getUserList(ids); + return UserConvert.INSTANCE.convertList4(users); + } + + @Override + public List getUserListByDeptIds(Collection deptIds) { + List users = userService.getUserListByDeptIds(deptIds); + return UserConvert.INSTANCE.convertList4(users); + } + + @Override + public List getUserListByPostIds(Collection postIds) { + List users = userService.getUserListByPostIds(postIds); + return UserConvert.INSTANCE.convertList4(users); + } + + @Override + public void validateUserList(Collection ids) { + userService.validateUserList(ids); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/AuthController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/AuthController.http new file mode 100644 index 0000000..00ae2ba --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/AuthController.http @@ -0,0 +1,33 @@ +### 请求 /login 接口 => 成功 +POST {{baseUrl}}/system/auth/login +Content-Type: application/json +tenant-id: {{adminTenentId}} +tag: Yunai.local + +{ + "username": "admin", + "password": "admin123", + "uuid": "3acd87a09a4f48fb9118333780e94883", + "code": "1024" +} + +### 请求 /login 接口 => 成功(无验证码) +POST {{baseUrl}}/system/auth/login +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "username": "admin", + "password": "admin123" +} + +### 请求 /get-permission-info 接口 => 成功 +GET {{baseUrl}}/system/auth/get-permission-info +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /list-menus 接口 => 成功 +GET {{baseUrl}}/system/list-menus +Authorization: Bearer {{token}} +#Authorization: Bearer a6aa7714a2e44c95aaa8a2c5adc2a67a +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/AuthController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/AuthController.java new file mode 100644 index 0000000..2d6ec1a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/AuthController.java @@ -0,0 +1,156 @@ +package com.yunxi.scm.module.system.controller.admin.auth; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.framework.security.config.SecurityProperties; +import com.yunxi.scm.module.system.controller.admin.auth.vo.*; +import com.yunxi.scm.module.system.convert.auth.AuthConvert; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.enums.logger.LoginLogTypeEnum; +import com.yunxi.scm.module.system.service.auth.AdminAuthService; +import com.yunxi.scm.module.system.service.permission.MenuService; +import com.yunxi.scm.module.system.service.permission.PermissionService; +import com.yunxi.scm.module.system.service.permission.RoleService; +import com.yunxi.scm.module.system.service.social.SocialUserService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization; + +@Tag(name = "管理后台 - 认证") +@RestController +@RequestMapping("/system/auth") +@Validated +@Slf4j +public class AuthController { + + @Resource + private AdminAuthService authService; + @Resource + private AdminUserService userService; + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; + @Resource + private PermissionService permissionService; + @Resource + private SocialUserService socialUserService; + + @Resource + private SecurityProperties securityProperties; + + @PostMapping("/login") + @PermitAll + @Operation(summary = "使用账号密码登录") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult login(@RequestBody @Valid AuthLoginReqVO reqVO) { + return success(authService.login(reqVO)); + } + + @PostMapping("/logout") + @PermitAll + @Operation(summary = "登出系统") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult logout(HttpServletRequest request) { + String token = obtainAuthorization(request, securityProperties.getTokenHeader()); + if (StrUtil.isNotBlank(token)) { + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + } + return success(true); + } + + @PostMapping("/refresh-token") + @PermitAll + @Operation(summary = "刷新令牌") + @Parameter(name = "refreshToken", description = "刷新令牌", required = true) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult refreshToken(@RequestParam("refreshToken") String refreshToken) { + return success(authService.refreshToken(refreshToken)); + } + + @GetMapping("/get-permission-info") + @Operation(summary = "获取登录用户的权限信息") + public CommonResult getPermissionInfo() { + // 1.1 获得用户信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + if (user == null) { + return null; + } + + // 1.2 获得角色列表 + Set roleIds = permissionService.getUserRoleIdListByUserId(getLoginUserId()); + List roles = roleService.getRoleList(roleIds); + roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); // 移除禁用的角色 + + // 1.3 获得菜单列表 + Set menuIds = permissionService.getRoleMenuListByRoleId(convertSet(roles, RoleDO::getId)); + List menuList = menuService.getMenuList(menuIds); + menuList.removeIf(menu -> !CommonStatusEnum.ENABLE.getStatus().equals(menu.getStatus())); // 移除禁用的菜单 + + // 2. 拼接结果返回 + return success(AuthConvert.INSTANCE.convert(user, roles, menuList)); + } + + // ========== 短信登录相关 ========== + + @PostMapping("/sms-login") + @PermitAll + @Operation(summary = "使用短信验证码登录") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { + return success(authService.smsLogin(reqVO)); + } + + @PostMapping("/send-sms-code") + @PermitAll + @Operation(summary = "发送手机验证码") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult sendLoginSmsCode(@RequestBody @Valid AuthSmsSendReqVO reqVO) { + authService.sendSmsCode(reqVO); + return success(true); + } + + // ========== 社交登录相关 ========== + + @GetMapping("/social-auth-redirect") + @PermitAll + @Operation(summary = "社交授权的跳转") + @Parameters({ + @Parameter(name = "type", description = "社交类型", required = true), + @Parameter(name = "redirectUri", description = "回调路径") + }) + public CommonResult socialLogin(@RequestParam("type") Integer type, + @RequestParam("redirectUri") String redirectUri) { + return CommonResult.success(socialUserService.getAuthorizeUrl(type, redirectUri)); + } + + @PostMapping("/social-login") + @PermitAll + @Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult socialQuickLogin(@RequestBody @Valid AuthSocialLoginReqVO reqVO) { + return success(authService.socialLogin(reqVO)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthLoginReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthLoginReqVO.java new file mode 100644 index 0000000..d28eefe --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthLoginReqVO.java @@ -0,0 +1,69 @@ +package com.yunxi.scm.module.system.controller.admin.auth.vo; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; + +@Schema(description = "管理后台 - 账号密码登录 Request VO,如果登录并绑定社交用户,需要传递 social 开头的参数") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthLoginReqVO { + + @Schema(description = "账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxiyuanma") + @NotEmpty(message = "登录账号不能为空") + @Length(min = 4, max = 16, message = "账号长度为 4-16 位") + @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "buzhidao") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + + // ========== 图片验证码相关 ========== + + @Schema(description = "验证码,验证码开启时,需要传递", requiredMode = Schema.RequiredMode.REQUIRED, + example = "PfcH6mgr8tpXuMWFjvW6YVaqrswIuwmWI5dsVZSg7sGpWtDCUbHuDEXl3cFB1+VvCC/rAkSwK8Fad52FSuncVg==") + @NotEmpty(message = "验证码不能为空", groups = CodeEnableGroup.class) + private String captchaVerification; + + // ========== 绑定社交登录时,需要传递如下参数 ========== + + @Schema(description = "社交平台的类型,参见 SysUserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + private Integer socialType; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private String socialCode; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + private String socialState; + + /** + * 开启验证码的 Group + */ + public interface CodeEnableGroup {} + + @AssertTrue(message = "授权码不能为空") + public boolean isSocialCodeValid() { + return socialType == null || StrUtil.isNotEmpty(socialCode); + } + + @AssertTrue(message = "授权 state 不能为空") + public boolean isSocialState() { + return socialType == null || StrUtil.isNotEmpty(socialState); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthLoginRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthLoginRespVO.java new file mode 100644 index 0000000..66a1fc4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthLoginRespVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 登录 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthLoginRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long userId; + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "happy") + private String accessToken; + + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + private String refreshToken; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime expiresTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthMenuRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthMenuRespVO.java new file mode 100644 index 0000000..15783d2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthMenuRespVO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 登录用户的菜单信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthMenuRespVO { + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private Long id; + + @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") + private String path; + + @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") + private String component; + + @Schema(description = "组件名", example = "SystemUser") + private String componentName; + + @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") + private String icon; + + @Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean visible; + + @Schema(description = "是否缓存", requiredMode = Schema.RequiredMode.REQUIRED, example = "false") + private Boolean keepAlive; + + @Schema(description = "是否总是显示", example = "false") + private Boolean alwaysShow; + + /** + * 子路由 + */ + private List children; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java new file mode 100644 index 0000000..ef4cacf --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthPermissionInfoRespVO.java @@ -0,0 +1,93 @@ +package com.yunxi.scm.module.system.controller.admin.auth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Set; + +@Schema(description = "管理后台 - 登录用户的权限信息 Response VO,额外包括用户信息和角色列表") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthPermissionInfoRespVO { + + @Schema(description = "用户信息", requiredMode = Schema.RequiredMode.REQUIRED) + private UserVO user; + + @Schema(description = "角色标识数组", requiredMode = Schema.RequiredMode.REQUIRED) + private Set roles; + + @Schema(description = "操作权限数组", requiredMode = Schema.RequiredMode.REQUIRED) + private Set permissions; + + @Schema(description = "菜单树", required = true) + private List menus; + + @Schema(description = "用户信息 VO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class UserVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + private String nickname; + + @Schema(description = "用户头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.jpg") + private String avatar; + + } + + @Schema(description = "管理后台 - 登录用户的菜单信息 Response VO") + @Data + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class MenuVO { + + @Schema(description = "菜单名称", required = true, example = "芋道") + private Long id; + + @Schema(description = "父菜单 ID", required = true, example = "1024") + private Long parentId; + + @Schema(description = "菜单名称", required = true, example = "芋道") + private String name; + + @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") + private String path; + + @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") + private String component; + + @Schema(description = "组件名", example = "SystemUser") + private String componentName; + + @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") + private String icon; + + @Schema(description = "是否可见", required = true, example = "false") + private Boolean visible; + + @Schema(description = "是否缓存", required = true, example = "false") + private Boolean keepAlive; + + @Schema(description = "是否总是显示", example = "false") + private Boolean alwaysShow; + + /** + * 子路由 + */ + private List children; + + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java new file mode 100644 index 0000000..e0a19b2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSmsLoginReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.controller.admin.auth.vo; + +import com.yunxi.scm.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 短信验证码的登录 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSmsLoginReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxiyuanma") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "短信验证码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "验证码不能为空") + private String code; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java new file mode 100644 index 0000000..793b957 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSmsSendReqVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.system.controller.admin.auth.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.framework.common.validation.Mobile; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 发送手机验证码 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSmsSendReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxiyuanma") + @NotEmpty(message = "手机号不能为空") + @Mobile + private String mobile; + + @Schema(description = "短信场景", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "发送场景不能为空") + @InEnum(SmsSceneEnum.class) + private Integer scene; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java new file mode 100644 index 0000000..e943bf2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/auth/vo/AuthSocialLoginReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.controller.admin.auth.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 社交绑定登录 Request VO,使用 code 授权码 + 账号密码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class AuthSocialLoginReqVO { + + @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/captcha/CaptchaController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/captcha/CaptchaController.java new file mode 100644 index 0000000..e2f0675 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/captcha/CaptchaController.java @@ -0,0 +1,61 @@ +package com.yunxi.scm.module.system.controller.admin.captcha; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.xingyuv.captcha.model.common.ResponseModel; +import com.xingyuv.captcha.model.vo.CaptchaVO; +import com.xingyuv.captcha.service.CaptchaService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; + +/** + * 验证码 + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - 验证码") +@RestController("adminCaptchaController") +@RequestMapping("/system/captcha") +public class CaptchaController { + + @Resource + private CaptchaService captchaService; + + @PostMapping({"/get"}) + @Operation(summary = "获得验证码") + @PermitAll + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public ResponseModel get(@RequestBody CaptchaVO data, HttpServletRequest request) { + assert request.getRemoteHost() != null; + data.setBrowserInfo(getRemoteId(request)); + return captchaService.get(data); + } + + @PostMapping("/check") + @Operation(summary = "校验验证码") + @PermitAll + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public ResponseModel check(@RequestBody CaptchaVO data, HttpServletRequest request) { + data.setBrowserInfo(getRemoteId(request)); + return captchaService.check(data); + } + + public static String getRemoteId(HttpServletRequest request) { + String ip = ServletUtils.getClientIP(request); + String ua = request.getHeader("user-agent"); + if (StrUtil.isNotBlank(ip)) { + return ip + ua; + } + return request.getRemoteAddr() + ua; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/DeptController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/DeptController.java new file mode 100644 index 0000000..41d2339 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/DeptController.java @@ -0,0 +1,86 @@ +package com.yunxi.scm.module.system.controller.admin.dept; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.*; +import com.yunxi.scm.module.system.convert.dept.DeptConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.service.dept.DeptService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 部门") +@RestController +@RequestMapping("/system/dept") +@Validated +public class DeptController { + + @Resource + private DeptService deptService; + + @PostMapping("create") + @Operation(summary = "创建部门") + @PreAuthorize("@ss.hasPermission('system:dept:create')") + public CommonResult createDept(@Valid @RequestBody DeptCreateReqVO reqVO) { + Long deptId = deptService.createDept(reqVO); + return success(deptId); + } + + @PutMapping("update") + @Operation(summary = "更新部门") + @PreAuthorize("@ss.hasPermission('system:dept:update')") + public CommonResult updateDept(@Valid @RequestBody DeptUpdateReqVO reqVO) { + deptService.updateDept(reqVO); + return success(true); + } + + @DeleteMapping("delete") + @Operation(summary = "删除部门") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dept:delete')") + public CommonResult deleteDept(@RequestParam("id") Long id) { + deptService.deleteDept(id); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获取部门列表") + @PreAuthorize("@ss.hasPermission('system:dept:query')") + public CommonResult> getDeptList(DeptListReqVO reqVO) { + List list = deptService.getDeptList(reqVO); + list.sort(Comparator.comparing(DeptDO::getSort)); + return success(DeptConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取部门精简信息列表", description = "只包含被开启的部门,主要用于前端的下拉选项") + public CommonResult> getSimpleDeptList() { + // 获得部门列表,只要开启状态的 + DeptListReqVO reqVO = new DeptListReqVO(); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + List list = deptService.getDeptList(reqVO); + // 排序后,返回给前端 + list.sort(Comparator.comparing(DeptDO::getSort)); + return success(DeptConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/get") + @Operation(summary = "获得部门信息") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dept:query')") + public CommonResult getDept(@RequestParam("id") Long id) { + return success(DeptConvert.INSTANCE.convert(deptService.getDept(id))); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/PostController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/PostController.java new file mode 100644 index 0000000..f2f4b51 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/PostController.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.module.system.controller.admin.dept; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.*; +import com.yunxi.scm.module.system.convert.dept.PostConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import com.yunxi.scm.module.system.service.dept.PostService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 岗位") +@RestController +@RequestMapping("/system/post") +@Validated +public class PostController { + + @Resource + private PostService postService; + + @PostMapping("/create") + @Operation(summary = "创建岗位") + @PreAuthorize("@ss.hasPermission('system:post:create')") + public CommonResult createPost(@Valid @RequestBody PostCreateReqVO reqVO) { + Long postId = postService.createPost(reqVO); + return success(postId); + } + + @PutMapping("/update") + @Operation(summary = "修改岗位") + @PreAuthorize("@ss.hasPermission('system:post:update')") + public CommonResult updatePost(@Valid @RequestBody PostUpdateReqVO reqVO) { + postService.updatePost(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除岗位") + @PreAuthorize("@ss.hasPermission('system:post:delete')") + public CommonResult deletePost(@RequestParam("id") Long id) { + postService.deletePost(id); + return success(true); + } + + @GetMapping(value = "/get") + @Operation(summary = "获得岗位信息") + @Parameter(name = "id", description = "岗位编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:post:query')") + public CommonResult getPost(@RequestParam("id") Long id) { + return success(PostConvert.INSTANCE.convert(postService.getPost(id))); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取岗位精简信息列表", description = "只包含被开启的岗位,主要用于前端的下拉选项") + public CommonResult> getSimplePostList() { + // 获得岗位列表,只要开启状态的 + List list = postService.getPostList(null, Collections.singleton(CommonStatusEnum.ENABLE.getStatus())); + // 排序后,返回给前端 + list.sort(Comparator.comparing(PostDO::getSort)); + return success(PostConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/page") + @Operation(summary = "获得岗位分页列表") + @PreAuthorize("@ss.hasPermission('system:post:query')") + public CommonResult> getPostPage(@Validated PostPageReqVO reqVO) { + return success(PostConvert.INSTANCE.convertPage(postService.getPostPage(reqVO))); + } + + @GetMapping("/export") + @Operation(summary = "岗位管理") + @PreAuthorize("@ss.hasPermission('system:post:export')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Validated PostExportReqVO reqVO) throws IOException { + List posts = postService.getPostList(reqVO); + List data = PostConvert.INSTANCE.convertList03(posts); + // 输出 + ExcelUtils.write(response, "岗位数据.xls", "岗位列表", PostExcelVO.class, data); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptBaseVO.java new file mode 100644 index 0000000..fc9a0af --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptBaseVO.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 部门 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class DeptBaseVO { + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotBlank(message = "部门名称不能为空") + @Size(max = 30, message = "部门名称长度不能超过30个字符") + private String name; + + @Schema(description = "父菜单 ID", example = "1024") + private Long parentId; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "负责人的用户编号", example = "2048") + private Long leaderUserId; + + @Schema(description = "联系电话", example = "15601691000") + @Size(max = 11, message = "联系电话长度不能超过11个字符") + private String phone; + + @Schema(description = "邮箱", example = "yunxi@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过50个字符") + private String email; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") +// @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptCreateReqVO.java new file mode 100644 index 0000000..35fc030 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptCreateReqVO.java @@ -0,0 +1,12 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.dept; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 部门创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class DeptCreateReqVO extends DeptBaseVO { +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java new file mode 100644 index 0000000..7fe4cb1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptListReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 部门列表 Request VO") +@Data +public class DeptListReqVO { + + @Schema(description = "部门名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptRespVO.java new file mode 100644 index 0000000..d924ed7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptRespVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 部门信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DeptRespVO extends DeptBaseVO { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java new file mode 100644 index 0000000..f84dab2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptSimpleRespVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 部门精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeptSimpleRespVO { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "父部门 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptUpdateReqVO.java new file mode 100644 index 0000000..c677609 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/dept/DeptUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.dept; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 部门更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DeptUpdateReqVO extends DeptBaseVO { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "部门编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostBaseVO.java new file mode 100644 index 0000000..8cec1a6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostBaseVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 岗位 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class PostBaseVO { + + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") + @NotBlank(message = "岗位名称不能为空") + @Size(max = 50, message = "岗位名称长度不能超过50个字符") + private String name; + + @Schema(description = "岗位编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi") + @NotBlank(message = "岗位编码不能为空") + @Size(max = 64, message = "岗位编码长度不能超过64个字符") + private String code; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "备注", example = "快乐的备注") + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostCreateReqVO.java new file mode 100644 index 0000000..585d3b7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostCreateReqVO.java @@ -0,0 +1,11 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 岗位创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostCreateReqVO extends PostBaseVO { +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostExcelVO.java new file mode 100644 index 0000000..4a6acbf --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostExcelVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 岗位 Excel 导出响应 VO + */ +@Data +public class PostExcelVO { + + @ExcelProperty("岗位序号") + private Long id; + + @ExcelProperty("岗位编码") + private String code; + + @ExcelProperty("岗位名称") + private String name; + + @ExcelProperty("岗位排序") + private Integer sort; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private String status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostExportReqVO.java new file mode 100644 index 0000000..410e48e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostExportReqVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 岗位导出 Request VO,参数和 PostExcelVO 是一致的") +@Data +public class PostExportReqVO { + + @Schema(description = "岗位编码,模糊匹配", example = "yunxi") + private String code; + + @Schema(description = "岗位名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostListReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostListReqVO.java new file mode 100644 index 0000000..79b36aa --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostListReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 岗位列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostListReqVO extends PostBaseVO { + + @Schema(description = "岗位名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostPageReqVO.java new file mode 100644 index 0000000..6ef4a23 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostPageReqVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 岗位分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostPageReqVO extends PageParam { + + @Schema(description = "岗位编码,模糊匹配", example = "yunxi") + private String code; + + @Schema(description = "岗位名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostRespVO.java new file mode 100644 index 0000000..313d442 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 岗位信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostRespVO extends PostBaseVO { + + @Schema(description = "岗位序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java new file mode 100644 index 0000000..c332816 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 岗位精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PostSimpleRespVO { + + @Schema(description = "岗位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostUpdateReqVO.java new file mode 100644 index 0000000..2703250 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dept/vo/post/PostUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.controller.admin.dept.vo.post; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 岗位更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class PostUpdateReqVO extends PostBaseVO { + + @Schema(description = "岗位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "岗位编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictDataController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictDataController.http new file mode 100644 index 0000000..f524315 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictDataController.http @@ -0,0 +1,4 @@ +### 请求 /menu/list 接口 => 成功 +GET {{baseUrl}}/system/dict-data/list-all-simple +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictDataController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictDataController.java new file mode 100644 index 0000000..cfc55a4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictDataController.java @@ -0,0 +1,95 @@ +package com.yunxi.scm.module.system.controller.admin.dict; + +import com.yunxi.scm.module.system.dal.dataobject.dict.DictDataDO; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.*; +import com.yunxi.scm.module.system.convert.dict.DictDataConvert; +import com.yunxi.scm.module.system.service.dict.DictDataService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 字典数据") +@RestController +@RequestMapping("/system/dict-data") +@Validated +public class DictDataController { + + @Resource + private DictDataService dictDataService; + + @PostMapping("/create") + @Operation(summary = "新增字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:create')") + public CommonResult createDictData(@Valid @RequestBody DictDataCreateReqVO reqVO) { + Long dictDataId = dictDataService.createDictData(reqVO); + return success(dictDataId); + } + + @PutMapping("/update") + @Operation(summary = "修改字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:update')") + public CommonResult updateDictData(@Valid @RequestBody DictDataUpdateReqVO reqVO) { + dictDataService.updateDictData(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除字典数据") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dict:delete')") + public CommonResult deleteDictData(Long id) { + dictDataService.deleteDictData(id); + return success(true); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获得全部字典数据列表", description = "一般用于管理后台缓存字典数据在本地") + // 无需添加权限认证,因为前端全局都需要 + public CommonResult> getSimpleDictDataList() { + List list = dictDataService.getDictDataList(); + return success(DictDataConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/page") + @Operation(summary = "/获得字典类型的分页列表") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult> getDictTypePage(@Valid DictDataPageReqVO reqVO) { + return success(DictDataConvert.INSTANCE.convertPage(dictDataService.getDictDataPage(reqVO))); + } + + @GetMapping(value = "/get") + @Operation(summary = "/查询字典数据详细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult getDictData(@RequestParam("id") Long id) { + return success(DictDataConvert.INSTANCE.convert(dictDataService.getDictData(id))); + } + + @GetMapping("/export") + @Operation(summary = "导出字典数据") + @PreAuthorize("@ss.hasPermission('system:dict:export')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Valid DictDataExportReqVO reqVO) throws IOException { + List list = dictDataService.getDictDataList(reqVO); + List data = DictDataConvert.INSTANCE.convertList02(list); + // 输出 + ExcelUtils.write(response, "字典数据.xls", "数据列表", DictDataExcelVO.class, data); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictTypeController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictTypeController.java new file mode 100644 index 0000000..9b16c8d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/DictTypeController.java @@ -0,0 +1,95 @@ +package com.yunxi.scm.module.system.controller.admin.dict; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.*; +import com.yunxi.scm.module.system.convert.dict.DictTypeConvert; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictTypeDO; +import com.yunxi.scm.module.system.service.dict.DictTypeService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 字典类型") +@RestController +@RequestMapping("/system/dict-type") +@Validated +public class DictTypeController { + + @Resource + private DictTypeService dictTypeService; + + @PostMapping("/create") + @Operation(summary = "创建字典类型") + @PreAuthorize("@ss.hasPermission('system:dict:create')") + public CommonResult createDictType(@Valid @RequestBody DictTypeCreateReqVO reqVO) { + Long dictTypeId = dictTypeService.createDictType(reqVO); + return success(dictTypeId); + } + + @PutMapping("/update") + @Operation(summary = "修改字典类型") + @PreAuthorize("@ss.hasPermission('system:dict:update')") + public CommonResult updateDictType(@Valid @RequestBody DictTypeUpdateReqVO reqVO) { + dictTypeService.updateDictType(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除字典类型") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:dict:delete')") + public CommonResult deleteDictType(Long id) { + dictTypeService.deleteDictType(id); + return success(true); + } + + @Operation(summary = "/获得字典类型的分页列表") + @GetMapping("/page") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult> pageDictTypes(@Valid DictTypePageReqVO reqVO) { + return success(DictTypeConvert.INSTANCE.convertPage(dictTypeService.getDictTypePage(reqVO))); + } + + @Operation(summary = "/查询字典类型详细") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @GetMapping(value = "/get") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + public CommonResult getDictType(@RequestParam("id") Long id) { + return success(DictTypeConvert.INSTANCE.convert(dictTypeService.getDictType(id))); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获得全部字典类型列表", description = "包括开启 + 禁用的字典类型,主要用于前端的下拉选项") + // 无需添加权限认证,因为前端全局都需要 + public CommonResult> getSimpleDictTypeList() { + List list = dictTypeService.getDictTypeList(); + return success(DictTypeConvert.INSTANCE.convertList(list)); + } + + @Operation(summary = "导出数据类型") + @GetMapping("/export") + @PreAuthorize("@ss.hasPermission('system:dict:query')") + @OperateLog(type = EXPORT) + public void export(HttpServletResponse response, @Valid DictTypeExportReqVO reqVO) throws IOException { + List list = dictTypeService.getDictTypeList(reqVO); + List data = DictTypeConvert.INSTANCE.convertList02(list); + // 输出 + ExcelUtils.write(response, "字典类型.xls", "类型列表", DictTypeExcelVO.class, data); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataBaseVO.java new file mode 100644 index 0000000..6de516b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataBaseVO.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 字典数据 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class DictDataBaseVO { + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotBlank(message = "字典标签不能为空") + @Size(max = 100, message = "字典标签长度不能超过100个字符") + private String label; + + @Schema(description = "字典值", requiredMode = Schema.RequiredMode.REQUIRED, example = "iocoder") + @NotBlank(message = "字典键值不能为空") + @Size(max = 100, message = "字典键值长度不能超过100个字符") + private String value; + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") + @NotBlank(message = "字典类型不能为空") + @Size(max = 100, message = "字典类型长度不能超过100个字符") + private String dictType; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") +// @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + + @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") + private String colorType; + @Schema(description = "css 样式", example = "btn-visible") + private String cssClass; + + @Schema(description = "备注", example = "我是一个角色") + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataCreateReqVO.java new file mode 100644 index 0000000..67a73bc --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataCreateReqVO.java @@ -0,0 +1,12 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 字典数据创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataCreateReqVO extends DictDataBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataExcelVO.java new file mode 100644 index 0000000..aaa2758 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataExcelVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.data; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 字典数据 Excel 导出响应 VO + */ +@Data +public class DictDataExcelVO { + + @ExcelProperty("字典编码") + private Long id; + + @ExcelProperty("字典排序") + private Integer sort; + + @ExcelProperty("字典标签") + private String label; + + @ExcelProperty("字典键值") + private String value; + + @ExcelProperty("字典类型") + private String dictType; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataExportReqVO.java new file mode 100644 index 0000000..d796560 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataExportReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 字典类型导出 Request VO") +@Data +public class DictDataExportReqVO { + + @Schema(description = "字典标签", example = "芋道") + @Size(max = 100, message = "字典标签长度不能超过100个字符") + private String label; + + @Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String dictType; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java new file mode 100644 index 0000000..1b4a6c8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataPageReqVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.data; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 字典类型分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataPageReqVO extends PageParam { + + @Schema(description = "字典标签", example = "芋道") + @Size(max = 100, message = "字典标签长度不能超过100个字符") + private String label; + + @Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String dictType; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataRespVO.java new file mode 100644 index 0000000..73d0b49 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataRespVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 字典数据信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DictDataRespVO extends DictDataBaseVO { + + @Schema(description = "字典数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java new file mode 100644 index 0000000..489c171 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataSimpleRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 数据字典精简 Response VO") +@Data +public class DictDataSimpleRespVO { + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "gender") + private String dictType; + + @Schema(description = "字典键值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private String value; + + @Schema(description = "字典标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "男") + private String label; + + @Schema(description = "颜色类型,default、primary、success、info、warning、danger", example = "default") + private String colorType; + + @Schema(description = "css 样式", example = "btn-visible") + private String cssClass; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataUpdateReqVO.java new file mode 100644 index 0000000..7fb790f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/data/DictDataUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 字典数据更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataUpdateReqVO extends DictDataBaseVO { + + @Schema(description = "字典数据编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "字典数据编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeBaseVO.java new file mode 100644 index 0000000..a1b1eb2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeBaseVO.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 字典类型 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class DictTypeBaseVO { + + @Schema(description = "字典名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "性别") + @NotBlank(message = "字典名称不能为空") + @Size(max = 100, message = "字典类型名称长度不能超过100个字符") + private String name; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "快乐的备注") + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeCreateReqVO.java new file mode 100644 index 0000000..b9a7d16 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 字典类型创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictTypeCreateReqVO extends DictTypeBaseVO { + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") + @NotNull(message = "字典类型不能为空") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String type; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeExcelVO.java new file mode 100644 index 0000000..c7f556e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeExcelVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.type; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 字典类型 Excel 导出响应 VO + */ +@Data +public class DictTypeExcelVO { + + @ExcelProperty("字典主键") + private Long id; + + @ExcelProperty("字典名称") + private String name; + + @ExcelProperty("字典类型") + private String type; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeExportReqVO.java new file mode 100644 index 0000000..2a735de --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeExportReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 字典类型分页列表 Request VO") +@Data +public class DictTypeExportReqVO { + + @Schema(description = "字典类型名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") + private String type; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java new file mode 100644 index 0000000..eea378f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypePageReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.type; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.Size; +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 字典类型分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictTypePageReqVO extends PageParam { + + @Schema(description = "字典类型名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "字典类型,模糊匹配", example = "sys_common_sex") + @Size(max = 100, message = "字典类型类型长度不能超过100个字符") + private String type; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java new file mode 100644 index 0000000..cc728a5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 字典类型信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class DictTypeRespVO extends DictTypeBaseVO { + + @Schema(description = "字典类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") + private String type; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java new file mode 100644 index 0000000..c334616 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeSimpleRespVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 字典类型精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DictTypeSimpleRespVO { + + @Schema(description = "字典类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "字典类型名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "字典类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "sys_common_sex") + private String type; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeUpdateReqVO.java new file mode 100644 index 0000000..bdb0ab2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/dict/vo/type/DictTypeUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.controller.admin.dict.vo.type; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 字典类型更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class DictTypeUpdateReqVO extends DictTypeBaseVO { + + @Schema(description = "字典类型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "字典类型编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/ErrorCodeController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/ErrorCodeController.http new file mode 100644 index 0000000..06b8723 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/ErrorCodeController.http @@ -0,0 +1,13 @@ +### 创建错误码 +POST {{baseUrl}}/inra/error-code/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "code": 200, + "message": "成功", + "group": "test", + "type": 1 +} + diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/ErrorCodeController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/ErrorCodeController.java new file mode 100644 index 0000000..a5fc973 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/ErrorCodeController.java @@ -0,0 +1,89 @@ +package com.yunxi.scm.module.system.controller.admin.errorcode; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.convert.errorcode.ErrorCodeConvert; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.*; +import com.yunxi.scm.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import com.yunxi.scm.module.system.service.errorcode.ErrorCodeService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 错误码") +@RestController +@RequestMapping("/system/error-code") +@Validated +public class ErrorCodeController { + + @Resource + private ErrorCodeService errorCodeService; + + @PostMapping("/create") + @Operation(summary = "创建错误码") + @PreAuthorize("@ss.hasPermission('system:error-code:create')") + public CommonResult createErrorCode(@Valid @RequestBody ErrorCodeCreateReqVO createReqVO) { + return success(errorCodeService.createErrorCode(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新错误码") + @PreAuthorize("@ss.hasPermission('system:error-code:update')") + public CommonResult updateErrorCode(@Valid @RequestBody ErrorCodeUpdateReqVO updateReqVO) { + errorCodeService.updateErrorCode(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除错误码") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:error-code:delete')") + public CommonResult deleteErrorCode(@RequestParam("id") Long id) { + errorCodeService.deleteErrorCode(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得错误码") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:error-code:query')") + public CommonResult getErrorCode(@RequestParam("id") Long id) { + ErrorCodeDO errorCode = errorCodeService.getErrorCode(id); + return success(ErrorCodeConvert.INSTANCE.convert(errorCode)); + } + + @GetMapping("/page") + @Operation(summary = "获得错误码分页") + @PreAuthorize("@ss.hasPermission('system:error-code:query')") + public CommonResult> getErrorCodePage(@Valid ErrorCodePageReqVO pageVO) { + PageResult pageResult = errorCodeService.getErrorCodePage(pageVO); + return success(ErrorCodeConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出错误码 Excel") + @PreAuthorize("@ss.hasPermission('system:error-code:export')") + @OperateLog(type = EXPORT) + public void exportErrorCodeExcel(@Valid ErrorCodeExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = errorCodeService.getErrorCodeList(exportReqVO); + // 导出 Excel + List datas = ErrorCodeConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "错误码.xls", "数据", ErrorCodeExcelVO.class, datas); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeBaseVO.java new file mode 100644 index 0000000..fd59bfc --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeBaseVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.controller.admin.errorcode.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 错误码 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class ErrorCodeBaseVO { + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "dashboard") + @NotNull(message = "应用名不能为空") + private String applicationName; + + @Schema(description = "错误码编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1234") + @NotNull(message = "错误码编码不能为空") + private Integer code; + + @Schema(description = "错误码错误提示", requiredMode = Schema.RequiredMode.REQUIRED, example = "帅气") + @NotNull(message = "错误码错误提示不能为空") + private String message; + + @Schema(description = "备注", example = "哈哈哈") + private String memo; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeCreateReqVO.java new file mode 100644 index 0000000..b6ce258 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.system.controller.admin.errorcode.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 错误码创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeCreateReqVO extends ErrorCodeBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeExcelVO.java new file mode 100644 index 0000000..4ee0189 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeExcelVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.controller.admin.errorcode.vo; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 错误码 Excel VO + * + * @author 芋道源码 + */ +@Data +public class ErrorCodeExcelVO { + + @ExcelProperty("错误码编号") + private Long id; + + @ExcelProperty(value = "错误码类型", converter = DictConvert.class) + @DictFormat("inf_error_code_type") // TODO 芋艿:得思考下杂解决枚举值 + private Integer type; + + @ExcelProperty("应用名") + private String applicationName; + + @ExcelProperty("错误码编码") + private Integer code; + + @ExcelProperty("错误码错误提示") + private String message; + + @ExcelProperty("备注") + private String memo; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeExportReqVO.java new file mode 100644 index 0000000..92eed63 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeExportReqVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.errorcode.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 错误码 Excel 导出 Request VO,参数和 InfErrorCodePageReqVO 是一致的") +@Data +public class ErrorCodeExportReqVO { + + @Schema(description = "错误码类型", example = "1") + private Integer type; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "错误码编码", example = "1234") + private Integer code; + + @Schema(description = "错误码错误提示", example = "帅气") + private String message; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java new file mode 100644 index 0000000..ddb16df --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodePageReqVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.system.controller.admin.errorcode.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 错误码分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodePageReqVO extends PageParam { + + @Schema(description = "错误码类型,参见 ErrorCodeTypeEnum 枚举类", example = "1") + private Integer type; + + @Schema(description = "应用名", example = "dashboard") + private String applicationName; + + @Schema(description = "错误码编码", example = "1234") + private Integer code; + + @Schema(description = "错误码错误提示", example = "帅气") + private String message; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java new file mode 100644 index 0000000..a8557ad --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.controller.admin.errorcode.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 错误码 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeRespVO extends ErrorCodeBaseVO { + + @Schema(description = "错误码编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "错误码类型,参见 ErrorCodeTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeUpdateReqVO.java new file mode 100644 index 0000000..acc763c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/errorcode/vo/ErrorCodeUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.errorcode.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 错误码更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeUpdateReqVO extends ErrorCodeBaseVO { + + @Schema(description = "错误码编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "错误码编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/AreaController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/AreaController.http new file mode 100644 index 0000000..f1b893d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/AreaController.http @@ -0,0 +1,5 @@ +### 获得地区树 +GET {{baseUrl}}/system/area/tree +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/AreaController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/AreaController.java new file mode 100644 index 0000000..ad1d32c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/AreaController.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.module.system.controller.admin.ip; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.ip.core.Area; +import com.yunxi.scm.framework.ip.core.utils.AreaUtils; +import com.yunxi.scm.framework.ip.core.utils.IPUtils; +import com.yunxi.scm.module.system.controller.admin.ip.vo.AreaNodeRespVO; +import com.yunxi.scm.module.system.controller.admin.ip.vo.AreaNodeSimpleRespVO; +import com.yunxi.scm.module.system.convert.ip.AreaConvert; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 地区") +@RestController +@RequestMapping("/system/area") +@Validated +public class AreaController { + + @GetMapping("/tree") + @Operation(summary = "获得地区树") + public CommonResult> getAreaTree() { + Area area = AreaUtils.getArea(Area.ID_CHINA); + Assert.notNull(area, "获取不到中国"); + return success(AreaConvert.INSTANCE.convertList(area.getChildren())); + } + + @GetMapping("/get-children") + @Operation(summary = "获得地区的下级区域") + @Parameter(name = "id", description = "区域编号", required = true, example = "150000") + public CommonResult> getChildren(@RequestParam("id") Integer id) { + Area area = AreaUtils.getArea(id); + Assert.notNull(area, String.format("获取不到 id : %d 的区域", id)); + return success(AreaConvert.INSTANCE.convertList2(area.getChildren())); + } + + // 4)方法改成 getAreaChildrenList 获得子节点们;5)url 可以已改成 children-list + //@芋艿 是不是叫 getAreaListByIds 更合适。 因为不一定是子节点。 用于前端树选择获取缓存数据。 见 + @GetMapping("/get-by-ids") + @Operation(summary = "通过区域 ids 获得地区列表") + @Parameter(name = "ids", description = "区域编号 ids", required = true, example = "1,150000") + public CommonResult> getAreaListByIds(@RequestParam("ids") Set ids) { + List areaList = new ArrayList<>(ids.size()); + for (Integer areaId : ids) { + areaList.add(AreaUtils.getArea(areaId)); + } + return success(AreaConvert.INSTANCE.convertList2(areaList)); + } + + @GetMapping("/get-by-ip") + @Operation(summary = "获得 IP 对应的地区名") + @Parameter(name = "ip", description = "IP", required = true) + public CommonResult getAreaByIp(@RequestParam("ip") String ip) { + // 获得城市 + Area area = IPUtils.getArea(ip); + if (area == null) { + return success("未知"); + } + // 格式化返回 + return success(AreaUtils.format(area.getId())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/vo/AreaNodeRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/vo/AreaNodeRespVO.java new file mode 100644 index 0000000..95ff98a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/vo/AreaNodeRespVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.ip.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "管理后台 - 地区节点 Response VO") +@Data +public class AreaNodeRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") + private Integer id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "北京") + private String name; + + /** + * 子节点 + */ + private List children; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/vo/AreaNodeSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/vo/AreaNodeSimpleRespVO.java new file mode 100644 index 0000000..d38e684 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/ip/vo/AreaNodeSimpleRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.system.controller.admin.ip.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 简洁的地区节点 Response VO") +@Data +public class AreaNodeSimpleRespVO { + + @Schema(description = "编号", required = true, example = "110000") + private Integer id; + + @Schema(description = "名字", required = true, example = "北京") + private String name; + + @Schema(description = "是否叶子节点", required = false, example = "false") + private Boolean leaf; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/LoginLogController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/LoginLogController.java new file mode 100644 index 0000000..61cc34e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/LoginLogController.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.system.controller.admin.logger; + +import com.yunxi.scm.module.system.dal.dataobject.logger.LoginLogDO; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogExcelVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogRespVO; +import com.yunxi.scm.module.system.convert.logger.LoginLogConvert; +import com.yunxi.scm.module.system.service.logger.LoginLogService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 登录日志") +@RestController +@RequestMapping("/system/login-log") +@Validated +public class LoginLogController { + + @Resource + private LoginLogService loginLogService; + + @GetMapping("/page") + @Operation(summary = "获得登录日志分页列表") + @PreAuthorize("@ss.hasPermission('system:login-log:query')") + public CommonResult> getLoginLogPage(@Valid LoginLogPageReqVO reqVO) { + PageResult page = loginLogService.getLoginLogPage(reqVO); + return CommonResult.success(LoginLogConvert.INSTANCE.convertPage(page)); + } + + @GetMapping("/export") + @Operation(summary = "导出登录日志 Excel") + @PreAuthorize("@ss.hasPermission('system:login-log:export')") + @OperateLog(type = EXPORT) + public void exportLoginLog(HttpServletResponse response, @Valid LoginLogExportReqVO reqVO) throws IOException { + List list = loginLogService.getLoginLogList(reqVO); + // 拼接数据 + List data = LoginLogConvert.INSTANCE.convertList(list); + // 输出 + ExcelUtils.write(response, "登录日志.xls", "数据列表", LoginLogExcelVO.class, data); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/OperateLogController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/OperateLogController.http new file mode 100644 index 0000000..f667482 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/OperateLogController.http @@ -0,0 +1,4 @@ +### 请求 /system/operate-log/demo 接口 => 成功 +GET {{baseUrl}}/system/operate-log/demo +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/OperateLogController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/OperateLogController.java new file mode 100644 index 0000000..b4a9527 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/OperateLogController.java @@ -0,0 +1,85 @@ +package com.yunxi.scm.module.system.controller.admin.logger; + +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogExcelVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; +import com.yunxi.scm.module.system.convert.logger.OperateLogConvert; +import com.yunxi.scm.module.system.dal.dataobject.logger.OperateLogDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.service.logger.OperateLogService; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 操作日志") +@RestController +@RequestMapping("/system/operate-log") +@Validated +public class OperateLogController { + + @Resource + private OperateLogService operateLogService; + @Resource + private AdminUserService userService; + + @GetMapping("/page") + @Operation(summary = "查看操作日志分页列表") + @PreAuthorize("@ss.hasPermission('system:operate-log:query')") + public CommonResult> pageOperateLog(@Valid OperateLogPageReqVO reqVO) { + PageResult pageResult = operateLogService.getOperateLogPage(reqVO); + + // 获得拼接需要的数据 + Collection userIds = CollectionUtils.convertList(pageResult.getList(), OperateLogDO::getUserId); + Map userMap = userService.getUserMap(userIds); + // 拼接数据 + List list = new ArrayList<>(pageResult.getList().size()); + pageResult.getList().forEach(operateLog -> { + OperateLogRespVO respVO = OperateLogConvert.INSTANCE.convert(operateLog); + list.add(respVO); + // 拼接用户信息 + MapUtils.findAndThen(userMap, operateLog.getUserId(), user -> respVO.setUserNickname(user.getNickname())); + }); + return success(new PageResult<>(list, pageResult.getTotal())); + } + + @Operation(summary = "导出操作日志") + @GetMapping("/export") + @PreAuthorize("@ss.hasPermission('system:operate-log:export')") + @OperateLog(type = EXPORT) + public void exportOperateLog(HttpServletResponse response, @Valid OperateLogExportReqVO reqVO) throws IOException { + List list = operateLogService.getOperateLogList(reqVO); + + // 获得拼接需要的数据 + Collection userIds = CollectionUtils.convertList(list, OperateLogDO::getUserId); + Map userMap = userService.getUserMap(userIds); + // 拼接数据 + List excelDataList = OperateLogConvert.INSTANCE.convertList(list, userMap); + // 输出 + ExcelUtils.write(response, "操作日志.xls", "数据列表", OperateLogExcelVO.class, excelDataList); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogBaseVO.java new file mode 100644 index 0000000..b8b12d1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogBaseVO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 登录日志 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class LoginLogBaseVO { + + @Schema(description = "日志类型,参见 LoginLogTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "日志类型不能为空") + private Integer logType; + + @Schema(description = "链路追踪编号", example = "89aca178-a370-411c-ae02-3f0d672be4ab") + @NotEmpty(message = "链路追踪编号不能为空") + private String traceId; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi") + @NotBlank(message = "用户账号不能为空") + @Size(max = 30, message = "用户账号长度不能超过30个字符") + private String username; + + @Schema(description = "登录结果,参见 LoginResultEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "登录结果不能为空") + private Integer result; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + @Schema(description = "浏览器 UserAgent", example = "Mozilla/5.0") + private String userAgent; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogExcelVO.java new file mode 100644 index 0000000..fee1d50 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogExcelVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 登录日志 Excel 导出响应 VO + */ +@Data +public class LoginLogExcelVO { + + @ExcelProperty("日志主键") + private Long id; + + @ExcelProperty("用户账号") + private String username; + + @ExcelProperty(value = "日志类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.LOGIN_TYPE) + private Integer logType; + + @ExcelProperty(value = "登录结果", converter = DictConvert.class) + @DictFormat(DictTypeConstants.LOGIN_RESULT) + private Integer result; + + @ExcelProperty("登录 IP") + private String userIp; + + @ExcelProperty("浏览器 UA") + private String userAgent; + + @ExcelProperty("登录时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogExportReqVO.java new file mode 100644 index 0000000..bb32eb4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogExportReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 登录日志分页列表 Request VO") +@Data +public class LoginLogExportReqVO { + + @Schema(description = "用户 IP,模拟匹配", example = "127.0.0.1") + private String userIp; + + @Schema(description = "用户账号,模拟匹配", example = "芋道") + private String username; + + @Schema(description = "操作状态", example = "true") + private Boolean status; + + @Schema(description = "登录时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java new file mode 100644 index 0000000..a5b0bd4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogPageReqVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 登录日志分页列表 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class LoginLogPageReqVO extends PageParam { + + @Schema(description = "用户 IP,模拟匹配", example = "127.0.0.1") + private String userIp; + + @Schema(description = "用户账号,模拟匹配", example = "芋道") + private String username; + + @Schema(description = "操作状态", example = "true") + private Boolean status; + + @Schema(description = "登录时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java new file mode 100644 index 0000000..d3dabe0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 登录日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class LoginLogRespVO extends LoginLogBaseVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户编号", example = "666") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "用户类型不能为空") + private Integer userType; + + @Schema(description = "登录时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogBaseVO.java new file mode 100644 index 0000000..51149e2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogBaseVO.java @@ -0,0 +1,85 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 操作日志 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class OperateLogBaseVO { + + @Schema(description = "链路追踪编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "89aca178-a370-411c-ae02-3f0d672be4ab") + @NotEmpty(message = "链路追踪编号不能为空") + private String traceId; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "操作模块", requiredMode = Schema.RequiredMode.REQUIRED, example = "订单") + @NotEmpty(message = "操作模块不能为空") + private String module; + + @Schema(description = "操作名", requiredMode = Schema.RequiredMode.REQUIRED, example = "创建订单") + @NotEmpty(message = "操作名") + private String name; + + @Schema(description = "操作分类,参见 OperateLogTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "操作分类不能为空") + private Integer type; + + @Schema(description = "操作明细", example = "修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。") + private String content; + + @Schema(description = "拓展字段", example = "{'orderId': 1}") + private Map exts; + + @Schema(description = "请求方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "GET") + @NotEmpty(message = "请求方法名不能为空") + private String requestMethod; + + @Schema(description = "请求地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "/xxx/yyy") + @NotEmpty(message = "请求地址不能为空") + private String requestUrl; + + @Schema(description = "用户 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "127.0.0.1") + @NotEmpty(message = "用户 IP 不能为空") + private String userIp; + + @Schema(description = "浏览器 UserAgent", requiredMode = Schema.RequiredMode.REQUIRED, example = "Mozilla/5.0") + @NotEmpty(message = "浏览器 UserAgent 不能为空") + private String userAgent; + + @Schema(description = "Java 方法名", requiredMode = Schema.RequiredMode.REQUIRED, example = "com.yunxi.scm.adminserver.UserController.save(...)") + @NotEmpty(message = "Java 方法名不能为空") + private String javaMethod; + + @Schema(description = "Java 方法的参数") + private String javaMethodArgs; + + @Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "开始时间不能为空") + private LocalDateTime startTime; + + @Schema(description = "执行时长,单位:毫秒", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "执行时长不能为空") + private Integer duration; + + @Schema(description = "结果码", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "结果码不能为空") + private Integer resultCode; + + @Schema(description = "结果提示") + private String resultMsg; + + @Schema(description = "结果数据") + private String resultData; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogExcelVO.java new file mode 100644 index 0000000..bd693e5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogExcelVO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 操作日志 Excel 导出响应 VO + */ +@Data +public class OperateLogExcelVO { + + @ExcelProperty("日志编号") + private Long id; + + @ExcelProperty("操作模块") + private String module; + + @ExcelProperty("操作名") + private String name; + + @ExcelProperty(value = "操作类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.OPERATE_TYPE) + private String type; + + @ExcelProperty("操作人") + private String userNickname; + + @ExcelProperty(value = "操作结果") // 成功 or 失败 + private String successStr; + + @ExcelProperty("操作日志") + private LocalDateTime startTime; + + @ExcelProperty("执行时长") + private Integer duration; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogExportReqVO.java new file mode 100644 index 0000000..f82e60e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogExportReqVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 操作日志分页列表 Request VO") +@Data +public class OperateLogExportReqVO { + + @Schema(description = "操作模块,模拟匹配", example = "订单") + private String module; + + @Schema(description = "用户昵称,模拟匹配", example = "芋道") + private String userNickname; + + @Schema(description = "操作分类,参见 OperateLogTypeEnum 枚举类", example = "1") + private Integer type; + + @Schema(description = "操作状态", example = "true") + private Boolean success; + + @Schema(description = "开始时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] startTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java new file mode 100644 index 0000000..6cfc902 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogPageReqVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 操作日志分页列表 Request VO") +@Data +public class OperateLogPageReqVO extends PageParam { + + @Schema(description = "操作模块,模拟匹配", example = "订单") + private String module; + + @Schema(description = "用户昵称,模拟匹配", example = "芋道") + private String userNickname; + + @Schema(description = "操作分类,参见 OperateLogTypeEnum 枚举类", example = "1") + private Integer type; + + @Schema(description = "操作状态", example = "true") + private Boolean success; + + @Schema(description = "开始时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] startTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java new file mode 100644 index 0000000..2a06c67 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 操作日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OperateLogRespVO extends OperateLogBaseVO { + + @Schema(description = "日志编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String userNickname; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailAccountController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailAccountController.java new file mode 100644 index 0000000..9a72a64 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailAccountController.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.system.controller.admin.mail; + + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.*; +import com.yunxi.scm.module.system.convert.mail.MailAccountConvert; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import com.yunxi.scm.module.system.service.mail.MailAccountService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 邮箱账号") +@RestController +@RequestMapping("/system/mail-account") +public class MailAccountController { + + @Resource + private MailAccountService mailAccountService; + + @PostMapping("/create") + @Operation(summary = "创建邮箱账号") + @PreAuthorize("@ss.hasPermission('system:mail-account:create')") + public CommonResult createMailAccount(@Valid @RequestBody MailAccountCreateReqVO createReqVO) { + return success(mailAccountService.createMailAccount(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改邮箱账号") + @PreAuthorize("@ss.hasPermission('system:mail-account:update')") + public CommonResult updateMailAccount(@Valid @RequestBody MailAccountUpdateReqVO updateReqVO) { + mailAccountService.updateMailAccount(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除邮箱账号") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:mail-account:delete')") + public CommonResult deleteMailAccount(@RequestParam Long id) { + mailAccountService.deleteMailAccount(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得邮箱账号") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:mail-account:get')") + public CommonResult getMailAccount(@RequestParam("id") Long id) { + MailAccountDO mailAccountDO = mailAccountService.getMailAccount(id); + return success(MailAccountConvert.INSTANCE.convert(mailAccountDO)); + } + + @GetMapping("/page") + @Operation(summary = "获得邮箱账号分页") + @PreAuthorize("@ss.hasPermission('system:mail-account:query')") + public CommonResult> getMailAccountPage(@Valid MailAccountPageReqVO pageReqVO) { + PageResult pageResult = mailAccountService.getMailAccountPage(pageReqVO); + return success(MailAccountConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获得邮箱账号精简列表") + public CommonResult> getSimpleMailAccountList() { + List list = mailAccountService.getMailAccountList(); + return success(MailAccountConvert.INSTANCE.convertList02(list)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailLogController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailLogController.java new file mode 100644 index 0000000..ab4e23a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailLogController.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.system.controller.admin.mail; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.log.MailLogRespVO; +import com.yunxi.scm.module.system.convert.mail.MailLogConvert; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailLogDO; +import com.yunxi.scm.module.system.service.mail.MailLogService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 邮件日志") +@RestController +@RequestMapping("/system/mail-log") +public class MailLogController { + + @Resource + private MailLogService mailLogService; + + @GetMapping("/page") + @Operation(summary = "获得邮箱日志分页") + @PreAuthorize("@ss.hasPermission('system:mail-log:query')") + public CommonResult> getMailLogPage(@Valid MailLogPageReqVO pageVO) { + PageResult pageResult = mailLogService.getMailLogPage(pageVO); + return success(MailLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/get") + @Operation(summary = "获得邮箱日志") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:mail-log:query')") + public CommonResult getMailTemplate(@RequestParam("id") Long id) { + MailLogDO mailLogDO = mailLogService.getMailLog(id); + return success(MailLogConvert.INSTANCE.convert(mailLogDO)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailTemplateController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailTemplateController.http new file mode 100644 index 0000000..f3c47f5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailTemplateController.http @@ -0,0 +1,14 @@ +### 请求 /system/mail-template/send-mail 接口 => 成功 +POST {{baseUrl}}/system/mail-template/send-mail +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "templateCode": "test_01", + "mail": "7685413@qq.com", + "templateParams": { + "key01": "value01", + "key02": "value02" + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailTemplateController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailTemplateController.java new file mode 100644 index 0000000..dcc5147 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/MailTemplateController.java @@ -0,0 +1,89 @@ +package com.yunxi.scm.module.system.controller.admin.mail; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.*; +import com.yunxi.scm.module.system.convert.mail.MailTemplateConvert; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.yunxi.scm.module.system.service.mail.MailSendService; +import com.yunxi.scm.module.system.service.mail.MailTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 邮件模版") +@RestController +@RequestMapping("/system/mail-template") +public class MailTemplateController { + + @Resource + private MailTemplateService mailTempleService; + @Resource + private MailSendService mailSendService; + + @PostMapping("/create") + @Operation(summary = "创建邮件模版") + @PreAuthorize("@ss.hasPermission('system:mail-template:create')") + public CommonResult createMailTemplate(@Valid @RequestBody MailTemplateCreateReqVO createReqVO){ + return success(mailTempleService.createMailTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改邮件模版") + @PreAuthorize("@ss.hasPermission('system:mail-template:update')") + public CommonResult updateMailTemplate(@Valid @RequestBody MailTemplateUpdateReqVO updateReqVO){ + mailTempleService.updateMailTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除邮件模版") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:mail-template:delete')") + public CommonResult deleteMailTemplate(@RequestParam("id") Long id) { + mailTempleService.deleteMailTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得邮件模版") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:mail-template:get')") + public CommonResult getMailTemplate(@RequestParam("id") Long id) { + MailTemplateDO mailTemplateDO = mailTempleService.getMailTemplate(id); + return success(MailTemplateConvert.INSTANCE.convert(mailTemplateDO)); + } + + @GetMapping("/page") + @Operation(summary = "获得邮件模版分页") + @PreAuthorize("@ss.hasPermission('system:mail-template:query')") + public CommonResult> getMailTemplatePage(@Valid MailTemplatePageReqVO pageReqVO) { + PageResult pageResult = mailTempleService.getMailTemplatePage(pageReqVO); + return success(MailTemplateConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获得邮件模版精简列表") + public CommonResult> getSimpleTemplateList() { + List list = mailTempleService.getMailTemplateList(); + return success(MailTemplateConvert.INSTANCE.convertList02(list)); + } + + @PostMapping("/send-mail") + @Operation(summary = "发送短信") + @PreAuthorize("@ss.hasPermission('system:mail-template:send-mail')") + public CommonResult sendMail(@Valid @RequestBody MailTemplateSendReqVO sendReqVO) { + return success(mailSendService.sendSingleMailToAdmin(sendReqVO.getMail(), getLoginUserId(), + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountBaseVO.java new file mode 100644 index 0000000..24120bf --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountBaseVO.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotNull; + +/** + * 邮箱账号 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MailAccountBaseVO { + + @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxiyuanma@123.com") + @NotNull(message = "邮箱不能为空") + @Email(message = "必须是 Email 格式") + private String mail; + + @Schema(description = "用户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi") + @NotNull(message = "用户名不能为空") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotNull(message = "密码必填") + private String password; + + @Schema(description = "SMTP 服务器域名", requiredMode = Schema.RequiredMode.REQUIRED, example = "www.iocoder.cn") + @NotNull(message = "SMTP 服务器域名不能为空") + private String host; + + @Schema(description = "SMTP 服务器端口", requiredMode = Schema.RequiredMode.REQUIRED, example = "80") + @NotNull(message = "SMTP 服务器端口不能为空") + private Integer port; + + @Schema(description = "是否开启 ssl", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否开启 ssl 必填") + private Boolean sslEnable; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountCreateReqVO.java new file mode 100644 index 0000000..3a609d9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 邮箱账号创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailAccountCreateReqVO extends MailAccountBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountPageReqVO.java new file mode 100644 index 0000000..2031532 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountPageReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.account; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 邮箱账号分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailAccountPageReqVO extends PageParam { + + @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxiyuanma@123.com") + private String mail; + + @Schema(description = "用户名" , requiredMode = Schema.RequiredMode.REQUIRED , example = "yunxi") + private String username; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountRespVO.java new file mode 100644 index 0000000..9696d68 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountRespVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 邮箱账号 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailAccountRespVO extends MailAccountBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountSimpleRespVO.java new file mode 100644 index 0000000..5b216df --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountSimpleRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 邮箱账号的精简 Response VO") +@Data +public class MailAccountSimpleRespVO { + + @Schema(description = "邮箱编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "768541388@qq.com") + private String mail; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountUpdateReqVO.java new file mode 100644 index 0000000..c9c5a51 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/account/MailAccountUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.account; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 邮箱账号修改 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailAccountUpdateReqVO extends MailAccountBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogBaseVO.java new file mode 100644 index 0000000..afc39ee --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogBaseVO.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 邮件日志 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MailLogBaseVO { + + @Schema(description = "用户编号", example = "30883") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") + private Byte userType; + + @Schema(description = "接收邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "76854@qq.com") + @NotNull(message = "接收邮箱地址不能为空") + private String toMail; + + @Schema(description = "邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "18107") + @NotNull(message = "邮箱账号编号不能为空") + private Long accountId; + + @Schema(description = "发送邮箱地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "85757@qq.com") + @NotNull(message = "发送邮箱地址不能为空") + private String fromMail; + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "5678") + @NotNull(message = "模板编号不能为空") + private Long templateId; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String templateCode; + + @Schema(description = "模版发送人名称", example = "李四") + private String templateNickname; + + @Schema(description = "邮件标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试标题") + @NotNull(message = "邮件标题不能为空") + private String templateTitle; + + @Schema(description = "邮件内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容") + @NotNull(message = "邮件内容不能为空") + private String templateContent; + + @Schema(description = "邮件参数", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "邮件参数不能为空") + private Map templateParams; + + @Schema(description = "发送状态,参见 MailSendStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "发送状态不能为空") + private Byte sendStatus; + + @Schema(description = "发送时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime sendTime; + + @Schema(description = "发送返回的消息 ID", example = "28568") + private String sendMessageId; + + @Schema(description = "发送异常") + private String sendException; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogPageReqVO.java new file mode 100644 index 0000000..fc2a78f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogPageReqVO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.log; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 邮箱日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailLogPageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "30883") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", example = "2") + private Integer userType; + + @Schema(description = "接收邮箱地址,模糊匹配", example = "76854@qq.com") + private String toMail; + + @Schema(description = "邮箱账号编号", example = "18107") + private Long accountId; + + @Schema(description = "模板编号", example = "5678") + private Long templateId; + + @Schema(description = "发送状态,参见 MailSendStatusEnum 枚举", example = "1") + private Integer sendStatus; + + @Schema(description = "发送时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] sendTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogRespVO.java new file mode 100644 index 0000000..df6ed1a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/log/MailLogRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 邮件日志 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailLogRespVO extends MailLogBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31020") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateBaseVO.java new file mode 100644 index 0000000..4c3a167 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateBaseVO.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 邮件模版 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MailTemplateBaseVO { + + @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试名字") + @NotNull(message = "名称不能为空") + private String name; + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "test") + @NotNull(message = "模版编号不能为空") + private String code; + + @Schema(description = "发送的邮箱账号编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "发送的邮箱账号编号不能为空") + private Long accountId; + + @Schema(description = "发送人名称", example = "芋头") + private String nickname; + + @Schema(description = "标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "注册成功") + @NotEmpty(message = "标题不能为空") + private String title; + + @Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,注册成功啦") + @NotEmpty(message = "内容不能为空") + private String content; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "奥特曼") + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateCreateReqVO.java new file mode 100644 index 0000000..ba27a0d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 邮件模版创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailTemplateCreateReqVO extends MailTemplateBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplatePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplatePageReqVO.java new file mode 100644 index 0000000..620b3c7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplatePageReqVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.template; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 邮件模版分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailTemplatePageReqVO extends PageParam { + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", example = "1") + private Integer status; + + @Schema(description = "标识,模糊匹配", example = "code_1024") + private String code; + + @Schema(description = "名称,模糊匹配", example = "芋头") + private String name; + + @Schema(description = "账号编号", example = "2048") + private Long accountId; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateRespVO.java new file mode 100644 index 0000000..2b88de1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateRespVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 邮件末班 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailTemplateRespVO extends MailTemplateBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "参数数组", example = "name,code") + private List params; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java new file mode 100644 index 0000000..844f19c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateSendReqVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 邮件发送 Req VO") +@Data +public class MailTemplateSendReqVO { + + @Schema(description = "接收邮箱", requiredMode = Schema.RequiredMode.REQUIRED, example = "7685413@qq.com") + @NotEmpty(message = "接收邮箱不能为空") + private String mail; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String templateCode; + + @Schema(description = "模板参数") + private Map templateParams; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java new file mode 100644 index 0000000..036014c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateSimpleRespVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 邮件模版的精简 Response VO") +@Data +public class MailTemplateSimpleRespVO { + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "模版名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "哒哒哒") + private String name; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateUpdateReqVO.java new file mode 100644 index 0000000..c41ff1b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/mail/vo/template/MailTemplateUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.mail.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 邮件模版修改 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class MailTemplateUpdateReqVO extends MailTemplateBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/NoticeController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/NoticeController.java new file mode 100644 index 0000000..225ac93 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/NoticeController.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.module.system.controller.admin.notice; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeRespVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import com.yunxi.scm.module.system.convert.notice.NoticeConvert; +import com.yunxi.scm.module.system.service.notice.NoticeService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 通知公告") +@RestController +@RequestMapping("/system/notice") +@Validated +public class NoticeController { + + @Resource + private NoticeService noticeService; + + @PostMapping("/create") + @Operation(summary = "创建通知公告") + @PreAuthorize("@ss.hasPermission('system:notice:create')") + public CommonResult createNotice(@Valid @RequestBody NoticeCreateReqVO reqVO) { + Long noticeId = noticeService.createNotice(reqVO); + return success(noticeId); + } + + @PutMapping("/update") + @Operation(summary = "修改通知公告") + @PreAuthorize("@ss.hasPermission('system:notice:update')") + public CommonResult updateNotice(@Valid @RequestBody NoticeUpdateReqVO reqVO) { + noticeService.updateNotice(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除通知公告") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notice:delete')") + public CommonResult deleteNotice(@RequestParam("id") Long id) { + noticeService.deleteNotice(id); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获取通知公告列表") + @PreAuthorize("@ss.hasPermission('system:notice:query')") + public CommonResult> getNoticePage(@Validated NoticePageReqVO reqVO) { + return success(NoticeConvert.INSTANCE.convertPage(noticeService.getNoticePage(reqVO))); + } + + @GetMapping("/get") + @Operation(summary = "获得通知公告") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notice:query')") + public CommonResult getNotice(@RequestParam("id") Long id) { + return success(NoticeConvert.INSTANCE.convert(noticeService.getNotice(id))); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeBaseVO.java new file mode 100644 index 0000000..41b07c2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeBaseVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.system.controller.admin.notice.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 通知公告 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class NoticeBaseVO { + + @Schema(description = "公告标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") + @NotBlank(message = "公告标题不能为空") + @Size(max = 50, message = "公告标题不能超过50个字符") + private String title; + + @Schema(description = "公告类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "小博主") + @NotNull(message = "公告类型不能为空") + private Integer type; + + @Schema(description = "公告内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "半生编码") + private String content; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeCreateReqVO.java new file mode 100644 index 0000000..82f20a0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeCreateReqVO.java @@ -0,0 +1,11 @@ +package com.yunxi.scm.module.system.controller.admin.notice.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 通知公告创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeCreateReqVO extends NoticeBaseVO { +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticePageReqVO.java new file mode 100644 index 0000000..c7fc33b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticePageReqVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.system.controller.admin.notice.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 通知公告分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticePageReqVO extends PageParam { + + @Schema(description = "通知公告名称,模糊匹配", example = "芋道") + private String title; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeRespVO.java new file mode 100644 index 0000000..0847c9f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.notice.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 通知公告信息 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeRespVO extends NoticeBaseVO { + + @Schema(description = "通知公告序号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeUpdateReqVO.java new file mode 100644 index 0000000..6b81f48 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notice/vo/NoticeUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.controller.admin.notice.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 岗位公告更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeUpdateReqVO extends NoticeBaseVO { + + @Schema(description = "岗位公告编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "岗位公告编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/NotifyMessageController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/NotifyMessageController.java new file mode 100644 index 0000000..7dba398 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/NotifyMessageController.java @@ -0,0 +1,95 @@ +package com.yunxi.scm.module.system.controller.admin.notify; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessageRespVO; +import com.yunxi.scm.module.system.convert.notify.NotifyMessageConvert; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyMessageDO; +import com.yunxi.scm.module.system.service.notify.NotifyMessageService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 我的站内信") +@RestController +@RequestMapping("/system/notify-message") +@Validated +public class NotifyMessageController { + + @Resource + private NotifyMessageService notifyMessageService; + + // ========== 管理所有的站内信 ========== + + @GetMapping("/get") + @Operation(summary = "获得站内信") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notify-message:query')") + public CommonResult getNotifyMessage(@RequestParam("id") Long id) { + NotifyMessageDO notifyMessage = notifyMessageService.getNotifyMessage(id); + return success(NotifyMessageConvert.INSTANCE.convert(notifyMessage)); + } + + @GetMapping("/page") + @Operation(summary = "获得站内信分页") + @PreAuthorize("@ss.hasPermission('system:notify-message:query')") + public CommonResult> getNotifyMessagePage(@Valid NotifyMessagePageReqVO pageVO) { + PageResult pageResult = notifyMessageService.getNotifyMessagePage(pageVO); + return success(NotifyMessageConvert.INSTANCE.convertPage(pageResult)); + } + + // ========== 查看自己的站内信 ========== + + @GetMapping("/my-page") + @Operation(summary = "获得我的站内信分页") + public CommonResult> getMyMyNotifyMessagePage(@Valid NotifyMessageMyPageReqVO pageVO) { + PageResult pageResult = notifyMessageService.getMyMyNotifyMessagePage(pageVO, + getLoginUserId(), UserTypeEnum.ADMIN.getValue()); + return success(NotifyMessageConvert.INSTANCE.convertPage(pageResult)); + } + + @PutMapping("/update-read") + @Operation(summary = "标记站内信为已读") + @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048") + public CommonResult updateNotifyMessageRead(@RequestParam("ids") List ids) { + notifyMessageService.updateNotifyMessageRead(ids, getLoginUserId(), UserTypeEnum.ADMIN.getValue()); + return success(Boolean.TRUE); + } + + @PutMapping("/update-all-read") + @Operation(summary = "标记所有站内信为已读") + public CommonResult updateAllNotifyMessageRead() { + notifyMessageService.updateAllNotifyMessageRead(getLoginUserId(), UserTypeEnum.ADMIN.getValue()); + return success(Boolean.TRUE); + } + + @GetMapping("/get-unread-list") + @Operation(summary = "获取当前用户的最新站内信列表,默认 10 条") + @Parameter(name = "size", description = "10") + public CommonResult> getUnreadNotifyMessageList( + @RequestParam(name = "size", defaultValue = "10") Integer size) { + List list = notifyMessageService.getUnreadNotifyMessageList( + getLoginUserId(), UserTypeEnum.ADMIN.getValue(), size); + return success(NotifyMessageConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/get-unread-count") + @Operation(summary = "获得当前用户的未读站内信数量") + public CommonResult getUnreadNotifyMessageCount() { + return success(notifyMessageService.getUnreadNotifyMessageCount(getLoginUserId(), UserTypeEnum.ADMIN.getValue())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/NotifyTemplateController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/NotifyTemplateController.java new file mode 100644 index 0000000..011feeb --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/NotifyTemplateController.java @@ -0,0 +1,83 @@ +package com.yunxi.scm.module.system.controller.admin.notify; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.*; +import com.yunxi.scm.module.system.convert.notify.NotifyTemplateConvert; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.yunxi.scm.module.system.service.notify.NotifySendService; +import com.yunxi.scm.module.system.service.notify.NotifyTemplateService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 站内信模版") +@RestController +@RequestMapping("/system/notify-template") +@Validated +public class NotifyTemplateController { + + @Resource + private NotifyTemplateService notifyTemplateService; + + @Resource + private NotifySendService notifySendService; + + @PostMapping("/create") + @Operation(summary = "创建站内信模版") + @PreAuthorize("@ss.hasPermission('system:notify-template:create')") + public CommonResult createNotifyTemplate(@Valid @RequestBody NotifyTemplateCreateReqVO createReqVO) { + return success(notifyTemplateService.createNotifyTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新站内信模版") + @PreAuthorize("@ss.hasPermission('system:notify-template:update')") + public CommonResult updateNotifyTemplate(@Valid @RequestBody NotifyTemplateUpdateReqVO updateReqVO) { + notifyTemplateService.updateNotifyTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除站内信模版") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:notify-template:delete')") + public CommonResult deleteNotifyTemplate(@RequestParam("id") Long id) { + notifyTemplateService.deleteNotifyTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得站内信模版") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:notify-template:query')") + public CommonResult getNotifyTemplate(@RequestParam("id") Long id) { + NotifyTemplateDO notifyTemplate = notifyTemplateService.getNotifyTemplate(id); + return success(NotifyTemplateConvert.INSTANCE.convert(notifyTemplate)); + } + + @GetMapping("/page") + @Operation(summary = "获得站内信模版分页") + @PreAuthorize("@ss.hasPermission('system:notify-template:query')") + public CommonResult> getNotifyTemplatePage(@Valid NotifyTemplatePageReqVO pageVO) { + PageResult pageResult = notifyTemplateService.getNotifyTemplatePage(pageVO); + return success(NotifyTemplateConvert.INSTANCE.convertPage(pageResult)); + } + + @PostMapping("/send-notify") + @Operation(summary = "发送站内信") + @PreAuthorize("@ss.hasPermission('system:notify-template:send-notify')") + public CommonResult sendNotify(@Valid @RequestBody NotifyTemplateSendReqVO sendReqVO) { + return success(notifySendService.sendSingleNotifyToAdmin(sendReqVO.getUserId(), + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageBaseVO.java new file mode 100644 index 0000000..01adc2b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageBaseVO.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.message; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import javax.validation.constraints.NotNull; +import java.time.LocalDateTime; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +/** + * 站内信消息 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class NotifyMessageBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "25025") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户类型不能为空") + private Byte userType; + + @Schema(description = "模版编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "13013") + @NotNull(message = "模版编号不能为空") + private Long templateId; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String templateCode; + + @Schema(description = "模版发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotNull(message = "模版发送人名称不能为空") + private String templateNickname; + + @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试内容") + @NotNull(message = "模版内容不能为空") + private String templateContent; + + @Schema(description = "模版类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @NotNull(message = "模版类型不能为空") + private Integer templateType; + + @Schema(description = "模版参数", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "模版参数不能为空") + private Map templateParams; + + @Schema(description = "是否已读", requiredMode = Schema.RequiredMode.REQUIRED, example = "true") + @NotNull(message = "是否已读不能为空") + private Boolean readStatus; + + @Schema(description = "阅读时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime readTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageMyPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageMyPageReqVO.java new file mode 100644 index 0000000..f8c69f1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageMyPageReqVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.message; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 站内信分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyMessageMyPageReqVO extends PageParam { + + @Schema(description = "是否已读", example = "true") + private Boolean readStatus; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessagePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessagePageReqVO.java new file mode 100644 index 0000000..4367ec2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessagePageReqVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.message; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 站内信分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyMessagePageReqVO extends PageParam { + + @Schema(description = "用户编号", example = "25025") + private Long userId; + + @Schema(description = "用户类型", example = "1") + private Integer userType; + + @Schema(description = "模板编码", example = "test_01") + private String templateCode; + + @Schema(description = "模版类型", example = "2") + private Integer templateType; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageRespVO.java new file mode 100644 index 0000000..3e25b7f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/message/NotifyMessageRespVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.message; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; + +@Schema(description = "管理后台 - 站内信 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyMessageRespVO extends NotifyMessageBaseVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateBaseVO.java new file mode 100644 index 0000000..549fdfe --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateBaseVO.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.template; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 站内信模版 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class NotifyTemplateBaseVO { + + @Schema(description = "模版名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "测试模版") + @NotEmpty(message = "模版名称不能为空") + private String name; + + @Schema(description = "模版编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "SEND_TEST") + @NotNull(message = "模版编码不能为空") + private String code; + + @Schema(description = "模版类型,对应 system_notify_template_type 字典", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "模版类型不能为空") + private Integer type; + + @Schema(description = "发送人名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + @NotEmpty(message = "发送人名称不能为空") + private String nickname; + + @Schema(description = "模版内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是模版内容") + @NotEmpty(message = "模版内容不能为空") + private String content; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "状态必须是 {value}") + private Integer status; + + @Schema(description = "备注", example = "我是备注") + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateCreateReqVO.java new file mode 100644 index 0000000..bcf33de --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateCreateReqVO.java @@ -0,0 +1,11 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "管理后台 - 站内信模版创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyTemplateCreateReqVO extends NotifyTemplateBaseVO { +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplatePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplatePageReqVO.java new file mode 100644 index 0000000..0b47f83 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplatePageReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.template; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 站内信模版分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyTemplatePageReqVO extends PageParam { + + @Schema(description = "模版编码", example = "test_01") + private String code; + + @Schema(description = "模版名称", example = "我是名称") + private String name; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java new file mode 100644 index 0000000..657e4e2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import java.util.*; + +@Schema(description = "管理后台 - 站内信模版 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyTemplateRespVO extends NotifyTemplateBaseVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "参数数组", example = "name,code") + private List params; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private Date createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateSendReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateSendReqVO.java new file mode 100644 index 0000000..fb63725 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateSendReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 站内信模板的发送 Request VO") +@Data +public class NotifyTemplateSendReqVO { + + @Schema(description = "用户id", requiredMode = Schema.RequiredMode.REQUIRED, example = "01") + @NotNull(message = "用户id不能为空") + private Long userId; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "01") + @NotEmpty(message = "模板编码不能为空") + private String templateCode; + + @Schema(description = "模板参数") + private Map templateParams; +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateUpdateReqVO.java new file mode 100644 index 0000000..bef781a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/notify/vo/template/NotifyTemplateUpdateReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.system.controller.admin.notify.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 站内信模版更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class NotifyTemplateUpdateReqVO extends NotifyTemplateBaseVO { + + @Schema(description = "ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "ID 不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2ClientController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2ClientController.http new file mode 100644 index 0000000..dcf60a6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2ClientController.http @@ -0,0 +1,23 @@ +### 请求 /login 接口 => 成功 +POST {{baseUrl}}/system/oauth2-client/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "id": "1", + "secret": "admin123", + "name": "芋道源码", + "logo": "https://www.iocoder.cn/images/favicon.ico", + "description": "我是描述", + "status": 0, + "accessTokenValiditySeconds": 180, + "refreshTokenValiditySeconds": 8640, + "redirectUris": ["https://www.iocoder.cn"], + "autoApprove": true, + "authorizedGrantTypes": ["password"], + "scopes": ["user_info"], + "authorities": ["system:user:query"], + "resource_ids": ["1024"], + "additionalInformation": "{}" +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2ClientController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2ClientController.java new file mode 100644 index 0000000..419b091 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2ClientController.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import com.yunxi.scm.module.system.convert.auth.OAuth2ClientConvert; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.service.oauth2.OAuth2ClientService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - OAuth2 客户端") +@RestController +@RequestMapping("/system/oauth2-client") +@Validated +public class OAuth2ClientController { + + @Resource + private OAuth2ClientService oAuth2ClientService; + + @PostMapping("/create") + @Operation(summary = "创建 OAuth2 客户端") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:create')") + public CommonResult createOAuth2Client(@Valid @RequestBody OAuth2ClientCreateReqVO createReqVO) { + return success(oAuth2ClientService.createOAuth2Client(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新 OAuth2 客户端") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:update')") + public CommonResult updateOAuth2Client(@Valid @RequestBody OAuth2ClientUpdateReqVO updateReqVO) { + oAuth2ClientService.updateOAuth2Client(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除 OAuth2 客户端") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:oauth2-client:delete')") + public CommonResult deleteOAuth2Client(@RequestParam("id") Long id) { + oAuth2ClientService.deleteOAuth2Client(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得 OAuth2 客户端") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") + public CommonResult getOAuth2Client(@RequestParam("id") Long id) { + OAuth2ClientDO oAuth2Client = oAuth2ClientService.getOAuth2Client(id); + return success(OAuth2ClientConvert.INSTANCE.convert(oAuth2Client)); + } + + @GetMapping("/page") + @Operation(summary = "获得OAuth2 客户端分页") + @PreAuthorize("@ss.hasPermission('system:oauth2-client:query')") + public CommonResult> getOAuth2ClientPage(@Valid OAuth2ClientPageReqVO pageVO) { + PageResult pageResult = oAuth2ClientService.getOAuth2ClientPage(pageVO); + return success(OAuth2ClientConvert.INSTANCE.convertPage(pageResult)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenController.http new file mode 100644 index 0000000..725a5d4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenController.http @@ -0,0 +1,54 @@ +### 请求 /system/oauth2/authorize 接口 => 成功 +GET {{baseUrl}}/system/oauth2/authorize?clientId=default +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/authorize + token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/authorize +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +response_type=token&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=true + +### 请求 /system/oauth2/authorize + code 接口 => 成功 +POST {{baseUrl}}/system/oauth2/authorize +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +response_type=code&client_id=default&scope={"user.read": true}&redirect_uri=https://www.iocoder.cn&auto_approve=false + +### 请求 /system/oauth2/token + code 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=authorization_code&redirect_uri=https://www.iocoder.cn&code=189956c07a174588a97157eabef2f93a + +### 请求 /system/oauth2/token + password 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=password&username=admin&password=admin123&scope=user.read + +### 请求 /system/oauth2/token + refresh_token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/token +Content-Type: application/x-www-form-urlencoded +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588 + +### 请求 /system/oauth2/token + DELETE 接口 => 成功 +DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/check-token 接口 => 成功 +POST {{baseUrl}}/system/oauth2/check-token?token=620d307c5b4148df8a98dd6c6c547106 +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenController.java new file mode 100644 index 0000000..0ac4190 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenController.java @@ -0,0 +1,302 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.http.HttpUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; +import com.yunxi.scm.module.system.convert.oauth2.OAuth2OpenConvert; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.enums.oauth2.OAuth2GrantTypeEnum; +import com.yunxi.scm.module.system.service.oauth2.OAuth2ApproveService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2ClientService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2GrantService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2TokenService; +import com.yunxi.scm.module.system.util.oauth2.OAuth2Utils; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 提供给外部应用调用为主 + * + * 一般来说,管理后台的 /system-api/* 是不直接提供给外部应用使用,主要是外部应用能够访问的数据与接口是有限的,而管理后台的 RBAC 无法很好的控制。 + * 参考大量的开放平台,都是独立的一套 OpenAPI,对应到【本系统】就是在 Controller 下新建 open 包,实现 /open-api/* 接口,然后通过 scope 进行控制。 + * 另外,一个公司如果有多个管理后台,它们 client_id 产生的 access token 相互之间是无法互通的,即无法访问它们系统的 API 接口,直到两个 client_id 产生信任授权。 + * + * 考虑到【本系统】暂时不想做的过于复杂,默认只有获取到 access token 之后,可以访问【本系统】管理后台的 /system-api/* 所有接口,除非手动添加 scope 控制。 + * scope 的使用示例,可见 {@link OAuth2UserController} 类 + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - OAuth2.0 授权") +@RestController +@RequestMapping("/system/oauth2") +@Validated +@Slf4j +public class OAuth2OpenController { + + @Resource + private OAuth2GrantService oauth2GrantService; + @Resource + private OAuth2ClientService oauth2ClientService; + @Resource + private OAuth2ApproveService oauth2ApproveService; + @Resource + private OAuth2TokenService oauth2TokenService; + + /** + * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法 + * + * 授权码 authorization_code 模式时:code + redirectUri + state 参数 + * 密码 password 模式时:username + password + scope 参数 + * 刷新 refresh_token 模式时:refreshToken 参数 + * 客户端 client_credentials 模式:scope 参数 + * 简化 implicit 模式时:不支持 + * + * 注意,默认需要传递 client_id + client_secret 参数 + */ + @PostMapping("/token") + @PermitAll + @Operation(summary = "获得访问令牌", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") + @Parameters({ + @Parameter(name = "grant_type", required = true, description = "授权类型", example = "code"), + @Parameter(name = "code", description = "授权范围", example = "userinfo.read"), + @Parameter(name = "redirect_uri", description = "重定向 URI", example = "https://www.iocoder.cn"), + @Parameter(name = "state", description = "状态", example = "1"), + @Parameter(name = "username", example = "tudou"), + @Parameter(name = "password", example = "cai"), // 多个使用空格分隔 + @Parameter(name = "scope", example = "user_info"), + @Parameter(name = "refresh_token", example = "123424233"), + }) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult postAccessToken(HttpServletRequest request, + @RequestParam("grant_type") String grantType, + @RequestParam(value = "code", required = false) String code, // 授权码模式 + @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式 + @RequestParam(value = "state", required = false) String state, // 授权码模式 + @RequestParam(value = "username", required = false) String username, // 密码模式 + @RequestParam(value = "password", required = false) String password, // 密码模式 + @RequestParam(value = "scope", required = false) String scope, // 密码模式 + @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式 + List scopes = OAuth2Utils.buildScopes(scope); + // 1.1 校验授权类型 + OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); + if (grantTypeEnum == null) { + throw exception0(BAD_REQUEST.getCode(), StrUtil.format("未知授权类型({})", grantType)); + } + if (grantTypeEnum == OAuth2GrantTypeEnum.IMPLICIT) { + throw exception0(BAD_REQUEST.getCode(), "Token 接口不支持 implicit 授权模式"); + } + + // 1.2 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + grantType, scopes, redirectUri); + + // 2. 根据授权模式,获取访问令牌 + OAuth2AccessTokenDO accessTokenDO; + switch (grantTypeEnum) { + case AUTHORIZATION_CODE: + accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state); + break; + case PASSWORD: + accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes); + break; + case CLIENT_CREDENTIALS: + accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes); + break; + case REFRESH_TOKEN: + accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId()); + break; + default: + throw new IllegalArgumentException("未知授权类型:" + grantType); + } + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO)); + } + + @DeleteMapping("/token") + @PermitAll + @Operation(summary = "删除访问令牌") + @Parameter(name = "token", required = true, description = "访问令牌", example = "biu") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult revokeToken(HttpServletRequest request, + @RequestParam("token") String token) { + // 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + null, null, null); + + // 删除访问令牌 + return success(oauth2GrantService.revokeToken(client.getClientId(), token)); + } + + /** + * 对应 Spring Security OAuth 的 CheckTokenEndpoint 类的 checkToken 方法 + */ + @PostMapping("/check-token") + @PermitAll + @Operation(summary = "校验访问令牌") + @Parameter(name = "token", required = true, description = "访问令牌", example = "biu") + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult checkToken(HttpServletRequest request, + @RequestParam("token") String token) { + // 校验客户端 + String[] clientIdAndSecret = obtainBasicAuthorization(request); + oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], + null, null, null); + + // 校验令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.checkAccessToken(token); + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + return success(OAuth2OpenConvert.INSTANCE.convert2(accessTokenDO)); + } + + /** + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 authorize 方法 + */ + @GetMapping("/authorize") + @Operation(summary = "获得授权信息", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【获取】调用") + @Parameter(name = "clientId", required = true, description = "客户端编号", example = "tudou") + public CommonResult authorize(@RequestParam("clientId") String clientId) { + // 0. 校验用户已经登录。通过 Spring Security 实现 + + // 1. 获得 Client 客户端的信息 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId); + // 2. 获得用户已经授权的信息 + List approves = oauth2ApproveService.getApproveList(getLoginUserId(), getUserType(), clientId); + // 拼接返回 + return success(OAuth2OpenConvert.INSTANCE.convert(client, approves)); + } + + /** + * 对应 Spring Security OAuth 的 AuthorizationEndpoint 类的 approveOrDeny 方法 + * + * 场景一:【自动授权 autoApprove = true】 + * 刚进入 sso.vue 界面,调用该接口,用户历史已经给该应用做过对应的授权,或者 OAuth2Client 支持该 scope 的自动授权 + * 场景二:【手动授权 autoApprove = false】 + * 在 sso.vue 界面,用户选择好 scope 授权范围,调用该接口,进行授权。此时,approved 为 true 或者 false + * + * 因为前后端分离,Axios 无法很好的处理 302 重定向,所以和 Spring Security OAuth 略有不同,返回结果是重定向的 URL,剩余交给前端处理 + */ + @PostMapping("/authorize") + @Operation(summary = "申请授权", description = "适合 code 授权码模式,或者 implicit 简化模式;在 sso.vue 单点登录界面被【提交】调用") + @Parameters({ + @Parameter(name = "response_type", required = true, description = "响应类型", example = "code"), + @Parameter(name = "client_id", required = true, description = "客户端编号", example = "tudou"), + @Parameter(name = "scope", description = "授权范围", example = "userinfo.read"), // 使用 Map 格式,Spring MVC 暂时不支持这么接收参数 + @Parameter(name = "redirect_uri", required = true, description = "重定向 URI", example = "https://www.iocoder.cn"), + @Parameter(name = "auto_approve", required = true, description = "用户是否接受", example = "true"), + @Parameter(name = "state", example = "1") + }) + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 + public CommonResult approveOrDeny(@RequestParam("response_type") String responseType, + @RequestParam("client_id") String clientId, + @RequestParam(value = "scope", required = false) String scope, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam(value = "auto_approve") Boolean autoApprove, + @RequestParam(value = "state", required = false) String state) { + @SuppressWarnings("unchecked") + Map scopes = JsonUtils.parseObject(scope, Map.class); + scopes = ObjectUtil.defaultIfNull(scopes, Collections.emptyMap()); + // 0. 校验用户已经登录。通过 Spring Security 实现 + + // 1.1 校验 responseType 是否满足 code 或者 token 值 + OAuth2GrantTypeEnum grantTypeEnum = getGrantTypeEnum(responseType); + // 1.2 校验 redirectUri 重定向域名是否合法 + 校验 scope 是否在 Client 授权范围内 + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientId, null, + grantTypeEnum.getGrantType(), scopes.keySet(), redirectUri); + + // 2.1 假设 approved 为 null,说明是场景一 + if (Boolean.TRUE.equals(autoApprove)) { + // 如果无法自动授权通过,则返回空 url,前端不进行跳转 + if (!oauth2ApproveService.checkForPreApproval(getLoginUserId(), getUserType(), clientId, scopes.keySet())) { + return success(null); + } + } else { // 2.2 假设 approved 非 null,说明是场景二 + // 如果计算后不通过,则跳转一个错误链接 + if (!oauth2ApproveService.updateAfterApproval(getLoginUserId(), getUserType(), clientId, scopes)) { + return success(OAuth2Utils.buildUnsuccessfulRedirect(redirectUri, responseType, state, + "access_denied", "User denied access")); + } + } + + // 3.1 如果是 code 授权码模式,则发放 code 授权码,并重定向 + List approveScopes = convertList(scopes.entrySet(), Map.Entry::getKey, Map.Entry::getValue); + if (grantTypeEnum == OAuth2GrantTypeEnum.AUTHORIZATION_CODE) { + return success(getAuthorizationCodeRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); + } + // 3.2 如果是 token 则是 implicit 简化模式,则发送 accessToken 访问令牌,并重定向 + return success(getImplicitGrantRedirect(getLoginUserId(), client, approveScopes, redirectUri, state)); + } + + private static OAuth2GrantTypeEnum getGrantTypeEnum(String responseType) { + if (StrUtil.equals(responseType, "code")) { + return OAuth2GrantTypeEnum.AUTHORIZATION_CODE; + } + if (StrUtil.equalsAny(responseType, "token")) { + return OAuth2GrantTypeEnum.IMPLICIT; + } + throw exception0(BAD_REQUEST.getCode(), "response_type 参数值只允许 code 和 token"); + } + + private String getImplicitGrantRedirect(Long userId, OAuth2ClientDO client, + List scopes, String redirectUri, String state) { + // 1. 创建 access token 访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2GrantService.grantImplicit(userId, getUserType(), client.getClientId(), scopes); + Assert.notNull(accessTokenDO, "访问令牌不能为空"); // 防御性检查 + // 2. 拼接重定向的 URL + // noinspection unchecked + return OAuth2Utils.buildImplicitRedirectUri(redirectUri, accessTokenDO.getAccessToken(), state, accessTokenDO.getExpiresTime(), + scopes, JsonUtils.parseObject(client.getAdditionalInformation(), Map.class)); + } + + private String getAuthorizationCodeRedirect(Long userId, OAuth2ClientDO client, + List scopes, String redirectUri, String state) { + // 1. 创建 code 授权码 + String authorizationCode = oauth2GrantService.grantAuthorizationCodeForCode(userId, getUserType(), client.getClientId(), scopes, + redirectUri, state); + // 2. 拼接重定向的 URL + return OAuth2Utils.buildAuthorizationCodeRedirectUri(redirectUri, authorizationCode, state); + } + + private Integer getUserType() { + return UserTypeEnum.ADMIN.getValue(); + } + + private String[] obtainBasicAuthorization(HttpServletRequest request) { + String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request); + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); + } + return clientIdAndSecret; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2TokenController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2TokenController.java new file mode 100644 index 0000000..351a1ae --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2TokenController.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenRespVO; +import com.yunxi.scm.module.system.convert.auth.OAuth2TokenConvert; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.enums.logger.LoginLogTypeEnum; +import com.yunxi.scm.module.system.service.auth.AdminAuthService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2TokenService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - OAuth2.0 令牌") +@RestController +@RequestMapping("/system/oauth2-token") +public class OAuth2TokenController { + + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private AdminAuthService authService; + + @GetMapping("/page") + @Operation(summary = "获得访问令牌分页", description = "只返回有效期内的") + @PreAuthorize("@ss.hasPermission('system:oauth2-token:page')") + public CommonResult> getAccessTokenPage(@Valid OAuth2AccessTokenPageReqVO reqVO) { + PageResult pageResult = oauth2TokenService.getAccessTokenPage(reqVO); + return success(OAuth2TokenConvert.INSTANCE.convert(pageResult)); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除访问令牌") + @Parameter(name = "accessToken", description = "访问令牌", required = true, example = "tudou") + @PreAuthorize("@ss.hasPermission('system:oauth2-token:delete')") + public CommonResult deleteAccessToken(@RequestParam("accessToken") String accessToken) { + authService.logout(accessToken, LoginLogTypeEnum.LOGOUT_DELETE.getType()); + return success(true); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2UserController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2UserController.http new file mode 100644 index 0000000..13c8545 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2UserController.http @@ -0,0 +1,14 @@ +### 请求 /system/oauth2/user/get 接口 => 成功 +GET {{baseUrl}}/system/oauth2/user/get +Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d +tenant-id: {{adminTenentId}} + +### 请求 /system/oauth2/user/update 接口 => 成功 +PUT {{baseUrl}}/system/oauth2/user/update +Content-Type: application/json +Authorization: Bearer 47f9c74ec11041f193b777ebb95c3b0d +tenant-id: {{adminTenentId}} + +{ + "nickname": "芋道源码" +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2UserController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2UserController.java new file mode 100644 index 0000000..696ed95 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2UserController.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.user.OAuth2UserInfoRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.user.OAuth2UserUpdateReqVO; +import com.yunxi.scm.module.system.convert.oauth2.OAuth2UserConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.service.dept.DeptService; +import com.yunxi.scm.module.system.service.dept.PostService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +/** + * 提供给外部应用调用为主 + * + * 1. 在 getUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.read')") 注解,声明需要满足 scope = user.read + * 2. 在 updateUserInfo 方法上,添加 @PreAuthorize("@ss.hasScope('user.write')") 注解,声明需要满足 scope = user.write + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - OAuth2.0 用户") +@RestController +@RequestMapping("/system/oauth2/user") +@Validated +@Slf4j +public class OAuth2UserController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + @Resource + private PostService postService; + + @GetMapping("/get") + @Operation(summary = "获得用户基本信息") + @PreAuthorize("@ss.hasScope('user.read')") // + public CommonResult getUserInfo() { + // 获得用户基本信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + OAuth2UserInfoRespVO resp = OAuth2UserConvert.INSTANCE.convert(user); + // 获得部门信息 + if (user.getDeptId() != null) { + DeptDO dept = deptService.getDept(user.getDeptId()); + resp.setDept(OAuth2UserConvert.INSTANCE.convert(dept)); + } + // 获得岗位信息 + if (CollUtil.isNotEmpty(user.getPostIds())) { + List posts = postService.getPostList(user.getPostIds()); + resp.setPosts(OAuth2UserConvert.INSTANCE.convertList(posts)); + } + return success(resp); + } + + @PutMapping("/update") + @Operation(summary = "更新用户基本信息") + @PreAuthorize("@ss.hasScope('user.write')") + public CommonResult updateUserInfo(@Valid @RequestBody OAuth2UserUpdateReqVO reqVO) { + // 这里将 UserProfileUpdateReqVO =》UserProfileUpdateReqVO 对象,实现接口的复用。 + // 主要是,AdminUserService 没有自己的 BO 对象,所以复用只能这么做 + userService.updateUserProfile(getLoginUserId(), OAuth2UserConvert.INSTANCE.convert(reqVO)); + return success(true); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientBaseVO.java new file mode 100644 index 0000000..42bbf8a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientBaseVO.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.client; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** +* OAuth2 客户端 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class OAuth2ClientBaseVO { + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + @NotNull(message = "客户端编号不能为空") + private String clientId; + + @Schema(description = "客户端密钥", requiredMode = Schema.RequiredMode.REQUIRED, example = "fan") + @NotNull(message = "客户端密钥不能为空") + private String secret; + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + @NotNull(message = "应用名不能为空") + private String name; + + @Schema(description = "应用图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + @NotNull(message = "应用图标不能为空") + @URL(message = "应用图标的地址不正确") + private String logo; + + @Schema(description = "应用描述", example = "我是一个应用") + private String description; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "访问令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640") + @NotNull(message = "访问令牌的有效期不能为空") + private Integer accessTokenValiditySeconds; + + @Schema(description = "刷新令牌的有效期", requiredMode = Schema.RequiredMode.REQUIRED, example = "8640000") + @NotNull(message = "刷新令牌的有效期不能为空") + private Integer refreshTokenValiditySeconds; + + @Schema(description = "可重定向的 URI 地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn") + @NotNull(message = "可重定向的 URI 地址不能为空") + private List<@NotEmpty(message = "重定向的 URI 不能为空") + @URL(message = "重定向的 URI 格式不正确") String> redirectUris; + + @Schema(description = "授权类型,参见 OAuth2GrantTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "password") + @NotNull(message = "授权类型不能为空") + private List authorizedGrantTypes; + + @Schema(description = "授权范围", example = "user_info") + private List scopes; + + @Schema(description = "自动通过的授权范围", example = "user_info") + private List autoApproveScopes; + + @Schema(description = "权限", example = "system:user:query") + private List authorities; + + @Schema(description = "资源", example = "1024") + private List resourceIds; + + @Schema(description = "附加信息", example = "{yunai: true}") + private String additionalInformation; + + @AssertTrue(message = "附加信息必须是 JSON 格式") + public boolean isAdditionalInformationJson() { + return StrUtil.isEmpty(additionalInformation) || JsonUtils.isJson(additionalInformation); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientCreateReqVO.java new file mode 100644 index 0000000..24ed409 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientCreateReqVO.java @@ -0,0 +1,12 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.client; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "管理后台 - OAuth2 客户端创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientCreateReqVO extends OAuth2ClientBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java new file mode 100644 index 0000000..eec8f2c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientPageReqVO.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.client; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import com.yunxi.scm.framework.common.pojo.PageParam; + +@Schema(description = "管理后台 - OAuth2 客户端分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientPageReqVO extends PageParam { + + @Schema(description = "应用名,模糊匹配", example = "土豆") + private String name; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java new file mode 100644 index 0000000..dfb1d37 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.client; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - OAuth2 客户端 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientRespVO extends OAuth2ClientBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientUpdateReqVO.java new file mode 100644 index 0000000..9390806 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/client/OAuth2ClientUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.client; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - OAuth2 客户端更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class OAuth2ClientUpdateReqVO extends OAuth2ClientBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java new file mode 100644 index 0000000..b5e0f64 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.open; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 【开放接口】访问令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAccessTokenRespVO { + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + @JsonProperty("access_token") + private String accessToken; + + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + @JsonProperty("refresh_token") + private String refreshToken; + + @Schema(description = "令牌类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "bearer") + @JsonProperty("token_type") + private String tokenType; + + @Schema(description = "过期时间,单位:秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "42430") + @JsonProperty("expires_in") + private Long expiresIn; + + @Schema(description = "授权范围,如果多个授权范围,使用空格分隔", example = "user_info") + private String scope; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java new file mode 100644 index 0000000..5620c59 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAuthorizeInfoRespVO.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.open; + +import com.yunxi.scm.framework.common.core.KeyValue; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 授权页的信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenAuthorizeInfoRespVO { + + /** + * 客户端 + */ + private Client client; + + @Schema(description = "scope 的选中信息,使用 List 保证有序性,Key 是 scope,Value 为是否选中", requiredMode = Schema.RequiredMode.REQUIRED) + private List> scopes; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class Client { + + @Schema(description = "应用名", requiredMode = Schema.RequiredMode.REQUIRED, example = "土豆") + private String name; + + @Schema(description = "应用图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/xx.png") + private String logo; + + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java new file mode 100644 index 0000000..7270ca5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/open/OAuth2OpenCheckTokenRespVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.open; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - 【开放接口】校验令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2OpenCheckTokenRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + @JsonProperty("user_id") + private Long userId; + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + @JsonProperty("user_type") + private Integer userType; + @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @JsonProperty("tenant_id") + private Long tenantId; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "car") + @JsonProperty("client_id") + private String clientId; + @Schema(description = "授权范围", requiredMode = Schema.RequiredMode.REQUIRED, example = "user_info") + private List scopes; + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + @JsonProperty("access_token") + private String accessToken; + + @Schema(description = "过期时间,时间戳 / 1000,即单位:秒", requiredMode = Schema.RequiredMode.REQUIRED, example = "1593092157") + private Long exp; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java new file mode 100644 index 0000000..d3c66dd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenPageReqVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.token; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 访问令牌分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2AccessTokenPageReqVO extends PageParam { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private String clientId; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java new file mode 100644 index 0000000..4c10a3f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/token/OAuth2AccessTokenRespVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.token; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 访问令牌 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2AccessTokenRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "访问令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "tudou") + private String accessToken; + + @Schema(description = "刷新令牌", requiredMode = Schema.RequiredMode.REQUIRED, example = "nice") + private String refreshToken; + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "666") + private Long userId; + + @Schema(description = "用户类型,参见 UserTypeEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private Integer userType; + + @Schema(description = "客户端编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") + private String clientId; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime expiresTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java new file mode 100644 index 0000000..4059f2d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/user/OAuth2UserInfoRespVO.java @@ -0,0 +1,70 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Schema(description = "管理后台 - OAuth2 获得用户基本信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2UserInfoRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + private String username; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String nickname; + + @Schema(description = "用户邮箱", example = "yunxi@iocoder.cn") + private String email; + @Schema(description = "手机号码", example = "15601691300") + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + + @Schema(description = "部门") + @Data + public static class Dept { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") + private String name; + + } + + @Schema(description = "岗位") + @Data + public static class Post { + + @Schema(description = "岗位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "开发") + private String name; + + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java new file mode 100644 index 0000000..24c3a28 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/oauth2/vo/user/OAuth2UserUpdateReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - OAuth2 更新用户基本信息 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class OAuth2UserUpdateReqVO { + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过 30 个字符") + private String nickname; + + @Schema(description = "用户邮箱", example = "yunxi@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + @Length(min = 11, max = 11, message = "手机号长度必须 11 位") + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/MenuController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/MenuController.http new file mode 100644 index 0000000..a90d8b8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/MenuController.http @@ -0,0 +1,4 @@ +### 请求 /menu/list 接口 => 成功 +GET {{baseUrl}}/system/menu/list +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/MenuController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/MenuController.java new file mode 100644 index 0000000..c530c8a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/MenuController.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.module.system.controller.admin.permission; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.*; +import com.yunxi.scm.module.system.convert.permission.MenuConvert; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.service.permission.MenuService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 菜单") +@RestController +@RequestMapping("/system/menu") +@Validated +public class MenuController { + + @Resource + private MenuService menuService; + + @PostMapping("/create") + @Operation(summary = "创建菜单") + @PreAuthorize("@ss.hasPermission('system:menu:create')") + public CommonResult createMenu(@Valid @RequestBody MenuCreateReqVO reqVO) { + Long menuId = menuService.createMenu(reqVO); + return success(menuId); + } + + @PutMapping("/update") + @Operation(summary = "修改菜单") + @PreAuthorize("@ss.hasPermission('system:menu:update')") + public CommonResult updateMenu(@Valid @RequestBody MenuUpdateReqVO reqVO) { + menuService.updateMenu(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除菜单") + @Parameter(name = "id", description = "角色编号", required= true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:menu:delete')") + public CommonResult deleteMenu(@RequestParam("id") Long id) { + menuService.deleteMenu(id); + return success(true); + } + + @GetMapping("/list") + @Operation(summary = "获取菜单列表", description = "用于【菜单管理】界面") + @PreAuthorize("@ss.hasPermission('system:menu:query')") + public CommonResult> getMenuList(MenuListReqVO reqVO) { + List list = menuService.getMenuList(reqVO); + list.sort(Comparator.comparing(MenuDO::getSort)); + return success(MenuConvert.INSTANCE.convertList(list)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取菜单精简信息列表", description = "只包含被开启的菜单,用于【角色分配菜单】功能的选项。" + + "在多租户的场景下,会只返回租户所在套餐有的菜单") + public CommonResult> getSimpleMenuList() { + // 获得菜单列表,只要开启状态的 + MenuListReqVO reqVO = new MenuListReqVO(); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + List list = menuService.getMenuListByTenant(reqVO); + // 排序后,返回给前端 + list.sort(Comparator.comparing(MenuDO::getSort)); + return success(MenuConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/get") + @Operation(summary = "获取菜单信息") + @PreAuthorize("@ss.hasPermission('system:menu:query')") + public CommonResult getMenu(Long id) { + MenuDO menu = menuService.getMenu(id); + return success(MenuConvert.INSTANCE.convert(menu)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/PermissionController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/PermissionController.java new file mode 100644 index 0000000..cc53fec --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/PermissionController.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.system.controller.admin.permission; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleDataScopeReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.permission.PermissionAssignRoleMenuReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.permission.PermissionAssignUserRoleReqVO; +import com.yunxi.scm.module.system.service.permission.PermissionService; +import com.yunxi.scm.module.system.service.tenant.TenantService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +/** + * 权限 Controller,提供赋予用户、角色的权限的 API 接口 + * + * @author 芋道源码 + */ +@Tag(name = "管理后台 - 权限") +@RestController +@RequestMapping("/system/permission") +public class PermissionController { + + @Resource + private PermissionService permissionService; + @Resource + private TenantService tenantService; + + @Operation(summary = "获得角色拥有的菜单编号") + @Parameter(name = "roleId", description = "角色编号", required = true) + @GetMapping("/list-role-menus") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')") + public CommonResult> getRoleMenuList(Long roleId) { + return success(permissionService.getRoleMenuListByRoleId(roleId)); + } + + @PostMapping("/assign-role-menu") + @Operation(summary = "赋予角色菜单") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-menu')") + public CommonResult assignRoleMenu(@Validated @RequestBody PermissionAssignRoleMenuReqVO reqVO) { + // 开启多租户的情况下,需要过滤掉未开通的菜单 + tenantService.handleTenantMenu(menuIds -> reqVO.getMenuIds().removeIf(menuId -> !CollUtil.contains(menuIds, menuId))); + + // 执行菜单的分配 + permissionService.assignRoleMenu(reqVO.getRoleId(), reqVO.getMenuIds()); + return success(true); + } + + @PostMapping("/assign-role-data-scope") + @Operation(summary = "赋予角色数据权限") + @PreAuthorize("@ss.hasPermission('system:permission:assign-role-data-scope')") + public CommonResult assignRoleDataScope(@Valid @RequestBody PermissionAssignRoleDataScopeReqVO reqVO) { + permissionService.assignRoleDataScope(reqVO.getRoleId(), reqVO.getDataScope(), reqVO.getDataScopeDeptIds()); + return success(true); + } + + @Operation(summary = "获得管理员拥有的角色编号列表") + @Parameter(name = "userId", description = "用户编号", required = true) + @GetMapping("/list-user-roles") + @PreAuthorize("@ss.hasPermission('system:permission:assign-user-role')") + public CommonResult> listAdminRoles(@RequestParam("userId") Long userId) { + return success(permissionService.getUserRoleIdListByUserId(userId)); + } + + @Operation(summary = "赋予用户角色") + @PostMapping("/assign-user-role") + @PreAuthorize("@ss.hasPermission('system:permission:assign-user-role')") + public CommonResult assignUserRole(@Validated @RequestBody PermissionAssignUserRoleReqVO reqVO) { + permissionService.assignUserRole(reqVO.getUserId(), reqVO.getRoleIds()); + return success(true); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/RoleController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/RoleController.http new file mode 100644 index 0000000..c68b86b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/RoleController.http @@ -0,0 +1,42 @@ +### /role/create 成功 +POST {{baseUrl}}/system/role/create +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "name": "测试角色", + "code": "test", + "sort": 0 +} + +### /role/update 成功 +POST {{baseUrl}}/system/role/update +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "id": 100, + "name": "测试角色", + "code": "test", + "sort": 10 +} +### /resource/delete 成功 +POST {{baseUrl}}/system/role/delete +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +roleId=14 + +### /role/get 成功 +GET {{baseUrl}}/system/role/get?id=100 +Content-Type: application/x-www-form-urlencoded +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +### /role/page 成功 +GET {{baseUrl}}/system/role/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/RoleController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/RoleController.java new file mode 100644 index 0000000..78c0a13 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/RoleController.java @@ -0,0 +1,106 @@ +package com.yunxi.scm.module.system.controller.admin.permission; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.*; +import com.yunxi.scm.module.system.convert.permission.RoleConvert; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.service.permission.RoleService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.Comparator; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; +import static java.util.Collections.singleton; + +@Tag(name = "管理后台 - 角色") +@RestController +@RequestMapping("/system/role") +@Validated +public class RoleController { + + @Resource + private RoleService roleService; + + @PostMapping("/create") + @Operation(summary = "创建角色") + @PreAuthorize("@ss.hasPermission('system:role:create')") + public CommonResult createRole(@Valid @RequestBody RoleCreateReqVO reqVO) { + return success(roleService.createRole(reqVO, null)); + } + + @PutMapping("/update") + @Operation(summary = "修改角色") + @PreAuthorize("@ss.hasPermission('system:role:update')") + public CommonResult updateRole(@Valid @RequestBody RoleUpdateReqVO reqVO) { + roleService.updateRole(reqVO); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "修改角色状态") + @PreAuthorize("@ss.hasPermission('system:role:update')") + public CommonResult updateRoleStatus(@Valid @RequestBody RoleUpdateStatusReqVO reqVO) { + roleService.updateRoleStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除角色") + @Parameter(name = "id", description = "角色编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:role:delete')") + public CommonResult deleteRole(@RequestParam("id") Long id) { + roleService.deleteRole(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得角色信息") + @PreAuthorize("@ss.hasPermission('system:role:query')") + public CommonResult getRole(@RequestParam("id") Long id) { + RoleDO role = roleService.getRole(id); + return success(RoleConvert.INSTANCE.convert(role)); + } + + @GetMapping("/page") + @Operation(summary = "获得角色分页") + @PreAuthorize("@ss.hasPermission('system:role:query')") + public CommonResult> getRolePage(RolePageReqVO reqVO) { + return success(roleService.getRolePage(reqVO)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取角色精简信息列表", description = "只包含被开启的角色,主要用于前端的下拉选项") + public CommonResult> getSimpleRoleList() { + // 获得角色列表,只要开启状态的 + List list = roleService.getRoleListByStatus(singleton(CommonStatusEnum.ENABLE.getStatus())); + // 排序后,返回给前端 + list.sort(Comparator.comparing(RoleDO::getSort)); + return success(RoleConvert.INSTANCE.convertList02(list)); + } + + @GetMapping("/export") + @OperateLog(type = EXPORT) + @PreAuthorize("@ss.hasPermission('system:role:export')") + public void export(HttpServletResponse response, @Validated RoleExportReqVO reqVO) throws IOException { + List list = roleService.getRoleList(reqVO); + List data = RoleConvert.INSTANCE.convertList03(list); + // 输出 + ExcelUtils.write(response, "角色数据.xls", "角色列表", RoleExcelVO.class, data); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuBaseVO.java new file mode 100644 index 0000000..31ce8e3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuBaseVO.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 菜单 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class MenuBaseVO { + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotBlank(message = "菜单名称不能为空") + @Size(max = 50, message = "菜单名称长度不能超过50个字符") + private String name; + + @Schema(description = "权限标识,仅菜单类型为按钮时,才需要传递", example = "sys:menu:add") + @Size(max = 100) + private String permission; + + @Schema(description = "类型,参见 MenuTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "菜单类型不能为空") + private Integer type; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "父菜单 ID 不能为空") + private Long parentId; + + @Schema(description = "路由地址,仅菜单类型为菜单或者目录时,才需要传", example = "post") + @Size(max = 200, message = "路由地址不能超过200个字符") + private String path; + + @Schema(description = "菜单图标,仅菜单类型为菜单或者目录时,才需要传", example = "/menu/list") + private String icon; + + @Schema(description = "组件路径,仅菜单类型为菜单时,才需要传", example = "system/post/index") + @Size(max = 200, message = "组件路径不能超过255个字符") + private String component; + + @Schema(description = "组件名", example = "SystemUser") + private String componentName; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "是否可见", example = "false") + private Boolean visible; + + @Schema(description = "是否缓存", example = "false") + private Boolean keepAlive; + + @Schema(description = "是否总是显示", example = "false") + private Boolean alwaysShow; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuCreateReqVO.java new file mode 100644 index 0000000..4e656da --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuCreateReqVO.java @@ -0,0 +1,10 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +@Schema(description = "管理后台 - 菜单创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuCreateReqVO extends MenuBaseVO { +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java new file mode 100644 index 0000000..2609343 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuListReqVO.java @@ -0,0 +1,16 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Schema(description = "管理后台 - 菜单列表 Request VO") +@Data +public class MenuListReqVO { + + @Schema(description = "菜单名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuRespVO.java new file mode 100644 index 0000000..4515301 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuRespVO.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 菜单信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class MenuRespVO extends MenuBaseVO { + + @Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java new file mode 100644 index 0000000..95ede97 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuSimpleRespVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 菜单精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MenuSimpleRespVO { + + @Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "菜单名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + + @Schema(description = "父菜单 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long parentId; + + @Schema(description = "类型,参见 MenuTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuUpdateReqVO.java new file mode 100644 index 0000000..ba9e60c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/menu/MenuUpdateReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.menu; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 菜单更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuUpdateReqVO extends MenuBaseVO { + + @Schema(description = "菜单编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "菜单编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java new file mode 100644 index 0000000..1986043 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleDataScopeReqVO.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.permission; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@Schema(description = "管理后台 - 赋予角色数据权限 Request VO") +@Data +public class PermissionAssignRoleDataScopeReqVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "角色编号不能为空") + private Long roleId; + + @Schema(description = "数据范围,参见 DataScopeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "数据范围不能为空") +// TODO 这里要多一个枚举校验 + private Integer dataScope; + + @Schema(description = "部门编号列表,只有范围类型为 DEPT_CUSTOM 时,该字段才需要", example = "1,3,5") + private Set dataScopeDeptIds = Collections.emptySet(); // 兜底 + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java new file mode 100644 index 0000000..719ac8e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignRoleMenuReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.permission; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@Schema(description = "管理后台 - 赋予角色菜单 Request VO") +@Data +public class PermissionAssignRoleMenuReqVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "角色编号不能为空") + private Long roleId; + + @Schema(description = "菜单编号列表", example = "1,3,5") + private Set menuIds = Collections.emptySet(); // 兜底 + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java new file mode 100644 index 0000000..d7ed5b2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/permission/PermissionAssignUserRoleReqVO.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.permission; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Set; + +@Schema(description = "管理后台 - 赋予用户角色 Request VO") +@Data +public class PermissionAssignUserRoleReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "用户编号不能为空") + private Long userId; + + @Schema(description = "角色编号列表", example = "1,3,5") + private Set roleIds = Collections.emptySet(); // 兜底 + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleBaseVO.java new file mode 100644 index 0000000..94576b8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleBaseVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 角色 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class RoleBaseVO { + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "管理员") + @NotBlank(message = "角色名称不能为空") + @Size(max = 30, message = "角色名称长度不能超过30个字符") + private String name; + + @NotBlank(message = "角色标志不能为空") + @Size(max = 100, message = "角色标志长度不能超过100个字符") + @Schema(description = "角色编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ADMIN") + private String code; + + @Schema(description = "显示顺序不能为空", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + @Schema(description = "备注", example = "我是一个角色") + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleCreateReqVO.java new file mode 100644 index 0000000..0446a8b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleCreateReqVO.java @@ -0,0 +1,12 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Schema(description = "管理后台 - 角色创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleCreateReqVO extends RoleBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleExcelVO.java new file mode 100644 index 0000000..f742b3e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleExcelVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +/** + * 角色 Excel 导出响应 VO + */ +@Data +public class RoleExcelVO { + + @ExcelProperty("角色序号") + private Long id; + + @ExcelProperty("角色名称") + private String name; + + @ExcelProperty("角色标志") + private String code; + + @ExcelProperty("角色排序") + private Integer sort; + + @ExcelProperty("数据范围") + private Integer dataScope; + + @ExcelProperty(value = "角色状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private String status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleExportReqVO.java new file mode 100644 index 0000000..c20bed3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleExportReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 角色分页 Request VO") +@Data +public class RoleExportReqVO { + + @Schema(description = "角色名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "角色标识,模糊匹配", example = "yunxi") + private String code; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @Schema(description = "开始时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RolePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RolePageReqVO.java new file mode 100644 index 0000000..7e9f794 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RolePageReqVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 角色分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class RolePageReqVO extends PageParam { + + @Schema(description = "角色名称,模糊匹配", example = "芋道") + private String name; + + @Schema(description = "角色标识,模糊匹配", example = "yunxi") + private String code; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleRespVO.java new file mode 100644 index 0000000..9d203c5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleRespVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.Set; + +@Schema(description = "管理后台 - 角色信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class RoleRespVO extends RoleBaseVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "数据范围,参见 DataScopeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer dataScope; + + @Schema(description = "数据范围(指定部门数组)", example = "1") + private Set dataScopeDeptIds; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "角色类型,参见 RoleTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer type; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java new file mode 100644 index 0000000..02e6ba7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleSimpleRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 角色精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class RoleSimpleRespVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String name; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleUpdateReqVO.java new file mode 100644 index 0000000..df3bf83 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 角色更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleUpdateReqVO extends RoleBaseVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "角色编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java new file mode 100644 index 0000000..8d44999 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/permission/vo/role/RoleUpdateStatusReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.permission.vo.role; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 角色更新状态 Request VO") +@Data +public class RoleUpdateStatusReqVO { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "角色编号不能为空") + private Long id; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/SensitiveWordController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/SensitiveWordController.http new file mode 100644 index 0000000..cd97d2d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/SensitiveWordController.http @@ -0,0 +1,4 @@ +### 请求 /system/sensitive-word/validate-text 接口 => 成功 +GET {{baseUrl}}/system/sensitive-word/validate-text?text=XXX&tags=短信&tags=蔬菜 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/SensitiveWordController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/SensitiveWordController.java new file mode 100644 index 0000000..ee15d46 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/SensitiveWordController.java @@ -0,0 +1,104 @@ +package com.yunxi.scm.module.system.controller.admin.sensitiveword; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.*; +import com.yunxi.scm.module.system.convert.sensitiveword.SensitiveWordConvert; +import com.yunxi.scm.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import com.yunxi.scm.module.system.service.sensitiveword.SensitiveWordService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 敏感词") +@RestController +@RequestMapping("/system/sensitive-word") +@Validated +public class SensitiveWordController { + + @Resource + private SensitiveWordService sensitiveWordService; + + @PostMapping("/create") + @Operation(summary = "创建敏感词") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:create')") + public CommonResult createSensitiveWord(@Valid @RequestBody SensitiveWordCreateReqVO createReqVO) { + return success(sensitiveWordService.createSensitiveWord(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新敏感词") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:update')") + public CommonResult updateSensitiveWord(@Valid @RequestBody SensitiveWordUpdateReqVO updateReqVO) { + sensitiveWordService.updateSensitiveWord(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除敏感词") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:sensitive-word:delete')") + public CommonResult deleteSensitiveWord(@RequestParam("id") Long id) { + sensitiveWordService.deleteSensitiveWord(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得敏感词") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult getSensitiveWord(@RequestParam("id") Long id) { + SensitiveWordDO sensitiveWord = sensitiveWordService.getSensitiveWord(id); + return success(SensitiveWordConvert.INSTANCE.convert(sensitiveWord)); + } + + @GetMapping("/page") + @Operation(summary = "获得敏感词分页") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult> getSensitiveWordPage(@Valid SensitiveWordPageReqVO pageVO) { + PageResult pageResult = sensitiveWordService.getSensitiveWordPage(pageVO); + return success(SensitiveWordConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出敏感词 Excel") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:export')") + @OperateLog(type = EXPORT) + public void exportSensitiveWordExcel(@Valid SensitiveWordExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = sensitiveWordService.getSensitiveWordList(exportReqVO); + // 导出 Excel + List datas = SensitiveWordConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "敏感词.xls", "数据", SensitiveWordExcelVO.class, datas); + } + + @GetMapping("/get-tags") + @Operation(summary = "获取所有敏感词的标签数组") + @PreAuthorize("@ss.hasPermission('system:sensitive-word:query')") + public CommonResult> getSensitiveWordTagSet() { + return success(sensitiveWordService.getSensitiveWordTagSet()); + } + + @GetMapping("/validate-text") + @Operation(summary = "获得文本所包含的不合法的敏感词数组") + public CommonResult> validateText(@RequestParam("text") String text, + @RequestParam(value = "tags", required = false) List tags) { + return success(sensitiveWordService.validateText(text, tags)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordBaseVO.java new file mode 100644 index 0000000..ed3aa47 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordBaseVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 敏感词 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class SensitiveWordBaseVO { + + @Schema(description = "敏感词", requiredMode = Schema.RequiredMode.REQUIRED, example = "敏感词") + @NotNull(message = "敏感词不能为空") + private String name; + + @Schema(description = "标签", requiredMode = Schema.RequiredMode.REQUIRED, example = "短信,评论") + @NotNull(message = "标签不能为空") + private List tags; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "描述", example = "污言秽语") + private String description; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordCreateReqVO.java new file mode 100644 index 0000000..67a1708 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 敏感词创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordCreateReqVO extends SensitiveWordBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordExcelVO.java new file mode 100644 index 0000000..db9602a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordExcelVO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.controller.admin.sensitiveword.vo; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.framework.excel.core.convert.JsonConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 敏感词 Excel VO + * + * @author 永不言败 + */ +@Data +public class SensitiveWordExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("敏感词") + private String name; + + @ExcelProperty(value = "标签", converter = JsonConvert.class) + private List tags; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("描述") + private String description; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordExportReqVO.java new file mode 100644 index 0000000..8a20056 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordExportReqVO.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 敏感词 Excel 导出 Request VO,参数和 SensitiveWordPageReqVO 是一致的") +@Data +public class SensitiveWordExportReqVO { + + @Schema(description = "敏感词", example = "敏感词") + private String name; + + @Schema(description = "标签", example = "短信,评论") + private String tag; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java new file mode 100644 index 0000000..2258007 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordPageReqVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.controller.admin.sensitiveword.vo; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 敏感词分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordPageReqVO extends PageParam { + + @Schema(description = "敏感词", example = "敏感词") + private String name; + + @Schema(description = "标签", example = "短信,评论") + private String tag; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java new file mode 100644 index 0000000..e247770 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 敏感词 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordRespVO extends SensitiveWordBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordUpdateReqVO.java new file mode 100644 index 0000000..40fa754 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sensitiveword/vo/SensitiveWordUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.sensitiveword.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 敏感词更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SensitiveWordUpdateReqVO extends SensitiveWordBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsCallbackController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsCallbackController.java new file mode 100644 index 0000000..a6987ed --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsCallbackController.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.system.controller.admin.sms; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.framework.sms.core.enums.SmsChannelEnum; +import com.yunxi.scm.module.system.service.sms.SmsSendService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletRequest; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 短信回调") +@RestController +@RequestMapping("/system/sms/callback") +public class SmsCallbackController { + + @Resource + private SmsSendService smsSendService; + + @PostMapping("/aliyun") + @PermitAll + @Operation(summary = "阿里云短信的回调", description = "参见 https://help.aliyun.com/document_detail/120998.html 文档") + @OperateLog(enable = false) + public CommonResult receiveAliyunSmsStatus(HttpServletRequest request) throws Throwable { + String text = ServletUtils.getBody(request); + smsSendService.receiveSmsStatus(SmsChannelEnum.ALIYUN.getCode(), text); + return success(true); + } + + @PostMapping("/tencent") + @PermitAll + @Operation(summary = "腾讯云短信的回调", description = "参见 https://cloud.tencent.com/document/product/382/52077 文档") + @OperateLog(enable = false) + public CommonResult receiveTencentSmsStatus(HttpServletRequest request) throws Throwable { + String text = ServletUtils.getBody(request); + smsSendService.receiveSmsStatus(SmsChannelEnum.TENCENT.getCode(), text); + return success(true); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsChannelController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsChannelController.java new file mode 100644 index 0000000..21e5aa4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsChannelController.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.system.controller.admin.sms; + +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.*; +import com.yunxi.scm.module.system.convert.sms.SmsChannelConvert; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import com.yunxi.scm.module.system.service.sms.SmsChannelService; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.Comparator; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 短信渠道") +@RestController +@RequestMapping("system/sms-channel") +public class SmsChannelController { + + @Resource + private SmsChannelService smsChannelService; + + @PostMapping("/create") + @Operation(summary = "创建短信渠道") + @PreAuthorize("@ss.hasPermission('system:sms-channel:create')") + public CommonResult createSmsChannel(@Valid @RequestBody SmsChannelCreateReqVO createReqVO) { + return success(smsChannelService.createSmsChannel(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新短信渠道") + @PreAuthorize("@ss.hasPermission('system:sms-channel:update')") + public CommonResult updateSmsChannel(@Valid @RequestBody SmsChannelUpdateReqVO updateReqVO) { + smsChannelService.updateSmsChannel(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除短信渠道") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:sms-channel:delete')") + public CommonResult deleteSmsChannel(@RequestParam("id") Long id) { + smsChannelService.deleteSmsChannel(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得短信渠道") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult getSmsChannel(@RequestParam("id") Long id) { + SmsChannelDO smsChannel = smsChannelService.getSmsChannel(id); + return success(SmsChannelConvert.INSTANCE.convert(smsChannel)); + } + + @GetMapping("/page") + @Operation(summary = "获得短信渠道分页") + @PreAuthorize("@ss.hasPermission('system:sms-channel:query')") + public CommonResult> getSmsChannelPage(@Valid SmsChannelPageReqVO pageVO) { + PageResult pageResult = smsChannelService.getSmsChannelPage(pageVO); + return success(SmsChannelConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获得短信渠道精简列表", description = "包含被禁用的短信渠道") + public CommonResult> getSimpleSmsChannelList() { + List list = smsChannelService.getSmsChannelList(); + // 排序后,返回给前端 + list.sort(Comparator.comparing(SmsChannelDO::getId)); + return success(SmsChannelConvert.INSTANCE.convertList03(list)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsLogController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsLogController.java new file mode 100644 index 0000000..5e06945 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsLogController.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.system.controller.admin.sms; + +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogExcelVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogRespVO; +import com.yunxi.scm.module.system.convert.sms.SmsLogConvert; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsLogDO; +import com.yunxi.scm.module.system.service.sms.SmsLogService; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 短信日志") +@RestController +@RequestMapping("/system/sms-log") +@Validated +public class SmsLogController { + + @Resource + private SmsLogService smsLogService; + + @GetMapping("/page") + @Operation(summary = "获得短信日志分页") + @PreAuthorize("@ss.hasPermission('system:sms-log:query')") + public CommonResult> getSmsLogPage(@Valid SmsLogPageReqVO pageVO) { + PageResult pageResult = smsLogService.getSmsLogPage(pageVO); + return success(SmsLogConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出短信日志 Excel") + @PreAuthorize("@ss.hasPermission('system:sms-log:export')") + @OperateLog(type = EXPORT) + public void exportSmsLogExcel(@Valid SmsLogExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = smsLogService.getSmsLogList(exportReqVO); + // 导出 Excel + List datas = SmsLogConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "短信日志.xls", "数据", SmsLogExcelVO.class, datas); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsTemplateController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsTemplateController.http new file mode 100644 index 0000000..e8213e5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsTemplateController.http @@ -0,0 +1,14 @@ +### 请求 /system/sms-template/send-sms 接口 => 成功 +POST {{baseUrl}}/system/sms-template/send-sms +Authorization: Bearer {{token}} +Content-Type: application/json +tenant-id: {{adminTenentId}} + +{ + "templateCode": "test_01", + "mobile": "156016913900", + "params": { + "key01": "value01", + "key02": "value02" + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsTemplateController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsTemplateController.java new file mode 100644 index 0000000..1d9bcc5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/SmsTemplateController.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.system.controller.admin.sms; + +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.*; +import com.yunxi.scm.module.system.convert.sms.SmsTemplateConvert; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.module.system.service.sms.SmsTemplateService; +import com.yunxi.scm.module.system.service.sms.SmsSendService; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 短信模板") +@RestController +@RequestMapping("/system/sms-template") +public class SmsTemplateController { + + @Resource + private SmsTemplateService smsTemplateService; + @Resource + private SmsSendService smsSendService; + + @PostMapping("/create") + @Operation(summary = "创建短信模板") + @PreAuthorize("@ss.hasPermission('system:sms-template:create')") + public CommonResult createSmsTemplate(@Valid @RequestBody SmsTemplateCreateReqVO createReqVO) { + return success(smsTemplateService.createSmsTemplate(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新短信模板") + @PreAuthorize("@ss.hasPermission('system:sms-template:update')") + public CommonResult updateSmsTemplate(@Valid @RequestBody SmsTemplateUpdateReqVO updateReqVO) { + smsTemplateService.updateSmsTemplate(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除短信模板") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:sms-template:delete')") + public CommonResult deleteSmsTemplate(@RequestParam("id") Long id) { + smsTemplateService.deleteSmsTemplate(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得短信模板") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:sms-template:query')") + public CommonResult getSmsTemplate(@RequestParam("id") Long id) { + SmsTemplateDO smsTemplate = smsTemplateService.getSmsTemplate(id); + return success(SmsTemplateConvert.INSTANCE.convert(smsTemplate)); + } + + @GetMapping("/page") + @Operation(summary = "获得短信模板分页") + @PreAuthorize("@ss.hasPermission('system:sms-template:query')") + public CommonResult> getSmsTemplatePage(@Valid SmsTemplatePageReqVO pageVO) { + PageResult pageResult = smsTemplateService.getSmsTemplatePage(pageVO); + return success(SmsTemplateConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出短信模板 Excel") + @PreAuthorize("@ss.hasPermission('system:sms-template:export')") + @OperateLog(type = EXPORT) + public void exportSmsTemplateExcel(@Valid SmsTemplateExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = smsTemplateService.getSmsTemplateList(exportReqVO); + // 导出 Excel + List datas = SmsTemplateConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "短信模板.xls", "数据", SmsTemplateExcelVO.class, datas); + } + + @PostMapping("/send-sms") + @Operation(summary = "发送短信") + @PreAuthorize("@ss.hasPermission('system:sms-template:send-sms')") + public CommonResult sendSms(@Valid @RequestBody SmsTemplateSendReqVO sendReqVO) { + return success(smsSendService.sendSingleSmsToAdmin(sendReqVO.getMobile(), null, + sendReqVO.getTemplateCode(), sendReqVO.getTemplateParams())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelBaseVO.java new file mode 100644 index 0000000..56bddf6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelBaseVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.channel; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotNull; + +/** +* 短信渠道 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class SmsChannelBaseVO { + + @Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + @NotNull(message = "短信签名不能为空") + private String signature; + + @Schema(description = "启用状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "启用状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "好吃!") + private String remark; + + @Schema(description = "短信 API 的账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi") + @NotNull(message = "短信 API 的账号不能为空") + private String apiKey; + + @Schema(description = "短信 API 的密钥", example = "yuanma") + private String apiSecret; + + @Schema(description = "短信发送回调 URL", example = "http://www.iocoder.cn") + @URL(message = "回调 URL 格式不正确") + private String callbackUrl; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelCreateReqVO.java new file mode 100644 index 0000000..5f42d40 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 短信渠道创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelCreateReqVO extends SmsChannelBaseVO { + + @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") + @NotNull(message = "渠道编码不能为空") + private String code; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java new file mode 100644 index 0000000..3a49b04 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelPageReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.channel; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 短信渠道分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelPageReqVO extends PageParam { + + @Schema(description = "任务状态", example = "1") + private Integer status; + + @Schema(description = "短信签名,模糊匹配", example = "芋道源码") + private String signature; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java new file mode 100644 index 0000000..075caa7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelRespVO.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 短信渠道 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelRespVO extends SmsChannelBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") + private String code; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java new file mode 100644 index 0000000..658ce02 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelSimpleRespVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 短信渠道精简 Response VO") +@Data +public class SmsChannelSimpleRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + + @Schema(description = "短信签名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码") + @NotNull(message = "短信签名不能为空") + private String signature; + + @Schema(description = "渠道编码,参见 SmsChannelEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "YUN_PIAN") + private String code; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelUpdateReqVO.java new file mode 100644 index 0000000..a378c9a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/channel/SmsChannelUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.channel; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 短信渠道更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelUpdateReqVO extends SmsChannelBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogExcelVO.java new file mode 100644 index 0000000..cd2f000 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogExcelVO.java @@ -0,0 +1,100 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.log; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.framework.excel.core.convert.JsonConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 短信日志 Excel VO + * + * @author 芋道源码 + */ +@Data +public class SmsLogExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty("短信渠道编号") + private Long channelId; + + @ExcelProperty("短信渠道编码") + private String channelCode; + + @ExcelProperty("模板编号") + private Long templateId; + + @ExcelProperty("模板编码") + private String templateCode; + + @ExcelProperty(value = "短信类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_TEMPLATE_TYPE) + private Integer templateType; + + @ExcelProperty("短信内容") + private String templateContent; + + @ExcelProperty(value = "短信参数", converter = JsonConvert.class) + private Map templateParams; + + @ExcelProperty("短信 API 的模板编号") + private String apiTemplateId; + + @ExcelProperty("手机号") + private String mobile; + + @ExcelProperty("用户编号") + private Long userId; + + @ExcelProperty(value = "用户类型", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_TYPE) + private Integer userType; + + @ExcelProperty(value = "发送状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_SEND_STATUS) + private Integer sendStatus; + + @ExcelProperty("发送时间") + private LocalDateTime sendTime; + + @ExcelProperty("发送结果的编码") + private Integer sendCode; + + @ExcelProperty("发送结果的提示") + private String sendMsg; + + @ExcelProperty("短信 API 发送结果的编码") + private String apiSendCode; + + @ExcelProperty("短信 API 发送失败的提示") + private String apiSendMsg; + + @ExcelProperty("短信 API 发送返回的唯一请求 ID") + private String apiRequestId; + + @ExcelProperty("短信 API 发送返回的序号") + private String apiSerialNo; + + @ExcelProperty(value = "接收状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_RECEIVE_STATUS) + private Integer receiveStatus; + + @ExcelProperty("接收时间") + private LocalDateTime receiveTime; + + @ExcelProperty("API 接收结果的编码") + private String apiReceiveCode; + + @ExcelProperty("API 接收结果的说明") + private String apiReceiveMsg; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogExportReqVO.java new file mode 100644 index 0000000..b42e0ea --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogExportReqVO.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 短信日志 Excel 导出 Request VO,参数和 SmsLogPageReqVO 是一致的") +@Data +public class SmsLogExportReqVO { + + @Schema(description = "短信渠道编号", example = "10") + private Long channelId; + + @Schema(description = "模板编号", example = "20") + private Long templateId; + + @Schema(description = "手机号", example = "15601691300") + private String mobile; + + @Schema(description = "发送状态", example = "1") + private Integer sendStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "开始发送时间") + private LocalDateTime[] sendTime; + + @Schema(description = "接收状态", example = "0") + private Integer receiveStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "开始接收时间") + private LocalDateTime[] receiveTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java new file mode 100644 index 0000000..7f4fadf --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogPageReqVO.java @@ -0,0 +1,43 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.log; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 短信日志分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsLogPageReqVO extends PageParam { + + @Schema(description = "短信渠道编号", example = "10") + private Long channelId; + + @Schema(description = "模板编号", example = "20") + private Long templateId; + + @Schema(description = "手机号", example = "15601691300") + private String mobile; + + @Schema(description = "发送状态,参见 SmsSendStatusEnum 枚举类", example = "1") + private Integer sendStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "发送时间") + private LocalDateTime[] sendTime; + + @Schema(description = "接收状态,参见 SmsReceiveStatusEnum 枚举类", example = "0") + private Integer receiveStatus; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "接收时间") + private LocalDateTime[] receiveTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java new file mode 100644 index 0000000..3cddade --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java @@ -0,0 +1,88 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.log; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.Map; + +@Schema(description = "管理后台 - 短信日志 Response VO") +@Data +public class SmsLogRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "短信渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Long channelId; + + @Schema(description = "短信渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ALIYUN") + private String channelCode; + + @Schema(description = "模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "20") + private Long templateId; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test-01") + private String templateCode; + + @Schema(description = "短信类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer templateType; + + @Schema(description = "短信内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,你的验证码是 1024") + private String templateContent; + + @Schema(description = "短信参数", requiredMode = Schema.RequiredMode.REQUIRED, example = "name,code") + private Map templateParams; + + @Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "SMS_207945135") + private String apiTemplateId; + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + private String mobile; + + @Schema(description = "用户编号", example = "10") + private Long userId; + + @Schema(description = "用户类型", example = "1") + private Integer userType; + + @Schema(description = "发送状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer sendStatus; + + @Schema(description = "发送时间") + private LocalDateTime sendTime; + + @Schema(description = "发送结果的编码", example = "0") + private Integer sendCode; + + @Schema(description = "发送结果的提示", example = "成功") + private String sendMsg; + + @Schema(description = "短信 API 发送结果的编码", example = "SUCCESS") + private String apiSendCode; + + @Schema(description = "短信 API 发送失败的提示", example = "成功") + private String apiSendMsg; + + @Schema(description = "短信 API 发送返回的唯一请求 ID", example = "3837C6D3-B96F-428C-BBB2-86135D4B5B99") + private String apiRequestId; + + @Schema(description = "短信 API 发送返回的序号", example = "62923244790") + private String apiSerialNo; + + @Schema(description = "接收状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0") + private Integer receiveStatus; + + @Schema(description = "接收时间") + private LocalDateTime receiveTime; + + @Schema(description = "API 接收结果的编码", example = "DELIVRD") + private String apiReceiveCode; + + @Schema(description = "API 接收结果的说明", example = "用户接收成功") + private String apiReceiveMsg; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateBaseVO.java new file mode 100644 index 0000000..f87a2af --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateBaseVO.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.template; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** +* 短信模板 Base VO,提供给添加、修改、详细的子 VO 使用 +* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 +*/ +@Data +public class SmsTemplateBaseVO { + + @Schema(description = "短信类型,参见 SmsTemplateTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "短信类型不能为空") + private Integer type; + + @Schema(description = "开启状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "开启状态不能为空") + private Integer status; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String code; + + @Schema(description = "模板名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi") + @NotNull(message = "模板名称不能为空") + private String name; + + @Schema(description = "模板内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "你好,{name}。你长的太{like}啦!") + @NotNull(message = "模板内容不能为空") + private String content; + + @Schema(description = "备注", example = "哈哈哈") + private String remark; + + @Schema(description = "短信 API 的模板编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "4383920") + @NotNull(message = "短信 API 的模板编号不能为空") + private String apiTemplateId; + + @Schema(description = "短信渠道编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @NotNull(message = "短信渠道编号不能为空") + private Long channelId; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateCreateReqVO.java new file mode 100644 index 0000000..eec23e9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateCreateReqVO.java @@ -0,0 +1,13 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.template; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 短信模板创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateCreateReqVO extends SmsTemplateBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateExcelVO.java new file mode 100644 index 0000000..0e9b4dd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateExcelVO.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.template; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 短信模板 Excel VO + * + * @author 芋道源码 + */ +@Data +public class SmsTemplateExcelVO { + + @ExcelProperty("编号") + private Long id; + + @ExcelProperty(value = "短信签名", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_TEMPLATE_TYPE) + private Integer type; + + @ExcelProperty(value = "开启状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("模板编码") + private String code; + + @ExcelProperty("模板名称") + private String name; + + @ExcelProperty("模板内容") + private String content; + + @ExcelProperty("备注") + private String remark; + + @ExcelProperty("短信 API 的模板编号") + private String apiTemplateId; + + @ExcelProperty("短信渠道编号") + private Long channelId; + + @ExcelProperty(value = "短信渠道编码", converter = DictConvert.class) + @DictFormat(DictTypeConstants.SMS_CHANNEL_CODE) + private String channelCode; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateExportReqVO.java new file mode 100644 index 0000000..84504e5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateExportReqVO.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 短信模板 Excel 导出 Request VO,参数和 SmsTemplatePageReqVO 是一致的") +@Data +public class SmsTemplateExportReqVO { + + @Schema(description = "短信签名", example = "1") + private Integer type; + + @Schema(description = "开启状态", example = "1") + private Integer status; + + @Schema(description = "模板编码,模糊匹配", example = "test_01") + private String code; + + @Schema(description = "模板内容,模糊匹配", example = "你好,{name}。你长的太{like}啦!") + private String content; + + @Schema(description = "短信 API 的模板编号,模糊匹配", example = "4383920") + private String apiTemplateId; + + @Schema(description = "短信渠道编号", example = "10") + private Long channelId; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java new file mode 100644 index 0000000..5dccb9d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplatePageReqVO.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.template; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 短信模板分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplatePageReqVO extends PageParam { + + @Schema(description = "短信签名", example = "1") + private Integer type; + + @Schema(description = "开启状态", example = "1") + private Integer status; + + @Schema(description = "模板编码,模糊匹配", example = "test_01") + private String code; + + @Schema(description = "模板内容,模糊匹配", example = "你好,{name}。你长的太{like}啦!") + private String content; + + @Schema(description = "短信 API 的模板编号,模糊匹配", example = "4383920") + private String apiTemplateId; + + @Schema(description = "短信渠道编号", example = "10") + private Long channelId; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java new file mode 100644 index 0000000..d6edc7a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; +import java.util.List; + +@Schema(description = "管理后台 - 短信模板 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateRespVO extends SmsTemplateBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "短信渠道编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "ALIYUN") + private String channelCode; + + @Schema(description = "参数数组", example = "name,code") + private List params; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java new file mode 100644 index 0000000..aad925e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateSendReqVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Map; + +@Schema(description = "管理后台 - 短信模板的发送 Request VO") +@Data +public class SmsTemplateSendReqVO { + + @Schema(description = "手机号", requiredMode = Schema.RequiredMode.REQUIRED, example = "15601691300") + @NotNull(message = "手机号不能为空") + private String mobile; + + @Schema(description = "模板编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "test_01") + @NotNull(message = "模板编码不能为空") + private String templateCode; + + @Schema(description = "模板参数") + private Map templateParams; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateUpdateReqVO.java new file mode 100644 index 0000000..13d5193 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/sms/vo/template/SmsTemplateUpdateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.sms.vo.template; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 短信模板更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateUpdateReqVO extends SmsTemplateBaseVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/SocialUserController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/SocialUserController.java new file mode 100644 index 0000000..de7a580 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/SocialUserController.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.controller.admin.socail; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.system.controller.admin.socail.vo.SocialUserBindReqVO; +import com.yunxi.scm.module.system.controller.admin.socail.vo.SocialUserUnbindReqVO; +import com.yunxi.scm.module.system.convert.social.SocialUserConvert; +import com.yunxi.scm.module.system.service.social.SocialUserService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; + +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; + +@Tag(name = "管理后台 - 社交用户") +@RestController +@RequestMapping("/system/social-user") +@Validated +public class SocialUserController { + + @Resource + private SocialUserService socialUserService; + + @PostMapping("/bind") + @Operation(summary = "社交绑定,使用 code 授权码") + public CommonResult socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { + socialUserService.bindSocialUser(SocialUserConvert.INSTANCE.convert(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO)); + return CommonResult.success(true); + } + + @DeleteMapping("/unbind") + @Operation(summary = "取消社交绑定") + public CommonResult socialUnbind(@RequestBody SocialUserUnbindReqVO reqVO) { + socialUserService.unbindSocialUser(getLoginUserId(), UserTypeEnum.ADMIN.getValue(), reqVO.getType(), reqVO.getOpenid()); + return CommonResult.success(true); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/vo/SocialUserBindReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/vo/SocialUserBindReqVO.java new file mode 100644 index 0000000..f2037bc --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/vo/SocialUserBindReqVO.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.controller.admin.socail.vo; + +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 社交绑定 Request VO,使用 code 授权码") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SocialUserBindReqVO { + + @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "授权码", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotEmpty(message = "授权码不能为空") + private String code; + + @Schema(description = "state", requiredMode = Schema.RequiredMode.REQUIRED, example = "9b2ffbc1-7425-4155-9894-9d5c08541d62") + @NotEmpty(message = "state 不能为空") + private String state; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/vo/SocialUserUnbindReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/vo/SocialUserUnbindReqVO.java new file mode 100644 index 0000000..d0eca23 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/socail/vo/SocialUserUnbindReqVO.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.controller.admin.socail.vo; + +import com.yunxi.scm.framework.common.validation.InEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 取消社交绑定 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class SocialUserUnbindReqVO { + + @Schema(description = "社交平台的类型,参见 UserSocialTypeEnum 枚举值", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + @InEnum(SocialTypeEnum.class) + @NotNull(message = "社交平台的类型不能为空") + private Integer type; + + @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + @NotEmpty(message = "社交用户的 openid 不能为空") + private String openid; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantController.http new file mode 100644 index 0000000..a4d5173 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantController.http @@ -0,0 +1,21 @@ +### 获取租户编号 /admin-api/system/get-id-by-name +GET {{baseUrl}}/system/tenant/get-id-by-name?name=芋道源码 + +### 创建租户 /admin-api/system/tenant/create +POST {{baseUrl}}/system/tenant/create +Content-Type: application/json +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} + +{ + "name": "芋道", + "contactName": "芋艿", + "contactMobile": "15601691300", + "status": 0, + "domain": "https://www.iocoder.cn", + "packageId": 110, + "expireTime": 1699545600000, + "accountCount": 20, + "username": "admin", + "password": "123321" +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantController.java new file mode 100644 index 0000000..ca05824 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantController.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.system.controller.admin.tenant; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.*; +import com.yunxi.scm.module.system.convert.tenant.TenantConvert; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import com.yunxi.scm.module.system.service.tenant.TenantService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.annotation.security.PermitAll; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 租户") +@RestController +@RequestMapping("/system/tenant") +public class TenantController { + + @Resource + private TenantService tenantService; + + @GetMapping("/get-id-by-name") + @PermitAll + @Operation(summary = "使用租户名,获得租户编号", description = "登录界面,根据用户的租户名,获得租户编号") + @Parameter(name = "name", description = "租户名", required = true, example = "1024") + public CommonResult getTenantIdByName(@RequestParam("name") String name) { + TenantDO tenantDO = tenantService.getTenantByName(name); + return success(tenantDO != null ? tenantDO.getId() : null); + } + + @PostMapping("/create") + @Operation(summary = "创建租户") + @PreAuthorize("@ss.hasPermission('system:tenant:create')") + public CommonResult createTenant(@Valid @RequestBody TenantCreateReqVO createReqVO) { + return success(tenantService.createTenant(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新租户") + @PreAuthorize("@ss.hasPermission('system:tenant:update')") + public CommonResult updateTenant(@Valid @RequestBody TenantUpdateReqVO updateReqVO) { + tenantService.updateTenant(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除租户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:tenant:delete')") + public CommonResult deleteTenant(@RequestParam("id") Long id) { + tenantService.deleteTenant(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得租户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:tenant:query')") + public CommonResult getTenant(@RequestParam("id") Long id) { + TenantDO tenant = tenantService.getTenant(id); + return success(TenantConvert.INSTANCE.convert(tenant)); + } + + @GetMapping("/page") + @Operation(summary = "获得租户分页") + @PreAuthorize("@ss.hasPermission('system:tenant:query')") + public CommonResult> getTenantPage(@Valid TenantPageReqVO pageVO) { + PageResult pageResult = tenantService.getTenantPage(pageVO); + return success(TenantConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/export-excel") + @Operation(summary = "导出租户 Excel") + @PreAuthorize("@ss.hasPermission('system:tenant:export')") + @OperateLog(type = EXPORT) + public void exportTenantExcel(@Valid TenantExportReqVO exportReqVO, + HttpServletResponse response) throws IOException { + List list = tenantService.getTenantList(exportReqVO); + // 导出 Excel + List datas = TenantConvert.INSTANCE.convertList02(list); + ExcelUtils.write(response, "租户.xls", "数据", TenantExcelVO.class, datas); + } + + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantPackageController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantPackageController.java new file mode 100644 index 0000000..62f704d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/TenantPackageController.java @@ -0,0 +1,81 @@ +package com.yunxi.scm.module.system.controller.admin.tenant; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.*; +import com.yunxi.scm.module.system.convert.tenant.TenantPackageConvert; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantPackageDO; +import com.yunxi.scm.module.system.service.tenant.TenantPackageService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "管理后台 - 租户套餐") +@RestController +@RequestMapping("/system/tenant-package") +@Validated +public class TenantPackageController { + + @Resource + private TenantPackageService tenantPackageService; + + @PostMapping("/create") + @Operation(summary = "创建租户套餐") + @PreAuthorize("@ss.hasPermission('system:tenant-package:create')") + public CommonResult createTenantPackage(@Valid @RequestBody TenantPackageCreateReqVO createReqVO) { + return success(tenantPackageService.createTenantPackage(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "更新租户套餐") + @PreAuthorize("@ss.hasPermission('system:tenant-package:update')") + public CommonResult updateTenantPackage(@Valid @RequestBody TenantPackageUpdateReqVO updateReqVO) { + tenantPackageService.updateTenantPackage(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除租户套餐") + @Parameter(name = "id", description = "编号", required = true) + @PreAuthorize("@ss.hasPermission('system:tenant-package:delete')") + public CommonResult deleteTenantPackage(@RequestParam("id") Long id) { + tenantPackageService.deleteTenantPackage(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得租户套餐") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:tenant-package:query')") + public CommonResult getTenantPackage(@RequestParam("id") Long id) { + TenantPackageDO tenantPackage = tenantPackageService.getTenantPackage(id); + return success(TenantPackageConvert.INSTANCE.convert(tenantPackage)); + } + + @GetMapping("/page") + @Operation(summary = "获得租户套餐分页") + @PreAuthorize("@ss.hasPermission('system:tenant-package:query')") + public CommonResult> getTenantPackagePage(@Valid TenantPackagePageReqVO pageVO) { + PageResult pageResult = tenantPackageService.getTenantPackagePage(pageVO); + return success(TenantPackageConvert.INSTANCE.convertPage(pageResult)); + } + + @GetMapping("/get-simple-list") + @Operation(summary = "获取租户套餐精简信息列表", description = "只包含被开启的租户套餐,主要用于前端的下拉选项") + public CommonResult> getTenantPackageList() { + // 获得角色列表,只要开启状态的 + List list = tenantPackageService.getTenantPackageListByStatus(CommonStatusEnum.ENABLE.getStatus()); + return success(TenantPackageConvert.INSTANCE.convertList02(list)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageBaseVO.java new file mode 100644 index 0000000..7c0c480 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageBaseVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.Set; + +/** + * 租户套餐 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class TenantPackageBaseVO { + + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") + @NotNull(message = "套餐名不能为空") + private String name; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + private Integer status; + + @Schema(description = "备注", example = "好") + private String remark; + + @Schema(description = "关联的菜单编号", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "关联的菜单编号不能为空") + private Set menuIds; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageCreateReqVO.java new file mode 100644 index 0000000..f0d3311 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageCreateReqVO.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +@Schema(description = "管理后台 - 租户套餐创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackageCreateReqVO extends TenantPackageBaseVO { + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java new file mode 100644 index 0000000..0dd8635 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackagePageReqVO.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.packages; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 租户套餐分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackagePageReqVO extends PageParam { + + @Schema(description = "套餐名", example = "VIP") + private String name; + + @Schema(description = "状态", example = "1") + private Integer status; + + @Schema(description = "备注", example = "好") + private String remark; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java new file mode 100644 index 0000000..49d13cd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageRespVO.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 租户套餐 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackageRespVO extends TenantPackageBaseVO { + + @Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java new file mode 100644 index 0000000..a11ed9b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageSimpleRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 租户套餐精简 Response VO") +@Data +public class TenantPackageSimpleRespVO { + + @Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "套餐编号不能为空") + private Long id; + + @Schema(description = "套餐名", requiredMode = Schema.RequiredMode.REQUIRED, example = "VIP") + @NotNull(message = "套餐名不能为空") + private String name; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageUpdateReqVO.java new file mode 100644 index 0000000..2780b53 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/packages/TenantPackageUpdateReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.packages; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 租户套餐更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPackageUpdateReqVO extends TenantPackageBaseVO { + + @Schema(description = "套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "套餐编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantBaseVO.java new file mode 100644 index 0000000..3696256 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantBaseVO.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import javax.validation.constraints.*; +import java.time.LocalDateTime; + +/** + * 租户 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class TenantBaseVO { + + @Schema(description = "租户名", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + @NotNull(message = "租户名不能为空") + private String name; + + @Schema(description = "联系人", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @NotNull(message = "联系人不能为空") + private String contactName; + + @Schema(description = "联系手机", example = "15601691300") + private String contactMobile; + + @Schema(description = "租户状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "租户状态") + private Integer status; + + @Schema(description = "绑定域名", example = "https://www.iocoder.cn") + private String domain; + + @Schema(description = "租户套餐编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "租户套餐编号不能为空") + private Long packageId; + + @Schema(description = "过期时间", requiredMode = Schema.RequiredMode.REQUIRED) + @NotNull(message = "过期时间不能为空") + private LocalDateTime expireTime; + + @Schema(description = "账号数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "账号数量不能为空") + private Integer accountCount; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantCreateReqVO.java new file mode 100644 index 0000000..6e92978 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantCreateReqVO.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; + +@Schema(description = "管理后台 - 租户创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantCreateReqVO extends TenantBaseVO { + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi") + @NotBlank(message = "用户账号不能为空") + @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") + @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") + private String username; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantExcelVO.java new file mode 100644 index 0000000..9ab13c7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantExcelVO.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant; + +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import lombok.*; +import java.time.LocalDateTime; + +import com.alibaba.excel.annotation.ExcelProperty; +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; + + +/** + * 租户 Excel VO + * + * @author 芋道源码 + */ +@Data +public class TenantExcelVO { + + @ExcelProperty("租户编号") + private Long id; + + @ExcelProperty("租户名") + private String name; + + @ExcelProperty("联系人") + private String contactName; + + @ExcelProperty("联系手机") + private String contactMobile; + + @ExcelProperty(value = "状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("创建时间") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantExportReqVO.java new file mode 100644 index 0000000..303712e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantExportReqVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 租户 Excel 导出 Request VO,参数和 TenantPageReqVO 是一致的") +@Data +public class TenantExportReqVO { + + @Schema(description = "租户名", example = "芋道") + private String name; + + @Schema(description = "联系人", example = "芋艿") + private String contactName; + + @Schema(description = "联系手机", example = "15601691300") + private String contactMobile; + + @Schema(description = "租户状态(0正常 1停用)", example = "1") + private Integer status; + + @Schema(description = "创建时间") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java new file mode 100644 index 0000000..0a37536 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantPageReqVO.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 租户分页 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantPageReqVO extends PageParam { + + @Schema(description = "租户名", example = "芋道") + private String name; + + @Schema(description = "联系人", example = "芋艿") + private String contactName; + + @Schema(description = "联系手机", example = "15601691300") + private String contactMobile; + + @Schema(description = "租户状态(0正常 1停用)", example = "1") + private Integer status; + + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + @Schema(description = "创建时间") + private LocalDateTime[] createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java new file mode 100644 index 0000000..88ab5ee --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + +@Schema(description = "管理后台 - 租户 Response VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantRespVO extends TenantBaseVO { + + @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantUpdateReqVO.java new file mode 100644 index 0000000..a116a81 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/tenant/vo/tenant/TenantUpdateReqVO.java @@ -0,0 +1,17 @@ +package com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import javax.validation.constraints.*; + +@Schema(description = "管理后台 - 租户更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class TenantUpdateReqVO extends TenantBaseVO { + + @Schema(description = "租户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "租户编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserController.http new file mode 100644 index 0000000..6d9cea8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserController.http @@ -0,0 +1,4 @@ +### 请求 /system/user/page 接口 => 没有权限 +GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserController.java new file mode 100644 index 0000000..e852794 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserController.java @@ -0,0 +1,192 @@ +package com.yunxi.scm.module.system.controller.admin.user; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.module.system.controller.admin.user.vo.user.*; +import com.yunxi.scm.module.system.convert.user.UserConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.service.dept.DeptService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import com.yunxi.scm.module.system.enums.common.SexEnum; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.framework.excel.core.util.ExcelUtils; +import com.yunxi.scm.framework.operatelog.core.annotations.OperateLog; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.Parameters; +import io.swagger.v3.oas.annotations.Operation; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; +import java.io.IOException; +import java.util.*; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum.EXPORT; + +@Tag(name = "管理后台 - 用户") +@RestController +@RequestMapping("/system/user") +@Validated +public class UserController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + + @PostMapping("/create") + @Operation(summary = "新增用户") + @PreAuthorize("@ss.hasPermission('system:user:create')") + public CommonResult createUser(@Valid @RequestBody UserCreateReqVO reqVO) { + Long id = userService.createUser(reqVO); + return success(id); + } + + @PutMapping("update") + @Operation(summary = "修改用户") + @PreAuthorize("@ss.hasPermission('system:user:update')") + public CommonResult updateUser(@Valid @RequestBody UserUpdateReqVO reqVO) { + userService.updateUser(reqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除用户") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:user:delete')") + public CommonResult deleteUser(@RequestParam("id") Long id) { + userService.deleteUser(id); + return success(true); + } + + @PutMapping("/update-password") + @Operation(summary = "重置用户密码") + @PreAuthorize("@ss.hasPermission('system:user:update-password')") + public CommonResult updateUserPassword(@Valid @RequestBody UserUpdatePasswordReqVO reqVO) { + userService.updateUserPassword(reqVO.getId(), reqVO.getPassword()); + return success(true); + } + + @PutMapping("/update-status") + @Operation(summary = "修改用户状态") + @PreAuthorize("@ss.hasPermission('system:user:update')") + public CommonResult updateUserStatus(@Valid @RequestBody UserUpdateStatusReqVO reqVO) { + userService.updateUserStatus(reqVO.getId(), reqVO.getStatus()); + return success(true); + } + + @GetMapping("/page") + @Operation(summary = "获得用户分页列表") + @PreAuthorize("@ss.hasPermission('system:user:list')") + public CommonResult> getUserPage(@Valid UserPageReqVO reqVO) { + // 获得用户分页列表 + PageResult pageResult = userService.getUserPage(reqVO); + if (CollUtil.isEmpty(pageResult.getList())) { + return success(new PageResult<>(pageResult.getTotal())); // 返回空 + } + + // 获得拼接需要的数据 + Collection deptIds = convertList(pageResult.getList(), AdminUserDO::getDeptId); + Map deptMap = deptService.getDeptMap(deptIds); + // 拼接结果返回 + List userList = new ArrayList<>(pageResult.getList().size()); + pageResult.getList().forEach(user -> { + UserPageItemRespVO respVO = UserConvert.INSTANCE.convert(user); + respVO.setDept(UserConvert.INSTANCE.convert(deptMap.get(user.getDeptId()))); + userList.add(respVO); + }); + return success(new PageResult<>(userList, pageResult.getTotal())); + } + + @GetMapping("/list-all-simple") + @Operation(summary = "获取用户精简信息列表", description = "只包含被开启的用户,主要用于前端的下拉选项") + public CommonResult> getSimpleUserList() { + // 获用户列表,只要开启状态的 + List list = userService.getUserListByStatus(CommonStatusEnum.ENABLE.getStatus()); + // 排序后,返回给前端 + return success(UserConvert.INSTANCE.convertList04(list)); + } + + @GetMapping("/get") + @Operation(summary = "获得用户详情") + @Parameter(name = "id", description = "编号", required = true, example = "1024") + @PreAuthorize("@ss.hasPermission('system:user:query')") + public CommonResult getUser(@RequestParam("id") Long id) { + AdminUserDO user = userService.getUser(id); + // 获得部门数据 + DeptDO dept = deptService.getDept(user.getDeptId()); + return success(UserConvert.INSTANCE.convert(user).setDept(UserConvert.INSTANCE.convert(dept))); + } + + @GetMapping("/export") + @Operation(summary = "导出用户") + @PreAuthorize("@ss.hasPermission('system:user:export')") + @OperateLog(type = EXPORT) + public void exportUserList(@Validated UserExportReqVO reqVO, + HttpServletResponse response) throws IOException { + // 获得用户列表 + List users = userService.getUserList(reqVO); + + // 获得拼接需要的数据 + Collection deptIds = convertList(users, AdminUserDO::getDeptId); + Map deptMap = deptService.getDeptMap(deptIds); + Map deptLeaderUserMap = userService.getUserMap( + convertSet(deptMap.values(), DeptDO::getLeaderUserId)); + // 拼接数据 + List excelUsers = new ArrayList<>(users.size()); + users.forEach(user -> { + UserExcelVO excelVO = UserConvert.INSTANCE.convert02(user); + // 设置部门 + MapUtils.findAndThen(deptMap, user.getDeptId(), dept -> { + excelVO.setDeptName(dept.getName()); + // 设置部门负责人的名字 + MapUtils.findAndThen(deptLeaderUserMap, dept.getLeaderUserId(), + deptLeaderUser -> excelVO.setDeptLeaderNickname(deptLeaderUser.getNickname())); + }); + excelUsers.add(excelVO); + }); + + // 输出 + ExcelUtils.write(response, "用户数据.xls", "用户列表", UserExcelVO.class, excelUsers); + } + + @GetMapping("/get-import-template") + @Operation(summary = "获得导入用户模板") + public void importTemplate(HttpServletResponse response) throws IOException { + // 手动创建导出 demo + List list = Arrays.asList( + UserImportExcelVO.builder().username("yunai").deptId(1L).email("yunai@iocoder.cn").mobile("15601691300") + .nickname("芋道").status(CommonStatusEnum.ENABLE.getStatus()).sex(SexEnum.MALE.getSex()).build(), + UserImportExcelVO.builder().username("yuanma").deptId(2L).email("yuanma@iocoder.cn").mobile("15601701300") + .nickname("源码").status(CommonStatusEnum.DISABLE.getStatus()).sex(SexEnum.FEMALE.getSex()).build() + ); + + // 输出 + ExcelUtils.write(response, "用户导入模板.xls", "用户列表", UserImportExcelVO.class, list); + } + + @PostMapping("/import") + @Operation(summary = "导入用户") + @Parameters({ + @Parameter(name = "file", description = "Excel 文件", required = true), + @Parameter(name = "updateSupport", description = "是否支持更新,默认为 false", example = "true") + }) + @PreAuthorize("@ss.hasPermission('system:user:import')") + public CommonResult importExcel(@RequestParam("file") MultipartFile file, + @RequestParam(value = "updateSupport", required = false, defaultValue = "false") Boolean updateSupport) throws Exception { + List list = ExcelUtils.read(file, UserImportExcelVO.class); + return success(userService.importUserList(list, updateSupport)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserProfileController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserProfileController.http new file mode 100644 index 0000000..f06037b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserProfileController.http @@ -0,0 +1,4 @@ +### 请求 /system/user/profile/get 接口 => 没有权限 +GET {{baseUrl}}/system/user/profile/get +Authorization: Bearer {{token}} +tenant-id: {{adminTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserProfileController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserProfileController.java new file mode 100644 index 0000000..c1f811d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/UserProfileController.java @@ -0,0 +1,108 @@ +package com.yunxi.scm.module.system.controller.admin.user; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileRespVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.yunxi.scm.module.system.convert.user.UserConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.service.dept.DeptService; +import com.yunxi.scm.module.system.service.dept.PostService; +import com.yunxi.scm.module.system.service.permission.PermissionService; +import com.yunxi.scm.module.system.service.permission.RoleService; +import com.yunxi.scm.module.system.service.social.SocialUserService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; +import static com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; +import static com.yunxi.scm.module.infra.enums.ErrorCodeConstants.FILE_IS_EMPTY; + +@Tag(name = "管理后台 - 用户个人中心") +@RestController +@RequestMapping("/system/user/profile") +@Validated +@Slf4j +public class UserProfileController { + + @Resource + private AdminUserService userService; + @Resource + private DeptService deptService; + @Resource + private PostService postService; + @Resource + private PermissionService permissionService; + @Resource + private RoleService roleService; + @Resource + private SocialUserService socialService; + + @GetMapping("/get") + @Operation(summary = "获得登录用户信息") + @DataPermission(enable = false) // 关闭数据权限,避免只查看自己时,查询不到部门。 + public CommonResult profile() { + // 获得用户基本信息 + AdminUserDO user = userService.getUser(getLoginUserId()); + UserProfileRespVO resp = UserConvert.INSTANCE.convert03(user); + // 获得用户角色 + List userRoles = roleService.getRoleListFromCache(permissionService.getUserRoleIdListByUserId(user.getId())); + resp.setRoles(UserConvert.INSTANCE.convertList(userRoles)); + // 获得部门信息 + if (user.getDeptId() != null) { + DeptDO dept = deptService.getDept(user.getDeptId()); + resp.setDept(UserConvert.INSTANCE.convert02(dept)); + } + // 获得岗位信息 + if (CollUtil.isNotEmpty(user.getPostIds())) { + List posts = postService.getPostList(user.getPostIds()); + resp.setPosts(UserConvert.INSTANCE.convertList02(posts)); + } + // 获得社交用户信息 + List socialUsers = socialService.getSocialUserList(user.getId(), UserTypeEnum.ADMIN.getValue()); + resp.setSocialUsers(UserConvert.INSTANCE.convertList03(socialUsers)); + return success(resp); + } + + @PutMapping("/update") + @Operation(summary = "修改用户个人信息") + public CommonResult updateUserProfile(@Valid @RequestBody UserProfileUpdateReqVO reqVO) { + userService.updateUserProfile(getLoginUserId(), reqVO); + return success(true); + } + + @PutMapping("/update-password") + @Operation(summary = "修改用户个人密码") + public CommonResult updateUserProfilePassword(@Valid @RequestBody UserProfileUpdatePasswordReqVO reqVO) { + userService.updateUserPassword(getLoginUserId(), reqVO); + return success(true); + } + + @RequestMapping(value = "/update-avatar", method = {RequestMethod.POST, RequestMethod.PUT}) // 解决 uni-app 不支持 Put 上传文件的问题 + @Operation(summary = "上传用户个人头像") + public CommonResult updateUserAvatar(@RequestParam("avatarFile") MultipartFile file) throws Exception { + if (file.isEmpty()) { + throw exception(FILE_IS_EMPTY); + } + String avatar = userService.updateUserAvatar(getLoginUserId(), file.getInputStream()); + return success(avatar); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java new file mode 100644 index 0000000..dad3c36 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileRespVO.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.profile; + +import com.yunxi.scm.module.system.controller.admin.user.vo.user.UserBaseVO; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "管理后台 - 用户个人中心信息 Response VO") +public class UserProfileRespVO extends UserBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "最后登录 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.1") + private String loginIp; + + @Schema(description = "最后登录时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime loginDate; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + + /** + * 所属角色 + */ + private List roles; + + /** + * 所在部门 + */ + private Dept dept; + + /** + * 所属岗位数组 + */ + private List posts; + /** + * 社交用户数组 + */ + private List socialUsers; + + @Schema(description = "角色") + @Data + public static class Role { + + @Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "普通角色") + private String name; + + } + + @Schema(description = "部门") + @Data + public static class Dept { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") + private String name; + + } + + @Schema(description = "岗位") + @Data + public static class Post { + + @Schema(description = "岗位编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "岗位名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "开发") + private String name; + + } + + @Schema(description = "社交用户") + @Data + public static class SocialUser { + + @Schema(description = "社交平台的类型,参见 SocialTypeEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") + private Integer type; + + @Schema(description = "社交用户的 openid", requiredMode = Schema.RequiredMode.REQUIRED, example = "IPRmJ0wvBptiPIlGEZiPewGwiEiE") + private String openid; + + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java new file mode 100644 index 0000000..3071c8a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileUpdatePasswordReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.profile; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 用户个人中心更新密码 Request VO") +@Data +public class UserProfileUpdatePasswordReqVO { + + @Schema(description = "旧密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "旧密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String oldPassword; + + @Schema(description = "新密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "654321") + @NotEmpty(message = "新密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String newPassword; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java new file mode 100644 index 0000000..efd4114 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/profile/UserProfileUpdateReqVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.profile; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.Email; +import javax.validation.constraints.Size; + + +@Schema(description = "管理后台 - 用户个人信息更新 Request VO") +@Data +public class UserProfileUpdateReqVO { + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过 30 个字符") + private String nickname; + + @Schema(description = "用户邮箱", example = "yunxi@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + @Length(min = 11, max = 11, message = "手机号长度必须 11 位") + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserBaseVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserBaseVO.java new file mode 100644 index 0000000..0a28360 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserBaseVO.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import com.yunxi.scm.framework.common.validation.Mobile; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.Email; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.util.Set; + +/** + * 用户 Base VO,提供给添加、修改、详细的子 VO 使用 + * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成 + */ +@Data +public class UserBaseVO { + + @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yunxi") + @NotBlank(message = "用户账号不能为空") + @Pattern(regexp = "^[a-zA-Z0-9]{4,30}$", message = "用户账号由 数字、字母 组成") + @Size(min = 4, max = 30, message = "用户账号长度为 4-30 个字符") + private String username; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿") + @Size(max = 30, message = "用户昵称长度不能超过30个字符") + private String nickname; + + @Schema(description = "备注", example = "我是一个用户") + private String remark; + + @Schema(description = "部门ID", example = "我是一个用户") + private Long deptId; + + @Schema(description = "岗位编号数组", example = "1") + private Set postIds; + + @Schema(description = "用户邮箱", example = "yunxi@iocoder.cn") + @Email(message = "邮箱格式不正确") + @Size(max = 50, message = "邮箱长度不能超过 50 个字符") + private String email; + + @Schema(description = "手机号码", example = "15601691300") + @Mobile + private String mobile; + + @Schema(description = "用户性别,参见 SexEnum 枚举类", example = "1") + private Integer sex; + + @Schema(description = "用户头像", example = "https://www.iocoder.cn/xxx.png") + private String avatar; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserCreateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserCreateReqVO.java new file mode 100644 index 0000000..2d45af9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserCreateReqVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; + +@Schema(description = "管理后台 - 用户创建 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class UserCreateReqVO extends UserBaseVO { + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserExcelVO.java new file mode 100644 index 0000000..49fa369 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserExcelVO.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用户 Excel 导出 VO + */ +@Data +public class UserExcelVO { + + @ExcelProperty("用户编号") + private Long id; + + @ExcelProperty("用户名称") + private String username; + + @ExcelProperty("用户昵称") + private String nickname; + + @ExcelProperty("用户邮箱") + private String email; + + @ExcelProperty("手机号码") + private String mobile; + + @ExcelProperty(value = "用户性别", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_SEX) + private Integer sex; + + @ExcelProperty(value = "帐号状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + + @ExcelProperty("最后登录IP") + private String loginIp; + + @ExcelProperty("最后登录时间") + private LocalDateTime loginDate; + + @ExcelProperty("部门名称") + private String deptName; + + @ExcelProperty("部门负责人") + private String deptLeaderNickname; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserExportReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserExportReqVO.java new file mode 100644 index 0000000..a815a83 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserExportReqVO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 用户导出 Request VO,参数和 UserPageReqVO 是一致的") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserExportReqVO { + + @Schema(description = "用户账号,模糊匹配", example = "yunxi") + private String username; + + @Schema(description = "手机号码,模糊匹配", example = "yunxi") + private String mobile; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00,2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "部门编号,同时筛选子部门", example = "1024") + private Long deptId; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserImportExcelVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserImportExcelVO.java new file mode 100644 index 0000000..244e223 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserImportExcelVO.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import com.yunxi.scm.framework.excel.core.annotations.DictFormat; +import com.yunxi.scm.framework.excel.core.convert.DictConvert; +import com.yunxi.scm.module.system.enums.DictTypeConstants; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +/** + * 用户 Excel 导入 VO + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Accessors(chain = false) // 设置 chain = false,避免用户导入有问题 +public class UserImportExcelVO { + + @ExcelProperty("登录名称") + private String username; + + @ExcelProperty("用户名称") + private String nickname; + + @ExcelProperty("部门编号") + private Long deptId; + + @ExcelProperty("用户邮箱") + private String email; + + @ExcelProperty("手机号码") + private String mobile; + + @ExcelProperty(value = "用户性别", converter = DictConvert.class) + @DictFormat(DictTypeConstants.USER_SEX) + private Integer sex; + + @ExcelProperty(value = "账号状态", converter = DictConvert.class) + @DictFormat(DictTypeConstants.COMMON_STATUS) + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserImportRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserImportRespVO.java new file mode 100644 index 0000000..8a644a5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserImportRespVO.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Schema(description = "管理后台 - 用户导入 Response VO") +@Data +@Builder +public class UserImportRespVO { + + @Schema(description = "创建成功的用户名数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List createUsernames; + + @Schema(description = "更新成功的用户名数组", requiredMode = Schema.RequiredMode.REQUIRED) + private List updateUsernames; + + @Schema(description = "导入失败的用户集合,key 为用户名,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED) + private Map failureUsernames; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserPageItemRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserPageItemRespVO.java new file mode 100644 index 0000000..da02bff --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserPageItemRespVO.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 用户分页时的信息 Response VO,相比用户基本信息来说,会多部门信息") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UserPageItemRespVO extends UserRespVO { + + /** + * 所在部门 + */ + private Dept dept; + + @Schema(description = "部门") + @Data + public static class Dept { + + @Schema(description = "部门编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "研发部") + private String name; + + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserPageReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserPageReqVO.java new file mode 100644 index 0000000..9762b86 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserPageReqVO.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import com.yunxi.scm.framework.common.pojo.PageParam; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDateTime; + +import static com.yunxi.scm.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; + +@Schema(description = "管理后台 - 用户分页 Request VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UserPageReqVO extends PageParam { + + @Schema(description = "用户账号,模糊匹配", example = "yunxi") + private String username; + + @Schema(description = "手机号码,模糊匹配", example = "yunxi") + private String mobile; + + @Schema(description = "展示状态,参见 CommonStatusEnum 枚举类", example = "1") + private Integer status; + + @Schema(description = "创建时间", example = "[2022-07-01 00:00:00, 2022-07-01 23:59:59]") + @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) + private LocalDateTime[] createTime; + + @Schema(description = "部门编号,同时筛选子部门", example = "1024") + private Long deptId; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserRespVO.java new file mode 100644 index 0000000..d37fefc --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserRespVO.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; + +import java.time.LocalDateTime; + + +@Schema(description = "管理后台 - 用户信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class UserRespVO extends UserBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Long id; + + @Schema(description = "状态,参见 CommonStatusEnum 枚举类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + private Integer status; + + @Schema(description = "最后登录 IP", requiredMode = Schema.RequiredMode.REQUIRED, example = "192.168.1.1") + private String loginIp; + + @Schema(description = "最后登录时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime loginDate; + + @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "时间戳格式") + private LocalDateTime createTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java new file mode 100644 index 0000000..375cb05 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserSimpleRespVO.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Schema(description = "管理后台 - 用户精简信息 Response VO") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserSimpleRespVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + private Long id; + + @Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") + private String nickname; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java new file mode 100644 index 0000000..78f0a3b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdatePasswordReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import org.hibernate.validator.constraints.Length; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户更新密码 Request VO") +@Data +public class UserUpdatePasswordReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long id; + + @Schema(description = "密码", requiredMode = Schema.RequiredMode.REQUIRED, example = "123456") + @NotEmpty(message = "密码不能为空") + @Length(min = 4, max = 16, message = "密码长度为 4-16 位") + private String password; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdateReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdateReqVO.java new file mode 100644 index 0000000..0b5b780 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdateReqVO.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户更新 Request VO") +@Data +@EqualsAndHashCode(callSuper = true) +public class UserUpdateReqVO extends UserBaseVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "用户编号不能为空") + private Long id; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java new file mode 100644 index 0000000..fb7d53f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.admin.user.vo.user; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.validation.InEnum; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +@Schema(description = "管理后台 - 用户更新状态 Request VO") +@Data +public class UserUpdateStatusReqVO { + + @Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") + @NotNull(message = "角色编号不能为空") + private Long id; + + @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") + @NotNull(message = "状态不能为空") + @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/dict/AppDictDataController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/dict/AppDictDataController.java new file mode 100644 index 0000000..e62fedd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/dict/AppDictDataController.java @@ -0,0 +1,4 @@ +package com.yunxi.scm.module.system.controller.app.dict; + +public class AppDictDataController { +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/ip/AppAreaController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/ip/AppAreaController.java new file mode 100644 index 0000000..f2fbbbf --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/ip/AppAreaController.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.controller.app.ip; + +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.ip.core.Area; +import com.yunxi.scm.framework.ip.core.utils.AreaUtils; +import com.yunxi.scm.module.system.controller.app.ip.vo.AppAreaNodeRespVO; +import com.yunxi.scm.module.system.convert.ip.AreaConvert; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "用户 App - 地区") +@RestController +@RequestMapping("/system/area") +@Validated +public class AppAreaController { + + @GetMapping("/tree") + @Operation(summary = "获得地区树") + public CommonResult> getAreaTree() { + Area area = AreaUtils.getArea(Area.ID_CHINA); + Assert.notNull(area, "获取不到中国"); + return success(AreaConvert.INSTANCE.convertList3(area.getChildren())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/ip/vo/AppAreaNodeRespVO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/ip/vo/AppAreaNodeRespVO.java new file mode 100644 index 0000000..b168046 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/ip/vo/AppAreaNodeRespVO.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.controller.app.ip.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +@Schema(description = "用户 App - 地区节点 Response VO") +@Data +public class AppAreaNodeRespVO { + + @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "110000") + private Integer id; + + @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "北京") + private String name; + + /** + * 子节点 + */ + private List children; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/package-info.java new file mode 100644 index 0000000..51fdede --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位,避免 package 无法提交到 Git 仓库 + */ +package com.yunxi.scm.module.system.controller.app; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/weixin/AppWxMpController.http b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/weixin/AppWxMpController.http new file mode 100644 index 0000000..6af305a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/weixin/AppWxMpController.http @@ -0,0 +1,4 @@ +### 请求 /login 接口 => 成功 +POST {{appApi}}/system/wx-mp/create-jsapi-signature?url=http://www.iocoder.cn +Authorization: Bearer {{appToken}} +tenant-id: {{appTenentId}} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/weixin/AppWxMpController.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/weixin/AppWxMpController.java new file mode 100644 index 0000000..640f205 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/app/weixin/AppWxMpController.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.system.controller.app.weixin; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import io.swagger.v3.oas.annotations.tags.Tag; +import io.swagger.v3.oas.annotations.Operation; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.mp.api.WxMpService; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.pojo.CommonResult.success; + +@Tag(name = "微信公众号") +@RestController +@RequestMapping("/system/wx-mp") +@Validated +@Slf4j +public class AppWxMpController { + + @Resource + private WxMpService mpService; + + // TODO @芋艿:需要额外考虑个问题;多租户下,如果每个小程序一个微信公众号,则会存在多个 appid; + @PostMapping("/create-jsapi-signature") + @Operation(summary = "创建微信 JS SDK 初始化所需的签名", + description = "参考 https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html 文档") + public CommonResult createJsapiSignature(@RequestParam("url") String url) throws WxErrorException { + return success(mpService.createJsapiSignature(url)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/package-info.java new file mode 100644 index 0000000..d8c6b17 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/controller/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 RESTful API 给前端: + * 1. admin 包:提供给管理后台 yunxi-ui-admin 前端项目 + * 2. app 包:提供给用户 APP yunxi-ui-app 前端项目,它的 Controller 和 VO 都要添加 App 前缀,用于和管理后台进行区分 + */ +package com.yunxi.scm.module.system.controller; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/AuthConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/AuthConvert.java new file mode 100644 index 0000000..6d5bec3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/AuthConvert.java @@ -0,0 +1,83 @@ +package com.yunxi.scm.module.system.convert.auth; + +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.controller.admin.auth.vo.*; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.enums.permission.MenuTypeEnum; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import org.slf4j.LoggerFactory; + +import java.util.*; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.filterList; +import static com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; + +@Mapper +public interface AuthConvert { + + AuthConvert INSTANCE = Mappers.getMapper(AuthConvert.class); + + AuthLoginRespVO convert(OAuth2AccessTokenDO bean); + + default AuthPermissionInfoRespVO convert(AdminUserDO user, List roleList, List menuList) { + return AuthPermissionInfoRespVO.builder() + .user(AuthPermissionInfoRespVO.UserVO.builder().id(user.getId()).nickname(user.getNickname()).avatar(user.getAvatar()).build()) + .roles(convertSet(roleList, RoleDO::getCode)) + // 权限标识信息 + .permissions(convertSet(menuList, MenuDO::getPermission)) + // 菜单树 + .menus(buildMenuTree(menuList)) + .build(); + } + + AuthPermissionInfoRespVO.MenuVO convertTreeNode(MenuDO menu); + + /** + * 将菜单列表,构建成菜单树 + * + * @param menuList 菜单列表 + * @return 菜单树 + */ + default List buildMenuTree(List menuList) { + // 移除按钮 + menuList.removeIf(menu -> menu.getType().equals(MenuTypeEnum.BUTTON.getType())); + // 排序,保证菜单的有序性 + menuList.sort(Comparator.comparing(MenuDO::getSort)); + + // 构建菜单树 + // 使用 LinkedHashMap 的原因,是为了排序 。实际也可以用 Stream API ,就是太丑了。 + Map treeNodeMap = new LinkedHashMap<>(); + menuList.forEach(menu -> treeNodeMap.put(menu.getId(), AuthConvert.INSTANCE.convertTreeNode(menu))); + // 处理父子关系 + treeNodeMap.values().stream().filter(node -> !node.getParentId().equals(ID_ROOT)).forEach(childNode -> { + // 获得父节点 + AuthPermissionInfoRespVO.MenuVO parentNode = treeNodeMap.get(childNode.getParentId()); + if (parentNode == null) { + LoggerFactory.getLogger(getClass()).error("[buildRouterTree][resource({}) 找不到父资源({})]", + childNode.getId(), childNode.getParentId()); + return; + } + // 将自己添加到父节点中 + if (parentNode.getChildren() == null) { + parentNode.setChildren(new ArrayList<>()); + } + parentNode.getChildren().add(childNode); + }); + // 获得到所有的根节点 + return filterList(treeNodeMap.values(), node -> ID_ROOT.equals(node.getParentId())); + } + + SocialUserBindReqDTO convert(Long userId, Integer userType, AuthSocialLoginReqVO reqVO); + + SmsCodeSendReqDTO convert(AuthSmsSendReqVO reqVO); + + SmsCodeUseReqDTO convert(AuthSmsLoginReqVO reqVO, Integer scene, String usedIp); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/OAuth2ClientConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/OAuth2ClientConvert.java new file mode 100644 index 0000000..fb28fac --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/OAuth2ClientConvert.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.convert.auth; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * OAuth2 客户端 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface OAuth2ClientConvert { + + OAuth2ClientConvert INSTANCE = Mappers.getMapper(OAuth2ClientConvert.class); + + OAuth2ClientDO convert(OAuth2ClientCreateReqVO bean); + + OAuth2ClientDO convert(OAuth2ClientUpdateReqVO bean); + + OAuth2ClientRespVO convert(OAuth2ClientDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/OAuth2TokenConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/OAuth2TokenConvert.java new file mode 100644 index 0000000..2effce9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/auth/OAuth2TokenConvert.java @@ -0,0 +1,22 @@ +package com.yunxi.scm.module.system.convert.auth; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO; +import com.yunxi.scm.module.system.api.oauth2.dto.OAuth2AccessTokenRespDTO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenRespVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface OAuth2TokenConvert { + + OAuth2TokenConvert INSTANCE = Mappers.getMapper(OAuth2TokenConvert.class); + + OAuth2AccessTokenCheckRespDTO convert(OAuth2AccessTokenDO bean); + + PageResult convert(PageResult page); + + OAuth2AccessTokenRespDTO convert2(OAuth2AccessTokenDO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dept/DeptConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dept/DeptConvert.java new file mode 100644 index 0000000..bb5df69 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dept/DeptConvert.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.convert.dept; + +import com.yunxi.scm.module.system.api.dept.dto.DeptRespDTO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptRespVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; + +@Mapper +public interface DeptConvert { + + DeptConvert INSTANCE = Mappers.getMapper(DeptConvert.class); + + List convertList(List list); + + List convertList02(List list); + + DeptRespVO convert(DeptDO bean); + + DeptDO convert(DeptCreateReqVO bean); + + DeptDO convert(DeptUpdateReqVO bean); + + List convertList03(List list); + + DeptRespDTO convert03(DeptDO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dept/PostConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dept/PostConvert.java new file mode 100644 index 0000000..be478f7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dept/PostConvert.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.convert.dept; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.*; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface PostConvert { + + PostConvert INSTANCE = Mappers.getMapper(PostConvert.class); + + List convertList02(List list); + + PageResult convertPage(PageResult page); + + PostRespVO convert(PostDO id); + + PostDO convert(PostCreateReqVO bean); + + PostDO convert(PostUpdateReqVO reqVO); + + List convertList03(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dict/DictDataConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dict/DictDataConvert.java new file mode 100644 index 0000000..32304eb --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dict/DictDataConvert.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.system.convert.dict; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.dict.dto.DictDataRespDTO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.*; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictDataDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DictDataConvert { + + DictDataConvert INSTANCE = Mappers.getMapper(DictDataConvert.class); + + List convertList(List list); + + DictDataRespVO convert(DictDataDO bean); + + PageResult convertPage(PageResult page); + + DictDataDO convert(DictDataUpdateReqVO bean); + + DictDataDO convert(DictDataCreateReqVO bean); + + List convertList02(List bean); + + DictDataRespDTO convert02(DictDataDO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dict/DictTypeConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dict/DictTypeConvert.java new file mode 100644 index 0000000..dbff99c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/dict/DictTypeConvert.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.convert.dict; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.*; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictTypeDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface DictTypeConvert { + + DictTypeConvert INSTANCE = Mappers.getMapper(DictTypeConvert.class); + + PageResult convertPage(PageResult bean); + + DictTypeRespVO convert(DictTypeDO bean); + + DictTypeDO convert(DictTypeCreateReqVO bean); + + DictTypeDO convert(DictTypeUpdateReqVO bean); + + List convertList(List list); + + List convertList02(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/errorcode/ErrorCodeConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/errorcode/ErrorCodeConvert.java new file mode 100644 index 0000000..a806dc5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/errorcode/ErrorCodeConvert.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.convert.errorcode; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeExcelVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeRespVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 错误码 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface ErrorCodeConvert { + + ErrorCodeConvert INSTANCE = Mappers.getMapper(ErrorCodeConvert.class); + + ErrorCodeDO convert(ErrorCodeCreateReqVO bean); + + ErrorCodeDO convert(ErrorCodeUpdateReqVO bean); + + ErrorCodeRespVO convert(ErrorCodeDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + ErrorCodeDO convert(ErrorCodeAutoGenerateReqDTO bean); + + List convertList03(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/ip/AreaConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/ip/AreaConvert.java new file mode 100644 index 0000000..4e8904a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/ip/AreaConvert.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.convert.ip; + +import com.yunxi.scm.framework.ip.core.Area; +import com.yunxi.scm.framework.ip.core.enums.AreaTypeEnum; +import com.yunxi.scm.module.system.controller.admin.ip.vo.AreaNodeRespVO; +import com.yunxi.scm.module.system.controller.admin.ip.vo.AreaNodeSimpleRespVO; +import com.yunxi.scm.module.system.controller.app.ip.vo.AppAreaNodeRespVO; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Objects; + +@Mapper +public interface AreaConvert { + + AreaConvert INSTANCE = Mappers.getMapper(AreaConvert.class); + + List convertList(List list); + + List convertList2(List list); + + @Mapping(source = "type", target = "leaf") + AreaNodeSimpleRespVO convert(Area area); + + default Boolean convertAreaType(Integer type) { + return Objects.equals(AreaTypeEnum.DISTRICT.getType(), type); + } + + List convertList3(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/logger/LoginLogConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/logger/LoginLogConvert.java new file mode 100644 index 0000000..494830a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/logger/LoginLogConvert.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.convert.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogExcelVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogRespVO; +import com.yunxi.scm.module.system.dal.dataobject.logger.LoginLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface LoginLogConvert { + + LoginLogConvert INSTANCE = Mappers.getMapper(LoginLogConvert.class); + + PageResult convertPage(PageResult page); + + List convertList(List list); + + LoginLogDO convert(LoginLogCreateReqDTO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/logger/OperateLogConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/logger/OperateLogConvert.java new file mode 100644 index 0000000..027fffe --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/logger/OperateLogConvert.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.system.convert.logger; + +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogExcelVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; +import com.yunxi.scm.module.system.dal.dataobject.logger.OperateLogDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.MapUtils; +import com.yunxi.scm.module.system.api.logger.dto.OperateLogCreateReqDTO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.SUCCESS; + +@Mapper +public interface OperateLogConvert { + + OperateLogConvert INSTANCE = Mappers.getMapper(OperateLogConvert.class); + + OperateLogDO convert(OperateLogCreateReqDTO bean); + + PageResult convertPage(PageResult page); + + OperateLogRespVO convert(OperateLogDO bean); + + default List convertList(List list, Map userMap) { + return list.stream().map(operateLog -> { + OperateLogExcelVO excelVO = convert02(operateLog); + MapUtils.findAndThen(userMap, operateLog.getUserId(), user -> excelVO.setUserNickname(user.getNickname())); + excelVO.setSuccessStr(SUCCESS.getCode().equals(operateLog.getResultCode()) ? "成功" : "失败"); + return excelVO; + }).collect(Collectors.toList()); + } + + OperateLogExcelVO convert02(OperateLogDO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailAccountConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailAccountConvert.java new file mode 100644 index 0000000..ac5eabd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailAccountConvert.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.system.convert.mail; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.mail.MailAccount; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.*; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MailAccountConvert { + + MailAccountConvert INSTANCE = Mappers.getMapper(MailAccountConvert.class); + + MailAccountDO convert(MailAccountCreateReqVO bean); + + MailAccountDO convert(MailAccountUpdateReqVO bean); + + MailAccountRespVO convert(MailAccountDO bean); + + PageResult convertPage(PageResult pageResult); + + List convertList02(List list); + + default MailAccount convert(MailAccountDO account, String nickname) { + String from = StrUtil.isNotEmpty(nickname) ? nickname + " <" + account.getMail() + ">" : account.getMail(); + return new MailAccount().setFrom(from).setAuth(true) + .setUser(account.getUsername()).setPass(account.getPassword()) + .setHost(account.getHost()).setPort(account.getPort()).setSslEnable(account.getSslEnable()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailLogConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailLogConvert.java new file mode 100644 index 0000000..e04ae70 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailLogConvert.java @@ -0,0 +1,18 @@ +package com.yunxi.scm.module.system.convert.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.log.MailLogRespVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailLogDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface MailLogConvert { + + MailLogConvert INSTANCE = Mappers.getMapper(MailLogConvert.class); + + PageResult convertPage(PageResult pageResult); + + MailLogRespVO convert(MailLogDO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailTemplateConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailTemplateConvert.java new file mode 100644 index 0000000..bd674d2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/mail/MailTemplateConvert.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.convert.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.*; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MailTemplateConvert { + + MailTemplateConvert INSTANCE = Mappers.getMapper(MailTemplateConvert.class); + + MailTemplateDO convert(MailTemplateUpdateReqVO bean); + + MailTemplateDO convert(MailTemplateCreateReqVO bean); + + MailTemplateRespVO convert(MailTemplateDO bean); + + PageResult convertPage(PageResult pageResult); + + List convertList02(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notice/NoticeConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notice/NoticeConvert.java new file mode 100644 index 0000000..88ea00e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notice/NoticeConvert.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.convert.notice; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeRespVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notice.NoticeDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface NoticeConvert { + + NoticeConvert INSTANCE = Mappers.getMapper(NoticeConvert.class); + + PageResult convertPage(PageResult page); + + NoticeRespVO convert(NoticeDO bean); + + NoticeDO convert(NoticeUpdateReqVO bean); + + NoticeDO convert(NoticeCreateReqVO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notify/NotifyMessageConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notify/NotifyMessageConvert.java new file mode 100644 index 0000000..5454caa --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notify/NotifyMessageConvert.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.convert.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessageRespVO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyMessageDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 站内信 Convert + * + * @author xrcoder + */ +@Mapper +public interface NotifyMessageConvert { + + NotifyMessageConvert INSTANCE = Mappers.getMapper(NotifyMessageConvert.class); + + NotifyMessageRespVO convert(NotifyMessageDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notify/NotifyTemplateConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notify/NotifyTemplateConvert.java new file mode 100644 index 0000000..bfefe17 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/notify/NotifyTemplateConvert.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.module.system.convert.notify; + +import java.util.*; + +import com.yunxi.scm.framework.common.pojo.PageResult; + +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateRespVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateUpdateReqVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; + +/** + * 站内信模版 Convert + * + * @author xrcoder + */ +@Mapper +public interface NotifyTemplateConvert { + + NotifyTemplateConvert INSTANCE = Mappers.getMapper(NotifyTemplateConvert.class); + + NotifyTemplateDO convert(NotifyTemplateCreateReqVO bean); + + NotifyTemplateDO convert(NotifyTemplateUpdateReqVO bean); + + NotifyTemplateRespVO convert(NotifyTemplateDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/oauth2/OAuth2OpenConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/oauth2/OAuth2OpenConvert.java new file mode 100644 index 0000000..57307b6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/oauth2/OAuth2OpenConvert.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.system.convert.oauth2; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.util.oauth2.OAuth2Utils; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Mapper +public interface OAuth2OpenConvert { + + OAuth2OpenConvert INSTANCE = Mappers.getMapper(OAuth2OpenConvert.class); + + default OAuth2OpenAccessTokenRespVO convert(OAuth2AccessTokenDO bean) { + OAuth2OpenAccessTokenRespVO respVO = convert0(bean); + respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); + respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime())); + respVO.setScope(OAuth2Utils.buildScopeStr(bean.getScopes())); + return respVO; + } + OAuth2OpenAccessTokenRespVO convert0(OAuth2AccessTokenDO bean); + + default OAuth2OpenCheckTokenRespVO convert2(OAuth2AccessTokenDO bean) { + OAuth2OpenCheckTokenRespVO respVO = convert3(bean); + respVO.setExp(LocalDateTimeUtil.toEpochMilli(bean.getExpiresTime()) / 1000L); + respVO.setUserType(UserTypeEnum.ADMIN.getValue()); + return respVO; + } + OAuth2OpenCheckTokenRespVO convert3(OAuth2AccessTokenDO bean); + + default OAuth2OpenAuthorizeInfoRespVO convert(OAuth2ClientDO client, List approves) { + // 构建 scopes + List> scopes = new ArrayList<>(client.getScopes().size()); + Map approveMap = CollectionUtils.convertMap(approves, OAuth2ApproveDO::getScope); + client.getScopes().forEach(scope -> { + OAuth2ApproveDO approve = approveMap.get(scope); + scopes.add(new KeyValue<>(scope, approve != null ? approve.getApproved() : false)); + }); + // 拼接返回 + return new OAuth2OpenAuthorizeInfoRespVO( + new OAuth2OpenAuthorizeInfoRespVO.Client(client.getName(), client.getLogo()), scopes); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/oauth2/OAuth2UserConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/oauth2/OAuth2UserConvert.java new file mode 100644 index 0000000..a529105 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/oauth2/OAuth2UserConvert.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.convert.oauth2; + +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.user.OAuth2UserInfoRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.user.OAuth2UserUpdateReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface OAuth2UserConvert { + + OAuth2UserConvert INSTANCE = Mappers.getMapper(OAuth2UserConvert.class); + + OAuth2UserInfoRespVO convert(AdminUserDO bean); + OAuth2UserInfoRespVO.Dept convert(DeptDO dept); + List convertList(List list); + + UserProfileUpdateReqVO convert(OAuth2UserUpdateReqVO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/package-info.java new file mode 100644 index 0000000..6824990 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/package-info.java @@ -0,0 +1,6 @@ +/** + * 提供 POJO 类的实体转换 + * + * 目前使用 MapStruct 框架 + */ +package com.yunxi.scm.module.system.convert; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/permission/MenuConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/permission/MenuConvert.java new file mode 100644 index 0000000..6492eea --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/permission/MenuConvert.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.convert.permission; + +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuRespVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuSimpleRespVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface MenuConvert { + + MenuConvert INSTANCE = Mappers.getMapper(MenuConvert.class); + + List convertList(List list); + + MenuDO convert(MenuCreateReqVO bean); + + MenuDO convert(MenuUpdateReqVO bean); + + MenuRespVO convert(MenuDO bean); + + List convertList02(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/permission/RoleConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/permission/RoleConvert.java new file mode 100644 index 0000000..b1481bd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/permission/RoleConvert.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.convert.permission; + +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.*; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.service.permission.bo.RoleCreateReqBO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface RoleConvert { + + RoleConvert INSTANCE = Mappers.getMapper(RoleConvert.class); + + RoleDO convert(RoleUpdateReqVO bean); + + RoleRespVO convert(RoleDO bean); + + RoleDO convert(RoleCreateReqVO bean); + + List convertList02(List list); + + List convertList03(List list); + + RoleDO convert(RoleCreateReqBO bean); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sensitiveword/SensitiveWordConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sensitiveword/SensitiveWordConvert.java new file mode 100644 index 0000000..93c248e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sensitiveword/SensitiveWordConvert.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.system.convert.sensitiveword; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordExcelVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordRespVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 敏感词 Convert + * + * @author 永不言败 + */ +@Mapper +public interface SensitiveWordConvert { + + SensitiveWordConvert INSTANCE = Mappers.getMapper(SensitiveWordConvert.class); + + SensitiveWordDO convert(SensitiveWordCreateReqVO bean); + + SensitiveWordDO convert(SensitiveWordUpdateReqVO bean); + + SensitiveWordRespVO convert(SensitiveWordDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsChannelConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsChannelConvert.java new file mode 100644 index 0000000..f6dc14c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsChannelConvert.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.system.convert.sms; + +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelRespVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelSimpleRespVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 短信渠道 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface SmsChannelConvert { + + SmsChannelConvert INSTANCE = Mappers.getMapper(SmsChannelConvert.class); + + SmsChannelDO convert(SmsChannelCreateReqVO bean); + + SmsChannelDO convert(SmsChannelUpdateReqVO bean); + + SmsChannelRespVO convert(SmsChannelDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + List convertList03(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsLogConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsLogConvert.java new file mode 100644 index 0000000..ce8e48d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsLogConvert.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.convert.sms; + +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogExcelVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogRespVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsLogDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 短信日志 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface SmsLogConvert { + + SmsLogConvert INSTANCE = Mappers.getMapper(SmsLogConvert.class); + + SmsLogRespVO convert(SmsLogDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsTemplateConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsTemplateConvert.java new file mode 100644 index 0000000..cbdd816 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/sms/SmsTemplateConvert.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.convert.sms; + +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateExcelVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateRespVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface SmsTemplateConvert { + + SmsTemplateConvert INSTANCE = Mappers.getMapper(SmsTemplateConvert.class); + + SmsTemplateDO convert(SmsTemplateCreateReqVO bean); + + SmsTemplateDO convert(SmsTemplateUpdateReqVO bean); + + SmsTemplateRespVO convert(SmsTemplateDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/social/SocialUserConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/social/SocialUserConvert.java new file mode 100644 index 0000000..9232e77 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/social/SocialUserConvert.java @@ -0,0 +1,19 @@ +package com.yunxi.scm.module.system.convert.social; + +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.api.social.dto.SocialUserUnbindReqDTO; +import com.yunxi.scm.module.system.controller.admin.socail.vo.SocialUserBindReqVO; +import com.yunxi.scm.module.system.controller.admin.socail.vo.SocialUserUnbindReqVO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface SocialUserConvert { + + SocialUserConvert INSTANCE = Mappers.getMapper(SocialUserConvert.class); + + SocialUserBindReqDTO convert(Long userId, Integer userType, SocialUserBindReqVO reqVO); + + SocialUserUnbindReqDTO convert(Long userId, Integer userType, SocialUserUnbindReqVO reqVO); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/tenant/TenantConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/tenant/TenantConvert.java new file mode 100644 index 0000000..cf53d55 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/tenant/TenantConvert.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.system.convert.tenant; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantExcelVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantRespVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.user.UserCreateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 租户 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface TenantConvert { + + TenantConvert INSTANCE = Mappers.getMapper(TenantConvert.class); + + TenantDO convert(TenantCreateReqVO bean); + + TenantDO convert(TenantUpdateReqVO bean); + + TenantRespVO convert(TenantDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + + default UserCreateReqVO convert02(TenantCreateReqVO bean) { + UserCreateReqVO reqVO = new UserCreateReqVO(); + reqVO.setUsername(bean.getUsername()); + reqVO.setPassword(bean.getPassword()); + reqVO.setNickname(bean.getContactName()).setMobile(bean.getContactMobile()); + return reqVO; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/tenant/TenantPackageConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/tenant/TenantPackageConvert.java new file mode 100644 index 0000000..098ab07 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/tenant/TenantPackageConvert.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.convert.tenant; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleSimpleRespVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageRespVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageSimpleRespVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantPackageDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 租户套餐 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface TenantPackageConvert { + + TenantPackageConvert INSTANCE = Mappers.getMapper(TenantPackageConvert.class); + + TenantPackageDO convert(TenantPackageCreateReqVO bean); + + TenantPackageDO convert(TenantPackageUpdateReqVO bean); + + TenantPackageRespVO convert(TenantPackageDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + + List convertList02(List list); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/user/UserConvert.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/user/UserConvert.java new file mode 100644 index 0000000..a6a10bd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/user/UserConvert.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.system.convert.user; + +import com.yunxi.scm.module.system.api.user.dto.AdminUserRespDTO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileRespVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.user.*; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +@Mapper +public interface UserConvert { + + UserConvert INSTANCE = Mappers.getMapper(UserConvert.class); + + UserPageItemRespVO convert(AdminUserDO bean); + + UserPageItemRespVO.Dept convert(DeptDO bean); + + AdminUserDO convert(UserCreateReqVO bean); + + AdminUserDO convert(UserUpdateReqVO bean); + + UserExcelVO convert02(AdminUserDO bean); + + AdminUserDO convert(UserImportExcelVO bean); + + UserProfileRespVO convert03(AdminUserDO bean); + + List convertList(List list); + + UserProfileRespVO.Dept convert02(DeptDO bean); + + AdminUserDO convert(UserProfileUpdateReqVO bean); + + AdminUserDO convert(UserProfileUpdatePasswordReqVO bean); + + List convertList02(List list); + + List convertList03(List list); + + List convertList04(List list); + + AdminUserRespDTO convert4(AdminUserDO bean); + + List convertList4(List users); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md new file mode 100644 index 0000000..19fbece --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/convert/《芋道 Spring Boot 对象转换 MapStruct 入门》.md @@ -0,0 +1 @@ + diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/DeptDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/DeptDO.java new file mode 100644 index 0000000..5dbd5aa --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/DeptDO.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.module.system.dal.dataobject.dept; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 部门表 + * + * @author ruoyi + * @author 芋道源码 + */ +@TableName("system_dept") +@KeySequence("system_dept_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class DeptDO extends TenantBaseDO { + + /** + * 部门ID + */ + @TableId + private Long id; + /** + * 部门名称 + */ + private String name; + /** + * 父部门ID + * + * 关联 {@link #id} + */ + private Long parentId; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 负责人 + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long leaderUserId; + /** + * 联系电话 + */ + private String phone; + /** + * 邮箱 + */ + private String email; + /** + * 部门状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/PostDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/PostDO.java new file mode 100644 index 0000000..c71281f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/PostDO.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.system.dal.dataobject.dept; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 岗位表 + * + * @author ruoyi + */ +@TableName("system_post") +@KeySequence("system_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class PostDO extends BaseDO { + + /** + * 岗位序号 + */ + @TableId + private Long id; + /** + * 岗位名称 + */ + private String name; + /** + * 岗位编码 + */ + private String code; + /** + * 岗位排序 + */ + private Integer sort; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/UserPostDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/UserPostDO.java new file mode 100644 index 0000000..58c30cd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dept/UserPostDO.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.dal.dataobject.dept; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户和岗位关联 + * + * @author ruoyi + */ +@TableName("system_user_post") +@KeySequence("system_user_post_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class UserPostDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 用户 ID + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long userId; + /** + * 角色 ID + * + * 关联 {@link PostDO#getId()} + */ + private Long postId; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dict/DictDataDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dict/DictDataDO.java new file mode 100644 index 0000000..7aef499 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dict/DictDataDO.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.system.dal.dataobject.dict; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 字典数据表 + * + * @author ruoyi + */ +@TableName("system_dict_data") +@KeySequence("system_dict_data_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class DictDataDO extends BaseDO { + + /** + * 字典数据编号 + */ + @TableId + private Long id; + /** + * 字典排序 + */ + private Integer sort; + /** + * 字典标签 + */ + private String label; + /** + * 字典值 + */ + private String value; + /** + * 字典类型 + * + * 冗余 {@link DictDataDO#getDictType()} + */ + private String dictType; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 颜色类型 + * + * 对应到 element-ui 为 default、primary、success、info、warning、danger + */ + private String colorType; + /** + * css 样式 + */ + @TableField(updateStrategy = FieldStrategy.IGNORED) + private String cssClass; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dict/DictTypeDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dict/DictTypeDO.java new file mode 100644 index 0000000..7f19b18 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/dict/DictTypeDO.java @@ -0,0 +1,57 @@ +package com.yunxi.scm.module.system.dal.dataobject.dict; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 字典类型表 + * + * @author ruoyi + */ +@TableName("system_dict_type") +@KeySequence("system_dict_type_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DictTypeDO extends BaseDO { + + /** + * 字典主键 + */ + @TableId + private Long id; + /** + * 字典名称 + */ + private String name; + /** + * 字典类型 + */ + private String type; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + + /** + * 删除时间 + */ + private LocalDateTime deletedTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/errorcode/ErrorCodeDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/errorcode/ErrorCodeDO.java new file mode 100644 index 0000000..a629baa --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/errorcode/ErrorCodeDO.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.system.dal.dataobject.errorcode; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.enums.errorcode.ErrorCodeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 错误码表 + * + * @author 芋道源码 + */ +@TableName(value = "system_error_code") +@KeySequence("system_error_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class ErrorCodeDO extends BaseDO { + + /** + * 错误码编号,自增 + */ + @TableId + private Long id; + /** + * 错误码类型 + * + * 枚举 {@link ErrorCodeTypeEnum} + */ + private Integer type; + /** + * 应用名 + */ + private String applicationName; + /** + * 错误码编码 + */ + private Integer code; + /** + * 错误码错误提示 + */ + private String message; + /** + * 错误码备注 + */ + private String memo; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/logger/LoginLogDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/logger/LoginLogDO.java new file mode 100644 index 0000000..c1052bd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/logger/LoginLogDO.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.module.system.dal.dataobject.logger; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.enums.logger.LoginLogTypeEnum; +import com.yunxi.scm.module.system.enums.logger.LoginResultEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 登录日志表 + * + * 注意,包括登录和登出两种行为 + * + * @author 芋道源码 + */ +@TableName("system_login_log") +@KeySequence("system_login_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class LoginLogDO extends BaseDO { + + /** + * 日志主键 + */ + private Long id; + /** + * 日志类型 + * + * 枚举 {@link LoginLogTypeEnum} + */ + private Integer logType; + /** + * 链路追踪编号 + */ + private String traceId; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 用户账号 + * + * 冗余,因为账号可以变更 + */ + private String username; + /** + * 登录结果 + * + * 枚举 {@link LoginResultEnum} + */ + private Integer result; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/logger/OperateLogDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/logger/OperateLogDO.java new file mode 100644 index 0000000..685c7d9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/logger/OperateLogDO.java @@ -0,0 +1,144 @@ +package com.yunxi.scm.module.system.dal.dataobject.logger; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 操作日志表 + * + * @author 芋道源码 + */ +@TableName(value = "system_operate_log", autoResultMap = true) +@KeySequence("system_operate_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OperateLogDO extends BaseDO { + + /** + * {@link #javaMethodArgs} 的最大长度 + */ + public static final Integer JAVA_METHOD_ARGS_MAX_LENGTH = 8000; + + /** + * {@link #resultData} 的最大长度 + */ + public static final Integer RESULT_MAX_LENGTH = 4000; + + /** + * 日志主键 + */ + @TableId + private Long id; + /** + * 链路追踪编号 + * + * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。 + */ + private String traceId; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 属性,或者 AdminUserDO 的 id 属性 + */ + private Long userId; + /** + * 用户类型 + * + * 关联 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 操作模块 + */ + private String module; + /** + * 操作名 + */ + private String name; + /** + * 操作分类 + * + * 枚举 {@link OperateTypeEnum} + */ + private Integer type; + /** + * 操作内容,记录整个操作的明细 + * 例如说,修改编号为 1 的用户信息,将性别从男改成女,将姓名从芋道改成源码。 + */ + private String content; + /** + * 拓展字段,有些复杂的业务,需要记录一些字段 + * 例如说,记录订单编号,则可以添加 key 为 "orderId",value 为订单编号 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map exts; + + /** + * 请求方法名 + */ + private String requestMethod; + /** + * 请求地址 + */ + private String requestUrl; + /** + * 用户 IP + */ + private String userIp; + /** + * 浏览器 UA + */ + private String userAgent; + + /** + * Java 方法名 + */ + private String javaMethod; + /** + * Java 方法的参数 + * + * 实际格式为 Map + * 不使用 @TableField(typeHandler = FastjsonTypeHandler.class) 注解的原因是,数据库存储有长度限制,会进行裁剪,会导致 JSON 反序列化失败 + * 其中,key 为参数名,value 为参数值 + */ + private String javaMethodArgs; + /** + * 开始时间 + */ + private LocalDateTime startTime; + /** + * 执行时长,单位:毫秒 + */ + private Integer duration; + /** + * 结果码 + * + * 目前使用的 {@link CommonResult#getCode()} 属性 + */ + private Integer resultCode; + /** + * 结果提示 + * + * 目前使用的 {@link CommonResult#getMsg()} 属性 + */ + private String resultMsg; + /** + * 结果数据 + * + * 如果是对象,则使用 JSON 格式化 + */ + private String resultData; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailAccountDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailAccountDO.java new file mode 100644 index 0000000..6af03d0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailAccountDO.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.system.dal.dataobject.mail; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 邮箱账号 DO + * + * 用途:配置发送邮箱的账号 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@TableName(value = "system_mail_account", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class MailAccountDO extends BaseDO { + + /** + * 主键 + */ + @TableId + private Long id; + /** + * 邮箱 + */ + private String mail; + + /** + * 用户名 + */ + private String username; + /** + * 密码 + */ + private String password; + /** + * SMTP 服务器域名 + */ + private String host; + /** + * SMTP 服务器端口 + */ + private Integer port; + /** + * 是否开启 SSL + */ + private Boolean sslEnable; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailLogDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailLogDO.java new file mode 100644 index 0000000..38bc52b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailLogDO.java @@ -0,0 +1,121 @@ +package com.yunxi.scm.module.system.dal.dataobject.mail; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.enums.mail.MailSendStatusEnum; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 邮箱日志 DO + * 记录每一次邮件的发送 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@TableName(value = "system_mail_log", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MailLogDO extends BaseDO implements Serializable { + + /** + * 日志编号,自增 + */ + private Long id; + + /** + * 用户编码 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 接收邮箱地址 + */ + private String toMail; + + /** + * 邮箱账号编号 + * + * 关联 {@link MailAccountDO#getId()} + */ + private Long accountId; + /** + * 发送邮箱地址 + * + * 冗余 {@link MailAccountDO#getMail()} + */ + private String fromMail; + + // ========= 模板相关字段 ========= + /** + * 模版编号 + * + * 关联 {@link MailTemplateDO#getId()} + */ + private Long templateId; + /** + * 模版编码 + * + * 冗余 {@link MailTemplateDO#getCode()} + */ + private String templateCode; + /** + * 模版发送人名称 + * + * 冗余 {@link MailTemplateDO#getNickname()} + */ + private String templateNickname; + /** + * 模版标题 + */ + private String templateTitle; + /** + * 模版内容 + * + * 基于 {@link MailTemplateDO#getContent()} 格式化后的内容 + */ + private String templateContent; + /** + * 模版参数 + * + * 基于 {@link MailTemplateDO#getParams()} 输入后的参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map templateParams; + + // ========= 发送相关字段 ========= + /** + * 发送状态 + * + * 枚举 {@link MailSendStatusEnum} + */ + private Integer sendStatus; + /** + * 发送时间 + */ + private LocalDateTime sendTime; + /** + * 发送返回的消息 ID + */ + private String sendMessageId; + /** + * 发送异常 + */ + private String sendException; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailTemplateDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailTemplateDO.java new file mode 100644 index 0000000..da0712a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/mail/MailTemplateDO.java @@ -0,0 +1,71 @@ +package com.yunxi.scm.module.system.dal.dataobject.mail; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * 邮件模版 DO + * + * @author wangjingyi + * @since 2022-03-21 + */ +@TableName(value = "system_mail_template", autoResultMap = true) +@Data +@EqualsAndHashCode(callSuper = true) +public class MailTemplateDO extends BaseDO { + + /** + * 主键 + */ + private Long id; + /** + * 模版名称 + */ + private String name; + /** + * 模版编号 + */ + private String code; + /** + * 发送的邮箱账号编号 + * + * 关联 {@link MailAccountDO#getId()} + */ + private Long accountId; + + /** + * 发送人名称 + */ + private String nickname; + /** + * 标题 + */ + private String title; + /** + * 内容 + */ + private String content; + /** + * 参数数组(自动根据内容生成) + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notice/NoticeDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notice/NoticeDO.java new file mode 100644 index 0000000..b8f659f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notice/NoticeDO.java @@ -0,0 +1,47 @@ +package com.yunxi.scm.module.system.dal.dataobject.notice; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.enums.notice.NoticeTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 通知公告表 + * + * @author ruoyi + */ +@TableName("system_notice") +@KeySequence("system_notice_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class NoticeDO extends BaseDO { + + /** + * 公告ID + */ + private Long id; + /** + * 公告标题 + */ + private String title; + /** + * 公告类型 + * + * 枚举 {@link NoticeTypeEnum} + */ + private Integer type; + /** + * 公告内容 + */ + private String content; + /** + * 公告状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notify/NotifyMessageDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notify/NotifyMessageDO.java new file mode 100644 index 0000000..b5cae17 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notify/NotifyMessageDO.java @@ -0,0 +1,101 @@ +package com.yunxi.scm.module.system.dal.dataobject.notify; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Map; + +/** + * 站内信 DO + * + * @author xrcoder + */ +@TableName(value = "system_notify_message", autoResultMap = true) +@KeySequence("system_notify_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NotifyMessageDO extends BaseDO { + + /** + * 站内信编号,自增 + */ + @TableId + private Long id; + /** + * 用户编号 + * + * 关联 MemberUserDO 的 id 字段、或者 AdminUserDO 的 id 字段 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + // ========= 模板相关字段 ========= + + /** + * 模版编号 + * + * 关联 {@link NotifyTemplateDO#getId()} + */ + private Long templateId; + /** + * 模版编码 + * + * 关联 {@link NotifyTemplateDO#getCode()} + */ + private String templateCode; + /** + * 模版类型 + * + * 冗余 {@link NotifyTemplateDO#getType()} + */ + private Integer templateType; + /** + * 模版发送人名称 + * + * 冗余 {@link NotifyTemplateDO#getNickname()} + */ + private String templateNickname; + /** + * 模版内容 + * + * 基于 {@link NotifyTemplateDO#getContent()} 格式化后的内容 + */ + private String templateContent; + /** + * 模版参数 + * + * 基于 {@link NotifyTemplateDO#getParams()} 输入后的参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map templateParams; + + // ========= 读取相关字段 ========= + + /** + * 是否已读 + */ + private Boolean readStatus; + /** + * 阅读时间 + */ + private LocalDateTime readTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notify/NotifyTemplateDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notify/NotifyTemplateDO.java new file mode 100644 index 0000000..b9431b0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/notify/NotifyTemplateDO.java @@ -0,0 +1,72 @@ +package com.yunxi.scm.module.system.dal.dataobject.notify; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.util.List; + +/** + * 站内信模版 DO + * + * @author xrcoder + */ +@TableName(value = "system_notify_template", autoResultMap = true) +@KeySequence("system_notify_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class NotifyTemplateDO extends BaseDO { + + /** + * ID + */ + @TableId + private Long id; + /** + * 模版名称 + */ + private String name; + /** + * 模版编码 + */ + private String code; + /** + * 模版类型 + * + * 对应 system_notify_template_type 字典 + */ + private Integer type; + /** + * 发送人名称 + */ + private String nickname; + /** + * 模版内容 + */ + private String content; + /** + * 参数数组 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java new file mode 100644 index 0000000..411a4c3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2AccessTokenDO.java @@ -0,0 +1,69 @@ +package com.yunxi.scm.module.system.dal.dataobject.oauth2; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * OAuth2 访问令牌 DO + * + * 如下字段,暂时未使用,暂时不支持: + * user_name、authentication(用户信息) + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_access_token", autoResultMap = true) +@KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2AccessTokenDO extends TenantBaseDO { + + /** + * 编号,数据库递增 + */ + @TableId + private Long id; + /** + * 访问令牌 + */ + private String accessToken; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java new file mode 100644 index 0000000..253cb7e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2ApproveDO.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.system.dal.dataobject.oauth2; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; + +/** + * OAuth2 批准 DO + * + * 用户在 sso.vue 界面时,记录接受的 scope 列表 + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_approve", autoResultMap = true) +@KeySequence("system_oauth2_approve_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2ApproveDO extends BaseDO { + + /** + * 编号,数据库自增 + */ + @TableId + private Long id; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + private String scope; + /** + * 是否接受 + * + * true - 接受 + * false - 拒绝 + */ + private Boolean approved; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java new file mode 100644 index 0000000..aae306f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2ClientDO.java @@ -0,0 +1,107 @@ +package com.yunxi.scm.module.system.dal.dataobject.oauth2; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.enums.oauth2.OAuth2GrantTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.List; + +/** + * OAuth2 客户端 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_client", autoResultMap = true) +@KeySequence("system_oauth2_client_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2ClientDO extends BaseDO { + + /** + * 编号,数据库自增 + * + * 由于 SQL Server 在存储 String 主键有点问题,所以暂时使用 Long 类型 + */ + @TableId + private Long id; + /** + * 客户端编号 + */ + private String clientId; + /** + * 客户端密钥 + */ + private String secret; + /** + * 应用名 + */ + private String name; + /** + * 应用图标 + */ + private String logo; + /** + * 应用描述 + */ + private String description; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 访问令牌的有效期 + */ + private Integer accessTokenValiditySeconds; + /** + * 刷新令牌的有效期 + */ + private Integer refreshTokenValiditySeconds; + /** + * 可重定向的 URI 地址 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List redirectUris; + /** + * 授权类型(模式) + * + * 枚举 {@link OAuth2GrantTypeEnum} + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authorizedGrantTypes; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 自动授权的 Scope + * + * code 授权时,如果 scope 在这个范围内,则自动通过 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List autoApproveScopes; + /** + * 权限 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List authorities; + /** + * 资源 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List resourceIds; + /** + * 附加信息,JSON 格式 + */ + private String additionalInformation; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java new file mode 100644 index 0000000..4253f92 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2CodeDO.java @@ -0,0 +1,68 @@ +package com.yunxi.scm.module.system.dal.dataobject.oauth2; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * OAuth2 授权码 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_code", autoResultMap = true) +@KeySequence("system_oauth2_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class OAuth2CodeDO extends BaseDO { + + /** + * 编号,数据库递增 + */ + private Long id; + /** + * 授权码 + */ + private String code; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getClientId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 重定向地址 + */ + private String redirectUri; + /** + * 状态 + */ + private String state; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java new file mode 100644 index 0000000..5c92263 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/oauth2/OAuth2RefreshTokenDO.java @@ -0,0 +1,63 @@ +package com.yunxi.scm.module.system.dal.dataobject.oauth2; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * OAuth2 刷新令牌 + * + * @author 芋道源码 + */ +@TableName(value = "system_oauth2_refresh_token", autoResultMap = true) +// 由于 Oracle 的 SEQ 的名字长度有限制,所以就先用 system_oauth2_access_token_seq 吧,反正也没啥问题 +@KeySequence("system_oauth2_access_token_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Accessors(chain = true) +public class OAuth2RefreshTokenDO extends BaseDO { + + /** + * 编号,数据库字典 + */ + private Long id; + /** + * 刷新令牌 + */ + private String refreshToken; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + /** + * 客户端编号 + * + * 关联 {@link OAuth2ClientDO#getId()} + */ + private String clientId; + /** + * 授权范围 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List scopes; + /** + * 过期时间 + */ + private LocalDateTime expiresTime; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/MenuDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/MenuDO.java new file mode 100644 index 0000000..102f96c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/MenuDO.java @@ -0,0 +1,107 @@ +package com.yunxi.scm.module.system.dal.dataobject.permission; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.enums.permission.MenuTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 菜单 DO + * + * @author ruoyi + */ +@TableName("system_menu") +@KeySequence("system_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class MenuDO extends BaseDO { + + /** + * 菜单编号 - 根节点 + */ + public static final Long ID_ROOT = 0L; + + /** + * 菜单编号 + */ + @TableId + private Long id; + /** + * 菜单名称 + */ + private String name; + /** + * 权限标识 + * + * 一般格式为:${系统}:${模块}:${操作} + * 例如说:system:admin:add,即 system 服务的添加管理员。 + * + * 当我们把该 MenuDO 赋予给角色后,意味着该角色有该资源: + * - 对于后端,配合 @PreAuthorize 注解,配置 API 接口需要该权限,从而对 API 接口进行权限控制。 + * - 对于前端,配合前端标签,配置按钮是否展示,避免用户没有该权限时,结果可以看到该操作。 + */ + private String permission; + /** + * 菜单类型 + * + * 枚举 {@link MenuTypeEnum} + */ + private Integer type; + /** + * 显示顺序 + */ + private Integer sort; + /** + * 父菜单ID + */ + private Long parentId; + /** + * 路由地址 + * + * 如果 path 为 http(s) 时,则它是外链 + */ + private String path; + /** + * 菜单图标 + */ + private String icon; + /** + * 组件路径 + */ + private String component; + /** + * 组件名 + */ + private String componentName; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 是否可见 + * + * 只有菜单、目录使用 + * 当设置为 true 时,该菜单不会展示在侧边栏,但是路由还是存在。例如说,一些独立的编辑页面 /edit/1024 等等 + */ + private Boolean visible; + /** + * 是否缓存 + * + * 只有菜单、目录使用,否使用 Vue 路由的 keep-alive 特性 + * 注意:如果开启缓存,则必须填写 {@link #componentName} 属性,否则无法缓存 + */ + private Boolean keepAlive; + /** + * 是否总是显示 + * + * 如果为 false 时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单 + */ + private Boolean alwaysShow; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/RoleDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/RoleDO.java new file mode 100644 index 0000000..09163bc --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/RoleDO.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.system.dal.dataobject.permission; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.yunxi.scm.module.system.enums.permission.DataScopeEnum; +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.yunxi.scm.module.system.enums.permission.RoleTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Set; + +/** + * 角色 DO + * + * @author ruoyi + */ +@TableName(value = "system_role", autoResultMap = true) +@KeySequence("system_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleDO extends TenantBaseDO { + + /** + * 角色ID + */ + @TableId + private Long id; + /** + * 角色名称 + */ + private String name; + /** + * 角色标识 + * + * 枚举 + */ + private String code; + /** + * 角色排序 + */ + private Integer sort; + /** + * 角色状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 角色类型 + * + * 枚举 {@link RoleTypeEnum} + */ + private Integer type; + /** + * 备注 + */ + private String remark; + + /** + * 数据范围 + * + * 枚举 {@link DataScopeEnum} + */ + private Integer dataScope; + /** + * 数据范围(指定部门数组) + * + * 适用于 {@link #dataScope} 的值为 {@link DataScopeEnum#DEPT_CUSTOM} 时 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set dataScopeDeptIds; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/RoleMenuDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/RoleMenuDO.java new file mode 100644 index 0000000..7b061d7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/RoleMenuDO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.system.dal.dataobject.permission; + +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 角色和菜单关联 + * + * @author ruoyi + */ +@TableName("system_role_menu") +@KeySequence("system_role_menu_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class RoleMenuDO extends TenantBaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 角色ID + */ + private Long roleId; + /** + * 菜单ID + */ + private Long menuId; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/UserRoleDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/UserRoleDO.java new file mode 100644 index 0000000..58cf352 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/permission/UserRoleDO.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.system.dal.dataobject.permission; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 用户和角色关联 + * + * @author ruoyi + */ +@TableName("system_user_role") +@KeySequence("system_user_role_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +public class UserRoleDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 用户 ID + */ + private Long userId; + /** + * 角色 ID + */ + private Long roleId; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java new file mode 100644 index 0000000..fb2f3eb --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sensitiveword/SensitiveWordDO.java @@ -0,0 +1,58 @@ +package com.yunxi.scm.module.system.dal.dataobject.sensitiveword; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.StringListTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.List; + +/** + * 敏感词 DO + * + * @author 永不言败 + */ +@TableName(value = "system_sensitive_word", autoResultMap = true) +@KeySequence("system_sensitive_word_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SensitiveWordDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 敏感词 + */ + private String name; + /** + * 描述 + */ + private String description; + /** + * 标签数组 + * + * 用于实现不同的业务场景下,需要使用不同标签的敏感词。 + * 例如说,tag 有短信、论坛两种,敏感词 "推广" 在短信下是敏感词,在论坛下不是敏感词。 + * 此时,我们会存储一条敏感词记录,它的 name 为"推广",tag 为短信。 + */ + @TableField(typeHandler = StringListTypeHandler.class) + private List tags; + /** + * 状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsChannelDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsChannelDO.java new file mode 100644 index 0000000..377bb5d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsChannelDO.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.module.system.dal.dataobject.sms; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.sms.core.enums.SmsChannelEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * 短信渠道 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_channel", autoResultMap = true) +@KeySequence("system_sms_channel_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsChannelDO extends BaseDO { + + /** + * 渠道编号 + */ + private Long id; + /** + * 短信签名 + */ + private String signature; + /** + * 渠道编码 + * + * 枚举 {@link SmsChannelEnum} + */ + private String code; + /** + * 启用状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 短信 API 的账号 + */ + private String apiKey; + /** + * 短信 API 的密钥 + */ + private String apiSecret; + /** + * 短信发送回调 URL + */ + private String callbackUrl; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsCodeDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsCodeDO.java new file mode 100644 index 0000000..f97a378 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsCodeDO.java @@ -0,0 +1,65 @@ +package com.yunxi.scm.module.system.dal.dataobject.sms; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 手机验证码 DO + * + * idx_mobile 索引:基于 {@link #mobile} 字段 + * + * @author 芋道源码 + */ +@TableName("system_sms_code") +@KeySequence("system_sms_code_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SmsCodeDO extends BaseDO { + + /** + * 编号 + */ + private Long id; + /** + * 手机号 + */ + private String mobile; + /** + * 验证码 + */ + private String code; + /** + * 发送场景 + * + * 枚举 {@link SmsCodeDO} + */ + private Integer scene; + /** + * 创建 IP + */ + private String createIp; + /** + * 今日发送的第几条 + */ + private Integer todayIndex; + /** + * 是否使用 + */ + private Boolean used; + /** + * 使用时间 + */ + private LocalDateTime usedTime; + /** + * 使用 IP + */ + private String usedIp; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsLogDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsLogDO.java new file mode 100644 index 0000000..71c01ec --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsLogDO.java @@ -0,0 +1,175 @@ +package com.yunxi.scm.module.system.dal.dataobject.sms; + +import com.yunxi.scm.module.system.enums.sms.SmsReceiveStatusEnum; +import com.yunxi.scm.module.system.enums.sms.SmsSendStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.sms.core.enums.SmsFrameworkErrorCodeConstants; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.*; + +import java.time.LocalDateTime; +import java.util.Map; + +/** + * 短信日志 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_log", autoResultMap = true) +@KeySequence("system_sms_log_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class SmsLogDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + // ========= 渠道相关字段 ========= + + /** + * 短信渠道编号 + * + * 关联 {@link SmsChannelDO#getId()} + */ + private Long channelId; + /** + * 短信渠道编码 + * + * 冗余 {@link SmsChannelDO#getCode()} + */ + private String channelCode; + + // ========= 模板相关字段 ========= + + /** + * 模板编号 + * + * 关联 {@link SmsTemplateDO#getId()} + */ + private Long templateId; + /** + * 模板编码 + * + * 冗余 {@link SmsTemplateDO#getCode()} + */ + private String templateCode; + /** + * 短信类型 + * + * 冗余 {@link SmsTemplateDO#getType()} + */ + private Integer templateType; + /** + * 基于 {@link SmsTemplateDO#getContent()} 格式化后的内容 + */ + private String templateContent; + /** + * 基于 {@link SmsTemplateDO#getParams()} 输入后的参数 + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private Map templateParams; + /** + * 短信 API 的模板编号 + * + * 冗余 {@link SmsTemplateDO#getApiTemplateId()} + */ + private String apiTemplateId; + + // ========= 手机相关字段 ========= + + /** + * 手机号 + */ + private String mobile; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + // ========= 发送相关字段 ========= + + /** + * 发送状态 + * + * 枚举 {@link SmsSendStatusEnum} + */ + private Integer sendStatus; + /** + * 发送时间 + */ + private LocalDateTime sendTime; + /** + * 发送结果的编码 + * + * 枚举 {@link SmsFrameworkErrorCodeConstants} + */ + private Integer sendCode; + /** + * 发送结果的提示 + * + * 一般情况下,使用 {@link SmsFrameworkErrorCodeConstants} + * 异常情况下,通过格式化 Exception 的提示存储 + */ + private String sendMsg; + /** + * 短信 API 发送结果的编码 + * + * 由于第三方的错误码可能是字符串,所以使用 String 类型 + */ + private String apiSendCode; + /** + * 短信 API 发送失败的提示 + */ + private String apiSendMsg; + /** + * 短信 API 发送返回的唯一请求 ID + * + * 用于和短信 API 进行定位于排错 + */ + private String apiRequestId; + /** + * 短信 API 发送返回的序号 + * + * 用于和短信 API 平台的发送记录关联 + */ + private String apiSerialNo; + + // ========= 接收相关字段 ========= + + /** + * 接收状态 + * + * 枚举 {@link SmsReceiveStatusEnum} + */ + private Integer receiveStatus; + /** + * 接收时间 + */ + private LocalDateTime receiveTime; + /** + * 短信 API 接收结果的编码 + */ + private String apiReceiveCode; + /** + * 短信 API 接收结果的提示 + */ + private String apiReceiveMsg; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsTemplateDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsTemplateDO.java new file mode 100644 index 0000000..165e0b6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/sms/SmsTemplateDO.java @@ -0,0 +1,91 @@ +package com.yunxi.scm.module.system.dal.dataobject.sms; + +import com.yunxi.scm.module.system.enums.sms.SmsTemplateTypeEnum; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.List; + +/** + * 短信模板 DO + * + * @author zzf + * @since 2021-01-25 + */ +@TableName(value = "system_sms_template", autoResultMap = true) +@KeySequence("system_sms_template_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +public class SmsTemplateDO extends BaseDO { + + /** + * 自增编号 + */ + private Long id; + + // ========= 模板相关字段 ========= + + /** + * 短信类型 + * + * 枚举 {@link SmsTemplateTypeEnum} + */ + private Integer type; + /** + * 启用状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 模板编码,保证唯一 + */ + private String code; + /** + * 模板名称 + */ + private String name; + /** + * 模板内容 + * + * 内容的参数,使用 {} 包括,例如说 {name} + */ + private String content; + /** + * 参数数组(自动根据内容生成) + */ + @TableField(typeHandler = JacksonTypeHandler.class) + private List params; + /** + * 备注 + */ + private String remark; + /** + * 短信 API 的模板编号 + */ + private String apiTemplateId; + + // ========= 渠道相关字段 ========= + + /** + * 短信渠道编号 + * + * 关联 {@link SmsChannelDO#getId()} + */ + private Long channelId; + /** + * 短信渠道编码 + * + * 冗余 {@link SmsChannelDO#getCode()} + */ + private String channelCode; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/social/SocialUserBindDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/social/SocialUserBindDO.java new file mode 100644 index 0000000..2daee18 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/social/SocialUserBindDO.java @@ -0,0 +1,56 @@ +package com.yunxi.scm.module.system.dal.dataobject.social; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 社交用户的绑定 + * 即 {@link SocialUserDO} 与 UserDO 的关联表 + * + * @author 芋道源码 + */ +@TableName(value = "system_social_user_bind", autoResultMap = true) +@KeySequence("system_social_user_bind_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserBindDO extends BaseDO { + + /** + * 编号 + */ + @TableId + private Long id; + /** + * 关联的用户编号 + * + * 关联 UserDO 的编号 + */ + private Long userId; + /** + * 用户类型 + * + * 枚举 {@link UserTypeEnum} + */ + private Integer userType; + + /** + * 社交平台的用户编号 + * + * 关联 {@link SocialUserDO#getId()} + */ + private Long socialUserId; + /** + * 社交平台的类型 + * + * 冗余 {@link SocialUserDO#getType()} + */ + private Integer socialType; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/social/SocialUserDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/social/SocialUserDO.java new file mode 100644 index 0000000..7bced8f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/social/SocialUserDO.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.module.system.dal.dataobject.social; + +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +/** + * 社交(三方)用户 + * + * @author weir + */ +@TableName(value = "system_social_user", autoResultMap = true) +@KeySequence("system_social_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SocialUserDO extends BaseDO { + + /** + * 自增主键 + */ + @TableId + private Long id; + /** + * 社交平台的类型 + * + * 枚举 {@link SocialTypeEnum} + */ + private Integer type; + + /** + * 社交 openid + */ + private String openid; + /** + * 社交 token + */ + private String token; + /** + * 原始 Token 数据,一般是 JSON 格式 + */ + private String rawTokenInfo; + + /** + * 用户昵称 + */ + private String nickname; + /** + * 用户头像 + */ + private String avatar; + /** + * 原始用户数据,一般是 JSON 格式 + */ + private String rawUserInfo; + + /** + * 最后一次的认证 code + */ + private String code; + /** + * 最后一次的认证 state + */ + private String state; + +} + + diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/tenant/TenantDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/tenant/TenantDO.java new file mode 100644 index 0000000..ed20ccd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/tenant/TenantDO.java @@ -0,0 +1,82 @@ +package com.yunxi.scm.module.system.dal.dataobject.tenant; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.time.LocalDateTime; + +/** + * 租户 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_tenant", autoResultMap = true) +@KeySequence("system_tenant_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TenantDO extends BaseDO { + + /** + * 套餐编号 - 系统 + */ + public static final Long PACKAGE_ID_SYSTEM = 0L; + + /** + * 租户编号,自增 + */ + private Long id; + /** + * 租户名,唯一 + */ + private String name; + /** + * 联系人的用户编号 + * + * 关联 {@link AdminUserDO#getId()} + */ + private Long contactUserId; + /** + * 联系人 + */ + private String contactName; + /** + * 联系手机 + */ + private String contactMobile; + /** + * 租户状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 绑定域名 + * + * TODO 芋艿:目前是预留字段,未来会支持根据域名,自动查询到对应的租户。等等 + */ + private String domain; + /** + * 租户套餐编号 + * + * 关联 {@link TenantPackageDO#getId()} + * 特殊逻辑:系统内置租户,不使用套餐,暂时使用 {@link #PACKAGE_ID_SYSTEM} 标识 + */ + private Long packageId; + /** + * 过期时间 + */ + private LocalDateTime expireTime; + /** + * 账号数量 + */ + private Integer accountCount; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/tenant/TenantPackageDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/tenant/TenantPackageDO.java new file mode 100644 index 0000000..550f8c4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/tenant/TenantPackageDO.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.system.dal.dataobject.tenant; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; + +import java.util.Set; + +/** + * 租户套餐 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_tenant_package", autoResultMap = true) +@KeySequence("system_tenant_package_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TenantPackageDO extends BaseDO { + + /** + * 套餐编号,自增 + */ + private Long id; + /** + * 套餐名,唯一 + */ + private String name; + /** + * 租户套餐状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 备注 + */ + private String remark; + /** + * 关联的菜单编号 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set menuIds; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/user/AdminUserDO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/user/AdminUserDO.java new file mode 100644 index 0000000..f991239 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/dataobject/user/AdminUserDO.java @@ -0,0 +1,96 @@ +package com.yunxi.scm.module.system.dal.dataobject.user; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.mybatis.core.type.JsonLongSetTypeHandler; +import com.yunxi.scm.framework.tenant.core.db.TenantBaseDO; +import com.yunxi.scm.module.system.enums.common.SexEnum; +import com.baomidou.mybatisplus.annotation.KeySequence; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.*; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.time.LocalDateTime; +import java.util.Set; + +/** + * 管理后台的用户 DO + * + * @author 芋道源码 + */ +@TableName(value = "system_users", autoResultMap = true) // 由于 SQL Server 的 system_user 是关键字,所以使用 system_users +@KeySequence("system_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。 +@Data +@EqualsAndHashCode(callSuper = true) +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AdminUserDO extends TenantBaseDO { + + /** + * 用户ID + */ + @TableId + private Long id; + /** + * 用户账号 + */ + private String username; + /** + * 加密后的密码 + * + * 因为目前使用 {@link BCryptPasswordEncoder} 加密器,所以无需自己处理 salt 盐 + */ + private String password; + /** + * 用户昵称 + */ + private String nickname; + /** + * 备注 + */ + private String remark; + /** + * 部门 ID + */ + private Long deptId; + /** + * 岗位编号数组 + */ + @TableField(typeHandler = JsonLongSetTypeHandler.class) + private Set postIds; + /** + * 用户邮箱 + */ + private String email; + /** + * 手机号码 + */ + private String mobile; + /** + * 用户性别 + * + * 枚举类 {@link SexEnum} + */ + private Integer sex; + /** + * 用户头像 + */ + private String avatar; + /** + * 帐号状态 + * + * 枚举 {@link CommonStatusEnum} + */ + private Integer status; + /** + * 最后登录IP + */ + private String loginIp; + /** + * 最后登录时间 + */ + private LocalDateTime loginDate; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/DeptMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/DeptMapper.java new file mode 100644 index 0000000..ded5820 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/DeptMapper.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.dal.mysql.dept; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DeptMapper extends BaseMapperX { + + default List selectList(DeptListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DeptDO::getName, reqVO.getName()) + .eqIfPresent(DeptDO::getStatus, reqVO.getStatus())); + } + + default DeptDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(DeptDO::getParentId, parentId, DeptDO::getName, name); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(DeptDO::getParentId, parentId); + } + + default List selectListByParentId(Collection parentIds) { + return selectList(DeptDO::getParentId, parentIds); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/PostMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/PostMapper.java new file mode 100644 index 0000000..0301696 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/PostMapper.java @@ -0,0 +1,46 @@ +package com.yunxi.scm.module.system.dal.mysql.dept; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface PostMapper extends BaseMapperX { + + default List selectList(Collection ids, Collection statuses) { + return selectList(new LambdaQueryWrapperX() + .inIfPresent(PostDO::getId, ids) + .inIfPresent(PostDO::getStatus, statuses)); + } + + default PageResult selectPage(PostPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(PostDO::getCode, reqVO.getCode()) + .likeIfPresent(PostDO::getName, reqVO.getName()) + .eqIfPresent(PostDO::getStatus, reqVO.getStatus()) + .orderByDesc(PostDO::getId)); + } + + default List selectList(PostExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(PostDO::getCode, reqVO.getCode()) + .likeIfPresent(PostDO::getName, reqVO.getName()) + .eqIfPresent(PostDO::getStatus, reqVO.getStatus())); + } + + default PostDO selectByName(String name) { + return selectOne(PostDO::getName, name); + } + + default PostDO selectByCode(String code) { + return selectOne(PostDO::getCode, code); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/UserPostMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/UserPostMapper.java new file mode 100644 index 0000000..4e999e4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dept/UserPostMapper.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.system.dal.mysql.dept; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.dal.dataobject.dept.UserPostDO; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface UserPostMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(UserPostDO::getUserId, userId); + } + + default void deleteByUserIdAndPostId(Long userId, Collection postIds) { + delete(new LambdaQueryWrapperX() + .eq(UserPostDO::getUserId, userId) + .in(UserPostDO::getPostId, postIds)); + } + + default List selectListByPostIds(Collection postIds) { + return selectList(UserPostDO::getPostId, postIds); + } + + default void deleteByUserId(Long userId) { + delete(Wrappers.lambdaUpdate(UserPostDO.class).eq(UserPostDO::getUserId, userId)); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dict/DictDataMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dict/DictDataMapper.java new file mode 100644 index 0000000..3862e35 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dict/DictDataMapper.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.module.system.dal.mysql.dict; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictDataDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface DictDataMapper extends BaseMapperX { + + default DictDataDO selectByDictTypeAndValue(String dictType, String value) { + return selectOne(DictDataDO::getDictType, dictType, DictDataDO::getValue, value); + } + + default DictDataDO selectByDictTypeAndLabel(String dictType, String label) { + return selectOne(DictDataDO::getDictType, dictType, DictDataDO::getLabel, label); + } + + default List selectByDictTypeAndValues(String dictType, Collection values) { + return selectList(new LambdaQueryWrapper().eq(DictDataDO::getDictType, dictType) + .in(DictDataDO::getValue, values)); + } + + default long selectCountByDictType(String dictType) { + return selectCount(DictDataDO::getDictType, dictType); + } + + default PageResult selectPage(DictDataPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DictDataDO::getLabel, reqVO.getLabel()) + .eqIfPresent(DictDataDO::getDictType, reqVO.getDictType()) + .eqIfPresent(DictDataDO::getStatus, reqVO.getStatus()) + .orderByDesc(Arrays.asList(DictDataDO::getDictType, DictDataDO::getSort))); + } + + default List selectList(DictDataExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DictDataDO::getLabel, reqVO.getLabel()) + .eqIfPresent(DictDataDO::getDictType, reqVO.getDictType()) + .eqIfPresent(DictDataDO::getStatus, reqVO.getStatus())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dict/DictTypeMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dict/DictTypeMapper.java new file mode 100644 index 0000000..b7c9c04 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/dict/DictTypeMapper.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.system.dal.mysql.dict; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictTypeDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Update; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface DictTypeMapper extends BaseMapperX { + + default PageResult selectPage(DictTypePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(DictTypeDO::getName, reqVO.getName()) + .likeIfPresent(DictTypeDO::getType, reqVO.getType()) + .eqIfPresent(DictTypeDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DictTypeDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(DictTypeDO::getId)); + } + + default List selectList(DictTypeExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(DictTypeDO::getName, reqVO.getName()) + .likeIfPresent(DictTypeDO::getType, reqVO.getType()) + .eqIfPresent(DictTypeDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(DictTypeDO::getCreateTime, reqVO.getCreateTime())); + } + + default DictTypeDO selectByType(String type) { + return selectOne(DictTypeDO::getType, type); + } + + default DictTypeDO selectByName(String name) { + return selectOne(DictTypeDO::getName, name); + } + + int deleteById(@Param("id") Long id, @Param("deletedTime") LocalDateTime deletedTime); + + @Update("UPDATE system_dict_type SET deleted = 1, deleted_time = #{deletedTime} WHERE id = #{id}") + void updateToDelete(@Param("id") Long id, @Param("deletedTime") LocalDateTime deletedTime); +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/errorcode/ErrorCodeMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/errorcode/ErrorCodeMapper.java new file mode 100644 index 0000000..0f25706 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/errorcode/ErrorCodeMapper.java @@ -0,0 +1,51 @@ +package com.yunxi.scm.module.system.dal.mysql.errorcode; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeExportReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface ErrorCodeMapper extends BaseMapperX { + + default PageResult selectPage(ErrorCodePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(ErrorCodeDO::getType, reqVO.getType()) + .likeIfPresent(ErrorCodeDO::getApplicationName, reqVO.getApplicationName()) + .eqIfPresent(ErrorCodeDO::getCode, reqVO.getCode()) + .likeIfPresent(ErrorCodeDO::getMessage, reqVO.getMessage()) + .betweenIfPresent(ErrorCodeDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(ErrorCodeDO::getCode)); + } + + default List selectList(ErrorCodeExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(ErrorCodeDO::getType, reqVO.getType()) + .likeIfPresent(ErrorCodeDO::getApplicationName, reqVO.getApplicationName()) + .eqIfPresent(ErrorCodeDO::getCode, reqVO.getCode()) + .likeIfPresent(ErrorCodeDO::getMessage, reqVO.getMessage()) + .betweenIfPresent(ErrorCodeDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(ErrorCodeDO::getCode)); + } + + default List selectListByCodes(Collection codes) { + return selectList(ErrorCodeDO::getCode, codes); + } + + default ErrorCodeDO selectByCode(Integer code) { + return selectOne(ErrorCodeDO::getCode, code); + } + + default List selectListByApplicationNameAndUpdateTimeGt(String applicationName, LocalDateTime minUpdateTime) { + return selectList(new LambdaQueryWrapperX().eq(ErrorCodeDO::getApplicationName, applicationName) + .gtIfPresent(ErrorCodeDO::getUpdateTime, minUpdateTime)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/logger/LoginLogMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/logger/LoginLogMapper.java new file mode 100644 index 0000000..7fab0ce --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/logger/LoginLogMapper.java @@ -0,0 +1,45 @@ +package com.yunxi.scm.module.system.dal.mysql.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.logger.LoginLogDO; +import com.yunxi.scm.module.system.enums.logger.LoginResultEnum; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface LoginLogMapper extends BaseMapperX { + + default PageResult selectPage(LoginLogPageReqVO reqVO) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(LoginLogDO::getUserIp, reqVO.getUserIp()) + .likeIfPresent(LoginLogDO::getUsername, reqVO.getUsername()) + .betweenIfPresent(LoginLogDO::getCreateTime, reqVO.getCreateTime()); + if (Boolean.TRUE.equals(reqVO.getStatus())) { + query.eq(LoginLogDO::getResult, LoginResultEnum.SUCCESS.getResult()); + } else if (Boolean.FALSE.equals(reqVO.getStatus())) { + query.gt(LoginLogDO::getResult, LoginResultEnum.SUCCESS.getResult()); + } + query.orderByDesc(LoginLogDO::getId); // 降序 + return selectPage(reqVO, query); + } + + default List selectList(LoginLogExportReqVO reqVO) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(LoginLogDO::getUserIp, reqVO.getUserIp()) + .likeIfPresent(LoginLogDO::getUsername, reqVO.getUsername()) + .betweenIfPresent(LoginLogDO::getCreateTime, reqVO.getCreateTime()); + if (Boolean.TRUE.equals(reqVO.getStatus())) { + query.eq(LoginLogDO::getResult, LoginResultEnum.SUCCESS.getResult()); + } else if (Boolean.FALSE.equals(reqVO.getStatus())) { + query.gt(LoginLogDO::getResult, LoginResultEnum.SUCCESS.getResult()); + } + query.orderByDesc(LoginLogDO::getId); // 降序 + return selectList(query); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/logger/OperateLogMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/logger/OperateLogMapper.java new file mode 100644 index 0000000..2130df3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/logger/OperateLogMapper.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.system.dal.mysql.logger; + +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.logger.OperateLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface OperateLogMapper extends BaseMapperX { + + default PageResult selectPage(OperateLogPageReqVO reqVO, Collection userIds) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(OperateLogDO::getModule, reqVO.getModule()) + .inIfPresent(OperateLogDO::getUserId, userIds) + .eqIfPresent(OperateLogDO::getType, reqVO.getType()) + .betweenIfPresent(OperateLogDO::getStartTime, reqVO.getStartTime()); + if (Boolean.TRUE.equals(reqVO.getSuccess())) { + query.eq(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } else if (Boolean.FALSE.equals(reqVO.getSuccess())) { + query.gt(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + query.orderByDesc(OperateLogDO::getId); // 降序 + return selectPage(reqVO, query); + } + + default List selectList(OperateLogExportReqVO reqVO, Collection userIds) { + LambdaQueryWrapperX query = new LambdaQueryWrapperX() + .likeIfPresent(OperateLogDO::getModule, reqVO.getModule()) + .inIfPresent(OperateLogDO::getUserId, userIds) + .eqIfPresent(OperateLogDO::getType, reqVO.getType()) + .betweenIfPresent(OperateLogDO::getStartTime, reqVO.getStartTime()); + if (Boolean.TRUE.equals(reqVO.getSuccess())) { + query.eq(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } else if (Boolean.FALSE.equals(reqVO.getSuccess())) { + query.gt(OperateLogDO::getResultCode, GlobalErrorCodeConstants.SUCCESS.getCode()); + } + query.orderByDesc(OperateLogDO::getId); // 降序 + return selectList(query); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailAccountMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailAccountMapper.java new file mode 100644 index 0000000..f523d4d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailAccountMapper.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.dal.mysql.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.framework.mybatis.core.query.QueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MailAccountMapper extends BaseMapperX { + + default PageResult selectPage(MailAccountPageReqVO pageReqVO) { + return selectPage(pageReqVO, new LambdaQueryWrapperX() + .likeIfPresent(MailAccountDO::getMail, pageReqVO.getMail()) + .likeIfPresent(MailAccountDO::getUsername , pageReqVO.getUsername())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailLogMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailLogMapper.java new file mode 100644 index 0000000..27b3ae1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailLogMapper.java @@ -0,0 +1,25 @@ +package com.yunxi.scm.module.system.dal.mysql.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailLogDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MailLogMapper extends BaseMapperX { + + default PageResult selectPage(MailLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(MailLogDO::getUserId, reqVO.getUserId()) + .eqIfPresent(MailLogDO::getUserType, reqVO.getUserType()) + .likeIfPresent(MailLogDO::getToMail, reqVO.getToMail()) + .eqIfPresent(MailLogDO::getAccountId, reqVO.getAccountId()) + .eqIfPresent(MailLogDO::getTemplateId, reqVO.getTemplateId()) + .eqIfPresent(MailLogDO::getSendStatus, reqVO.getSendStatus()) + .betweenIfPresent(MailLogDO::getSendTime, reqVO.getSendTime()) + .orderByDesc(MailLogDO::getId)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailTemplateMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailTemplateMapper.java new file mode 100644 index 0000000..64f0b54 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/mail/MailTemplateMapper.java @@ -0,0 +1,35 @@ +package com.yunxi.scm.module.system.dal.mysql.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.framework.mybatis.core.query.QueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.util.Date; + +@Mapper +public interface MailTemplateMapper extends BaseMapperX { + + default PageResult selectPage(MailTemplatePageReqVO pageReqVO){ + return selectPage(pageReqVO , new LambdaQueryWrapperX() + .eqIfPresent(MailTemplateDO::getStatus, pageReqVO.getStatus()) + .likeIfPresent(MailTemplateDO::getCode, pageReqVO.getCode()) + .likeIfPresent(MailTemplateDO::getName, pageReqVO.getName()) + .eqIfPresent(MailTemplateDO::getAccountId, pageReqVO.getAccountId()) + .betweenIfPresent(MailTemplateDO::getCreateTime, pageReqVO.getCreateTime())); + } + + default Long selectCountByAccountId(Long accountId) { + return selectCount(MailTemplateDO::getAccountId, accountId); + } + + default MailTemplateDO selectByCode(String code) { + return selectOne(MailTemplateDO::getCode, code); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notice/NoticeMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notice/NoticeMapper.java new file mode 100644 index 0000000..e5377ef --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notice/NoticeMapper.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.dal.mysql.notice; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notice.NoticeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NoticeMapper extends BaseMapperX { + + default PageResult selectPage(NoticePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(NoticeDO::getTitle, reqVO.getTitle()) + .eqIfPresent(NoticeDO::getStatus, reqVO.getStatus()) + .orderByDesc(NoticeDO::getId)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notify/NotifyMessageMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notify/NotifyMessageMapper.java new file mode 100644 index 0000000..dcd1d89 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notify/NotifyMessageMapper.java @@ -0,0 +1,70 @@ +package com.yunxi.scm.module.system.dal.mysql.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.framework.mybatis.core.query.QueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyMessageDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +@Mapper +public interface NotifyMessageMapper extends BaseMapperX { + + default PageResult selectPage(NotifyMessagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(NotifyMessageDO::getUserId, reqVO.getUserId()) + .eqIfPresent(NotifyMessageDO::getUserType, reqVO.getUserType()) + .likeIfPresent(NotifyMessageDO::getTemplateCode, reqVO.getTemplateCode()) + .eqIfPresent(NotifyMessageDO::getTemplateType, reqVO.getTemplateType()) + .betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(NotifyMessageDO::getId)); + } + + default PageResult selectPage(NotifyMessageMyPageReqVO reqVO, Long userId, Integer userType) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(NotifyMessageDO::getReadStatus, reqVO.getReadStatus()) + .betweenIfPresent(NotifyMessageDO::getCreateTime, reqVO.getCreateTime()) + .eq(NotifyMessageDO::getUserId, userId) + .eq(NotifyMessageDO::getUserType, userType) + .orderByDesc(NotifyMessageDO::getId)); + } + + default int updateListRead(Collection ids, Long userId, Integer userType) { + return update(new NotifyMessageDO().setReadStatus(true).setReadTime(LocalDateTime.now()), + new LambdaQueryWrapperX() + .in(NotifyMessageDO::getId, ids) + .eq(NotifyMessageDO::getUserId, userId) + .eq(NotifyMessageDO::getUserType, userType) + .eq(NotifyMessageDO::getReadStatus, false)); + } + + default int updateListRead(Long userId, Integer userType) { + return update(new NotifyMessageDO().setReadStatus(true).setReadTime(LocalDateTime.now()), + new LambdaQueryWrapperX() + .eq(NotifyMessageDO::getUserId, userId) + .eq(NotifyMessageDO::getUserType, userType) + .eq(NotifyMessageDO::getReadStatus, false)); + } + + default List selectUnreadListByUserIdAndUserType(Long userId, Integer userType, Integer size) { + return selectList(new QueryWrapperX() // 由于要使用 limitN 语句,所以只能用 QueryWrapperX + .eq("user_id", userId) + .eq("user_type", userType) + .eq("read_status", false) + .orderByDesc("id").limitN(size)); + } + + default Long selectUnreadCountByUserIdAndUserType(Long userId, Integer userType) { + return selectCount(new LambdaQueryWrapperX() + .eq(NotifyMessageDO::getReadStatus, false) + .eq(NotifyMessageDO::getUserId, userId) + .eq(NotifyMessageDO::getUserType, userType)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notify/NotifyTemplateMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notify/NotifyTemplateMapper.java new file mode 100644 index 0000000..710da08 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/notify/NotifyTemplateMapper.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.dal.mysql.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface NotifyTemplateMapper extends BaseMapperX { + + default NotifyTemplateDO selectByCode(String code) { + return selectOne(NotifyTemplateDO::getCode, code); + } + + default PageResult selectPage(NotifyTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(NotifyTemplateDO::getCode, reqVO.getCode()) + .likeIfPresent(NotifyTemplateDO::getName, reqVO.getName()) + .eqIfPresent(NotifyTemplateDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(NotifyTemplateDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(NotifyTemplateDO::getId)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java new file mode 100644 index 0000000..43d5a5d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2AccessTokenMapper.java @@ -0,0 +1,33 @@ +package com.yunxi.scm.module.system.dal.mysql.oauth2; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import org.apache.ibatis.annotations.Mapper; + +import java.time.LocalDateTime; +import java.util.List; + +@Mapper +public interface OAuth2AccessTokenMapper extends BaseMapperX { + + default OAuth2AccessTokenDO selectByAccessToken(String accessToken) { + return selectOne(OAuth2AccessTokenDO::getAccessToken, accessToken); + } + + default List selectListByRefreshToken(String refreshToken) { + return selectList(OAuth2AccessTokenDO::getRefreshToken, refreshToken); + } + + default PageResult selectPage(OAuth2AccessTokenPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(OAuth2AccessTokenDO::getUserId, reqVO.getUserId()) + .eqIfPresent(OAuth2AccessTokenDO::getUserType, reqVO.getUserType()) + .likeIfPresent(OAuth2AccessTokenDO::getClientId, reqVO.getClientId()) + .gt(OAuth2AccessTokenDO::getExpiresTime, LocalDateTime.now()) + .orderByDesc(OAuth2AccessTokenDO::getId)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java new file mode 100644 index 0000000..ac99ec7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2ApproveMapper.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.dal.mysql.oauth2; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface OAuth2ApproveMapper extends BaseMapperX { + + default int update(OAuth2ApproveDO updateObj) { + return update(updateObj, new LambdaQueryWrapperX() + .eq(OAuth2ApproveDO::getUserId, updateObj.getUserId()) + .eq(OAuth2ApproveDO::getUserType, updateObj.getUserType()) + .eq(OAuth2ApproveDO::getClientId, updateObj.getClientId()) + .eq(OAuth2ApproveDO::getScope, updateObj.getScope())); + } + + default List selectListByUserIdAndUserTypeAndClientId(Long userId, Integer userType, String clientId) { + return selectList(new LambdaQueryWrapperX() + .eq(OAuth2ApproveDO::getUserId, userId) + .eq(OAuth2ApproveDO::getUserType, userType) + .eq(OAuth2ApproveDO::getClientId, clientId)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java new file mode 100644 index 0000000..30c1aec --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2ClientMapper.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.dal.mysql.oauth2; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import org.apache.ibatis.annotations.Mapper; + + +/** + * OAuth2 客户端 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface OAuth2ClientMapper extends BaseMapperX { + + default PageResult selectPage(OAuth2ClientPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(OAuth2ClientDO::getName, reqVO.getName()) + .eqIfPresent(OAuth2ClientDO::getStatus, reqVO.getStatus()) + .orderByDesc(OAuth2ClientDO::getId)); + } + + default OAuth2ClientDO selectByClientId(String clientId) { + return selectOne(OAuth2ClientDO::getClientId, clientId); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java new file mode 100644 index 0000000..390f4ae --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2CodeMapper.java @@ -0,0 +1,14 @@ +package com.yunxi.scm.module.system.dal.mysql.oauth2; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OAuth2CodeMapper extends BaseMapperX { + + default OAuth2CodeDO selectByCode(String code) { + return selectOne(OAuth2CodeDO::getCode, code); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java new file mode 100644 index 0000000..9213287 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/oauth2/OAuth2RefreshTokenMapper.java @@ -0,0 +1,20 @@ +package com.yunxi.scm.module.system.dal.mysql.oauth2; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface OAuth2RefreshTokenMapper extends BaseMapperX { + + default int deleteByRefreshToken(String refreshToken) { + return delete(new LambdaQueryWrapperX() + .eq(OAuth2RefreshTokenDO::getRefreshToken, refreshToken)); + } + + default OAuth2RefreshTokenDO selectByRefreshToken(String refreshToken) { + return selectOne(OAuth2RefreshTokenDO::getRefreshToken, refreshToken); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/package-info.java new file mode 100644 index 0000000..d110702 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/package-info.java @@ -0,0 +1,9 @@ +/** + * DAL = Data Access Layer 数据访问层 + * 1. data object:数据对象 + * 2. redis:Redis 的 CRUD 操作 + * 3. mysql:MySQL 的 CRUD 操作 + * + * 其中,MySQL 的表以 system_ 作为前缀 + */ +package com.yunxi.scm.module.system.dal.mysql; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/MenuMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/MenuMapper.java new file mode 100644 index 0000000..e0f23d7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/MenuMapper.java @@ -0,0 +1,31 @@ +package com.yunxi.scm.module.system.dal.mysql.permission; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface MenuMapper extends BaseMapperX { + + default MenuDO selectByParentIdAndName(Long parentId, String name) { + return selectOne(MenuDO::getParentId, parentId, MenuDO::getName, name); + } + + default Long selectCountByParentId(Long parentId) { + return selectCount(MenuDO::getParentId, parentId); + } + + default List selectList(MenuListReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(MenuDO::getName, reqVO.getName()) + .eqIfPresent(MenuDO::getStatus, reqVO.getStatus())); + } + + default List selectListByPermission(String permission) { + return selectList(MenuDO::getPermission, permission); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/RoleMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/RoleMapper.java new file mode 100644 index 0000000..cd59645 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/RoleMapper.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.system.dal.mysql.permission; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.dataobject.BaseDO; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleExportReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import org.apache.ibatis.annotations.Mapper; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface RoleMapper extends BaseMapperX { + + default PageResult selectPage(RolePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(RoleDO::getName, reqVO.getName()) + .likeIfPresent(RoleDO::getCode, reqVO.getCode()) + .eqIfPresent(RoleDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(RoleDO::getId)); + } + + default List selectList(RoleExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(RoleDO::getName, reqVO.getName()) + .likeIfPresent(RoleDO::getCode, reqVO.getCode()) + .eqIfPresent(RoleDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(BaseDO::getCreateTime, reqVO.getCreateTime())); + } + + default RoleDO selectByName(String name) { + return selectOne(RoleDO::getName, name); + } + + default RoleDO selectByCode(String code) { + return selectOne(RoleDO::getCode, code); + } + + default List selectListByStatus(@Nullable Collection statuses) { + return selectList(RoleDO::getStatus, statuses); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/RoleMenuMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/RoleMenuMapper.java new file mode 100644 index 0000000..a85e5de --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/RoleMenuMapper.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.dal.mysql.permission; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleMenuDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface RoleMenuMapper extends BaseMapperX { + + default List selectListByRoleId(Long roleId) { + return selectList(RoleMenuDO::getRoleId, roleId); + } + + default List selectListByRoleId(Collection roleIds) { + return selectList(RoleMenuDO::getRoleId, roleIds); + } + + default List selectListByMenuId(Long menuId) { + return selectList(RoleMenuDO::getMenuId, menuId); + } + + default void deleteListByRoleIdAndMenuIds(Long roleId, Collection menuIds) { + delete(new LambdaQueryWrapper() + .eq(RoleMenuDO::getRoleId, roleId) + .in(RoleMenuDO::getMenuId, menuIds)); + } + + default void deleteListByMenuId(Long menuId) { + delete(new LambdaQueryWrapper().eq(RoleMenuDO::getMenuId, menuId)); + } + + default void deleteListByRoleId(Long roleId) { + delete(new LambdaQueryWrapper().eq(RoleMenuDO::getRoleId, roleId)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/UserRoleMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/UserRoleMapper.java new file mode 100644 index 0000000..f439d2d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/permission/UserRoleMapper.java @@ -0,0 +1,36 @@ +package com.yunxi.scm.module.system.dal.mysql.permission; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.module.system.dal.dataobject.permission.UserRoleDO; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface UserRoleMapper extends BaseMapperX { + + default List selectListByUserId(Long userId) { + return selectList(UserRoleDO::getUserId, userId); + } + + default void deleteListByUserIdAndRoleIdIds(Long userId, Collection roleIds) { + delete(new LambdaQueryWrapper() + .eq(UserRoleDO::getUserId, userId) + .in(UserRoleDO::getRoleId, roleIds)); + } + + default void deleteListByUserId(Long userId) { + delete(new LambdaQueryWrapper().eq(UserRoleDO::getUserId, userId)); + } + + default void deleteListByRoleId(Long roleId) { + delete(new LambdaQueryWrapper().eq(UserRoleDO::getRoleId, roleId)); + } + + default List selectListByRoleIds(Collection roleIds) { + return selectList(UserRoleDO::getRoleId, roleIds); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java new file mode 100644 index 0000000..0f75a47 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sensitiveword/SensitiveWordMapper.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.system.dal.mysql.sensitiveword; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 敏感词 Mapper + * + * @author 永不言败 + */ +@Mapper +public interface SensitiveWordMapper extends BaseMapperX { + + default PageResult selectPage(SensitiveWordPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SensitiveWordDO::getName, reqVO.getName()) + .likeIfPresent(SensitiveWordDO::getTags, reqVO.getTag()) + .eqIfPresent(SensitiveWordDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SensitiveWordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SensitiveWordDO::getId)); + } + + default List selectList(SensitiveWordExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(SensitiveWordDO::getName, reqVO.getName()) + .likeIfPresent(SensitiveWordDO::getTags, reqVO.getTag()) + .eqIfPresent(SensitiveWordDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SensitiveWordDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SensitiveWordDO::getId)); + } + + default SensitiveWordDO selectByName(String name) { + return selectOne(SensitiveWordDO::getName, name); + } + + @Select("SELECT COUNT(*) FROM system_sensitive_word WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxTime); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsChannelMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsChannelMapper.java new file mode 100644 index 0000000..d42eea3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsChannelMapper.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.dal.mysql.sms; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Select; + +import java.time.LocalDateTime; + +@Mapper +public interface SmsChannelMapper extends BaseMapperX { + + default PageResult selectPage(SmsChannelPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(SmsChannelDO::getSignature, reqVO.getSignature()) + .eqIfPresent(SmsChannelDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(SmsChannelDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SmsChannelDO::getId)); + } + + @Select("SELECT COUNT(*) FROM system_sms_channel WHERE update_time > #{maxUpdateTime}") + Long selectCountByUpdateTimeGt(LocalDateTime maxTime); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsCodeMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsCodeMapper.java new file mode 100644 index 0000000..449f6c3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsCodeMapper.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.dal.mysql.sms; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.QueryWrapperX; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsCodeDO; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface SmsCodeMapper extends BaseMapperX { + + /** + * 获得手机号的最后一个手机验证码 + * + * @param mobile 手机号 + * @param scene 发送场景,选填 + * @param code 验证码 选填 + * @return 手机验证码 + */ + default SmsCodeDO selectLastByMobile(String mobile, String code, Integer scene) { + return selectOne(new QueryWrapperX() + .eq("mobile", mobile) + .eqIfPresent("scene", scene) + .eqIfPresent("code", code) + .orderByDesc("id") + .limitN(1)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsLogMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsLogMapper.java new file mode 100644 index 0000000..1170ba5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsLogMapper.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.dal.mysql.sms; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsLogDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SmsLogMapper extends BaseMapperX { + + default PageResult selectPage(SmsLogPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(SmsLogDO::getChannelId, reqVO.getChannelId()) + .eqIfPresent(SmsLogDO::getTemplateId, reqVO.getTemplateId()) + .likeIfPresent(SmsLogDO::getMobile, reqVO.getMobile()) + .eqIfPresent(SmsLogDO::getSendStatus, reqVO.getSendStatus()) + .betweenIfPresent(SmsLogDO::getSendTime, reqVO.getSendTime()) + .eqIfPresent(SmsLogDO::getReceiveStatus, reqVO.getReceiveStatus()) + .betweenIfPresent(SmsLogDO::getReceiveTime, reqVO.getReceiveTime()) + .orderByDesc(SmsLogDO::getId)); + } + + default List selectList(SmsLogExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(SmsLogDO::getChannelId, reqVO.getChannelId()) + .eqIfPresent(SmsLogDO::getTemplateId, reqVO.getTemplateId()) + .likeIfPresent(SmsLogDO::getMobile, reqVO.getMobile()) + .eqIfPresent(SmsLogDO::getSendStatus, reqVO.getSendStatus()) + .betweenIfPresent(SmsLogDO::getSendTime, reqVO.getSendTime()) + .eqIfPresent(SmsLogDO::getReceiveStatus, reqVO.getReceiveStatus()) + .betweenIfPresent(SmsLogDO::getReceiveTime, reqVO.getReceiveTime()) + .orderByDesc(SmsLogDO::getId)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsTemplateMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsTemplateMapper.java new file mode 100644 index 0000000..731bc26 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/sms/SmsTemplateMapper.java @@ -0,0 +1,48 @@ +package com.yunxi.scm.module.system.dal.mysql.sms; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SmsTemplateMapper extends BaseMapperX { + + default SmsTemplateDO selectByCode(String code) { + return selectOne(SmsTemplateDO::getCode, code); + } + + default PageResult selectPage(SmsTemplatePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .eqIfPresent(SmsTemplateDO::getType, reqVO.getType()) + .eqIfPresent(SmsTemplateDO::getStatus, reqVO.getStatus()) + .likeIfPresent(SmsTemplateDO::getCode, reqVO.getCode()) + .likeIfPresent(SmsTemplateDO::getContent, reqVO.getContent()) + .likeIfPresent(SmsTemplateDO::getApiTemplateId, reqVO.getApiTemplateId()) + .eqIfPresent(SmsTemplateDO::getChannelId, reqVO.getChannelId()) + .betweenIfPresent(SmsTemplateDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SmsTemplateDO::getId)); + } + + default List selectList(SmsTemplateExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .eqIfPresent(SmsTemplateDO::getType, reqVO.getType()) + .eqIfPresent(SmsTemplateDO::getStatus, reqVO.getStatus()) + .likeIfPresent(SmsTemplateDO::getCode, reqVO.getCode()) + .likeIfPresent(SmsTemplateDO::getContent, reqVO.getContent()) + .likeIfPresent(SmsTemplateDO::getApiTemplateId, reqVO.getApiTemplateId()) + .eqIfPresent(SmsTemplateDO::getChannelId, reqVO.getChannelId()) + .betweenIfPresent(SmsTemplateDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(SmsTemplateDO::getId)); + } + + default Long selectCountByChannelId(Long channelId) { + return selectCount(SmsTemplateDO::getChannelId, channelId); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/social/SocialUserBindMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/social/SocialUserBindMapper.java new file mode 100644 index 0000000..89f5c48 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/social/SocialUserBindMapper.java @@ -0,0 +1,37 @@ +package com.yunxi.scm.module.system.dal.mysql.social; + +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserBindDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface SocialUserBindMapper extends BaseMapperX { + + default void deleteByUserTypeAndUserIdAndSocialType(Integer userType, Long userId, Integer socialType) { + delete(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getSocialType, socialType)); + } + + default void deleteByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { + delete(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserType, userType) + .eq(SocialUserBindDO::getSocialUserId, socialUserId)); + } + + default SocialUserBindDO selectByUserTypeAndSocialUserId(Integer userType, Long socialUserId) { + return selectOne(SocialUserBindDO::getUserType, userType, + SocialUserBindDO::getSocialUserId, socialUserId); + } + + default List selectListByUserIdAndUserType(Long userId, Integer userType) { + return selectList(new LambdaQueryWrapperX() + .eq(SocialUserBindDO::getUserId, userId) + .eq(SocialUserBindDO::getUserType, userType)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/social/SocialUserMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/social/SocialUserMapper.java new file mode 100644 index 0000000..c796ab0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/social/SocialUserMapper.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.dal.mysql.social; + +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserDO; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface SocialUserMapper extends BaseMapperX { + + default SocialUserDO selectByTypeAndCodeAnState(Integer type, String code, String state) { + return selectOne(new LambdaQueryWrapper() + .eq(SocialUserDO::getType, type) + .eq(SocialUserDO::getCode, code) + .eq(SocialUserDO::getState, state)); + } + + default SocialUserDO selectByTypeAndOpenid(Integer type, String openid) { + return selectOne(new LambdaQueryWrapper() + .eq(SocialUserDO::getType, type) + .eq(SocialUserDO::getOpenid, openid)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/tenant/TenantMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/tenant/TenantMapper.java new file mode 100644 index 0000000..79bc109 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/tenant/TenantMapper.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.system.dal.mysql.tenant; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantExportReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 租户 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface TenantMapper extends BaseMapperX { + + default PageResult selectPage(TenantPageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TenantDO::getName, reqVO.getName()) + .likeIfPresent(TenantDO::getContactName, reqVO.getContactName()) + .likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile()) + .eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TenantDO::getId)); + } + + default List selectList(TenantExportReqVO reqVO) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(TenantDO::getName, reqVO.getName()) + .likeIfPresent(TenantDO::getContactName, reqVO.getContactName()) + .likeIfPresent(TenantDO::getContactMobile, reqVO.getContactMobile()) + .eqIfPresent(TenantDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(TenantDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TenantDO::getId)); + } + + default TenantDO selectByName(String name) { + return selectOne(TenantDO::getName, name); + } + + default Long selectCountByPackageId(Long packageId) { + return selectCount(TenantDO::getPackageId, packageId); + } + + default List selectListByPackageId(Long packageId) { + return selectList(TenantDO::getPackageId, packageId); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/tenant/TenantPackageMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/tenant/TenantPackageMapper.java new file mode 100644 index 0000000..8b5fc16 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/tenant/TenantPackageMapper.java @@ -0,0 +1,32 @@ +package com.yunxi.scm.module.system.dal.mysql.tenant; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantPackageDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +/** + * 租户套餐 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface TenantPackageMapper extends BaseMapperX { + + default PageResult selectPage(TenantPackagePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(TenantPackageDO::getName, reqVO.getName()) + .eqIfPresent(TenantPackageDO::getStatus, reqVO.getStatus()) + .likeIfPresent(TenantPackageDO::getRemark, reqVO.getRemark()) + .betweenIfPresent(TenantPackageDO::getCreateTime, reqVO.getCreateTime()) + .orderByDesc(TenantPackageDO::getId)); + } + + default List selectListByStatus(Integer status) { + return selectList(TenantPackageDO::getStatus, status); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/user/AdminUserMapper.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/user/AdminUserMapper.java new file mode 100644 index 0000000..0ea75ee --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/mysql/user/AdminUserMapper.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.system.dal.mysql.user; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.mapper.BaseMapperX; +import com.yunxi.scm.framework.mybatis.core.query.LambdaQueryWrapperX; +import com.yunxi.scm.module.system.controller.admin.user.vo.user.UserExportReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.user.UserPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import org.apache.ibatis.annotations.Mapper; + +import java.util.Collection; +import java.util.List; + +@Mapper +public interface AdminUserMapper extends BaseMapperX { + + default AdminUserDO selectByUsername(String username) { + return selectOne(AdminUserDO::getUsername, username); + } + + default AdminUserDO selectByEmail(String email) { + return selectOne(AdminUserDO::getEmail, email); + } + + default AdminUserDO selectByMobile(String mobile) { + return selectOne(AdminUserDO::getMobile, mobile); + } + + default PageResult selectPage(UserPageReqVO reqVO, Collection deptIds) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()) + .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()) + .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime()) + .inIfPresent(AdminUserDO::getDeptId, deptIds) + .orderByDesc(AdminUserDO::getId)); + } + + default List selectList(UserExportReqVO reqVO, Collection deptIds) { + return selectList(new LambdaQueryWrapperX() + .likeIfPresent(AdminUserDO::getUsername, reqVO.getUsername()) + .likeIfPresent(AdminUserDO::getMobile, reqVO.getMobile()) + .eqIfPresent(AdminUserDO::getStatus, reqVO.getStatus()) + .betweenIfPresent(AdminUserDO::getCreateTime, reqVO.getCreateTime()) + .inIfPresent(AdminUserDO::getDeptId, deptIds)); + } + + default List selectListByNickname(String nickname) { + return selectList(new LambdaQueryWrapperX().like(AdminUserDO::getNickname, nickname)); + } + + default List selectListByStatus(Integer status) { + return selectList(AdminUserDO::getStatus, status); + } + + default List selectListByDeptIds(Collection deptIds) { + return selectList(AdminUserDO::getDeptId, deptIds); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/redis/RedisKeyConstants.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/redis/RedisKeyConstants.java new file mode 100644 index 0000000..e7386b0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/redis/RedisKeyConstants.java @@ -0,0 +1,101 @@ +package com.yunxi.scm.module.system.dal.redis; + +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +/** + * System Redis Key 枚举类 + * + * @author 芋道源码 + */ +public interface RedisKeyConstants { + + /** + * 指定部门的所有子部门编号数组的缓存 + *

+ * KEY 格式:dept_children_ids:{id} + * VALUE 数据类型:String 子部门编号集合 + */ + String DEPT_CHILDREN_ID_LIST = "dept_children_ids"; + + /** + * 角色的缓存 + *

+ * KEY 格式:role:{id} + * VALUE 数据类型:String 角色信息 + */ + String ROLE = "role"; + + /** + * 用户拥有的角色编号的缓存 + *

+ * KEY 格式:user_role_ids:{userId} + * VALUE 数据类型:String 角色编号集合 + */ + String USER_ROLE_ID_LIST = "user_role_ids"; + + /** + * 拥有指定菜单的角色编号的缓存 + *

+ * KEY 格式:user_role_ids:{menuId} + * VALUE 数据类型:String 角色编号集合 + */ + String MENU_ROLE_ID_LIST = "menu_role_ids"; + + /** + * 拥有权限对应的菜单编号数组的缓存 + *

+ * KEY 格式:permission_menu_ids:{permission} + * VALUE 数据类型:String 菜单编号数组 + */ + String PERMISSION_MENU_ID_LIST = "permission_menu_ids"; + + /** + * OAuth2 客户端的缓存 + *

+ * KEY 格式:user:{id} + * VALUE 数据类型:String 客户端信息 + */ + String OAUTH_CLIENT = "oauth_client"; + + /** + * 访问令牌的缓存 + *

+ * KEY 格式:oauth2_access_token:{token} + * VALUE 数据类型:String 访问令牌信息 {@link OAuth2AccessTokenDO} + *

+ * 由于动态过期时间,使用 RedisTemplate 操作 + */ + String OAUTH2_ACCESS_TOKEN = "oauth2_access_token:%s"; + + /** + * 站内信模版的缓存 + *

+ * KEY 格式:notify_template:{code} + * VALUE 数据格式:String 模版信息 + */ + String NOTIFY_TEMPLATE = "notify_template"; + + /** + * 邮件账号的缓存 + *

+ * KEY 格式:sms_template:{id} + * VALUE 数据格式:String 账号信息 + */ + String MAIL_ACCOUNT = "mail_account"; + + /** + * 邮件模版的缓存 + *

+ * KEY 格式:mail_template:{code} + * VALUE 数据格式:String 模版信息 + */ + String MAIL_TEMPLATE = "mail_template"; + + /** + * 短信模版的缓存 + *

+ * KEY 格式:sms_template:{id} + * VALUE 数据格式:String 模版信息 + */ + String SMS_TEMPLATE = "sms_template"; +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java new file mode 100644 index 0000000..6745274 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/dal/redis/oauth2/OAuth2AccessTokenRedisDAO.java @@ -0,0 +1,59 @@ +package com.yunxi.scm.module.system.dal.redis.oauth2; + +import cn.hutool.core.date.LocalDateTimeUtil; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.json.JsonUtils; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Repository; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN; + +/** + * {@link OAuth2AccessTokenDO} 的 RedisDAO + * + * @author 芋道源码 + */ +@Repository +public class OAuth2AccessTokenRedisDAO { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + public OAuth2AccessTokenDO get(String accessToken) { + String redisKey = formatKey(accessToken); + return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class); + } + + public void set(OAuth2AccessTokenDO accessTokenDO) { + String redisKey = formatKey(accessTokenDO.getAccessToken()); + // 清理多余字段,避免缓存 + accessTokenDO.setUpdater(null).setUpdateTime(null).setCreateTime(null).setCreator(null).setDeleted(null); + long time = LocalDateTimeUtil.between(LocalDateTime.now(), accessTokenDO.getExpiresTime(), ChronoUnit.SECONDS); + if (time > 0) { + stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(accessTokenDO), time, TimeUnit.SECONDS); + } + } + + public void delete(String accessToken) { + String redisKey = formatKey(accessToken); + stringRedisTemplate.delete(redisKey); + } + + public void deleteList(Collection accessTokens) { + List redisKeys = CollectionUtils.convertList(accessTokens, OAuth2AccessTokenRedisDAO::formatKey); + stringRedisTemplate.delete(redisKeys); + } + + private static String formatKey(String accessToken) { + return String.format(OAUTH2_ACCESS_TOKEN, accessToken); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/datapermission/config/DataPermissionConfiguration.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/datapermission/config/DataPermissionConfiguration.java new file mode 100644 index 0000000..403a823 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/datapermission/config/DataPermissionConfiguration.java @@ -0,0 +1,28 @@ +package com.yunxi.scm.module.system.framework.datapermission.config; + +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * system 模块的数据权限 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class DataPermissionConfiguration { + + @Bean + public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { + return rule -> { + // dept + rule.addDeptColumn(AdminUserDO.class); + rule.addDeptColumn(DeptDO.class, "id"); + // user + rule.addUserColumn(AdminUserDO.class, "id"); + }; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/datapermission/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/datapermission/package-info.java new file mode 100644 index 0000000..65199d4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/datapermission/package-info.java @@ -0,0 +1,4 @@ +/** + * system 模块的数据权限配置 + */ +package com.yunxi.scm.module.system.framework.datapermission; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/package-info.java new file mode 100644 index 0000000..ff2e3f1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/package-info.java @@ -0,0 +1,6 @@ +/** + * 属于 system 模块的 framework 封装 + * + * @author 芋道源码 + */ +package com.yunxi.scm.module.system.framework; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/sms/SmsCodeConfiguration.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/sms/SmsCodeConfiguration.java new file mode 100644 index 0000000..041a681 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/sms/SmsCodeConfiguration.java @@ -0,0 +1,9 @@ +package com.yunxi.scm.module.system.framework.sms; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties(SmsCodeProperties.class) +public class SmsCodeConfiguration { +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/sms/SmsCodeProperties.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/sms/SmsCodeProperties.java new file mode 100644 index 0000000..6491f69 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/sms/SmsCodeProperties.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.system.framework.sms; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotNull; +import java.time.Duration; + +@ConfigurationProperties(prefix = "yunxi.sms-code") +@Validated +@Data +public class SmsCodeProperties { + + /** + * 过期时间 + */ + @NotNull(message = "过期时间不能为空") + private Duration expireTimes; + /** + * 短信发送频率 + */ + @NotNull(message = "短信发送频率不能为空") + private Duration sendFrequency; + /** + * 每日发送最大数量 + */ + @NotNull(message = "每日发送最大数量不能为空") + private Integer sendMaximumQuantityPerDay; + /** + * 验证码最小值 + */ + @NotNull(message = "验证码最小值不能为空") + private Integer beginCode; + /** + * 验证码最大值 + */ + @NotNull(message = "验证码最大值不能为空") + private Integer endCode; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/web/config/SystemWebConfiguration.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/web/config/SystemWebConfiguration.java new file mode 100644 index 0000000..34d8bc2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/web/config/SystemWebConfiguration.java @@ -0,0 +1,24 @@ +package com.yunxi.scm.module.system.framework.web.config; + +import com.yunxi.scm.framework.swagger.config.YunxiSwaggerAutoConfiguration; +import org.springdoc.core.GroupedOpenApi; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * system 模块的 web 组件的 Configuration + * + * @author 芋道源码 + */ +@Configuration(proxyBeanMethods = false) +public class SystemWebConfiguration { + + /** + * system 模块的 API 分组 + */ + @Bean + public GroupedOpenApi systemGroupedOpenApi() { + return YunxiSwaggerAutoConfiguration.buildGroupedOpenApi("system"); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/web/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/web/package-info.java new file mode 100644 index 0000000..4417667 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/framework/web/package-info.java @@ -0,0 +1,4 @@ +/** + * system 模块的 web 配置 + */ +package com.yunxi.scm.module.system.framework.web; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/job/DemoJob.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/job/DemoJob.java new file mode 100644 index 0000000..4906434 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/job/DemoJob.java @@ -0,0 +1,27 @@ +package com.yunxi.scm.module.system.job; + +import com.yunxi.scm.framework.quartz.core.handler.JobHandler; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.tenant.core.job.TenantJob; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.dal.mysql.user.AdminUserMapper; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +@Component +@TenantJob // 标记多租户 +public class DemoJob implements JobHandler { + + @Resource + private AdminUserMapper adminUserMapper; + + @Override + public String execute(String param) throws Exception { + System.out.println("当前租户:" + TenantContextHolder.getTenantId()); + List users = adminUserMapper.selectList(); + return "用户数量:" + users.size(); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/job/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/job/package-info.java new file mode 100644 index 0000000..a75bd24 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/job/package-info.java @@ -0,0 +1 @@ +package com.yunxi.scm.module.system.job; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/consumer/mail/MailSendConsumer.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/consumer/mail/MailSendConsumer.java new file mode 100644 index 0000000..83696cc --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/consumer/mail/MailSendConsumer.java @@ -0,0 +1,30 @@ +package com.yunxi.scm.module.system.mq.consumer.mail; + +import com.yunxi.scm.framework.mq.core.stream.AbstractStreamMessageListener; +import com.yunxi.scm.module.system.mq.message.mail.MailSendMessage; +import com.yunxi.scm.module.system.mq.message.sms.SmsSendMessage; +import com.yunxi.scm.module.system.service.mail.MailSendService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link MailSendMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class MailSendConsumer extends AbstractStreamMessageListener { + + @Resource + private MailSendService mailSendService; + + @Override + public void onMessage(MailSendMessage message) { + log.info("[onMessage][消息内容({})]", message); + mailSendService.doSendMail(message); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/consumer/sms/SmsSendConsumer.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/consumer/sms/SmsSendConsumer.java new file mode 100644 index 0000000..cb0c8e8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/consumer/sms/SmsSendConsumer.java @@ -0,0 +1,29 @@ +package com.yunxi.scm.module.system.mq.consumer.sms; + +import com.yunxi.scm.module.system.mq.message.sms.SmsSendMessage; +import com.yunxi.scm.module.system.service.sms.SmsSendService; +import com.yunxi.scm.framework.mq.core.stream.AbstractStreamMessageListener; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SmsSendMessage} 的消费者 + * + * @author zzf + */ +@Component +@Slf4j +public class SmsSendConsumer extends AbstractStreamMessageListener { + + @Resource + private SmsSendService smsSendService; + + @Override + public void onMessage(SmsSendMessage message) { + log.info("[onMessage][消息内容({})]", message); + smsSendService.doSendSms(message); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/message/mail/MailSendMessage.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/message/mail/MailSendMessage.java new file mode 100644 index 0000000..033638f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/message/mail/MailSendMessage.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.system.mq.message.mail; + +import com.yunxi.scm.framework.mq.core.stream.AbstractStreamMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +/** + * 邮箱发送消息 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class MailSendMessage extends AbstractStreamMessage { + + /** + * 邮件日志编号 + */ + @NotNull(message = "邮件日志编号不能为空") + private Long logId; + /** + * 接收邮件地址 + */ + @NotNull(message = "接收邮件地址不能为空") + private String mail; + /** + * 邮件账号编号 + */ + @NotNull(message = "邮件账号编号不能为空") + private Long accountId; + + /** + * 邮件发件人 + */ + private String nickname; + /** + * 邮件标题 + */ + @NotEmpty(message = "邮件标题不能为空") + private String title; + /** + * 邮件内容 + */ + @NotEmpty(message = "邮件内容不能为空") + private String content; + + @Override + public String getStreamKey() { + return "system.mail.send"; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/message/sms/SmsSendMessage.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/message/sms/SmsSendMessage.java new file mode 100644 index 0000000..19bd749 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/message/sms/SmsSendMessage.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.module.system.mq.message.sms; + +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.mq.core.stream.AbstractStreamMessage; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 短信发送消息 + * + * @author 芋道源码 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SmsSendMessage extends AbstractStreamMessage { + + /** + * 短信日志编号 + */ + @NotNull(message = "短信日志编号不能为空") + private Long logId; + /** + * 手机号 + */ + @NotNull(message = "手机号不能为空") + private String mobile; + /** + * 短信渠道编号 + */ + @NotNull(message = "短信渠道编号不能为空") + private Long channelId; + /** + * 短信 API 的模板编号 + */ + @NotNull(message = "短信 API 的模板编号不能为空") + private String apiTemplateId; + /** + * 短信模板参数 + */ + private List> templateParams; + + @Override + public String getStreamKey() { + return "system.sms.send"; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/producer/mail/MailProducer.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/producer/mail/MailProducer.java new file mode 100644 index 0000000..042e149 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/producer/mail/MailProducer.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.system.mq.producer.mail; + +import com.yunxi.scm.framework.mq.core.RedisMQTemplate; +import com.yunxi.scm.module.system.mq.message.mail.MailSendMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * Mail 邮件相关消息的 Producer + * + * @author wangjingyi + * @since 2021/4/19 13:33 + */ +@Slf4j +@Component +public class MailProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link MailSendMessage} 消息 + * + * @param sendLogId 发送日志编码 + * @param mail 接收邮件地址 + * @param accountId 邮件账号编号 + * @param nickname 邮件发件人 + * @param title 邮件标题 + * @param content 邮件内容 + */ + public void sendMailSendMessage(Long sendLogId, String mail, Long accountId, + String nickname, String title, String content) { + MailSendMessage message = new MailSendMessage() + .setLogId(sendLogId).setMail(mail).setAccountId(accountId) + .setNickname(nickname).setTitle(title).setContent(content); + redisMQTemplate.send(message); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/producer/sms/SmsProducer.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/producer/sms/SmsProducer.java new file mode 100644 index 0000000..d060617 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/mq/producer/sms/SmsProducer.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.system.mq.producer.sms; + +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.mq.core.RedisMQTemplate; +import com.yunxi.scm.module.system.mq.message.sms.SmsSendMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; +import java.util.List; + +/** + * Sms 短信相关消息的 Producer + * + * @author zzf + * @since 2021/3/9 16:35 + */ +@Slf4j +@Component +public class SmsProducer { + + @Resource + private RedisMQTemplate redisMQTemplate; + + /** + * 发送 {@link SmsSendMessage} 消息 + * + * @param logId 短信日志编号 + * @param mobile 手机号 + * @param channelId 渠道编号 + * @param apiTemplateId 短信模板编号 + * @param templateParams 短信模板参数 + */ + public void sendSmsSendMessage(Long logId, String mobile, + Long channelId, String apiTemplateId, List> templateParams) { + SmsSendMessage message = new SmsSendMessage().setLogId(logId).setMobile(mobile); + message.setChannelId(channelId).setApiTemplateId(apiTemplateId).setTemplateParams(templateParams); + redisMQTemplate.send(message); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/package-info.java new file mode 100644 index 0000000..cc7a31d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/package-info.java @@ -0,0 +1,8 @@ +/** + * system 模块下,我们放通用业务,支撑上层的核心业务。 + * 例如说:用户、部门、权限、数据字典等等 + * + * 1. Controller URL:以 /system/ 开头,避免和其它 Module 冲突 + * 2. DataObject 表名:以 system_ 开头,方便在数据库中区分 + */ +package com.yunxi.scm.module.system; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/auth/AdminAuthService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/auth/AdminAuthService.java new file mode 100644 index 0000000..e89602e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/auth/AdminAuthService.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.module.system.service.auth; + +import com.yunxi.scm.module.system.controller.admin.auth.vo.*; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; + +import javax.validation.Valid; + +/** + * 管理后台的认证 Service 接口 + * + * 提供用户的登录、登出的能力 + * + * @author 芋道源码 + */ +public interface AdminAuthService { + + /** + * 验证账号 + 密码。如果通过,则返回用户 + * + * @param username 账号 + * @param password 密码 + * @return 用户 + */ + AdminUserDO authenticate(String username, String password); + + /** + * 账号登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO login(@Valid AuthLoginReqVO reqVO); + + /** + * 基于 token 退出登录 + * + * @param token token + * @param logType 登出类型 + */ + void logout(String token, Integer logType); + + /** + * 短信验证码发送 + * + * @param reqVO 发送请求 + */ + void sendSmsCode(AuthSmsSendReqVO reqVO); + + /** + * 短信登录 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) ; + + /** + * 社交快捷登录,使用 code 授权码 + * + * @param reqVO 登录信息 + * @return 登录结果 + */ + AuthLoginRespVO socialLogin(@Valid AuthSocialLoginReqVO reqVO); + + /** + * 刷新访问令牌 + * + * @param refreshToken 刷新令牌 + * @return 登录结果 + */ + AuthLoginRespVO refreshToken(String refreshToken); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/auth/AdminAuthServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/auth/AdminAuthServiceImpl.java new file mode 100644 index 0000000..bda0b33 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/auth/AdminAuthServiceImpl.java @@ -0,0 +1,249 @@ +package com.yunxi.scm.module.system.service.auth; + +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.util.monitor.TracerUtils; +import com.yunxi.scm.framework.common.util.servlet.ServletUtils; +import com.yunxi.scm.framework.common.util.validation.ValidationUtils; +import com.yunxi.scm.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.yunxi.scm.module.system.api.sms.SmsCodeApi; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.controller.admin.auth.vo.*; +import com.yunxi.scm.module.system.convert.auth.AuthConvert; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.enums.logger.LoginLogTypeEnum; +import com.yunxi.scm.module.system.enums.logger.LoginResultEnum; +import com.yunxi.scm.module.system.enums.oauth2.OAuth2ClientConstants; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import com.yunxi.scm.module.system.service.logger.LoginLogService; +import com.yunxi.scm.module.system.service.member.MemberService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2TokenService; +import com.yunxi.scm.module.system.service.social.SocialUserService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import com.xingyuv.captcha.model.common.ResponseModel; +import com.xingyuv.captcha.model.vo.CaptchaVO; +import com.xingyuv.captcha.service.CaptchaService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import javax.validation.Validator; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.servlet.ServletUtils.getClientIP; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * Auth Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class AdminAuthServiceImpl implements AdminAuthService { + + @Resource + private AdminUserService userService; + @Resource + private LoginLogService loginLogService; + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private SocialUserService socialUserService; + @Resource + private MemberService memberService; + @Resource + private Validator validator; + @Resource + private CaptchaService captchaService; + @Resource + private SmsCodeApi smsCodeApi; + + /** + * 验证码的开关,默认为 true + */ + @Value("${yunxi.captcha.enable:true}") + private Boolean captchaEnable; + + @Override + public AdminUserDO authenticate(String username, String password) { + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; + // 校验账号是否存在 + AdminUserDO user = userService.getUserByUsername(username); + if (user == null) { + createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + if (!userService.isPasswordMatch(password, user.getPassword())) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); + } + // 校验是否禁用 + if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); + throw exception(AUTH_LOGIN_USER_DISABLED); + } + return user; + } + + @Override + public AuthLoginRespVO login(AuthLoginReqVO reqVO) { + // 校验验证码 + validateCaptcha(reqVO); + + // 使用账号密码,进行登录 + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); + + // 如果 socialType 非空,说明需要绑定社交用户 + if (reqVO.getSocialType() != null) { + socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(), + reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState())); + } + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); + } + + @Override + public void sendSmsCode(AuthSmsSendReqVO reqVO) { + // 登录场景,验证是否存在 + if (userService.getUserByMobile(reqVO.getMobile()) == null) { + throw exception(AUTH_MOBILE_NOT_EXISTS); + } + // 发送验证码 + smsCodeApi.sendSmsCode(AuthConvert.INSTANCE.convert(reqVO).setCreateIp(getClientIP())); + } + + @Override + public AuthLoginRespVO smsLogin(AuthSmsLoginReqVO reqVO) { + // 校验验证码 + smsCodeApi.useSmsCode(AuthConvert.INSTANCE.convert(reqVO, SmsSceneEnum.ADMIN_MEMBER_LOGIN.getScene(), getClientIP())); + + // 获得用户信息 + AdminUserDO user = userService.getUserByMobile(reqVO.getMobile()); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), reqVO.getMobile(), LoginLogTypeEnum.LOGIN_MOBILE); + } + + private void createLoginLog(Long userId, String username, + LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { + // 插入登录日志 + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logTypeEnum.getType()); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(getUserType().getValue()); + reqDTO.setUsername(username); + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setResult(loginResult.getResult()); + loginLogService.createLoginLog(reqDTO); + // 更新最后登录时间 + if (userId != null && Objects.equals(LoginResultEnum.SUCCESS.getResult(), loginResult.getResult())) { + userService.updateUserLogin(userId, ServletUtils.getClientIP()); + } + } + + @Override + public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) { + // 使用 code 授权码,进行登录。然后,获得到绑定的用户编号 + Long userId = socialUserService.getBindUserId(UserTypeEnum.ADMIN.getValue(), reqVO.getType(), + reqVO.getCode(), reqVO.getState()); + if (userId == null) { + throw exception(AUTH_THIRD_LOGIN_NOT_BIND); + } + + // 获得用户 + AdminUserDO user = userService.getUser(userId); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + + // 创建 Token 令牌,记录登录日志 + return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL); + } + + @VisibleForTesting + void validateCaptcha(AuthLoginReqVO reqVO) { + // 如果验证码关闭,则不进行校验 + if (!captchaEnable) { + return; + } + // 校验验证码 + ValidationUtils.validate(validator, reqVO, AuthLoginReqVO.CodeEnableGroup.class); + CaptchaVO captchaVO = new CaptchaVO(); + captchaVO.setCaptchaVerification(reqVO.getCaptchaVerification()); + ResponseModel response = captchaService.verification(captchaVO); + // 验证不通过 + if (!response.isSuccess()) { + // 创建登录失败日志(验证码不正确) + createLoginLog(null, reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME, LoginResultEnum.CAPTCHA_CODE_ERROR); + throw exception(AUTH_LOGIN_CAPTCHA_CODE_ERROR, response.getRepMsg()); + } + } + + private AuthLoginRespVO createTokenAfterLoginSuccess(Long userId, String username, LoginLogTypeEnum logType) { + // 插入登陆日志 + createLoginLog(userId, username, logType, LoginResultEnum.SUCCESS); + // 创建访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, getUserType().getValue(), + OAuth2ClientConstants.CLIENT_ID_DEFAULT, null); + // 构建返回结果 + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + @Override + public AuthLoginRespVO refreshToken(String refreshToken) { + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, OAuth2ClientConstants.CLIENT_ID_DEFAULT); + return AuthConvert.INSTANCE.convert(accessTokenDO); + } + + @Override + public void logout(String token, Integer logType) { + // 删除访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token); + if (accessTokenDO == null) { + return; + } + // 删除成功,则记录登出日志 + createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType); + } + + private void createLogoutLog(Long userId, Integer userType, Integer logType) { + LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO(); + reqDTO.setLogType(logType); + reqDTO.setTraceId(TracerUtils.getTraceId()); + reqDTO.setUserId(userId); + reqDTO.setUserType(userType); + if (ObjectUtil.equal(getUserType().getValue(), userType)) { + reqDTO.setUsername(getUsername(userId)); + } else { + reqDTO.setUsername(memberService.getMemberUserMobile(userId)); + } + reqDTO.setUserAgent(ServletUtils.getUserAgent()); + reqDTO.setUserIp(ServletUtils.getClientIP()); + reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); + loginLogService.createLoginLog(reqDTO); + } + + private String getUsername(Long userId) { + if (userId == null) { + return null; + } + AdminUserDO user = userService.getUser(userId); + return user != null ? user.getUsername() : null; + } + + private UserTypeEnum getUserType() { + return UserTypeEnum.ADMIN; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/DeptService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/DeptService.java new file mode 100644 index 0000000..dc73e62 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/DeptService.java @@ -0,0 +1,104 @@ +package com.yunxi.scm.module.system.service.dept; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; + +import java.util.*; + +/** + * 部门 Service 接口 + * + * @author 芋道源码 + */ +public interface DeptService { + + /** + * 创建部门 + * + * @param reqVO 部门信息 + * @return 部门编号 + */ + Long createDept(DeptCreateReqVO reqVO); + + /** + * 更新部门 + * + * @param reqVO 部门信息 + */ + void updateDept(DeptUpdateReqVO reqVO); + + /** + * 删除部门 + * + * @param id 部门编号 + */ + void deleteDept(Long id); + + /** + * 获得部门信息 + * + * @param id 部门编号 + * @return 部门信息 + */ + DeptDO getDept(Long id); + + /** + * 获得部门信息数组 + * + * @param ids 部门编号数组 + * @return 部门信息数组 + */ + List getDeptList(Collection ids); + + /** + * 筛选部门列表 + * + * @param reqVO 筛选条件请求 VO + * @return 部门列表 + */ + List getDeptList(DeptListReqVO reqVO); + + /** + * 获得指定编号的部门 Map + * + * @param ids 部门编号数组 + * @return 部门 Map + */ + default Map getDeptMap(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyMap(); + } + List list = getDeptList(ids); + return CollectionUtils.convertMap(list, DeptDO::getId); + } + + /** + * 获得指定部门的所有子部门 + * + * @param id 部门编号 + * @return 子部门列表 + */ + List getChildDeptList(Long id); + + /** + * 获得所有子部门,从缓存中 + * + * @param id 父部门编号 + * @return 子部门列表 + */ + Set getChildDeptIdListFromCache(Long id); + + /** + * 校验部门们是否有效。如下情况,视为无效: + * 1. 部门编号不存在 + * 2. 部门被禁用 + * + * @param ids 角色编号数组 + */ + void validateDeptList(Collection ids); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/DeptServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/DeptServiceImpl.java new file mode 100644 index 0000000..d904739 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/DeptServiceImpl.java @@ -0,0 +1,205 @@ +package com.yunxi.scm.module.system.service.dept; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; +import com.yunxi.scm.module.system.convert.dept.DeptConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.mysql.dept.DeptMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import com.yunxi.scm.module.system.enums.dept.DeptIdEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 部门 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class DeptServiceImpl implements DeptService { + + @Resource + private DeptMapper deptMapper; + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 + public Long createDept(DeptCreateReqVO reqVO) { + // 校验正确性 + if (reqVO.getParentId() == null) { + reqVO.setParentId(DeptIdEnum.ROOT.getId()); + } + validateForCreateOrUpdate(null, reqVO.getParentId(), reqVO.getName()); + // 插入部门 + DeptDO dept = DeptConvert.INSTANCE.convert(reqVO); + deptMapper.insert(dept); + return dept.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 + public void updateDept(DeptUpdateReqVO reqVO) { + // 校验正确性 + if (reqVO.getParentId() == null) { + reqVO.setParentId(DeptIdEnum.ROOT.getId()); + } + validateForCreateOrUpdate(reqVO.getId(), reqVO.getParentId(), reqVO.getName()); + // 更新部门 + DeptDO updateObj = DeptConvert.INSTANCE.convert(reqVO); + deptMapper.updateById(updateObj); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为操作一个部门,涉及到多个缓存 + public void deleteDept(Long id) { + // 校验是否存在 + validateDeptExists(id); + // 校验是否有子部门 + if (deptMapper.selectCountByParentId(id) > 0) { + throw exception(DEPT_EXITS_CHILDREN); + } + // 删除部门 + deptMapper.deleteById(id); + } + + private void validateForCreateOrUpdate(Long id, Long parentId, String name) { + // 校验自己存在 + validateDeptExists(id); + // 校验父部门的有效性 + validateParentDept(id, parentId); + // 校验部门名的唯一性 + validateDeptNameUnique(id, parentId, name); + } + + @VisibleForTesting + void validateDeptExists(Long id) { + if (id == null) { + return; + } + DeptDO dept = deptMapper.selectById(id); + if (dept == null) { + throw exception(DEPT_NOT_FOUND); + } + } + + @VisibleForTesting + void validateParentDept(Long id, Long parentId) { + if (parentId == null || DeptIdEnum.ROOT.getId().equals(parentId)) { + return; + } + // 不能设置自己为父部门 + if (parentId.equals(id)) { + throw exception(DEPT_PARENT_ERROR); + } + // 父岗位不存在 + DeptDO dept = deptMapper.selectById(parentId); + if (dept == null) { + throw exception(DEPT_PARENT_NOT_EXITS); + } + // 父部门不能是原来的子部门 + List children = getChildDeptList(id); + if (children.stream().anyMatch(dept1 -> dept1.getId().equals(parentId))) { + throw exception(DEPT_PARENT_IS_CHILD); + } + } + + @VisibleForTesting + void validateDeptNameUnique(Long id, Long parentId, String name) { + DeptDO dept = deptMapper.selectByParentIdAndName(parentId, name); + if (dept == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的岗位 + if (id == null) { + throw exception(DEPT_NAME_DUPLICATE); + } + if (ObjectUtil.notEqual(dept.getId(), id)) { + throw exception(DEPT_NAME_DUPLICATE); + } + } + + @Override + public DeptDO getDept(Long id) { + return deptMapper.selectById(id); + } + + @Override + public List getDeptList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return deptMapper.selectBatchIds(ids); + } + + @Override + public List getDeptList(DeptListReqVO reqVO) { + return deptMapper.selectList(reqVO); + } + + @Override + public List getChildDeptList(Long id) { + List children = new LinkedList<>(); + // 遍历每一层 + Collection parentIds = Collections.singleton(id); + for (int i = 0; i < Short.MAX_VALUE; i++) { // 使用 Short.MAX_VALUE 避免 bug 场景下,存在死循环 + // 查询当前层,所有的子部门 + List depts = deptMapper.selectListByParentId(parentIds); + // 1. 如果没有子部门,则结束遍历 + if (CollUtil.isEmpty(depts)) { + break; + } + // 2. 如果有子部门,继续遍历 + children.addAll(depts); + parentIds = convertSet(depts, DeptDO::getId); + } + return children; + } + + @Override + @DataPermission(enable = false) // 禁用数据权限,避免简历不正确的缓存 + @Cacheable(cacheNames = RedisKeyConstants.DEPT_CHILDREN_ID_LIST, key = "#id") + public Set getChildDeptIdListFromCache(Long id) { + List children = getChildDeptList(id); + return convertSet(children, DeptDO::getId); + } + + @Override + public void validateDeptList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得科室信息 + Map deptMap = getDeptMap(ids); + // 校验 + ids.forEach(id -> { + DeptDO dept = deptMap.get(id); + if (dept == null) { + throw exception(DEPT_NOT_FOUND); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) { + throw exception(DEPT_NOT_ENABLE, dept.getName()); + } + }); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/PostService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/PostService.java new file mode 100644 index 0000000..6dd7bd2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/PostService.java @@ -0,0 +1,98 @@ +package com.yunxi.scm.module.system.service.dept; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import org.springframework.lang.Nullable; + +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; + +/** + * 岗位 Service 接口 + * + * @author 芋道源码 + */ +public interface PostService { + + /** + * 创建岗位 + * + * @param reqVO 岗位信息 + * @return 岗位编号 + */ + Long createPost(PostCreateReqVO reqVO); + + /** + * 更新岗位 + * + * @param reqVO 岗位信息 + */ + void updatePost(PostUpdateReqVO reqVO); + + /** + * 删除岗位信息 + * + * @param id 岗位编号 + */ + void deletePost(Long id); + + /** + * 获得岗位列表 + * + * @param ids 岗位编号数组。如果为空,不进行筛选 + * @return 部门列表 + */ + default List getPostList(@Nullable Collection ids) { + return getPostList(ids, asSet(CommonStatusEnum.ENABLE.getStatus(), CommonStatusEnum.DISABLE.getStatus())); + } + + /** + * 获得符合条件的岗位列表 + * + * @param ids 岗位编号数组。如果为空,不进行筛选 + * @param statuses 状态数组。如果为空,不进行筛选 + * @return 部门列表 + */ + List getPostList(@Nullable Collection ids, @Nullable Collection statuses); + + /** + * 获得岗位分页列表 + * + * @param reqVO 分页条件 + * @return 部门分页列表 + */ + PageResult getPostPage(PostPageReqVO reqVO); + + /** + * 获得岗位列表 + * + * @param reqVO 查询条件 + * @return 部门列表 + */ + List getPostList(PostExportReqVO reqVO); + + /** + * 获得岗位信息 + * + * @param id 岗位编号 + * @return 岗位信息 + */ + PostDO getPost(Long id); + + /** + * 校验岗位们是否有效。如下情况,视为无效: + * 1. 岗位编号不存在 + * 2. 岗位被禁用 + * + * @param ids 岗位编号数组 + */ + void validatePostList(Collection ids); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/PostServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/PostServiceImpl.java new file mode 100644 index 0000000..bb42e53 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dept/PostServiceImpl.java @@ -0,0 +1,151 @@ +package com.yunxi.scm.module.system.service.dept; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostUpdateReqVO; +import com.yunxi.scm.module.system.convert.dept.PostConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import com.yunxi.scm.module.system.dal.mysql.dept.PostMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 岗位 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class PostServiceImpl implements PostService { + + @Resource + private PostMapper postMapper; + + @Override + public Long createPost(PostCreateReqVO reqVO) { + // 校验正确性 + validatePostForCreateOrUpdate(null, reqVO.getName(), reqVO.getCode()); + + // 插入岗位 + PostDO post = PostConvert.INSTANCE.convert(reqVO); + postMapper.insert(post); + return post.getId(); + } + + @Override + public void updatePost(PostUpdateReqVO reqVO) { + // 校验正确性 + validatePostForCreateOrUpdate(reqVO.getId(), reqVO.getName(), reqVO.getCode()); + + // 更新岗位 + PostDO updateObj = PostConvert.INSTANCE.convert(reqVO); + postMapper.updateById(updateObj); + } + + @Override + public void deletePost(Long id) { + // 校验是否存在 + validatePostExists(id); + // 删除部门 + postMapper.deleteById(id); + } + + private void validatePostForCreateOrUpdate(Long id, String name, String code) { + // 校验自己存在 + validatePostExists(id); + // 校验岗位名的唯一性 + validatePostNameUnique(id, name); + // 校验岗位编码的唯一性 + validatePostCodeUnique(id, code); + } + + private void validatePostNameUnique(Long id, String name) { + PostDO post = postMapper.selectByName(name); + if (post == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的岗位 + if (id == null) { + throw exception(POST_NAME_DUPLICATE); + } + if (!post.getId().equals(id)) { + throw exception(POST_NAME_DUPLICATE); + } + } + + private void validatePostCodeUnique(Long id, String code) { + PostDO post = postMapper.selectByCode(code); + if (post == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的岗位 + if (id == null) { + throw exception(POST_CODE_DUPLICATE); + } + if (!post.getId().equals(id)) { + throw exception(POST_CODE_DUPLICATE); + } + } + + private void validatePostExists(Long id) { + if (id == null) { + return; + } + if (postMapper.selectById(id) == null) { + throw exception(POST_NOT_FOUND); + } + } + + @Override + public List getPostList(Collection ids, Collection statuses) { + return postMapper.selectList(ids, statuses); + } + + @Override + public PageResult getPostPage(PostPageReqVO reqVO) { + return postMapper.selectPage(reqVO); + } + + @Override + public List getPostList(PostExportReqVO reqVO) { + return postMapper.selectList(reqVO); + } + + @Override + public PostDO getPost(Long id) { + return postMapper.selectById(id); + } + + @Override + public void validatePostList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得岗位信息 + List posts = postMapper.selectBatchIds(ids); + Map postMap = convertMap(posts, PostDO::getId); + // 校验 + ids.forEach(id -> { + PostDO post = postMap.get(id); + if (post == null) { + throw exception(POST_NOT_FOUND); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(post.getStatus())) { + throw exception(POST_NOT_ENABLE, post.getName()); + } + }); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictDataService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictDataService.java new file mode 100644 index 0000000..f2468e4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictDataService.java @@ -0,0 +1,108 @@ +package com.yunxi.scm.module.system.service.dict; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictDataDO; + +import java.util.Collection; +import java.util.List; + +/** + * 字典数据 Service 接口 + * + * @author ruoyi + */ +public interface DictDataService { + + /** + * 创建字典数据 + * + * @param reqVO 字典数据信息 + * @return 字典数据编号 + */ + Long createDictData(DictDataCreateReqVO reqVO); + + /** + * 更新字典数据 + * + * @param reqVO 字典数据信息 + */ + void updateDictData(DictDataUpdateReqVO reqVO); + + /** + * 删除字典数据 + * + * @param id 字典数据编号 + */ + void deleteDictData(Long id); + + /** + * 获得字典数据列表 + * + * @return 字典数据全列表 + */ + List getDictDataList(); + + /** + * 获得字典数据分页列表 + * + * @param reqVO 分页请求 + * @return 字典数据分页列表 + */ + PageResult getDictDataPage(DictDataPageReqVO reqVO); + + /** + * 获得字典数据列表 + * + * @param reqVO 列表请求 + * @return 字典数据列表 + */ + List getDictDataList(DictDataExportReqVO reqVO); + + /** + * 获得字典数据详情 + * + * @param id 字典数据编号 + * @return 字典数据 + */ + DictDataDO getDictData(Long id); + + /** + * 获得指定字典类型的数据数量 + * + * @param dictType 字典类型 + * @return 数据数量 + */ + long countByDictType(String dictType); + + /** + * 校验字典数据们是否有效。如下情况,视为无效: + * 1. 字典数据不存在 + * 2. 字典数据被禁用 + * + * @param dictType 字典类型 + * @param values 字典数据值的数组 + */ + void validateDictDataList(String dictType, Collection values); + + /** + * 获得指定的字典数据 + * + * @param dictType 字典类型 + * @param value 字典数据值 + * @return 字典数据 + */ + DictDataDO getDictData(String dictType, String value); + + /** + * 解析获得指定的字典数据,从缓存中 + * + * @param dictType 字典类型 + * @param label 字典数据标签 + * @return 字典数据 + */ + DictDataDO parseDictData(String dictType, String label); +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictDataServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictDataServiceImpl.java new file mode 100644 index 0000000..61dc720 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictDataServiceImpl.java @@ -0,0 +1,184 @@ +package com.yunxi.scm.module.system.service.dict; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataUpdateReqVO; +import com.yunxi.scm.module.system.convert.dict.DictDataConvert; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictDataDO; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictTypeDO; +import com.yunxi.scm.module.system.dal.mysql.dict.DictDataMapper; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 字典数据 Service 实现类 + * + * @author ruoyi + */ +@Service +@Slf4j +public class DictDataServiceImpl implements DictDataService { + + /** + * 排序 dictType > sort + */ + private static final Comparator COMPARATOR_TYPE_AND_SORT = Comparator + .comparing(DictDataDO::getDictType) + .thenComparingInt(DictDataDO::getSort); + + @Resource + private DictTypeService dictTypeService; + + @Resource + private DictDataMapper dictDataMapper; + + @Override + public List getDictDataList() { + List list = dictDataMapper.selectList(); + list.sort(COMPARATOR_TYPE_AND_SORT); + return list; + } + + @Override + public PageResult getDictDataPage(DictDataPageReqVO reqVO) { + return dictDataMapper.selectPage(reqVO); + } + + @Override + public List getDictDataList(DictDataExportReqVO reqVO) { + List list = dictDataMapper.selectList(reqVO); + list.sort(COMPARATOR_TYPE_AND_SORT); + return list; + } + + @Override + public DictDataDO getDictData(Long id) { + return dictDataMapper.selectById(id); + } + + @Override + public Long createDictData(DictDataCreateReqVO reqVO) { + // 校验正确性 + validateDictDataForCreateOrUpdate(null, reqVO.getValue(), reqVO.getDictType()); + + // 插入字典类型 + DictDataDO dictData = DictDataConvert.INSTANCE.convert(reqVO); + dictDataMapper.insert(dictData); + return dictData.getId(); + } + + @Override + public void updateDictData(DictDataUpdateReqVO reqVO) { + // 校验正确性 + validateDictDataForCreateOrUpdate(reqVO.getId(), reqVO.getValue(), reqVO.getDictType()); + + // 更新字典类型 + DictDataDO updateObj = DictDataConvert.INSTANCE.convert(reqVO); + dictDataMapper.updateById(updateObj); + } + + @Override + public void deleteDictData(Long id) { + // 校验是否存在 + validateDictDataExists(id); + + // 删除字典数据 + dictDataMapper.deleteById(id); + } + + @Override + public long countByDictType(String dictType) { + return dictDataMapper.selectCountByDictType(dictType); + } + + private void validateDictDataForCreateOrUpdate(Long id, String value, String dictType) { + // 校验自己存在 + validateDictDataExists(id); + // 校验字典类型有效 + validateDictTypeExists(dictType); + // 校验字典数据的值的唯一性 + validateDictDataValueUnique(id, dictType, value); + } + + @VisibleForTesting + public void validateDictDataValueUnique(Long id, String dictType, String value) { + DictDataDO dictData = dictDataMapper.selectByDictTypeAndValue(dictType, value); + if (dictData == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典数据 + if (id == null) { + throw exception(DICT_DATA_VALUE_DUPLICATE); + } + if (!dictData.getId().equals(id)) { + throw exception(DICT_DATA_VALUE_DUPLICATE); + } + } + + @VisibleForTesting + public void validateDictDataExists(Long id) { + if (id == null) { + return; + } + DictDataDO dictData = dictDataMapper.selectById(id); + if (dictData == null) { + throw exception(DICT_DATA_NOT_EXISTS); + } + } + + @VisibleForTesting + public void validateDictTypeExists(String type) { + DictTypeDO dictType = dictTypeService.getDictType(type); + if (dictType == null) { + throw exception(DICT_TYPE_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dictType.getStatus())) { + throw exception(DICT_TYPE_NOT_ENABLE); + } + } + + @Override + public void validateDictDataList(String dictType, Collection values) { + if (CollUtil.isEmpty(values)) { + return; + } + Map dictDataMap = CollectionUtils.convertMap( + dictDataMapper.selectByDictTypeAndValues(dictType, values), DictDataDO::getValue); + // 校验 + values.forEach(value -> { + DictDataDO dictData = dictDataMap.get(value); + if (dictData == null) { + throw exception(DICT_DATA_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(dictData.getStatus())) { + throw exception(DICT_DATA_NOT_ENABLE, dictData.getLabel()); + } + }); + } + + @Override + public DictDataDO getDictData(String dictType, String value) { + return dictDataMapper.selectByDictTypeAndValue(dictType, value); + } + + @Override + public DictDataDO parseDictData(String dictType, String label) { + return dictDataMapper.selectByDictTypeAndLabel(dictType, label); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictTypeService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictTypeService.java new file mode 100644 index 0000000..86cb142 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictTypeService.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.system.service.dict; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictTypeDO; + +import java.util.List; + +/** + * 字典类型 Service 接口 + * + * @author 芋道源码 + */ +public interface DictTypeService { + + /** + * 创建字典类型 + * + * @param reqVO 字典类型信息 + * @return 字典类型编号 + */ + Long createDictType(DictTypeCreateReqVO reqVO); + + /** + * 更新字典类型 + * + * @param reqVO 字典类型信息 + */ + void updateDictType(DictTypeUpdateReqVO reqVO); + + /** + * 删除字典类型 + * + * @param id 字典类型编号 + */ + void deleteDictType(Long id); + + /** + * 获得字典类型分页列表 + * + * @param reqVO 分页请求 + * @return 字典类型分页列表 + */ + PageResult getDictTypePage(DictTypePageReqVO reqVO); + + /** + * 获得字典类型列表 + * + * @param reqVO 列表请求 + * @return 字典类型列表 + */ + List getDictTypeList(DictTypeExportReqVO reqVO); + + /** + * 获得字典类型详情 + * + * @param id 字典类型编号 + * @return 字典类型 + */ + DictTypeDO getDictType(Long id); + + /** + * 获得字典类型详情 + * + * @param type 字典类型 + * @return 字典类型详情 + */ + DictTypeDO getDictType(String type); + + /** + * 获得全部字典类型列表 + * + * @return 字典类型列表 + */ + List getDictTypeList(); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictTypeServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictTypeServiceImpl.java new file mode 100644 index 0000000..b97e50a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/dict/DictTypeServiceImpl.java @@ -0,0 +1,150 @@ +package com.yunxi.scm.module.system.service.dict; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeUpdateReqVO; +import com.yunxi.scm.module.system.convert.dict.DictTypeConvert; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictTypeDO; +import com.yunxi.scm.module.system.dal.mysql.dict.DictTypeMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 字典类型 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class DictTypeServiceImpl implements DictTypeService { + + @Resource + private DictDataService dictDataService; + + @Resource + private DictTypeMapper dictTypeMapper; + + @Override + public PageResult getDictTypePage(DictTypePageReqVO reqVO) { + return dictTypeMapper.selectPage(reqVO); + } + + @Override + public List getDictTypeList(DictTypeExportReqVO reqVO) { + return dictTypeMapper.selectList(reqVO); + } + + @Override + public DictTypeDO getDictType(Long id) { + return dictTypeMapper.selectById(id); + } + + @Override + public DictTypeDO getDictType(String type) { + return dictTypeMapper.selectByType(type); + } + + @Override + public Long createDictType(DictTypeCreateReqVO reqVO) { + // 校验正确性 + validateDictTypeForCreateOrUpdate(null, reqVO.getName(), reqVO.getType()); + + // 插入字典类型 + DictTypeDO dictType = DictTypeConvert.INSTANCE.convert(reqVO) + .setDeletedTime(LocalDateTimeUtils.EMPTY); // 唯一索引,避免 null 值 + dictTypeMapper.insert(dictType); + return dictType.getId(); + } + + @Override + public void updateDictType(DictTypeUpdateReqVO reqVO) { + // 校验正确性 + validateDictTypeForCreateOrUpdate(reqVO.getId(), reqVO.getName(), null); + + // 更新字典类型 + DictTypeDO updateObj = DictTypeConvert.INSTANCE.convert(reqVO); + dictTypeMapper.updateById(updateObj); + } + + @Override + public void deleteDictType(Long id) { + // 校验是否存在 + DictTypeDO dictType = validateDictTypeExists(id); + // 校验是否有字典数据 + if (dictDataService.countByDictType(dictType.getType()) > 0) { + throw exception(DICT_TYPE_HAS_CHILDREN); + } + // 删除字典类型 + dictTypeMapper.updateToDelete(id, LocalDateTime.now()); + } + + @Override + public List getDictTypeList() { + return dictTypeMapper.selectList(); + } + + private void validateDictTypeForCreateOrUpdate(Long id, String name, String type) { + // 校验自己存在 + validateDictTypeExists(id); + // 校验字典类型的名字的唯一性 + validateDictTypeNameUnique(id, name); + // 校验字典类型的类型的唯一性 + validateDictTypeUnique(id, type); + } + + @VisibleForTesting + void validateDictTypeNameUnique(Long id, String name) { + DictTypeDO dictType = dictTypeMapper.selectByName(name); + if (dictType == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(DICT_TYPE_NAME_DUPLICATE); + } + if (!dictType.getId().equals(id)) { + throw exception(DICT_TYPE_NAME_DUPLICATE); + } + } + + @VisibleForTesting + void validateDictTypeUnique(Long id, String type) { + if (StrUtil.isEmpty(type)) { + return; + } + DictTypeDO dictType = dictTypeMapper.selectByType(type); + if (dictType == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(DICT_TYPE_TYPE_DUPLICATE); + } + if (!dictType.getId().equals(id)) { + throw exception(DICT_TYPE_TYPE_DUPLICATE); + } + } + + @VisibleForTesting + DictTypeDO validateDictTypeExists(Long id) { + if (id == null) { + return null; + } + DictTypeDO dictType = dictTypeMapper.selectById(id); + if (dictType == null) { + throw exception(DICT_TYPE_NOT_EXISTS); + } + return dictType; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeService.java new file mode 100644 index 0000000..5a49715 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeService.java @@ -0,0 +1,87 @@ +package com.yunxi.scm.module.system.service.errorcode; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeExportReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.errorcode.ErrorCodeDO; + +import javax.validation.Valid; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 错误码 Service 接口 + * + * @author 芋道源码 + */ +public interface ErrorCodeService { + + /** + * 自动创建错误码 + * + * @param autoGenerateDTOs 错误码信息 + */ + void autoGenerateErrorCodes(@Valid List autoGenerateDTOs); + + /** + * 增量获得错误码数组 + * + * 如果 minUpdateTime 为空时,则获取所有错误码 + * + * @param applicationName 应用名 + * @param minUpdateTime 最小更新时间 + * @return 错误码数组 + */ + List getErrorCodeList(String applicationName, LocalDateTime minUpdateTime); + + /** + * 创建错误码 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createErrorCode(@Valid ErrorCodeCreateReqVO createReqVO); + + /** + * 更新错误码 + * + * @param updateReqVO 更新信息 + */ + void updateErrorCode(@Valid ErrorCodeUpdateReqVO updateReqVO); + + /** + * 删除错误码 + * + * @param id 编号 + */ + void deleteErrorCode(Long id); + + /** + * 获得错误码 + * + * @param id 编号 + * @return 错误码 + */ + ErrorCodeDO getErrorCode(Long id); + + /** + * 获得错误码分页 + * + * @param pageReqVO 分页查询 + * @return 错误码分页 + */ + PageResult getErrorCodePage(ErrorCodePageReqVO pageReqVO); + + /** + * 获得错误码列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 错误码列表 + */ + List getErrorCodeList(ErrorCodeExportReqVO exportReqVO); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeServiceImpl.java new file mode 100644 index 0000000..74e47af --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeServiceImpl.java @@ -0,0 +1,174 @@ +package com.yunxi.scm.module.system.service.errorcode; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeExportReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeUpdateReqVO; +import com.yunxi.scm.module.system.convert.errorcode.ErrorCodeConvert; +import com.yunxi.scm.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import com.yunxi.scm.module.system.dal.mysql.errorcode.ErrorCodeMapper; +import com.yunxi.scm.module.system.enums.errorcode.ErrorCodeTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.ERROR_CODE_DUPLICATE; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.ERROR_CODE_NOT_EXISTS; + +/** + * 错误码 Service 实现类 + * + * @author dlyan + */ +@Service +@Validated +@Slf4j +public class ErrorCodeServiceImpl implements ErrorCodeService { + + @Resource + private ErrorCodeMapper errorCodeMapper; + + @Override + public Long createErrorCode(ErrorCodeCreateReqVO createReqVO) { + // 校验 code 重复 + validateCodeDuplicate(createReqVO.getCode(), null); + + // 插入 + ErrorCodeDO errorCode = ErrorCodeConvert.INSTANCE.convert(createReqVO) + .setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()); + errorCodeMapper.insert(errorCode); + // 返回 + return errorCode.getId(); + } + + @Override + public void updateErrorCode(ErrorCodeUpdateReqVO updateReqVO) { + // 校验存在 + validateErrorCodeExists(updateReqVO.getId()); + // 校验 code 重复 + validateCodeDuplicate(updateReqVO.getCode(), updateReqVO.getId()); + + // 更新 + ErrorCodeDO updateObj = ErrorCodeConvert.INSTANCE.convert(updateReqVO) + .setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()); + errorCodeMapper.updateById(updateObj); + } + + @Override + public void deleteErrorCode(Long id) { + // 校验存在 + validateErrorCodeExists(id); + // 删除 + errorCodeMapper.deleteById(id); + } + + /** + * 校验错误码的唯一字段是否重复 + * + * 是否存在相同编码的错误码 + * + * @param code 错误码编码 + * @param id 错误码编号 + */ + @VisibleForTesting + public void validateCodeDuplicate(Integer code, Long id) { + ErrorCodeDO errorCodeDO = errorCodeMapper.selectByCode(code); + if (errorCodeDO == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的错误码 + if (id == null) { + throw exception(ERROR_CODE_DUPLICATE); + } + if (!errorCodeDO.getId().equals(id)) { + throw exception(ERROR_CODE_DUPLICATE); + } + } + + @VisibleForTesting + void validateErrorCodeExists(Long id) { + if (errorCodeMapper.selectById(id) == null) { + throw exception(ERROR_CODE_NOT_EXISTS); + } + } + + @Override + public ErrorCodeDO getErrorCode(Long id) { + return errorCodeMapper.selectById(id); + } + + @Override + public PageResult getErrorCodePage(ErrorCodePageReqVO pageReqVO) { + return errorCodeMapper.selectPage(pageReqVO); + } + + @Override + public List getErrorCodeList(ErrorCodeExportReqVO exportReqVO) { + return errorCodeMapper.selectList(exportReqVO); + } + + @Override + @Transactional + public void autoGenerateErrorCodes(List autoGenerateDTOs) { + if (CollUtil.isEmpty(autoGenerateDTOs)) { + return; + } + // 获得错误码 + List errorCodeDOs = errorCodeMapper.selectListByCodes( + convertSet(autoGenerateDTOs, ErrorCodeAutoGenerateReqDTO::getCode)); + Map errorCodeDOMap = convertMap(errorCodeDOs, ErrorCodeDO::getCode); + + // 遍历 autoGenerateBOs 数组,逐个插入或更新。考虑到每次量级不大,就不走批量了 + autoGenerateDTOs.forEach(autoGenerateDTO -> { + ErrorCodeDO errorCodeDO = errorCodeDOMap.get(autoGenerateDTO.getCode()); + // 不存在,则进行新增 + if (errorCodeDO == null) { + errorCodeDO = ErrorCodeConvert.INSTANCE.convert(autoGenerateDTO) + .setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + errorCodeMapper.insert(errorCodeDO); + return; + } + // 存在,则进行更新。更新有三个前置条件: + // 条件 1. 只更新自动生成的错误码,即 Type 为 ErrorCodeTypeEnum.AUTO_GENERATION + if (!ErrorCodeTypeEnum.AUTO_GENERATION.getType().equals(errorCodeDO.getType())) { + return; + } + // 条件 2. 分组 applicationName 必须匹配,避免存在错误码冲突的情况 + if (!autoGenerateDTO.getApplicationName().equals(errorCodeDO.getApplicationName())) { + log.error("[autoGenerateErrorCodes][自动创建({}/{}) 错误码失败,数据库中已经存在({}/{})]", + autoGenerateDTO.getCode(), autoGenerateDTO.getApplicationName(), + errorCodeDO.getCode(), errorCodeDO.getApplicationName()); + return; + } + // 条件 3. 错误提示语存在差异 + if (autoGenerateDTO.getMessage().equals(errorCodeDO.getMessage())) { + return; + } + // 最终匹配,进行更新 + errorCodeMapper.updateById(new ErrorCodeDO().setId(errorCodeDO.getId()).setMessage(autoGenerateDTO.getMessage())); + }); + } + + @Override + public List getErrorCodeList(String applicationName, LocalDateTime minUpdateTime) { + List errorCodeDOs = errorCodeMapper.selectListByApplicationNameAndUpdateTimeGt( + applicationName, minUpdateTime); + return ErrorCodeConvert.INSTANCE.convertList03(errorCodeDOs); + } + +} + diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/LoginLogService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/LoginLogService.java new file mode 100644 index 0000000..ea1280a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/LoginLogService.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.service.logger; + +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.logger.LoginLogDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.logger.dto.LoginLogCreateReqDTO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 登录日志 Service 接口 + */ +public interface LoginLogService { + + /** + * 获得登录日志分页 + * + * @param reqVO 分页条件 + * @return 登录日志分页 + */ + PageResult getLoginLogPage(LoginLogPageReqVO reqVO); + + /** + * 获得登录日志列表 + * + * @param reqVO 列表条件 + * @return 登录日志列表 + */ + List getLoginLogList(LoginLogExportReqVO reqVO); + + /** + * 创建登录日志 + * + * @param reqDTO 日志信息 + */ + void createLoginLog(@Valid LoginLogCreateReqDTO reqDTO); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/LoginLogServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/LoginLogServiceImpl.java new file mode 100644 index 0000000..98fd320 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/LoginLogServiceImpl.java @@ -0,0 +1,42 @@ +package com.yunxi.scm.module.system.service.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.yunxi.scm.module.system.convert.logger.LoginLogConvert; +import com.yunxi.scm.module.system.dal.dataobject.logger.LoginLogDO; +import com.yunxi.scm.module.system.dal.mysql.logger.LoginLogMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +/** + * 登录日志 Service 实现 + */ +@Service +@Validated +public class LoginLogServiceImpl implements LoginLogService { + + @Resource + private LoginLogMapper loginLogMapper; + + @Override + public PageResult getLoginLogPage(LoginLogPageReqVO reqVO) { + return loginLogMapper.selectPage(reqVO); + } + + @Override + public List getLoginLogList(LoginLogExportReqVO reqVO) { + return loginLogMapper.selectList(reqVO); + } + + @Override + public void createLoginLog(LoginLogCreateReqDTO reqDTO) { + LoginLogDO loginLog = LoginLogConvert.INSTANCE.convert(reqDTO); + loginLogMapper.insert(loginLog); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/OperateLogService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/OperateLogService.java new file mode 100644 index 0000000..77d1e99 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/OperateLogService.java @@ -0,0 +1,41 @@ +package com.yunxi.scm.module.system.service.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.api.logger.dto.OperateLogCreateReqDTO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.logger.OperateLogDO; + +import java.util.List; + +/** + * 操作日志 Service 接口 + * + * @author 芋道源码 + */ +public interface OperateLogService { + + /** + * 记录操作日志 + * + * @param createReqDTO 操作日志请求 + */ + void createOperateLog(OperateLogCreateReqDTO createReqDTO); + + /** + * 获得操作日志分页列表 + * + * @param reqVO 分页条件 + * @return 操作日志分页列表 + */ + PageResult getOperateLogPage(OperateLogPageReqVO reqVO); + + /** + * 获得操作日志列表 + * + * @param reqVO 列表条件 + * @return 日志列表 + */ + List getOperateLogList(OperateLogExportReqVO reqVO); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/OperateLogServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/OperateLogServiceImpl.java new file mode 100644 index 0000000..2d82d91 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/logger/OperateLogServiceImpl.java @@ -0,0 +1,75 @@ +package com.yunxi.scm.module.system.service.logger; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.string.StrUtils; +import com.yunxi.scm.module.system.api.logger.dto.OperateLogCreateReqDTO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.yunxi.scm.module.system.convert.logger.OperateLogConvert; +import com.yunxi.scm.module.system.dal.dataobject.logger.OperateLogDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.dal.mysql.logger.OperateLogMapper; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.system.dal.dataobject.logger.OperateLogDO.JAVA_METHOD_ARGS_MAX_LENGTH; +import static com.yunxi.scm.module.system.dal.dataobject.logger.OperateLogDO.RESULT_MAX_LENGTH; + +@Service +@Validated +@Slf4j +public class OperateLogServiceImpl implements OperateLogService { + + @Resource + private OperateLogMapper operateLogMapper; + + @Resource + private AdminUserService userService; + + @Override + public void createOperateLog(OperateLogCreateReqDTO createReqDTO) { + OperateLogDO logDO = OperateLogConvert.INSTANCE.convert(createReqDTO); + logDO.setJavaMethodArgs(StrUtils.maxLength(logDO.getJavaMethodArgs(), JAVA_METHOD_ARGS_MAX_LENGTH)); + logDO.setResultData(StrUtils.maxLength(logDO.getResultData(), RESULT_MAX_LENGTH)); + operateLogMapper.insert(logDO); + } + + @Override + public PageResult getOperateLogPage(OperateLogPageReqVO reqVO) { + // 处理基于用户昵称的查询 + Collection userIds = null; + if (StrUtil.isNotEmpty(reqVO.getUserNickname())) { + userIds = convertSet(userService.getUserListByNickname(reqVO.getUserNickname()), AdminUserDO::getId); + if (CollUtil.isEmpty(userIds)) { + return PageResult.empty(); + } + } + // 查询分页 + return operateLogMapper.selectPage(reqVO, userIds); + } + + @Override + public List getOperateLogList(OperateLogExportReqVO reqVO) { + // 处理基于用户昵称的查询 + Collection userIds = null; + if (StrUtil.isNotEmpty(reqVO.getUserNickname())) { + userIds = convertSet(userService.getUserListByNickname(reqVO.getUserNickname()), AdminUserDO::getId); + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + } + // 查询列表 + return operateLogMapper.selectList(reqVO, userIds); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailAccountService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailAccountService.java new file mode 100644 index 0000000..7f32b34 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailAccountService.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.module.system.service.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 邮箱账号 Service 接口 + * + * @author wangjingyi + * @since 2022-03-21 + */ +public interface MailAccountService { + + /** + * 创建邮箱账号 + * + * @param createReqVO 邮箱账号信息 + * @return 编号 + */ + Long createMailAccount(@Valid MailAccountCreateReqVO createReqVO); + + /** + * 修改邮箱账号 + * + * @param updateReqVO 邮箱账号信息 + */ + void updateMailAccount(@Valid MailAccountUpdateReqVO updateReqVO); + + /** + * 删除邮箱账号 + * + * @param id 编号 + */ + void deleteMailAccount(Long id); + + /** + * 获取邮箱账号信息 + * + * @param id 编号 + * @return 邮箱账号信息 + */ + MailAccountDO getMailAccount(Long id); + + /** + * 从缓存中获取邮箱账号 + * + * @param id 编号 + * @return 邮箱账号 + */ + MailAccountDO getMailAccountFromCache(Long id); + + /** + * 获取邮箱账号分页信息 + * + * @param pageReqVO 邮箱账号分页参数 + * @return 邮箱账号分页信息 + */ + PageResult getMailAccountPage(MailAccountPageReqVO pageReqVO); + + /** + * 获取邮箱数组信息 + * + * @return 邮箱账号信息数组 + */ + List getMailAccountList(); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailAccountServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailAccountServiceImpl.java new file mode 100644 index 0000000..ba897f3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailAccountServiceImpl.java @@ -0,0 +1,101 @@ +package com.yunxi.scm.module.system.service.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountUpdateReqVO; +import com.yunxi.scm.module.system.convert.mail.MailAccountConvert; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import com.yunxi.scm.module.system.dal.mysql.mail.MailAccountMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_NOT_EXISTS; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS; + +/** + * 邮箱账号 Service 实现类 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@Service +@Validated +@Slf4j +public class MailAccountServiceImpl implements MailAccountService { + + @Resource + private MailAccountMapper mailAccountMapper; + + @Resource + private MailTemplateService mailTemplateService; + + @Override + public Long createMailAccount(MailAccountCreateReqVO createReqVO) { + // 插入 + MailAccountDO account = MailAccountConvert.INSTANCE.convert(createReqVO); + mailAccountMapper.insert(account); + return account.getId(); + } + + @Override + @CacheEvict(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#updateReqVO.id") + public void updateMailAccount(MailAccountUpdateReqVO updateReqVO) { + // 校验是否存在 + validateMailAccountExists(updateReqVO.getId()); + + // 更新 + MailAccountDO updateObj = MailAccountConvert.INSTANCE.convert(updateReqVO); + mailAccountMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#id") + public void deleteMailAccount(Long id) { + // 校验是否存在账号 + validateMailAccountExists(id); + // 校验是否存在关联模版 + if (mailTemplateService.countByAccountId(id) > 0) { + throw exception(MAIL_ACCOUNT_RELATE_TEMPLATE_EXISTS); + } + + // 删除 + mailAccountMapper.deleteById(id); + } + + private void validateMailAccountExists(Long id) { + if (mailAccountMapper.selectById(id) == null) { + throw exception(MAIL_ACCOUNT_NOT_EXISTS); + } + } + + @Override + public MailAccountDO getMailAccount(Long id) { + return mailAccountMapper.selectById(id); + } + + @Override + @Cacheable(value = RedisKeyConstants.MAIL_ACCOUNT, key = "#id", unless = "#result == null") + public MailAccountDO getMailAccountFromCache(Long id) { + return getMailAccount(id); + } + + @Override + public PageResult getMailAccountPage(MailAccountPageReqVO pageReqVO) { + return mailAccountMapper.selectPage(pageReqVO); + } + + @Override + public List getMailAccountList() { + return mailAccountMapper.selectList(); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailLogService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailLogService.java new file mode 100644 index 0000000..12527e8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailLogService.java @@ -0,0 +1,61 @@ +package com.yunxi.scm.module.system.service.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailLogDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; + +import java.util.Map; + +/** + * 邮件日志 Service 接口 + * + * @author wangjingyi + * @since 2022-03-21 + */ +public interface MailLogService { + + /** + * 邮件日志分页 + * + * @param pageVO 分页参数 + * @return 分页结果 + */ + PageResult getMailLogPage(MailLogPageReqVO pageVO); + + /** + * 获得指定编号的邮件日志 + * + * @param id 日志编号 + * @return 邮件日志 + */ + MailLogDO getMailLog(Long id); + + /** + * 创建邮件日志 + * + * @param userId 用户编码 + * @param userType 用户类型 + * @param toMail 收件人邮件 + * @param account 邮件账号信息 + * @param template 模版信息 + * @param templateContent 模版内容 + * @param templateParams 模版参数 + * @param isSend 是否发送成功 + * @return 日志编号 + */ + Long createMailLog(Long userId, Integer userType, String toMail, + MailAccountDO account, MailTemplateDO template , + String templateContent, Map templateParams, Boolean isSend); + + /** + * 更新邮件发送结果 + * + * @param logId 日志编号 + * @param messageId 发送后的消息编号 + * @param exception 发送异常 + */ + void updateMailSendResult(Long logId, String messageId, Exception exception); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailLogServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailLogServiceImpl.java new file mode 100644 index 0000000..2994a32 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailLogServiceImpl.java @@ -0,0 +1,79 @@ +package com.yunxi.scm.module.system.service.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailLogDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.mail.MailLogMapper; +import com.yunxi.scm.module.system.enums.mail.MailSendStatusEnum; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage; + +/** + * 邮件日志 Service 实现类 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@Service +@Validated +public class MailLogServiceImpl implements MailLogService { + + @Resource + private MailLogMapper mailLogMapper; + + @Override + public PageResult getMailLogPage(MailLogPageReqVO pageVO) { + return mailLogMapper.selectPage(pageVO); + } + + @Override + public MailLogDO getMailLog(Long id) { + return mailLogMapper.selectById(id); + } + + @Override + public Long createMailLog(Long userId, Integer userType, String toMail, + MailAccountDO account, MailTemplateDO template, + String templateContent, Map templateParams, Boolean isSend) { + MailLogDO.MailLogDOBuilder logDOBuilder = MailLogDO.builder(); + // 根据是否要发送,设置状态 + logDOBuilder.sendStatus(Objects.equals(isSend, true) ? MailSendStatusEnum.INIT.getStatus() + : MailSendStatusEnum.IGNORE.getStatus()) + // 用户信息 + .userId(userId).userType(userType).toMail(toMail) + .accountId(account.getId()).fromMail(account.getMail()) + // 模板相关字段 + .templateId(template.getId()).templateCode(template.getCode()).templateNickname(template.getNickname()) + .templateTitle(template.getTitle()).templateContent(templateContent).templateParams(templateParams); + + // 插入数据库 + MailLogDO logDO = logDOBuilder.build(); + mailLogMapper.insert(logDO); + return logDO.getId(); + } + + @Override + public void updateMailSendResult(Long logId, String messageId, Exception exception) { + // 1. 成功 + if (exception == null) { + mailLogMapper.updateById(new MailLogDO().setId(logId).setSendTime(LocalDateTime.now()) + .setSendStatus(MailSendStatusEnum.SUCCESS.getStatus()).setSendMessageId(messageId)); + return; + } + // 2. 失败 + mailLogMapper.updateById(new MailLogDO().setId(logId).setSendTime(LocalDateTime.now()) + .setSendStatus(MailSendStatusEnum.FAILURE.getStatus()).setSendException(getRootCauseMessage(exception))); + + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailSendService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailSendService.java new file mode 100644 index 0000000..a781962 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailSendService.java @@ -0,0 +1,60 @@ +package com.yunxi.scm.module.system.service.mail; + +import com.yunxi.scm.module.system.mq.message.mail.MailSendMessage; + +import java.util.Map; + +/** + * 邮件发送 Service 接口 + * + * @author wangjingyi + * @since 2022-03-21 + */ +public interface MailSendService { + + /** + * 发送单条邮件给管理后台的用户 + * + * @param mail 邮箱 + * @param userId 用户编码 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendSingleMailToAdmin(String mail, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条邮件给用户 APP 的用户 + * + * @param mail 邮箱 + * @param userId 用户编码 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendSingleMailToMember(String mail, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条邮件给用户 + * + * @param mail 邮箱 + * @param userId 用户编码 + * @param userType 用户类型 + * @param templateCode 邮件模版编码 + * @param templateParams 邮件模版参数 + * @return 发送日志编号 + */ + Long sendSingleMail(String mail, Long userId, Integer userType, + String templateCode, Map templateParams); + + /** + * 执行真正的邮件发送 + * 注意,该方法仅仅提供给 MQ Consumer 使用 + * + * @param message 邮件 + */ + void doSendMail(MailSendMessage message); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailSendServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailSendServiceImpl.java new file mode 100644 index 0000000..7d44270 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailSendServiceImpl.java @@ -0,0 +1,167 @@ +package com.yunxi.scm.module.system.service.mail; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.mail.MailAccount; +import cn.hutool.extra.mail.MailUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.module.system.convert.mail.MailAccountConvert; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.mq.message.mail.MailSendMessage; +import com.yunxi.scm.module.system.mq.producer.mail.MailProducer; +import com.yunxi.scm.module.system.service.member.MemberService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 邮箱发送 Service 实现类 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@Service +@Validated +@Slf4j +public class MailSendServiceImpl implements MailSendService { + + @Resource + private AdminUserService adminUserService; + @Resource + private MemberService memberService; + + @Resource + private MailAccountService mailAccountService; + @Resource + private MailTemplateService mailTemplateService; + + @Resource + private MailLogService mailLogService; + @Resource + private MailProducer mailProducer; + + @Override + public Long sendSingleMailToAdmin(String mail, Long userId, + String templateCode, Map templateParams) { + // 如果 mail 为空,则加载用户编号对应的邮箱 + if (StrUtil.isEmpty(mail)) { + AdminUserDO user = adminUserService.getUser(userId); + if (user != null) { + mail = user.getEmail(); + } + } + // 执行发送 + return sendSingleMail(mail, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleMailToMember(String mail, Long userId, + String templateCode, Map templateParams) { + // 如果 mail 为空,则加载用户编号对应的邮箱 + if (StrUtil.isEmpty(mail)) { + mail = memberService.getMemberUserEmail(userId); + } + // 执行发送 + return sendSingleMail(mail, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleMail(String mail, Long userId, Integer userType, + String templateCode, Map templateParams) { + // 校验邮箱模版是否合法 + MailTemplateDO template = validateMailTemplate(templateCode); + // 校验邮箱账号是否合法 + MailAccountDO account = validateMailAccount(template.getAccountId()); + + // 校验邮箱是否存在 + mail = validateMail(mail); + validateTemplateParams(template, templateParams); + + // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()); + String title = mailTemplateService.formatMailTemplateContent(template.getTitle(), templateParams); + String content = mailTemplateService.formatMailTemplateContent(template.getContent(), templateParams); + Long sendLogId = mailLogService.createMailLog(userId, userType, mail, + account, template, content, templateParams, isSend); + // 发送 MQ 消息,异步执行发送短信 + if (isSend) { + mailProducer.sendMailSendMessage(sendLogId, mail, account.getId(), + template.getNickname(), title, content); + } + return sendLogId; + } + + @Override + public void doSendMail(MailSendMessage message) { + // 1. 创建发送账号 + MailAccountDO account = validateMailAccount(message.getAccountId()); + MailAccount mailAccount = MailAccountConvert.INSTANCE.convert(account, message.getNickname()); + // 2. 发送邮件 + try { + String messageId = MailUtil.send(mailAccount, message.getMail(), + message.getTitle(), message.getContent(),true); + // 3. 更新结果(成功) + mailLogService.updateMailSendResult(message.getLogId(), messageId, null); + } catch (Exception e) { + // 3. 更新结果(异常) + mailLogService.updateMailSendResult(message.getLogId(), null, e); + } + } + + @VisibleForTesting + MailTemplateDO validateMailTemplate(String templateCode) { + // 获得邮件模板。考虑到效率,从缓存中获取 + MailTemplateDO template = mailTemplateService.getMailTemplateByCodeFromCache(templateCode); + // 邮件模板不存在 + if (template == null) { + throw exception(MAIL_TEMPLATE_NOT_EXISTS); + } + return template; + } + + @VisibleForTesting + MailAccountDO validateMailAccount(Long accountId) { + // 获得邮箱账号。考虑到效率,从缓存中获取 + MailAccountDO account = mailAccountService.getMailAccountFromCache(accountId); + // 邮箱账号不存在 + if (account == null) { + throw exception(MAIL_ACCOUNT_NOT_EXISTS); + } + return account; + } + + @VisibleForTesting + String validateMail(String mail) { + if (StrUtil.isEmpty(mail)) { + throw exception(MAIL_SEND_MAIL_NOT_EXISTS); + } + return mail; + } + + /** + * 校验邮件参数是否确实 + * + * @param template 邮箱模板 + * @param templateParams 参数列表 + */ + @VisibleForTesting + void validateTemplateParams(MailTemplateDO template, Map templateParams) { + template.getParams().forEach(key -> { + Object value = templateParams.get(key); + if (value == null) { + throw exception(MAIL_SEND_TEMPLATE_PARAM_MISS, key); + } + }); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailTemplateService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailTemplateService.java new file mode 100644 index 0000000..34ff253 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailTemplateService.java @@ -0,0 +1,91 @@ +package com.yunxi.scm.module.system.service.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplateUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +/** + * 邮件模版 Service 接口 + * + * @author wangjingyi + * @since 2022-03-21 + */ +public interface MailTemplateService { + + /** + * 邮件模版创建 + * + * @param createReqVO 邮件信息 + * @return 编号 + */ + Long createMailTemplate(@Valid MailTemplateCreateReqVO createReqVO); + + /** + * 邮件模版修改 + * + * @param updateReqVO 邮件信息 + */ + void updateMailTemplate(@Valid MailTemplateUpdateReqVO updateReqVO); + + /** + * 邮件模版删除 + * + * @param id 编号 + */ + void deleteMailTemplate(Long id); + + /** + * 获取邮件模版 + * + * @param id 编号 + * @return 邮件模版 + */ + MailTemplateDO getMailTemplate(Long id); + + /** + * 获取邮件模版分页 + * + * @param pageReqVO 模版信息 + * @return 邮件模版分页信息 + */ + PageResult getMailTemplatePage(MailTemplatePageReqVO pageReqVO); + + /** + * 获取邮件模板数组 + * + * @return 模版数组 + */ + List getMailTemplateList(); + + /** + * 从缓存中获取邮件模版 + * + * @param code 模板编码 + * @return 邮件模板 + */ + MailTemplateDO getMailTemplateByCodeFromCache(String code); + + /** + * 邮件模版内容合成 + * + * @param content 邮件模版 + * @param params 合成参数 + * @return 格式化后的内容 + */ + String formatMailTemplateContent(String content, Map params); + + /** + * 获得指定邮件账号下的邮件模板数量 + * + * @param accountId 账号编号 + * @return 数量 + */ + long countByAccountId(Long accountId); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailTemplateServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailTemplateServiceImpl.java new file mode 100644 index 0000000..95ef456 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/mail/MailTemplateServiceImpl.java @@ -0,0 +1,139 @@ +package com.yunxi.scm.module.system.service.mail; + +import cn.hutool.core.util.ObjUtil; +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplateUpdateReqVO; +import com.yunxi.scm.module.system.convert.mail.MailTemplateConvert; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.mail.MailTemplateMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import javax.validation.Valid; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_CODE_EXISTS; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_NOT_EXISTS; + +/** + * 邮箱模版 Service 实现类 + * + * @author wangjingyi + * @since 2022-03-21 + */ +@Service +@Validated +@Slf4j +public class MailTemplateServiceImpl implements MailTemplateService { + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + + @Resource + private MailTemplateMapper mailTemplateMapper; + + @Override + public Long createMailTemplate(MailTemplateCreateReqVO createReqVO) { + // 校验 code 是否唯一 + validateCodeUnique(null, createReqVO.getCode()); + + // 插入 + MailTemplateDO template = MailTemplateConvert.INSTANCE.convert(createReqVO) + .setParams(parseTemplateContentParams(createReqVO.getContent())); + mailTemplateMapper.insert(template); + return template.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 + public void updateMailTemplate(@Valid MailTemplateUpdateReqVO updateReqVO) { + // 校验是否存在 + validateMailTemplateExists(updateReqVO.getId()); + // 校验 code 是否唯一 + validateCodeUnique(updateReqVO.getId(),updateReqVO.getCode()); + + // 更新 + MailTemplateDO updateObj = MailTemplateConvert.INSTANCE.convert(updateReqVO) + .setParams(parseTemplateContentParams(updateReqVO.getContent())); + mailTemplateMapper.updateById(updateObj); + } + + @VisibleForTesting + void validateCodeUnique(Long id, String code) { + MailTemplateDO template = mailTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 存在 template 记录的情况下 + if (id == null // 新增时,说明重复 + || ObjUtil.notEqual(id, template.getId())) { // 更新时,如果 id 不一致,说明重复 + throw exception(MAIL_TEMPLATE_CODE_EXISTS); + } + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 + public void deleteMailTemplate(Long id) { + // 校验是否存在 + validateMailTemplateExists(id); + + // 删除 + mailTemplateMapper.deleteById(id); + } + + private void validateMailTemplateExists(Long id) { + if (mailTemplateMapper.selectById(id) == null) { + throw exception(MAIL_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public MailTemplateDO getMailTemplate(Long id) {return mailTemplateMapper.selectById(id);} + + @Override + @Cacheable(value = RedisKeyConstants.MAIL_TEMPLATE, key = "#code", unless = "#result == null") + public MailTemplateDO getMailTemplateByCodeFromCache(String code) { + return mailTemplateMapper.selectByCode(code); + } + + @Override + public PageResult getMailTemplatePage(MailTemplatePageReqVO pageReqVO) { + return mailTemplateMapper.selectPage(pageReqVO); + } + + @Override + public List getMailTemplateList() {return mailTemplateMapper.selectList();} + + @Override + public String formatMailTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } + + @VisibleForTesting + public List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + + @Override + public long countByAccountId(Long accountId) { + return mailTemplateMapper.selectCountByAccountId(accountId); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/MemberService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/MemberService.java new file mode 100644 index 0000000..ca11bad --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/MemberService.java @@ -0,0 +1,26 @@ +package com.yunxi.scm.module.system.service.member; + +/** + * Member Service 接口 + * + * @author 芋道源码 + */ +public interface MemberService { + + /** + * 获得会员用户的手机号码 + * + * @param id 会员用户编号 + * @return 手机号码 + */ + String getMemberUserMobile(Long id); + + /** + * 获得会员用户的邮箱 + * + * @param id 会员用户编号 + * @return 邮箱 + */ + String getMemberUserEmail(Long id); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/MemberServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/MemberServiceImpl.java new file mode 100644 index 0000000..3e8e7b5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/MemberServiceImpl.java @@ -0,0 +1,54 @@ +package com.yunxi.scm.module.system.service.member; + +import cn.hutool.core.util.ClassUtil; +import cn.hutool.core.util.ReflectUtil; +import cn.hutool.extra.spring.SpringUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** + * Member Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class MemberServiceImpl implements MemberService { + + @Value("${yunxi.info.base-package}") + private String basePackage; + + private volatile Object memberUserApi; + + @Override + public String getMemberUserMobile(Long id) { + Object user = getMemberUser(id); + if (user == null) { + return null; + } + return ReflectUtil.invoke(user, "getMobile"); + } + + @Override + public String getMemberUserEmail(Long id) { + Object user = getMemberUser(id); + if (user == null) { + return null; + } + return ReflectUtil.invoke(user, "getEmail"); + } + + private Object getMemberUser(Long id) { + if (id == null) { + return null; + } + return ReflectUtil.invoke(getMemberUserApi(), "getUser", id); + } + + private Object getMemberUserApi() { + if (memberUserApi == null) { + memberUserApi = SpringUtil.getBean(ClassUtil.loadClass(String.format("%s.module.member.api.user.MemberUserApi", basePackage))); + } + return memberUserApi; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/package-info.java new file mode 100644 index 0000000..611cd50 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/member/package-info.java @@ -0,0 +1,4 @@ +/** + * yunxi-module-member 模块的适配,解除 yunxi-module-system 对它们的依赖 + */ +package com.yunxi.scm.module.system.service.member; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notice/NoticeService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notice/NoticeService.java new file mode 100644 index 0000000..a3f93d9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notice/NoticeService.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.system.service.notice; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notice.NoticeDO; + +/** + * 通知公告 Service 接口 + */ +public interface NoticeService { + + /** + * 创建岗位公告公告 + * + * @param reqVO 岗位公告公告信息 + * @return 岗位公告公告编号 + */ + Long createNotice(NoticeCreateReqVO reqVO); + + /** + * 更新岗位公告公告 + * + * @param reqVO 岗位公告公告信息 + */ + void updateNotice(NoticeUpdateReqVO reqVO); + + /** + * 删除岗位公告公告信息 + * + * @param id 岗位公告公告编号 + */ + void deleteNotice(Long id); + + /** + * 获得岗位公告公告分页列表 + * + * @param reqVO 分页条件 + * @return 部门分页列表 + */ + PageResult getNoticePage(NoticePageReqVO reqVO); + + /** + * 获得岗位公告公告信息 + * + * @param id 岗位公告公告编号 + * @return 岗位公告公告信息 + */ + NoticeDO getNotice(Long id); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notice/NoticeServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notice/NoticeServiceImpl.java new file mode 100644 index 0000000..11a1a6a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notice/NoticeServiceImpl.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.system.service.notice; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import com.yunxi.scm.module.system.convert.notice.NoticeConvert; +import com.yunxi.scm.module.system.dal.dataobject.notice.NoticeDO; +import com.yunxi.scm.module.system.dal.mysql.notice.NoticeMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; + +/** + * 通知公告 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class NoticeServiceImpl implements NoticeService { + + @Resource + private NoticeMapper noticeMapper; + + @Override + public Long createNotice(NoticeCreateReqVO reqVO) { + NoticeDO notice = NoticeConvert.INSTANCE.convert(reqVO); + noticeMapper.insert(notice); + return notice.getId(); + } + + @Override + public void updateNotice(NoticeUpdateReqVO reqVO) { + // 校验是否存在 + validateNoticeExists(reqVO.getId()); + // 更新通知公告 + NoticeDO updateObj = NoticeConvert.INSTANCE.convert(reqVO); + noticeMapper.updateById(updateObj); + } + + @Override + public void deleteNotice(Long id) { + // 校验是否存在 + validateNoticeExists(id); + // 删除通知公告 + noticeMapper.deleteById(id); + } + + @Override + public PageResult getNoticePage(NoticePageReqVO reqVO) { + return noticeMapper.selectPage(reqVO); + } + + @Override + public NoticeDO getNotice(Long id) { + return noticeMapper.selectById(id); + } + + @VisibleForTesting + public void validateNoticeExists(Long id) { + if (id == null) { + return; + } + NoticeDO notice = noticeMapper.selectById(id); + if (notice == null) { + throw exception(NOTICE_NOT_FOUND); + } + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyMessageService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyMessageService.java new file mode 100644 index 0000000..48fc8f1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyMessageService.java @@ -0,0 +1,97 @@ +package com.yunxi.scm.module.system.service.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyMessageDO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 站内信 Service 接口 + * + * @author xrcoder + */ +public interface NotifyMessageService { + + /** + * 创建站内信 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param template 模版信息 + * @param templateContent 模版内容 + * @param templateParams 模版参数 + * @return 站内信编号 + */ + Long createNotifyMessage(Long userId, Integer userType, + NotifyTemplateDO template, String templateContent, Map templateParams); + + /** + * 获得站内信分页 + * + * @param pageReqVO 分页查询 + * @return 站内信分页 + */ + PageResult getNotifyMessagePage(NotifyMessagePageReqVO pageReqVO); + + /** + * 获得【我的】站内信分页 + * + * @param pageReqVO 分页查询 + * @param userId 用户编号 + * @param userType 用户类型 + * @return 站内信分页 + */ + PageResult getMyMyNotifyMessagePage(NotifyMessageMyPageReqVO pageReqVO, Long userId, Integer userType); + + /** + * 获得站内信 + * + * @param id 编号 + * @return 站内信 + */ + NotifyMessageDO getNotifyMessage(Long id); + + /** + * 获得【我的】未读站内信列表 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param size 数量 + * @return 站内信列表 + */ + List getUnreadNotifyMessageList(Long userId, Integer userType, Integer size); + + /** + * 统计用户未读站内信条数 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 返回未读站内信条数 + */ + Long getUnreadNotifyMessageCount(Long userId, Integer userType); + + /** + * 标记站内信为已读 + * + * @param ids 站内信编号集合 + * @param userId 用户编号 + * @param userType 用户类型 + * @return 更新到的条数 + */ + int updateNotifyMessageRead(Collection ids, Long userId, Integer userType); + + /** + * 标记所有站内信为已读 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 更新到的条数 + */ + int updateAllNotifyMessageRead(Long userId, Integer userType); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyMessageServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyMessageServiceImpl.java new file mode 100644 index 0000000..b4beb85 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyMessageServiceImpl.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.module.system.service.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyMessageDO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.notify.NotifyMessageMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * 站内信 Service 实现类 + * + * @author xrcoder + */ +@Service +@Validated +public class NotifyMessageServiceImpl implements NotifyMessageService { + + @Resource + private NotifyMessageMapper notifyMessageMapper; + + @Override + public Long createNotifyMessage(Long userId, Integer userType, + NotifyTemplateDO template, String templateContent, Map templateParams) { + NotifyMessageDO message = new NotifyMessageDO().setUserId(userId).setUserType(userType) + .setTemplateId(template.getId()).setTemplateCode(template.getCode()) + .setTemplateType(template.getType()).setTemplateNickname(template.getNickname()) + .setTemplateContent(templateContent).setTemplateParams(templateParams).setReadStatus(false); + notifyMessageMapper.insert(message); + return message.getId(); + } + + @Override + public PageResult getNotifyMessagePage(NotifyMessagePageReqVO pageReqVO) { + return notifyMessageMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getMyMyNotifyMessagePage(NotifyMessageMyPageReqVO pageReqVO, Long userId, Integer userType) { + return notifyMessageMapper.selectPage(pageReqVO, userId, userType); + } + + @Override + public NotifyMessageDO getNotifyMessage(Long id) { + return notifyMessageMapper.selectById(id); + } + + @Override + public List getUnreadNotifyMessageList(Long userId, Integer userType, Integer size) { + return notifyMessageMapper.selectUnreadListByUserIdAndUserType(userId, userType, size); + } + + @Override + public Long getUnreadNotifyMessageCount(Long userId, Integer userType) { + return notifyMessageMapper.selectUnreadCountByUserIdAndUserType(userId, userType); + } + + @Override + public int updateNotifyMessageRead(Collection ids, Long userId, Integer userType) { + return notifyMessageMapper.updateListRead(ids, userId, userType); + } + + @Override + public int updateAllNotifyMessageRead(Long userId, Integer userType) { + return notifyMessageMapper.updateListRead(userId, userType); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifySendService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifySendService.java new file mode 100644 index 0000000..797ab74 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifySendService.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.system.service.notify; + +import java.util.List; +import java.util.Map; + +/** + * 站内信发送 Service 接口 + * + * @author xrcoder + */ +public interface NotifySendService { + + /** + * 发送单条站内信给管理后台的用户 + * + * 在 mobile 为空时,使用 userId 加载对应管理员的手机号 + * + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleNotifyToAdmin(Long userId, + String templateCode, Map templateParams); + /** + * 发送单条站内信给用户 APP 的用户 + * + * 在 mobile 为空时,使用 userId 加载对应会员的手机号 + * + * @param userId 用户编号 + * @param templateCode 站内信模板编号 + * @param templateParams 站内信模板参数 + * @return 发送日志编号 + */ + Long sendSingleNotifyToMember(Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条站内信给用户 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param templateCode 站内信模板编号 + * @param templateParams 站内信模板参数 + * @return 发送日志编号 + */ + Long sendSingleNotify( Long userId, Integer userType, + String templateCode, Map templateParams); + + default void sendBatchNotify(List mobiles, List userIds, Integer userType, + String templateCode, Map templateParams) { + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifySendServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifySendServiceImpl.java new file mode 100644 index 0000000..e32949e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifySendServiceImpl.java @@ -0,0 +1,86 @@ +package com.yunxi.scm.module.system.service.notify; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Map; +import java.util.Objects; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 站内信发送 Service 实现类 + * + * @author xrcoder + */ +@Service +@Validated +@Slf4j +public class NotifySendServiceImpl implements NotifySendService { + + @Resource + private NotifyTemplateService notifyTemplateService; + + @Resource + private NotifyMessageService notifyMessageService; + + @Override + public Long sendSingleNotifyToAdmin(Long userId, String templateCode, Map templateParams) { + return sendSingleNotify(userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleNotifyToMember(Long userId, String templateCode, Map templateParams) { + return sendSingleNotify(userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleNotify(Long userId, Integer userType, String templateCode, Map templateParams) { + // 校验模版 + NotifyTemplateDO template = validateNotifyTemplate(templateCode); + if (Objects.equals(template.getStatus(), CommonStatusEnum.DISABLE.getStatus())) { + log.info("[sendSingleNotify][模版({})已经关闭,无法给用户({}/{})发送]", templateCode, userId, userType); + return null; + } + // 校验参数 + validateTemplateParams(template, templateParams); + + // 发送站内信 + String content = notifyTemplateService.formatNotifyTemplateContent(template.getContent(), templateParams); + return notifyMessageService.createNotifyMessage(userId, userType, template, content, templateParams); + } + + @VisibleForTesting + public NotifyTemplateDO validateNotifyTemplate(String templateCode) { + // 获得站内信模板。考虑到效率,从缓存中获取 + NotifyTemplateDO template = notifyTemplateService.getNotifyTemplateByCodeFromCache(templateCode); + // 站内信模板不存在 + if (template == null) { + throw exception(NOTICE_NOT_FOUND); + } + return template; + } + + /** + * 校验站内信模版参数是否确实 + * + * @param template 邮箱模板 + * @param templateParams 参数列表 + */ + @VisibleForTesting + public void validateTemplateParams(NotifyTemplateDO template, Map templateParams) { + template.getParams().forEach(key -> { + Object value = templateParams.get(key); + if (value == null) { + throw exception(NOTIFY_SEND_TEMPLATE_PARAM_MISS, key); + } + }); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateService.java new file mode 100644 index 0000000..a5ccf94 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateService.java @@ -0,0 +1,74 @@ +package com.yunxi.scm.module.system.service.notify; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; + +import javax.validation.Valid; +import java.util.Map; + +/** + * 站内信模版 Service 接口 + * + * @author xrcoder + */ +public interface NotifyTemplateService { + + /** + * 创建站内信模版 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createNotifyTemplate(@Valid NotifyTemplateCreateReqVO createReqVO); + + /** + * 更新站内信模版 + * + * @param updateReqVO 更新信息 + */ + void updateNotifyTemplate(@Valid NotifyTemplateUpdateReqVO updateReqVO); + + /** + * 删除站内信模版 + * + * @param id 编号 + */ + void deleteNotifyTemplate(Long id); + + /** + * 获得站内信模版 + * + * @param id 编号 + * @return 站内信模版 + */ + NotifyTemplateDO getNotifyTemplate(Long id); + + /** + * 获得站内信模板,从缓存中 + * + * @param code 模板编码 + * @return 站内信模板 + */ + NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code); + + /** + * 获得站内信模版分页 + * + * @param pageReqVO 分页查询 + * @return 站内信模版分页 + */ + PageResult getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO); + + /** + * 格式化站内信内容 + * + * @param content 站内信模板的内容 + * @param params 站内信内容的参数 + * @return 格式化后的内容 + */ + String formatNotifyTemplateContent(String content, Map params); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateServiceImpl.java new file mode 100644 index 0000000..04b1f1c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateServiceImpl.java @@ -0,0 +1,138 @@ +package com.yunxi.scm.module.system.service.notify; + +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateUpdateReqVO; +import com.yunxi.scm.module.system.convert.notify.NotifyTemplateConvert; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.notify.NotifyTemplateMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_CODE_DUPLICATE; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS; + +/** + * 站内信模版 Service 实现类 + * + * @author xrcoder + */ +@Service +@Validated +@Slf4j +public class NotifyTemplateServiceImpl implements NotifyTemplateService { + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + + @Resource + private NotifyTemplateMapper notifyTemplateMapper; + + @Override + public Long createNotifyTemplate(NotifyTemplateCreateReqVO createReqVO) { + // 校验站内信编码是否重复 + validateNotifyTemplateCodeDuplicate(null, createReqVO.getCode()); + + // 插入 + NotifyTemplateDO notifyTemplate = NotifyTemplateConvert.INSTANCE.convert(createReqVO); + notifyTemplate.setParams(parseTemplateContentParams(notifyTemplate.getContent())); + notifyTemplateMapper.insert(notifyTemplate); + return notifyTemplate.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 + public void updateNotifyTemplate(NotifyTemplateUpdateReqVO updateReqVO) { + // 校验存在 + validateNotifyTemplateExists(updateReqVO.getId()); + // 校验站内信编码是否重复 + validateNotifyTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); + + // 更新 + NotifyTemplateDO updateObj = NotifyTemplateConvert.INSTANCE.convert(updateReqVO); + updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); + notifyTemplateMapper.updateById(updateObj); + } + + @VisibleForTesting + public List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 + public void deleteNotifyTemplate(Long id) { + // 校验存在 + validateNotifyTemplateExists(id); + // 删除 + notifyTemplateMapper.deleteById(id); + } + + private void validateNotifyTemplateExists(Long id) { + if (notifyTemplateMapper.selectById(id) == null) { + throw exception(NOTIFY_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public NotifyTemplateDO getNotifyTemplate(Long id) { + return notifyTemplateMapper.selectById(id); + } + + @Override + @Cacheable(cacheNames = RedisKeyConstants.NOTIFY_TEMPLATE, key = "#code", + unless = "#result == null") + public NotifyTemplateDO getNotifyTemplateByCodeFromCache(String code) { + return notifyTemplateMapper.selectByCode(code); + } + + @Override + public PageResult getNotifyTemplatePage(NotifyTemplatePageReqVO pageReqVO) { + return notifyTemplateMapper.selectPage(pageReqVO); + } + + @VisibleForTesting + void validateNotifyTemplateCodeDuplicate(Long id, String code) { + NotifyTemplateDO template = notifyTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code); + } + if (!template.getId().equals(id)) { + throw exception(NOTIFY_TEMPLATE_CODE_DUPLICATE, code); + } + } + + /** + * 格式化站内信内容 + * + * @param content 站内信模板的内容 + * @param params 站内信内容的参数 + * @return 格式化后的内容 + */ + @Override + public String formatNotifyTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveService.java new file mode 100644 index 0000000..cbca8b6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveService.java @@ -0,0 +1,52 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * OAuth2 批准 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 ApprovalStoreUserApprovalHandler 的功能,记录用户针对指定客户端的授权,减少手动确定。 + * + * @author 芋道源码 + */ +public interface OAuth2ApproveService { + + /** + * 获得指定用户,针对指定客户端的指定授权,是否通过 + * + * 参考 ApprovalStoreUserApprovalHandler 的 checkForPreApproval 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param requestedScopes 授权范围 + * @return 是否授权通过 + */ + boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection requestedScopes); + + /** + * 在用户发起批准时,基于 scopes 的选项,计算最终是否通过 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param requestedScopes 授权范围 + * @return 是否授权通过 + */ + boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map requestedScopes); + + /** + * 获得用户的批准列表,排除已过期的 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @return 是否授权通过 + */ + List getApproveList(Long userId, Integer userType, String clientId); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveServiceImpl.java new file mode 100644 index 0000000..6b749e3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveServiceImpl.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2ApproveMapper; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.*; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * OAuth2 批准 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class OAuth2ApproveServiceImpl implements OAuth2ApproveService { + + /** + * 批准的过期时间,默认 30 天 + */ + private static final Integer TIMEOUT = 30 * 24 * 60 * 60; // 单位:秒 + + @Resource + private OAuth2ClientService oauth2ClientService; + + @Resource + private OAuth2ApproveMapper oauth2ApproveMapper; + + @Override + @Transactional + public boolean checkForPreApproval(Long userId, Integer userType, String clientId, Collection requestedScopes) { + // 第一步,基于 Client 的自动授权计算,如果 scopes 都在自动授权中,则返回 true 通过 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + Assert.notNull(clientDO, "客户端不能为空"); // 防御性编程 + if (CollUtil.containsAll(clientDO.getAutoApproveScopes(), requestedScopes)) { + // gh-877 - if all scopes are auto approved, approvals still need to be added to the approval store. + LocalDateTime expireTime = LocalDateTime.now().plusSeconds(TIMEOUT); + for (String scope : requestedScopes) { + saveApprove(userId, userType, clientId, scope, true, expireTime); + } + return true; + } + + // 第二步,算上用户已经批准的授权。如果 scopes 都包含,则返回 true + List approveDOs = getApproveList(userId, userType, clientId); + Set scopes = convertSet(approveDOs, OAuth2ApproveDO::getScope, + OAuth2ApproveDO::getApproved); // 只保留未过期的 + 同意的 + return CollUtil.containsAll(scopes, requestedScopes); + } + + @Override + @Transactional + public boolean updateAfterApproval(Long userId, Integer userType, String clientId, Map requestedScopes) { + // 如果 requestedScopes 为空,说明没有要求,则返回 true 通过 + if (CollUtil.isEmpty(requestedScopes)) { + return true; + } + + // 更新批准的信息 + boolean success = false; // 需要至少有一个同意 + LocalDateTime expireTime = LocalDateTime.now().plusSeconds(TIMEOUT); + for (Map.Entry entry : requestedScopes.entrySet()) { + if (entry.getValue()) { + success = true; + } + saveApprove(userId, userType, clientId, entry.getKey(), entry.getValue(), expireTime); + } + return success; + } + + @Override + public List getApproveList(Long userId, Integer userType, String clientId) { + List approveDOs = oauth2ApproveMapper.selectListByUserIdAndUserTypeAndClientId( + userId, userType, clientId); + approveDOs.removeIf(o -> DateUtils.isExpired(o.getExpiresTime())); + return approveDOs; + } + + @VisibleForTesting + void saveApprove(Long userId, Integer userType, String clientId, + String scope, Boolean approved, LocalDateTime expireTime) { + // 先更新 + OAuth2ApproveDO approveDO = new OAuth2ApproveDO().setUserId(userId).setUserType(userType) + .setClientId(clientId).setScope(scope).setApproved(approved).setExpiresTime(expireTime); + if (oauth2ApproveMapper.update(approveDO) == 1) { + return; + } + // 失败,则说明不存在,进行更新 + oauth2ApproveMapper.insert(approveDO); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientService.java new file mode 100644 index 0000000..18057fe --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientService.java @@ -0,0 +1,91 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; + +import javax.validation.Valid; +import java.util.Collection; + +/** + * OAuth2.0 Client Service 接口 + * + * 从功能上,和 JdbcClientDetailsService 的功能,提供客户端的操作 + * + * @author 芋道源码 + */ +public interface OAuth2ClientService { + + /** + * 创建 OAuth2 客户端 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createOAuth2Client(@Valid OAuth2ClientCreateReqVO createReqVO); + + /** + * 更新 OAuth2 客户端 + * + * @param updateReqVO 更新信息 + */ + void updateOAuth2Client(@Valid OAuth2ClientUpdateReqVO updateReqVO); + + /** + * 删除 OAuth2 客户端 + * + * @param id 编号 + */ + void deleteOAuth2Client(Long id); + + /** + * 获得 OAuth2 客户端 + * + * @param id 编号 + * @return OAuth2 客户端 + */ + OAuth2ClientDO getOAuth2Client(Long id); + + /** + * 获得 OAuth2 客户端,从缓存中 + * + * @param clientId 客户端编号 + * @return OAuth2 客户端 + */ + OAuth2ClientDO getOAuth2ClientFromCache(String clientId); + + /** + * 获得 OAuth2 客户端分页 + * + * @param pageReqVO 分页查询 + * @return OAuth2 客户端分页 + */ + PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO); + + /** + * 从缓存中,校验客户端是否合法 + * + * @return 客户端 + */ + default OAuth2ClientDO validOAuthClientFromCache(String clientId) { + return validOAuthClientFromCache(clientId, null, null, null, null); + } + + /** + * 从缓存中,校验客户端是否合法 + * + * 非空时,进行校验 + * + * @param clientId 客户端编号 + * @param clientSecret 客户端密钥 + * @param authorizedGrantType 授权方式 + * @param scopes 授权范围 + * @param redirectUri 重定向地址 + * @return 客户端 + */ + OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType, + Collection scopes, String redirectUri); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientServiceImpl.java new file mode 100644 index 0000000..d81ee5f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientServiceImpl.java @@ -0,0 +1,154 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.string.StrUtils; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import com.yunxi.scm.module.system.convert.auth.OAuth2ClientConvert; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2ClientMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collection; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * OAuth2.0 Client Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class OAuth2ClientServiceImpl implements OAuth2ClientService { + + @Resource + private OAuth2ClientMapper oauth2ClientMapper; + + @Override + public Long createOAuth2Client(OAuth2ClientCreateReqVO createReqVO) { + validateClientIdExists(null, createReqVO.getClientId()); + // 插入 + OAuth2ClientDO oauth2Client = OAuth2ClientConvert.INSTANCE.convert(createReqVO); + oauth2ClientMapper.insert(oauth2Client); + return oauth2Client.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 clientId 字段,不好清理 + public void updateOAuth2Client(OAuth2ClientUpdateReqVO updateReqVO) { + // 校验存在 + validateOAuth2ClientExists(updateReqVO.getId()); + // 校验 Client 未被占用 + validateClientIdExists(updateReqVO.getId(), updateReqVO.getClientId()); + + // 更新 + OAuth2ClientDO updateObj = OAuth2ClientConvert.INSTANCE.convert(updateReqVO); + oauth2ClientMapper.updateById(updateObj); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.OAUTH_CLIENT, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 key,不好清理 + public void deleteOAuth2Client(Long id) { + // 校验存在 + validateOAuth2ClientExists(id); + // 删除 + oauth2ClientMapper.deleteById(id); + } + + private void validateOAuth2ClientExists(Long id) { + if (oauth2ClientMapper.selectById(id) == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + } + + @VisibleForTesting + void validateClientIdExists(Long id, String clientId) { + OAuth2ClientDO client = oauth2ClientMapper.selectByClientId(clientId); + if (client == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的客户端 + if (id == null) { + throw exception(OAUTH2_CLIENT_EXISTS); + } + if (!client.getId().equals(id)) { + throw exception(OAUTH2_CLIENT_EXISTS); + } + } + + @Override + public OAuth2ClientDO getOAuth2Client(Long id) { + return oauth2ClientMapper.selectById(id); + } + + @Override + @Cacheable(cacheNames = RedisKeyConstants.OAUTH_CLIENT, key = "#clientId", + unless = "#result == null") + public OAuth2ClientDO getOAuth2ClientFromCache(String clientId) { + return oauth2ClientMapper.selectByClientId(clientId); + } + + @Override + public PageResult getOAuth2ClientPage(OAuth2ClientPageReqVO pageReqVO) { + return oauth2ClientMapper.selectPage(pageReqVO); + } + + @Override + public OAuth2ClientDO validOAuthClientFromCache(String clientId, String clientSecret, String authorizedGrantType, + Collection scopes, String redirectUri) { + // 校验客户端存在、且开启 + OAuth2ClientDO client = getSelf().getOAuth2ClientFromCache(clientId); + if (client == null) { + throw exception(OAUTH2_CLIENT_NOT_EXISTS); + } + if (ObjectUtil.notEqual(client.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(OAUTH2_CLIENT_DISABLE); + } + + // 校验客户端密钥 + if (StrUtil.isNotEmpty(clientSecret) && ObjectUtil.notEqual(client.getSecret(), clientSecret)) { + throw exception(OAUTH2_CLIENT_CLIENT_SECRET_ERROR); + } + // 校验授权方式 + if (StrUtil.isNotEmpty(authorizedGrantType) && !CollUtil.contains(client.getAuthorizedGrantTypes(), authorizedGrantType)) { + throw exception(OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); + } + // 校验授权范围 + if (CollUtil.isNotEmpty(scopes) && !CollUtil.containsAll(client.getScopes(), scopes)) { + throw exception(OAUTH2_CLIENT_SCOPE_OVER); + } + // 校验回调地址 + if (StrUtil.isNotEmpty(redirectUri) && !StrUtils.startWithAny(redirectUri, client.getRedirectUris())) { + throw exception(OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, redirectUri); + } + return client; + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private OAuth2ClientServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeService.java new file mode 100644 index 0000000..5f43b49 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeService.java @@ -0,0 +1,39 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2CodeDO; + +import java.util.List; + +/** + * OAuth2.0 授权码 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 JdbcAuthorizationCodeServices 的功能,提供授权码的操作 + * + * @author 芋道源码 + */ +public interface OAuth2CodeService { + + /** + * 创建授权码 + * + * 参考 JdbcAuthorizationCodeServices 的 createAuthorizationCode 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 授权码的信息 + */ + OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, + List scopes, String redirectUri, String state); + + /** + * 使用授权码 + * + * @param code 授权码 + */ + OAuth2CodeDO consumeAuthorizationCode(String code); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeServiceImpl.java new file mode 100644 index 0000000..b5085a0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeServiceImpl.java @@ -0,0 +1,64 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.core.util.IdUtil; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2CodeMapper; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS; + +/** + * OAuth2.0 授权码 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class OAuth2CodeServiceImpl implements OAuth2CodeService { + + /** + * 授权码的过期时间,默认 5 分钟 + */ + private static final Integer TIMEOUT = 5 * 60; + + @Resource + private OAuth2CodeMapper oauth2CodeMapper; + + @Override + public OAuth2CodeDO createAuthorizationCode(Long userId, Integer userType, String clientId, + List scopes, String redirectUri, String state) { + OAuth2CodeDO codeDO = new OAuth2CodeDO().setCode(generateCode()) + .setUserId(userId).setUserType(userType) + .setClientId(clientId).setScopes(scopes) + .setExpiresTime(LocalDateTime.now().plusSeconds(TIMEOUT)) + .setRedirectUri(redirectUri).setState(state); + oauth2CodeMapper.insert(codeDO); + return codeDO; + } + + @Override + public OAuth2CodeDO consumeAuthorizationCode(String code) { + OAuth2CodeDO codeDO = oauth2CodeMapper.selectByCode(code); + if (codeDO == null) { + throw exception(OAUTH2_CODE_NOT_EXISTS); + } + if (DateUtils.isExpired(codeDO.getExpiresTime())) { + throw exception(OAUTH2_CODE_EXPIRE); + } + oauth2CodeMapper.deleteById(codeDO.getId()); + return codeDO; + } + + private static String generateCode() { + return IdUtil.fastSimpleUUID(); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantService.java new file mode 100644 index 0000000..cd16613 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantService.java @@ -0,0 +1,113 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +import java.util.List; + +/** + * OAuth2 授予 Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 TokenGranter 的功能,提供访问令牌、刷新令牌的操作 + * + * 将自身的 AdminUser 用户,授权给第三方应用,采用 OAuth2.0 的协议。 + * + * 问题:为什么自身也作为一个第三方应用,也走这套流程呢? + * 回复:当然可以这么做,采用 password 模式。考虑到大多数开发者使用不到这个特性,OAuth2.0 毕竟有一定学习成本,所以暂时没有采取这种方式。 + * + * @author 芋道源码 + */ +public interface OAuth2GrantService { + + /** + * 简化模式 + * + * 对应 Spring Security OAuth2 的 ImplicitTokenGranter 功能 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, + String clientId, List scopes); + + /** + * 授权码模式,第一阶段,获得 code 授权码 + * + * 对应 Spring Security OAuth2 的 AuthorizationEndpoint 的 generateCode 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 授权码 + */ + String grantAuthorizationCodeForCode(Long userId, Integer userType, + String clientId, List scopes, + String redirectUri, String state); + + /** + * 授权码模式,第二阶段,获得 accessToken 访问令牌 + * + * 对应 Spring Security OAuth2 的 AuthorizationCodeTokenGranter 功能 + * + * @param clientId 客户端编号 + * @param code 授权码 + * @param redirectUri 重定向 URI + * @param state 状态 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, + String redirectUri, String state); + + /** + * 密码模式 + * + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 + * + * @param username 账号 + * @param password 密码 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantPassword(String username, String password, + String clientId, List scopes); + + /** + * 刷新模式 + * + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId); + + /** + * 客户端模式 + * + * 对应 Spring Security OAuth2 的 ClientCredentialsTokenGranter 功能 + * + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌 + */ + OAuth2AccessTokenDO grantClientCredentials(String clientId, List scopes); + + /** + * 移除访问令牌 + * + * 对应 Spring Security OAuth2 的 ConsumerTokenServices 的 revokeToken 方法 + * + * @param accessToken 访问令牌 + * @param clientId 客户端编号 + * @return 是否移除到 + */ + boolean revokeToken(String clientId, String accessToken); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantServiceImpl.java new file mode 100644 index 0000000..19d3f4c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantServiceImpl.java @@ -0,0 +1,104 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.enums.ErrorCodeConstants; +import com.yunxi.scm.module.system.service.auth.AdminAuthService; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; + +/** + * OAuth2 授予 Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2GrantServiceImpl implements OAuth2GrantService { + + @Resource + private OAuth2TokenService oauth2TokenService; + @Resource + private OAuth2CodeService oauth2CodeService; + @Resource + private AdminAuthService adminAuthService; + + @Override + public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, + String clientId, List scopes) { + return oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); + } + + @Override + public String grantAuthorizationCodeForCode(Long userId, Integer userType, + String clientId, List scopes, + String redirectUri, String state) { + return oauth2CodeService.createAuthorizationCode(userId, userType, clientId, scopes, + redirectUri, state).getCode(); + } + + @Override + public OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, + String redirectUri, String state) { + OAuth2CodeDO codeDO = oauth2CodeService.consumeAuthorizationCode(code); + Assert.notNull(codeDO, "授权码不能为空"); // 防御性编程 + // 校验 clientId 是否匹配 + if (!StrUtil.equals(clientId, codeDO.getClientId())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_CLIENT_ID_MISMATCH); + } + // 校验 redirectUri 是否匹配 + if (!StrUtil.equals(redirectUri, codeDO.getRedirectUri())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_REDIRECT_URI_MISMATCH); + } + // 校验 state 是否匹配 + state = StrUtil.nullToDefault(state, ""); // 数据库 state 为 null 时,会设置为 "" 空串 + if (!StrUtil.equals(state, codeDO.getState())) { + throw exception(ErrorCodeConstants.OAUTH2_GRANT_STATE_MISMATCH); + } + + // 创建访问令牌 + return oauth2TokenService.createAccessToken(codeDO.getUserId(), codeDO.getUserType(), + codeDO.getClientId(), codeDO.getScopes()); + } + + @Override + public OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List scopes) { + // 使用账号 + 密码进行登录 + AdminUserDO user = adminAuthService.authenticate(username, password); + Assert.notNull(user, "用户不能为空!"); // 防御性编程 + + // 创建访问令牌 + return oauth2TokenService.createAccessToken(user.getId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes); + } + + @Override + public OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId) { + return oauth2TokenService.refreshAccessToken(refreshToken, clientId); + } + + @Override + public OAuth2AccessTokenDO grantClientCredentials(String clientId, List scopes) { + // TODO 芋艿:项目中使用 OAuth2 解决的是三方应用的授权,内部的 SSO 等问题,所以暂时不考虑 client_credentials 这个场景 + throw new UnsupportedOperationException("暂时不支持 client_credentials 授权模式"); + } + + @Override + public boolean revokeToken(String clientId, String accessToken) { + // 先查询,保证 clientId 时匹配的 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.getAccessToken(accessToken); + if (accessTokenDO == null || ObjectUtil.notEqual(clientId, accessTokenDO.getClientId())) { + return false; + } + // 再删除 + return oauth2TokenService.removeAccessToken(accessToken) != null; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenService.java new file mode 100644 index 0000000..515b335 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenService.java @@ -0,0 +1,80 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; + +import java.util.List; + +/** + * OAuth2.0 Token Service 接口 + * + * 从功能上,和 Spring Security OAuth 的 DefaultTokenServices + JdbcTokenStore 的功能,提供访问令牌、刷新令牌的操作 + * + * @author 芋道源码 + */ +public interface OAuth2TokenService { + + /** + * 创建访问令牌 + * 注意:该流程中,会包含创建刷新令牌的创建 + * + * 参考 DefaultTokenServices 的 createAccessToken 方法 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @param clientId 客户端编号 + * @param scopes 授权范围 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes); + + /** + * 刷新访问令牌 + * + * 参考 DefaultTokenServices 的 refreshAccessToken 方法 + * + * @param refreshToken 刷新令牌 + * @param clientId 客户端编号 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId); + + /** + * 获得访问令牌 + * + * 参考 DefaultTokenServices 的 getAccessToken 方法 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO getAccessToken(String accessToken); + + /** + * 校验访问令牌 + * + * @param accessToken 访问令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO checkAccessToken(String accessToken); + + /** + * 移除访问令牌 + * 注意:该流程中,会移除相关的刷新令牌 + * + * 参考 DefaultTokenServices 的 revokeToken 方法 + * + * @param accessToken 刷新令牌 + * @return 访问令牌的信息 + */ + OAuth2AccessTokenDO removeAccessToken(String accessToken); + + /** + * 获得访问令牌分页 + * + * @param reqVO 请求 + * @return 访问令牌分页 + */ + PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenServiceImpl.java new file mode 100644 index 0000000..df283b8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenServiceImpl.java @@ -0,0 +1,166 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; +import com.yunxi.scm.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Calendar; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception0; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; + +/** + * OAuth2.0 Token Service 实现类 + * + * @author 芋道源码 + */ +@Service +public class OAuth2TokenServiceImpl implements OAuth2TokenService { + + @Resource + private OAuth2AccessTokenMapper oauth2AccessTokenMapper; + @Resource + private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; + + @Resource + private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; + + @Resource + private OAuth2ClientService oauth2ClientService; + + @Override + @Transactional + public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String clientId, List scopes) { + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + // 创建刷新令牌 + OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO, scopes); + // 创建访问令牌 + return createOAuth2AccessToken(refreshTokenDO, clientDO); + } + + @Override + public OAuth2AccessTokenDO refreshAccessToken(String refreshToken, String clientId) { + // 查询访问令牌 + OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectByRefreshToken(refreshToken); + if (refreshTokenDO == null) { + throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "无效的刷新令牌"); + } + + // 校验 Client 匹配 + OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId); + if (ObjectUtil.notEqual(clientId, refreshTokenDO.getClientId())) { + throw exception0(GlobalErrorCodeConstants.BAD_REQUEST.getCode(), "刷新令牌的客户端编号不正确"); + } + + // 移除相关的访问令牌 + List accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshToken); + if (CollUtil.isNotEmpty(accessTokenDOs)) { + oauth2AccessTokenMapper.deleteBatchIds(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getId)); + oauth2AccessTokenRedisDAO.deleteList(convertSet(accessTokenDOs, OAuth2AccessTokenDO::getAccessToken)); + } + + // 已过期的情况下,删除刷新令牌 + if (DateUtils.isExpired(refreshTokenDO.getExpiresTime())) { + oauth2RefreshTokenMapper.deleteById(refreshTokenDO.getId()); + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "刷新令牌已过期"); + } + + // 创建访问令牌 + return createOAuth2AccessToken(refreshTokenDO, clientDO); + } + + @Override + public OAuth2AccessTokenDO getAccessToken(String accessToken) { + // 优先从 Redis 中获取 + OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken); + if (accessTokenDO != null) { + return accessTokenDO; + } + + // 获取不到,从 MySQL 中获取 + accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); + // 如果在 MySQL 存在,则往 Redis 中写入 + if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) { + oauth2AccessTokenRedisDAO.set(accessTokenDO); + } + return accessTokenDO; + } + + @Override + public OAuth2AccessTokenDO checkAccessToken(String accessToken) { + OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken); + if (accessTokenDO == null) { + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌不存在"); + } + if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) { + throw exception0(GlobalErrorCodeConstants.UNAUTHORIZED.getCode(), "访问令牌已过期"); + } + return accessTokenDO; + } + + @Override + public OAuth2AccessTokenDO removeAccessToken(String accessToken) { + // 删除访问令牌 + OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessToken); + if (accessTokenDO == null) { + return null; + } + oauth2AccessTokenMapper.deleteById(accessTokenDO.getId()); + oauth2AccessTokenRedisDAO.delete(accessToken); + // 删除刷新令牌 + oauth2RefreshTokenMapper.deleteByRefreshToken(accessTokenDO.getRefreshToken()); + return accessTokenDO; + } + + @Override + public PageResult getAccessTokenPage(OAuth2AccessTokenPageReqVO reqVO) { + return oauth2AccessTokenMapper.selectPage(reqVO); + } + + private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) { + OAuth2AccessTokenDO accessTokenDO = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken()) + .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()) + .setClientId(clientDO.getClientId()).setScopes(refreshTokenDO.getScopes()) + .setRefreshToken(refreshTokenDO.getRefreshToken()) + .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getAccessTokenValiditySeconds())); + accessTokenDO.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号 + oauth2AccessTokenMapper.insert(accessTokenDO); + // 记录到 Redis 中 + oauth2AccessTokenRedisDAO.set(accessTokenDO); + return accessTokenDO; + } + + private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO, List scopes) { + OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken()) + .setUserId(userId).setUserType(userType) + .setClientId(clientDO.getClientId()).setScopes(scopes) + .setExpiresTime(LocalDateTime.now().plusSeconds(clientDO.getRefreshTokenValiditySeconds())); + oauth2RefreshTokenMapper.insert(refreshToken); + return refreshToken; + } + + private static String generateAccessToken() { + return IdUtil.fastSimpleUUID(); + } + + private static String generateRefreshToken() { + return IdUtil.fastSimpleUUID(); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/MenuService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/MenuService.java new file mode 100644 index 0000000..604a5bd --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/MenuService.java @@ -0,0 +1,88 @@ +package com.yunxi.scm.module.system.service.permission; + +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; + +import java.util.Collection; +import java.util.List; + +/** + * 菜单 Service 接口 + * + * @author 芋道源码 + */ +public interface MenuService { + + /** + * 创建菜单 + * + * @param reqVO 菜单信息 + * @return 创建出来的菜单编号 + */ + Long createMenu(MenuCreateReqVO reqVO); + + /** + * 更新菜单 + * + * @param reqVO 菜单信息 + */ + void updateMenu(MenuUpdateReqVO reqVO); + + /** + * 删除菜单 + * + * @param id 菜单编号 + */ + void deleteMenu(Long id); + + /** + * 获得所有菜单列表 + * + * @return 菜单列表 + */ + List getMenuList(); + + /** + * 基于租户,筛选菜单列表 + * 注意,如果是系统租户,返回的还是全菜单 + * + * @param reqVO 筛选条件请求 VO + * @return 菜单列表 + */ + List getMenuListByTenant(MenuListReqVO reqVO); + + /** + * 筛选菜单列表 + * + * @param reqVO 筛选条件请求 VO + * @return 菜单列表 + */ + List getMenuList(MenuListReqVO reqVO); + + /** + * 获得权限对应的菜单编号数组 + * + * @param permission 权限标识 + * @return 数组 + */ + List getMenuIdListByPermissionFromCache(String permission); + + /** + * 获得菜单 + * + * @param id 菜单编号 + * @return 菜单 + */ + MenuDO getMenu(Long id); + + /** + * 获得菜单数组 + * + * @param ids 菜单编号数组 + * @return 菜单数组 + */ + List getMenuList(Collection ids); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/MenuServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/MenuServiceImpl.java new file mode 100644 index 0000000..d3306ee --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/MenuServiceImpl.java @@ -0,0 +1,208 @@ +package com.yunxi.scm.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO; +import com.yunxi.scm.module.system.convert.permission.MenuConvert; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.dal.mysql.permission.MenuMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import com.yunxi.scm.module.system.enums.permission.MenuTypeEnum; +import com.yunxi.scm.module.system.service.tenant.TenantService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 菜单 Service 实现 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class MenuServiceImpl implements MenuService { + + @Resource + private MenuMapper menuMapper; + @Resource + private PermissionService permissionService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private TenantService tenantService; + + @Override + @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#reqVO.permission") + public Long createMenu(MenuCreateReqVO reqVO) { + // 校验父菜单存在 + validateParentMenu(reqVO.getParentId(), null); + // 校验菜单(自己) + validateMenu(reqVO.getParentId(), reqVO.getName(), null); + + // 插入数据库 + MenuDO menu = MenuConvert.INSTANCE.convert(reqVO); + initMenuProperty(menu); + menuMapper.insert(menu); + // 返回 + return menu.getId(); + } + + @Override + @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为 permission 如果变更,涉及到新老两个 permission。直接清理,简单有效 + public void updateMenu(MenuUpdateReqVO reqVO) { + // 校验更新的菜单是否存在 + if (menuMapper.selectById(reqVO.getId()) == null) { + throw exception(MENU_NOT_EXISTS); + } + // 校验父菜单存在 + validateParentMenu(reqVO.getParentId(), reqVO.getId()); + // 校验菜单(自己) + validateMenu(reqVO.getParentId(), reqVO.getName(), reqVO.getId()); + + // 更新到数据库 + MenuDO updateObject = MenuConvert.INSTANCE.convert(reqVO); + initMenuProperty(updateObject); + menuMapper.updateById(updateObject); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,因为此时不知道 id 对应的 permission 是多少。直接清理,简单有效 + public void deleteMenu(Long id) { + // 校验是否还有子菜单 + if (menuMapper.selectCountByParentId(id) > 0) { + throw exception(MENU_EXISTS_CHILDREN); + } + // 校验删除的菜单是否存在 + if (menuMapper.selectById(id) == null) { + throw exception(MENU_NOT_EXISTS); + } + // 标记删除 + menuMapper.deleteById(id); + // 删除授予给角色的权限 + permissionService.processMenuDeleted(id); + } + + @Override + public List getMenuList() { + return menuMapper.selectList(); + } + + @Override + public List getMenuListByTenant(MenuListReqVO reqVO) { + List menus = getMenuList(reqVO); + // 开启多租户的情况下,需要过滤掉未开通的菜单 + tenantService.handleTenantMenu(menuIds -> menus.removeIf(menu -> !CollUtil.contains(menuIds, menu.getId()))); + return menus; + } + + @Override + public List getMenuList(MenuListReqVO reqVO) { + return menuMapper.selectList(reqVO); + } + + @Override + @Cacheable(value = RedisKeyConstants.PERMISSION_MENU_ID_LIST, key = "#permission") + public List getMenuIdListByPermissionFromCache(String permission) { + List menus = menuMapper.selectListByPermission(permission); + return convertList(menus, MenuDO::getId); + } + + @Override + public MenuDO getMenu(Long id) { + return menuMapper.selectById(id); + } + + @Override + public List getMenuList(Collection ids) { + return menuMapper.selectBatchIds(ids); + } + + /** + * 校验父菜单是否合法 + *

+ * 1. 不能设置自己为父菜单 + * 2. 父菜单不存在 + * 3. 父菜单必须是 {@link MenuTypeEnum#MENU} 菜单类型 + * + * @param parentId 父菜单编号 + * @param childId 当前菜单编号 + */ + @VisibleForTesting + void validateParentMenu(Long parentId, Long childId) { + if (parentId == null || ID_ROOT.equals(parentId)) { + return; + } + // 不能设置自己为父菜单 + if (parentId.equals(childId)) { + throw exception(MENU_PARENT_ERROR); + } + MenuDO menu = menuMapper.selectById(parentId); + // 父菜单不存在 + if (menu == null) { + throw exception(MENU_PARENT_NOT_EXISTS); + } + // 父菜单必须是目录或者菜单类型 + if (!MenuTypeEnum.DIR.getType().equals(menu.getType()) + && !MenuTypeEnum.MENU.getType().equals(menu.getType())) { + throw exception(MENU_PARENT_NOT_DIR_OR_MENU); + } + } + + /** + * 校验菜单是否合法 + *

+ * 1. 校验相同父菜单编号下,是否存在相同的菜单名 + * + * @param name 菜单名字 + * @param parentId 父菜单编号 + * @param id 菜单编号 + */ + @VisibleForTesting + void validateMenu(Long parentId, String name, Long id) { + MenuDO menu = menuMapper.selectByParentIdAndName(parentId, name); + if (menu == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的菜单 + if (id == null) { + throw exception(MENU_NAME_DUPLICATE); + } + if (!menu.getId().equals(id)) { + throw exception(MENU_NAME_DUPLICATE); + } + } + + /** + * 初始化菜单的通用属性。 + *

+ * 例如说,只有目录或者菜单类型的菜单,才设置 icon + * + * @param menu 菜单 + */ + private void initMenuProperty(MenuDO menu) { + // 菜单为按钮类型时,无需 component、icon、path 属性,进行置空 + if (MenuTypeEnum.BUTTON.getType().equals(menu.getType())) { + menu.setComponent(""); + menu.setComponentName(""); + menu.setIcon(""); + menu.setPath(""); + } + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/PermissionService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/PermissionService.java new file mode 100644 index 0000000..6f73930 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/PermissionService.java @@ -0,0 +1,146 @@ +package com.yunxi.scm.module.system.service.permission; + +import com.yunxi.scm.module.system.api.permission.dto.DeptDataPermissionRespDTO; + +import java.util.Collection; +import java.util.Set; + +import static java.util.Collections.singleton; + +/** + * 权限 Service 接口 + *

+ * 提供用户-角色、角色-菜单、角色-部门的关联权限处理 + * + * @author 芋道源码 + */ +public interface PermissionService { + + /** + * 判断是否有权限,任一一个即可 + * + * @param userId 用户编号 + * @param permissions 权限 + * @return 是否 + */ + boolean hasAnyPermissions(Long userId, String... permissions); + + /** + * 判断是否有角色,任一一个即可 + * + * @param roles 角色数组 + * @return 是否 + */ + boolean hasAnyRoles(Long userId, String... roles); + + // ========== 角色-菜单的相关方法 ========== + + /** + * 设置角色菜单 + * + * @param roleId 角色编号 + * @param menuIds 菜单编号集合 + */ + void assignRoleMenu(Long roleId, Set menuIds); + + /** + * 处理角色删除时,删除关联授权数据 + * + * @param roleId 角色编号 + */ + void processRoleDeleted(Long roleId); + + /** + * 处理菜单删除时,删除关联授权数据 + * + * @param menuId 菜单编号 + */ + void processMenuDeleted(Long menuId); + + /** + * 获得角色拥有的菜单编号集合 + * + * @param roleId 角色编号 + * @return 菜单编号集合 + */ + default Set getRoleMenuListByRoleId(Long roleId) { + return getRoleMenuListByRoleId(singleton(roleId)); + } + + /** + * 获得角色们拥有的菜单编号集合 + * + * @param roleIds 角色编号数组 + * @return 菜单编号集合 + */ + Set getRoleMenuListByRoleId(Collection roleIds); + + /** + * 获得拥有指定菜单的角色编号数组,从缓存中获取 + * + * @param menuId 菜单编号 + * @return 角色编号数组 + */ + Set getMenuRoleIdListByMenuIdFromCache(Long menuId); + + // ========== 用户-角色的相关方法 ========== + + /** + * 设置用户角色 + * + * @param userId 角色编号 + * @param roleIds 角色编号集合 + */ + void assignUserRole(Long userId, Set roleIds); + + /** + * 处理用户删除时,删除关联授权数据 + * + * @param userId 用户编号 + */ + void processUserDeleted(Long userId); + + /** + * 获得拥有多个角色的用户编号集合 + * + * @param roleIds 角色编号集合 + * @return 用户编号集合 + */ + Set getUserRoleIdListByRoleId(Collection roleIds); + + /** + * 获得用户拥有的角色编号集合 + * + * @param userId 用户编号 + * @return 角色编号集合 + */ + Set getUserRoleIdListByUserId(Long userId); + + /** + * 获得用户拥有的角色编号集合,从缓存中获取 + * + * @param userId 用户编号 + * @return 角色编号集合 + */ + Set getUserRoleIdListByUserIdFromCache(Long userId); + + // ========== 用户-部门的相关方法 ========== + + /** + * 设置角色的数据权限 + * + * @param roleId 角色编号 + * @param dataScope 数据范围 + * @param dataScopeDeptIds 部门编号数组 + */ + void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds); + + /** + * 获得登陆用户的部门数据权限 + * + * @param userId 用户编号 + * @return 部门数据权限 + */ + DeptDataPermissionRespDTO getDeptDataPermission(Long userId); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/PermissionServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/PermissionServiceImpl.java new file mode 100644 index 0000000..cbf0cee --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/PermissionServiceImpl.java @@ -0,0 +1,335 @@ +package com.yunxi.scm.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleMenuDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.UserRoleDO; +import com.yunxi.scm.module.system.dal.mysql.permission.RoleMenuMapper; +import com.yunxi.scm.module.system.dal.mysql.permission.UserRoleMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import com.yunxi.scm.module.system.enums.permission.DataScopeEnum; +import com.yunxi.scm.module.system.service.dept.DeptService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Suppliers; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.util.*; +import java.util.function.Supplier; + +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; + +/** + * 权限 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class PermissionServiceImpl implements PermissionService { + + @Resource + private RoleMenuMapper roleMenuMapper; + @Resource + private UserRoleMapper userRoleMapper; + + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; + @Resource + private DeptService deptService; + @Resource + private AdminUserService userService; + + @Override + public boolean hasAnyPermissions(Long userId, String... permissions) { + // 如果为空,说明已经有权限 + if (ArrayUtil.isEmpty(permissions)) { + return true; + } + + // 获得当前登录的角色。如果为空,说明没有权限 + List roles = getEnableUserRoleListByUserIdFromCache(userId); + if (CollUtil.isEmpty(roles)) { + return false; + } + + // 情况一:遍历判断每个权限,如果有一满足,说明有权限 + for (String permission : permissions) { + if (hasAnyPermission(roles, permission)) { + return true; + } + } + + // 情况二:如果是超管,也说明有权限 + return roleService.hasAnySuperAdmin(convertSet(roles, RoleDO::getId)); + } + + /** + * 判断指定角色,是否拥有该 permission 权限 + * + * @param roles 指定角色数组 + * @param permission 权限标识 + * @return 是否拥有 + */ + private boolean hasAnyPermission(List roles, String permission) { + List menuIds = menuService.getMenuIdListByPermissionFromCache(permission); + // 采用严格模式,如果权限找不到对应的 Menu 的话,也认为没有权限 + if (CollUtil.isEmpty(menuIds)) { + return false; + } + + // 判断是否有权限 + Set roleIds = convertSet(roles, RoleDO::getId); + for (Long menuId : menuIds) { + // 获得拥有该菜单的角色编号集合 + Set menuRoleIds = getSelf().getMenuRoleIdListByMenuIdFromCache(menuId); + // 如果有交集,说明有权限 + if (CollUtil.containsAny(menuRoleIds, roleIds)) { + return true; + } + } + return false; + } + + @Override + public boolean hasAnyRoles(Long userId, String... roles) { + // 如果为空,说明已经有权限 + if (ArrayUtil.isEmpty(roles)) { + return true; + } + + // 获得当前登录的角色。如果为空,说明没有权限 + List roleList = getEnableUserRoleListByUserIdFromCache(userId); + if (CollUtil.isEmpty(roleList)) { + return false; + } + + // 判断是否有角色 + Set userRoles = convertSet(roleList, RoleDO::getCode); + return CollUtil.containsAny(userRoles, Sets.newHashSet(roles)); + } + + // ========== 角色-菜单的相关方法 ========== + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,主要一次更新涉及到的 menuIds 较多,反倒批量会更快 + public void assignRoleMenu(Long roleId, Set menuIds) { + // 获得角色拥有菜单编号 + Set dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId); + // 计算新增和删除的菜单编号 + Collection createMenuIds = CollUtil.subtract(menuIds, dbMenuIds); + Collection deleteMenuIds = CollUtil.subtract(dbMenuIds, menuIds); + // 执行新增和删除。对于已经授权的菜单,不用做任何处理 + if (CollUtil.isNotEmpty(createMenuIds)) { + roleMenuMapper.insertBatch(CollectionUtils.convertList(createMenuIds, menuId -> { + RoleMenuDO entity = new RoleMenuDO(); + entity.setRoleId(roleId); + entity.setMenuId(menuId); + return entity; + })); + } + if (CollUtil.isNotEmpty(deleteMenuIds)) { + roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + @Caching(evict = { + @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, + allEntries = true), // allEntries 清空所有缓存,此处无法方便获得 roleId 对应的 menu 缓存们 + @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, + allEntries = true) // allEntries 清空所有缓存,此处无法方便获得 roleId 对应的 user 缓存们 + }) + public void processRoleDeleted(Long roleId) { + // 标记删除 UserRole + userRoleMapper.deleteListByRoleId(roleId); + // 标记删除 RoleMenu + roleMenuMapper.deleteListByRoleId(roleId); + } + + @Override + @CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") + public void processMenuDeleted(Long menuId) { + roleMenuMapper.deleteListByMenuId(menuId); + } + + @Override + public Set getRoleMenuListByRoleId(Collection roleIds) { + if (CollUtil.isEmpty(roleIds)) { + return Collections.emptySet(); + } + + // 如果是管理员的情况下,获取全部菜单编号 + if (roleService.hasAnySuperAdmin(roleIds)) { + return convertSet(menuService.getMenuList(), MenuDO::getId); + } + // 如果是非管理员的情况下,获得拥有的菜单编号 + return convertSet(roleMenuMapper.selectListByRoleId(roleIds), RoleMenuDO::getMenuId); + } + + @Override + @Cacheable(value = RedisKeyConstants.MENU_ROLE_ID_LIST, key = "#menuId") + public Set getMenuRoleIdListByMenuIdFromCache(Long menuId) { + return convertSet(roleMenuMapper.selectListByMenuId(menuId), RoleMenuDO::getRoleId); + } + + // ========== 用户-角色的相关方法 ========== + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") + public void assignUserRole(Long userId, Set roleIds) { + // 获得角色拥有角色编号 + Set dbRoleIds = convertSet(userRoleMapper.selectListByUserId(userId), + UserRoleDO::getRoleId); + // 计算新增和删除的角色编号 + Collection createRoleIds = CollUtil.subtract(roleIds, dbRoleIds); + Collection deleteMenuIds = CollUtil.subtract(dbRoleIds, roleIds); + // 执行新增和删除。对于已经授权的角色,不用做任何处理 + if (!CollectionUtil.isEmpty(createRoleIds)) { + userRoleMapper.insertBatch(CollectionUtils.convertList(createRoleIds, roleId -> { + UserRoleDO entity = new UserRoleDO(); + entity.setUserId(userId); + entity.setRoleId(roleId); + return entity; + })); + } + if (!CollectionUtil.isEmpty(deleteMenuIds)) { + userRoleMapper.deleteListByUserIdAndRoleIdIds(userId, deleteMenuIds); + } + } + + @Override + @CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") + public void processUserDeleted(Long userId) { + userRoleMapper.deleteListByUserId(userId); + } + + @Override + public Set getUserRoleIdListByUserId(Long userId) { + return convertSet(userRoleMapper.selectListByUserId(userId), UserRoleDO::getRoleId); + } + + @Override + @Cacheable(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") + public Set getUserRoleIdListByUserIdFromCache(Long userId) { + return getUserRoleIdListByUserId(userId); + } + + @Override + public Set getUserRoleIdListByRoleId(Collection roleIds) { + return convertSet(userRoleMapper.selectListByRoleIds(roleIds), UserRoleDO::getUserId); + } + + /** + * 获得用户拥有的角色,并且这些角色是开启状态的 + * + * @param userId 用户编号 + * @return 用户拥有的角色 + */ + @VisibleForTesting + List getEnableUserRoleListByUserIdFromCache(Long userId) { + // 获得用户拥有的角色编号 + Set roleIds = getSelf().getUserRoleIdListByUserIdFromCache(userId); + // 获得角色数组,并移除被禁用的 + List roles = roleService.getRoleListFromCache(roleIds); + roles.removeIf(role -> !CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())); + return roles; + } + + // ========== 用户-部门的相关方法 ========== + + @Override + public void assignRoleDataScope(Long roleId, Integer dataScope, Set dataScopeDeptIds) { + roleService.updateRoleDataScope(roleId, dataScope, dataScopeDeptIds); + } + + @Override + @DataPermission(enable = false) // 关闭数据权限,不然就会出现递归获取数据权限的问题 + public DeptDataPermissionRespDTO getDeptDataPermission(Long userId) { + // 获得用户的角色 + List roles = getEnableUserRoleListByUserIdFromCache(userId); + + // 如果角色为空,则只能查看自己 + DeptDataPermissionRespDTO result = new DeptDataPermissionRespDTO(); + if (CollUtil.isEmpty(roles)) { + result.setSelf(true); + return result; + } + + // 获得用户的部门编号的缓存,通过 Guava 的 Suppliers 惰性求值,即有且仅有第一次发起 DB 的查询 + Supplier userDeptId = Suppliers.memoize(() -> userService.getUser(userId).getDeptId()); + // 遍历每个角色,计算 + for (RoleDO role : roles) { + // 为空时,跳过 + if (role.getDataScope() == null) { + continue; + } + // 情况一,ALL + if (Objects.equals(role.getDataScope(), DataScopeEnum.ALL.getScope())) { + result.setAll(true); + continue; + } + // 情况二,DEPT_CUSTOM + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_CUSTOM.getScope())) { + CollUtil.addAll(result.getDeptIds(), role.getDataScopeDeptIds()); + // 自定义可见部门时,保证可以看到自己所在的部门。否则,一些场景下可能会有问题。 + // 例如说,登录时,基于 t_user 的 username 查询会可能被 dept_id 过滤掉 + CollUtil.addAll(result.getDeptIds(), userDeptId.get()); + continue; + } + // 情况三,DEPT_ONLY + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_ONLY.getScope())) { + CollectionUtils.addIfNotNull(result.getDeptIds(), userDeptId.get()); + continue; + } + // 情况四,DEPT_DEPT_AND_CHILD + if (Objects.equals(role.getDataScope(), DataScopeEnum.DEPT_AND_CHILD.getScope())) { + CollUtil.addAll(result.getDeptIds(), deptService.getChildDeptIdListFromCache(userDeptId.get())); + // 添加本身部门编号 + CollUtil.addAll(result.getDeptIds(), userDeptId.get()); + continue; + } + // 情况五,SELF + if (Objects.equals(role.getDataScope(), DataScopeEnum.SELF.getScope())) { + result.setSelf(true); + continue; + } + // 未知情况,error log 即可 + log.error("[getDeptDataPermission][LoginUser({}) role({}) 无法处理]", userId, toJsonString(result)); + } + return result; + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private PermissionServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/RoleService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/RoleService.java new file mode 100644 index 0000000..be5b491 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/RoleService.java @@ -0,0 +1,142 @@ +package com.yunxi.scm.module.system.service.permission; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleExportReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; + +import javax.validation.Valid; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * 角色 Service 接口 + * + * @author 芋道源码 + */ +public interface RoleService { + + /** + * 创建角色 + * + * @param reqVO 创建角色信息 + * @param type 角色类型 + * @return 角色编号 + */ + Long createRole(@Valid RoleCreateReqVO reqVO, Integer type); + + /** + * 更新角色 + * + * @param reqVO 更新角色信息 + */ + void updateRole(@Valid RoleUpdateReqVO reqVO); + + /** + * 删除角色 + * + * @param id 角色编号 + */ + void deleteRole(Long id); + + /** + * 更新角色状态 + * + * @param id 角色编号 + * @param status 状态 + */ + void updateRoleStatus(Long id, Integer status); + + /** + * 设置角色的数据权限 + * + * @param id 角色编号 + * @param dataScope 数据范围 + * @param dataScopeDeptIds 部门编号数组 + */ + void updateRoleDataScope(Long id, Integer dataScope, Set dataScopeDeptIds); + + /** + * 获得角色 + * + * @param id 角色编号 + * @return 角色 + */ + RoleDO getRole(Long id); + + /** + * 获得角色,从缓存中 + * + * @param id 角色编号 + * @return 角色 + */ + RoleDO getRoleFromCache(Long id); + + /** + * 获得角色列表 + * + * @param ids 角色编号数组 + * @return 角色列表 + */ + List getRoleList(Collection ids); + + /** + * 获得角色数组,从缓存中 + * + * @param ids 角色编号数组 + * @return 角色数组 + */ + List getRoleListFromCache(Collection ids); + + /** + * 获得角色列表 + * + * @param statuses 筛选的状态 + * @return 角色列表 + */ + List getRoleListByStatus(Collection statuses); + + /** + * 获得所有角色列表 + * + * @return 角色列表 + */ + List getRoleList(); + + /** + * 获得角色分页 + * + * @param reqVO 角色分页查询 + * @return 角色分页结果 + */ + PageResult getRolePage(RolePageReqVO reqVO); + + /** + * 获得角色列表 + * + * @param reqVO 列表查询 + * @return 角色列表 + */ + List getRoleList(RoleExportReqVO reqVO); + + /** + * 判断角色编号数组中,是否有管理员 + * + * @param ids 角色编号数组 + * @return 是否有管理员 + */ + boolean hasAnySuperAdmin(Collection ids); + + /** + * 校验角色们是否有效。如下情况,视为无效: + * 1. 角色编号不存在 + * 2. 角色被禁用 + * + * @param ids 角色编号数组 + */ + void validateRoleList(Collection ids); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/RoleServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/RoleServiceImpl.java new file mode 100644 index 0000000..1d6725c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/RoleServiceImpl.java @@ -0,0 +1,257 @@ +package com.yunxi.scm.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleExportReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleUpdateReqVO; +import com.yunxi.scm.module.system.convert.permission.RoleConvert; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.mysql.permission.RoleMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import com.yunxi.scm.module.system.enums.permission.DataScopeEnum; +import com.yunxi.scm.module.system.enums.permission.RoleCodeEnum; +import com.yunxi.scm.module.system.enums.permission.RoleTypeEnum; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import javax.annotation.Resource; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertMap; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 角色 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Slf4j +public class RoleServiceImpl implements RoleService { + + @Resource + private PermissionService permissionService; + + @Resource + private RoleMapper roleMapper; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createRole(RoleCreateReqVO reqVO, Integer type) { + // 校验角色 + validateRoleDuplicate(reqVO.getName(), reqVO.getCode(), null); + // 插入到数据库 + RoleDO role = RoleConvert.INSTANCE.convert(reqVO); + role.setType(ObjectUtil.defaultIfNull(type, RoleTypeEnum.CUSTOM.getType())); + role.setStatus(CommonStatusEnum.ENABLE.getStatus()); + role.setDataScope(DataScopeEnum.ALL.getScope()); // 默认可查看所有数据。原因是,可能一些项目不需要项目权限 + roleMapper.insert(role); + // 返回 + return role.getId(); + } + + @Override + @CacheEvict(value = RedisKeyConstants.ROLE, key = "#reqVO.id") + public void updateRole(RoleUpdateReqVO reqVO) { + // 校验是否可以更新 + validateRoleForUpdate(reqVO.getId()); + // 校验角色的唯一字段是否重复 + validateRoleDuplicate(reqVO.getName(), reqVO.getCode(), reqVO.getId()); + + // 更新到数据库 + RoleDO updateObj = RoleConvert.INSTANCE.convert(reqVO); + roleMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") + public void updateRoleStatus(Long id, Integer status) { + // 校验是否可以更新 + validateRoleForUpdate(id); + + // 更新状态 + RoleDO updateObj = new RoleDO().setId(id).setStatus(status); + roleMapper.updateById(updateObj); + } + + @Override + @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") + public void updateRoleDataScope(Long id, Integer dataScope, Set dataScopeDeptIds) { + // 校验是否可以更新 + validateRoleForUpdate(id); + + // 更新数据范围 + RoleDO updateObject = new RoleDO(); + updateObject.setId(id); + updateObject.setDataScope(dataScope); + updateObject.setDataScopeDeptIds(dataScopeDeptIds); + roleMapper.updateById(updateObject); + } + + @Override + @Transactional(rollbackFor = Exception.class) + @CacheEvict(value = RedisKeyConstants.ROLE, key = "#id") + public void deleteRole(Long id) { + // 校验是否可以更新 + validateRoleForUpdate(id); + // 标记删除 + roleMapper.deleteById(id); + // 删除相关数据 + permissionService.processRoleDeleted(id); + } + + /** + * 校验角色的唯一字段是否重复 + * + * 1. 是否存在相同名字的角色 + * 2. 是否存在相同编码的角色 + * + * @param name 角色名字 + * @param code 角色额编码 + * @param id 角色编号 + */ + @VisibleForTesting + void validateRoleDuplicate(String name, String code, Long id) { + // 0. 超级管理员,不允许创建 + if (RoleCodeEnum.isSuperAdmin(code)) { + throw exception(ROLE_ADMIN_CODE_ERROR, code); + } + // 1. 该 name 名字被其它角色所使用 + RoleDO role = roleMapper.selectByName(name); + if (role != null && !role.getId().equals(id)) { + throw exception(ROLE_NAME_DUPLICATE, name); + } + // 2. 是否存在相同编码的角色 + if (!StringUtils.hasText(code)) { + return; + } + // 该 code 编码被其它角色所使用 + role = roleMapper.selectByCode(code); + if (role != null && !role.getId().equals(id)) { + throw exception(ROLE_CODE_DUPLICATE, code); + } + } + + /** + * 校验角色是否可以被更新 + * + * @param id 角色编号 + */ + @VisibleForTesting + void validateRoleForUpdate(Long id) { + RoleDO roleDO = roleMapper.selectById(id); + if (roleDO == null) { + throw exception(ROLE_NOT_EXISTS); + } + // 内置角色,不允许删除 + if (RoleTypeEnum.SYSTEM.getType().equals(roleDO.getType())) { + throw exception(ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); + } + } + + @Override + public RoleDO getRole(Long id) { + return roleMapper.selectById(id); + } + + @Override + @Cacheable(value = RedisKeyConstants.ROLE, key = "#id", + unless = "#result == null") + public RoleDO getRoleFromCache(Long id) { + return roleMapper.selectById(id); + } + + + @Override + public List getRoleListByStatus(Collection statuses) { + return roleMapper.selectListByStatus(statuses); + } + + @Override + public List getRoleList() { + return roleMapper.selectList(); + } + + @Override + public List getRoleList(Collection ids) { + if (CollectionUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return roleMapper.selectBatchIds(ids); + } + + @Override + public List getRoleListFromCache(Collection ids) { + if (CollectionUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + // 这里采用 for 循环从缓存中获取,主要考虑 Spring CacheManager 无法批量操作的问题 + RoleServiceImpl self = getSelf(); + return convertList(ids, self::getRoleFromCache); + } + + @Override + public PageResult getRolePage(RolePageReqVO reqVO) { + return roleMapper.selectPage(reqVO); + } + + @Override + public List getRoleList(RoleExportReqVO reqVO) { + return roleMapper.selectList(reqVO); + } + + @Override + public boolean hasAnySuperAdmin(Collection ids) { + if (CollectionUtil.isEmpty(ids)) { + return false; + } + RoleServiceImpl self = getSelf(); + return ids.stream().anyMatch(id -> { + RoleDO role = self.getRoleFromCache(id); + return role != null && RoleCodeEnum.isSuperAdmin(role.getCode()); + }); + } + + @Override + public void validateRoleList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得角色信息 + List roles = roleMapper.selectBatchIds(ids); + Map roleMap = convertMap(roles, RoleDO::getId); + // 校验 + ids.forEach(id -> { + RoleDO role = roleMap.get(id); + if (role == null) { + throw exception(ROLE_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(role.getStatus())) { + throw exception(ROLE_IS_DISABLE, role.getName()); + } + }); + } + + /** + * 获得自身的代理对象,解决 AOP 生效问题 + * + * @return 自己 + */ + private RoleServiceImpl getSelf() { + return SpringUtil.getBean(getClass()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/bo/RoleCreateReqBO.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/bo/RoleCreateReqBO.java new file mode 100644 index 0000000..94ad411 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/permission/bo/RoleCreateReqBO.java @@ -0,0 +1,49 @@ +package com.yunxi.scm.module.system.service.permission.bo; + +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * 角色创建 Request BO + * + * @author 芋道源码 + */ +@Data +public class RoleCreateReqBO { + + /** + * 租户编号 + */ + @NotNull(message = "租户编号不能为空") + private Long tenantId; + + /** + * 角色名称 + */ + @NotBlank(message = "角色名称不能为空") + @Size(max = 30, message = "角色名称长度不能超过30个字符") + private String name; + + /** + * 角色标志 + */ + @NotBlank(message = "角色标志不能为空") + @Size(max = 100, message = "角色标志长度不能超过100个字符") + private String code; + + /** + * 显示顺序 + */ + @NotNull(message = "显示顺序不能为空") + private Integer sort; + + /** + * 角色类型 + */ + @NotNull(message = "角色类型不能为空") + private Integer type; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordService.java new file mode 100644 index 0000000..1764981 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordService.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.module.system.service.sensitiveword; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; + +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +/** + * 敏感词 Service 接口 + * + * @author 永不言败 + */ +public interface SensitiveWordService { + + /** + * 创建敏感词 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSensitiveWord(@Valid SensitiveWordCreateReqVO createReqVO); + + /** + * 更新敏感词 + * + * @param updateReqVO 更新信息 + */ + void updateSensitiveWord(@Valid SensitiveWordUpdateReqVO updateReqVO); + + /** + * 删除敏感词 + * + * @param id 编号 + */ + void deleteSensitiveWord(Long id); + + /** + * 获得敏感词 + * + * @param id 编号 + * @return 敏感词 + */ + SensitiveWordDO getSensitiveWord(Long id); + + /** + * 获得敏感词列表 + * + * @return 敏感词列表 + */ + List getSensitiveWordList(); + + /** + * 获得敏感词分页 + * + * @param pageReqVO 分页查询 + * @return 敏感词分页 + */ + PageResult getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO); + + /** + * 获得敏感词列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 敏感词列表 + */ + List getSensitiveWordList(SensitiveWordExportReqVO exportReqVO); + + /** + * 获得所有敏感词的标签数组 + * + * @return 标签数组 + */ + Set getSensitiveWordTagSet(); + + /** + * 获得文本所包含的不合法的敏感词数组 + * + * @param text 文本 + * @param tags 标签数组 + * @return 不合法的敏感词数组 + */ + List validateText(String text, List tags); + + /** + * 判断文本是否包含敏感词 + * + * @param text 文本 + * @param tags 表述数组 + * @return 是否包含 + */ + boolean isTextValid(String text, List tags); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordServiceImpl.java new file mode 100644 index 0000000..09f4533 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordServiceImpl.java @@ -0,0 +1,252 @@ +package com.yunxi.scm.module.system.service.sensitiveword; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO; +import com.yunxi.scm.module.system.convert.sensitiveword.SensitiveWordConvert; +import com.yunxi.scm.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import com.yunxi.scm.module.system.dal.mysql.sensitiveword.SensitiveWordMapper; +import com.yunxi.scm.module.system.util.collection.SimpleTrie; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.filterList; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.getMaxValue; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_EXISTS; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS; + +/** + * 敏感词 Service 实现类 + * + * @author 永不言败 + */ +@Service +@Slf4j +@Validated +public class SensitiveWordServiceImpl implements SensitiveWordService { + + /** + * 敏感词列表缓存 + */ + @Getter + private volatile List sensitiveWordCache = Collections.emptyList(); + /** + * 敏感词标签缓存 + * key:敏感词编号 {@link SensitiveWordDO#getId()} + *

+ * 这里声明 volatile 修饰的原因是,每次刷新时,直接修改指向 + */ + @Getter + private volatile Set sensitiveWordTagsCache = Collections.emptySet(); + + @Resource + private SensitiveWordMapper sensitiveWordMapper; + + /** + * 默认的敏感词的字典树,包含所有敏感词 + */ + @Getter + private volatile SimpleTrie defaultSensitiveWordTrie = new SimpleTrie(Collections.emptySet()); + /** + * 标签与敏感词的字段数的映射 + */ + @Getter + private volatile Map tagSensitiveWordTries = Collections.emptyMap(); + + /** + * 初始化缓存 + */ + @PostConstruct + public void initLocalCache() { + // 第一步:查询数据 + List sensitiveWords = sensitiveWordMapper.selectList(); + log.info("[initLocalCache][缓存敏感词,数量为:{}]", sensitiveWords.size()); + + // 第二步:构建缓存 + // 写入 sensitiveWordTagsCache 缓存 + Set tags = new HashSet<>(); + sensitiveWords.forEach(word -> tags.addAll(word.getTags())); + sensitiveWordTagsCache = tags; + sensitiveWordCache = sensitiveWords; + // 写入 defaultSensitiveWordTrie、tagSensitiveWordTries 缓存 + initSensitiveWordTrie(sensitiveWords); + } + + private void initSensitiveWordTrie(List wordDOs) { + // 过滤禁用的敏感词 + wordDOs = filterList(wordDOs, word -> word.getStatus().equals(CommonStatusEnum.ENABLE.getStatus())); + + // 初始化默认的 defaultSensitiveWordTrie + this.defaultSensitiveWordTrie = new SimpleTrie(CollectionUtils.convertList(wordDOs, SensitiveWordDO::getName)); + + // 初始化 tagSensitiveWordTries + Multimap tagWords = HashMultimap.create(); + for (SensitiveWordDO word : wordDOs) { + if (CollUtil.isEmpty(word.getTags())) { + continue; + } + word.getTags().forEach(tag -> tagWords.put(tag, word.getName())); + } + // 添加到 tagSensitiveWordTries 中 + Map tagSensitiveWordTries = new HashMap<>(); + tagWords.asMap().forEach((tag, words) -> tagSensitiveWordTries.put(tag, new SimpleTrie(words))); + this.tagSensitiveWordTries = tagSensitiveWordTries; + } + + /** + * 通过定时任务轮询,刷新缓存 + * + * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 + */ + @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) + public void refreshLocalCache() { + // 情况一:如果缓存里没有数据,则直接刷新缓存 + if (CollUtil.isEmpty(sensitiveWordCache)) { + initLocalCache(); + return; + } + + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 + LocalDateTime maxTime = getMaxValue(sensitiveWordCache, SensitiveWordDO::getUpdateTime); + if (sensitiveWordMapper.selectCountByUpdateTimeGt(maxTime) > 0) { + initLocalCache(); + } + } + + @Override + public Long createSensitiveWord(SensitiveWordCreateReqVO createReqVO) { + // 校验唯一性 + validateSensitiveWordNameUnique(null, createReqVO.getName()); + + // 插入 + SensitiveWordDO sensitiveWord = SensitiveWordConvert.INSTANCE.convert(createReqVO); + sensitiveWordMapper.insert(sensitiveWord); + + // 刷新缓存 + initLocalCache(); + return sensitiveWord.getId(); + } + + @Override + public void updateSensitiveWord(SensitiveWordUpdateReqVO updateReqVO) { + // 校验唯一性 + validateSensitiveWordExists(updateReqVO.getId()); + validateSensitiveWordNameUnique(updateReqVO.getId(), updateReqVO.getName()); + + // 更新 + SensitiveWordDO updateObj = SensitiveWordConvert.INSTANCE.convert(updateReqVO); + sensitiveWordMapper.updateById(updateObj); + + // 刷新缓存 + initLocalCache(); + } + + @Override + public void deleteSensitiveWord(Long id) { + // 校验存在 + validateSensitiveWordExists(id); + // 删除 + sensitiveWordMapper.deleteById(id); + + // 刷新缓存 + initLocalCache(); + } + + private void validateSensitiveWordNameUnique(Long id, String name) { + SensitiveWordDO word = sensitiveWordMapper.selectByName(name); + if (word == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的敏感词 + if (id == null) { + throw exception(SENSITIVE_WORD_EXISTS); + } + if (!word.getId().equals(id)) { + throw exception(SENSITIVE_WORD_EXISTS); + } + } + + private void validateSensitiveWordExists(Long id) { + if (sensitiveWordMapper.selectById(id) == null) { + throw exception(SENSITIVE_WORD_NOT_EXISTS); + } + } + + @Override + public SensitiveWordDO getSensitiveWord(Long id) { + return sensitiveWordMapper.selectById(id); + } + + @Override + public List getSensitiveWordList() { + return sensitiveWordMapper.selectList(); + } + + @Override + public PageResult getSensitiveWordPage(SensitiveWordPageReqVO pageReqVO) { + return sensitiveWordMapper.selectPage(pageReqVO); + } + + @Override + public List getSensitiveWordList(SensitiveWordExportReqVO exportReqVO) { + return sensitiveWordMapper.selectList(exportReqVO); + } + + @Override + public Set getSensitiveWordTagSet() { + return sensitiveWordTagsCache; + } + + @Override + public List validateText(String text, List tags) { + if (CollUtil.isEmpty(tags)) { + return defaultSensitiveWordTrie.validate(text); + } + // 有标签的情况 + Set result = new HashSet<>(); + tags.forEach(tag -> { + SimpleTrie trie = tagSensitiveWordTries.get(tag); + if (trie == null) { + return; + } + result.addAll(trie.validate(text)); + }); + return new ArrayList<>(result); + } + + @Override + public boolean isTextValid(String text, List tags) { + if (CollUtil.isEmpty(tags)) { + return defaultSensitiveWordTrie.isValid(text); + } + // 有标签的情况 + for (String tag : tags) { + SimpleTrie trie = tagSensitiveWordTries.get(tag); + if (trie == null) { + continue; + } + if (!trie.isValid(text)) { + return false; + } + } + return true; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsChannelService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsChannelService.java new file mode 100644 index 0000000..2194034 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsChannelService.java @@ -0,0 +1,70 @@ +package com.yunxi.scm.module.system.service.sms; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 短信渠道 Service 接口 + * + * @author zzf + * @date 2021/1/25 9:24 + */ +public interface SmsChannelService { + + /** + * 初始化短信客户端 + */ + void initLocalCache(); + + /** + * 创建短信渠道 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSmsChannel(@Valid SmsChannelCreateReqVO createReqVO); + + /** + * 更新短信渠道 + * + * @param updateReqVO 更新信息 + */ + void updateSmsChannel(@Valid SmsChannelUpdateReqVO updateReqVO); + + /** + * 删除短信渠道 + * + * @param id 编号 + */ + void deleteSmsChannel(Long id); + + /** + * 获得短信渠道 + * + * @param id 编号 + * @return 短信渠道 + */ + SmsChannelDO getSmsChannel(Long id); + + /** + * 获得所有短信渠道列表 + * + * @return 短信渠道列表 + */ + List getSmsChannelList(); + + /** + * 获得短信渠道分页 + * + * @param pageReqVO 分页查询 + * @return 短信渠道分页 + */ + PageResult getSmsChannelPage(SmsChannelPageReqVO pageReqVO); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsChannelServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsChannelServiceImpl.java new file mode 100644 index 0000000..1ca39c1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsChannelServiceImpl.java @@ -0,0 +1,146 @@ +package com.yunxi.scm.module.system.service.sms; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.sms.core.client.SmsClientFactory; +import com.yunxi.scm.framework.sms.core.property.SmsChannelProperties; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO; +import com.yunxi.scm.module.system.convert.sms.SmsChannelConvert; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import com.yunxi.scm.module.system.dal.mysql.sms.SmsChannelMapper; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.getMaxValue; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; + +/** + * 短信渠道 Service 实现类 + * + * @author zzf + */ +@Service +@Slf4j +public class SmsChannelServiceImpl implements SmsChannelService { + + /** + * 短信渠道列表的缓存 + */ + @Getter + private volatile List channelCache = Collections.emptyList(); + + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SmsChannelMapper smsChannelMapper; + + @Resource + private SmsTemplateService smsTemplateService; + + @Override + @PostConstruct + public void initLocalCache() { + // 第一步:查询数据 + List channels = smsChannelMapper.selectList(); + log.info("[initLocalCache][缓存短信渠道,数量为:{}]", channels.size()); + + // 第二步:构建缓存:创建或更新短信 Client + List propertiesList = SmsChannelConvert.INSTANCE.convertList02(channels); + propertiesList.forEach(properties -> smsClientFactory.createOrUpdateSmsClient(properties)); + this.channelCache = channels; + } + + /** + * 通过定时任务轮询,刷新缓存 + * + * 目的:多节点部署时,通过轮询”通知“所有节点,进行刷新 + */ + @Scheduled(initialDelay = 60, fixedRate = 60, timeUnit = TimeUnit.SECONDS) + public void refreshLocalCache() { + // 情况一:如果缓存里没有数据,则直接刷新缓存 + if (CollUtil.isEmpty(channelCache)) { + initLocalCache(); + return; + } + + // 情况二,如果缓存里数据,则通过 updateTime 判断是否有数据变更,有变更则刷新缓存 + LocalDateTime maxTime = getMaxValue(channelCache, SmsChannelDO::getUpdateTime); + if (smsChannelMapper.selectCountByUpdateTimeGt(maxTime) > 0) { + initLocalCache(); + } + } + + @Override + public Long createSmsChannel(SmsChannelCreateReqVO createReqVO) { + // 插入 + SmsChannelDO smsChannel = SmsChannelConvert.INSTANCE.convert(createReqVO); + smsChannelMapper.insert(smsChannel); + + // 刷新缓存 + initLocalCache(); + return smsChannel.getId(); + } + + @Override + public void updateSmsChannel(SmsChannelUpdateReqVO updateReqVO) { + // 校验存在 + validateSmsChannelExists(updateReqVO.getId()); + // 更新 + SmsChannelDO updateObj = SmsChannelConvert.INSTANCE.convert(updateReqVO); + smsChannelMapper.updateById(updateObj); + + // 刷新缓存 + initLocalCache(); + } + + @Override + public void deleteSmsChannel(Long id) { + // 校验存在 + validateSmsChannelExists(id); + // 校验是否有在使用该账号的模版 + if (smsTemplateService.countByChannelId(id) > 0) { + throw exception(SMS_CHANNEL_HAS_CHILDREN); + } + // 删除 + smsChannelMapper.deleteById(id); + + // 刷新缓存 + initLocalCache(); + } + + private void validateSmsChannelExists(Long id) { + if (smsChannelMapper.selectById(id) == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + } + + @Override + public SmsChannelDO getSmsChannel(Long id) { + return smsChannelMapper.selectById(id); + } + + @Override + public List getSmsChannelList() { + return smsChannelMapper.selectList(); + } + + @Override + public PageResult getSmsChannelPage(SmsChannelPageReqVO pageReqVO) { + return smsChannelMapper.selectPage(pageReqVO); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsCodeService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsCodeService.java new file mode 100644 index 0000000..0702974 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsCodeService.java @@ -0,0 +1,40 @@ +package com.yunxi.scm.module.system.service.sms; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeUseReqDTO; + +import javax.validation.Valid; + +/** + * 短信验证码 Service 接口 + * + * @author 芋道源码 + */ +public interface SmsCodeService { + + /** + * 创建短信验证码,并进行发送 + * + * @param reqDTO 发送请求 + */ + void sendSmsCode(@Valid SmsCodeSendReqDTO reqDTO); + + /** + * 验证短信验证码,并进行使用 + * 如果正确,则将验证码标记成已使用 + * 如果错误,则抛出 {@link ServiceException} 异常 + * + * @param reqDTO 使用请求 + */ + void useSmsCode(@Valid SmsCodeUseReqDTO reqDTO); + + /** + * 检查验证码是否有效 + * + * @param reqDTO 校验请求 + */ + void validateSmsCode(@Valid SmsCodeValidateReqDTO reqDTO); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsCodeServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsCodeServiceImpl.java new file mode 100644 index 0000000..bee73d8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsCodeServiceImpl.java @@ -0,0 +1,111 @@ +package com.yunxi.scm.module.system.service.sms; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsCodeDO; +import com.yunxi.scm.module.system.dal.mysql.sms.SmsCodeMapper; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import com.yunxi.scm.module.system.framework.sms.SmsCodeProperties; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.randomInt; +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.date.DateUtils.isToday; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信验证码 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class SmsCodeServiceImpl implements SmsCodeService { + + @Resource + private SmsCodeProperties smsCodeProperties; + + @Resource + private SmsCodeMapper smsCodeMapper; + + @Resource + private SmsSendService smsSendService; + + @Override + public void sendSmsCode(SmsCodeSendReqDTO reqDTO) { + SmsSceneEnum sceneEnum = SmsSceneEnum.getCodeByScene(reqDTO.getScene()); + Assert.notNull(sceneEnum, "验证码场景({}) 查找不到配置", reqDTO.getScene()); + // 创建验证码 + String code = createSmsCode(reqDTO.getMobile(), reqDTO.getScene(), reqDTO.getCreateIp()); + // 发送验证码 + smsSendService.sendSingleSms(reqDTO.getMobile(), null, null, + sceneEnum.getTemplateCode(), MapUtil.of("code", code)); + } + + private String createSmsCode(String mobile, Integer scene, String ip) { + // 校验是否可以发送验证码,不用筛选场景 + SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, null, null); + if (lastSmsCode != null) { + if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis() + < smsCodeProperties.getSendFrequency().toMillis()) { // 发送过于频繁 + throw exception(SMS_CODE_SEND_TOO_FAST); + } + if (isToday(lastSmsCode.getCreateTime()) && // 必须是今天,才能计算超过当天的上限 + lastSmsCode.getTodayIndex() >= smsCodeProperties.getSendMaximumQuantityPerDay()) { // 超过当天发送的上限。 + throw exception(SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY); + } + // TODO 芋艿:提升,每个 IP 每天可发送数量 + // TODO 芋艿:提升,每个 IP 每小时可发送数量 + } + + // 创建验证码记录 + String code = String.valueOf(randomInt(smsCodeProperties.getBeginCode(), smsCodeProperties.getEndCode() + 1)); + SmsCodeDO newSmsCode = SmsCodeDO.builder().mobile(mobile).code(code).scene(scene) + .todayIndex(lastSmsCode != null && isToday(lastSmsCode.getCreateTime()) ? lastSmsCode.getTodayIndex() + 1 : 1) + .createIp(ip).used(false).build(); + smsCodeMapper.insert(newSmsCode); + return code; + } + + @Override + public void useSmsCode(SmsCodeUseReqDTO reqDTO) { + // 检测验证码是否有效 + SmsCodeDO lastSmsCode = validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); + // 使用验证码 + smsCodeMapper.updateById(SmsCodeDO.builder().id(lastSmsCode.getId()) + .used(true).usedTime(LocalDateTime.now()).usedIp(reqDTO.getUsedIp()).build()); + } + + @Override + public void validateSmsCode(SmsCodeValidateReqDTO reqDTO) { + validateSmsCode0(reqDTO.getMobile(), reqDTO.getCode(), reqDTO.getScene()); + } + + private SmsCodeDO validateSmsCode0(String mobile, String code, Integer scene) { + // 校验验证码 + SmsCodeDO lastSmsCode = smsCodeMapper.selectLastByMobile(mobile, code, scene); + // 若验证码不存在,抛出异常 + if (lastSmsCode == null) { + throw exception(SMS_CODE_NOT_FOUND); + } + // 超过时间 + if (LocalDateTimeUtil.between(lastSmsCode.getCreateTime(), LocalDateTime.now()).toMillis() + >= smsCodeProperties.getExpireTimes().toMillis()) { // 验证码已过期 + throw exception(SMS_CODE_EXPIRED); + } + // 判断验证码是否已被使用 + if (Boolean.TRUE.equals(lastSmsCode.getUsed())) { + throw exception(SMS_CODE_USED); + } + return lastSmsCode; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsLogService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsLogService.java new file mode 100644 index 0000000..8d7bd5e --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsLogService.java @@ -0,0 +1,77 @@ +package com.yunxi.scm.module.system.service.sms; + +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsLogDO; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +/** + * 短信日志 Service 接口 + * + * @author zzf + * @date 13:48 2021/3/2 + */ +public interface SmsLogService { + + /** + * 创建短信日志 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param isSend 是否发送 + * @param template 短信模板 + * @param templateContent 短信内容 + * @param templateParams 短信参数 + * @return 发送日志编号 + */ + Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, + SmsTemplateDO template, String templateContent, Map templateParams); + + /** + * 更新日志的发送结果 + * + * @param id 日志编号 + * @param sendCode 发送结果的编码 + * @param sendMsg 发送结果的提示 + * @param apiSendCode 短信 API 发送结果的编码 + * @param apiSendMsg 短信 API 发送失败的提示 + * @param apiRequestId 短信 API 发送返回的唯一请求 ID + * @param apiSerialNo 短信 API 发送返回的序号 + */ + void updateSmsSendResult(Long id, Integer sendCode, String sendMsg, + String apiSendCode, String apiSendMsg, String apiRequestId, String apiSerialNo); + + /** + * 更新日志的接收结果 + * + * @param id 日志编号 + * @param success 是否接收成功 + * @param receiveTime 用户接收时间 + * @param apiReceiveCode API 接收结果的编码 + * @param apiReceiveMsg API 接收结果的说明 + */ + void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime, String apiReceiveCode, String apiReceiveMsg); + + /** + * 获得短信日志分页 + * + * @param pageReqVO 分页查询 + * @return 短信日志分页 + */ + PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO); + + /** + * 获得短信日志列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 短信日志列表 + */ + List getSmsLogList(SmsLogExportReqVO exportReqVO); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsLogServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsLogServiceImpl.java new file mode 100644 index 0000000..df13c6d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsLogServiceImpl.java @@ -0,0 +1,88 @@ +package com.yunxi.scm.module.system.service.sms; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsLogDO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.sms.SmsLogMapper; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.enums.sms.SmsReceiveStatusEnum; +import com.yunxi.scm.module.system.enums.sms.SmsSendStatusEnum; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * 短信日志 Service 实现类 + * + * @author zzf + */ +@Slf4j +@Service +public class SmsLogServiceImpl implements SmsLogService { + + @Resource + private SmsLogMapper smsLogMapper; + + @Override + public Long createSmsLog(String mobile, Long userId, Integer userType, Boolean isSend, + SmsTemplateDO template, String templateContent, Map templateParams) { + SmsLogDO.SmsLogDOBuilder logBuilder = SmsLogDO.builder(); + // 根据是否要发送,设置状态 + logBuilder.sendStatus(Objects.equals(isSend, true) ? SmsSendStatusEnum.INIT.getStatus() + : SmsSendStatusEnum.IGNORE.getStatus()); + // 设置手机相关字段 + logBuilder.mobile(mobile).userId(userId).userType(userType); + // 设置模板相关字段 + logBuilder.templateId(template.getId()).templateCode(template.getCode()).templateType(template.getType()); + logBuilder.templateContent(templateContent).templateParams(templateParams) + .apiTemplateId(template.getApiTemplateId()); + // 设置渠道相关字段 + logBuilder.channelId(template.getChannelId()).channelCode(template.getChannelCode()); + // 设置接收相关字段 + logBuilder.receiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + + // 插入数据库 + SmsLogDO logDO = logBuilder.build(); + smsLogMapper.insert(logDO); + return logDO.getId(); + } + + @Override + public void updateSmsSendResult(Long id, Integer sendCode, String sendMsg, + String apiSendCode, String apiSendMsg, + String apiRequestId, String apiSerialNo) { + SmsSendStatusEnum sendStatus = CommonResult.isSuccess(sendCode) ? + SmsSendStatusEnum.SUCCESS : SmsSendStatusEnum.FAILURE; + smsLogMapper.updateById(SmsLogDO.builder().id(id).sendStatus(sendStatus.getStatus()) + .sendTime(LocalDateTime.now()).sendCode(sendCode).sendMsg(sendMsg) + .apiSendCode(apiSendCode).apiSendMsg(apiSendMsg) + .apiRequestId(apiRequestId).apiSerialNo(apiSerialNo).build()); + } + + @Override + public void updateSmsReceiveResult(Long id, Boolean success, LocalDateTime receiveTime, + String apiReceiveCode, String apiReceiveMsg) { + SmsReceiveStatusEnum receiveStatus = Objects.equals(success, true) ? + SmsReceiveStatusEnum.SUCCESS : SmsReceiveStatusEnum.FAILURE; + smsLogMapper.updateById(SmsLogDO.builder().id(id).receiveStatus(receiveStatus.getStatus()) + .receiveTime(receiveTime).apiReceiveCode(apiReceiveCode).apiReceiveMsg(apiReceiveMsg).build()); + } + + @Override + public PageResult getSmsLogPage(SmsLogPageReqVO pageReqVO) { + return smsLogMapper.selectPage(pageReqVO); + } + + @Override + public List getSmsLogList(SmsLogExportReqVO exportReqVO) { + return smsLogMapper.selectList(exportReqVO); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsSendService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsSendService.java new file mode 100644 index 0000000..f8b01b8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsSendService.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.system.service.sms; + +import com.yunxi.scm.module.system.mq.message.sms.SmsSendMessage; + +import java.util.List; +import java.util.Map; + +/** + * 短信发送 Service 接口 + * + * @author 芋道源码 + */ +public interface SmsSendService { + + /** + * 发送单条短信给管理后台的用户 + * + * 在 mobile 为空时,使用 userId 加载对应管理员的手机号 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSmsToAdmin(String mobile, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条短信给用户 APP 的用户 + * + * 在 mobile 为空时,使用 userId 加载对应会员的手机号 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSmsToMember(String mobile, Long userId, + String templateCode, Map templateParams); + + /** + * 发送单条短信给用户 + * + * @param mobile 手机号 + * @param userId 用户编号 + * @param userType 用户类型 + * @param templateCode 短信模板编号 + * @param templateParams 短信模板参数 + * @return 发送日志编号 + */ + Long sendSingleSms(String mobile, Long userId, Integer userType, + String templateCode, Map templateParams); + + default void sendBatchSms(List mobiles, List userIds, Integer userType, + String templateCode, Map templateParams) { + throw new UnsupportedOperationException("暂时不支持该操作,感兴趣可以实现该功能哟!"); + } + + /** + * 执行真正的短信发送 + * 注意,该方法仅仅提供给 MQ Consumer 使用 + * + * @param message 短信 + */ + void doSendSms(SmsSendMessage message); + + /** + * 接收短信的接收结果 + * + * @param channelCode 渠道编码 + * @param text 结果内容 + * @throws Throwable 处理失败时,抛出异常 + */ + void receiveSmsStatus(String channelCode, String text) throws Throwable; + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsSendServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsSendServiceImpl.java new file mode 100644 index 0000000..8be2ac2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsSendServiceImpl.java @@ -0,0 +1,188 @@ +package com.yunxi.scm.module.system.service.sms; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.datapermission.core.annotation.DataPermission; +import com.yunxi.scm.framework.sms.core.client.SmsClient; +import com.yunxi.scm.framework.sms.core.client.SmsClientFactory; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.mq.message.sms.SmsSendMessage; +import com.yunxi.scm.module.system.mq.producer.sms.SmsProducer; +import com.yunxi.scm.module.system.service.member.MemberService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import com.google.common.annotations.VisibleForTesting; +import org.springframework.stereotype.Service; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信发送 Service 发送的实现 + * + * @author 芋道源码 + */ +@Service +public class SmsSendServiceImpl implements SmsSendService { + + @Resource + private AdminUserService adminUserService; + @Resource + private MemberService memberService; + @Resource + private SmsChannelService smsChannelService; + @Resource + private SmsTemplateService smsTemplateService; + @Resource + private SmsLogService smsLogService; + + @Resource + private SmsClientFactory smsClientFactory; + + @Resource + private SmsProducer smsProducer; + + @Override + @DataPermission(enable = false) // 发送短信时,无需考虑数据权限 + public Long sendSingleSmsToAdmin(String mobile, Long userId, String templateCode, Map templateParams) { + // 如果 mobile 为空,则加载用户编号对应的手机号 + if (StrUtil.isEmpty(mobile)) { + AdminUserDO user = adminUserService.getUser(userId); + if (user != null) { + mobile = user.getMobile(); + } + } + // 执行发送 + return sendSingleSms(mobile, userId, UserTypeEnum.ADMIN.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleSmsToMember(String mobile, Long userId, String templateCode, Map templateParams) { + // 如果 mobile 为空,则加载用户编号对应的手机号 + if (StrUtil.isEmpty(mobile)) { + mobile = memberService.getMemberUserMobile(userId); + } + // 执行发送 + return sendSingleSms(mobile, userId, UserTypeEnum.MEMBER.getValue(), templateCode, templateParams); + } + + @Override + public Long sendSingleSms(String mobile, Long userId, Integer userType, + String templateCode, Map templateParams) { + // 校验短信模板是否合法 + SmsTemplateDO template = validateSmsTemplate(templateCode); + // 校验短信渠道是否合法 + SmsChannelDO smsChannel = validateSmsChannel(template.getChannelId()); + + // 校验手机号码是否存在 + mobile = validateMobile(mobile); + // 构建有序的模板参数。为什么放在这个位置,是提前保证模板参数的正确性,而不是到了插入发送日志 + List> newTemplateParams = buildTemplateParams(template, templateParams); + + // 创建发送日志。如果模板被禁用,则不发送短信,只记录日志 + Boolean isSend = CommonStatusEnum.ENABLE.getStatus().equals(template.getStatus()) + && CommonStatusEnum.ENABLE.getStatus().equals(smsChannel.getStatus()); + ; + String content = smsTemplateService.formatSmsTemplateContent(template.getContent(), templateParams); + Long sendLogId = smsLogService.createSmsLog(mobile, userId, userType, isSend, template, content, templateParams); + + // 发送 MQ 消息,异步执行发送短信 + if (isSend) { + smsProducer.sendSmsSendMessage(sendLogId, mobile, template.getChannelId(), + template.getApiTemplateId(), newTemplateParams); + } + return sendLogId; + } + + @VisibleForTesting + SmsChannelDO validateSmsChannel(Long channelId) { + // 获得短信模板。考虑到效率,从缓存中获取 + SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); + // 短信模板不存在 + if (channelDO == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + return channelDO; + } + + @VisibleForTesting + SmsTemplateDO validateSmsTemplate(String templateCode) { + // 获得短信模板。考虑到效率,从缓存中获取 + SmsTemplateDO template = smsTemplateService.getSmsTemplateByCodeFromCache(templateCode); + // 短信模板不存在 + if (template == null) { + throw exception(SMS_SEND_TEMPLATE_NOT_EXISTS); + } + return template; + } + + /** + * 将参数模板,处理成有序的 KeyValue 数组 + *

+ * 原因是,部分短信平台并不是使用 key 作为参数,而是数组下标,例如说腾讯云 https://cloud.tencent.com/document/product/382/39023 + * + * @param template 短信模板 + * @param templateParams 原始参数 + * @return 处理后的参数 + */ + @VisibleForTesting + List> buildTemplateParams(SmsTemplateDO template, Map templateParams) { + return template.getParams().stream().map(key -> { + Object value = templateParams.get(key); + if (value == null) { + throw exception(SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, key); + } + return new KeyValue<>(key, value); + }).collect(Collectors.toList()); + } + + @VisibleForTesting + public String validateMobile(String mobile) { + if (StrUtil.isEmpty(mobile)) { + throw exception(SMS_SEND_MOBILE_NOT_EXISTS); + } + return mobile; + } + + @Override + public void doSendSms(SmsSendMessage message) { + // 获得渠道对应的 SmsClient 客户端 + SmsClient smsClient = smsClientFactory.getSmsClient(message.getChannelId()); + Assert.notNull(smsClient, "短信客户端({}) 不存在", message.getChannelId()); + // 发送短信 + SmsCommonResult sendResult = smsClient.sendSms(message.getLogId(), message.getMobile(), + message.getApiTemplateId(), message.getTemplateParams()); + smsLogService.updateSmsSendResult(message.getLogId(), sendResult.getCode(), sendResult.getMsg(), + sendResult.getApiCode(), sendResult.getApiMsg(), sendResult.getApiRequestId(), + sendResult.getData() != null ? sendResult.getData().getSerialNo() : null); + } + + @Override + public void receiveSmsStatus(String channelCode, String text) throws Throwable { + // 获得渠道对应的 SmsClient 客户端 + SmsClient smsClient = smsClientFactory.getSmsClient(channelCode); + Assert.notNull(smsClient, "短信客户端({}) 不存在", channelCode); + // 解析内容 + List receiveResults = smsClient.parseSmsReceiveStatus(text); + if (CollUtil.isEmpty(receiveResults)) { + return; + } + // 更新短信日志的接收结果. 因为量一般不大,所以先使用 for 循环更新 + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(result.getLogId(), + result.getSuccess(), result.getReceiveTime(), result.getErrorCode(), result.getErrorMsg())); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsTemplateService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsTemplateService.java new file mode 100644 index 0000000..929fc3d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsTemplateService.java @@ -0,0 +1,94 @@ +package com.yunxi.scm.module.system.service.sms; + +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.framework.common.pojo.PageResult; + +import javax.validation.Valid; +import java.util.List; +import java.util.Map; + +/** + * 短信模板 Service 接口 + * + * @author zzf + * @since 2021/1/25 9:24 + */ +public interface SmsTemplateService { + + /** + * 创建短信模板 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createSmsTemplate(@Valid SmsTemplateCreateReqVO createReqVO); + + /** + * 更新短信模板 + * + * @param updateReqVO 更新信息 + */ + void updateSmsTemplate(@Valid SmsTemplateUpdateReqVO updateReqVO); + + /** + * 删除短信模板 + * + * @param id 编号 + */ + void deleteSmsTemplate(Long id); + + /** + * 获得短信模板 + * + * @param id 编号 + * @return 短信模板 + */ + SmsTemplateDO getSmsTemplate(Long id); + + /** + * 获得短信模板,从缓存中 + * + * @param code 模板编码 + * @return 短信模板 + */ + SmsTemplateDO getSmsTemplateByCodeFromCache(String code); + + /** + * 获得短信模板分页 + * + * @param pageReqVO 分页查询 + * @return 短信模板分页 + */ + PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO); + + /** + * 获得短信模板列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 短信模板分页 + */ + List getSmsTemplateList(SmsTemplateExportReqVO exportReqVO); + + /** + * 获得指定短信渠道下的短信模板数量 + * + * @param channelId 短信渠道编号 + * @return 数量 + */ + Long countByChannelId(Long channelId); + + + /** + * 格式化短信内容 + * + * @param content 短信模板的内容 + * @param params 内容的参数 + * @return 格式化后的内容 + */ + String formatSmsTemplateContent(String content, Map params); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsTemplateServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsTemplateServiceImpl.java new file mode 100644 index 0000000..cd3d149 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/sms/SmsTemplateServiceImpl.java @@ -0,0 +1,194 @@ +package com.yunxi.scm.module.system.service.sms; + +import cn.hutool.core.util.ReUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.sms.core.client.SmsClient; +import com.yunxi.scm.framework.sms.core.client.SmsClientFactory; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO; +import com.yunxi.scm.module.system.convert.sms.SmsTemplateConvert; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.sms.SmsTemplateMapper; +import com.yunxi.scm.module.system.dal.redis.RedisKeyConstants; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.regex.Pattern; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 短信模板 Service 实现类 + * + * @author zzf + * @since 2021/1/25 9:25 + */ +@Service +@Slf4j +public class SmsTemplateServiceImpl implements SmsTemplateService { + + /** + * 正则表达式,匹配 {} 中的变量 + */ + private static final Pattern PATTERN_PARAMS = Pattern.compile("\\{(.*?)}"); + + @Resource + private SmsTemplateMapper smsTemplateMapper; + + @Resource + private SmsChannelService smsChannelService; + + @Resource + private SmsClientFactory smsClientFactory; + + @Override + public Long createSmsTemplate(SmsTemplateCreateReqVO createReqVO) { + // 校验短信渠道 + SmsChannelDO channelDO = validateSmsChannel(createReqVO.getChannelId()); + // 校验短信编码是否重复 + validateSmsTemplateCodeDuplicate(null, createReqVO.getCode()); + // 校验短信模板 + validateApiTemplate(createReqVO.getChannelId(), createReqVO.getApiTemplateId()); + + // 插入 + SmsTemplateDO template = SmsTemplateConvert.INSTANCE.convert(createReqVO); + template.setParams(parseTemplateContentParams(template.getContent())); + template.setChannelCode(channelDO.getCode()); + smsTemplateMapper.insert(template); + // 返回 + return template.getId(); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.SMS_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为可能修改到 code 字段,不好清理 + public void updateSmsTemplate(SmsTemplateUpdateReqVO updateReqVO) { + // 校验存在 + validateSmsTemplateExists(updateReqVO.getId()); + // 校验短信渠道 + SmsChannelDO channelDO = validateSmsChannel(updateReqVO.getChannelId()); + // 校验短信编码是否重复 + validateSmsTemplateCodeDuplicate(updateReqVO.getId(), updateReqVO.getCode()); + // 校验短信模板 + validateApiTemplate(updateReqVO.getChannelId(), updateReqVO.getApiTemplateId()); + + // 更新 + SmsTemplateDO updateObj = SmsTemplateConvert.INSTANCE.convert(updateReqVO); + updateObj.setParams(parseTemplateContentParams(updateObj.getContent())); + updateObj.setChannelCode(channelDO.getCode()); + smsTemplateMapper.updateById(updateObj); + } + + @Override + @CacheEvict(cacheNames = RedisKeyConstants.SMS_TEMPLATE, + allEntries = true) // allEntries 清空所有缓存,因为 id 不是直接的缓存 code,不好清理 + public void deleteSmsTemplate(Long id) { + // 校验存在 + validateSmsTemplateExists(id); + // 更新 + smsTemplateMapper.deleteById(id); + } + + private void validateSmsTemplateExists(Long id) { + if (smsTemplateMapper.selectById(id) == null) { + throw exception(SMS_TEMPLATE_NOT_EXISTS); + } + } + + @Override + public SmsTemplateDO getSmsTemplate(Long id) { + return smsTemplateMapper.selectById(id); + } + + @Override + @Cacheable(cacheNames = RedisKeyConstants.SMS_TEMPLATE, key = "#code", + unless = "#result == null") + public SmsTemplateDO getSmsTemplateByCodeFromCache(String code) { + return smsTemplateMapper.selectByCode(code); + } + + @Override + public PageResult getSmsTemplatePage(SmsTemplatePageReqVO pageReqVO) { + return smsTemplateMapper.selectPage(pageReqVO); + } + + @Override + public List getSmsTemplateList(SmsTemplateExportReqVO exportReqVO) { + return smsTemplateMapper.selectList(exportReqVO); + } + + @Override + public Long countByChannelId(Long channelId) { + return smsTemplateMapper.selectCountByChannelId(channelId); + } + + @VisibleForTesting + public SmsChannelDO validateSmsChannel(Long channelId) { + SmsChannelDO channelDO = smsChannelService.getSmsChannel(channelId); + if (channelDO == null) { + throw exception(SMS_CHANNEL_NOT_EXISTS); + } + if (!Objects.equals(channelDO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { + throw exception(SMS_CHANNEL_DISABLE); + } + return channelDO; + } + + @VisibleForTesting + public void validateSmsTemplateCodeDuplicate(Long id, String code) { + SmsTemplateDO template = smsTemplateMapper.selectByCode(code); + if (template == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的字典类型 + if (id == null) { + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); + } + if (!template.getId().equals(id)) { + throw exception(SMS_TEMPLATE_CODE_DUPLICATE, code); + } + } + + /** + * 校验 API 短信平台的模板是否有效 + * + * @param channelId 渠道编号 + * @param apiTemplateId API 模板编号 + */ + @VisibleForTesting + void validateApiTemplate(Long channelId, String apiTemplateId) { + // 获得短信模板 + SmsClient smsClient = smsClientFactory.getSmsClient(channelId); + Assert.notNull(smsClient, String.format("短信客户端(%d) 不存在", channelId)); + SmsCommonResult templateResult = smsClient.getSmsTemplate(apiTemplateId); + // 校验短信模板是否正确 + templateResult.checkError(); + } + + @Override + public String formatSmsTemplateContent(String content, Map params) { + return StrUtil.format(content, params); + } + + @VisibleForTesting + List parseTemplateContentParams(String content) { + return ReUtil.findAllGroup1(PATTERN_PARAMS, content); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/social/SocialUserService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/social/SocialUserService.java new file mode 100644 index 0000000..ddebbeb --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/social/SocialUserService.java @@ -0,0 +1,78 @@ +package com.yunxi.scm.module.system.service.social; + +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserDO; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * 社交用户 Service 接口,例如说社交平台的授权登录 + * + * @author 芋道源码 + */ +public interface SocialUserService { + + /** + * 获得社交平台的授权 URL + * + * @param type 社交平台的类型 {@link SocialTypeEnum} + * @param redirectUri 重定向 URL + * @return 社交平台的授权 URL + */ + String getAuthorizeUrl(Integer type, String redirectUri); + + /** + * 授权获得对应的社交用户 + * 如果授权失败,则会抛出 {@link ServiceException} 异常 + * + * @param type 社交平台的类型 {@link SocialTypeEnum} + * @param code 授权码 + * @param state state + * @return 授权用户 + */ + @NotNull + SocialUserDO authSocialUser(Integer type, String code, String state); + + /** + * 获得指定用户的社交用户列表 + * + * @param userId 用户编号 + * @param userType 用户类型 + * @return 社交用户列表 + */ + List getSocialUserList(Long userId, Integer userType); + + /** + * 绑定社交用户 + * + * @param reqDTO 绑定信息 + */ + void bindSocialUser(@Valid SocialUserBindReqDTO reqDTO); + + /** + * 取消绑定社交用户 + * + * @param userId 用户编号 + * @param userType 全局用户类型 + * @param type 社交平台的类型 {@link SocialTypeEnum} + * @param openid 社交平台的 openid + */ + void unbindSocialUser(Long userId, Integer userType, Integer type, String openid); + + /** + * 获得社交用户的绑定用户编号 + * 注意,返回的是 MemberUser 或者 AdminUser 的 id 编号! + * 在认证信息不正确的情况下,也会抛出 {@link ServiceException} 业务异常 + * + * @param userType 用户类型 + * @param type 社交平台的类型 + * @param code 授权码 + * @param state state + * @return 绑定用户编号 + */ + Long getBindUserId(Integer userType, Integer type, String code, String state); +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/social/SocialUserServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/social/SocialUserServiceImpl.java new file mode 100644 index 0000000..ffebdb3 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/social/SocialUserServiceImpl.java @@ -0,0 +1,167 @@ +package com.yunxi.scm.module.system.service.social; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import com.yunxi.scm.framework.common.util.http.HttpUtils; +import com.yunxi.scm.framework.social.core.YunxiAuthRequestFactory; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserBindDO; +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserDO; +import com.yunxi.scm.module.system.dal.mysql.social.SocialUserBindMapper; +import com.yunxi.scm.module.system.dal.mysql.social.SocialUserMapper; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import com.xingyuv.jushauth.model.AuthCallback; +import com.xingyuv.jushauth.model.AuthResponse; +import com.xingyuv.jushauth.model.AuthUser; +import com.xingyuv.jushauth.request.AuthRequest; +import com.xingyuv.jushauth.utils.AuthStateUtils; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 社交用户 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class SocialUserServiceImpl implements SocialUserService { + + @Resource// 由于自定义了 YunxiAuthRequestFactory 无法覆盖默认的 AuthRequestFactory,所以只能注入它 + private YunxiAuthRequestFactory yunxiAuthRequestFactory; + + @Resource + private SocialUserBindMapper socialUserBindMapper; + @Resource + private SocialUserMapper socialUserMapper; + + @Override + public String getAuthorizeUrl(Integer type, String redirectUri) { + // 获得对应的 AuthRequest 实现 + AuthRequest authRequest = yunxiAuthRequestFactory.get(SocialTypeEnum.valueOfType(type).getSource()); + // 生成跳转地址 + String authorizeUri = authRequest.authorize(AuthStateUtils.createState()); + return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri); + } + + @Override + public SocialUserDO authSocialUser(Integer type, String code, String state) { + // 优先从 DB 中获取,因为 code 有且可以使用一次。 + // 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次 + SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(type, code, state); + if (socialUser != null) { + return socialUser; + } + + // 请求获取 + AuthUser authUser = getAuthUser(type, code, state); + Assert.notNull(authUser, "三方用户不能为空"); + + // 保存到 DB 中 + socialUser = socialUserMapper.selectByTypeAndOpenid(type, authUser.getUuid()); + if (socialUser == null) { + socialUser = new SocialUserDO(); + } + socialUser.setType(type).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询 + .setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken()))) + .setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo())); + if (socialUser.getId() == null) { + socialUserMapper.insert(socialUser); + } else { + socialUserMapper.updateById(socialUser); + } + return socialUser; + } + + @Override + public List getSocialUserList(Long userId, Integer userType) { + // 获得绑定 + List socialUserBinds = socialUserBindMapper.selectListByUserIdAndUserType(userId, userType); + if (CollUtil.isEmpty(socialUserBinds)) { + return Collections.emptyList(); + } + // 获得社交用户 + return socialUserMapper.selectBatchIds(convertSet(socialUserBinds, SocialUserBindDO::getSocialUserId)); + } + + @Override + @Transactional + public void bindSocialUser(SocialUserBindReqDTO reqDTO) { + // 获得社交用户 + SocialUserDO socialUser = authSocialUser(reqDTO.getType(), reqDTO.getCode(), reqDTO.getState()); + Assert.notNull(socialUser, "社交用户不能为空"); + + // 社交用户可能之前绑定过别的用户,需要进行解绑 + socialUserBindMapper.deleteByUserTypeAndSocialUserId(reqDTO.getUserType(), socialUser.getId()); + + // 用户可能之前已经绑定过该社交类型,需要进行解绑 + socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(reqDTO.getUserType(), reqDTO.getUserId(), + socialUser.getType()); + + // 绑定当前登录的社交用户 + SocialUserBindDO socialUserBind = SocialUserBindDO.builder() + .userId(reqDTO.getUserId()).userType(reqDTO.getUserType()) + .socialUserId(socialUser.getId()).socialType(socialUser.getType()).build(); + socialUserBindMapper.insert(socialUserBind); + } + + @Override + public void unbindSocialUser(Long userId, Integer userType, Integer type, String openid) { + // 获得 openid 对应的 SocialUserDO 社交用户 + SocialUserDO socialUser = socialUserMapper.selectByTypeAndOpenid(type, openid); + if (socialUser == null) { + throw exception(SOCIAL_USER_NOT_FOUND); + } + + // 获得对应的社交绑定关系 + socialUserBindMapper.deleteByUserTypeAndUserIdAndSocialType(userType, userId, socialUser.getType()); + } + + @Override + public Long getBindUserId(Integer userType, Integer type, String code, String state) { + // 获得社交用户 + SocialUserDO socialUser = authSocialUser(type, code, state); + Assert.notNull(socialUser, "社交用户不能为空"); + + // 如果未绑定的社交用户,则无法自动登录,进行报错 + SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType, + socialUser.getId()); + if (socialUserBind == null) { + throw exception(AUTH_THIRD_LOGIN_NOT_BIND); + } + return socialUserBind.getUserId(); + } + + /** + * 请求社交平台,获得授权的用户 + * + * @param type 社交平台的类型 + * @param code 授权码 + * @param state 授权 state + * @return 授权的用户 + */ + private AuthUser getAuthUser(Integer type, String code, String state) { + AuthRequest authRequest = yunxiAuthRequestFactory.get(SocialTypeEnum.valueOfType(type).getSource()); + AuthCallback authCallback = AuthCallback.builder().code(code).state(state).build(); + AuthResponse authResponse = authRequest.login(authCallback); + log.info("[getAuthUser][请求社交平台 type({}) request({}) response({})]", type, + toJsonString(authCallback), toJsonString(authResponse)); + if (!authResponse.ok()) { + throw exception(SOCIAL_USER_AUTH_FAILURE, authResponse.getMsg()); + } + return (AuthUser) authResponse.getData(); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantPackageService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantPackageService.java new file mode 100644 index 0000000..966dbb8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantPackageService.java @@ -0,0 +1,73 @@ +package com.yunxi.scm.module.system.service.tenant; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantPackageDO; + +import javax.validation.Valid; +import java.util.List; + +/** + * 租户套餐 Service 接口 + * + * @author 芋道源码 + */ +public interface TenantPackageService { + + /** + * 创建租户套餐 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTenantPackage(@Valid TenantPackageCreateReqVO createReqVO); + + /** + * 更新租户套餐 + * + * @param updateReqVO 更新信息 + */ + void updateTenantPackage(@Valid TenantPackageUpdateReqVO updateReqVO); + + /** + * 删除租户套餐 + * + * @param id 编号 + */ + void deleteTenantPackage(Long id); + + /** + * 获得租户套餐 + * + * @param id 编号 + * @return 租户套餐 + */ + TenantPackageDO getTenantPackage(Long id); + + /** + * 获得租户套餐分页 + * + * @param pageReqVO 分页查询 + * @return 租户套餐分页 + */ + PageResult getTenantPackagePage(TenantPackagePageReqVO pageReqVO); + + /** + * 校验租户套餐 + * + * @param id 编号 + * @return 租户套餐 + */ + TenantPackageDO validTenantPackage(Long id); + + /** + * 获得指定状态的租户套餐列表 + * + * @param status 状态 + * @return 租户套餐 + */ + List getTenantPackageListByStatus(Integer status); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantPackageServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantPackageServiceImpl.java new file mode 100644 index 0000000..5a47db0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantPackageServiceImpl.java @@ -0,0 +1,115 @@ +package com.yunxi.scm.module.system.service.tenant; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO; +import com.yunxi.scm.module.system.convert.tenant.TenantPackageConvert; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantPackageDO; +import com.yunxi.scm.module.system.dal.mysql.tenant.TenantPackageMapper; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 租户套餐 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +public class TenantPackageServiceImpl implements TenantPackageService { + + @Resource + private TenantPackageMapper tenantPackageMapper; + + @Resource + @Lazy // 避免循环依赖的报错 + private TenantService tenantService; + + @Override + public Long createTenantPackage(TenantPackageCreateReqVO createReqVO) { + // 插入 + TenantPackageDO tenantPackage = TenantPackageConvert.INSTANCE.convert(createReqVO); + tenantPackageMapper.insert(tenantPackage); + // 返回 + return tenantPackage.getId(); + } + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + public void updateTenantPackage(TenantPackageUpdateReqVO updateReqVO) { + // 校验存在 + TenantPackageDO tenantPackage = validateTenantPackageExists(updateReqVO.getId()); + // 更新 + TenantPackageDO updateObj = TenantPackageConvert.INSTANCE.convert(updateReqVO); + tenantPackageMapper.updateById(updateObj); + // 如果菜单发生变化,则修改每个租户的菜单 + if (!CollUtil.isEqualList(tenantPackage.getMenuIds(), updateReqVO.getMenuIds())) { + List tenants = tenantService.getTenantListByPackageId(tenantPackage.getId()); + tenants.forEach(tenant -> tenantService.updateTenantRoleMenu(tenant.getId(), updateReqVO.getMenuIds())); + } + } + + @Override + public void deleteTenantPackage(Long id) { + // 校验存在 + validateTenantPackageExists(id); + // 校验正在使用 + validateTenantUsed(id); + // 删除 + tenantPackageMapper.deleteById(id); + } + + private TenantPackageDO validateTenantPackageExists(Long id) { + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); + if (tenantPackage == null) { + throw exception(TENANT_PACKAGE_NOT_EXISTS); + } + return tenantPackage; + } + + private void validateTenantUsed(Long id) { + if (tenantService.getTenantCountByPackageId(id) > 0) { + throw exception(TENANT_PACKAGE_USED); + } + } + + @Override + public TenantPackageDO getTenantPackage(Long id) { + return tenantPackageMapper.selectById(id); + } + + @Override + public PageResult getTenantPackagePage(TenantPackagePageReqVO pageReqVO) { + return tenantPackageMapper.selectPage(pageReqVO); + } + + @Override + public TenantPackageDO validTenantPackage(Long id) { + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(id); + if (tenantPackage == null) { + throw exception(TENANT_PACKAGE_NOT_EXISTS); + } + if (tenantPackage.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { + throw exception(TENANT_PACKAGE_DISABLE, tenantPackage.getName()); + } + return tenantPackage; + } + + @Override + public List getTenantPackageListByStatus(Integer status) { + return tenantPackageMapper.selectListByStatus(status); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantService.java new file mode 100644 index 0000000..7632e45 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantService.java @@ -0,0 +1,131 @@ +package com.yunxi.scm.module.system.service.tenant; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantExportReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import com.yunxi.scm.module.system.service.tenant.handler.TenantInfoHandler; +import com.yunxi.scm.module.system.service.tenant.handler.TenantMenuHandler; + +import javax.validation.Valid; +import java.util.List; +import java.util.Set; + +/** + * 租户 Service 接口 + * + * @author 芋道源码 + */ +public interface TenantService { + + /** + * 创建租户 + * + * @param createReqVO 创建信息 + * @return 编号 + */ + Long createTenant(@Valid TenantCreateReqVO createReqVO); + + /** + * 更新租户 + * + * @param updateReqVO 更新信息 + */ + void updateTenant(@Valid TenantUpdateReqVO updateReqVO); + + /** + * 更新租户的角色菜单 + * + * @param tenantId 租户编号 + * @param menuIds 菜单编号数组 + */ + void updateTenantRoleMenu(Long tenantId, Set menuIds); + + /** + * 删除租户 + * + * @param id 编号 + */ + void deleteTenant(Long id); + + /** + * 获得租户 + * + * @param id 编号 + * @return 租户 + */ + TenantDO getTenant(Long id); + + /** + * 获得租户分页 + * + * @param pageReqVO 分页查询 + * @return 租户分页 + */ + PageResult getTenantPage(TenantPageReqVO pageReqVO); + + /** + * 获得租户列表, 用于 Excel 导出 + * + * @param exportReqVO 查询条件 + * @return 租户列表 + */ + List getTenantList(TenantExportReqVO exportReqVO); + + /** + * 获得名字对应的租户 + * + * @param name 组户名 + * @return 租户 + */ + TenantDO getTenantByName(String name); + + /** + * 获得使用指定套餐的租户数量 + * + * @param packageId 租户套餐编号 + * @return 租户数量 + */ + Long getTenantCountByPackageId(Long packageId); + + /** + * 获得使用指定套餐的租户数组 + * + * @param packageId 租户套餐编号 + * @return 租户数组 + */ + List getTenantListByPackageId(Long packageId); + + /** + * 进行租户的信息处理逻辑 + * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 + * + * @param handler 处理器 + */ + void handleTenantInfo(TenantInfoHandler handler); + + /** + * 进行租户的菜单处理逻辑 + * 其中,租户编号从 {@link TenantContextHolder} 上下文中获取 + * + * @param handler 处理器 + */ + void handleTenantMenu(TenantMenuHandler handler); + + /** + * 获得所有租户 + * + * @return 租户编号数组 + */ + List getTenantIdList(); + + /** + * 校验租户是否合法 + * + * @param id 租户编号 + */ + void validTenant(Long id); +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantServiceImpl.java new file mode 100644 index 0000000..621f1ed --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/TenantServiceImpl.java @@ -0,0 +1,286 @@ +package com.yunxi.scm.module.system.service.tenant; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.tenant.config.TenantProperties; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.tenant.core.util.TenantUtils; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantExportReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO; +import com.yunxi.scm.module.system.convert.tenant.TenantConvert; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantPackageDO; +import com.yunxi.scm.module.system.dal.mysql.tenant.TenantMapper; +import com.yunxi.scm.module.system.enums.permission.RoleCodeEnum; +import com.yunxi.scm.module.system.enums.permission.RoleTypeEnum; +import com.yunxi.scm.module.system.service.permission.MenuService; +import com.yunxi.scm.module.system.service.permission.PermissionService; +import com.yunxi.scm.module.system.service.permission.RoleService; +import com.yunxi.scm.module.system.service.tenant.handler.TenantInfoHandler; +import com.yunxi.scm.module.system.service.tenant.handler.TenantMenuHandler; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import com.baomidou.dynamic.datasource.annotation.DSTransactional; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.validation.annotation.Validated; + +import javax.annotation.Resource; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; + +/** + * 租户 Service 实现类 + * + * @author 芋道源码 + */ +@Service +@Validated +@Slf4j +public class TenantServiceImpl implements TenantService { + + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired(required = false) // 由于 yunxi.tenant.enable 配置项,可以关闭多租户的功能,所以这里只能不强制注入 + private TenantProperties tenantProperties; + + @Resource + private TenantMapper tenantMapper; + + @Resource + private TenantPackageService tenantPackageService; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private AdminUserService userService; + @Resource + private RoleService roleService; + @Resource + private MenuService menuService; + @Resource + private PermissionService permissionService; + + @Override + public List getTenantIdList() { + List tenants = tenantMapper.selectList(); + return CollectionUtils.convertList(tenants, TenantDO::getId); + } + + @Override + public void validTenant(Long id) { + TenantDO tenant = getTenant(id); + if (tenant == null) { + throw exception(TENANT_NOT_EXISTS); + } + if (tenant.getStatus().equals(CommonStatusEnum.DISABLE.getStatus())) { + throw exception(TENANT_DISABLE, tenant.getName()); + } + if (DateUtils.isExpired(tenant.getExpireTime())) { + throw exception(TENANT_EXPIRE, tenant.getName()); + } + } + + @Override + @DSTransactional // 多数据源,使用 @DSTransactional 保证本地事务,以及数据源的切换 + public Long createTenant(TenantCreateReqVO createReqVO) { + // 校验租户名称是否重复 + validTenantNameDuplicate(createReqVO.getName(), null); + // 校验套餐被禁用 + TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(createReqVO.getPackageId()); + + // 创建租户 + TenantDO tenant = TenantConvert.INSTANCE.convert(createReqVO); + tenantMapper.insert(tenant); + + TenantUtils.execute(tenant.getId(), () -> { + // 创建角色 + Long roleId = createRole(tenantPackage); + // 创建用户,并分配角色 + Long userId = createUser(roleId, createReqVO); + // 修改租户的管理员 + tenantMapper.updateById(new TenantDO().setId(tenant.getId()).setContactUserId(userId)); + }); + return tenant.getId(); + } + + private Long createUser(Long roleId, TenantCreateReqVO createReqVO) { + // 创建用户 + Long userId = userService.createUser(TenantConvert.INSTANCE.convert02(createReqVO)); + // 分配角色 + permissionService.assignUserRole(userId, singleton(roleId)); + return userId; + } + + private Long createRole(TenantPackageDO tenantPackage) { + // 创建角色 + RoleCreateReqVO reqVO = new RoleCreateReqVO(); + reqVO.setName(RoleCodeEnum.TENANT_ADMIN.getName()).setCode(RoleCodeEnum.TENANT_ADMIN.getCode()) + .setSort(0).setRemark("系统自动生成"); + Long roleId = roleService.createRole(reqVO, RoleTypeEnum.SYSTEM.getType()); + // 分配权限 + permissionService.assignRoleMenu(roleId, tenantPackage.getMenuIds()); + return roleId; + } + + @Override + @DSTransactional + public void updateTenant(TenantUpdateReqVO updateReqVO) { + // 校验存在 + TenantDO tenant = validateUpdateTenant(updateReqVO.getId()); + // 校验租户名称是否重复 + validTenantNameDuplicate(updateReqVO.getName(), updateReqVO.getId()); + // 校验套餐被禁用 + TenantPackageDO tenantPackage = tenantPackageService.validTenantPackage(updateReqVO.getPackageId()); + + // 更新租户 + TenantDO updateObj = TenantConvert.INSTANCE.convert(updateReqVO); + tenantMapper.updateById(updateObj); + // 如果套餐发生变化,则修改其角色的权限 + if (ObjectUtil.notEqual(tenant.getPackageId(), updateReqVO.getPackageId())) { + updateTenantRoleMenu(tenant.getId(), tenantPackage.getMenuIds()); + } + } + + private void validTenantNameDuplicate(String name, Long id) { + TenantDO tenant = tenantMapper.selectByName(name); + if (tenant == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同名字的租户 + if (id == null) { + throw exception(TENANT_NAME_DUPLICATE, name); + } + if (!tenant.getId().equals(id)) { + throw exception(TENANT_NAME_DUPLICATE, name); + } + } + + @Override + @DSTransactional + public void updateTenantRoleMenu(Long tenantId, Set menuIds) { + TenantUtils.execute(tenantId, () -> { + // 获得所有角色 + List roles = roleService.getRoleListByStatus(null); + roles.forEach(role -> Assert.isTrue(tenantId.equals(role.getTenantId()), "角色({}/{}) 租户不匹配", + role.getId(), role.getTenantId(), tenantId)); // 兜底校验 + // 重新分配每个角色的权限 + roles.forEach(role -> { + // 如果是租户管理员,重新分配其权限为租户套餐的权限 + if (Objects.equals(role.getCode(), RoleCodeEnum.TENANT_ADMIN.getCode())) { + permissionService.assignRoleMenu(role.getId(), menuIds); + log.info("[updateTenantRoleMenu][租户管理员({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), menuIds); + return; + } + // 如果是其他角色,则去掉超过套餐的权限 + Set roleMenuIds = permissionService.getRoleMenuListByRoleId(role.getId()); + roleMenuIds = CollUtil.intersectionDistinct(roleMenuIds, menuIds); + permissionService.assignRoleMenu(role.getId(), roleMenuIds); + log.info("[updateTenantRoleMenu][角色({}/{}) 的权限修改为({})]", role.getId(), role.getTenantId(), roleMenuIds); + }); + }); + } + + @Override + public void deleteTenant(Long id) { + // 校验存在 + validateUpdateTenant(id); + // 删除 + tenantMapper.deleteById(id); + } + + private TenantDO validateUpdateTenant(Long id) { + TenantDO tenant = tenantMapper.selectById(id); + if (tenant == null) { + throw exception(TENANT_NOT_EXISTS); + } + // 内置租户,不允许删除 + if (isSystemTenant(tenant)) { + throw exception(TENANT_CAN_NOT_UPDATE_SYSTEM); + } + return tenant; + } + + @Override + public TenantDO getTenant(Long id) { + return tenantMapper.selectById(id); + } + + @Override + public PageResult getTenantPage(TenantPageReqVO pageReqVO) { + return tenantMapper.selectPage(pageReqVO); + } + + @Override + public List getTenantList(TenantExportReqVO exportReqVO) { + return tenantMapper.selectList(exportReqVO); + } + + @Override + public TenantDO getTenantByName(String name) { + return tenantMapper.selectByName(name); + } + + @Override + public Long getTenantCountByPackageId(Long packageId) { + return tenantMapper.selectCountByPackageId(packageId); + } + + @Override + public List getTenantListByPackageId(Long packageId) { + return tenantMapper.selectListByPackageId(packageId); + } + + @Override + public void handleTenantInfo(TenantInfoHandler handler) { + // 如果禁用,则不执行逻辑 + if (isTenantDisable()) { + return; + } + // 获得租户 + TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); + // 执行处理器 + handler.handle(tenant); + } + + @Override + public void handleTenantMenu(TenantMenuHandler handler) { + // 如果禁用,则不执行逻辑 + if (isTenantDisable()) { + return; + } + // 获得租户,然后获得菜单 + TenantDO tenant = getTenant(TenantContextHolder.getRequiredTenantId()); + Set menuIds; + if (isSystemTenant(tenant)) { // 系统租户,菜单是全量的 + menuIds = CollectionUtils.convertSet(menuService.getMenuList(), MenuDO::getId); + } else { + menuIds = tenantPackageService.getTenantPackage(tenant.getPackageId()).getMenuIds(); + } + // 执行处理器 + handler.handle(menuIds); + } + + private static boolean isSystemTenant(TenantDO tenant) { + return Objects.equals(tenant.getPackageId(), TenantDO.PACKAGE_ID_SYSTEM); + } + + private boolean isTenantDisable() { + return tenantProperties == null || Boolean.FALSE.equals(tenantProperties.getEnable()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/handler/TenantInfoHandler.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/handler/TenantInfoHandler.java new file mode 100644 index 0000000..7214188 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/handler/TenantInfoHandler.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.service.tenant.handler; + +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; + +/** + * 租户信息处理 + * 目的:尽量减少租户逻辑耦合到系统中 + * + * @author 芋道源码 + */ +public interface TenantInfoHandler { + + /** + * 基于传入的租户信息,进行相关逻辑的执行 + * 例如说,创建用户时,超过最大账户配额 + * + * @param tenant 租户信息 + */ + void handle(TenantDO tenant); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/handler/TenantMenuHandler.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/handler/TenantMenuHandler.java new file mode 100644 index 0000000..a93ad6f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/tenant/handler/TenantMenuHandler.java @@ -0,0 +1,21 @@ +package com.yunxi.scm.module.system.service.tenant.handler; + +import java.util.Set; + +/** + * 租户菜单处理 + * 目的:尽量减少租户逻辑耦合到系统中 + * + * @author 芋道源码 + */ +public interface TenantMenuHandler { + + /** + * 基于传入的租户菜单【全】列表,进行相关逻辑的执行 + * 例如说,返回可分配菜单的时候,可以移除多余的 + * + * @param menuIds 菜单列表 + */ + void handle(Set menuIds); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/user/AdminUserService.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/user/AdminUserService.java new file mode 100644 index 0000000..dfc1894 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/user/AdminUserService.java @@ -0,0 +1,212 @@ +package com.yunxi.scm.module.system.service.user; + +import cn.hutool.core.collection.CollUtil; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.user.*; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; + +import javax.validation.Valid; +import java.io.InputStream; +import java.util.*; + +/** + * 后台用户 Service 接口 + * + * @author 芋道源码 + */ +public interface AdminUserService { + + /** + * 创建用户 + * + * @param reqVO 用户信息 + * @return 用户编号 + */ + Long createUser(@Valid UserCreateReqVO reqVO); + + /** + * 修改用户 + * + * @param reqVO 用户信息 + */ + void updateUser(@Valid UserUpdateReqVO reqVO); + + /** + * 更新用户的最后登陆信息 + * + * @param id 用户编号 + * @param loginIp 登陆 IP + */ + void updateUserLogin(Long id, String loginIp); + + /** + * 修改用户个人信息 + * + * @param id 用户编号 + * @param reqVO 用户个人信息 + */ + void updateUserProfile(Long id, @Valid UserProfileUpdateReqVO reqVO); + + /** + * 修改用户个人密码 + * + * @param id 用户编号 + * @param reqVO 更新用户个人密码 + */ + void updateUserPassword(Long id, @Valid UserProfileUpdatePasswordReqVO reqVO); + + /** + * 更新用户头像 + * + * @param id 用户 id + * @param avatarFile 头像文件 + */ + String updateUserAvatar(Long id, InputStream avatarFile) throws Exception; + + /** + * 修改密码 + * + * @param id 用户编号 + * @param password 密码 + */ + void updateUserPassword(Long id, String password); + + /** + * 修改状态 + * + * @param id 用户编号 + * @param status 状态 + */ + void updateUserStatus(Long id, Integer status); + + /** + * 删除用户 + * + * @param id 用户编号 + */ + void deleteUser(Long id); + + /** + * 通过用户名查询用户 + * + * @param username 用户名 + * @return 用户对象信息 + */ + AdminUserDO getUserByUsername(String username); + + /** + * 通过手机号获取用户 + * + * @param mobile 手机号 + * @return 用户对象信息 + */ + AdminUserDO getUserByMobile(String mobile); + + /** + * 获得用户分页列表 + * + * @param reqVO 分页条件 + * @return 分页列表 + */ + PageResult getUserPage(UserPageReqVO reqVO); + + /** + * 通过用户 ID 查询用户 + * + * @param id 用户ID + * @return 用户对象信息 + */ + AdminUserDO getUser(Long id); + + /** + * 获得指定部门的用户数组 + * + * @param deptIds 部门数组 + * @return 用户数组 + */ + List getUserListByDeptIds(Collection deptIds); + + /** + * 获得指定岗位的用户数组 + * + * @param postIds 岗位数组 + * @return 用户数组 + */ + List getUserListByPostIds(Collection postIds); + + /** + * 获得用户列表 + * + * @param ids 用户编号数组 + * @return 用户列表 + */ + List getUserList(Collection ids); + + /** + * 校验用户们是否有效。如下情况,视为无效: + * 1. 用户编号不存在 + * 2. 用户被禁用 + * + * @param ids 用户编号数组 + */ + void validateUserList(Collection ids); + + /** + * 获得用户 Map + * + * @param ids 用户编号数组 + * @return 用户 Map + */ + default Map getUserMap(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return new HashMap<>(); + } + return CollectionUtils.convertMap(getUserList(ids), AdminUserDO::getId); + } + + /** + * 获得用户列表 + * + * @param reqVO 列表请求 + * @return 用户列表 + */ + List getUserList(UserExportReqVO reqVO); + + /** + * 获得用户列表,基于昵称模糊匹配 + * + * @param nickname 昵称 + * @return 用户列表 + */ + List getUserListByNickname(String nickname); + + /** + * 批量导入用户 + * + * @param importUsers 导入用户列表 + * @param isUpdateSupport 是否支持更新 + * @return 导入结果 + */ + UserImportRespVO importUserList(List importUsers, boolean isUpdateSupport); + + /** + * 获得指定状态的用户们 + * + * @param status 状态 + * @return 用户们 + */ + List getUserListByStatus(Integer status); + + /** + * 判断密码是否匹配 + * + * @param rawPassword 未加密的密码 + * @param encodedPassword 加密后的密码 + * @return 是否匹配 + */ + boolean isPasswordMatch(String rawPassword, String encodedPassword); + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/user/AdminUserServiceImpl.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/user/AdminUserServiceImpl.java new file mode 100644 index 0000000..3735219 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/service/user/AdminUserServiceImpl.java @@ -0,0 +1,456 @@ +package com.yunxi.scm.module.system.service.user; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.datapermission.core.util.DataPermissionUtils; +import com.yunxi.scm.module.infra.api.file.FileApi; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.user.*; +import com.yunxi.scm.module.system.convert.user.UserConvert; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.dept.UserPostDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.dal.mysql.dept.UserPostMapper; +import com.yunxi.scm.module.system.dal.mysql.user.AdminUserMapper; +import com.yunxi.scm.module.system.service.dept.DeptService; +import com.yunxi.scm.module.system.service.dept.PostService; +import com.yunxi.scm.module.system.service.permission.PermissionService; +import com.yunxi.scm.module.system.service.tenant.TenantService; +import com.google.common.annotations.VisibleForTesting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import javax.annotation.Resource; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.util.*; + +import static com.yunxi.scm.framework.common.exception.util.ServiceExceptionUtil.exception; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertList; +import static com.yunxi.scm.framework.common.util.collection.CollectionUtils.convertSet; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; + +/** + * 后台用户 Service 实现类 + * + * @author 芋道源码 + */ +@Service("adminUserService") +@Slf4j +public class AdminUserServiceImpl implements AdminUserService { + + @Value("${sys.user.init-password:yunxiyuanma}") + private String userInitPassword; + + @Resource + private AdminUserMapper userMapper; + + @Resource + private DeptService deptService; + @Resource + private PostService postService; + @Resource + private PermissionService permissionService; + @Resource + private PasswordEncoder passwordEncoder; + @Resource + @Lazy // 延迟,避免循环依赖报错 + private TenantService tenantService; + + @Resource + private UserPostMapper userPostMapper; + + @Resource + private FileApi fileApi; + + @Override + @Transactional(rollbackFor = Exception.class) + public Long createUser(UserCreateReqVO reqVO) { + // 校验账户配合 + tenantService.handleTenantInfo(tenant -> { + long count = userMapper.selectCount(); + if (count >= tenant.getAccountCount()) { + throw exception(USER_COUNT_MAX, tenant.getAccountCount()); + } + }); + // 校验正确性 + validateUserForCreateOrUpdate(null, reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(), + reqVO.getDeptId(), reqVO.getPostIds()); + // 插入用户 + AdminUserDO user = UserConvert.INSTANCE.convert(reqVO); + user.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 默认开启 + user.setPassword(encodePassword(reqVO.getPassword())); // 加密密码 + userMapper.insert(user); + // 插入关联岗位 + if (CollectionUtil.isNotEmpty(user.getPostIds())) { + userPostMapper.insertBatch(convertList(user.getPostIds(), + postId -> new UserPostDO().setUserId(user.getId()).setPostId(postId))); + } + return user.getId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void updateUser(UserUpdateReqVO reqVO) { + // 校验正确性 + validateUserForCreateOrUpdate(reqVO.getId(), reqVO.getUsername(), reqVO.getMobile(), reqVO.getEmail(), + reqVO.getDeptId(), reqVO.getPostIds()); + // 更新用户 + AdminUserDO updateObj = UserConvert.INSTANCE.convert(reqVO); + userMapper.updateById(updateObj); + // 更新岗位 + updateUserPost(reqVO, updateObj); + } + + private void updateUserPost(UserUpdateReqVO reqVO, AdminUserDO updateObj) { + Long userId = reqVO.getId(); + Set dbPostIds = convertSet(userPostMapper.selectListByUserId(userId), UserPostDO::getPostId); + // 计算新增和删除的岗位编号 + Set postIds = updateObj.getPostIds(); + Collection createPostIds = CollUtil.subtract(postIds, dbPostIds); + Collection deletePostIds = CollUtil.subtract(dbPostIds, postIds); + // 执行新增和删除。对于已经授权的菜单,不用做任何处理 + if (!CollectionUtil.isEmpty(createPostIds)) { + userPostMapper.insertBatch(convertList(createPostIds, + postId -> new UserPostDO().setUserId(userId).setPostId(postId))); + } + if (!CollectionUtil.isEmpty(deletePostIds)) { + userPostMapper.deleteByUserIdAndPostId(userId, deletePostIds); + } + } + + @Override + public void updateUserLogin(Long id, String loginIp) { + userMapper.updateById(new AdminUserDO().setId(id).setLoginIp(loginIp).setLoginDate(LocalDateTime.now())); + } + + @Override + public void updateUserProfile(Long id, UserProfileUpdateReqVO reqVO) { + // 校验正确性 + validateUserExists(id); + validateEmailUnique(id, reqVO.getEmail()); + validateMobileUnique(id, reqVO.getMobile()); + // 执行更新 + userMapper.updateById(UserConvert.INSTANCE.convert(reqVO).setId(id)); + } + + @Override + public void updateUserPassword(Long id, UserProfileUpdatePasswordReqVO reqVO) { + // 校验旧密码密码 + validateOldPassword(id, reqVO.getOldPassword()); + // 执行更新 + AdminUserDO updateObj = new AdminUserDO().setId(id); + updateObj.setPassword(encodePassword(reqVO.getNewPassword())); // 加密密码 + userMapper.updateById(updateObj); + } + + @Override + public String updateUserAvatar(Long id, InputStream avatarFile) throws Exception { + validateUserExists(id); + // 存储文件 + String avatar = fileApi.createFile(IoUtil.readBytes(avatarFile)); + // 更新路径 + AdminUserDO sysUserDO = new AdminUserDO(); + sysUserDO.setId(id); + sysUserDO.setAvatar(avatar); + userMapper.updateById(sysUserDO); + return avatar; + } + + @Override + public void updateUserPassword(Long id, String password) { + // 校验用户存在 + validateUserExists(id); + // 更新密码 + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(id); + updateObj.setPassword(encodePassword(password)); // 加密密码 + userMapper.updateById(updateObj); + } + + @Override + public void updateUserStatus(Long id, Integer status) { + // 校验用户存在 + validateUserExists(id); + // 更新状态 + AdminUserDO updateObj = new AdminUserDO(); + updateObj.setId(id); + updateObj.setStatus(status); + userMapper.updateById(updateObj); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteUser(Long id) { + // 校验用户存在 + validateUserExists(id); + // 删除用户 + userMapper.deleteById(id); + // 删除用户关联数据 + permissionService.processUserDeleted(id); + // 删除用户岗位 + userPostMapper.deleteByUserId(id); + } + + @Override + public AdminUserDO getUserByUsername(String username) { + return userMapper.selectByUsername(username); + } + + @Override + public AdminUserDO getUserByMobile(String mobile) { + return userMapper.selectByMobile(mobile); + } + + @Override + public PageResult getUserPage(UserPageReqVO reqVO) { + return userMapper.selectPage(reqVO, getDeptCondition(reqVO.getDeptId())); + } + + @Override + public AdminUserDO getUser(Long id) { + return userMapper.selectById(id); + } + + @Override + public List getUserListByDeptIds(Collection deptIds) { + if (CollUtil.isEmpty(deptIds)) { + return Collections.emptyList(); + } + return userMapper.selectListByDeptIds(deptIds); + } + + @Override + public List getUserListByPostIds(Collection postIds) { + if (CollUtil.isEmpty(postIds)) { + return Collections.emptyList(); + } + Set userIds = convertSet(userPostMapper.selectListByPostIds(postIds), UserPostDO::getUserId); + if (CollUtil.isEmpty(userIds)) { + return Collections.emptyList(); + } + return userMapper.selectBatchIds(userIds); + } + + @Override + public List getUserList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return Collections.emptyList(); + } + return userMapper.selectBatchIds(ids); + } + + @Override + public void validateUserList(Collection ids) { + if (CollUtil.isEmpty(ids)) { + return; + } + // 获得岗位信息 + List users = userMapper.selectBatchIds(ids); + Map userMap = CollectionUtils.convertMap(users, AdminUserDO::getId); + // 校验 + ids.forEach(id -> { + AdminUserDO user = userMap.get(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + if (!CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus())) { + throw exception(USER_IS_DISABLE, user.getNickname()); + } + }); + } + + @Override + public List getUserList(UserExportReqVO reqVO) { + return userMapper.selectList(reqVO, getDeptCondition(reqVO.getDeptId())); + } + + @Override + public List getUserListByNickname(String nickname) { + return userMapper.selectListByNickname(nickname); + } + + /** + * 获得部门条件:查询指定部门的子部门编号们,包括自身 + * @param deptId 部门编号 + * @return 部门编号集合 + */ + private Set getDeptCondition(Long deptId) { + if (deptId == null) { + return Collections.emptySet(); + } + Set deptIds = convertSet(deptService.getChildDeptList(deptId), DeptDO::getId); + deptIds.add(deptId); // 包括自身 + return deptIds; + } + + private void validateUserForCreateOrUpdate(Long id, String username, String mobile, String email, + Long deptId, Set postIds) { + // 关闭数据权限,避免因为没有数据权限,查询不到数据,进而导致唯一校验不正确 + DataPermissionUtils.executeIgnore(() -> { + // 校验用户存在 + validateUserExists(id); + // 校验用户名唯一 + validateUsernameUnique(id, username); + // 校验手机号唯一 + validateMobileUnique(id, mobile); + // 校验邮箱唯一 + validateEmailUnique(id, email); + // 校验部门处于开启状态 + deptService.validateDeptList(CollectionUtils.singleton(deptId)); + // 校验岗位处于开启状态 + postService.validatePostList(postIds); + }); + } + + @VisibleForTesting + void validateUserExists(Long id) { + if (id == null) { + return; + } + AdminUserDO user = userMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + } + + @VisibleForTesting + void validateUsernameUnique(Long id, String username) { + if (StrUtil.isBlank(username)) { + return; + } + AdminUserDO user = userMapper.selectByUsername(username); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_USERNAME_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_USERNAME_EXISTS); + } + } + + @VisibleForTesting + void validateEmailUnique(Long id, String email) { + if (StrUtil.isBlank(email)) { + return; + } + AdminUserDO user = userMapper.selectByEmail(email); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_EMAIL_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_EMAIL_EXISTS); + } + } + + @VisibleForTesting + void validateMobileUnique(Long id, String mobile) { + if (StrUtil.isBlank(mobile)) { + return; + } + AdminUserDO user = userMapper.selectByMobile(mobile); + if (user == null) { + return; + } + // 如果 id 为空,说明不用比较是否为相同 id 的用户 + if (id == null) { + throw exception(USER_MOBILE_EXISTS); + } + if (!user.getId().equals(id)) { + throw exception(USER_MOBILE_EXISTS); + } + } + + /** + * 校验旧密码 + * @param id 用户 id + * @param oldPassword 旧密码 + */ + @VisibleForTesting + void validateOldPassword(Long id, String oldPassword) { + AdminUserDO user = userMapper.selectById(id); + if (user == null) { + throw exception(USER_NOT_EXISTS); + } + if (!isPasswordMatch(oldPassword, user.getPassword())) { + throw exception(USER_PASSWORD_FAILED); + } + } + + @Override + @Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入 + public UserImportRespVO importUserList(List importUsers, boolean isUpdateSupport) { + if (CollUtil.isEmpty(importUsers)) { + throw exception(USER_IMPORT_LIST_IS_EMPTY); + } + UserImportRespVO respVO = UserImportRespVO.builder().createUsernames(new ArrayList<>()) + .updateUsernames(new ArrayList<>()).failureUsernames(new LinkedHashMap<>()).build(); + importUsers.forEach(importUser -> { + // 校验,判断是否有不符合的原因 + try { + validateUserForCreateOrUpdate(null, null, importUser.getMobile(), importUser.getEmail(), + importUser.getDeptId(), null); + } catch (ServiceException ex) { + respVO.getFailureUsernames().put(importUser.getUsername(), ex.getMessage()); + return; + } + // 判断如果不存在,在进行插入 + AdminUserDO existUser = userMapper.selectByUsername(importUser.getUsername()); + if (existUser == null) { + userMapper.insert(UserConvert.INSTANCE.convert(importUser) + .setPassword(encodePassword(userInitPassword)).setPostIds(new HashSet<>())); // 设置默认密码及空岗位编号数组 + respVO.getCreateUsernames().add(importUser.getUsername()); + return; + } + // 如果存在,判断是否允许更新 + if (!isUpdateSupport) { + respVO.getFailureUsernames().put(importUser.getUsername(), USER_USERNAME_EXISTS.getMsg()); + return; + } + AdminUserDO updateUser = UserConvert.INSTANCE.convert(importUser); + updateUser.setId(existUser.getId()); + userMapper.updateById(updateUser); + respVO.getUpdateUsernames().add(importUser.getUsername()); + }); + return respVO; + } + + @Override + public List getUserListByStatus(Integer status) { + return userMapper.selectListByStatus(status); + } + + @Override + public boolean isPasswordMatch(String rawPassword, String encodedPassword) { + return passwordEncoder.matches(rawPassword, encodedPassword); + } + + /** + * 对密码进行加密 + * + * @param password 密码 + * @return 加密后的密码 + */ + private String encodePassword(String password) { + return passwordEncoder.encode(password); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/collection/SimpleTrie.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/collection/SimpleTrie.java new file mode 100644 index 0000000..80bd60a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/collection/SimpleTrie.java @@ -0,0 +1,145 @@ +package com.yunxi.scm.module.system.util.collection; + +import cn.hutool.core.collection.CollUtil; + +import java.util.*; + +/** + * 基于前缀树,实现敏感词的校验 + *

+ * 相比 Apache Common 提供的 PatriciaTrie 来说,性能可能会更加好一些。 + * + * @author 芋道源码 + */ +@SuppressWarnings("unchecked") +public class SimpleTrie { + + /** + * 一个敏感词结束后对应的 key + */ + private static final Character CHARACTER_END = '\0'; + + /** + * 使用敏感词,构建的前缀树 + */ + private final Map children; + + /** + * 基于字符串,构建前缀树 + * + * @param strs 字符串数组 + */ + public SimpleTrie(Collection strs) { + children = new HashMap<>(); + // 构建树 + CollUtil.sort(strs, String::compareTo); // 排序,优先使用较短的前缀 + for (String str : strs) { + Map child = children; + // 遍历每个字符 + for (Character c : str.toCharArray()) { + // 如果已经到达结束,就没必要在添加更长的敏感词。 + // 例如说,有两个敏感词是:吃饭啊、吃饭。输入一句话是 “我要吃饭啊”,则只要匹配到 “吃饭” 这个敏感词即可。 + if (child.containsKey(CHARACTER_END)) { + break; + } + if (!child.containsKey(c)) { + child.put(c, new HashMap<>()); + } + child = (Map) child.get(c); + } + // 结束 + child.put(CHARACTER_END, null); + } + } + + /** + * 验证文本是否合法,即不包含敏感词 + * + * @param text 文本 + * @return 是否 ok + */ + public boolean isValid(String text) { + // 遍历 text,使用每一个 [i, n) 段的字符串,使用 children 前缀树匹配,是否包含敏感词 + for (int i = 0; i < text.length() - 1; i++) { + Map child = (Map) children.get(text.charAt(i)); + if (child == null) { + continue; + } + boolean ok = recursion(text, i + 1, child); + if (!ok) { + return false; + } + } + return true; + } + + /** + * 验证文本从指定位置开始,是否包含某个敏感词 + * + * @param text 文本 + * @param index 开始位置 + * @param child 节点(当前遍历到的) + * @return 是否包含 + */ + private boolean recursion(String text, int index, Map child) { + if (index == text.length()) { + return true; + } + child = (Map) child.get(text.charAt(index)); + return child == null || !child.containsKey(CHARACTER_END) && recursion(text, ++index, child); + } + + /** + * 获得文本所包含的不合法的敏感词 + * + * 注意,才当即最短匹配原则。例如说:当敏感词存在 “煞笔”,“煞笔二货 ”时,只会返回 “煞笔”。 + * + * @param text 文本 + * @return 匹配的敏感词 + */ + public List validate(String text) { + Set results = new HashSet<>(); + for (int i = 0; i < text.length() - 1; i++) { + Character c = text.charAt(i); + Map child = (Map) children.get(c); + if (child == null) { + continue; + } + StringBuilder result = new StringBuilder().append(c); + boolean ok = recursionWithResult(text, i + 1, child, result); + if (!ok) { + results.add(result.toString()); + } + } + return new ArrayList<>(results); + } + + /** + * 返回文本从 index 开始的敏感词,并使用 StringBuilder 参数进行返回 + * + * 逻辑和 {@link #recursion(String, int, Map)} 是一致,只是多了 result 返回结果 + * + * @param text 文本 + * @param index 开始未知 + * @param child 节点(当前遍历到的) + * @param result 返回敏感词 + * @return 是否有敏感词 + */ + @SuppressWarnings("unchecked") + private static boolean recursionWithResult(String text, int index, Map child, StringBuilder result) { + if (index == text.length()) { + return true; + } + Character c = text.charAt(index); + child = (Map) child.get(c); + if (child == null) { + return true; + } + if (child.containsKey(CHARACTER_END)) { + result.append(c); + return false; + } + return recursionWithResult(text, ++index, child, result.append(c)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/oauth2/OAuth2Utils.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/oauth2/OAuth2Utils.java new file mode 100644 index 0000000..cb3ec39 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/oauth2/OAuth2Utils.java @@ -0,0 +1,103 @@ +package com.yunxi.scm.module.system.util.oauth2; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.http.HttpUtils; +import com.yunxi.scm.framework.security.core.util.SecurityFrameworkUtils; + +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.*; + +/** + * OAuth2 相关的工具类 + * + * @author 芋道源码 + */ +public class OAuth2Utils { + + /** + * 构建授权码模式下,重定向的 URI + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 getSuccessfulRedirect 方法 + * + * @param redirectUri 重定向 URI + * @param authorizationCode 授权码 + * @param state 状态 + * @return 授权码模式下的重定向 URI + */ + public static String buildAuthorizationCodeRedirectUri(String redirectUri, String authorizationCode, String state) { + Map query = new LinkedHashMap<>(); + query.put("code", authorizationCode); + if (state != null) { + query.put("state", state); + } + return HttpUtils.append(redirectUri, query, null, false); + } + + /** + * 构建简化模式下,重定向的 URI + * + * copy from Spring Security OAuth2 的 AuthorizationEndpoint 类的 appendAccessToken 方法 + * + * @param redirectUri 重定向 URI + * @param accessToken 访问令牌 + * @param state 状态 + * @param expireTime 过期时间 + * @param scopes 授权范围 + * @param additionalInformation 附加信息 + * @return 简化授权模式下的重定向 URI + */ + public static String buildImplicitRedirectUri(String redirectUri, String accessToken, String state, LocalDateTime expireTime, + Collection scopes, Map additionalInformation) { + Map vars = new LinkedHashMap(); + Map keys = new HashMap(); + vars.put("access_token", accessToken); + vars.put("token_type", SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); + if (state != null) { + vars.put("state", state); + } + if (expireTime != null) { + vars.put("expires_in", getExpiresIn(expireTime)); + } + if (CollUtil.isNotEmpty(scopes)) { + vars.put("scope", buildScopeStr(scopes)); + } + if (CollUtil.isNotEmpty(additionalInformation)) { + for (String key : additionalInformation.keySet()) { + Object value = additionalInformation.get(key); + if (value != null) { + keys.put("extra_" + key, key); + vars.put("extra_" + key, value); + } + } + } + // Do not include the refresh token (even if there is one) + return HttpUtils.append(redirectUri, vars, keys, true); + } + + public static String buildUnsuccessfulRedirect(String redirectUri, String responseType, String state, + String error, String description) { + Map query = new LinkedHashMap(); + query.put("error", error); + query.put("error_description", description); + if (state != null) { + query.put("state", state); + } + return HttpUtils.append(redirectUri, query, null, !responseType.contains("code")); + } + + public static long getExpiresIn(LocalDateTime expireTime) { + return LocalDateTimeUtil.between(LocalDateTime.now(), expireTime, ChronoUnit.SECONDS); + } + + public static String buildScopeStr(Collection scopes) { + return CollUtil.join(scopes, " "); + } + + public static List buildScopes(String scope) { + return StrUtil.split(scope, ' '); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/package-info.java new file mode 100644 index 0000000..1c0a36c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/main/java/com/yunxi/scm/module/system/util/package-info.java @@ -0,0 +1,4 @@ +/** + * 每个模块的 util 包,放专属当前模块的 Utils 工具类 + */ +package com.yunxi.scm.module.system.util; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/job/SchedulerManagerTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/job/SchedulerManagerTest.java new file mode 100644 index 0000000..e35d947 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/job/SchedulerManagerTest.java @@ -0,0 +1,53 @@ +package com.yunxi.scm.module.system.job; + +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.quartz.core.scheduler.SchedulerManager; +import com.yunxi.scm.module.system.job.auth.UserSessionTimeoutJob; +import com.yunxi.scm.module.system.test.BaseDbUnitTest; +import org.junit.jupiter.api.Test; +import org.quartz.SchedulerException; + +import javax.annotation.Resource; + +public class SchedulerManagerTest extends BaseDbUnitTest { + + @Resource + private SchedulerManager schedulerManager; + + @Test + public void testAddJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.addJob(1L, jobHandlerName, "test", "0/10 * * * * ? *", 0, 0); + } + + @Test + public void testUpdateJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.updateJob(jobHandlerName, "hahaha", "0/20 * * * * ? *", 0, 0); + } + + @Test + public void testDeleteJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.deleteJob(jobHandlerName); + } + + @Test + public void testPauseJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.pauseJob(jobHandlerName); + } + + @Test + public void testResumeJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.resumeJob(jobHandlerName); + } + + @Test + public void testTriggerJob() throws SchedulerException { + String jobHandlerName = StrUtil.lowerFirst(UserSessionTimeoutJob.class.getSimpleName()); + schedulerManager.triggerJob(1L, jobHandlerName, "niubi!!!"); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/mq/RedisStreamTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/mq/RedisStreamTest.java new file mode 100644 index 0000000..643b658 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/mq/RedisStreamTest.java @@ -0,0 +1,62 @@ +package com.yunxi.scm.module.system.mq; + +import cn.hutool.core.thread.ThreadUtil; +import com.yunxi.scm.framework.mq.core.RedisMQTemplate; +import com.yunxi.scm.module.system.mq.consumer.mail.MailSendConsumer; +import com.yunxi.scm.module.system.mq.consumer.sms.SmsSendConsumer; +import com.yunxi.scm.module.system.mq.message.mail.MailSendMessage; +import com.yunxi.scm.module.system.mq.message.sms.SmsSendMessage; +import com.yunxi.scm.module.system.test.BaseRedisIntegrationTest; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.core.RedisTemplate; + +import javax.annotation.Resource; +import java.util.concurrent.TimeUnit; + +public class RedisStreamTest { + + @Import({SmsSendConsumer.class, MailSendConsumer.class}) + @Disabled + public static class ConsumerTest extends BaseRedisIntegrationTest { + + @Test + public void testConsumer() { + ThreadUtil.sleep(1, TimeUnit.DAYS); + } + + } + + @Disabled + public static class ProducerTest extends BaseRedisIntegrationTest { + + @Resource + private RedisMQTemplate redisMQTemplate; + + @Resource + private RedisTemplate redisTemplate; + + @Test + public void testProducer01() { + for (int i = 0; i < 100; i++) { + // 创建消息 + SmsSendMessage message = new SmsSendMessage(); + message.setMobile("15601691300").setApiTemplateId("test:" + i); + // 发送消息 + redisMQTemplate.send(message); + } + } + + @Test + public void testProducer02() { + // 创建消息 + MailSendMessage message = new MailSendMessage(); + message.setAddress("fangfang@mihayou.com").setTemplateCode("test"); + // 发送消息 + redisMQTemplate.send(message); + } + + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/service/package-info.java b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/service/package-info.java new file mode 100644 index 0000000..c16040c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/service/package-info.java @@ -0,0 +1,4 @@ +/** + * 占位 + */ +package com.yunxi.scm.module.system.service; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/service/sms/SmsServiceIntegrationTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/service/sms/SmsServiceIntegrationTest.java new file mode 100644 index 0000000..6d893c0 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/service/sms/SmsServiceIntegrationTest.java @@ -0,0 +1,55 @@ +package com.yunxi.scm.module.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.thread.ThreadUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.sms.config.YunxiSmsAutoConfiguration; +import com.yunxi.scm.module.system.test.BaseDbAndRedisIntegrationTest; +import com.yunxi.scm.module.system.mq.consumer.sms.SmsSendConsumer; +import com.yunxi.scm.module.system.mq.producer.sms.SmsProducer; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +// TODO @芋艿:需要迁移 +@Import({YunxiSmsAutoConfiguration.class, + SmsChannelServiceImpl.class, SmsSendServiceImpl.class, SmsTemplateServiceImpl.class, SmsLogServiceImpl.class, + SmsProducer.class, SmsSendConsumer.class}) +public class SmsServiceIntegrationTest extends BaseDbAndRedisIntegrationTest { + + @Resource + private SmsSendServiceImpl smsService; + @Resource + private SmsChannelServiceImpl smsChannelService; + + @MockBean + private AdminUserService userService; + + @Test + public void testSendSingleSms_aliyunSuccess() { + // 参数准备 + String mobile = "15601691399"; + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + String templateCode = "test_02"; + Map templateParams = MapUtil.builder() + .put("code", "1234").build(); + // 调用 + smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + + // 等待 MQ 消费 + ThreadUtil.sleep(1, TimeUnit.HOURS); + } + +// @Test +// public void testDoSendSms() { +// // 等待 MQ 消费 +// ThreadUtil.sleep(1, TimeUnit.HOURS); +// } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/test/BaseDbAndRedisIntegrationTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/test/BaseDbAndRedisIntegrationTest.java new file mode 100644 index 0000000..27e7bb2 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/test/BaseDbAndRedisIntegrationTest.java @@ -0,0 +1,38 @@ +package com.yunxi.scm.module.system.test; + +import com.yunxi.scm.framework.datasource.config.YunxiDataSourceAutoConfiguration; +import com.yunxi.scm.framework.mybatis.config.YunxiMybatisAutoConfiguration; +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration; +import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseDbAndRedisIntegrationTest { + + @Import({ + // DB 配置类 + DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类 + YunxiDataSourceAutoConfiguration.class, // 自己的 DB 配置类 + DataSourceAutoConfiguration.class, // Spring DB 自动配置类 + DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类 + // MyBatis 配置类 + YunxiMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 + MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 + + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YunxiRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/test/BaseRedisIntegrationTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/test/BaseRedisIntegrationTest.java new file mode 100644 index 0000000..07c6126 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/java/com/yunxi/scm/module/system/test/BaseRedisIntegrationTest.java @@ -0,0 +1,23 @@ +package com.yunxi.scm.module.system.test; + +import com.yunxi.scm.framework.redis.config.YunxiRedisAutoConfiguration; +import org.redisson.spring.starter.RedissonAutoConfiguration; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class) +@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件 +public class BaseRedisIntegrationTest { + + @Import({ + // Redis 配置类 + RedisAutoConfiguration.class, // Spring Redis 自动配置类 + YunxiRedisAutoConfiguration.class, // 自己的 Redis 配置类 + RedissonAutoConfiguration.class, // Redisson 自动高配置类 + }) + public static class Application { + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test-integration/resources/application-integration-test.yaml b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/resources/application-integration-test.yaml new file mode 100644 index 0000000..cb99c69 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test-integration/resources/application-integration-test.yaml @@ -0,0 +1,108 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 + slave: # 模拟从库,可根据自己需要修改 + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT + driver-class-name: com.mysql.jdbc.Driver + username: root + password: 123456 + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + security: + token-header: Authorization + token-secret: abcdefghijklmnopqrstuvwxyz + token-timeout: 1d + session-timeout: 30m + mock-enable: true + mock-secret: test + swagger: + enable: false # 单元测试,禁用 Swagger + file: + base-path: http://127.0.0.1:${server.port}/${yunxi.web.api-prefix}/file/get/ + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java new file mode 100644 index 0000000..2d93551 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/controller/admin/oauth2/OAuth2OpenControllerTest.java @@ -0,0 +1,337 @@ +package com.yunxi.scm.module.system.controller.admin.oauth2; + +import cn.hutool.core.collection.ListUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAccessTokenRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenAuthorizeInfoRespVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.open.OAuth2OpenCheckTokenRespVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.enums.oauth2.OAuth2GrantTypeEnum; +import com.yunxi.scm.module.system.service.oauth2.OAuth2ApproveService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2ClientService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2GrantService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2TokenService; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import javax.servlet.http.HttpServletRequest; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static java.util.Arrays.asList; +import static org.hamcrest.CoreMatchers.anyOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * {@link OAuth2OpenController} 的单元测试 + * + * @author 芋道源码 + */ +public class OAuth2OpenControllerTest extends BaseMockitoUnitTest { + + @InjectMocks + private OAuth2OpenController oauth2OpenController; + + @Mock + private OAuth2GrantService oauth2GrantService; + @Mock + private OAuth2ClientService oauth2ClientService; + @Mock + private OAuth2ApproveService oauth2ApproveService; + @Mock + private OAuth2TokenService oauth2TokenService; + + @Test + public void testPostAccessToken_authorizationCode() { + // 准备参数 + String granType = OAuth2GrantTypeEnum.AUTHORIZATION_CODE.getGrantType(); + String code = randomString(); + String redirectUri = randomString(); + String state = randomString(); + HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); + when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), eq(granType), eq(new ArrayList<>()), eq(redirectUri))).thenReturn(client); + + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS)); + when(oauth2GrantService.grantAuthorizationCodeForAccessToken(eq("test_client_id"), + eq(code), eq(redirectUri), eq(state))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.postAccessToken(request, granType, + code, redirectUri, state, null, null, null, null); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(accessTokenDO, result.getData()); + assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L)); // 执行过程会过去几毫秒 + } + + @Test + public void testPostAccessToken_password() { + // 准备参数 + String granType = OAuth2GrantTypeEnum.PASSWORD.getGrantType(); + String username = randomString(); + String password = randomString(); + String scope = "write read"; + HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); + when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), + eq(granType), eq(Lists.newArrayList("write", "read")), isNull())).thenReturn(client); + + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS)); + when(oauth2GrantService.grantPassword(eq(username), eq(password), eq("test_client_id"), + eq(Lists.newArrayList("write", "read")))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.postAccessToken(request, granType, + null, null, null, username, password, scope, null); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(accessTokenDO, result.getData()); + assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L)); // 执行过程会过去几毫秒 + } + + @Test + public void testPostAccessToken_refreshToken() { + // 准备参数 + String granType = OAuth2GrantTypeEnum.REFRESH_TOKEN.getGrantType(); + String refreshToken = randomString(); + String password = randomString(); + HttpServletRequest request = mockRequest("test_client_id", "test_client_secret"); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("test_client_id"); + when(oauth2ClientService.validOAuthClientFromCache(eq("test_client_id"), eq("test_client_secret"), + eq(granType), eq(Lists.newArrayList()), isNull())).thenReturn(client); + + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30000L, ChronoUnit.MILLIS)); + when(oauth2GrantService.grantRefreshToken(eq(refreshToken), eq("test_client_id"))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.postAccessToken(request, granType, + null, null, null, null, password, null, refreshToken); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(accessTokenDO, result.getData()); + assertTrue(ObjectUtils.equalsAny(result.getData().getExpiresIn(), 29L, 30L)); // 执行过程会过去几毫秒 + } + + @Test + public void testPostAccessToken_implicit() { + // 调用,并断言 + assertServiceException(() -> oauth2OpenController.postAccessToken(null, + OAuth2GrantTypeEnum.IMPLICIT.getGrantType(), null, null, null, + null, null, null, null), + new ErrorCode(400, "Token 接口不支持 implicit 授权模式")); + } + + @Test + public void testRevokeToken() { + // 准备参数 + HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret"); + String token = randomString(); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id"); + when(oauth2ClientService.validOAuthClientFromCache(eq("demo_client_id"), + eq("demo_client_secret"), isNull(), isNull(), isNull())).thenReturn(client); + // mock 方法(移除) + when(oauth2GrantService.revokeToken(eq("demo_client_id"), eq(token))).thenReturn(true); + + // 调用 + CommonResult result = oauth2OpenController.revokeToken(request, token); + // 断言 + assertEquals(0, result.getCode()); + assertTrue(result.getData()); + } + + @Test + public void testCheckToken() { + // 准备参数 + HttpServletRequest request = mockRequest("demo_client_id", "demo_client_secret"); + String token = randomString(); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setUserType(UserTypeEnum.ADMIN.getValue()).setExpiresTime(LocalDateTimeUtil.of(1653485731195L)); + when(oauth2TokenService.checkAccessToken(eq(token))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.checkToken(request, token); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(accessTokenDO, result.getData()); + assertEquals(1653485731L, result.getData().getExp()); // 执行过程会过去几毫秒 + } + + @Test + public void testAuthorize() { + // 准备参数 + String clientId = randomString(); + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("demo_client_id").setScopes(ListUtil.toList("read", "write", "all")); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(client); + // mock 方法(approve) + List approves = asList( + randomPojo(OAuth2ApproveDO.class).setScope("read").setApproved(true), + randomPojo(OAuth2ApproveDO.class).setScope("write").setApproved(false)); + when(oauth2ApproveService.getApproveList(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId))).thenReturn(approves); + + // 调用 + CommonResult result = oauth2OpenController.authorize(clientId); + // 断言 + assertEquals(0, result.getCode()); + assertPojoEquals(client, result.getData().getClient()); + assertEquals(new KeyValue<>("read", true), result.getData().getScopes().get(0)); + assertEquals(new KeyValue<>("write", false), result.getData().getScopes().get(1)); + assertEquals(new KeyValue<>("all", false), result.getData().getScopes().get(2)); + } + + @Test + public void testApproveOrDeny_grantTypeError() { + // 调用,并断言 + assertServiceException(() -> oauth2OpenController.approveOrDeny(randomString(), null, + null, null, null, null), + new ErrorCode(400, "response_type 参数值只允许 code 和 token")); + } + + @Test // autoApprove = true,但是不通过 + public void testApproveOrDeny_autoApproveNo() { + // 准备参数 + String responseType = "code"; + String clientId = randomString(); + String scope = "{\"read\": true, \"write\": false}"; + String redirectUri = randomString(); + String state = randomString(); + // mock 方法 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"), + eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); + + // 调用 + CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, + scope, redirectUri, true, state); + // 断言 + assertEquals(0, result.getCode()); + assertNull(result.getData()); + } + + @Test // autoApprove = false,但是不通过 + public void testApproveOrDeny_ApproveNo() { + // 准备参数 + String responseType = "token"; + String clientId = randomString(); + String scope = "{\"read\": true, \"write\": false}"; + String redirectUri = "https://www.iocoder.cn"; + String state = "test"; + // mock 方法 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"), + eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); + + // 调用 + CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, + scope, redirectUri, false, state); + // 断言 + assertEquals(0, result.getCode()); + assertEquals("https://www.iocoder.cn#error=access_denied&error_description=User%20denied%20access&state=test", result.getData()); + } + + @Test // autoApprove = true,通过 + token + public void testApproveOrDeny_autoApproveWithToken() { + // 准备参数 + String responseType = "token"; + String clientId = randomString(); + String scope = "{\"read\": true, \"write\": false}"; + String redirectUri = "https://www.iocoder.cn"; + String state = "test"; + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("implicit"), + eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); + // mock 方法(场景一) + when(oauth2ApproveService.checkForPreApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), + eq(clientId), eq(SetUtils.asSet("read", "write")))).thenReturn(true); + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setAccessToken("test_access_token").setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 30010L, ChronoUnit.MILLIS)); + when(oauth2GrantService.grantImplicit(isNull(), eq(UserTypeEnum.ADMIN.getValue()), + eq(clientId), eq(ListUtil.toList("read")))).thenReturn(accessTokenDO); + + // 调用 + CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, + scope, redirectUri, true, state); + // 断言 + assertEquals(0, result.getCode()); + assertThat(result.getData(), anyOf( // 29 和 30 都有一定概率,主要是时间计算 + is("https://www.iocoder.cn#access_token=test_access_token&token_type=bearer&state=test&expires_in=29&scope=read"), + is("https://www.iocoder.cn#access_token=test_access_token&token_type=bearer&state=test&expires_in=30&scope=read") + )); + } + + @Test // autoApprove = false,通过 + code + public void testApproveOrDeny_approveWithCode() { + // 准备参数 + String responseType = "code"; + String clientId = randomString(); + String scope = "{\"read\": true, \"write\": false}"; + String redirectUri = "https://www.iocoder.cn"; + String state = "test"; + // mock 方法(client) + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId(clientId).setAdditionalInformation(null); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId), isNull(), eq("authorization_code"), + eq(asSet("read", "write")), eq(redirectUri))).thenReturn(client); + // mock 方法(场景二) + when(oauth2ApproveService.updateAfterApproval(isNull(), eq(UserTypeEnum.ADMIN.getValue()), eq(clientId), + eq(MapUtil.builder(new LinkedHashMap()).put("read", true).put("write", false).build()))) + .thenReturn(true); + // mock 方法(访问令牌) + String authorizationCode = "test_code"; + when(oauth2GrantService.grantAuthorizationCodeForCode(isNull(), eq(UserTypeEnum.ADMIN.getValue()), + eq(clientId), eq(ListUtil.toList("read")), eq(redirectUri), eq(state))).thenReturn(authorizationCode); + + // 调用 + CommonResult result = oauth2OpenController.approveOrDeny(responseType, clientId, + scope, redirectUri, false, state); + // 断言 + assertEquals(0, result.getCode()); + assertEquals("https://www.iocoder.cn?code=test_code&state=test", result.getData()); + } + + private HttpServletRequest mockRequest(String clientId, String secret) { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getParameter(eq("client_id"))).thenReturn(clientId); + when(request.getParameter(eq("client_secret"))).thenReturn(secret); + return request; + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/auth/AdminAuthServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/auth/AdminAuthServiceImplTest.java new file mode 100644 index 0000000..d8199c9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/auth/AdminAuthServiceImplTest.java @@ -0,0 +1,371 @@ +package com.yunxi.scm.module.system.service.auth; + +import cn.hutool.core.util.ReflectUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.api.sms.SmsCodeApi; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.controller.admin.auth.vo.*; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.enums.logger.LoginLogTypeEnum; +import com.yunxi.scm.module.system.enums.logger.LoginResultEnum; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import com.yunxi.scm.module.system.service.logger.LoginLogService; +import com.yunxi.scm.module.system.service.member.MemberService; +import com.yunxi.scm.module.system.service.oauth2.OAuth2TokenService; +import com.yunxi.scm.module.system.service.social.SocialUserService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import com.xingyuv.captcha.model.common.ResponseModel; +import com.xingyuv.captcha.service.CaptchaService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import javax.validation.ConstraintViolationException; +import javax.validation.Validation; +import javax.validation.Validator; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import(AdminAuthServiceImpl.class) +public class AdminAuthServiceImplTest extends BaseDbUnitTest { + + @Resource + private AdminAuthServiceImpl authService; + + @MockBean + private AdminUserService userService; + @MockBean + private CaptchaService captchaService; + @MockBean + private LoginLogService loginLogService; + @MockBean + private SocialUserService socialUserService; + @MockBean + private SmsCodeApi smsCodeApi; + @MockBean + private OAuth2TokenService oauth2TokenService; + @MockBean + private MemberService memberService; + @MockBean + private Validator validator; + + @BeforeEach + public void setUp() { + ReflectUtil.setFieldValue(authService, "captchaEnable", true); + // 注入一个 Validator 对象 + ReflectUtil.setFieldValue(authService, "validator", + Validation.buildDefaultValidatorFactory().getValidator()); + } + + @Test + public void testAuthenticate_success() { + // 准备参数 + String username = randomString(); + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.getUserByUsername(eq(username))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); + + // 调用 + AdminUserDO loginUser = authService.authenticate(username, password); + // 校验 + assertPojoEquals(user, loginUser); + } + + @Test + public void testAuthenticate_userNotFound() { + // 准备参数 + String username = randomString(); + String password = randomString(); + + // 调用, 并断言异常 + assertServiceException(() -> authService.authenticate(username, password), + AUTH_LOGIN_BAD_CREDENTIALS); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()) + && o.getUserId() == null) + ); + } + + @Test + public void testAuthenticate_badCredentials() { + // 准备参数 + String username = randomString(); + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.getUserByUsername(eq(username))).thenReturn(user); + + // 调用, 并断言异常 + assertServiceException(() -> authService.authenticate(username, password), + AUTH_LOGIN_BAD_CREDENTIALS); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.BAD_CREDENTIALS.getResult()) + && o.getUserId().equals(user.getId())) + ); + } + + @Test + public void testAuthenticate_userDisabled() { + // 准备参数 + String username = randomString(); + String password = randomString(); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setUsername(username) + .setPassword(password).setStatus(CommonStatusEnum.DISABLE.getStatus())); + when(userService.getUserByUsername(eq(username))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); + + // 调用, 并断言异常 + assertServiceException(() -> authService.authenticate(username, password), + AUTH_LOGIN_USER_DISABLED); + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.USER_DISABLED.getResult()) + && o.getUserId().equals(user.getId())) + ); + } + + @Test + public void testLogin_success() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class, o -> + o.setUsername("test_username").setPassword("test_password") + .setSocialType(randomEle(SocialTypeEnum.values()).getType())); + + // mock 验证码正确 + ReflectUtil.setFieldValue(authService, "captchaEnable", false); + // mock user 数据 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L).setUsername("test_username") + .setPassword("test_password").setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(userService.getUserByUsername(eq("test_username"))).thenReturn(user); + // mock password 匹配 + when(userService.isPasswordMatch(eq("test_password"), eq(user.getPassword()))).thenReturn(true); + // mock 缓存登录用户到 Redis + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) + .thenReturn(accessTokenDO); + + // 调用,并校验 + AuthLoginRespVO loginRespVO = authService.login(reqVO); + assertPojoEquals(accessTokenDO, loginRespVO); + // 校验调用参数 + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) + && o.getUserId().equals(user.getId())) + ); + verify(socialUserService).bindSocialUser(eq(new SocialUserBindReqDTO( + user.getId(), UserTypeEnum.ADMIN.getValue(), + reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()))); + } + + @Test + public void testSendSmsCode() { + // 准备参数 + String mobile = randomString(); + Integer scene = randomEle(SmsSceneEnum.values()).getScene(); + AuthSmsSendReqVO reqVO = new AuthSmsSendReqVO(mobile, scene); + // mock 方法(用户信息) + AdminUserDO user = randomPojo(AdminUserDO.class); + when(userService.getUserByMobile(eq(mobile))).thenReturn(user); + + // 调用 + authService.sendSmsCode(reqVO); + // 断言 + verify(smsCodeApi).sendSmsCode(argThat(sendReqDTO -> { + assertEquals(mobile, sendReqDTO.getMobile()); + assertEquals(scene, sendReqDTO.getScene()); + return true; + })); + } + + @Test + public void testSmsLogin_success() { + // 准备参数 + String mobile = randomString(); + String scene = randomString(); + AuthSmsLoginReqVO reqVO = new AuthSmsLoginReqVO(mobile, scene); + // mock 方法(用户信息) + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(1L)); + when(userService.getUserByMobile(eq(mobile))).thenReturn(user); + // mock 缓存登录用户到 Redis + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) + .thenReturn(accessTokenDO); + + // 调用,并断言 + AuthLoginRespVO loginRespVO = authService.smsLogin(reqVO); + assertPojoEquals(accessTokenDO, loginRespVO); + // 断言调用 + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_MOBILE.getType()) + && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) + && o.getUserId().equals(user.getId())) + ); + } + + @Test + public void testSocialLogin_success() { + // 准备参数 + AuthSocialLoginReqVO reqVO = randomPojo(AuthSocialLoginReqVO.class); + // mock 方法(绑定的用户编号) + Long userId = 1L; + when(socialUserService.getBindUserId(eq(UserTypeEnum.ADMIN.getValue()), eq(reqVO.getType()), + eq(reqVO.getCode()), eq(reqVO.getState()))).thenReturn(userId); + // mock(用户) + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setId(userId)); + when(userService.getUser(eq(userId))).thenReturn(user); + // mock 缓存登录用户到 Redis + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.createAccessToken(eq(1L), eq(UserTypeEnum.ADMIN.getValue()), eq("default"), isNull())) + .thenReturn(accessTokenDO); + + // 调用,并断言 + AuthLoginRespVO loginRespVO = authService.socialLogin(reqVO); + assertPojoEquals(accessTokenDO, loginRespVO); + // 断言调用 + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_SOCIAL.getType()) + && o.getResult().equals(LoginResultEnum.SUCCESS.getResult()) + && o.getUserId().equals(user.getId())) + ); + } + + @Test + public void testValidateCaptcha_successWithEnable() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); + + // mock 验证码打开 + ReflectUtil.setFieldValue(authService, "captchaEnable", true); + // mock 验证通过 + when(captchaService.verification(argThat(captchaVO -> { + assertEquals(reqVO.getCaptchaVerification(), captchaVO.getCaptchaVerification()); + return true; + }))).thenReturn(ResponseModel.success()); + + // 调用,无需断言 + authService.validateCaptcha(reqVO); + } + + @Test + public void testValidateCaptcha_successWithDisable() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); + + // mock 验证码关闭 + ReflectUtil.setFieldValue(authService, "captchaEnable", false); + + // 调用,无需断言 + authService.validateCaptcha(reqVO); + } + + @Test + public void testValidateCaptcha_constraintViolationException() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class).setCaptchaVerification(null); + + // mock 验证码打开 + ReflectUtil.setFieldValue(authService, "captchaEnable", true); + + // 调用,并断言异常 + assertThrows(ConstraintViolationException.class, () -> authService.validateCaptcha(reqVO), + "验证码不能为空"); + } + + + @Test + public void testCaptcha_fail() { + // 准备参数 + AuthLoginReqVO reqVO = randomPojo(AuthLoginReqVO.class); + + // mock 验证码打开 + ReflectUtil.setFieldValue(authService, "captchaEnable", true); + // mock 验证通过 + when(captchaService.verification(argThat(captchaVO -> { + assertEquals(reqVO.getCaptchaVerification(), captchaVO.getCaptchaVerification()); + return true; + }))).thenReturn(ResponseModel.errorMsg("就是不对")); + + // 调用, 并断言异常 + assertServiceException(() -> authService.validateCaptcha(reqVO), AUTH_LOGIN_CAPTCHA_CODE_ERROR, "就是不对"); + // 校验调用参数 + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) + && o.getResult().equals(LoginResultEnum.CAPTCHA_CODE_ERROR.getResult())) + ); + } + + @Test + public void testRefreshToken() { + // 准备参数 + String refreshToken = randomString(); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.refreshAccessToken(eq(refreshToken), eq("default"))) + .thenReturn(accessTokenDO); + + // 调用 + AuthLoginRespVO loginRespVO = authService.refreshToken(refreshToken); + // 断言 + assertPojoEquals(accessTokenDO, loginRespVO); + } + + @Test + public void testLogout_success() { + // 准备参数 + String token = randomString(); + // mock + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class, o -> o.setUserId(1L) + .setUserType(UserTypeEnum.ADMIN.getValue())); + when(oauth2TokenService.removeAccessToken(eq(token))).thenReturn(accessTokenDO); + + // 调用 + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + // 校验调用参数 + verify(loginLogService).createLoginLog( + argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGOUT_SELF.getType()) + && o.getResult().equals(LoginResultEnum.SUCCESS.getResult())) + ); + // 调用,并校验 + + } + + @Test + public void testLogout_fail() { + // 准备参数 + String token = randomString(); + + // 调用 + authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); + // 校验调用参数 + verify(loginLogService, never()).createLoginLog(any()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dept/DeptServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dept/DeptServiceImplTest.java new file mode 100644 index 0000000..8d22c8a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dept/DeptServiceImplTest.java @@ -0,0 +1,297 @@ +package com.yunxi.scm.module.system.service.dept; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptListReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.dept.DeptUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.mysql.dept.DeptMapper; +import com.yunxi.scm.module.system.enums.dept.DeptIdEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link DeptServiceImpl} 的单元测试类 + * + * @author niudehua + */ +@Import(DeptServiceImpl.class) +public class DeptServiceImplTest extends BaseDbUnitTest { + + @Resource + private DeptServiceImpl deptService; + @Resource + private DeptMapper deptMapper; + + @Test + public void testCreateDept() { + // 准备参数 + DeptCreateReqVO reqVO = randomPojo(DeptCreateReqVO.class, o -> { + o.setParentId(DeptIdEnum.ROOT.getId()); + o.setStatus(randomCommonStatus()); + }); + + // 调用 + Long deptId = deptService.createDept(reqVO); + // 断言 + assertNotNull(deptId); + // 校验记录的属性是否正确 + DeptDO deptDO = deptMapper.selectById(deptId); + assertPojoEquals(reqVO, deptDO); + } + + @Test + public void testUpdateDept() { + // mock 数据 + DeptDO dbDeptDO = randomPojo(DeptDO.class, o -> o.setStatus(randomCommonStatus())); + deptMapper.insert(dbDeptDO);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DeptUpdateReqVO reqVO = randomPojo(DeptUpdateReqVO.class, o -> { + // 设置更新的 ID + o.setParentId(DeptIdEnum.ROOT.getId()); + o.setId(dbDeptDO.getId()); + o.setStatus(randomCommonStatus()); + }); + + // 调用 + deptService.updateDept(reqVO); + // 校验是否更新正确 + DeptDO deptDO = deptMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, deptDO); + } + + @Test + public void testDeleteDept_success() { + // mock 数据 + DeptDO dbDeptDO = randomPojo(DeptDO.class); + deptMapper.insert(dbDeptDO);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDeptDO.getId(); + + // 调用 + deptService.deleteDept(id); + // 校验数据不存在了 + assertNull(deptMapper.selectById(id)); + } + + @Test + public void testDeleteDept_exitsChildren() { + // mock 数据 + DeptDO parentDept = randomPojo(DeptDO.class); + deptMapper.insert(parentDept);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DeptDO childrenDeptDO = randomPojo(DeptDO.class, o -> { + o.setParentId(parentDept.getId()); + o.setStatus(randomCommonStatus()); + }); + // 插入子部门 + deptMapper.insert(childrenDeptDO); + + // 调用, 并断言异常 + assertServiceException(() -> deptService.deleteDept(parentDept.getId()), DEPT_EXITS_CHILDREN); + } + + @Test + public void testValidateDeptExists_notFound() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> deptService.validateDeptExists(id), DEPT_NOT_FOUND); + } + + @Test + public void testValidateParentDept_parentError() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> deptService.validateParentDept(id, id), + DEPT_PARENT_ERROR); + } + + @Test + public void testValidateParentDept_parentIsChild() { + // mock 数据(父节点) + DeptDO parentDept = randomPojo(DeptDO.class); + deptMapper.insert(parentDept); + // mock 数据(子节点) + DeptDO childDept = randomPojo(DeptDO.class, o -> { + o.setParentId(parentDept.getId()); + }); + deptMapper.insert(childDept); + + // 准备参数 + Long id = parentDept.getId(); + Long parentId = childDept.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> deptService.validateParentDept(id, parentId), DEPT_PARENT_IS_CHILD); + } + + @Test + public void testValidateNameUnique_duplicate() { + // mock 数据 + DeptDO deptDO = randomPojo(DeptDO.class); + deptMapper.insert(deptDO); + + // 准备参数 + Long id = randomLongId(); + Long parentId = deptDO.getParentId(); + String name = deptDO.getName(); + + // 调用, 并断言异常 + assertServiceException(() -> deptService.validateDeptNameUnique(id, parentId, name), + DEPT_NAME_DUPLICATE); + } + + @Test + public void testGetDept() { + // mock 数据 + DeptDO deptDO = randomPojo(DeptDO.class); + deptMapper.insert(deptDO); + // 准备参数 + Long id = deptDO.getId(); + + // 调用 + DeptDO dbDept = deptService.getDept(id); + // 断言 + assertEquals(deptDO, dbDept); + } + + @Test + public void testGetDeptList_ids() { + // mock 数据 + DeptDO deptDO01 = randomPojo(DeptDO.class); + deptMapper.insert(deptDO01); + DeptDO deptDO02 = randomPojo(DeptDO.class); + deptMapper.insert(deptDO02); + // 准备参数 + List ids = Arrays.asList(deptDO01.getId(), deptDO02.getId()); + + // 调用 + List deptDOList = deptService.getDeptList(ids); + // 断言 + assertEquals(2, deptDOList.size()); + assertEquals(deptDO01, deptDOList.get(0)); + assertEquals(deptDO02, deptDOList.get(1)); + } + + @Test + public void testGetDeptList_reqVO() { + // mock 数据 + DeptDO dept = randomPojo(DeptDO.class, o -> { // 等会查询到 + o.setName("开发部"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + deptMapper.insert(dept); + // 测试 name 不匹配 + deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> o.setName("发"))); + // 测试 status 不匹配 + deptMapper.insert(ObjectUtils.cloneIgnoreId(dept, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + DeptListReqVO reqVO = new DeptListReqVO(); + reqVO.setName("开"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List sysDeptDOS = deptService.getDeptList(reqVO); + // 断言 + assertEquals(1, sysDeptDOS.size()); + assertPojoEquals(dept, sysDeptDOS.get(0)); + } + + @Test + public void testGetChildDeptList() { + // mock 数据(1 级别子节点) + DeptDO dept1 = randomPojo(DeptDO.class, o -> o.setName("1")); + deptMapper.insert(dept1); + DeptDO dept2 = randomPojo(DeptDO.class, o -> o.setName("2")); + deptMapper.insert(dept2); + // mock 数据(2 级子节点) + DeptDO dept1a = randomPojo(DeptDO.class, o -> o.setName("1-a").setParentId(dept1.getId())); + deptMapper.insert(dept1a); + DeptDO dept2a = randomPojo(DeptDO.class, o -> o.setName("2-a").setParentId(dept2.getId())); + deptMapper.insert(dept2a); + // 准备参数 + Long id = dept1.getParentId(); + + // 调用 + List result = deptService.getChildDeptList(id); + // 断言 + assertEquals(result.size(), 2); + assertPojoEquals(dept1, result.get(0)); + assertPojoEquals(dept1a, result.get(1)); + } + + @Test + public void testGetChildDeptListFromCache() { + // mock 数据(1 级别子节点) + DeptDO dept1 = randomPojo(DeptDO.class, o -> o.setName("1")); + deptMapper.insert(dept1); + DeptDO dept2 = randomPojo(DeptDO.class, o -> o.setName("2")); + deptMapper.insert(dept2); + // mock 数据(2 级子节点) + DeptDO dept1a = randomPojo(DeptDO.class, o -> o.setName("1-a").setParentId(dept1.getId())); + deptMapper.insert(dept1a); + DeptDO dept2a = randomPojo(DeptDO.class, o -> o.setName("2-a").setParentId(dept2.getId())); + deptMapper.insert(dept2a); + // 准备参数 + Long id = dept1.getParentId(); + + // 调用 + Set result = deptService.getChildDeptIdListFromCache(id); + // 断言 + assertEquals(result.size(), 2); + assertTrue(result.contains(dept1.getId())); + assertTrue(result.contains(dept1a.getId())); + } + + @Test + public void testValidateDeptList_success() { + // mock 数据 + DeptDO deptDO = randomPojo(DeptDO.class).setStatus(CommonStatusEnum.ENABLE.getStatus()); + deptMapper.insert(deptDO); + // 准备参数 + List ids = singletonList(deptDO.getId()); + + // 调用,无需断言 + deptService.validateDeptList(ids); + } + + @Test + public void testValidateDeptList_notFound() { + // 准备参数 + List ids = singletonList(randomLongId()); + + // 调用, 并断言异常 + assertServiceException(() -> deptService.validateDeptList(ids), DEPT_NOT_FOUND); + } + + @Test + public void testValidateDeptList_notEnable() { + // mock 数据 + DeptDO deptDO = randomPojo(DeptDO.class).setStatus(CommonStatusEnum.DISABLE.getStatus()); + deptMapper.insert(deptDO); + // 准备参数 + List ids = singletonList(deptDO.getId()); + + // 调用, 并断言异常 + assertServiceException(() -> deptService.validateDeptList(ids), DEPT_NOT_ENABLE, deptDO.getName()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dept/PostServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dept/PostServiceImplTest.java new file mode 100644 index 0000000..f340618 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dept/PostServiceImplTest.java @@ -0,0 +1,254 @@ +package com.yunxi.scm.module.system.service.dept; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostPageReqVO; +import com.yunxi.scm.module.system.controller.admin.dept.vo.post.PostUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import com.yunxi.scm.module.system.dal.mysql.dept.PostMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link PostServiceImpl} 的单元测试类 + * + * @author niudehua + */ +@Import(PostServiceImpl.class) +public class PostServiceImplTest extends BaseDbUnitTest { + + @Resource + private PostServiceImpl postService; + + @Resource + private PostMapper postMapper; + + @Test + public void testCreatePost_success() { + // 准备参数 + PostCreateReqVO reqVO = randomPojo(PostCreateReqVO.class, + o -> o.setStatus(randomEle(CommonStatusEnum.values()).getStatus())); + // 调用 + Long postId = postService.createPost(reqVO); + + // 断言 + assertNotNull(postId); + // 校验记录的属性是否正确 + PostDO post = postMapper.selectById(postId); + assertPojoEquals(reqVO, post); + } + + @Test + public void testUpdatePost_success() { + // mock 数据 + PostDO postDO = randomPostDO(); + postMapper.insert(postDO);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PostUpdateReqVO reqVO = randomPojo(PostUpdateReqVO.class, o -> { + // 设置更新的 ID + o.setId(postDO.getId()); + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); + }); + + // 调用 + postService.updatePost(reqVO); + // 校验是否更新正确 + PostDO post = postMapper.selectById(reqVO.getId()); + assertPojoEquals(reqVO, post); + } + + @Test + public void testDeletePost_success() { + // mock 数据 + PostDO postDO = randomPostDO(); + postMapper.insert(postDO); + // 准备参数 + Long id = postDO.getId(); + + // 调用 + postService.deletePost(id); + assertNull(postMapper.selectById(id)); + } + + @Test + public void testValidatePost_notFoundForDelete() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> postService.deletePost(id), POST_NOT_FOUND); + } + + @Test + public void testValidatePost_nameDuplicateForCreate() { + // mock 数据 + PostDO postDO = randomPostDO(); + postMapper.insert(postDO);// @Sql: 先插入出一条存在的数据 + // 准备参数 + PostCreateReqVO reqVO = randomPojo(PostCreateReqVO.class, + // 模拟 name 重复 + o -> o.setName(postDO.getName())); + assertServiceException(() -> postService.createPost(reqVO), POST_NAME_DUPLICATE); + } + + @Test + public void testValidatePost_codeDuplicateForUpdate() { + // mock 数据 + PostDO postDO = randomPostDO(); + postMapper.insert(postDO); + // mock 数据:稍后模拟重复它的 code + PostDO codePostDO = randomPostDO(); + postMapper.insert(codePostDO); + // 准备参数 + PostUpdateReqVO reqVO = randomPojo(PostUpdateReqVO.class, o -> { + // 设置更新的 ID + o.setId(postDO.getId()); + // 模拟 code 重复 + o.setCode(codePostDO.getCode()); + }); + + // 调用, 并断言异常 + assertServiceException(() -> postService.updatePost(reqVO), POST_CODE_DUPLICATE); + } + + @Test + public void testGetPostPage() { + // mock 数据 + PostDO postDO = randomPojo(PostDO.class, o -> { + o.setName("码仔"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + postMapper.insert(postDO); + // 测试 name 不匹配 + postMapper.insert(cloneIgnoreId(postDO, o -> o.setName("程序员"))); + // 测试 status 不匹配 + postMapper.insert(cloneIgnoreId(postDO, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + PostPageReqVO reqVO = new PostPageReqVO(); + reqVO.setName("码"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + PageResult pageResult = postService.getPostPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(postDO, pageResult.getList().get(0)); + } + + @Test + public void testGetPostList_export() { + // mock 数据 + PostDO postDO = randomPojo(PostDO.class, o -> { + o.setName("码仔"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + postMapper.insert(postDO); + // 测试 name 不匹配 + postMapper.insert(cloneIgnoreId(postDO, o -> o.setName("程序员"))); + // 测试 status 不匹配 + postMapper.insert(cloneIgnoreId(postDO, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + PostExportReqVO reqVO = new PostExportReqVO(); + reqVO.setName("码"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List list = postService.getPostList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(postDO, list.get(0)); + } + + @Test + public void testGetPostList() { + // mock 数据 + PostDO postDO01 = randomPojo(PostDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + postMapper.insert(postDO01); + // 测试 status 不匹配 + PostDO postDO02 = randomPojo(PostDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + postMapper.insert(postDO02); + // 准备参数 + List ids = Arrays.asList(postDO01.getId(), postDO02.getId()); + + // 调用 + List list = postService.getPostList(ids, singletonList(CommonStatusEnum.ENABLE.getStatus())); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(postDO01, list.get(0)); + } + + @Test + public void testGetPost() { + // mock 数据 + PostDO dbPostDO = randomPostDO(); + postMapper.insert(dbPostDO); + // 准备参数 + Long id = dbPostDO.getId(); + // 调用 + PostDO post = postService.getPost(id); + // 断言 + assertNotNull(post); + assertPojoEquals(dbPostDO, post); + } + + @Test + public void testValidatePostList_success() { + // mock 数据 + PostDO postDO = randomPostDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); + postMapper.insert(postDO); + // 准备参数 + List ids = singletonList(postDO.getId()); + + // 调用,无需断言 + postService.validatePostList(ids); + } + + @Test + public void testValidatePostList_notFound() { + // 准备参数 + List ids = singletonList(randomLongId()); + + // 调用, 并断言异常 + assertServiceException(() -> postService.validatePostList(ids), POST_NOT_FOUND); + } + + @Test + public void testValidatePostList_notEnable() { + // mock 数据 + PostDO postDO = randomPostDO().setStatus(CommonStatusEnum.DISABLE.getStatus()); + postMapper.insert(postDO); + // 准备参数 + List ids = singletonList(postDO.getId()); + + // 调用, 并断言异常 + assertServiceException(() -> postService.validatePostList(ids), POST_NOT_ENABLE, + postDO.getName()); + } + + @SafeVarargs + private static PostDO randomPostDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomCommonStatus()); // 保证 status 的范围 + }; + return randomPojo(PostDO.class, ArrayUtils.append(consumer, consumers)); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dict/DictDataServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dict/DictDataServiceImplTest.java new file mode 100644 index 0000000..67aff1b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dict/DictDataServiceImplTest.java @@ -0,0 +1,371 @@ +package com.yunxi.scm.module.system.service.dict; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataPageReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.data.DictDataUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictDataDO; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictTypeDO; +import com.yunxi.scm.module.system.dal.mysql.dict.DictDataMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import(DictDataServiceImpl.class) +public class DictDataServiceImplTest extends BaseDbUnitTest { + + @Resource + private DictDataServiceImpl dictDataService; + + @Resource + private DictDataMapper dictDataMapper; + @MockBean + private DictTypeService dictTypeService; + + @Test + public void testGetDictDataList() { + // mock 数据 + DictDataDO dictDataDO01 = randomDictDataDO().setDictType("yunai").setSort(2); + dictDataMapper.insert(dictDataDO01); + DictDataDO dictDataDO02 = randomDictDataDO().setDictType("yunai").setSort(1); + dictDataMapper.insert(dictDataDO02); + // 准备参数 + + // 调用 + List dictDataDOList = dictDataService.getDictDataList(); + // 断言 + assertEquals(2, dictDataDOList.size()); + assertPojoEquals(dictDataDO02, dictDataDOList.get(0)); + assertPojoEquals(dictDataDO01, dictDataDOList.get(1)); + } + + @Test + public void testGetDictDataPage() { + // mock 数据 + DictDataDO dbDictData = randomPojo(DictDataDO.class, o -> { // 等会查询到 + o.setLabel("芋艿"); + o.setDictType("yunai"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + dictDataMapper.insert(dbDictData); + // 测试 label 不匹配 + dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setLabel("艿"))); + // 测试 dictType 不匹配 + dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setDictType("nai"))); + // 测试 status 不匹配 + dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + DictDataPageReqVO reqVO = new DictDataPageReqVO(); + reqVO.setLabel("芋"); + reqVO.setDictType("yunai"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + PageResult pageResult = dictDataService.getDictDataPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbDictData, pageResult.getList().get(0)); + } + + @Test + public void testGetDictDataList_export() { + // mock 数据 + DictDataDO dbDictData = randomPojo(DictDataDO.class, o -> { // 等会查询到 + o.setLabel("芋艿"); + o.setDictType("yunai"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + dictDataMapper.insert(dbDictData); + // 测试 label 不匹配 + dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setLabel("艿"))); + // 测试 dictType 不匹配 + dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setDictType("nai"))); + // 测试 status 不匹配 + dictDataMapper.insert(cloneIgnoreId(dbDictData, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + DictDataExportReqVO reqVO = new DictDataExportReqVO(); + reqVO.setLabel("芋"); + reqVO.setDictType("yunai"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List list = dictDataService.getDictDataList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbDictData, list.get(0)); + } + + @Test + public void testGetDictData() { + // mock 数据 + DictDataDO dbDictData = randomDictDataDO(); + dictDataMapper.insert(dbDictData); + // 准备参数 + Long id = dbDictData.getId(); + + // 调用 + DictDataDO dictData = dictDataService.getDictData(id); + // 断言 + assertPojoEquals(dbDictData, dictData); + } + + @Test + public void testCreateDictData_success() { + // 准备参数 + DictDataCreateReqVO reqVO = randomPojo(DictDataCreateReqVO.class, + o -> o.setStatus(randomCommonStatus())); + // mock 方法 + when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType())); + + // 调用 + Long dictDataId = dictDataService.createDictData(reqVO); + // 断言 + assertNotNull(dictDataId); + // 校验记录的属性是否正确 + DictDataDO dictData = dictDataMapper.selectById(dictDataId); + assertPojoEquals(reqVO, dictData); + } + + @Test + public void testUpdateDictData_success() { + // mock 数据 + DictDataDO dbDictData = randomDictDataDO(); + dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DictDataUpdateReqVO reqVO = randomPojo(DictDataUpdateReqVO.class, o -> { + o.setId(dbDictData.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + }); + // mock 方法,字典类型 + when(dictTypeService.getDictType(eq(reqVO.getDictType()))).thenReturn(randomDictTypeDO(reqVO.getDictType())); + + // 调用 + dictDataService.updateDictData(reqVO); + // 校验是否更新正确 + DictDataDO dictData = dictDataMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, dictData); + } + + @Test + public void testDeleteDictData_success() { + // mock 数据 + DictDataDO dbDictData = randomDictDataDO(); + dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDictData.getId(); + + // 调用 + dictDataService.deleteDictData(id); + // 校验数据不存在了 + assertNull(dictDataMapper.selectById(id)); + } + + @Test + public void testValidateDictDataExists_success() { + // mock 数据 + DictDataDO dbDictData = randomDictDataDO(); + dictDataMapper.insert(dbDictData);// @Sql: 先插入出一条存在的数据 + + // 调用成功 + dictDataService.validateDictDataExists(dbDictData.getId()); + } + + @Test + public void testValidateDictDataExists_notExists() { + assertServiceException(() -> dictDataService.validateDictDataExists(randomLongId()), DICT_DATA_NOT_EXISTS); + } + + @Test + public void testValidateDictTypeExists_success() { + // mock 方法,数据类型被禁用 + String type = randomString(); + when(dictTypeService.getDictType(eq(type))).thenReturn(randomDictTypeDO(type)); + + // 调用, 成功 + dictDataService.validateDictTypeExists(type); + } + + @Test + public void testValidateDictTypeExists_notExists() { + assertServiceException(() -> dictDataService.validateDictTypeExists(randomString()), DICT_TYPE_NOT_EXISTS); + } + + @Test + public void testValidateDictTypeExists_notEnable() { + // mock 方法,数据类型被禁用 + String dictType = randomString(); + when(dictTypeService.getDictType(eq(dictType))).thenReturn( + randomPojo(DictTypeDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + + // 调用, 并断言异常 + assertServiceException(() -> dictDataService.validateDictTypeExists(dictType), DICT_TYPE_NOT_ENABLE); + } + + @Test + public void testValidateDictDataValueUnique_success() { + // 调用,成功 + dictDataService.validateDictDataValueUnique(randomLongId(), randomString(), randomString()); + } + + @Test + public void testValidateDictDataValueUnique_valueDuplicateForCreate() { + // 准备参数 + String dictType = randomString(); + String value = randomString(); + // mock 数据 + dictDataMapper.insert(randomDictDataDO(o -> { + o.setDictType(dictType); + o.setValue(value); + })); + + // 调用,校验异常 + assertServiceException(() -> dictDataService.validateDictDataValueUnique(null, dictType, value), + DICT_DATA_VALUE_DUPLICATE); + } + + @Test + public void testValidateDictDataValueUnique_valueDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String dictType = randomString(); + String value = randomString(); + // mock 数据 + dictDataMapper.insert(randomDictDataDO(o -> { + o.setDictType(dictType); + o.setValue(value); + })); + + // 调用,校验异常 + assertServiceException(() -> dictDataService.validateDictDataValueUnique(id, dictType, value), + DICT_DATA_VALUE_DUPLICATE); + } + + @Test + public void testCountByDictType() { + // mock 数据 + dictDataMapper.insert(randomDictDataDO(o -> o.setDictType("yunai"))); + dictDataMapper.insert(randomDictDataDO(o -> o.setDictType("tudou"))); + dictDataMapper.insert(randomDictDataDO(o -> o.setDictType("yunai"))); + // 准备参数 + String dictType = "yunai"; + + // 调用 + long count = dictDataService.countByDictType(dictType); + // 校验 + assertEquals(2L, count); + } + + @Test + public void testValidateDictDataList_success() { + // mock 数据 + DictDataDO dictDataDO = randomDictDataDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); + dictDataMapper.insert(dictDataDO); + // 准备参数 + String dictType = dictDataDO.getDictType(); + List values = singletonList(dictDataDO.getValue()); + + // 调用,无需断言 + dictDataService.validateDictDataList(dictType, values); + } + + @Test + public void testValidateDictDataList_notFound() { + // 准备参数 + String dictType = randomString(); + List values = singletonList(randomString()); + + // 调用, 并断言异常 + assertServiceException(() -> dictDataService.validateDictDataList(dictType, values), DICT_DATA_NOT_EXISTS); + } + + @Test + public void testValidateDictDataList_notEnable() { + // mock 数据 + DictDataDO dictDataDO = randomDictDataDO().setStatus(CommonStatusEnum.DISABLE.getStatus()); + dictDataMapper.insert(dictDataDO); + // 准备参数 + String dictType = dictDataDO.getDictType(); + List values = singletonList(dictDataDO.getValue()); + + // 调用, 并断言异常 + assertServiceException(() -> dictDataService.validateDictDataList(dictType, values), + DICT_DATA_NOT_ENABLE, dictDataDO.getLabel()); + } + + @Test + public void testGetDictData_dictType() { + // mock 数据 + DictDataDO dictDataDO = randomDictDataDO().setDictType("yunai").setValue("1"); + dictDataMapper.insert(dictDataDO); + DictDataDO dictDataDO02 = randomDictDataDO().setDictType("yunai").setValue("2"); + dictDataMapper.insert(dictDataDO02); + // 准备参数 + String dictType = "yunai"; + String value = "1"; + + // 调用 + DictDataDO dbDictData = dictDataService.getDictData(dictType, value); + // 断言 + assertEquals(dictDataDO, dbDictData); + } + + @Test + public void testParseDictData() { + // mock 数据 + DictDataDO dictDataDO = randomDictDataDO().setDictType("yunai").setLabel("1"); + dictDataMapper.insert(dictDataDO); + DictDataDO dictDataDO02 = randomDictDataDO().setDictType("yunai").setLabel("2"); + dictDataMapper.insert(dictDataDO02); + // 准备参数 + String dictType = "yunai"; + String label = "1"; + + // 调用 + DictDataDO dbDictData = dictDataService.parseDictData(dictType, label); + // 断言 + assertEquals(dictDataDO, dbDictData); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static DictDataDO randomDictDataDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomCommonStatus()); // 保证 status 的范围 + }; + return randomPojo(DictDataDO.class, ArrayUtils.append(consumer, consumers)); + } + + /** + * 生成一个有效的字典类型 + * + * @param type 字典类型 + * @return DictTypeDO 对象 + */ + private static DictTypeDO randomDictTypeDO(String type) { + return randomPojo(DictTypeDO.class, o -> { + o.setType(type); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 是开启 + }); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dict/DictTypeServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dict/DictTypeServiceImplTest.java new file mode 100644 index 0000000..7ef9100 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/dict/DictTypeServiceImplTest.java @@ -0,0 +1,304 @@ +package com.yunxi.scm.module.system.service.dict; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeExportReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypePageReqVO; +import com.yunxi.scm.module.system.controller.admin.dict.vo.type.DictTypeUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.dict.DictTypeDO; +import com.yunxi.scm.module.system.dal.mysql.dict.DictTypeMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@Import(DictTypeServiceImpl.class) +public class DictTypeServiceImplTest extends BaseDbUnitTest { + + @Resource + private DictTypeServiceImpl dictTypeService; + + @Resource + private DictTypeMapper dictTypeMapper; + @MockBean + private DictDataService dictDataService; + + @Test + public void testGetDictTypePage() { + // mock 数据 + DictTypeDO dbDictType = randomPojo(DictTypeDO.class, o -> { // 等会查询到 + o.setName("yunai"); + o.setType("芋艿"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021, 1, 15)); + }); + dictTypeMapper.insert(dbDictType); + // 测试 name 不匹配 + dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setName("tudou"))); + // 测试 type 不匹配 + dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setType("土豆"))); + // 测试 status 不匹配 + dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + DictTypePageReqVO reqVO = new DictTypePageReqVO(); + reqVO.setName("nai"); + reqVO.setType("艿"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 10, 2021, 1, 20)); + + // 调用 + PageResult pageResult = dictTypeService.getDictTypePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbDictType, pageResult.getList().get(0)); + } + + @Test + public void testGetDictTypeList_export() { + // mock 数据 + DictTypeDO dbDictType = randomPojo(DictTypeDO.class, o -> { // 等会查询到 + o.setName("yunai"); + o.setType("芋艿"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2021, 1, 15)); + }); + dictTypeMapper.insert(dbDictType); + // 测试 name 不匹配 + dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setName("tudou"))); + // 测试 type 不匹配 + dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setType("土豆"))); + // 测试 status 不匹配 + dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + dictTypeMapper.insert(cloneIgnoreId(dbDictType, o -> o.setCreateTime(buildTime(2021, 1, 1)))); + // 准备参数 + DictTypeExportReqVO reqVO = new DictTypeExportReqVO(); + reqVO.setName("nai"); + reqVO.setType("艿"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2021, 1, 10, 2021, 1, 20)); + + // 调用 + List list = dictTypeService.getDictTypeList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbDictType, list.get(0)); + } + + @Test + public void testGetDictType_id() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType); + // 准备参数 + Long id = dbDictType.getId(); + + // 调用 + DictTypeDO dictType = dictTypeService.getDictType(id); + // 断言 + assertNotNull(dictType); + assertPojoEquals(dbDictType, dictType); + } + + @Test + public void testGetDictType_type() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType); + // 准备参数 + String type = dbDictType.getType(); + + // 调用 + DictTypeDO dictType = dictTypeService.getDictType(type); + // 断言 + assertNotNull(dictType); + assertPojoEquals(dbDictType, dictType); + } + + @Test + public void testCreateDictType_success() { + // 准备参数 + DictTypeCreateReqVO reqVO = randomPojo(DictTypeCreateReqVO.class, + o -> o.setStatus(randomEle(CommonStatusEnum.values()).getStatus())); + + // 调用 + Long dictTypeId = dictTypeService.createDictType(reqVO); + // 断言 + assertNotNull(dictTypeId); + // 校验记录的属性是否正确 + DictTypeDO dictType = dictTypeMapper.selectById(dictTypeId); + assertPojoEquals(reqVO, dictType); + } + + @Test + public void testUpdateDictType_success() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 + // 准备参数 + DictTypeUpdateReqVO reqVO = randomPojo(DictTypeUpdateReqVO.class, o -> { + o.setId(dbDictType.getId()); // 设置更新的 ID + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); + }); + + // 调用 + dictTypeService.updateDictType(reqVO); + // 校验是否更新正确 + DictTypeDO dictType = dictTypeMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, dictType); + } + + @Test + public void testDeleteDictType_success() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDictType.getId(); + + // 调用 + dictTypeService.deleteDictType(id); + // 校验数据不存在了 + assertNull(dictTypeMapper.selectById(id)); + } + + @Test + public void testDeleteDictType_hasChildren() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbDictType.getId(); + // mock 方法 + when(dictDataService.countByDictType(eq(dbDictType.getType()))).thenReturn(1L); + + // 调用, 并断言异常 + assertServiceException(() -> dictTypeService.deleteDictType(id), DICT_TYPE_HAS_CHILDREN); + } + + @Test + public void testGetDictTypeList() { + // 准备参数 + DictTypeDO dictTypeDO01 = randomDictTypeDO(); + dictTypeMapper.insert(dictTypeDO01); + DictTypeDO dictTypeDO02 = randomDictTypeDO(); + dictTypeMapper.insert(dictTypeDO02); + // mock 方法 + + // 调用 + List dictTypeDOList = dictTypeService.getDictTypeList(); + // 断言 + assertEquals(2, dictTypeDOList.size()); + assertPojoEquals(dictTypeDO01, dictTypeDOList.get(0)); + assertPojoEquals(dictTypeDO02, dictTypeDOList.get(1)); + } + + @Test + public void testValidateDictDataExists_success() { + // mock 数据 + DictTypeDO dbDictType = randomDictTypeDO(); + dictTypeMapper.insert(dbDictType);// @Sql: 先插入出一条存在的数据 + + // 调用成功 + dictTypeService.validateDictTypeExists(dbDictType.getId()); + } + + @Test + public void testValidateDictDataExists_notExists() { + assertServiceException(() -> dictTypeService.validateDictTypeExists(randomLongId()), DICT_TYPE_NOT_EXISTS); + } + + @Test + public void testValidateDictTypeUnique_success() { + // 调用,成功 + dictTypeService.validateDictTypeUnique(randomLongId(), randomString()); + } + + @Test + public void testValidateDictTypeUnique_valueDuplicateForCreate() { + // 准备参数 + String type = randomString(); + // mock 数据 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type))); + + // 调用,校验异常 + assertServiceException(() -> dictTypeService.validateDictTypeUnique(null, type), + DICT_TYPE_TYPE_DUPLICATE); + } + + @Test + public void testValidateDictTypeUnique_valueDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String type = randomString(); + // mock 数据 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setType(type))); + + // 调用,校验异常 + assertServiceException(() -> dictTypeService.validateDictTypeUnique(id, type), + DICT_TYPE_TYPE_DUPLICATE); + } + + @Test + public void testValidateDictTypNameUnique_success() { + // 调用,成功 + dictTypeService.validateDictTypeNameUnique(randomLongId(), randomString()); + } + + @Test + public void testValidateDictTypeNameUnique_nameDuplicateForCreate() { + // 准备参数 + String name = randomString(); + // mock 数据 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name))); + + // 调用,校验异常 + assertServiceException(() -> dictTypeService.validateDictTypeNameUnique(null, name), + DICT_TYPE_NAME_DUPLICATE); + } + + @Test + public void testValidateDictTypeNameUnique_nameDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String name = randomString(); + // mock 数据 + dictTypeMapper.insert(randomDictTypeDO(o -> o.setName(name))); + + // 调用,校验异常 + assertServiceException(() -> dictTypeService.validateDictTypeNameUnique(id, name), + DICT_TYPE_NAME_DUPLICATE); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static DictTypeDO randomDictTypeDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + }; + return randomPojo(DictTypeDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeServiceTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeServiceTest.java new file mode 100644 index 0000000..0c1b1db --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/errorcode/ErrorCodeServiceTest.java @@ -0,0 +1,328 @@ +package com.yunxi.scm.module.system.service.errorcode; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeAutoGenerateReqDTO; +import com.yunxi.scm.module.system.api.errorcode.dto.ErrorCodeRespDTO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeExportReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodePageReqVO; +import com.yunxi.scm.module.system.controller.admin.errorcode.vo.ErrorCodeUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.errorcode.ErrorCodeDO; +import com.yunxi.scm.module.system.dal.mysql.errorcode.ErrorCodeMapper; +import com.yunxi.scm.module.system.enums.errorcode.ErrorCodeTypeEnum; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.ERROR_CODE_DUPLICATE; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.ERROR_CODE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +@Import(ErrorCodeServiceImpl.class) +public class ErrorCodeServiceTest extends BaseDbUnitTest { + + @Resource + private ErrorCodeServiceImpl errorCodeService; + + @Resource + private ErrorCodeMapper errorCodeMapper; + + @Test + public void testCreateErrorCode_success() { + // 准备参数 + ErrorCodeCreateReqVO reqVO = randomPojo(ErrorCodeCreateReqVO.class); + + // 调用 + Long errorCodeId = errorCodeService.createErrorCode(reqVO); + // 断言 + assertNotNull(errorCodeId); + // 校验记录的属性是否正确 + ErrorCodeDO errorCode = errorCodeMapper.selectById(errorCodeId); + assertPojoEquals(reqVO, errorCode); + assertEquals(ErrorCodeTypeEnum.MANUAL_OPERATION.getType(), errorCode.getType()); + } + + @Test + public void testUpdateErrorCode_success() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomErrorCodeDO(); + errorCodeMapper.insert(dbErrorCode);// @Sql: 先插入出一条存在的数据 + // 准备参数 + ErrorCodeUpdateReqVO reqVO = randomPojo(ErrorCodeUpdateReqVO.class, o -> { + o.setId(dbErrorCode.getId()); // 设置更新的 ID + }); + + // 调用 + errorCodeService.updateErrorCode(reqVO); + // 校验是否更新正确 + ErrorCodeDO errorCode = errorCodeMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, errorCode); + assertEquals(ErrorCodeTypeEnum.MANUAL_OPERATION.getType(), errorCode.getType()); + } + + @Test + public void testDeleteErrorCode_success() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomErrorCodeDO(); + errorCodeMapper.insert(dbErrorCode);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbErrorCode.getId(); + + // 调用 + errorCodeService.deleteErrorCode(id); + // 校验数据不存在了 + assertNull(errorCodeMapper.selectById(id)); + } + + @Test + public void testGetErrorCodePage() { + // mock 数据 + ErrorCodeDO dbErrorCode = initGetErrorCodePage(); + // 准备参数 + ErrorCodePageReqVO reqVO = new ErrorCodePageReqVO(); + reqVO.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + reqVO.setApplicationName("tu"); + reqVO.setCode(1); + reqVO.setMessage("ma"); + reqVO.setCreateTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30)); + + // 调用 + PageResult pageResult = errorCodeService.getErrorCodePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbErrorCode, pageResult.getList().get(0)); + } + + /** + * 初始化 getErrorCodePage 方法的测试数据 + */ + private ErrorCodeDO initGetErrorCodePage() { + ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> { // 等会查询到 + o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + o.setApplicationName("tudou"); + o.setCode(1); + o.setMessage("yuanma"); + o.setCreateTime(buildTime(2020, 11, 11)); + }); + errorCodeMapper.insert(dbErrorCode); + // 测试 type 不匹配 + errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType()))); + // 测试 applicationName 不匹配 + errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setApplicationName("yuan"))); + // 测试 code 不匹配 + errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setCode(2))); + // 测试 message 不匹配 + errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setMessage("nai"))); + // 测试 createTime 不匹配 + errorCodeMapper.insert(cloneIgnoreId(dbErrorCode, o -> o.setCreateTime(buildTime(2020, 12, 12)))); + return dbErrorCode; + } + + @Test + public void testGetErrorCodeList_export() { + // mock 数据 + ErrorCodeDO dbErrorCode = initGetErrorCodePage(); + // 准备参数 + ErrorCodeExportReqVO reqVO = new ErrorCodeExportReqVO(); + reqVO.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType()); + reqVO.setApplicationName("tu"); + reqVO.setCode(1); + reqVO.setMessage("ma"); + reqVO.setCreateTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30)); + + // 调用 + List list = errorCodeService.getErrorCodeList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbErrorCode, list.get(0)); + } + + @Test + public void testValidateCodeDuplicate_codeDuplicateForCreate() { + // 准备参数 + Integer code = randomInteger(); + // mock 数据 + errorCodeMapper.insert(randomErrorCodeDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> errorCodeService.validateCodeDuplicate(code, null), + ERROR_CODE_DUPLICATE); + } + + @Test + public void testValidateCodeDuplicate_codeDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + Integer code = randomInteger(); + // mock 数据 + errorCodeMapper.insert(randomErrorCodeDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> errorCodeService.validateCodeDuplicate(code, id), + ERROR_CODE_DUPLICATE); + } + + @Test + public void testValidateErrorCodeExists_notExists() { + assertServiceException(() -> errorCodeService.validateErrorCodeExists(null), + ERROR_CODE_NOT_EXISTS); + } + + /** + * 情况 1,错误码不存在的情况 + */ + @Test + public void testAutoGenerateErrorCodes_01() { + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言 + ErrorCodeDO errorCode = errorCodeMapper.selectOne(null); + assertPojoEquals(generateReqDTO, errorCode); + assertEquals(ErrorCodeTypeEnum.AUTO_GENERATION.getType(), errorCode.getType()); + } + + /** + * 情况 2.1,错误码存在,但是是 ErrorCodeTypeEnum.MANUAL_OPERATION 类型 + */ + @Test + public void testAutoGenerateErrorCodes_021() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.MANUAL_OPERATION.getType())); + errorCodeMapper.insert(dbErrorCode); + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class, + o -> o.setCode(dbErrorCode.getCode())); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言,相等,说明不会更新 + ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId()); + assertPojoEquals(dbErrorCode, errorCode); + } + + /** + * 情况 2.2,错误码存在,但是是 applicationName 不匹配 + */ + @Test + public void testAutoGenerateErrorCodes_022() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType())); + errorCodeMapper.insert(dbErrorCode); + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class, + o -> o.setCode(dbErrorCode.getCode()).setApplicationName(randomString())); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言,相等,说明不会更新 + ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId()); + assertPojoEquals(dbErrorCode, errorCode); + } + + /** + * 情况 2.3,错误码存在,但是是 message 相同 + */ + @Test + public void testAutoGenerateErrorCodes_023() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType())); + errorCodeMapper.insert(dbErrorCode); + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class, + o -> o.setCode(dbErrorCode.getCode()).setApplicationName(dbErrorCode.getApplicationName()) + .setMessage(dbErrorCode.getMessage())); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言,相等,说明不会更新 + ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId()); + assertPojoEquals(dbErrorCode, errorCode); + } + + /** + * 情况 2.3,错误码存在,但是是 message 不同,则进行更新 + */ + @Test + public void testAutoGenerateErrorCodes_024() { + // mock 数据 + ErrorCodeDO dbErrorCode = randomErrorCodeDO(o -> o.setType(ErrorCodeTypeEnum.AUTO_GENERATION.getType())); + errorCodeMapper.insert(dbErrorCode); + // 准备参数 + ErrorCodeAutoGenerateReqDTO generateReqDTO = randomPojo(ErrorCodeAutoGenerateReqDTO.class, + o -> o.setCode(dbErrorCode.getCode()).setApplicationName(dbErrorCode.getApplicationName())); + // mock 方法 + + // 调用 + errorCodeService.autoGenerateErrorCodes(Lists.newArrayList(generateReqDTO)); + // 断言,匹配 + ErrorCodeDO errorCode = errorCodeMapper.selectById(dbErrorCode.getId()); + assertPojoEquals(generateReqDTO, errorCode); + } + + @Test + public void testGetErrorCode() { + // 准备参数 + ErrorCodeDO errorCodeDO = randomErrorCodeDO(); + errorCodeMapper.insert(errorCodeDO); + // mock 方法 + Long id = errorCodeDO.getId(); + + // 调用 + ErrorCodeDO dbErrorCode = errorCodeService.getErrorCode(id); + // 断言 + assertPojoEquals(errorCodeDO, dbErrorCode); + } + + @Test + public void testGetErrorCodeList() { + // 准备参数 + ErrorCodeDO errorCodeDO01 = randomErrorCodeDO( + o -> o.setApplicationName("yunai_server").setUpdateTime(buildTime(2022, 1, 10))); + errorCodeMapper.insert(errorCodeDO01); + ErrorCodeDO errorCodeDO02 = randomErrorCodeDO( + o -> o.setApplicationName("yunai_server").setUpdateTime(buildTime(2022, 1, 12))); + errorCodeMapper.insert(errorCodeDO02); + // mock 方法 + String applicationName = "yunai_server"; + LocalDateTime minUpdateTime = buildTime(2022, 1, 11); + + // 调用 + List errorCodeList = errorCodeService.getErrorCodeList(applicationName, minUpdateTime); + // 断言 + assertEquals(1, errorCodeList.size()); + assertPojoEquals(errorCodeDO02, errorCodeList.get(0)); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static ErrorCodeDO randomErrorCodeDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setType(randomEle(ErrorCodeTypeEnum.values()).getType()); // 保证 key 的范围 + }; + return randomPojo(ErrorCodeDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/logger/LoginLogServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/logger/LoginLogServiceImplTest.java new file mode 100644 index 0000000..136808b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/logger/LoginLogServiceImplTest.java @@ -0,0 +1,110 @@ +package com.yunxi.scm.module.system.service.logger; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.api.logger.dto.LoginLogCreateReqDTO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.loginlog.LoginLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.logger.LoginLogDO; +import com.yunxi.scm.module.system.dal.mysql.logger.LoginLogMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.system.enums.logger.LoginResultEnum.CAPTCHA_CODE_ERROR; +import static com.yunxi.scm.module.system.enums.logger.LoginResultEnum.SUCCESS; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Import(LoginLogServiceImpl.class) +public class LoginLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private LoginLogServiceImpl loginLogService; + + @Resource + private LoginLogMapper loginLogMapper; + + @Test + public void testGetLoginLogPage() { + // mock 数据 + LoginLogDO loginLogDO = randomPojo(LoginLogDO.class, o -> { + o.setUserIp("192.168.199.16"); + o.setUsername("wang"); + o.setResult(SUCCESS.getResult()); + o.setCreateTime(buildTime(2021, 3, 6)); + }); + loginLogMapper.insert(loginLogDO); + // 测试 status 不匹配 + loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setResult(CAPTCHA_CODE_ERROR.getResult()))); + // 测试 ip 不匹配 + loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setUserIp("192.168.128.18"))); + // 测试 username 不匹配 + loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setUsername("yunai"))); + // 测试 createTime 不匹配 + loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setCreateTime(buildTime(2021, 2, 6)))); + // 构造调用参数 + LoginLogPageReqVO reqVO = new LoginLogPageReqVO(); + reqVO.setUsername("wang"); + reqVO.setUserIp("192.168.199"); + reqVO.setStatus(true); + reqVO.setCreateTime(buildBetweenTime(2021, 3, 5, 2021, 3, 7)); + + // 调用 + PageResult pageResult = loginLogService.getLoginLogPage(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(loginLogDO, pageResult.getList().get(0)); + } + + @Test + public void testGetLoginLogList() { + // mock 数据 + LoginLogDO loginLogDO = randomPojo(LoginLogDO.class, o -> { + o.setUserIp("192.168.199.16"); + o.setUsername("wang"); + o.setResult(SUCCESS.getResult()); + o.setCreateTime(buildTime(2021, 3, 6)); + }); + loginLogMapper.insert(loginLogDO); + // 测试 status 不匹配 + loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setResult(CAPTCHA_CODE_ERROR.getResult()))); + // 测试 ip 不匹配 + loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setUserIp("192.168.128.18"))); + // 测试 username 不匹配 + loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setUsername("yunai"))); + // 测试 createTime 不匹配 + loginLogMapper.insert(cloneIgnoreId(loginLogDO, o -> o.setCreateTime(buildTime(2021, 2, 6)))); + // 构造调用参数 + LoginLogExportReqVO reqVO = new LoginLogExportReqVO(); + reqVO.setUsername("wang"); + reqVO.setUserIp("192.168.199"); + reqVO.setStatus(true); + reqVO.setCreateTime(buildBetweenTime(2021, 3, 5, 2021, 3, 7)); + + // 调用service方法 + List list = loginLogService.getLoginLogList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(loginLogDO, list.get(0)); + } + + @Test + public void testCreateLoginLog() { + LoginLogCreateReqDTO reqDTO = randomPojo(LoginLogCreateReqDTO.class); + + // 调用 + loginLogService.createLoginLog(reqDTO); + // 断言 + LoginLogDO loginLogDO = loginLogMapper.selectOne(null); + assertPojoEquals(reqDTO, loginLogDO); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/logger/OperateLogServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/logger/OperateLogServiceImplTest.java new file mode 100644 index 0000000..7fd2193 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/logger/OperateLogServiceImplTest.java @@ -0,0 +1,155 @@ +package com.yunxi.scm.module.system.service.logger; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.operatelog.core.enums.OperateTypeEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.framework.test.core.util.RandomUtils; +import com.yunxi.scm.module.system.api.logger.dto.OperateLogCreateReqDTO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.logger.vo.operatelog.OperateLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.logger.OperateLogDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.dal.mysql.logger.OperateLogMapper; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Collections; +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.BAD_REQUEST; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +@Import({OperateLogServiceImpl.class}) +public class OperateLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private OperateLogService operateLogServiceImpl; + + @Resource + private OperateLogMapper operateLogMapper; + + @MockBean + private AdminUserService userService; + + @Test + public void testCreateOperateLogAsync() { + OperateLogCreateReqDTO reqVO = RandomUtils.randomPojo(OperateLogCreateReqDTO.class, + o -> o.setExts(MapUtil.builder("orderId", randomLongId()).build())); + + // 调研 + operateLogServiceImpl.createOperateLog(reqVO); + // 断言 + OperateLogDO operateLogDO = operateLogMapper.selectOne(null); + assertPojoEquals(reqVO, operateLogDO); + } + + @Test + public void testGetOperateLogPage() { + // mock(用户信息) + AdminUserDO user = RandomUtils.randomPojo(AdminUserDO.class, o -> { + o.setNickname("wang"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(userService.getUserListByNickname("wang")).thenReturn(Collections.singletonList(user)); + Long userId = user.getId(); + + // 构造操作日志 + OperateLogDO operateLogDO = RandomUtils.randomPojo(OperateLogDO.class, o -> { + o.setUserId(userId); + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); + o.setModule("order"); + o.setType(OperateTypeEnum.CREATE.getType()); + o.setStartTime(buildTime(2021, 3, 6)); + o.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + o.setExts(MapUtil.builder("orderId", randomLongId()).build()); + }); + operateLogMapper.insert(operateLogDO); + // 测试 userId 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setUserId(userId + 1))); + // 测试 module 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setModule("user"))); + // 测试 type 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setType(OperateTypeEnum.IMPORT.getType()))); + // 测试 createTime 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setStartTime(buildTime(2021, 2, 6)))); + // 测试 resultCode 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setResultCode(BAD_REQUEST.getCode()))); + + // 构造调用参数 + OperateLogPageReqVO reqVO = new OperateLogPageReqVO(); + reqVO.setUserNickname("wang"); + reqVO.setModule("order"); + reqVO.setType(OperateTypeEnum.CREATE.getType()); + reqVO.setStartTime(buildBetweenTime(2021, 3, 5, 2021, 3, 7)); + reqVO.setSuccess(true); + + // 调用 + PageResult pageResult = operateLogServiceImpl.getOperateLogPage(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(operateLogDO, pageResult.getList().get(0)); + } + + @Test + public void testGetOperateLogs() { + // mock(用户信息) + AdminUserDO user = RandomUtils.randomPojo(AdminUserDO.class, o -> { + o.setNickname("wang"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(userService.getUserListByNickname("wang")).thenReturn(Collections.singletonList(user)); + Long userId = user.getId(); + + // 构造操作日志 + OperateLogDO operateLogDO = RandomUtils.randomPojo(OperateLogDO.class, o -> { + o.setUserId(userId); + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); + o.setModule("order"); + o.setType(OperateTypeEnum.CREATE.getType()); + o.setStartTime(buildTime(2021, 3, 6)); + o.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode()); + o.setExts(MapUtil.builder("orderId", randomLongId()).build()); + }); + operateLogMapper.insert(operateLogDO); + // 测试 userId 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setUserId(userId + 1))); + // 测试 module 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setModule("user"))); + // 测试 type 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setType(OperateTypeEnum.IMPORT.getType()))); + // 测试 createTime 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setStartTime(buildTime(2021, 2, 6)))); + // 测试 resultCode 不匹配 + operateLogMapper.insert(cloneIgnoreId(operateLogDO, o -> o.setResultCode(BAD_REQUEST.getCode()))); + + // 构造调用参数 + OperateLogExportReqVO reqVO = new OperateLogExportReqVO(); + reqVO.setUserNickname("wang"); + reqVO.setModule("order"); + reqVO.setType(OperateTypeEnum.CREATE.getType()); + reqVO.setStartTime(buildBetweenTime(2021, 3, 5, 2021, 3, 7)); + reqVO.setSuccess(true); + + // 调用 service 方法 + List list = operateLogServiceImpl.getOperateLogList(reqVO); + // 断言,只查到了一条符合条件的 + assertEquals(1, list.size()); + assertPojoEquals(operateLogDO, list.get(0)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailAccountServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailAccountServiceImplTest.java new file mode 100644 index 0000000..9f8808a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailAccountServiceImplTest.java @@ -0,0 +1,179 @@ +package com.yunxi.scm.module.system.service.mail; + +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountPageReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.account.MailAccountUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import com.yunxi.scm.module.system.dal.mysql.mail.MailAccountMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.MAIL_ACCOUNT_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link MailAccountServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(MailAccountServiceImpl.class) +public class MailAccountServiceImplTest extends BaseDbUnitTest { + + @Resource + private MailAccountServiceImpl mailAccountService; + + @Resource + private MailAccountMapper mailAccountMapper; + + @MockBean + private MailTemplateService mailTemplateService; + + @Test + public void testCreateMailAccount_success() { + // 准备参数 + MailAccountCreateReqVO reqVO = randomPojo(MailAccountCreateReqVO.class, o -> o.setMail(randomEmail())); + + // 调用 + Long mailAccountId = mailAccountService.createMailAccount(reqVO); + // 断言 + assertNotNull(mailAccountId); + // 校验记录的属性是否正确 + MailAccountDO mailAccount = mailAccountMapper.selectById(mailAccountId); + assertPojoEquals(reqVO, mailAccount); + } + + @Test + public void testUpdateMailAccount_success() { + // mock 数据 + MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class); + mailAccountMapper.insert(dbMailAccount);// @Sql: 先插入出一条存在的数据 + // 准备参数 + MailAccountUpdateReqVO reqVO = randomPojo(MailAccountUpdateReqVO.class, o -> { + o.setId(dbMailAccount.getId()); // 设置更新的 ID + o.setMail(randomEmail()); + }); + + // 调用 + mailAccountService.updateMailAccount(reqVO); + // 校验是否更新正确 + MailAccountDO mailAccount = mailAccountMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, mailAccount); + } + + @Test + public void testUpdateMailAccount_notExists() { + // 准备参数 + MailAccountUpdateReqVO reqVO = randomPojo(MailAccountUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> mailAccountService.updateMailAccount(reqVO), MAIL_ACCOUNT_NOT_EXISTS); + } + + @Test + public void testDeleteMailAccount_success() { + // mock 数据 + MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class); + mailAccountMapper.insert(dbMailAccount);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbMailAccount.getId(); + // mock 方法(无关联模版) + when(mailTemplateService.countByAccountId(eq(id))).thenReturn(0L); + + // 调用 + mailAccountService.deleteMailAccount(id); + // 校验数据不存在了 + assertNull(mailAccountMapper.selectById(id)); + } + + @Test + public void testGetMailAccountFromCache() { + // mock 数据 + MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class); + mailAccountMapper.insert(dbMailAccount);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbMailAccount.getId(); + + // 调用 + MailAccountDO mailAccount = mailAccountService.getMailAccountFromCache(id); + // 断言 + assertPojoEquals(dbMailAccount, mailAccount); + } + + @Test + public void testDeleteMailAccount_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> mailAccountService.deleteMailAccount(id), MAIL_ACCOUNT_NOT_EXISTS); + } + + @Test + public void testGetMailAccountPage() { + // mock 数据 + MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class, o -> { // 等会查询到 + o.setMail("768@qq.com"); + o.setUsername("yunai"); + }); + mailAccountMapper.insert(dbMailAccount); + // 测试 mail 不匹配 + mailAccountMapper.insert(cloneIgnoreId(dbMailAccount, o -> o.setMail("788@qq.com"))); + // 测试 username 不匹配 + mailAccountMapper.insert(cloneIgnoreId(dbMailAccount, o -> o.setUsername("tudou"))); + // 准备参数 + MailAccountPageReqVO reqVO = new MailAccountPageReqVO(); + reqVO.setMail("768"); + reqVO.setUsername("yu"); + + // 调用 + PageResult pageResult = mailAccountService.getMailAccountPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbMailAccount, pageResult.getList().get(0)); + } + + @Test + public void testGetMailAccount() { + // mock 数据 + MailAccountDO dbMailAccount = randomPojo(MailAccountDO.class); + mailAccountMapper.insert(dbMailAccount);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbMailAccount.getId(); + + // 调用 + MailAccountDO mailAccount = mailAccountService.getMailAccount(id); + // 断言 + assertPojoEquals(dbMailAccount, mailAccount); + } + + @Test + public void testGetMailAccountList() { + // mock 数据 + MailAccountDO dbMailAccount01 = randomPojo(MailAccountDO.class); + mailAccountMapper.insert(dbMailAccount01); + MailAccountDO dbMailAccount02 = randomPojo(MailAccountDO.class); + mailAccountMapper.insert(dbMailAccount02); + // 准备参数 + + // 调用 + List list = mailAccountService.getMailAccountList(); + // 断言 + assertEquals(2, list.size()); + assertPojoEquals(dbMailAccount01, list.get(0)); + assertPojoEquals(dbMailAccount02, list.get(1)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailLogServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailLogServiceImplTest.java new file mode 100644 index 0000000..b158b4a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailLogServiceImplTest.java @@ -0,0 +1,183 @@ +package com.yunxi.scm.module.system.service.mail; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.mail.vo.log.MailLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailLogDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.mail.MailLogMapper; +import com.yunxi.scm.module.system.enums.mail.MailSendStatusEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link MailLogServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(MailLogServiceImpl.class) +public class MailLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private MailLogServiceImpl mailLogService; + + @Resource + private MailLogMapper mailLogMapper; + + @Test + public void testCreateMailLog() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String toMail = randomEmail(); + MailAccountDO account = randomPojo(MailAccountDO.class); + MailTemplateDO template = randomPojo(MailTemplateDO.class); + String templateContent = randomString(); + Map templateParams = randomTemplateParams(); + Boolean isSend = true; + // mock 方法 + + // 调用 + Long logId = mailLogService.createMailLog(userId, userType, toMail, account, template, templateContent, templateParams, isSend); + // 断言 + MailLogDO log = mailLogMapper.selectById(logId); + assertNotNull(log); + assertEquals(MailSendStatusEnum.INIT.getStatus(), log.getSendStatus()); + assertEquals(userId, log.getUserId()); + assertEquals(userType, log.getUserType()); + assertEquals(toMail, log.getToMail()); + assertEquals(account.getId(), log.getAccountId()); + assertEquals(account.getMail(), log.getFromMail()); + assertEquals(template.getId(), log.getTemplateId()); + assertEquals(template.getCode(), log.getTemplateCode()); + assertEquals(template.getNickname(), log.getTemplateNickname()); + assertEquals(template.getTitle(), log.getTemplateTitle()); + assertEquals(templateContent, log.getTemplateContent()); + assertEquals(templateParams, log.getTemplateParams()); + } + + @Test + public void testUpdateMailSendResult_success() { + // mock 数据 + MailLogDO log = randomPojo(MailLogDO.class, o -> { + o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); + o.setSendTime(null).setSendMessageId(null).setSendException(null) + .setTemplateParams(randomTemplateParams()); + }); + mailLogMapper.insert(log); + // 准备参数 + Long logId = log.getId(); + String messageId = randomString(); + + // 调用 + mailLogService.updateMailSendResult(logId, messageId, null); + // 断言 + MailLogDO dbLog = mailLogMapper.selectById(logId); + assertEquals(MailSendStatusEnum.SUCCESS.getStatus(), dbLog.getSendStatus()); + assertNotNull(dbLog.getSendTime()); + assertEquals(messageId, dbLog.getSendMessageId()); + assertNull(dbLog.getSendException()); + } + + @Test + public void testUpdateMailSendResult_exception() { + // mock 数据 + MailLogDO log = randomPojo(MailLogDO.class, o -> { + o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); + o.setSendTime(null).setSendMessageId(null).setSendException(null) + .setTemplateParams(randomTemplateParams()); + }); + mailLogMapper.insert(log); + // 准备参数 + Long logId = log.getId(); + Exception exception = new NullPointerException("测试异常"); + + // 调用 + mailLogService.updateMailSendResult(logId, null, exception); + // 断言 + MailLogDO dbLog = mailLogMapper.selectById(logId); + assertEquals(MailSendStatusEnum.FAILURE.getStatus(), dbLog.getSendStatus()); + assertNotNull(dbLog.getSendTime()); + assertNull(dbLog.getSendMessageId()); + assertEquals("NullPointerException: 测试异常", dbLog.getSendException()); + } + + @Test + public void testGetMailLog() { + // mock 数据 + MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> o.setTemplateParams(randomTemplateParams())); + mailLogMapper.insert(dbMailLog); + // 准备参数 + Long id = dbMailLog.getId(); + + // 调用 + MailLogDO mailLog = mailLogService.getMailLog(id); + // 断言 + assertPojoEquals(dbMailLog, mailLog); + } + + @Test + public void testGetMailLogPage() { + // mock 数据 + MailLogDO dbMailLog = randomPojo(MailLogDO.class, o -> { // 等会查询到 + o.setUserId(1L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setToMail("768@qq.com"); + o.setAccountId(10L); + o.setTemplateId(100L); + o.setSendStatus(MailSendStatusEnum.INIT.getStatus()); + o.setSendTime(buildTime(2023, 2, 10)); + o.setTemplateParams(randomTemplateParams()); + }); + mailLogMapper.insert(dbMailLog); + // 测试 userId 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserId(2L))); + // 测试 userType 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 toMail 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setToMail("788@.qq.com"))); + // 测试 accountId 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setAccountId(11L))); + // 测试 templateId 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setTemplateId(101L))); + // 测试 sendStatus 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setSendStatus(MailSendStatusEnum.SUCCESS.getStatus()))); + // 测试 sendTime 不匹配 + mailLogMapper.insert(cloneIgnoreId(dbMailLog, o -> o.setSendTime(buildTime(2023, 3, 10)))); + // 准备参数 + MailLogPageReqVO reqVO = new MailLogPageReqVO(); + reqVO.setUserId(1L); + reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); + reqVO.setToMail("768"); + reqVO.setAccountId(10L); + reqVO.setTemplateId(100L); + reqVO.setSendStatus(MailSendStatusEnum.INIT.getStatus()); + reqVO.setSendTime((buildBetweenTime(2023, 2, 1, 2023, 2, 15))); + + // 调用 + PageResult pageResult = mailLogService.getMailLogPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbMailLog, pageResult.getList().get(0)); + } + + private static Map randomTemplateParams() { + return MapUtil.builder().put(randomString(), randomString()) + .put(randomString(), randomString()).build(); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailSendServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailSendServiceImplTest.java new file mode 100644 index 0000000..317e63f --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailSendServiceImplTest.java @@ -0,0 +1,332 @@ +package com.yunxi.scm.module.system.service.mail; + +import cn.hutool.core.map.MapUtil; +import cn.hutool.extra.mail.MailAccount; +import cn.hutool.extra.mail.MailUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.framework.test.core.util.RandomUtils; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailAccountDO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.mq.message.mail.MailSendMessage; +import com.yunxi.scm.module.system.mq.producer.mail.MailProducer; +import com.yunxi.scm.module.system.service.member.MemberService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.HashMap; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class MailSendServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private MailSendServiceImpl mailSendService; + + @Mock + private AdminUserService adminUserService; + @Mock + private MemberService memberService; + @Mock + private MailAccountService mailAccountService; + @Mock + private MailTemplateService mailTemplateService; + @Mock + private MailLogService mailLogService; + @Mock + private MailProducer mailProducer; + + /** + * 用于快速测试你的邮箱账号是否正常 + */ + @Test + @Disabled + public void testDemo() { + MailAccount mailAccount = new MailAccount() +// .setFrom("奥特曼 ") + .setFrom("ydym_test@163.com") // 邮箱地址 + .setHost("smtp.163.com").setPort(465).setSslEnable(true) // SMTP 服务器 + .setAuth(true).setUser("ydym_test@163.com").setPass("WBZTEINMIFVRYSOE"); // 登录账号密码 + String messageId = MailUtil.send(mailAccount, "7685413@qq.com", "主题", "内容", false); + System.out.println("发送结果:" + messageId); + } + + @Test + public void testSendSingleMailToAdmin() { + // 准备参数 + Long userId = randomLongId(); + String templateCode = RandomUtils.randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock adminUserService 的方法 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setMobile("15601691300")); + when(adminUserService.getUser(eq(userId))).thenReturn(user); + + // mock MailTemplateService 的方法 + MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String title = RandomUtils.randomString(); + when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) + .thenReturn(title); + String content = RandomUtils.randomString(); + when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock MailAccountService 的方法 + MailAccountDO account = randomPojo(MailAccountDO.class); + when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); + // mock MailLogService 的方法 + Long mailLogId = randomLongId(); + when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(user.getEmail()), + eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); + + // 调用 + Long resultMailLogId = mailSendService.sendSingleMailToAdmin(null, userId, templateCode, templateParams); + // 断言 + assertEquals(mailLogId, resultMailLogId); + // 断言调用 + verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(user.getEmail()), + eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); + } + + @Test + public void testSendSingleMailToMember() { + // 准备参数 + Long userId = randomLongId(); + String templateCode = RandomUtils.randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock memberService 的方法 + String mail = randomEmail(); + when(memberService.getMemberUserEmail(eq(userId))).thenReturn(mail); + + // mock MailTemplateService 的方法 + MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String title = RandomUtils.randomString(); + when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) + .thenReturn(title); + String content = RandomUtils.randomString(); + when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock MailAccountService 的方法 + MailAccountDO account = randomPojo(MailAccountDO.class); + when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); + // mock MailLogService 的方法 + Long mailLogId = randomLongId(); + when(mailLogService.createMailLog(eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(mail), + eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); + + // 调用 + Long resultMailLogId = mailSendService.sendSingleMailToMember(null, userId, templateCode, templateParams); + // 断言 + assertEquals(mailLogId, resultMailLogId); + // 断言调用 + verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail), + eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); + } + + /** + * 发送成功,当短信模板开启时 + */ + @Test + public void testSendSingleMail_successWhenMailTemplateEnable() { + // 准备参数 + String mail = randomEmail(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = RandomUtils.randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock MailTemplateService 的方法 + MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String title = RandomUtils.randomString(); + when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) + .thenReturn(title); + String content = RandomUtils.randomString(); + when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock MailAccountService 的方法 + MailAccountDO account = randomPojo(MailAccountDO.class); + when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); + // mock MailLogService 的方法 + Long mailLogId = randomLongId(); + when(mailLogService.createMailLog(eq(userId), eq(userType), eq(mail), + eq(account), eq(template), eq(content), eq(templateParams), eq(true))).thenReturn(mailLogId); + + // 调用 + Long resultMailLogId = mailSendService.sendSingleMail(mail, userId, userType, templateCode, templateParams); + // 断言 + assertEquals(mailLogId, resultMailLogId); + // 断言调用 + verify(mailProducer).sendMailSendMessage(eq(mailLogId), eq(mail), + eq(account.getId()), eq(template.getNickname()), eq(title), eq(content)); + } + + /** + * 发送成功,当短信模板关闭时 + */ + @Test + public void testSendSingleMail_successWhenSmsTemplateDisable() { + // 准备参数 + String mail = randomEmail(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = RandomUtils.randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock MailTemplateService 的方法 + MailTemplateDO template = randomPojo(MailTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(mailTemplateService.getMailTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String title = RandomUtils.randomString(); + when(mailTemplateService.formatMailTemplateContent(eq(template.getTitle()), eq(templateParams))) + .thenReturn(title); + String content = RandomUtils.randomString(); + when(mailTemplateService.formatMailTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock MailAccountService 的方法 + MailAccountDO account = randomPojo(MailAccountDO.class); + when(mailAccountService.getMailAccountFromCache(eq(template.getAccountId()))).thenReturn(account); + // mock MailLogService 的方法 + Long mailLogId = randomLongId(); + when(mailLogService.createMailLog(eq(userId), eq(userType), eq(mail), + eq(account), eq(template), eq(content), eq(templateParams), eq(false))).thenReturn(mailLogId); + + // 调用 + Long resultMailLogId = mailSendService.sendSingleMail(mail, userId, userType, templateCode, templateParams); + // 断言 + assertEquals(mailLogId, resultMailLogId); + // 断言调用 + verify(mailProducer, times(0)).sendMailSendMessage(anyLong(), anyString(), + anyLong(), anyString(), anyString(), anyString()); + } + + @Test + public void testValidateMailTemplateValid_notExists() { + // 准备参数 + String templateCode = RandomUtils.randomString(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> mailSendService.validateMailTemplate(templateCode), + MAIL_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testValidateTemplateParams_paramMiss() { + // 准备参数 + MailTemplateDO template = randomPojo(MailTemplateDO.class, + o -> o.setParams(Lists.newArrayList("code"))); + Map templateParams = new HashMap<>(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> mailSendService.validateTemplateParams(template, templateParams), + MAIL_SEND_TEMPLATE_PARAM_MISS, "code"); + } + + @Test + public void testValidateMail_notExists() { + // 准备参数 + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> mailSendService.validateMail(null), + MAIL_SEND_MAIL_NOT_EXISTS); + } + + @Test + public void testDoSendMail_success() { + try (MockedStatic mailUtilMock = mockStatic(MailUtil.class)) { + // 准备参数 + MailSendMessage message = randomPojo(MailSendMessage.class, o -> o.setNickname("芋艿")); + // mock 方法(获得邮箱账号) + MailAccountDO account = randomPojo(MailAccountDO.class, o -> o.setMail("7685@qq.com")); + when(mailAccountService.getMailAccountFromCache(eq(message.getAccountId()))) + .thenReturn(account); + + // mock 方法(发送邮件) + String messageId = randomString(); + mailUtilMock.when(() -> MailUtil.send( + argThat(mailAccount -> { + assertEquals("芋艿 <7685@qq.com>", mailAccount.getFrom()); + assertTrue(mailAccount.isAuth()); + assertEquals(account.getUsername(), mailAccount.getUser()); + assertEquals(account.getPassword(), mailAccount.getPass()); + assertEquals(account.getHost(), mailAccount.getHost()); + assertEquals(account.getPort(), mailAccount.getPort()); + assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); + return true; + }), eq(message.getMail()), eq(message.getTitle()), eq(message.getContent()), eq(true))) + .thenReturn(messageId); + + // 调用 + mailSendService.doSendMail(message); + // 断言 + verify(mailLogService).updateMailSendResult(eq(message.getLogId()), eq(messageId), isNull()); + } + } + + @Test + public void testDoSendMail_exception() { + try (MockedStatic mailUtilMock = mockStatic(MailUtil.class)) { + // 准备参数 + MailSendMessage message = randomPojo(MailSendMessage.class, o -> o.setNickname("芋艿")); + // mock 方法(获得邮箱账号) + MailAccountDO account = randomPojo(MailAccountDO.class, o -> o.setMail("7685@qq.com")); + when(mailAccountService.getMailAccountFromCache(eq(message.getAccountId()))) + .thenReturn(account); + + // mock 方法(发送邮件) + Exception e = new NullPointerException("啦啦啦"); + mailUtilMock.when(() -> MailUtil.send(argThat(mailAccount -> { + assertEquals("芋艿 <7685@qq.com>", mailAccount.getFrom()); + assertTrue(mailAccount.isAuth()); + assertEquals(account.getUsername(), mailAccount.getUser()); + assertEquals(account.getPassword(), mailAccount.getPass()); + assertEquals(account.getHost(), mailAccount.getHost()); + assertEquals(account.getPort(), mailAccount.getPort()); + assertEquals(account.getSslEnable(), mailAccount.isSslEnable()); + return true; + }), eq(message.getMail()), eq(message.getTitle()), eq(message.getContent()), eq(true))) + .thenThrow(e); + + // 调用 + mailSendService.doSendMail(message); + // 断言 + verify(mailLogService).updateMailSendResult(eq(message.getLogId()), isNull(), same(e)); + } + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailTemplateServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailTemplateServiceImplTest.java new file mode 100644 index 0000000..1c605f9 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/mail/MailTemplateServiceImplTest.java @@ -0,0 +1,215 @@ +package com.yunxi.scm.module.system.service.mail; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.mail.vo.template.MailTemplateUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.mail.MailTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.mail.MailTemplateMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.MAIL_TEMPLATE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link MailTemplateServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(MailTemplateServiceImpl.class) +public class MailTemplateServiceImplTest extends BaseDbUnitTest { + + @Resource + private MailTemplateServiceImpl mailTemplateService; + + @Resource + private MailTemplateMapper mailTemplateMapper; + + @Test + public void testCreateMailTemplate_success() { + // 准备参数 + MailTemplateCreateReqVO reqVO = randomPojo(MailTemplateCreateReqVO.class); + + // 调用 + Long mailTemplateId = mailTemplateService.createMailTemplate(reqVO); + // 断言 + assertNotNull(mailTemplateId); + // 校验记录的属性是否正确 + MailTemplateDO mailTemplate = mailTemplateMapper.selectById(mailTemplateId); + assertPojoEquals(reqVO, mailTemplate); + } + + @Test + public void testUpdateMailTemplate_success() { + // mock 数据 + MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); + mailTemplateMapper.insert(dbMailTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + MailTemplateUpdateReqVO reqVO = randomPojo(MailTemplateUpdateReqVO.class, o -> { + o.setId(dbMailTemplate.getId()); // 设置更新的 ID + }); + + // 调用 + mailTemplateService.updateMailTemplate(reqVO); + // 校验是否更新正确 + MailTemplateDO mailTemplate = mailTemplateMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, mailTemplate); + } + + @Test + public void testUpdateMailTemplate_notExists() { + // 准备参数 + MailTemplateUpdateReqVO reqVO = randomPojo(MailTemplateUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> mailTemplateService.updateMailTemplate(reqVO), MAIL_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testDeleteMailTemplate_success() { + // mock 数据 + MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); + mailTemplateMapper.insert(dbMailTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbMailTemplate.getId(); + + // 调用 + mailTemplateService.deleteMailTemplate(id); + // 校验数据不存在了 + assertNull(mailTemplateMapper.selectById(id)); + } + + @Test + public void testDeleteMailTemplate_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> mailTemplateService.deleteMailTemplate(id), MAIL_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testGetMailTemplatePage() { + // mock 数据 + MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class, o -> { // 等会查询到 + o.setName("源码"); + o.setCode("test_01"); + o.setAccountId(1L); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2023, 2, 3)); + }); + mailTemplateMapper.insert(dbMailTemplate); + // 测试 name 不匹配 + mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setName("芋道"))); + // 测试 code 不匹配 + mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setCode("test_02"))); + // 测试 accountId 不匹配 + mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setAccountId(2L))); + // 测试 status 不匹配 + mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setCreateTime(buildTime(2023, 1, 5)))); + // 准备参数 + MailTemplatePageReqVO reqVO = new MailTemplatePageReqVO(); + reqVO.setName("源"); + reqVO.setCode("est_01"); + reqVO.setAccountId(1L); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 5)); + + // 调用 + PageResult pageResult = mailTemplateService.getMailTemplatePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbMailTemplate, pageResult.getList().get(0)); + } + + @Test + public void testGetMailTemplateList() { + // mock 数据 + MailTemplateDO dbMailTemplate01 = randomPojo(MailTemplateDO.class); + mailTemplateMapper.insert(dbMailTemplate01); + MailTemplateDO dbMailTemplate02 = randomPojo(MailTemplateDO.class); + mailTemplateMapper.insert(dbMailTemplate02); + + // 调用 + List list = mailTemplateService.getMailTemplateList(); + // 断言 + assertEquals(2, list.size()); + assertEquals(dbMailTemplate01, list.get(0)); + assertEquals(dbMailTemplate02, list.get(1)); + } + + @Test + public void testGetMailTemplate() { + // mock 数据 + MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); + mailTemplateMapper.insert(dbMailTemplate); + // 准备参数 + Long id = dbMailTemplate.getId(); + + // 调用 + MailTemplateDO mailTemplate = mailTemplateService.getMailTemplate(id); + // 断言 + assertPojoEquals(dbMailTemplate, mailTemplate); + } + + @Test + public void testGetMailTemplateByCodeFromCache() { + // mock 数据 + MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); + mailTemplateMapper.insert(dbMailTemplate); + // 准备参数 + String code = dbMailTemplate.getCode(); + + // 调用 + MailTemplateDO mailTemplate = mailTemplateService.getMailTemplateByCodeFromCache(code); + // 断言 + assertPojoEquals(dbMailTemplate, mailTemplate); + } + + @Test + public void testFormatMailTemplateContent() { + // 准备参数 + Map params = new HashMap<>(); + params.put("name", "小红"); + params.put("what", "饭"); + + // 调用,并断言 + assertEquals("小红,你好,饭吃了吗?", + mailTemplateService.formatMailTemplateContent("{name},你好,{what}吃了吗?", params)); + } + + @Test + public void testCountByAccountId() { + // mock 数据 + MailTemplateDO dbMailTemplate = randomPojo(MailTemplateDO.class); + mailTemplateMapper.insert(dbMailTemplate); + // 测试 accountId 不匹配 + mailTemplateMapper.insert(cloneIgnoreId(dbMailTemplate, o -> o.setAccountId(2L))); + // 准备参数 + Long accountId = dbMailTemplate.getAccountId(); + + // 调用 + long count = mailTemplateService.countByAccountId(accountId); + // 断言 + assertEquals(1, count); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notice/NoticeServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notice/NoticeServiceImplTest.java new file mode 100644 index 0000000..5b83929 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notice/NoticeServiceImplTest.java @@ -0,0 +1,130 @@ +package com.yunxi.scm.module.system.service.notice; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticePageReqVO; +import com.yunxi.scm.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notice.NoticeDO; +import com.yunxi.scm.module.system.dal.mysql.notice.NoticeMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; +import static org.junit.jupiter.api.Assertions.*; + +@Import(NoticeServiceImpl.class) +class NoticeServiceImplTest extends BaseDbUnitTest { + + @Resource + private NoticeServiceImpl noticeService; + + @Resource + private NoticeMapper noticeMapper; + + @Test + public void testGetNoticePage_success() { + // 插入前置数据 + NoticeDO dbNotice = randomPojo(NoticeDO.class, o -> { + o.setTitle("尼古拉斯赵四来啦!"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + noticeMapper.insert(dbNotice); + // 测试 title 不匹配 + noticeMapper.insert(cloneIgnoreId(dbNotice, o -> o.setTitle("尼古拉斯凯奇也来啦!"))); + // 测试 status 不匹配 + noticeMapper.insert(cloneIgnoreId(dbNotice, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + NoticePageReqVO reqVO = new NoticePageReqVO(); + reqVO.setTitle("尼古拉斯赵四来啦!"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + PageResult pageResult = noticeService.getNoticePage(reqVO); + // 验证查询结果经过筛选 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbNotice, pageResult.getList().get(0)); + } + + @Test + public void testGetNotice_success() { + // 插入前置数据 + NoticeDO dbNotice = randomPojo(NoticeDO.class); + noticeMapper.insert(dbNotice); + + // 查询 + NoticeDO notice = noticeService.getNotice(dbNotice.getId()); + + // 验证插入与读取对象是否一致 + assertNotNull(notice); + assertPojoEquals(dbNotice, notice); + } + + @Test + public void testCreateNotice_success() { + // 准备参数 + NoticeCreateReqVO reqVO = randomPojo(NoticeCreateReqVO.class); + + // 调用 + Long noticeId = noticeService.createNotice(reqVO); + // 校验插入属性是否正确 + assertNotNull(noticeId); + NoticeDO notice = noticeMapper.selectById(noticeId); + assertPojoEquals(reqVO, notice); + } + + @Test + public void testUpdateNotice_success() { + // 插入前置数据 + NoticeDO dbNoticeDO = randomPojo(NoticeDO.class); + noticeMapper.insert(dbNoticeDO); + + // 准备更新参数 + NoticeUpdateReqVO reqVO = randomPojo(NoticeUpdateReqVO.class, o -> o.setId(dbNoticeDO.getId())); + + // 更新 + noticeService.updateNotice(reqVO); + // 检验是否更新成功 + NoticeDO notice = noticeMapper.selectById(reqVO.getId()); + assertPojoEquals(reqVO, notice); + } + + @Test + public void testDeleteNotice_success() { + // 插入前置数据 + NoticeDO dbNotice = randomPojo(NoticeDO.class); + noticeMapper.insert(dbNotice); + + // 删除 + noticeService.deleteNotice(dbNotice.getId()); + + // 检查是否删除成功 + assertNull(noticeMapper.selectById(dbNotice.getId())); + } + + @Test + public void testValidateNoticeExists_success() { + // 插入前置数据 + NoticeDO dbNotice = randomPojo(NoticeDO.class); + noticeMapper.insert(dbNotice); + + // 成功调用 + noticeService.validateNoticeExists(dbNotice.getId()); + } + + @Test + public void testValidateNoticeExists_noExists() { + assertServiceException(() -> + noticeService.validateNoticeExists(randomLongId()), NOTICE_NOT_FOUND); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifyMessageServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifyMessageServiceImplTest.java new file mode 100644 index 0000000..1560931 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifyMessageServiceImplTest.java @@ -0,0 +1,280 @@ +package com.yunxi.scm.module.system.service.notify; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.mybatis.core.enums.SqlConstants; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessageMyPageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.message.NotifyMessagePageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyMessageDO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.notify.NotifyMessageMapper; +import com.baomidou.mybatisplus.annotation.DbType; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; + +/** +* {@link NotifyMessageServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(NotifyMessageServiceImpl.class) +public class NotifyMessageServiceImplTest extends BaseDbUnitTest { + + @Resource + private NotifyMessageServiceImpl notifyMessageService; + + @Resource + private NotifyMessageMapper notifyMessageMapper; + + @Test + public void testCreateNotifyMessage_success() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class); + String templateContent = randomString(); + Map templateParams = randomTemplateParams(); + // mock 方法 + + // 调用 + Long messageId = notifyMessageService.createNotifyMessage(userId, userType, + template, templateContent, templateParams); + // 断言 + NotifyMessageDO message = notifyMessageMapper.selectById(messageId); + assertNotNull(message); + assertEquals(userId, message.getUserId()); + assertEquals(userType, message.getUserType()); + assertEquals(template.getId(), message.getTemplateId()); + assertEquals(template.getCode(), message.getTemplateCode()); + assertEquals(template.getType(), message.getTemplateType()); + assertEquals(template.getNickname(), message.getTemplateNickname()); + assertEquals(templateContent, message.getTemplateContent()); + assertEquals(templateParams, message.getTemplateParams()); + assertEquals(false, message.getReadStatus()); + assertNull(message.getReadTime()); + } + + @Test + public void testGetNotifyMessagePage() { + // mock 数据 + NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 + o.setUserId(1L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setTemplateCode("test_01"); + o.setTemplateType(10); + o.setCreateTime(buildTime(2022, 1, 2)); + o.setTemplateParams(randomTemplateParams()); + }); + notifyMessageMapper.insert(dbNotifyMessage); + // 测试 userId 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); + // 测试 userType 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 templateCode 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setTemplateCode("test_11"))); + // 测试 templateType 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setTemplateType(20))); + // 测试 createTime 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setCreateTime(buildTime(2022, 2, 1)))); + // 准备参数 + NotifyMessagePageReqVO reqVO = new NotifyMessagePageReqVO(); + reqVO.setUserId(1L); + reqVO.setUserType(UserTypeEnum.ADMIN.getValue()); + reqVO.setTemplateCode("est_01"); + reqVO.setTemplateType(10); + reqVO.setCreateTime(buildBetweenTime(2022, 1, 1, 2022, 1, 10)); + + // 调用 + PageResult pageResult = notifyMessageService.getNotifyMessagePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbNotifyMessage, pageResult.getList().get(0)); + } + + @Test + public void testGetNotifyMessage() { + // mock 数据 + NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, + o -> o.setTemplateParams(randomTemplateParams())); + notifyMessageMapper.insert(dbNotifyMessage); + // 准备参数 + Long id = dbNotifyMessage.getId(); + + // 调用 + NotifyMessageDO notifyMessage = notifyMessageService.getNotifyMessage(id); + assertPojoEquals(dbNotifyMessage, notifyMessage); + } + + @Test + public void testGetMyNotifyMessagePage() { + // mock 数据 + NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 + o.setUserId(1L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setReadStatus(true); + o.setCreateTime(buildTime(2022, 1, 2)); + o.setTemplateParams(randomTemplateParams()); + }); + notifyMessageMapper.insert(dbNotifyMessage); + // 测试 userId 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); + // 测试 userType 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 readStatus 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(false))); + // 测试 createTime 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setCreateTime(buildTime(2022, 2, 1)))); + // 准备参数 + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + NotifyMessageMyPageReqVO reqVO = new NotifyMessageMyPageReqVO(); + reqVO.setReadStatus(true); + reqVO.setCreateTime(buildBetweenTime(2022, 1, 1, 2022, 1, 10)); + + // 调用 + PageResult pageResult = notifyMessageService.getMyMyNotifyMessagePage(reqVO, userId, userType); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbNotifyMessage, pageResult.getList().get(0)); + } + + @Test + public void testGetUnreadNotifyMessageList() { + SqlConstants.init(DbType.MYSQL); + // mock 数据 + NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 + o.setUserId(1L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setReadStatus(false); + o.setTemplateParams(randomTemplateParams()); + }); + notifyMessageMapper.insert(dbNotifyMessage); + // 测试 userId 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); + // 测试 userType 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 readStatus 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(true))); + // 准备参数 + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + Integer size = 10; + + // 调用 + List list = notifyMessageService.getUnreadNotifyMessageList(userId, userType, size); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbNotifyMessage, list.get(0)); + } + + @Test + public void testGetUnreadNotifyMessageCount() { + SqlConstants.init(DbType.MYSQL); + // mock 数据 + NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 + o.setUserId(1L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setReadStatus(false); + o.setTemplateParams(randomTemplateParams()); + }); + notifyMessageMapper.insert(dbNotifyMessage); + // 测试 userId 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); + // 测试 userType 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 readStatus 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(true))); + // 准备参数 + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + + // 调用,并断言 + assertEquals(1, notifyMessageService.getUnreadNotifyMessageCount(userId, userType)); + } + + @Test + public void testUpdateNotifyMessageRead() { + // mock 数据 + NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 + o.setUserId(1L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setReadStatus(false); + o.setReadTime(null); + o.setTemplateParams(randomTemplateParams()); + }); + notifyMessageMapper.insert(dbNotifyMessage); + // 测试 userId 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); + // 测试 userType 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 readStatus 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(true))); + // 准备参数 + Collection ids = Arrays.asList(dbNotifyMessage.getId(), dbNotifyMessage.getId() + 1, + dbNotifyMessage.getId() + 2, dbNotifyMessage.getId() + 3); + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + + // 调用 + int updateCount = notifyMessageService.updateNotifyMessageRead(ids, userId, userType); + // 断言 + assertEquals(1, updateCount); + NotifyMessageDO notifyMessage = notifyMessageMapper.selectById(dbNotifyMessage.getId()); + assertTrue(notifyMessage.getReadStatus()); + assertNotNull(notifyMessage.getReadTime()); + } + + @Test + public void testUpdateAllNotifyMessageRead() { + // mock 数据 + NotifyMessageDO dbNotifyMessage = randomPojo(NotifyMessageDO.class, o -> { // 等会查询到 + o.setUserId(1L); + o.setUserType(UserTypeEnum.ADMIN.getValue()); + o.setReadStatus(false); + o.setReadTime(null); + o.setTemplateParams(randomTemplateParams()); + }); + notifyMessageMapper.insert(dbNotifyMessage); + // 测试 userId 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserId(2L))); + // 测试 userType 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setUserType(UserTypeEnum.MEMBER.getValue()))); + // 测试 readStatus 不匹配 + notifyMessageMapper.insert(cloneIgnoreId(dbNotifyMessage, o -> o.setReadStatus(true))); + // 准备参数 + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + + // 调用 + int updateCount = notifyMessageService.updateAllNotifyMessageRead(userId, userType); + // 断言 + assertEquals(1, updateCount); + NotifyMessageDO notifyMessage = notifyMessageMapper.selectById(dbNotifyMessage.getId()); + assertTrue(notifyMessage.getReadStatus()); + assertNotNull(notifyMessage.getReadTime()); + } + + private static Map randomTemplateParams() { + return MapUtil.builder().put(randomString(), randomString()) + .put(randomString(), randomString()).build(); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifySendServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifySendServiceImplTest.java new file mode 100644 index 0000000..1cd6a3a --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifySendServiceImplTest.java @@ -0,0 +1,176 @@ +package com.yunxi.scm.module.system.service.notify; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.NOTICE_NOT_FOUND; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.NOTIFY_SEND_TEMPLATE_PARAM_MISS; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class NotifySendServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private NotifySendServiceImpl notifySendService; + + @Mock + private NotifyTemplateService notifyTemplateService; + @Mock + private NotifyMessageService notifyMessageService; + + @Test + public void testSendSingleNotifyToAdmin() { + // 准备参数 + Long userId = randomLongId(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock NotifyTemplateService 的方法 + NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(notifyTemplateService.formatNotifyTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock NotifyMessageService 的方法 + Long messageId = randomLongId(); + when(notifyMessageService.createNotifyMessage(eq(userId), eq(UserTypeEnum.ADMIN.getValue()), + eq(template), eq(content), eq(templateParams))).thenReturn(messageId); + + // 调用 + Long resultMessageId = notifySendService.sendSingleNotifyToAdmin(userId, templateCode, templateParams); + // 断言 + assertEquals(messageId, resultMessageId); + } + + @Test + public void testSendSingleNotifyToMember() { + // 准备参数 + Long userId = randomLongId(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock NotifyTemplateService 的方法 + NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(notifyTemplateService.formatNotifyTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock NotifyMessageService 的方法 + Long messageId = randomLongId(); + when(notifyMessageService.createNotifyMessage(eq(userId), eq(UserTypeEnum.MEMBER.getValue()), + eq(template), eq(content), eq(templateParams))).thenReturn(messageId); + + // 调用 + Long resultMessageId = notifySendService.sendSingleNotifyToMember(userId, templateCode, templateParams); + // 断言 + assertEquals(messageId, resultMessageId); + } + + /** + * 发送成功,当短信模板开启时 + */ + @Test + public void testSendSingleNotify_successWhenMailTemplateEnable() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock NotifyTemplateService 的方法 + NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(notifyTemplateService.formatNotifyTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock NotifyMessageService 的方法 + Long messageId = randomLongId(); + when(notifyMessageService.createNotifyMessage(eq(userId), eq(userType), + eq(template), eq(content), eq(templateParams))).thenReturn(messageId); + + // 调用 + Long resultMessageId = notifySendService.sendSingleNotify(userId, userType, templateCode, templateParams); + // 断言 + assertEquals(messageId, resultMessageId); + } + + /** + * 发送成功,当短信模板关闭时 + */ + @Test + public void testSendSingleMail_successWhenSmsTemplateDisable() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock NotifyTemplateService 的方法 + NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(notifyTemplateService.getNotifyTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + + // 调用 + Long resultMessageId = notifySendService.sendSingleNotify(userId, userType, templateCode, templateParams); + // 断言 + assertNull(resultMessageId); + verify(notifyTemplateService, never()).formatNotifyTemplateContent(anyString(), anyMap()); + verify(notifyMessageService, never()).createNotifyMessage(anyLong(), anyInt(), any(), anyString(), anyMap()); + } + + @Test + public void testCheckMailTemplateValid_notExists() { + // 准备参数 + String templateCode = randomString(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> notifySendService.validateNotifyTemplate(templateCode), + NOTICE_NOT_FOUND); + } + + @Test + public void testCheckTemplateParams_paramMiss() { + // 准备参数 + NotifyTemplateDO template = randomPojo(NotifyTemplateDO.class, + o -> o.setParams(Lists.newArrayList("code"))); + Map templateParams = new HashMap<>(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> notifySendService.validateTemplateParams(template, templateParams), + NOTIFY_SEND_TEMPLATE_PARAM_MISS, "code"); + } + + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateServiceImplTest.java new file mode 100644 index 0000000..6fd0683 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/notify/NotifyTemplateServiceImplTest.java @@ -0,0 +1,178 @@ +package com.yunxi.scm.module.system.service.notify; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.notify.vo.template.NotifyTemplateUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.notify.NotifyTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.notify.NotifyTemplateMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.HashMap; +import java.util.Map; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.NOTIFY_TEMPLATE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link NotifyTemplateServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(NotifyTemplateServiceImpl.class) +public class NotifyTemplateServiceImplTest extends BaseDbUnitTest { + + @Resource + private NotifyTemplateServiceImpl notifyTemplateService; + + @Resource + private NotifyTemplateMapper notifyTemplateMapper; + + @Test + public void testCreateNotifyTemplate_success() { + // 准备参数 + NotifyTemplateCreateReqVO reqVO = randomPojo(NotifyTemplateCreateReqVO.class, + o -> o.setStatus(randomCommonStatus())); + + // 调用 + Long notifyTemplateId = notifyTemplateService.createNotifyTemplate(reqVO); + // 断言 + assertNotNull(notifyTemplateId); + // 校验记录的属性是否正确 + NotifyTemplateDO notifyTemplate = notifyTemplateMapper.selectById(notifyTemplateId); + assertPojoEquals(reqVO, notifyTemplate); + } + + @Test + public void testUpdateNotifyTemplate_success() { + // mock 数据 + NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class); + notifyTemplateMapper.insert(dbNotifyTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + NotifyTemplateUpdateReqVO reqVO = randomPojo(NotifyTemplateUpdateReqVO.class, o -> { + o.setId(dbNotifyTemplate.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + }); + + // 调用 + notifyTemplateService.updateNotifyTemplate(reqVO); + // 校验是否更新正确 + NotifyTemplateDO notifyTemplate = notifyTemplateMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, notifyTemplate); + } + + @Test + public void testUpdateNotifyTemplate_notExists() { + // 准备参数 + NotifyTemplateUpdateReqVO reqVO = randomPojo(NotifyTemplateUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> notifyTemplateService.updateNotifyTemplate(reqVO), NOTIFY_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testDeleteNotifyTemplate_success() { + // mock 数据 + NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class); + notifyTemplateMapper.insert(dbNotifyTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbNotifyTemplate.getId(); + + // 调用 + notifyTemplateService.deleteNotifyTemplate(id); + // 校验数据不存在了 + assertNull(notifyTemplateMapper.selectById(id)); + } + + @Test + public void testDeleteNotifyTemplate_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> notifyTemplateService.deleteNotifyTemplate(id), NOTIFY_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testGetNotifyTemplatePage() { + // mock 数据 + NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class, o -> { // 等会查询到 + o.setName("芋头"); + o.setCode("test_01"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2022, 2, 3)); + }); + notifyTemplateMapper.insert(dbNotifyTemplate); + // 测试 name 不匹配 + notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, o -> o.setName("投"))); + // 测试 code 不匹配 + notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, o -> o.setCode("test_02"))); + // 测试 status 不匹配 + notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + notifyTemplateMapper.insert(cloneIgnoreId(dbNotifyTemplate, o -> o.setCreateTime(buildTime(2022, 1, 5)))); + // 准备参数 + NotifyTemplatePageReqVO reqVO = new NotifyTemplatePageReqVO(); + reqVO.setName("芋"); + reqVO.setCode("est_01"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2022, 2, 1, 2022, 2, 5)); + + // 调用 + PageResult pageResult = notifyTemplateService.getNotifyTemplatePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbNotifyTemplate, pageResult.getList().get(0)); + } + + @Test + public void testGetNotifyTemplate() { + // mock 数据 + NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class); + notifyTemplateMapper.insert(dbNotifyTemplate); + // 准备参数 + Long id = dbNotifyTemplate.getId(); + + // 调用 + NotifyTemplateDO notifyTemplate = notifyTemplateService.getNotifyTemplate(id); + // 断言 + assertPojoEquals(dbNotifyTemplate, notifyTemplate); + } + + @Test + public void testGetNotifyTemplateByCodeFromCache() { + // mock 数据 + NotifyTemplateDO dbNotifyTemplate = randomPojo(NotifyTemplateDO.class); + notifyTemplateMapper.insert(dbNotifyTemplate); + // 准备参数 + String code = dbNotifyTemplate.getCode(); + + // 调用 + NotifyTemplateDO notifyTemplate = notifyTemplateService.getNotifyTemplateByCodeFromCache(code); + // 断言 + assertPojoEquals(dbNotifyTemplate, notifyTemplate); + } + + @Test + public void testFormatNotifyTemplateContent() { + // 准备参数 + Map params = new HashMap<>(); + params.put("name", "小红"); + params.put("what", "饭"); + + // 调用,并断言 + assertEquals("小红,你好,饭吃了吗?", + notifyTemplateService.formatNotifyTemplateContent("{name},你好,{what}吃了吗?", params)); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java new file mode 100644 index 0000000..59102f4 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ApproveServiceImplTest.java @@ -0,0 +1,269 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.ObjectUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ApproveDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2ApproveMapper; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.*; + +import static cn.hutool.core.util.RandomUtil.*; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomString; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link OAuth2ApproveServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(OAuth2ApproveServiceImpl.class) +public class OAuth2ApproveServiceImplTest extends BaseDbUnitTest { + + @Resource + private OAuth2ApproveServiceImpl oauth2ApproveService; + + @Resource + private OAuth2ApproveMapper oauth2ApproveMapper; + + @MockBean + private OAuth2ClientService oauth2ClientService; + + @Test + public void checkForPreApproval_clientAutoApprove() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List requestedScopes = Lists.newArrayList("read"); + // mock 方法 + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) + .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(requestedScopes)); + + // 调用 + boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, + clientId, requestedScopes); + // 断言 + assertTrue(success); + List result = oauth2ApproveMapper.selectList(); + assertEquals(1, result.size()); + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals("read", result.get(0).getScope()); + assertTrue(result.get(0).getApproved()); + assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); + } + + @Test + public void checkForPreApproval_approve() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List requestedScopes = Lists.newArrayList("read"); + // mock 方法 + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) + .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(null)); + // mock 数据 + OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) + .setUserType(userType).setClientId(clientId).setScope("read") + .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 1L, ChronoUnit.DAYS)).setApproved(true); // 同意 + oauth2ApproveMapper.insert(approve); + + // 调用 + boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, + clientId, requestedScopes); + // 断言 + assertTrue(success); + } + + @Test + public void checkForPreApproval_reject() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List requestedScopes = Lists.newArrayList("read"); + // mock 方法 + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))) + .thenReturn(randomPojo(OAuth2ClientDO.class).setAutoApproveScopes(null)); + // mock 数据 + OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) + .setUserType(userType).setClientId(clientId).setScope("read") + .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 1L, ChronoUnit.DAYS)).setApproved(false); // 拒绝 + oauth2ApproveMapper.insert(approve); + + // 调用 + boolean success = oauth2ApproveService.checkForPreApproval(userId, userType, + clientId, requestedScopes); + // 断言 + assertFalse(success); + } + + @Test + public void testUpdateAfterApproval_none() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + + // 调用 + boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, + null); + // 断言 + assertTrue(success); + List result = oauth2ApproveMapper.selectList(); + assertEquals(0, result.size()); + } + + @Test + public void testUpdateAfterApproval_approved() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + Map requestedScopes = new LinkedHashMap<>(); // 有序,方便判断 + requestedScopes.put("read", true); + requestedScopes.put("write", false); + // mock 方法 + + // 调用 + boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, + requestedScopes); + // 断言 + assertTrue(success); + List result = oauth2ApproveMapper.selectList(); + assertEquals(2, result.size()); + // read + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals("read", result.get(0).getScope()); + assertTrue(result.get(0).getApproved()); + assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); + // write + assertEquals(userId, result.get(1).getUserId()); + assertEquals(userType, result.get(1).getUserType()); + assertEquals(clientId, result.get(1).getClientId()); + assertEquals("write", result.get(1).getScope()); + assertFalse(result.get(1).getApproved()); + assertFalse(DateUtils.isExpired(result.get(1).getExpiresTime())); + } + + @Test + public void testUpdateAfterApproval_reject() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + Map requestedScopes = new LinkedHashMap<>(); + requestedScopes.put("write", false); + // mock 方法 + + // 调用 + boolean success = oauth2ApproveService.updateAfterApproval(userId, userType, clientId, + requestedScopes); + // 断言 + assertFalse(success); + List result = oauth2ApproveMapper.selectList(); + assertEquals(1, result.size()); + // write + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals("write", result.get(0).getScope()); + assertFalse(result.get(0).getApproved()); + assertFalse(DateUtils.isExpired(result.get(0).getExpiresTime())); + } + + @Test + public void testGetApproveList() { + // 准备参数 + Long userId = 10L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + String clientId = randomString(); + // mock 数据 + OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class).setUserId(userId) + .setUserType(userType).setClientId(clientId).setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), 1L, ChronoUnit.DAYS)); + oauth2ApproveMapper.insert(approve); // 未过期 + oauth2ApproveMapper.insert(ObjectUtil.clone(approve).setId(null) + .setExpiresTime(LocalDateTimeUtil.offset(LocalDateTime.now(), -1L, ChronoUnit.DAYS))); // 已过期 + + // 调用 + List result = oauth2ApproveService.getApproveList(userId, userType, clientId); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(approve, result.get(0)); + } + + @Test + public void testSaveApprove_insert() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + String scope = randomString(); + Boolean approved = randomBoolean(); + LocalDateTime expireTime = LocalDateTime.ofInstant(randomDay(1, 30).toInstant(), ZoneId.systemDefault()); + // mock 方法 + + // 调用 + oauth2ApproveService.saveApprove(userId, userType, clientId, + scope, approved, expireTime); + // 断言 + List result = oauth2ApproveMapper.selectList(); + assertEquals(1, result.size()); + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals(scope, result.get(0).getScope()); + assertEquals(approved, result.get(0).getApproved()); + assertEquals(expireTime, result.get(0).getExpiresTime()); + } + + @Test + public void testSaveApprove_update() { + // mock 数据 + OAuth2ApproveDO approve = randomPojo(OAuth2ApproveDO.class); + oauth2ApproveMapper.insert(approve); + // 准备参数 + Long userId = approve.getUserId(); + Integer userType = approve.getUserType(); + String clientId = approve.getClientId(); + String scope = approve.getScope(); + Boolean approved = randomBoolean(); + LocalDateTime expireTime = LocalDateTime.ofInstant(randomDay(1, 30).toInstant(), ZoneId.systemDefault()); + // mock 方法 + + // 调用 + oauth2ApproveService.saveApprove(userId, userType, clientId, + scope, approved, expireTime); + // 断言 + List result = oauth2ApproveMapper.selectList(); + assertEquals(1, result.size()); + assertEquals(approve.getId(), result.get(0).getId()); + assertEquals(userId, result.get(0).getUserId()); + assertEquals(userType, result.get(0).getUserType()); + assertEquals(clientId, result.get(0).getClientId()); + assertEquals(scope, result.get(0).getScope()); + assertEquals(approved, result.get(0).getApproved()); + assertEquals(expireTime, result.get(0).getExpiresTime()); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientServiceImplTest.java new file mode 100644 index 0000000..ecd30f1 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2ClientServiceImplTest.java @@ -0,0 +1,220 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientPageReqVO; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.client.OAuth2ClientUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2ClientMapper; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Collections; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; + +/** + * {@link OAuth2ClientServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(OAuth2ClientServiceImpl.class) +public class OAuth2ClientServiceImplTest extends BaseDbUnitTest { + + @Resource + private OAuth2ClientServiceImpl oauth2ClientService; + + @Resource + private OAuth2ClientMapper oauth2ClientMapper; + + @Test + public void testCreateOAuth2Client_success() { + // 准备参数 + OAuth2ClientCreateReqVO reqVO = randomPojo(OAuth2ClientCreateReqVO.class, + o -> o.setLogo(randomString())); + + // 调用 + Long oauth2ClientId = oauth2ClientService.createOAuth2Client(reqVO); + // 断言 + assertNotNull(oauth2ClientId); + // 校验记录的属性是否正确 + OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(oauth2ClientId); + assertPojoEquals(reqVO, oAuth2Client); + } + + @Test + public void testUpdateOAuth2Client_success() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); + oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 + // 准备参数 + OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class, o -> { + o.setId(dbOAuth2Client.getId()); // 设置更新的 ID + o.setLogo(randomString()); + }); + + // 调用 + oauth2ClientService.updateOAuth2Client(reqVO); + // 校验是否更新正确 + OAuth2ClientDO oAuth2Client = oauth2ClientMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, oAuth2Client); + } + + @Test + public void testUpdateOAuth2Client_notExists() { + // 准备参数 + OAuth2ClientUpdateReqVO reqVO = randomPojo(OAuth2ClientUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> oauth2ClientService.updateOAuth2Client(reqVO), OAUTH2_CLIENT_NOT_EXISTS); + } + + @Test + public void testDeleteOAuth2Client_success() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class); + oauth2ClientMapper.insert(dbOAuth2Client);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbOAuth2Client.getId(); + + // 调用 + oauth2ClientService.deleteOAuth2Client(id); + // 校验数据不存在了 + assertNull(oauth2ClientMapper.selectById(id)); + } + + @Test + public void testDeleteOAuth2Client_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> oauth2ClientService.deleteOAuth2Client(id), OAUTH2_CLIENT_NOT_EXISTS); + } + + @Test + public void testValidateClientIdExists_withId() { + // mock 数据 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("tudou"); + oauth2ClientMapper.insert(client); + // 准备参数 + Long id = randomLongId(); + String clientId = "tudou"; + + // 调用,不会报错 + assertServiceException(() -> oauth2ClientService.validateClientIdExists(id, clientId), OAUTH2_CLIENT_EXISTS); + } + + @Test + public void testValidateClientIdExists_noId() { + // mock 数据 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("tudou"); + oauth2ClientMapper.insert(client); + // 准备参数 + String clientId = "tudou"; + + // 调用,不会报错 + assertServiceException(() -> oauth2ClientService.validateClientIdExists(null, clientId), OAUTH2_CLIENT_EXISTS); + } + + @Test + public void testGetOAuth2Client() { + // mock 数据 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class); + oauth2ClientMapper.insert(clientDO); + // 准备参数 + Long id = clientDO.getId(); + + // 调用,并断言 + OAuth2ClientDO dbClientDO = oauth2ClientService.getOAuth2Client(id); + assertPojoEquals(clientDO, dbClientDO); + } + + @Test + public void testGetOAuth2ClientFromCache() { + // mock 数据 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class); + oauth2ClientMapper.insert(clientDO); + // 准备参数 + String clientId = clientDO.getClientId(); + + // 调用,并断言 + OAuth2ClientDO dbClientDO = oauth2ClientService.getOAuth2ClientFromCache(clientId); + assertPojoEquals(clientDO, dbClientDO); + } + + @Test + public void testGetOAuth2ClientPage() { + // mock 数据 + OAuth2ClientDO dbOAuth2Client = randomPojo(OAuth2ClientDO.class, o -> { // 等会查询到 + o.setName("潜龙"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + oauth2ClientMapper.insert(dbOAuth2Client); + // 测试 name 不匹配 + oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setName("凤凰"))); + // 测试 status 不匹配 + oauth2ClientMapper.insert(cloneIgnoreId(dbOAuth2Client, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 准备参数 + OAuth2ClientPageReqVO reqVO = new OAuth2ClientPageReqVO(); + reqVO.setName("龙"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + PageResult pageResult = oauth2ClientService.getOAuth2ClientPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbOAuth2Client, pageResult.getList().get(0)); + } + + @Test + public void testValidOAuthClientFromCache() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(OAuth2ClientServiceImpl.class))) + .thenReturn(oauth2ClientService); + + // mock 方法 + OAuth2ClientDO client = randomPojo(OAuth2ClientDO.class).setClientId("default") + .setStatus(CommonStatusEnum.ENABLE.getStatus()); + oauth2ClientMapper.insert(client); + OAuth2ClientDO client02 = randomPojo(OAuth2ClientDO.class).setClientId("disable") + .setStatus(CommonStatusEnum.DISABLE.getStatus()); + oauth2ClientMapper.insert(client02); + + // 调用,并断言 + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache(randomString(), + null, null, null, null), OAUTH2_CLIENT_NOT_EXISTS); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("disable", + null, null, null, null), OAUTH2_CLIENT_DISABLE); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", + randomString(), null, null, null), OAUTH2_CLIENT_CLIENT_SECRET_ERROR); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", + null, randomString(), null, null), OAUTH2_CLIENT_AUTHORIZED_GRANT_TYPE_NOT_EXISTS); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", + null, null, Collections.singleton(randomString()), null), OAUTH2_CLIENT_SCOPE_OVER); + assertServiceException(() -> oauth2ClientService.validOAuthClientFromCache("default", + null, null, null, "test"), OAUTH2_CLIENT_REDIRECT_URI_NOT_MATCH, "test"); + // 成功调用(1:参数完整) + OAuth2ClientDO result = oauth2ClientService.validOAuthClientFromCache(client.getClientId(), client.getSecret(), + client.getAuthorizedGrantTypes().get(0), client.getScopes(), client.getRedirectUris().get(0)); + assertPojoEquals(client, result); + // 成功调用(2:只有 clientId 参数) + result = oauth2ClientService.validOAuthClientFromCache(client.getClientId()); + assertPojoEquals(client, result); + } + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeServiceImplTest.java new file mode 100644 index 0000000..521b428 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2CodeServiceImplTest.java @@ -0,0 +1,99 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2CodeMapper; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_EXPIRE; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.OAUTH2_CODE_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link OAuth2CodeServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(OAuth2CodeServiceImpl.class) +class OAuth2CodeServiceImplTest extends BaseDbUnitTest { + + @Resource + private OAuth2CodeServiceImpl oauth2CodeService; + + @Resource + private OAuth2CodeMapper oauth2CodeMapper; + + @Test + public void testCreateAuthorizationCode() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = RandomUtil.randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + String redirectUri = randomString(); + String state = randomString(); + + // 调用 + OAuth2CodeDO codeDO = oauth2CodeService.createAuthorizationCode(userId, userType, clientId, + scopes, redirectUri, state); + // 断言 + OAuth2CodeDO dbCodeDO = oauth2CodeMapper.selectByCode(codeDO.getCode()); + assertPojoEquals(codeDO, dbCodeDO, "createTime", "updateTime", "deleted"); + assertEquals(userId, codeDO.getUserId()); + assertEquals(userType, codeDO.getUserType()); + assertEquals(clientId, codeDO.getClientId()); + assertEquals(scopes, codeDO.getScopes()); + assertEquals(redirectUri, codeDO.getRedirectUri()); + assertEquals(state, codeDO.getState()); + assertFalse(DateUtils.isExpired(codeDO.getExpiresTime())); + } + + @Test + public void testConsumeAuthorizationCode_null() { + // 调用,并断言 + assertServiceException(() -> oauth2CodeService.consumeAuthorizationCode(randomString()), + OAUTH2_CODE_NOT_EXISTS); + } + + @Test + public void testConsumeAuthorizationCode_expired() { + // 准备参数 + String code = "test_code"; + // mock 数据 + OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class).setCode(code) + .setExpiresTime(LocalDateTime.now().minusDays(1)); + oauth2CodeMapper.insert(codeDO); + + // 调用,并断言 + assertServiceException(() -> oauth2CodeService.consumeAuthorizationCode(code), + OAUTH2_CODE_EXPIRE); + } + + @Test + public void testConsumeAuthorizationCode_success() { + // 准备参数 + String code = "test_code"; + // mock 数据 + OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class).setCode(code) + .setExpiresTime(LocalDateTime.now().plusDays(1)); + oauth2CodeMapper.insert(codeDO); + + // 调用 + OAuth2CodeDO result = oauth2CodeService.consumeAuthorizationCode(code); + assertPojoEquals(codeDO, result); + assertNull(oauth2CodeMapper.selectByCode(code)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantServiceImplTest.java new file mode 100644 index 0000000..a26ebf6 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2GrantServiceImplTest.java @@ -0,0 +1,173 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2CodeDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.service.auth.AdminAuthService; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link OAuth2GrantServiceImpl} 的单元测试 + * + * @author 芋道源码 + */ +public class OAuth2GrantServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private OAuth2GrantServiceImpl oauth2GrantService; + + @Mock + private OAuth2TokenService oauth2TokenService; + @Mock + private OAuth2CodeService oauth2CodeService; + @Mock + private AdminAuthService adminAuthService; + + @Test + public void testGrantImplicit() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.createAccessToken(eq(userId), eq(userType), + eq(clientId), eq(scopes))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertPojoEquals(accessTokenDO, oauth2GrantService.grantImplicit( + userId, userType, clientId, scopes)); + } + + @Test + public void testGrantAuthorizationCodeForCode() { + // 准备参数 + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + String redirectUri = randomString(); + String state = randomString(); + // mock 方法 + OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class); + when(oauth2CodeService.createAuthorizationCode(eq(userId), eq(userType), + eq(clientId), eq(scopes), eq(redirectUri), eq(state))).thenReturn(codeDO); + + // 调用,并断言 + assertEquals(codeDO.getCode(), oauth2GrantService.grantAuthorizationCodeForCode(userId, userType, + clientId, scopes, redirectUri, state)); + } + + @Test + public void testGrantAuthorizationCodeForAccessToken() { + // 准备参数 + String clientId = randomString(); + String code = randomString(); + List scopes = Lists.newArrayList("read", "write"); + String redirectUri = randomString(); + String state = randomString(); + // mock 方法(code) + OAuth2CodeDO codeDO = randomPojo(OAuth2CodeDO.class, o -> { + o.setClientId(clientId); + o.setRedirectUri(redirectUri); + o.setState(state); + o.setScopes(scopes); + }); + when(oauth2CodeService.consumeAuthorizationCode(eq(code))).thenReturn(codeDO); + // mock 方法(创建令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.createAccessToken(eq(codeDO.getUserId()), eq(codeDO.getUserType()), + eq(codeDO.getClientId()), eq(codeDO.getScopes()))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertPojoEquals(accessTokenDO, oauth2GrantService.grantAuthorizationCodeForAccessToken( + clientId, code, redirectUri, state)); + } + + @Test + public void testGrantPassword() { + // 准备参数 + String username = randomString(); + String password = randomString(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + // mock 方法(认证) + AdminUserDO user = randomPojo(AdminUserDO.class); + when(adminAuthService.authenticate(eq(username), eq(password))).thenReturn(user); + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.createAccessToken(eq(user.getId()), eq(UserTypeEnum.ADMIN.getValue()), + eq(clientId), eq(scopes))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertPojoEquals(accessTokenDO, oauth2GrantService.grantPassword( + username, password, clientId, scopes)); + } + + @Test + public void testGrantRefreshToken() { + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.refreshAccessToken(eq(refreshToken), eq(clientId))) + .thenReturn(accessTokenDO); + + // 调用,并断言 + assertPojoEquals(accessTokenDO, oauth2GrantService.grantRefreshToken( + refreshToken, clientId)); + } + + @Test + public void testGrantClientCredentials() { + assertThrows(UnsupportedOperationException.class, + () -> oauth2GrantService.grantClientCredentials(randomString(), emptyList()), + "暂时不支持 client_credentials 授权模式"); + } + + @Test + public void testRevokeToken_clientIdError() { + // 准备参数 + String clientId = randomString(); + String accessToken = randomString(); + // mock 方法 + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class); + when(oauth2TokenService.getAccessToken(eq(accessToken))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertFalse(oauth2GrantService.revokeToken(clientId, accessToken)); + } + + @Test + public void testRevokeToken_success() { + // 准备参数 + String clientId = randomString(); + String accessToken = randomString(); + // mock 方法(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setClientId(clientId); + when(oauth2TokenService.getAccessToken(eq(accessToken))).thenReturn(accessTokenDO); + // mock 方法(移除) + when(oauth2TokenService.removeAccessToken(eq(accessToken))).thenReturn(accessTokenDO); + + // 调用,并断言 + assertTrue(oauth2GrantService.revokeToken(clientId, accessToken)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenServiceImplTest.java new file mode 100644 index 0000000..b6ad25b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/oauth2/OAuth2TokenServiceImplTest.java @@ -0,0 +1,289 @@ +package com.yunxi.scm.module.system.service.oauth2; + +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.exception.ErrorCode; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.date.DateUtils; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.test.core.ut.BaseDbAndRedisUnitTest; +import com.yunxi.scm.module.system.controller.admin.oauth2.vo.token.OAuth2AccessTokenPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2AccessTokenDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2ClientDO; +import com.yunxi.scm.module.system.dal.dataobject.oauth2.OAuth2RefreshTokenDO; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2AccessTokenMapper; +import com.yunxi.scm.module.system.dal.mysql.oauth2.OAuth2RefreshTokenMapper; +import com.yunxi.scm.module.system.dal.redis.oauth2.OAuth2AccessTokenRedisDAO; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +/** + * {@link OAuth2TokenServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import({OAuth2TokenServiceImpl.class, OAuth2AccessTokenRedisDAO.class}) +public class OAuth2TokenServiceImplTest extends BaseDbAndRedisUnitTest { + + @Resource + private OAuth2TokenServiceImpl oauth2TokenService; + + @Resource + private OAuth2AccessTokenMapper oauth2AccessTokenMapper; + @Resource + private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper; + + @Resource + private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO; + + @MockBean + private OAuth2ClientService oauth2ClientService; + + @Test + public void testCreateAccessToken() { + TenantContextHolder.setTenantId(0L); + // 准备参数 + Long userId = randomLongId(); + Integer userType = RandomUtil.randomEle(UserTypeEnum.values()).getValue(); + String clientId = randomString(); + List scopes = Lists.newArrayList("read", "write"); + // mock 方法 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId) + .setAccessTokenValiditySeconds(30).setRefreshTokenValiditySeconds(60); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); + + // 调用 + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.createAccessToken(userId, userType, clientId, scopes); + // 断言访问令牌 + OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken()); + assertPojoEquals(accessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted"); + assertEquals(userId, accessTokenDO.getUserId()); + assertEquals(userType, accessTokenDO.getUserType()); + assertEquals(clientId, accessTokenDO.getClientId()); + assertEquals(scopes, accessTokenDO.getScopes()); + assertFalse(DateUtils.isExpired(accessTokenDO.getExpiresTime())); + // 断言访问令牌的缓存 + OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken()); + assertPojoEquals(accessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted"); + // 断言刷新令牌 + OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectList().get(0); + assertPojoEquals(accessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted"); + assertFalse(DateUtils.isExpired(refreshTokenDO.getExpiresTime())); + } + + @Test + public void testRefreshAccessToken_null() { + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + + // 调用,并断言 + assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), + new ErrorCode(400, "无效的刷新令牌")); + } + + @Test + public void testRefreshAccessToken_clientIdError() { + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); + // mock 数据(访问令牌) + OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) + .setRefreshToken(refreshToken).setClientId("error"); + oauth2RefreshTokenMapper.insert(refreshTokenDO); + + // 调用,并断言 + assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), + new ErrorCode(400, "刷新令牌的客户端编号不正确")); + } + + @Test + public void testRefreshAccessToken_expired() { + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); + // mock 数据(访问令牌) + OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) + .setRefreshToken(refreshToken).setClientId(clientId) + .setExpiresTime(LocalDateTime.now().minusDays(1)); + oauth2RefreshTokenMapper.insert(refreshTokenDO); + + // 调用,并断言 + assertServiceException(() -> oauth2TokenService.refreshAccessToken(refreshToken, clientId), + new ErrorCode(401, "刷新令牌已过期")); + assertEquals(0, oauth2RefreshTokenMapper.selectCount()); + } + + @Test + public void testRefreshAccessToken_success() { + TenantContextHolder.setTenantId(0L); + // 准备参数 + String refreshToken = randomString(); + String clientId = randomString(); + // mock 方法 + OAuth2ClientDO clientDO = randomPojo(OAuth2ClientDO.class).setClientId(clientId) + .setAccessTokenValiditySeconds(30); + when(oauth2ClientService.validOAuthClientFromCache(eq(clientId))).thenReturn(clientDO); + // mock 数据(访问令牌) + OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) + .setRefreshToken(refreshToken).setClientId(clientId) + .setExpiresTime(LocalDateTime.now().plusDays(1)); + oauth2RefreshTokenMapper.insert(refreshTokenDO); + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class).setRefreshToken(refreshToken); + oauth2AccessTokenMapper.insert(accessTokenDO); + oauth2AccessTokenRedisDAO.set(accessTokenDO); + + // 调用 + OAuth2AccessTokenDO newAccessTokenDO = oauth2TokenService.refreshAccessToken(refreshToken, clientId); + // 断言,老的访问令牌被删除 + assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken())); + assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken())); + // 断言,新的访问令牌 + OAuth2AccessTokenDO dbAccessTokenDO = oauth2AccessTokenMapper.selectByAccessToken(newAccessTokenDO.getAccessToken()); + assertPojoEquals(newAccessTokenDO, dbAccessTokenDO, "createTime", "updateTime", "deleted"); + assertPojoEquals(newAccessTokenDO, refreshTokenDO, "id", "expiresTime", "createTime", "updateTime", "deleted", + "creator", "updater"); + assertFalse(DateUtils.isExpired(newAccessTokenDO.getExpiresTime())); + // 断言,新的访问令牌的缓存 + OAuth2AccessTokenDO redisAccessTokenDO = oauth2AccessTokenRedisDAO.get(newAccessTokenDO.getAccessToken()); + assertPojoEquals(newAccessTokenDO, redisAccessTokenDO, "createTime", "updateTime", "deleted"); + } + + @Test + public void testGetAccessToken() { + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(LocalDateTime.now().plusDays(1)); + oauth2AccessTokenMapper.insert(accessTokenDO); + // 准备参数 + String accessToken = accessTokenDO.getAccessToken(); + + // 调用 + OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); + // 断言 + assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + "creator", "updater"); + assertPojoEquals(accessTokenDO, oauth2AccessTokenRedisDAO.get(accessToken), "createTime", "updateTime", "deleted", + "creator", "updater"); + } + + @Test + public void testCheckAccessToken_null() { + // 调研,并断言 + assertServiceException(() -> oauth2TokenService.checkAccessToken(randomString()), + new ErrorCode(401, "访问令牌不存在")); + } + + @Test + public void testCheckAccessToken_expired() { + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(LocalDateTime.now().minusDays(1)); + oauth2AccessTokenMapper.insert(accessTokenDO); + // 准备参数 + String accessToken = accessTokenDO.getAccessToken(); + + // 调研,并断言 + assertServiceException(() -> oauth2TokenService.checkAccessToken(accessToken), + new ErrorCode(401, "访问令牌已过期")); + } + + @Test + public void testCheckAccessToken_success() { + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(LocalDateTime.now().plusDays(1)); + oauth2AccessTokenMapper.insert(accessTokenDO); + // 准备参数 + String accessToken = accessTokenDO.getAccessToken(); + + // 调研,并断言 + OAuth2AccessTokenDO result = oauth2TokenService.getAccessToken(accessToken); + // 断言 + assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + "creator", "updater"); + } + + @Test + public void testRemoveAccessToken_null() { + // 调用,并断言 + assertNull(oauth2TokenService.removeAccessToken(randomString())); + } + + @Test + public void testRemoveAccessToken_success() { + // mock 数据(访问令牌) + OAuth2AccessTokenDO accessTokenDO = randomPojo(OAuth2AccessTokenDO.class) + .setExpiresTime(LocalDateTime.now().plusDays(1)); + oauth2AccessTokenMapper.insert(accessTokenDO); + // mock 数据(刷新令牌) + OAuth2RefreshTokenDO refreshTokenDO = randomPojo(OAuth2RefreshTokenDO.class) + .setRefreshToken(accessTokenDO.getRefreshToken()); + oauth2RefreshTokenMapper.insert(refreshTokenDO); + // 调用 + OAuth2AccessTokenDO result = oauth2TokenService.removeAccessToken(accessTokenDO.getAccessToken()); + assertPojoEquals(accessTokenDO, result, "createTime", "updateTime", "deleted", + "creator", "updater"); + // 断言数据 + assertNull(oauth2AccessTokenMapper.selectByAccessToken(accessTokenDO.getAccessToken())); + assertNull(oauth2RefreshTokenMapper.selectByRefreshToken(accessTokenDO.getRefreshToken())); + assertNull(oauth2AccessTokenRedisDAO.get(accessTokenDO.getAccessToken())); + } + + + @Test + public void testGetAccessTokenPage() { + // mock 数据 + OAuth2AccessTokenDO dbAccessToken = randomPojo(OAuth2AccessTokenDO.class, o -> { // 等会查询到 + o.setUserId(10L); + o.setUserType(1); + o.setClientId("test_client"); + o.setExpiresTime(LocalDateTime.now().plusDays(1)); + }); + oauth2AccessTokenMapper.insert(dbAccessToken); + // 测试 userId 不匹配 + oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setUserId(20L))); + // 测试 userType 不匹配 + oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setUserType(2))); + // 测试 userType 不匹配 + oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setClientId("it_client"))); + // 测试 expireTime 不匹配 + oauth2AccessTokenMapper.insert(cloneIgnoreId(dbAccessToken, o -> o.setExpiresTime(LocalDateTimeUtil.now()))); + // 准备参数 + OAuth2AccessTokenPageReqVO reqVO = new OAuth2AccessTokenPageReqVO(); + reqVO.setUserId(10L); + reqVO.setUserType(1); + reqVO.setClientId("test"); + + // 调用 + PageResult pageResult = oauth2TokenService.getAccessTokenPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbAccessToken, pageResult.getList().get(0)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/MenuServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/MenuServiceImplTest.java new file mode 100644 index 0000000..3a88618 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/MenuServiceImplTest.java @@ -0,0 +1,295 @@ +package com.yunxi.scm.module.system.service.permission; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuListReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.menu.MenuUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.dal.mysql.permission.MenuMapper; +import com.yunxi.scm.module.system.enums.permission.MenuTypeEnum; +import com.yunxi.scm.module.system.service.tenant.TenantService; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.*; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO.ID_ROOT; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; + +@Import(MenuServiceImpl.class) +public class MenuServiceImplTest extends BaseDbUnitTest { + + @Resource + private MenuServiceImpl menuService; + + @Resource + private MenuMapper menuMapper; + + @MockBean + private PermissionService permissionService; + @MockBean + private TenantService tenantService; + + @Test + public void testCreateMenu_success() { + // mock 数据(构造父菜单) + MenuDO menuDO = buildMenuDO(MenuTypeEnum.MENU, + "parent", 0L); + menuMapper.insert(menuDO); + Long parentId = menuDO.getId(); + // 准备参数 + MenuCreateReqVO reqVO = randomPojo(MenuCreateReqVO.class, o -> { + o.setParentId(parentId); + o.setName("testSonName"); + o.setType(MenuTypeEnum.MENU.getType()); + }); + Long menuId = menuService.createMenu(reqVO); + + // 校验记录的属性是否正确 + MenuDO dbMenu = menuMapper.selectById(menuId); + assertPojoEquals(reqVO, dbMenu); + } + + @Test + public void testUpdateMenu_success() { + // mock 数据(构造父子菜单) + MenuDO sonMenuDO = createParentAndSonMenu(); + Long sonId = sonMenuDO.getId(); + // 准备参数 + MenuUpdateReqVO reqVO = randomPojo(MenuUpdateReqVO.class, o -> { + o.setId(sonId); + o.setName("testSonName"); // 修改名字 + o.setParentId(sonMenuDO.getParentId()); + o.setType(MenuTypeEnum.MENU.getType()); + }); + + // 调用 + menuService.updateMenu(reqVO); + // 校验记录的属性是否正确 + MenuDO dbMenu = menuMapper.selectById(sonId); + assertPojoEquals(reqVO, dbMenu); + } + + @Test + public void testUpdateMenu_sonIdNotExist() { + // 准备参数 + MenuUpdateReqVO reqVO = randomPojo(MenuUpdateReqVO.class); + // 调用,并断言异常 + assertServiceException(() -> menuService.updateMenu(reqVO), MENU_NOT_EXISTS); + } + + @Test + public void testDeleteMenu_success() { + // mock 数据 + MenuDO menuDO = randomPojo(MenuDO.class); + menuMapper.insert(menuDO); + // 准备参数 + Long id = menuDO.getId(); + + // 调用 + menuService.deleteMenu(id); + // 断言 + MenuDO dbMenuDO = menuMapper.selectById(id); + assertNull(dbMenuDO); + verify(permissionService).processMenuDeleted(id); + } + + @Test + public void testDeleteMenu_menuNotExist() { + assertServiceException(() -> menuService.deleteMenu(randomLongId()), + MENU_NOT_EXISTS); + } + + @Test + public void testDeleteMenu_existChildren() { + // mock 数据(构造父子菜单) + MenuDO sonMenu = createParentAndSonMenu(); + // 准备参数 + Long parentId = sonMenu.getParentId(); + + // 调用并断言异常 + assertServiceException(() -> menuService.deleteMenu(parentId), MENU_EXISTS_CHILDREN); + } + + @Test + public void testGetMenuList_all() { + // mock 数据 + MenuDO menu100 = randomPojo(MenuDO.class); + menuMapper.insert(menu100); + MenuDO menu101 = randomPojo(MenuDO.class); + menuMapper.insert(menu101); + // 准备参数 + + // 调用 + List list = menuService.getMenuList(); + // 断言 + assertEquals(2, list.size()); + assertPojoEquals(menu100, list.get(0)); + assertPojoEquals(menu101, list.get(1)); + } + + @Test + public void testGetMenuList() { + // mock 数据 + MenuDO menuDO = randomPojo(MenuDO.class, o -> o.setName("芋艿").setStatus(CommonStatusEnum.ENABLE.getStatus())); + menuMapper.insert(menuDO); + // 测试 status 不匹配 + menuMapper.insert(cloneIgnoreId(menuDO, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 name 不匹配 + menuMapper.insert(cloneIgnoreId(menuDO, o -> o.setName("艿"))); + // 准备参数 + MenuListReqVO reqVO = new MenuListReqVO().setName("芋").setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List result = menuService.getMenuList(reqVO); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(menuDO, result.get(0)); + } + + @Test + public void testGetMenuListByTenant() { + // mock 数据 + MenuDO menu100 = randomPojo(MenuDO.class, o -> o.setId(100L).setStatus(CommonStatusEnum.ENABLE.getStatus())); + menuMapper.insert(menu100); + MenuDO menu101 = randomPojo(MenuDO.class, o -> o.setId(101L).setStatus(CommonStatusEnum.DISABLE.getStatus())); + menuMapper.insert(menu101); + MenuDO menu102 = randomPojo(MenuDO.class, o -> o.setId(102L).setStatus(CommonStatusEnum.ENABLE.getStatus())); + menuMapper.insert(menu102); + // mock 过滤菜单 + Set menuIds = asSet(100L, 101L); + doNothing().when(tenantService).handleTenantMenu(argThat(handler -> { + handler.handle(menuIds); + return true; + })); + // 准备参数 + MenuListReqVO reqVO = new MenuListReqVO().setStatus(CommonStatusEnum.ENABLE.getStatus()); + + // 调用 + List result = menuService.getMenuListByTenant(reqVO); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(menu100, result.get(0)); + } + + @Test + public void testGetMenu() { + // mock 数据 + MenuDO menu = randomPojo(MenuDO.class); + menuMapper.insert(menu); + // 准备参数 + Long id = menu.getId(); + + // 调用 + MenuDO dbMenu = menuService.getMenu(id); + // 断言 + assertPojoEquals(menu, dbMenu); + } + + @Test + public void testValidateParentMenu_success() { + // mock 数据 + MenuDO menuDO = buildMenuDO(MenuTypeEnum.MENU, "parent", 0L); + menuMapper.insert(menuDO); + // 准备参数 + Long parentId = menuDO.getId(); + + // 调用,无需断言 + menuService.validateParentMenu(parentId, null); + } + + @Test + public void testValidateParentMenu_canNotSetSelfToBeParent() { + // 调用,并断言异常 + assertServiceException(() -> menuService.validateParentMenu(1L, 1L), + MENU_PARENT_ERROR); + } + + @Test + public void testValidateParentMenu_parentNotExist() { + // 调用,并断言异常 + assertServiceException(() -> menuService.validateParentMenu(randomLongId(), null), + MENU_PARENT_NOT_EXISTS); + } + + @Test + public void testValidateParentMenu_parentTypeError() { + // mock 数据 + MenuDO menuDO = buildMenuDO(MenuTypeEnum.BUTTON, "parent", 0L); + menuMapper.insert(menuDO); + // 准备参数 + Long parentId = menuDO.getId(); + + // 调用,并断言异常 + assertServiceException(() -> menuService.validateParentMenu(parentId, null), + MENU_PARENT_NOT_DIR_OR_MENU); + } + + @Test + public void testValidateMenu_success() { + // mock 父子菜单 + MenuDO sonMenu = createParentAndSonMenu(); + // 准备参数 + Long parentId = sonMenu.getParentId(); + Long otherSonMenuId = randomLongId(); + String otherSonMenuName = randomString(); + + // 调用,无需断言 + menuService.validateMenu(parentId, otherSonMenuName, otherSonMenuId); + } + + @Test + public void testValidateMenu_sonMenuNameDuplicate() { + // mock 父子菜单 + MenuDO sonMenu = createParentAndSonMenu(); + // 准备参数 + Long parentId = sonMenu.getParentId(); + Long otherSonMenuId = randomLongId(); + String otherSonMenuName = sonMenu.getName(); //相同名称 + + // 调用,并断言异常 + assertServiceException(() -> menuService.validateMenu(parentId, otherSonMenuName, otherSonMenuId), + MENU_NAME_DUPLICATE); + } + + // ====================== 初始化方法 ====================== + + /** + * 插入父子菜单,返回子菜单 + * + * @return 子菜单 + */ + private MenuDO createParentAndSonMenu() { + // 构造父子菜单 + MenuDO parentMenuDO = buildMenuDO(MenuTypeEnum.MENU, "parent", ID_ROOT); + menuMapper.insert(parentMenuDO); + // 构建子菜单 + MenuDO sonMenuDO = buildMenuDO(MenuTypeEnum.MENU, "testSonName", + parentMenuDO.getParentId()); + menuMapper.insert(sonMenuDO); + return sonMenuDO; + } + + private MenuDO buildMenuDO(MenuTypeEnum type, String name, Long parentId) { + return buildMenuDO(type, name, parentId, randomCommonStatus()); + } + + private MenuDO buildMenuDO(MenuTypeEnum type, String name, Long parentId, Integer status) { + return randomPojo(MenuDO.class, o -> o.setId(null).setName(name).setParentId(parentId) + .setType(type.getType()).setStatus(status)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/PermissionServiceTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/PermissionServiceTest.java new file mode 100644 index 0000000..f7f9216 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/PermissionServiceTest.java @@ -0,0 +1,527 @@ +package com.yunxi.scm.module.system.service.permission; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.api.permission.dto.DeptDataPermissionRespDTO; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleMenuDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.UserRoleDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.dal.mysql.permission.RoleMenuMapper; +import com.yunxi.scm.module.system.dal.mysql.permission.UserRoleMapper; +import com.yunxi.scm.module.system.enums.permission.DataScopeEnum; +import com.yunxi.scm.module.system.service.dept.DeptService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static cn.hutool.core.collection.ListUtil.toList; +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import({PermissionServiceImpl.class}) +public class PermissionServiceTest extends BaseDbUnitTest { + + @Resource + private PermissionServiceImpl permissionService; + + @Resource + private RoleMenuMapper roleMenuMapper; + @Resource + private UserRoleMapper userRoleMapper; + + @MockBean + private RoleService roleService; + @MockBean + private MenuService menuService; + @MockBean + private DeptService deptService; + @MockBean + private AdminUserService userService; + + @Test + public void testHasAnyPermissions_superAdmin() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"system:user:query", "system:user:create"}; + // mock 用户登录的角色 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(100L)); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(100L)))).thenReturn(toList(role)); + // mock 其它方法 + when(roleService.hasAnySuperAdmin(eq(asSet(100L)))).thenReturn(true); + + // 调用,并断言 + assertTrue(permissionService.hasAnyPermissions(userId, roles)); + } + } + + @Test + public void testHasAnyPermissions_normal() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"system:user:query", "system:user:create"}; + // mock 用户登录的角色 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(100L)); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(100L)))).thenReturn(toList(role)); + // mock 菜单 + Long menuId = 1000L; + when(menuService.getMenuIdListByPermissionFromCache( + eq("system:user:create"))).thenReturn(singletonList(menuId)); + roleMenuMapper.insert(randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(1000L)); + + // 调用,并断言 + assertTrue(permissionService.hasAnyPermissions(userId, roles)); + } + } + + @Test + public void testHasAnyRoles() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + String[] roles = new String[]{"yunai", "tudou"}; + // mock 用户与角色的缓存 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(100L)); + RoleDO role = randomPojo(RoleDO.class, o -> o.setId(100L).setCode("tudou") + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(100L)))).thenReturn(toList(role)); + + // 调用,并断言 + assertTrue(permissionService.hasAnyRoles(userId, roles)); + } + } + + // ========== 角色-菜单的相关方法 ========== + + @Test + public void testAssignRoleMenu() { + // 准备参数 + Long roleId = 1L; + Set menuIds = asSet(200L, 300L); + // mock 数据 + RoleMenuDO roleMenu01 = randomPojo(RoleMenuDO.class).setRoleId(1L).setMenuId(100L); + roleMenuMapper.insert(roleMenu01); + RoleMenuDO roleMenu02 = randomPojo(RoleMenuDO.class).setRoleId(1L).setMenuId(200L); + roleMenuMapper.insert(roleMenu02); + + // 调用 + permissionService.assignRoleMenu(roleId, menuIds); + // 断言 + List roleMenuList = roleMenuMapper.selectList(); + assertEquals(2, roleMenuList.size()); + assertEquals(1L, roleMenuList.get(0).getRoleId()); + assertEquals(200L, roleMenuList.get(0).getMenuId()); + assertEquals(1L, roleMenuList.get(1).getRoleId()); + assertEquals(300L, roleMenuList.get(1).getMenuId()); + } + + @Test + public void testProcessRoleDeleted() { + // 准备参数 + Long roleId = randomLongId(); + // mock 数据 UserRole + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setRoleId(roleId)); // 被删除 + userRoleMapper.insert(userRoleDO01); + UserRoleDO userRoleDO02 = randomPojo(UserRoleDO.class); // 不被删除 + userRoleMapper.insert(userRoleDO02); + // mock 数据 RoleMenu + RoleMenuDO roleMenuDO01 = randomPojo(RoleMenuDO.class, o -> o.setRoleId(roleId)); // 被删除 + roleMenuMapper.insert(roleMenuDO01); + RoleMenuDO roleMenuDO02 = randomPojo(RoleMenuDO.class); // 不被删除 + roleMenuMapper.insert(roleMenuDO02); + + // 调用 + permissionService.processRoleDeleted(roleId); + // 断言数据 RoleMenuDO + List dbRoleMenus = roleMenuMapper.selectList(); + assertEquals(1, dbRoleMenus.size()); + assertPojoEquals(dbRoleMenus.get(0), roleMenuDO02); + // 断言数据 UserRoleDO + List dbUserRoles = userRoleMapper.selectList(); + assertEquals(1, dbUserRoles.size()); + assertPojoEquals(dbUserRoles.get(0), userRoleDO02); + } + + @Test + public void testProcessMenuDeleted() { + // 准备参数 + Long menuId = randomLongId(); + // mock 数据 + RoleMenuDO roleMenuDO01 = randomPojo(RoleMenuDO.class, o -> o.setMenuId(menuId)); // 被删除 + roleMenuMapper.insert(roleMenuDO01); + RoleMenuDO roleMenuDO02 = randomPojo(RoleMenuDO.class); // 不被删除 + roleMenuMapper.insert(roleMenuDO02); + + // 调用 + permissionService.processMenuDeleted(menuId); + // 断言数据 + List dbRoleMenus = roleMenuMapper.selectList(); + assertEquals(1, dbRoleMenus.size()); + assertPojoEquals(dbRoleMenus.get(0), roleMenuDO02); + } + + @Test + public void testGetRoleMenuIds_superAdmin() { + // 准备参数 + Long roleId = 100L; + // mock 方法 + when(roleService.hasAnySuperAdmin(eq(singleton(100L)))).thenReturn(true); + List menuList = singletonList(randomPojo(MenuDO.class).setId(1L)); + when(menuService.getMenuList()).thenReturn(menuList); + + // 调用 + Set menuIds = permissionService.getRoleMenuListByRoleId(roleId); + // 断言 + assertEquals(singleton(1L), menuIds); + } + + @Test + public void testGetRoleMenuIds_normal() { + // 准备参数 + Long roleId = 100L; + // mock 数据 + RoleMenuDO roleMenu01 = randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(1L); + roleMenuMapper.insert(roleMenu01); + RoleMenuDO roleMenu02 = randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(2L); + roleMenuMapper.insert(roleMenu02); + + // 调用 + Set menuIds = permissionService.getRoleMenuListByRoleId(roleId); + // 断言 + assertEquals(asSet(1L, 2L), menuIds); + } + + @Test + public void testGetMenuRoleIdListByMenuIdFromCache() { + // 准备参数 + Long menuId = 1L; + // mock 数据 + RoleMenuDO roleMenu01 = randomPojo(RoleMenuDO.class).setRoleId(100L).setMenuId(1L); + roleMenuMapper.insert(roleMenu01); + RoleMenuDO roleMenu02 = randomPojo(RoleMenuDO.class).setRoleId(200L).setMenuId(1L); + roleMenuMapper.insert(roleMenu02); + + // 调用 + Set roleIds = permissionService.getMenuRoleIdListByMenuIdFromCache(menuId); + // 断言 + assertEquals(asSet(100L, 200L), roleIds); + } + + // ========== 用户-角色的相关方法 ========== + + @Test + public void testAssignUserRole() { + // 准备参数 + Long userId = 1L; + Set roleIds = asSet(200L, 300L); + // mock 数据 + UserRoleDO userRole01 = randomPojo(UserRoleDO.class).setUserId(1L).setRoleId(100L); + userRoleMapper.insert(userRole01); + UserRoleDO userRole02 = randomPojo(UserRoleDO.class).setUserId(1L).setRoleId(200L); + userRoleMapper.insert(userRole02); + + // 调用 + permissionService.assignUserRole(userId, roleIds); + // 断言 + List userRoleDOList = userRoleMapper.selectList(); + assertEquals(2, userRoleDOList.size()); + assertEquals(1L, userRoleDOList.get(0).getUserId()); + assertEquals(200L, userRoleDOList.get(0).getRoleId()); + assertEquals(1L, userRoleDOList.get(1).getUserId()); + assertEquals(300L, userRoleDOList.get(1).getRoleId()); + } + + @Test + public void testProcessUserDeleted() { + // 准备参数 + Long userId = randomLongId(); + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(userId)); // 被删除 + userRoleMapper.insert(userRoleDO01); + UserRoleDO userRoleDO02 = randomPojo(UserRoleDO.class); // 不被删除 + userRoleMapper.insert(userRoleDO02); + + // 调用 + permissionService.processUserDeleted(userId); + // 断言数据 + List dbUserRoles = userRoleMapper.selectList(); + assertEquals(1, dbUserRoles.size()); + assertPojoEquals(dbUserRoles.get(0), userRoleDO02); + } + + @Test + public void testGetUserRoleIdListByUserId() { + // 准备参数 + Long userId = 1L; + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); + userRoleMapper.insert(userRoleDO01); + UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(20L)); + userRoleMapper.insert(roleMenuDO02); + + // 调用 + Set result = permissionService.getUserRoleIdListByUserId(userId); + // 断言 + assertEquals(asSet(10L, 20L), result); + } + + @Test + public void testGetUserRoleIdListByUserIdFromCache() { + // 准备参数 + Long userId = 1L; + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); + userRoleMapper.insert(userRoleDO01); + UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(20L)); + userRoleMapper.insert(roleMenuDO02); + + // 调用 + Set result = permissionService.getUserRoleIdListByUserIdFromCache(userId); + // 断言 + assertEquals(asSet(10L, 20L), result); + } + + @Test + public void testGetUserRoleIdsFromCache() { + // 准备参数 + Long userId = 1L; + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); + userRoleMapper.insert(userRoleDO01); + UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(20L)); + userRoleMapper.insert(roleMenuDO02); + + // 调用 + Set result = permissionService.getUserRoleIdListByUserIdFromCache(userId); + // 断言 + assertEquals(asSet(10L, 20L), result); + } + + @Test + public void testGetUserRoleIdListByRoleId() { + // 准备参数 + Collection roleIds = asSet(10L, 20L); + // mock 数据 + UserRoleDO userRoleDO01 = randomPojo(UserRoleDO.class, o -> o.setUserId(1L).setRoleId(10L)); + userRoleMapper.insert(userRoleDO01); + UserRoleDO roleMenuDO02 = randomPojo(UserRoleDO.class, o -> o.setUserId(2L).setRoleId(20L)); + userRoleMapper.insert(roleMenuDO02); + + // 调用 + Set result = permissionService.getUserRoleIdListByRoleId(roleIds); + // 断言 + assertEquals(asSet(1L, 2L), result); + } + + @Test + public void testGetEnableUserRoleListByUserIdFromCache() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + // mock 用户登录的角色 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(100L)); + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(200L)); + RoleDO role01 = randomPojo(RoleDO.class, o -> o.setId(100L) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + RoleDO role02 = randomPojo(RoleDO.class, o -> o.setId(200L) + .setStatus(CommonStatusEnum.DISABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(asSet(100L, 200L)))) + .thenReturn(toList(role01, role02)); + + // 调用 + List result = permissionService.getEnableUserRoleListByUserIdFromCache(userId); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(role01, result.get(0)); + } + } + + // ========== 用户-部门的相关方法 ========== + + @Test + public void testAssignRoleDataScope() { + // 准备参数 + Long roleId = 1L; + Integer dataScope = 2; + Set dataScopeDeptIds = asSet(10L, 20L); + + // 调用 + permissionService.assignRoleDataScope(roleId, dataScope, dataScopeDeptIds); + // 断言 + verify(roleService).updateRoleDataScope(eq(roleId), eq(dataScope), eq(dataScopeDeptIds)); + } + + @Test + public void testGetDeptDataPermission_All() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.ALL.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertTrue(result.getAll()); + assertFalse(result.getSelf()); + assertTrue(CollUtil.isEmpty(result.getDeptIds())); + } + } + + @Test + public void testGetDeptDataPermission_DeptCustom() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_CUSTOM.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), + null, null); // 最后返回 null 的目的,看看会不会重复调用 + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertFalse(result.getAll()); + assertFalse(result.getSelf()); + assertEquals(roleDO.getDataScopeDeptIds().size() + 1, result.getDeptIds().size()); + assertTrue(CollUtil.containsAll(result.getDeptIds(), roleDO.getDataScopeDeptIds())); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); + } + } + + @Test + public void testGetDeptDataPermission_DeptOnly() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_ONLY.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), + null, null); // 最后返回 null 的目的,看看会不会重复调用 + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertFalse(result.getAll()); + assertFalse(result.getSelf()); + assertEquals(1, result.getDeptIds().size()); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); + } + } + + @Test + public void testGetDeptDataPermission_DeptAndChild() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.DEPT_AND_CHILD.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); + // mock 部门的返回 + when(userService.getUser(eq(1L))).thenReturn(new AdminUserDO().setDeptId(3L), + null, null); // 最后返回 null 的目的,看看会不会重复调用 + // mock 方法(部门) + DeptDO deptDO = randomPojo(DeptDO.class); + when(deptService.getChildDeptIdListFromCache(eq(3L))).thenReturn(singleton(deptDO.getId())); + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertFalse(result.getAll()); + assertFalse(result.getSelf()); + assertEquals(2, result.getDeptIds().size()); + assertTrue(CollUtil.contains(result.getDeptIds(), deptDO.getId())); + assertTrue(CollUtil.contains(result.getDeptIds(), 3L)); + } + } + + @Test + public void testGetDeptDataPermission_Self() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(PermissionServiceImpl.class))) + .thenReturn(permissionService); + + // 准备参数 + Long userId = 1L; + // mock 用户的角色编号 + userRoleMapper.insert(randomPojo(UserRoleDO.class).setUserId(userId).setRoleId(2L)); + // mock 获得用户的角色 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setDataScope(DataScopeEnum.SELF.getScope()) + .setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(roleService.getRoleListFromCache(eq(singleton(2L)))).thenReturn(toList(roleDO)); + + // 调用 + DeptDataPermissionRespDTO result = permissionService.getDeptDataPermission(userId); + // 断言 + assertFalse(result.getAll()); + assertTrue(result.getSelf()); + assertTrue(CollUtil.isEmpty(result.getDeptIds())); + } + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/RoleServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/RoleServiceImplTest.java new file mode 100644 index 0000000..5636749 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/permission/RoleServiceImplTest.java @@ -0,0 +1,386 @@ +package com.yunxi.scm.module.system.service.permission; + +import cn.hutool.extra.spring.SpringUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleExportReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RolePageReqVO; +import com.yunxi.scm.module.system.controller.admin.permission.vo.role.RoleUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.mysql.permission.RoleMapper; +import com.yunxi.scm.module.system.enums.permission.DataScopeEnum; +import com.yunxi.scm.module.system.enums.permission.RoleTypeEnum; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; + +@Import(RoleServiceImpl.class) +public class RoleServiceImplTest extends BaseDbUnitTest { + + @Resource + private RoleServiceImpl roleService; + + @Resource + private RoleMapper roleMapper; + + @MockBean + private PermissionService permissionService; + + @Test + public void testCreateRole() { + // 准备参数 + RoleCreateReqVO reqVO = randomPojo(RoleCreateReqVO.class); + + // 调用 + Long roleId = roleService.createRole(reqVO, null); + // 断言 + RoleDO roleDO = roleMapper.selectById(roleId); + assertPojoEquals(reqVO, roleDO); + assertEquals(RoleTypeEnum.CUSTOM.getType(), roleDO.getType()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), roleDO.getStatus()); + assertEquals(DataScopeEnum.ALL.getScope(), roleDO.getDataScope()); + } + + @Test + public void testUpdateRole() { + // mock 数据 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setType(RoleTypeEnum.CUSTOM.getType())); + roleMapper.insert(roleDO); + // 准备参数 + Long id = roleDO.getId(); + RoleUpdateReqVO reqVO = randomPojo(RoleUpdateReqVO.class, o -> o.setId(id)); + + // 调用 + roleService.updateRole(reqVO); + // 断言 + RoleDO newRoleDO = roleMapper.selectById(id); + assertPojoEquals(reqVO, newRoleDO); + } + + @Test + public void testUpdateRoleStatus() { + // mock 数据 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setType(RoleTypeEnum.CUSTOM.getType())); + roleMapper.insert(roleDO); + + // 准备参数 + Long roleId = roleDO.getId(); + + // 调用 + roleService.updateRoleStatus(roleId, CommonStatusEnum.DISABLE.getStatus()); + // 断言 + RoleDO dbRoleDO = roleMapper.selectById(roleId); + assertEquals(CommonStatusEnum.DISABLE.getStatus(), dbRoleDO.getStatus()); + } + + @Test + public void testUpdateRoleDataScope() { + // mock 数据 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setType(RoleTypeEnum.CUSTOM.getType())); + roleMapper.insert(roleDO); + // 准备参数 + Long id = roleDO.getId(); + Integer dataScope = randomEle(DataScopeEnum.values()).getScope(); + Set dataScopeRoleIds = randomSet(Long.class); + + // 调用 + roleService.updateRoleDataScope(id, dataScope, dataScopeRoleIds); + // 断言 + RoleDO dbRoleDO = roleMapper.selectById(id); + assertEquals(dataScope, dbRoleDO.getDataScope()); + assertEquals(dataScopeRoleIds, dbRoleDO.getDataScopeDeptIds()); + } + + @Test + public void testDeleteRole() { + // mock 数据 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setType(RoleTypeEnum.CUSTOM.getType())); + roleMapper.insert(roleDO); + // 参数准备 + Long id = roleDO.getId(); + + // 调用 + roleService.deleteRole(id); + // 断言 + assertNull(roleMapper.selectById(id)); + // verify 删除相关数据 + verify(permissionService).processRoleDeleted(id); + } + + @Test + public void testValidateRoleDuplicate_success() { + // 调用,不会抛异常 + roleService.validateRoleDuplicate(randomString(), randomString(), null); + } + + @Test + public void testValidateRoleDuplicate_nameDuplicate() { + // mock 数据 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setName("role_name")); + roleMapper.insert(roleDO); + // 准备参数 + String name = "role_name"; + + // 调用,并断言异常 + assertServiceException(() -> roleService.validateRoleDuplicate(name, randomString(), null), + ROLE_NAME_DUPLICATE, name); + } + + @Test + public void testValidateRoleDuplicate_codeDuplicate() { + // mock 数据 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setCode("code")); + roleMapper.insert(roleDO); + // 准备参数 + String code = "code"; + + // 调用,并断言异常 + assertServiceException(() -> roleService.validateRoleDuplicate(randomString(), code, null), + ROLE_CODE_DUPLICATE, code); + } + + @Test + public void testValidateUpdateRole_success() { + RoleDO roleDO = randomPojo(RoleDO.class); + roleMapper.insert(roleDO); + // 准备参数 + Long id = roleDO.getId(); + + // 调用,无异常 + roleService.validateRoleForUpdate(id); + } + + @Test + public void testValidateUpdateRole_roleIdNotExist() { + assertServiceException(() -> roleService.validateRoleForUpdate(randomLongId()), ROLE_NOT_EXISTS); + } + + @Test + public void testValidateUpdateRole_systemRoleCanNotBeUpdate() { + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setType(RoleTypeEnum.SYSTEM.getType())); + roleMapper.insert(roleDO); + // 准备参数 + Long id = roleDO.getId(); + + assertServiceException(() -> roleService.validateRoleForUpdate(id), + ROLE_CAN_NOT_UPDATE_SYSTEM_TYPE_ROLE); + } + + @Test + public void testGetRole() { + // mock 数据 + RoleDO roleDO = randomPojo(RoleDO.class); + roleMapper.insert(roleDO); + // 参数准备 + Long id = roleDO.getId(); + + // 调用 + RoleDO dbRoleDO = roleService.getRole(id); + // 断言 + assertPojoEquals(roleDO, dbRoleDO); + } + + @Test + public void testGetRoleFromCache() { + // mock 数据(缓存) + RoleDO roleDO = randomPojo(RoleDO.class); + roleMapper.insert(roleDO); + // 参数准备 + Long id = roleDO.getId(); + + // 调用 + RoleDO dbRoleDO = roleService.getRoleFromCache(id); + // 断言 + assertPojoEquals(roleDO, dbRoleDO); + } + + @Test + public void testGetRoleListByStatus() { + // mock 数据 + RoleDO dbRole01 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + roleMapper.insert(dbRole01); + RoleDO dbRole02 = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + roleMapper.insert(dbRole02); + + // 调用 + List list = roleService.getRoleListByStatus( + singleton(CommonStatusEnum.ENABLE.getStatus())); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbRole01, list.get(0)); + } + + @Test + public void testGetRoleListFromCache() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) + .thenReturn(roleService); + + // mock 数据 + RoleDO dbRole = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + roleMapper.insert(dbRole); + // 测试 id 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> {})); + // 准备参数 + Collection ids = singleton(dbRole.getId()); + + // 调用 + List list = roleService.getRoleListFromCache(ids); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbRole, list.get(0)); + } + } + + @Test + public void testGetRoleList() { + // mock 数据 + RoleDO dbRole = randomPojo(RoleDO.class, o -> { // 等会查询到 + o.setName("土豆"); + o.setCode("tudou"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2022, 2, 8)); + }); + roleMapper.insert(dbRole); + // 测试 name 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setName("红薯"))); + // 测试 code 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCode("hong"))); + // 测试 createTime 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCreateTime(buildTime(2022, 2, 16)))); + // 准备参数 + RoleExportReqVO reqVO = new RoleExportReqVO(); + reqVO.setName("土豆"); + reqVO.setCode("tu"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2022, 2, 1, 2022, 2, 12)); + + // 调用 + List list = roleService.getRoleList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbRole, list.get(0)); + } + + @Test + public void testGetRolePage() { + // mock 数据 + RoleDO dbRole = randomPojo(RoleDO.class, o -> { // 等会查询到 + o.setName("土豆"); + o.setCode("tudou"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2022, 2, 8)); + }); + roleMapper.insert(dbRole); + // 测试 name 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setName("红薯"))); + // 测试 code 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCode("hong"))); + // 测试 createTime 不匹配 + roleMapper.insert(cloneIgnoreId(dbRole, o -> o.setCreateTime(buildTime(2022, 2, 16)))); + // 准备参数 + RolePageReqVO reqVO = new RolePageReqVO(); + reqVO.setName("土豆"); + reqVO.setCode("tu"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2022, 2, 1, 2022, 2, 12)); + + // 调用 + PageResult pageResult = roleService.getRolePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbRole, pageResult.getList().get(0)); + } + + @Test + public void testHasAnySuperAdmin_true() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) + .thenReturn(roleService); + + // mock 数据 + RoleDO dbRole = randomPojo(RoleDO.class).setCode("super_admin"); + roleMapper.insert(dbRole); + // 准备参数 + Long id = dbRole.getId(); + + // 调用,并调用 + assertTrue(roleService.hasAnySuperAdmin(singletonList(id))); + } + } + + @Test + public void testHasAnySuperAdmin_false() { + try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) { + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(RoleServiceImpl.class))) + .thenReturn(roleService); + + // mock 数据 + RoleDO dbRole = randomPojo(RoleDO.class).setCode("tenant_admin"); + roleMapper.insert(dbRole); + // 准备参数 + Long id = dbRole.getId(); + + // 调用,并调用 + assertFalse(roleService.hasAnySuperAdmin(singletonList(id))); + } + } + + @Test + public void testValidateRoleList_success() { + // mock 数据 + RoleDO roleDO = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + roleMapper.insert(roleDO); + // 准备参数 + List ids = singletonList(roleDO.getId()); + + // 调用,无需断言 + roleService.validateRoleList(ids); + } + + @Test + public void testValidateRoleList_notFound() { + // 准备参数 + List ids = singletonList(randomLongId()); + + // 调用, 并断言异常 + assertServiceException(() -> roleService.validateRoleList(ids), ROLE_NOT_EXISTS); + } + + @Test + public void testValidateRoleList_notEnable() { + // mock 数据 + RoleDO RoleDO = randomPojo(RoleDO.class, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + roleMapper.insert(RoleDO); + // 准备参数 + List ids = singletonList(RoleDO.getId()); + + // 调用, 并断言异常 + assertServiceException(() -> roleService.validateRoleList(ids), ROLE_IS_DISABLE, RoleDO.getName()); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordServiceImplTest.java new file mode 100644 index 0000000..2315fd5 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sensitiveword/SensitiveWordServiceImplTest.java @@ -0,0 +1,267 @@ +package com.yunxi.scm.module.system.service.sensitiveword; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordPageReqVO; +import com.yunxi.scm.module.system.controller.admin.sensitiveword.vo.SensitiveWordUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sensitiveword.SensitiveWordDO; +import com.yunxi.scm.module.system.dal.mysql.sensitiveword.SensitiveWordMapper; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.Arrays; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS; +import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link SensitiveWordServiceImpl} 的单元测试类 + * + * @author 永不言败 + */ +@Import(SensitiveWordServiceImpl.class) +public class SensitiveWordServiceImplTest extends BaseDbUnitTest { + + @Resource + private SensitiveWordServiceImpl sensitiveWordService; + + @Resource + private SensitiveWordMapper sensitiveWordMapper; + + @Test + public void testInitLocalCache() { + SensitiveWordDO wordDO1 = randomPojo(SensitiveWordDO.class, o -> o.setName("傻瓜") + .setTags(singletonList("论坛")).setStatus(CommonStatusEnum.ENABLE.getStatus())); + sensitiveWordMapper.insert(wordDO1); + SensitiveWordDO wordDO2 = randomPojo(SensitiveWordDO.class, o -> o.setName("笨蛋") + .setTags(singletonList("蔬菜")).setStatus(CommonStatusEnum.ENABLE.getStatus())); + sensitiveWordMapper.insert(wordDO2); + + // 调用 + sensitiveWordService.initLocalCache(); + // 断言 sensitiveWordTagsCache 缓存 + assertEquals(SetUtils.asSet("论坛", "蔬菜"), sensitiveWordService.getSensitiveWordTagSet()); + // 断言 sensitiveWordCache + assertEquals(2, sensitiveWordService.getSensitiveWordCache().size()); + assertPojoEquals(wordDO1, sensitiveWordService.getSensitiveWordCache().get(0)); + assertPojoEquals(wordDO2, sensitiveWordService.getSensitiveWordCache().get(1)); + // 断言 tagSensitiveWordTries 缓存 + assertNotNull(sensitiveWordService.getDefaultSensitiveWordTrie()); + assertEquals(2, sensitiveWordService.getTagSensitiveWordTries().size()); + assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("论坛")); + assertNotNull(sensitiveWordService.getTagSensitiveWordTries().get("蔬菜")); + } + + @Test + public void testCreateSensitiveWord_success() { + // 准备参数 + SensitiveWordCreateReqVO reqVO = randomPojo(SensitiveWordCreateReqVO.class); + + // 调用 + Long sensitiveWordId = sensitiveWordService.createSensitiveWord(reqVO); + // 断言 + assertNotNull(sensitiveWordId); + // 校验记录的属性是否正确 + SensitiveWordDO sensitiveWord = sensitiveWordMapper.selectById(sensitiveWordId); + assertPojoEquals(reqVO, sensitiveWord); + } + + @Test + public void testUpdateSensitiveWord_success() { + // mock 数据 + SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class); + sensitiveWordMapper.insert(dbSensitiveWord);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SensitiveWordUpdateReqVO reqVO = randomPojo(SensitiveWordUpdateReqVO.class, o -> { + o.setId(dbSensitiveWord.getId()); // 设置更新的 ID + }); + + // 调用 + sensitiveWordService.updateSensitiveWord(reqVO); + // 校验是否更新正确 + SensitiveWordDO sensitiveWord = sensitiveWordMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, sensitiveWord); + } + + @Test + public void testUpdateSensitiveWord_notExists() { + // 准备参数 + SensitiveWordUpdateReqVO reqVO = randomPojo(SensitiveWordUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> sensitiveWordService.updateSensitiveWord(reqVO), SENSITIVE_WORD_NOT_EXISTS); + } + + @Test + public void testDeleteSensitiveWord_success() { + // mock 数据 + SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class); + sensitiveWordMapper.insert(dbSensitiveWord);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSensitiveWord.getId(); + + // 调用 + sensitiveWordService.deleteSensitiveWord(id); + // 校验数据不存在了 + assertNull(sensitiveWordMapper.selectById(id)); + } + + @Test + public void testDeleteSensitiveWord_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> sensitiveWordService.deleteSensitiveWord(id), SENSITIVE_WORD_NOT_EXISTS); + } + + @Test + public void testGetSensitiveWord() { + // mock 数据 + SensitiveWordDO sensitiveWord = randomPojo(SensitiveWordDO.class); + sensitiveWordMapper.insert(sensitiveWord); + // 准备参数 + Long id = sensitiveWord.getId(); + + // 调用 + SensitiveWordDO dbSensitiveWord = sensitiveWordService.getSensitiveWord(id); + // 断言 + assertPojoEquals(sensitiveWord, dbSensitiveWord); + } + + @Test + public void testGetSensitiveWordList() { + // mock 数据 + SensitiveWordDO sensitiveWord01 = randomPojo(SensitiveWordDO.class); + sensitiveWordMapper.insert(sensitiveWord01); + SensitiveWordDO sensitiveWord02 = randomPojo(SensitiveWordDO.class); + sensitiveWordMapper.insert(sensitiveWord02); + + // 调用 + List list = sensitiveWordService.getSensitiveWordList(); + // 断言 + assertEquals(2, list.size()); + assertEquals(sensitiveWord01, list.get(0)); + assertEquals(sensitiveWord02, list.get(1)); + } + + @Test + public void testGetSensitiveWordPage() { + // mock 数据 + SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class, o -> { // 等会查询到 + o.setName("笨蛋"); + o.setTags(Arrays.asList("论坛", "蔬菜")); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2022, 2, 8)); + }); + sensitiveWordMapper.insert(dbSensitiveWord); + // 测试 name 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setName("傻瓜"))); + // 测试 tags 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setTags(Arrays.asList("短信", "日用品")))); + // 测试 createTime 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setCreateTime(buildTime(2022, 2, 16)))); + // 准备参数 + SensitiveWordPageReqVO reqVO = new SensitiveWordPageReqVO(); + reqVO.setName("笨"); + reqVO.setTag("论坛"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2022, 2, 1, 2022, 2, 12)); + + // 调用 + PageResult pageResult = sensitiveWordService.getSensitiveWordPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSensitiveWord, pageResult.getList().get(0)); + } + + @Test + public void testGetSensitiveWordList_export() { + // mock 数据 + SensitiveWordDO dbSensitiveWord = randomPojo(SensitiveWordDO.class, o -> { // 等会查询到 + o.setName("笨蛋"); + o.setTags(Arrays.asList("论坛", "蔬菜")); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2022, 2, 8)); + }); + sensitiveWordMapper.insert(dbSensitiveWord); + // 测试 name 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setName("傻瓜"))); + // 测试 tags 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setTags(Arrays.asList("短信", "日用品")))); + // 测试 createTime 不匹配 + sensitiveWordMapper.insert(cloneIgnoreId(dbSensitiveWord, o -> o.setCreateTime(buildTime(2022, 2, 16)))); + // 准备参数 + SensitiveWordExportReqVO reqVO = new SensitiveWordExportReqVO(); + reqVO.setName("笨"); + reqVO.setTag("论坛"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2022, 2, 1, 2022, 2, 12)); + + // 调用 + List list = sensitiveWordService.getSensitiveWordList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSensitiveWord, list.get(0)); + } + + @Test + public void testValidateText_noTag() { + testInitLocalCache(); + // 准备参数 + String text = "你是傻瓜,你是笨蛋"; + + // 调用 + List result = sensitiveWordService.validateText(text, null); + // 断言 + assertEquals(Arrays.asList("傻瓜", "笨蛋"), result); + } + + @Test + public void testValidateText_hasTag() { + testInitLocalCache(); + // 准备参数 + String text = "你是傻瓜,你是笨蛋"; + + // 调用 + List result = sensitiveWordService.validateText(text, singletonList("论坛")); + // 断言 + assertEquals(singletonList("傻瓜"), result); + } + + @Test + public void testIsTestValid_noTag() { + testInitLocalCache(); + // 准备参数 + String text = "你是傻瓜,你是笨蛋"; + + // 调用,断言 + assertFalse(sensitiveWordService.isTextValid(text, null)); + } + + @Test + public void testIsTestValid_hasTag() { + testInitLocalCache(); + // 准备参数 + String text = "你是傻瓜,你是笨蛋"; + + // 调用,断言 + assertFalse(sensitiveWordService.isTextValid(text, singletonList("论坛"))); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsChannelServiceTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsChannelServiceTest.java new file mode 100644 index 0000000..8112c49 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsChannelServiceTest.java @@ -0,0 +1,202 @@ +package com.yunxi.scm.module.system.service.sms; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.sms.core.client.SmsClientFactory; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelPageReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.channel.SmsChannelUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import com.yunxi.scm.module.system.dal.mysql.sms.SmsChannelMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.*; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_HAS_CHILDREN; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SMS_CHANNEL_NOT_EXISTS; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import(SmsChannelServiceImpl.class) +public class SmsChannelServiceTest extends BaseDbUnitTest { + + @Resource + private SmsChannelServiceImpl smsChannelService; + + @Resource + private SmsChannelMapper smsChannelMapper; + + @MockBean + private SmsClientFactory smsClientFactory; + @MockBean + private SmsTemplateService smsTemplateService; + + @Test + public void testInitLocalCache_success() { + // mock 数据 + SmsChannelDO smsChannelDO01 = randomPojo(SmsChannelDO.class); + smsChannelMapper.insert(smsChannelDO01); + SmsChannelDO smsChannelDO02 = randomPojo(SmsChannelDO.class); + smsChannelMapper.insert(smsChannelDO02); + + // 调用 + smsChannelService.initLocalCache(); + // 校验调用 + verify(smsClientFactory, times(1)).createOrUpdateSmsClient( + argThat(properties -> isPojoEquals(smsChannelDO01, properties))); + verify(smsClientFactory, times(1)).createOrUpdateSmsClient( + argThat(properties -> isPojoEquals(smsChannelDO02, properties))); + // 断言 channelCache 缓存 + assertEquals(2, smsChannelService.getChannelCache().size()); + assertPojoEquals(smsChannelDO01, smsChannelService.getChannelCache().get(0)); + assertPojoEquals(smsChannelDO02, smsChannelService.getChannelCache().get(1)); + } + + @Test + public void testCreateSmsChannel_success() { + // 准备参数 + SmsChannelCreateReqVO reqVO = randomPojo(SmsChannelCreateReqVO.class, o -> o.setStatus(randomCommonStatus())); + + // 调用 + Long smsChannelId = smsChannelService.createSmsChannel(reqVO); + // 断言 + assertNotNull(smsChannelId); + // 校验记录的属性是否正确 + SmsChannelDO smsChannel = smsChannelMapper.selectById(smsChannelId); + assertPojoEquals(reqVO, smsChannel); + } + + @Test + public void testUpdateSmsChannel_success() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SmsChannelUpdateReqVO reqVO = randomPojo(SmsChannelUpdateReqVO.class, o -> { + o.setId(dbSmsChannel.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + o.setCallbackUrl(randomString()); + }); + + // 调用 + smsChannelService.updateSmsChannel(reqVO); + // 校验是否更新正确 + SmsChannelDO smsChannel = smsChannelMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, smsChannel); + } + + @Test + public void testUpdateSmsChannel_notExists() { + // 准备参数 + SmsChannelUpdateReqVO reqVO = randomPojo(SmsChannelUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.updateSmsChannel(reqVO), SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testDeleteSmsChannel_success() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsChannel.getId(); + + // 调用 + smsChannelService.deleteSmsChannel(id); + // 校验数据不存在了 + assertNull(smsChannelMapper.selectById(id)); + } + + @Test + public void testDeleteSmsChannel_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testDeleteSmsChannel_hasChildren() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class); + smsChannelMapper.insert(dbSmsChannel);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsChannel.getId(); + // mock 方法 + when(smsTemplateService.countByChannelId(eq(id))).thenReturn(10L); + + // 调用, 并断言异常 + assertServiceException(() -> smsChannelService.deleteSmsChannel(id), SMS_CHANNEL_HAS_CHILDREN); + } + + @Test + public void testGetSmsChannel() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class); + smsChannelMapper.insert(dbSmsChannel); // @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsChannel.getId(); + + // 调用,并断言 + assertPojoEquals(dbSmsChannel, smsChannelService.getSmsChannel(id)); + } + + @Test + public void testGetSmsChannelList() { + // mock 数据 + SmsChannelDO dbSmsChannel01 = randomPojo(SmsChannelDO.class); + smsChannelMapper.insert(dbSmsChannel01); + SmsChannelDO dbSmsChannel02 = randomPojo(SmsChannelDO.class); + smsChannelMapper.insert(dbSmsChannel02); + // 准备参数 + + // 调用 + List list = smsChannelService.getSmsChannelList(); + // 断言 + assertEquals(2, list.size()); + assertPojoEquals(dbSmsChannel01, list.get(0)); + assertPojoEquals(dbSmsChannel02, list.get(1)); + } + + @Test + public void testGetSmsChannelPage() { + // mock 数据 + SmsChannelDO dbSmsChannel = randomPojo(SmsChannelDO.class, o -> { // 等会查询到 + o.setSignature("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + }); + smsChannelMapper.insert(dbSmsChannel); + // 测试 signature 不匹配 + smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setSignature("源码"))); + // 测试 status 不匹配 + smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + smsChannelMapper.insert(cloneIgnoreId(dbSmsChannel, o -> o.setCreateTime(buildTime(2020, 11, 11)))); + // 准备参数 + SmsChannelPageReqVO reqVO = new SmsChannelPageReqVO(); + reqVO.setSignature("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); + + // 调用 + PageResult pageResult = smsChannelService.getSmsChannelPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsChannel, pageResult.getList().get(0)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsCodeServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsCodeServiceImplTest.java new file mode 100644 index 0000000..710afa7 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsCodeServiceImplTest.java @@ -0,0 +1,209 @@ +package com.yunxi.scm.module.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.mybatis.core.enums.SqlConstants; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeSendReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeUseReqDTO; +import com.yunxi.scm.module.system.api.sms.dto.code.SmsCodeValidateReqDTO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsCodeDO; +import com.yunxi.scm.module.system.dal.mysql.sms.SmsCodeMapper; +import com.yunxi.scm.module.system.enums.sms.SmsSceneEnum; +import com.yunxi.scm.module.system.framework.sms.SmsCodeProperties; +import com.baomidou.mybatisplus.annotation.DbType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.Duration; +import java.time.LocalDateTime; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@Import(SmsCodeServiceImpl.class) +public class SmsCodeServiceImplTest extends BaseDbUnitTest { + + @Resource + private SmsCodeServiceImpl smsCodeService; + + @Resource + private SmsCodeMapper smsCodeMapper; + + @MockBean + private SmsCodeProperties smsCodeProperties; + @MockBean + private SmsSendService smsSendService; + + @BeforeEach + public void setUp() { + when(smsCodeProperties.getExpireTimes()).thenReturn(Duration.ofMinutes(5)); + when(smsCodeProperties.getSendFrequency()).thenReturn(Duration.ofMinutes(1)); + when(smsCodeProperties.getSendMaximumQuantityPerDay()).thenReturn(10); + when(smsCodeProperties.getBeginCode()).thenReturn(9999); + when(smsCodeProperties.getEndCode()).thenReturn(9999); + } + + @Test + public void sendSmsCode_success() { + // 准备参数 + SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> { + o.setMobile("15601691300"); + o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene()); + }); + // mock 方法 + SqlConstants.init(DbType.MYSQL); + + // 调用 + smsCodeService.sendSmsCode(reqDTO); + // 断言 code 验证码 + SmsCodeDO smsCodeDO = smsCodeMapper.selectOne(null); + assertPojoEquals(reqDTO, smsCodeDO); + assertEquals("9999", smsCodeDO.getCode()); + assertEquals(1, smsCodeDO.getTodayIndex()); + assertFalse(smsCodeDO.getUsed()); + // 断言调用 + verify(smsSendService).sendSingleSms(eq(reqDTO.getMobile()), isNull(), isNull(), + eq("user-sms-login"), eq(MapUtil.of("code", "9999"))); + } + + @Test + public void sendSmsCode_tooFast() { + // mock 数据 + SmsCodeDO smsCodeDO = randomPojo(SmsCodeDO.class, + o -> o.setMobile("15601691300").setTodayIndex(1)); + smsCodeMapper.insert(smsCodeDO); + // 准备参数 + SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> { + o.setMobile("15601691300"); + o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene()); + }); + // mock 方法 + SqlConstants.init(DbType.MYSQL); + + // 调用,并断言异常 + assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), + SMS_CODE_SEND_TOO_FAST); + } + + @Test + public void sendSmsCode_exceedDay() { + // mock 数据 + SmsCodeDO smsCodeDO = randomPojo(SmsCodeDO.class, + o -> o.setMobile("15601691300").setTodayIndex(10).setCreateTime(LocalDateTime.now())); + smsCodeMapper.insert(smsCodeDO); + // 准备参数 + SmsCodeSendReqDTO reqDTO = randomPojo(SmsCodeSendReqDTO.class, o -> { + o.setMobile("15601691300"); + o.setScene(SmsSceneEnum.MEMBER_LOGIN.getScene()); + }); + // mock 方法 + SqlConstants.init(DbType.MYSQL); + when(smsCodeProperties.getSendFrequency()).thenReturn(Duration.ofMillis(0)); + + // 调用,并断言异常 + assertServiceException(() -> smsCodeService.sendSmsCode(reqDTO), + SMS_CODE_EXCEED_SEND_MAXIMUM_QUANTITY_PER_DAY); + } + + @Test + public void testUseSmsCode_success() { + // 准备参数 + SmsCodeUseReqDTO reqDTO = randomPojo(SmsCodeUseReqDTO.class, o -> { + o.setMobile("15601691300"); + o.setScene(randomEle(SmsSceneEnum.values()).getScene()); + }); + // mock 数据 + SqlConstants.init(DbType.MYSQL); + smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> { + o.setMobile(reqDTO.getMobile()).setScene(reqDTO.getScene()) + .setCode(reqDTO.getCode()).setUsed(false); + })); + + // 调用 + smsCodeService.useSmsCode(reqDTO); + // 断言 + SmsCodeDO smsCodeDO = smsCodeMapper.selectOne(null); + assertTrue(smsCodeDO.getUsed()); + assertNotNull(smsCodeDO.getUsedTime()); + assertEquals(reqDTO.getUsedIp(), smsCodeDO.getUsedIp()); + } + + @Test + public void validateSmsCode_success() { + // 准备参数 + SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> { + o.setMobile("15601691300"); + o.setScene(randomEle(SmsSceneEnum.values()).getScene()); + }); + // mock 数据 + SqlConstants.init(DbType.MYSQL); + smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile()) + .setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(false))); + + // 调用 + smsCodeService.validateSmsCode(reqDTO); + } + + @Test + public void validateSmsCode_notFound() { + // 准备参数 + SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> { + o.setMobile("15601691300"); + o.setScene(randomEle(SmsSceneEnum.values()).getScene()); + }); + // mock 数据 + SqlConstants.init(DbType.MYSQL); + + // 调用,并断言异常 + assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO), + SMS_CODE_NOT_FOUND); + } + + @Test + public void validateSmsCode_expired() { + // 准备参数 + SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> { + o.setMobile("15601691300"); + o.setScene(randomEle(SmsSceneEnum.values()).getScene()); + }); + // mock 数据 + SqlConstants.init(DbType.MYSQL); + smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile()) + .setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(false) + .setCreateTime(LocalDateTime.now().minusMinutes(6)))); + + // 调用,并断言异常 + assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO), + SMS_CODE_EXPIRED); + } + + @Test + public void validateSmsCode_used() { + // 准备参数 + SmsCodeValidateReqDTO reqDTO = randomPojo(SmsCodeValidateReqDTO.class, o -> { + o.setMobile("15601691300"); + o.setScene(randomEle(SmsSceneEnum.values()).getScene()); + }); + // mock 数据 + SqlConstants.init(DbType.MYSQL); + smsCodeMapper.insert(randomPojo(SmsCodeDO.class, o -> o.setMobile(reqDTO.getMobile()) + .setScene(reqDTO.getScene()).setCode(reqDTO.getCode()).setUsed(true) + .setCreateTime(LocalDateTime.now()))); + + // 调用,并断言异常 + assertServiceException(() -> smsCodeService.validateSmsCode(reqDTO), + SMS_CODE_USED); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsLogServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsLogServiceImplTest.java new file mode 100644 index 0000000..5100757 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsLogServiceImplTest.java @@ -0,0 +1,239 @@ +package com.yunxi.scm.module.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.common.pojo.CommonResult; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.log.SmsLogPageReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsLogDO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.sms.SmsLogMapper; +import com.yunxi.scm.module.system.enums.sms.SmsReceiveStatusEnum; +import com.yunxi.scm.module.system.enums.sms.SmsSendStatusEnum; +import com.yunxi.scm.module.system.enums.sms.SmsTemplateTypeEnum; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomBoolean; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@Import(SmsLogServiceImpl.class) +public class SmsLogServiceImplTest extends BaseDbUnitTest { + + @Resource + private SmsLogServiceImpl smsLogService; + + @Resource + private SmsLogMapper smsLogMapper; + + @Test + public void testGetSmsLogPage() { + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 + o.setChannelId(1L); + o.setTemplateId(10L); + o.setMobile("15601691300"); + o.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + o.setSendTime(buildTime(2020, 11, 11)); + o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + o.setReceiveTime(buildTime(2021, 11, 11)); + }); + smsLogMapper.insert(dbSmsLog); + // 测试 channelId 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L))); + // 测试 templateId 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L))); + // 测试 mobile 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999"))); + // 测试 sendStatus 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus()))); + // 测试 sendTime 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); + // 测试 receiveStatus 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus()))); + // 测试 receiveTime 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsLogPageReqVO reqVO = new SmsLogPageReqVO(); + reqVO.setChannelId(1L); + reqVO.setTemplateId(10L); + reqVO.setMobile("156"); + reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + reqVO.setSendTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30)); + reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + reqVO.setReceiveTime(buildBetweenTime(2021, 11, 1, 2021, 11, 30)); + + // 调用 + PageResult pageResult = smsLogService.getSmsLogPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsLog, pageResult.getList().get(0)); + } + + @Test + public void testGetSmsLogList() { + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO(o -> { // 等会查询到 + o.setChannelId(1L); + o.setTemplateId(10L); + o.setMobile("15601691300"); + o.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + o.setSendTime(buildTime(2020, 11, 11)); + o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + o.setReceiveTime(buildTime(2021, 11, 11)); + }); + smsLogMapper.insert(dbSmsLog); + // 测试 channelId 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setChannelId(2L))); + // 测试 templateId 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setTemplateId(20L))); + // 测试 mobile 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setMobile("18818260999"))); + // 测试 sendStatus 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus()))); + // 测试 sendTime 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setSendTime(buildTime(2020, 12, 12)))); + // 测试 receiveStatus 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveStatus(SmsReceiveStatusEnum.SUCCESS.getStatus()))); + // 测试 receiveTime 不匹配 + smsLogMapper.insert(cloneIgnoreId(dbSmsLog, o -> o.setReceiveTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsLogExportReqVO reqVO = new SmsLogExportReqVO(); + reqVO.setChannelId(1L); + reqVO.setTemplateId(10L); + reqVO.setMobile("156"); + reqVO.setSendStatus(SmsSendStatusEnum.INIT.getStatus()); + reqVO.setSendTime(buildBetweenTime(2020, 11, 1, 2020, 11, 30)); + reqVO.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus()); + reqVO.setReceiveTime(buildBetweenTime(2021, 11, 1, 2021, 11, 30)); + + // 调用 + List list = smsLogService.getSmsLogList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSmsLog, list.get(0)); + } + + @Test + public void testCreateSmsLog() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + Boolean isSend = randomBoolean(); + SmsTemplateDO templateDO = randomPojo(SmsTemplateDO.class, + o -> o.setType(randomEle(SmsTemplateTypeEnum.values()).getType())); + String templateContent = randomString(); + Map templateParams = randomTemplateParams(); + // mock 方法 + + // 调用 + Long logId = smsLogService.createSmsLog(mobile, userId, userType, isSend, + templateDO, templateContent, templateParams); + // 断言 + SmsLogDO logDO = smsLogMapper.selectById(logId); + assertEquals(isSend ? SmsSendStatusEnum.INIT.getStatus() : SmsSendStatusEnum.IGNORE.getStatus(), + logDO.getSendStatus()); + assertEquals(mobile, logDO.getMobile()); + assertEquals(userType, logDO.getUserType()); + assertEquals(userId, logDO.getUserId()); + assertEquals(templateDO.getId(), logDO.getTemplateId()); + assertEquals(templateDO.getCode(), logDO.getTemplateCode()); + assertEquals(templateDO.getType(), logDO.getTemplateType()); + assertEquals(templateDO.getChannelId(), logDO.getChannelId()); + assertEquals(templateDO.getChannelCode(), logDO.getChannelCode()); + assertEquals(templateContent, logDO.getTemplateContent()); + assertEquals(templateParams, logDO.getTemplateParams()); + assertEquals(SmsReceiveStatusEnum.INIT.getStatus(), logDO.getReceiveStatus()); + } + + @Test + public void testUpdateSmsSendResult() { + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO( + o -> o.setSendStatus(SmsSendStatusEnum.IGNORE.getStatus())); + smsLogMapper.insert(dbSmsLog); + // 准备参数 + Long id = dbSmsLog.getId(); + Integer sendCode = randomInteger(); + String sendMsg = randomString(); + String apiSendCode = randomString(); + String apiSendMsg = randomString(); + String apiRequestId = randomString(); + String apiSerialNo = randomString(); + + // 调用 + smsLogService.updateSmsSendResult(id, sendCode, sendMsg, + apiSendCode, apiSendMsg, apiRequestId, apiSerialNo); + // 断言 + dbSmsLog = smsLogMapper.selectById(id); + assertEquals(CommonResult.isSuccess(sendCode) ? SmsSendStatusEnum.SUCCESS.getStatus() + : SmsSendStatusEnum.FAILURE.getStatus(), dbSmsLog.getSendStatus()); + assertNotNull(dbSmsLog.getSendTime()); + assertEquals(sendMsg, dbSmsLog.getSendMsg()); + assertEquals(apiSendCode, dbSmsLog.getApiSendCode()); + assertEquals(apiSendMsg, dbSmsLog.getApiSendMsg()); + assertEquals(apiRequestId, dbSmsLog.getApiRequestId()); + assertEquals(apiSerialNo, dbSmsLog.getApiSerialNo()); + } + + @Test + public void testUpdateSmsReceiveResult() { + // mock 数据 + SmsLogDO dbSmsLog = randomSmsLogDO( + o -> o.setReceiveStatus(SmsReceiveStatusEnum.INIT.getStatus())); + smsLogMapper.insert(dbSmsLog); + // 准备参数 + Long id = dbSmsLog.getId(); + Boolean success = randomBoolean(); + LocalDateTime receiveTime = randomLocalDateTime(); + String apiReceiveCode = randomString(); + String apiReceiveMsg = randomString(); + + // 调用 + smsLogService.updateSmsReceiveResult(id, success, receiveTime, apiReceiveCode, apiReceiveMsg); + // 断言 + dbSmsLog = smsLogMapper.selectById(id); + assertEquals(success ? SmsReceiveStatusEnum.SUCCESS.getStatus() + : SmsReceiveStatusEnum.FAILURE.getStatus(), dbSmsLog.getReceiveStatus()); + assertEquals(receiveTime, dbSmsLog.getReceiveTime()); + assertEquals(apiReceiveCode, dbSmsLog.getApiReceiveCode()); + assertEquals(apiReceiveMsg, dbSmsLog.getApiReceiveMsg()); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static SmsLogDO randomSmsLogDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setTemplateParams(randomTemplateParams()); + o.setTemplateType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 templateType 的范围 + o.setUserType(randomEle(UserTypeEnum.values()).getValue()); // 保证 userType 的范围 + o.setSendStatus(randomEle(SmsSendStatusEnum.values()).getStatus()); // 保证 sendStatus 的范围 + o.setReceiveStatus(randomEle(SmsReceiveStatusEnum.values()).getStatus()); // 保证 receiveStatus 的范围 + }; + return randomPojo(SmsLogDO.class, ArrayUtils.append(consumer, consumers)); + } + + private static Map randomTemplateParams() { + return MapUtil.builder().put(randomString(), randomString()) + .put(randomString(), randomString()).build(); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsSendServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsSendServiceImplTest.java new file mode 100644 index 0000000..0a69418 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsSendServiceImplTest.java @@ -0,0 +1,289 @@ +package com.yunxi.scm.module.system.service.sms; + +import cn.hutool.core.map.MapUtil; +import com.yunxi.scm.framework.common.core.KeyValue; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.sms.core.client.SmsClient; +import com.yunxi.scm.framework.sms.core.client.SmsClientFactory; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsReceiveRespDTO; +import com.yunxi.scm.framework.sms.core.client.dto.SmsSendRespDTO; +import com.yunxi.scm.framework.test.core.ut.BaseMockitoUnitTest; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.mq.message.sms.SmsSendMessage; +import com.yunxi.scm.module.system.mq.producer.sms.SmsProducer; +import com.yunxi.scm.module.system.service.member.MemberService; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import org.assertj.core.util.Lists; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class SmsSendServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private SmsSendServiceImpl smsService; + + @Mock + private AdminUserService adminUserService; + @Mock + private MemberService memberService; + @Mock + private SmsChannelService smsChannelService; + @Mock + private SmsTemplateService smsTemplateService; + @Mock + private SmsLogService smsLogService; + @Mock + private SmsProducer smsProducer; + + @Mock + private SmsClientFactory smsClientFactory; + + @Test + public void testSendSingleSmsToAdmin() { + // 准备参数 + Long userId = randomLongId(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock adminUserService 的方法 + AdminUserDO user = randomPojo(AdminUserDO.class, o -> o.setMobile("15601691300")); + when(adminUserService.getUser(eq(userId))).thenReturn(user); + + // mock SmsTemplateService 的方法 + SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock SmsChannelService 的方法 + SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel); + // mock SmsLogService 的方法 + Long smsLogId = randomLongId(); + when(smsLogService.createSmsLog(eq(user.getMobile()), eq(userId), eq(UserTypeEnum.ADMIN.getValue()), eq(Boolean.TRUE), eq(template), + eq(content), eq(templateParams))).thenReturn(smsLogId); + + // 调用 + Long resultSmsLogId = smsService.sendSingleSmsToAdmin(null, userId, templateCode, templateParams); + // 断言 + assertEquals(smsLogId, resultSmsLogId); + // 断言调用 + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(user.getMobile()), + eq(template.getChannelId()), eq(template.getApiTemplateId()), + eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); + } + + @Test + public void testSendSingleSmsToUser() { + // 准备参数 + Long userId = randomLongId(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock memberService 的方法 + String mobile = "15601691300"; + when(memberService.getMemberUserMobile(eq(userId))).thenReturn(mobile); + + // mock SmsTemplateService 的方法 + SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock SmsChannelService 的方法 + SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel); + // mock SmsLogService 的方法 + Long smsLogId = randomLongId(); + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(UserTypeEnum.MEMBER.getValue()), eq(Boolean.TRUE), eq(template), + eq(content), eq(templateParams))).thenReturn(smsLogId); + + // 调用 + Long resultSmsLogId = smsService.sendSingleSmsToMember(null, userId, templateCode, templateParams); + // 断言 + assertEquals(smsLogId, resultSmsLogId); + // 断言调用 + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), + eq(template.getChannelId()), eq(template.getApiTemplateId()), + eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); + } + + /** + * 发送成功,当短信模板开启时 + */ + @Test + public void testSendSingleSms_successWhenSmsTemplateEnable() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock SmsTemplateService 的方法 + SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock SmsChannelService 的方法 + SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel); + // mock SmsLogService 的方法 + Long smsLogId = randomLongId(); + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.TRUE), eq(template), + eq(content), eq(templateParams))).thenReturn(smsLogId); + + // 调用 + Long resultSmsLogId = smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + // 断言 + assertEquals(smsLogId, resultSmsLogId); + // 断言调用 + verify(smsProducer).sendSmsSendMessage(eq(smsLogId), eq(mobile), + eq(template.getChannelId()), eq(template.getApiTemplateId()), + eq(Lists.newArrayList(new KeyValue<>("code", "1234"), new KeyValue<>("op", "login")))); + } + + /** + * 发送成功,当短信模板关闭时 + */ + @Test + public void testSendSingleSms_successWhenSmsTemplateDisable() { + // 准备参数 + String mobile = randomString(); + Long userId = randomLongId(); + Integer userType = randomEle(UserTypeEnum.values()).getValue(); + String templateCode = randomString(); + Map templateParams = MapUtil.builder().put("code", "1234") + .put("op", "login").build(); + // mock SmsTemplateService 的方法 + SmsTemplateDO template = randomPojo(SmsTemplateDO.class, o -> { + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); + o.setContent("验证码为{code}, 操作为{op}"); + o.setParams(Lists.newArrayList("code", "op")); + }); + when(smsTemplateService.getSmsTemplateByCodeFromCache(eq(templateCode))).thenReturn(template); + String content = randomString(); + when(smsTemplateService.formatSmsTemplateContent(eq(template.getContent()), eq(templateParams))) + .thenReturn(content); + // mock SmsChannelService 的方法 + SmsChannelDO smsChannel = randomPojo(SmsChannelDO.class, o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + when(smsChannelService.getSmsChannel(eq(template.getChannelId()))).thenReturn(smsChannel); + // mock SmsLogService 的方法 + Long smsLogId = randomLongId(); + when(smsLogService.createSmsLog(eq(mobile), eq(userId), eq(userType), eq(Boolean.FALSE), eq(template), + eq(content), eq(templateParams))).thenReturn(smsLogId); + + // 调用 + Long resultSmsLogId = smsService.sendSingleSms(mobile, userId, userType, templateCode, templateParams); + // 断言 + assertEquals(smsLogId, resultSmsLogId); + // 断言调用 + verify(smsProducer, times(0)).sendSmsSendMessage(anyLong(), anyString(), + anyLong(), any(), anyList()); + } + + @Test + public void testCheckSmsTemplateValid_notExists() { + // 准备参数 + String templateCode = randomString(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.validateSmsTemplate(templateCode), + SMS_SEND_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testBuildTemplateParams_paramMiss() { + // 准备参数 + SmsTemplateDO template = randomPojo(SmsTemplateDO.class, + o -> o.setParams(Lists.newArrayList("code"))); + Map templateParams = new HashMap<>(); + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.buildTemplateParams(template, templateParams), + SMS_SEND_MOBILE_TEMPLATE_PARAM_MISS, "code"); + } + + @Test + public void testCheckMobile_notExists() { + // 准备参数 + // mock 方法 + + // 调用,并断言异常 + assertServiceException(() -> smsService.validateMobile(null), + SMS_SEND_MOBILE_NOT_EXISTS); + } + + @Test + @SuppressWarnings("unchecked") + public void testDoSendSms() { + // 准备参数 + SmsSendMessage message = randomPojo(SmsSendMessage.class); + // mock SmsClientFactory 的方法 + SmsClient smsClient = spy(SmsClient.class); + when(smsClientFactory.getSmsClient(eq(message.getChannelId()))).thenReturn(smsClient); + // mock SmsClient 的方法 + SmsCommonResult sendResult = randomPojo(SmsCommonResult.class, SmsSendRespDTO.class); + sendResult.setData(randomPojo(SmsSendRespDTO.class)); + when(smsClient.sendSms(eq(message.getLogId()), eq(message.getMobile()), eq(message.getApiTemplateId()), + eq(message.getTemplateParams()))).thenReturn(sendResult); + + // 调用 + smsService.doSendSms(message); + // 断言 + verify(smsLogService).updateSmsSendResult(eq(message.getLogId()), + eq(sendResult.getCode()), eq(sendResult.getMsg()), eq(sendResult.getApiCode()), + eq(sendResult.getApiMsg()), eq(sendResult.getApiRequestId()), eq(sendResult.getData().getSerialNo())); + } + + @Test + public void testReceiveSmsStatus() throws Throwable { + // 准备参数 + String channelCode = randomString(); + String text = randomString(); + // mock SmsClientFactory 的方法 + SmsClient smsClient = spy(SmsClient.class); + when(smsClientFactory.getSmsClient(eq(channelCode))).thenReturn(smsClient); + // mock SmsClient 的方法 + List receiveResults = randomPojoList(SmsReceiveRespDTO.class); + + // 调用 + smsService.receiveSmsStatus(channelCode, text); + // 断言 + receiveResults.forEach(result -> smsLogService.updateSmsReceiveResult(eq(result.getLogId()), eq(result.getSuccess()), + eq(result.getReceiveTime()), eq(result.getErrorCode()), eq(result.getErrorCode()))); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsTemplateServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsTemplateServiceImplTest.java new file mode 100644 index 0000000..231c62d --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/sms/SmsTemplateServiceImplTest.java @@ -0,0 +1,339 @@ +package com.yunxi.scm.module.system.service.sms; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.common.util.object.ObjectUtils; +import com.yunxi.scm.framework.sms.core.client.SmsClient; +import com.yunxi.scm.framework.sms.core.client.SmsClientFactory; +import com.yunxi.scm.framework.sms.core.client.SmsCommonResult; +import com.yunxi.scm.framework.sms.core.client.dto.SmsTemplateRespDTO; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateExportReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplatePageReqVO; +import com.yunxi.scm.module.system.controller.admin.sms.vo.template.SmsTemplateUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsChannelDO; +import com.yunxi.scm.module.system.dal.dataobject.sms.SmsTemplateDO; +import com.yunxi.scm.module.system.dal.mysql.sms.SmsTemplateMapper; +import com.yunxi.scm.module.system.enums.sms.SmsTemplateTypeEnum; +import com.google.common.collect.Lists; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +@Import(SmsTemplateServiceImpl.class) +public class SmsTemplateServiceImplTest extends BaseDbUnitTest { + + @Resource + private SmsTemplateServiceImpl smsTemplateService; + + @Resource + private SmsTemplateMapper smsTemplateMapper; + + @MockBean + private SmsChannelService smsChannelService; + @MockBean + private SmsClientFactory smsClientFactory; + @MockBean + private SmsClient smsClient; + + @Test + public void testParseTemplateContentParams() { + // 准备参数 + String content = "正在进行登录操作{operation},您的验证码是{code}"; + // mock 方法 + + // 调用 + List params = smsTemplateService.parseTemplateContentParams(content); + // 断言 + assertEquals(Lists.newArrayList("operation", "code"), params); + } + + @Test + @SuppressWarnings("unchecked") + public void testCreateSmsTemplate_success() { + // 准备参数 + SmsTemplateCreateReqVO reqVO = randomPojo(SmsTemplateCreateReqVO.class, o -> { + o.setContent("正在进行登录操作{operation},您的验证码是{code}"); + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }); + // mock Channel 的方法 + SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { + o.setId(reqVO.getChannelId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO); + // mock 获得 API 短信模板成功 + when(smsClientFactory.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient); + when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn(randomPojo(SmsCommonResult.class, SmsTemplateRespDTO.class, + o -> o.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()))); + + // 调用 + Long smsTemplateId = smsTemplateService.createSmsTemplate(reqVO); + // 断言 + assertNotNull(smsTemplateId); + // 校验记录的属性是否正确 + SmsTemplateDO smsTemplate = smsTemplateMapper.selectById(smsTemplateId); + assertPojoEquals(reqVO, smsTemplate); + assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams()); + assertEquals(channelDO.getCode(), smsTemplate.getChannelCode()); + } + + @Test + @SuppressWarnings("unchecked") + public void testUpdateSmsTemplate_success() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); + smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + SmsTemplateUpdateReqVO reqVO = randomPojo(SmsTemplateUpdateReqVO.class, o -> { + o.setId(dbSmsTemplate.getId()); // 设置更新的 ID + o.setContent("正在进行登录操作{operation},您的验证码是{code}"); + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }); + // mock 方法 + SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { + o.setId(reqVO.getChannelId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelDO.getId()))).thenReturn(channelDO); + // mock 获得 API 短信模板成功 + when(smsClientFactory.getSmsClient(eq(reqVO.getChannelId()))).thenReturn(smsClient); + when(smsClient.getSmsTemplate(eq(reqVO.getApiTemplateId()))).thenReturn(randomPojo(SmsCommonResult.class, SmsTemplateRespDTO.class, + o -> o.setCode(GlobalErrorCodeConstants.SUCCESS.getCode()))); + + // 调用 + smsTemplateService.updateSmsTemplate(reqVO); + // 校验是否更新正确 + SmsTemplateDO smsTemplate = smsTemplateMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, smsTemplate); + assertEquals(Lists.newArrayList("operation", "code"), smsTemplate.getParams()); + assertEquals(channelDO.getCode(), smsTemplate.getChannelCode()); + } + + @Test + public void testUpdateSmsTemplate_notExists() { + // 准备参数 + SmsTemplateUpdateReqVO reqVO = randomPojo(SmsTemplateUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> smsTemplateService.updateSmsTemplate(reqVO), SMS_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testDeleteSmsTemplate_success() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomSmsTemplateDO(); + smsTemplateMapper.insert(dbSmsTemplate);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbSmsTemplate.getId(); + + // 调用 + smsTemplateService.deleteSmsTemplate(id); + // 校验数据不存在了 + assertNull(smsTemplateMapper.selectById(id)); + } + + @Test + public void testDeleteSmsTemplate_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> smsTemplateService.deleteSmsTemplate(id), SMS_TEMPLATE_NOT_EXISTS); + } + + @Test + public void testGetSmsTemplatePage() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomPojo(SmsTemplateDO.class, o -> { // 等会查询到 + o.setType(SmsTemplateTypeEnum.PROMOTION.getType()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCode("tudou"); + o.setContent("芋道源码"); + o.setApiTemplateId("yunai"); + o.setChannelId(1L); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + smsTemplateMapper.insert(dbSmsTemplate); + // 测试 type 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setType(SmsTemplateTypeEnum.VERIFICATION_CODE.getType()))); + // 测试 status 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 code 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCode("yuanma"))); + // 测试 content 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setContent("源码"))); + // 测试 apiTemplateId 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setApiTemplateId("nai"))); + // 测试 channelId 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setChannelId(2L))); + // 测试 createTime 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsTemplatePageReqVO reqVO = new SmsTemplatePageReqVO(); + reqVO.setType(SmsTemplateTypeEnum.PROMOTION.getType()); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCode("tu"); + reqVO.setContent("芋道"); + reqVO.setApiTemplateId("yu"); + reqVO.setChannelId(1L); + reqVO.setCreateTime(buildBetweenTime(2021, 11, 1, 2021, 12, 1)); + + // 调用 + PageResult pageResult = smsTemplateService.getSmsTemplatePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbSmsTemplate, pageResult.getList().get(0)); + } + + @Test + public void testGetSmsTemplateList() { + // mock 数据 + SmsTemplateDO dbSmsTemplate = randomPojo(SmsTemplateDO.class, o -> { // 等会查询到 + o.setType(SmsTemplateTypeEnum.PROMOTION.getType()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCode("tudou"); + o.setContent("芋道源码"); + o.setApiTemplateId("yunai"); + o.setChannelId(1L); + o.setCreateTime(buildTime(2021, 11, 11)); + }); + smsTemplateMapper.insert(dbSmsTemplate); + // 测试 type 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setType(SmsTemplateTypeEnum.VERIFICATION_CODE.getType()))); + // 测试 status 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 code 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCode("yuanma"))); + // 测试 content 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setContent("源码"))); + // 测试 apiTemplateId 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setApiTemplateId("nai"))); + // 测试 channelId 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setChannelId(2L))); + // 测试 createTime 不匹配 + smsTemplateMapper.insert(ObjectUtils.cloneIgnoreId(dbSmsTemplate, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + SmsTemplateExportReqVO reqVO = new SmsTemplateExportReqVO(); + reqVO.setType(SmsTemplateTypeEnum.PROMOTION.getType()); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCode("tu"); + reqVO.setContent("芋道"); + reqVO.setApiTemplateId("yu"); + reqVO.setChannelId(1L); + reqVO.setCreateTime(buildBetweenTime(2021, 11, 1, 2021, 12, 1)); + + // 调用 + List list = smsTemplateService.getSmsTemplateList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbSmsTemplate, list.get(0)); + } + + @Test + public void testValidateSmsChannel_success() { + // 准备参数 + Long channelId = randomLongId(); + // mock 方法 + SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { + o.setId(channelId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); // 保证 status 开启,创建必须处于这个状态 + }); + when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO); + + // 调用 + SmsChannelDO returnChannelDO = smsTemplateService.validateSmsChannel(channelId); + // 断言 + assertPojoEquals(returnChannelDO, channelDO); + } + + @Test + public void testValidateSmsChannel_notExists() { + // 准备参数 + Long channelId = randomLongId(); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.validateSmsChannel(channelId), + SMS_CHANNEL_NOT_EXISTS); + } + + @Test + public void testValidateSmsChannel_disable() { + // 准备参数 + Long channelId = randomLongId(); + // mock 方法 + SmsChannelDO channelDO = randomPojo(SmsChannelDO.class, o -> { + o.setId(channelId); + o.setStatus(CommonStatusEnum.DISABLE.getStatus()); // 保证 status 禁用,触发失败 + }); + when(smsChannelService.getSmsChannel(eq(channelId))).thenReturn(channelDO); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.validateSmsChannel(channelId), + SMS_CHANNEL_DISABLE); + } + + @Test + public void testValidateDictDataValueUnique_success() { + // 调用,成功 + smsTemplateService.validateSmsTemplateCodeDuplicate(randomLongId(), randomString()); + } + + @Test + public void testValidateSmsTemplateCodeDuplicate_valueDuplicateForCreate() { + // 准备参数 + String code = randomString(); + // mock 数据 + smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.validateSmsTemplateCodeDuplicate(null, code), + SMS_TEMPLATE_CODE_DUPLICATE, code); + } + + @Test + public void testValidateDictDataValueUnique_valueDuplicateForUpdate() { + // 准备参数 + Long id = randomLongId(); + String code = randomString(); + // mock 数据 + smsTemplateMapper.insert(randomSmsTemplateDO(o -> o.setCode(code))); + + // 调用,校验异常 + assertServiceException(() -> smsTemplateService.validateSmsTemplateCodeDuplicate(id, code), + SMS_TEMPLATE_CODE_DUPLICATE, code); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static SmsTemplateDO randomSmsTemplateDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setType(randomEle(SmsTemplateTypeEnum.values()).getType()); // 保证 type 的 范围 + }; + return randomPojo(SmsTemplateDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/social/SocialUserServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/social/SocialUserServiceImplTest.java new file mode 100644 index 0000000..0b4ed9c --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/social/SocialUserServiceImplTest.java @@ -0,0 +1,256 @@ +package com.yunxi.scm.module.system.service.social; + +import com.yunxi.scm.framework.common.enums.UserTypeEnum; +import com.yunxi.scm.framework.social.core.YunxiAuthRequestFactory; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.api.social.dto.SocialUserBindReqDTO; +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserBindDO; +import com.yunxi.scm.module.system.dal.dataobject.social.SocialUserDO; +import com.yunxi.scm.module.system.dal.mysql.social.SocialUserBindMapper; +import com.yunxi.scm.module.system.dal.mysql.social.SocialUserMapper; +import com.yunxi.scm.module.system.enums.social.SocialTypeEnum; +import com.xingyuv.jushauth.enums.AuthResponseStatus; +import com.xingyuv.jushauth.model.AuthCallback; +import com.xingyuv.jushauth.model.AuthResponse; +import com.xingyuv.jushauth.model.AuthUser; +import com.xingyuv.jushauth.request.AuthRequest; +import com.xingyuv.jushauth.utils.AuthStateUtils; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.util.List; + +import static cn.hutool.core.util.RandomUtil.randomLong; +import static cn.hutool.core.util.RandomUtil.randomString; +import static com.yunxi.scm.framework.common.util.json.JsonUtils.toJsonString; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SOCIAL_USER_AUTH_FAILURE; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.SOCIAL_USER_NOT_FOUND; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.*; + +@Import(SocialUserServiceImpl.class) +public class SocialUserServiceImplTest extends BaseDbUnitTest { + + @Resource + private SocialUserServiceImpl socialUserService; + + @Resource + private SocialUserMapper socialUserMapper; + @Resource + private SocialUserBindMapper socialUserBindMapper; + + @MockBean + private YunxiAuthRequestFactory authRequestFactory; + + @Test + public void testGetAuthorizeUrl() { + try (MockedStatic authStateUtilsMock = mockStatic(AuthStateUtils.class)) { + // 准备参数 + Integer type = SocialTypeEnum.WECHAT_MP.getType(); + String redirectUri = "sss"; + // mock 获得对应的 AuthRequest 实现 + AuthRequest authRequest = mock(AuthRequest.class); + when(authRequestFactory.get(eq("WECHAT_MP"))).thenReturn(authRequest); + // mock 方法 + authStateUtilsMock.when(AuthStateUtils::createState).thenReturn("aoteman"); + when(authRequest.authorize(eq("aoteman"))).thenReturn("https://www.iocoder.cn?redirect_uri=yyy"); + + // 调用 + String url = socialUserService.getAuthorizeUrl(type, redirectUri); + // 断言 + assertEquals("https://www.iocoder.cn?redirect_uri=sss", url); + } + } + + @Test + public void testAuthSocialUser_exists() { + // 准备参数 + Integer type = SocialTypeEnum.GITEE.getType(); + String code = "tudou"; + String state = "yuanma"; + // mock 方法 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(type).setCode(code).setState(state); + socialUserMapper.insert(socialUser); + + // 调用 + SocialUserDO result = socialUserService.authSocialUser(type, code, state); + // 断言 + assertPojoEquals(socialUser, result); + } + + @Test + public void testAuthSocialUser_authFailure() { + // 准备参数 + Integer type = SocialTypeEnum.GITEE.getType(); + // mock 方法 + AuthRequest authRequest = mock(AuthRequest.class); + when(authRequestFactory.get(anyString())).thenReturn(authRequest); + AuthResponse authResponse = new AuthResponse<>(0, "模拟失败", null); + when(authRequest.login(any(AuthCallback.class))).thenReturn(authResponse); + + // 调用并断言 + assertServiceException( + () -> socialUserService.authSocialUser(type, randomString(10), randomString(10)), + SOCIAL_USER_AUTH_FAILURE, "模拟失败"); + } + + @Test + public void testAuthSocialUser_insert() { + // 准备参数 + Integer type = SocialTypeEnum.GITEE.getType(); + String code = "tudou"; + String state = "yuanma"; + // mock 方法 + AuthRequest authRequest = mock(AuthRequest.class); + when(authRequestFactory.get(eq(SocialTypeEnum.GITEE.getSource()))).thenReturn(authRequest); + AuthUser authUser = randomPojo(AuthUser.class); + AuthResponse authResponse = new AuthResponse<>(AuthResponseStatus.SUCCESS.getCode(), null, authUser); + when(authRequest.login(any(AuthCallback.class))).thenReturn(authResponse); + + // 调用 + SocialUserDO result = socialUserService.authSocialUser(type, code, state); + // 断言 + assertBindSocialUser(type, result, authResponse.getData()); + assertEquals(code, result.getCode()); + assertEquals(state, result.getState()); + } + + @Test + public void testAuthSocialUser_update() { + // 准备参数 + Integer type = SocialTypeEnum.GITEE.getType(); + String code = "tudou"; + String state = "yuanma"; + // mock 数据 + socialUserMapper.insert(randomPojo(SocialUserDO.class).setType(type).setOpenid("test_openid")); + // mock 方法 + AuthRequest authRequest = mock(AuthRequest.class); + when(authRequestFactory.get(eq(SocialTypeEnum.GITEE.getSource()))).thenReturn(authRequest); + AuthUser authUser = randomPojo(AuthUser.class); + authUser.getToken().setOpenId("test_openid"); + AuthResponse authResponse = new AuthResponse<>(AuthResponseStatus.SUCCESS.getCode(), null, authUser); + when(authRequest.login(any(AuthCallback.class))).thenReturn(authResponse); + + // 调用 + SocialUserDO result = socialUserService.authSocialUser(type, code, state); + // 断言 + assertBindSocialUser(type, result, authResponse.getData()); + assertEquals(code, result.getCode()); + assertEquals(state, result.getState()); + } + + private void assertBindSocialUser(Integer type, SocialUserDO socialUser, AuthUser authUser) { + assertEquals(authUser.getToken().getAccessToken(), socialUser.getToken()); + assertEquals(toJsonString(authUser.getToken()), socialUser.getRawTokenInfo()); + assertEquals(authUser.getNickname(), socialUser.getNickname()); + assertEquals(authUser.getAvatar(), socialUser.getAvatar()); + assertEquals(toJsonString(authUser.getRawUserInfo()), socialUser.getRawUserInfo()); + assertEquals(type, socialUser.getType()); + assertEquals(authUser.getUuid(), socialUser.getOpenid()); + } + + @Test + public void testGetSocialUserList() { + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + // mock 获得社交用户 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(SocialTypeEnum.GITEE.getType()); + socialUserMapper.insert(socialUser); // 可被查到 + socialUserMapper.insert(randomPojo(SocialUserDO.class)); // 不可被查到 + // mock 获得绑定 + socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class) // 可被查询到 + .setUserId(userId).setUserType(userType).setSocialType(SocialTypeEnum.GITEE.getType()) + .setSocialUserId(socialUser.getId())); + socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class) // 不可被查询到 + .setUserId(2L).setUserType(userType).setSocialType(SocialTypeEnum.DINGTALK.getType())); + + // 调用 + List result = socialUserService.getSocialUserList(userId, userType); + // 断言 + assertEquals(1, result.size()); + assertPojoEquals(socialUser, result.get(0)); + } + + @Test + public void testBindSocialUser() { + // 准备参数 + SocialUserBindReqDTO reqDTO = new SocialUserBindReqDTO() + .setUserId(1L).setUserType(UserTypeEnum.ADMIN.getValue()) + .setType(SocialTypeEnum.GITEE.getType()).setCode("test_code").setState("test_state"); + // mock 数据:获得社交用户 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(reqDTO.getType()) + .setCode(reqDTO.getCode()).setState(reqDTO.getState()); + socialUserMapper.insert(socialUser); + // mock 数据:用户可能之前已经绑定过该社交类型 + socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class).setUserId(1L).setUserType(UserTypeEnum.ADMIN.getValue()) + .setSocialType(SocialTypeEnum.GITEE.getType()).setSocialUserId(-1L)); + // mock 数据:社交用户可能之前绑定过别的用户 + socialUserBindMapper.insert(randomPojo(SocialUserBindDO.class).setUserType(UserTypeEnum.ADMIN.getValue()) + .setSocialType(SocialTypeEnum.GITEE.getType()).setSocialUserId(socialUser.getId())); + + // 调用 + socialUserService.bindSocialUser(reqDTO); + // 断言 + List socialUserBinds = socialUserBindMapper.selectList(); + assertEquals(1, socialUserBinds.size()); + } + + @Test + public void testUnbindSocialUser_success() { + // 准备参数 + Long userId = 1L; + Integer userType = UserTypeEnum.ADMIN.getValue(); + Integer type = SocialTypeEnum.GITEE.getType(); + String openid = "test_openid"; + // mock 数据:社交用户 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(type).setOpenid(openid); + socialUserMapper.insert(socialUser); + // mock 数据:社交绑定关系 + SocialUserBindDO socialUserBind = randomPojo(SocialUserBindDO.class).setUserType(userType) + .setUserId(userId).setSocialType(type); + socialUserBindMapper.insert(socialUserBind); + + // 调用 + socialUserService.unbindSocialUser(userId, userType, type, openid); + // 断言 + assertEquals(0, socialUserBindMapper.selectCount(null).intValue()); + } + + @Test + public void testUnbindSocialUser_notFound() { + // 调用,并断言 + assertServiceException( + () -> socialUserService.unbindSocialUser(randomLong(), UserTypeEnum.ADMIN.getValue(), + SocialTypeEnum.GITEE.getType(), "test_openid"), + SOCIAL_USER_NOT_FOUND); + } + + @Test + public void testGetBindUserId() { + // 准备参数 + Integer userType = UserTypeEnum.ADMIN.getValue(); + Integer type = SocialTypeEnum.GITEE.getType(); + String code = "tudou"; + String state = "yuanma"; + // mock 社交用户 + SocialUserDO socialUser = randomPojo(SocialUserDO.class).setType(type).setCode(code).setState(state); + socialUserMapper.insert(socialUser); + // mock 社交用户的绑定 + Long userId = randomLong(); + SocialUserBindDO socialUserBind = randomPojo(SocialUserBindDO.class).setUserType(userType).setUserId(userId) + .setSocialType(type).setSocialUserId(socialUser.getId()); + socialUserBindMapper.insert(socialUserBind); + + // 调用 + Long result = socialUserService.getBindUserId(userType, type, code, state); + // 断言 + assertEquals(userId, result); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/tenant/TenantPackageServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/tenant/TenantPackageServiceImplTest.java new file mode 100644 index 0000000..ce33516 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/tenant/TenantPackageServiceImplTest.java @@ -0,0 +1,235 @@ +package com.yunxi.scm.module.system.service.tenant; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackagePageReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.packages.TenantPackageUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantPackageDO; +import com.yunxi.scm.module.system.dal.mysql.tenant.TenantPackageMapper; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; + +import java.util.List; + +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomLongId; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.randomPojo; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** +* {@link TenantPackageServiceImpl} 的单元测试类 +* +* @author 芋道源码 +*/ +@Import(TenantPackageServiceImpl.class) +public class TenantPackageServiceImplTest extends BaseDbUnitTest { + + @Resource + private TenantPackageServiceImpl tenantPackageService; + + @Resource + private TenantPackageMapper tenantPackageMapper; + + @MockBean + private TenantService tenantService; + + @Test + public void testCreateTenantPackage_success() { + // 准备参数 + TenantPackageCreateReqVO reqVO = randomPojo(TenantPackageCreateReqVO.class); + + // 调用 + Long tenantPackageId = tenantPackageService.createTenantPackage(reqVO); + // 断言 + assertNotNull(tenantPackageId); + // 校验记录的属性是否正确 + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(tenantPackageId); + assertPojoEquals(reqVO, tenantPackage); + } + + @Test + public void testUpdateTenantPackage_success() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + // 准备参数 + TenantPackageUpdateReqVO reqVO = randomPojo(TenantPackageUpdateReqVO.class, o -> { + o.setId(dbTenantPackage.getId()); // 设置更新的 ID + }); + // mock 方法 + Long tenantId01 = randomLongId(); + Long tenantId02 = randomLongId(); + when(tenantService.getTenantListByPackageId(eq(reqVO.getId()))).thenReturn( + asList(randomPojo(TenantDO.class, o -> o.setId(tenantId01)), + randomPojo(TenantDO.class, o -> o.setId(tenantId02)))); + + // 调用 + tenantPackageService.updateTenantPackage(reqVO); + // 校验是否更新正确 + TenantPackageDO tenantPackage = tenantPackageMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, tenantPackage); + // 校验调用租户的菜单 + verify(tenantService).updateTenantRoleMenu(eq(tenantId01), eq(reqVO.getMenuIds())); + verify(tenantService).updateTenantRoleMenu(eq(tenantId02), eq(reqVO.getMenuIds())); + } + + @Test + public void testUpdateTenantPackage_notExists() { + // 准备参数 + TenantPackageUpdateReqVO reqVO = randomPojo(TenantPackageUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.updateTenantPackage(reqVO), TENANT_PACKAGE_NOT_EXISTS); + } + + @Test + public void testDeleteTenantPackage_success() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenantPackage.getId(); + // mock 租户未使用该套餐 + when(tenantService.getTenantCountByPackageId(eq(id))).thenReturn(0L); + + // 调用 + tenantPackageService.deleteTenantPackage(id); + // 校验数据不存在了 + assertNull(tenantPackageMapper.selectById(id)); + } + + @Test + public void testDeleteTenantPackage_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.deleteTenantPackage(id), TENANT_PACKAGE_NOT_EXISTS); + } + + @Test + public void testDeleteTenantPackage_used() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenantPackage.getId(); + // mock 租户在使用该套餐 + when(tenantService.getTenantCountByPackageId(eq(id))).thenReturn(1L); + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.deleteTenantPackage(id), TENANT_PACKAGE_USED); + } + + @Test + public void testGetTenantPackagePage() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setRemark("源码解析"); + o.setCreateTime(buildTime(2022, 10, 10)); + }); + tenantPackageMapper.insert(dbTenantPackage); + // 测试 name 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setName("源码"))); + // 测试 status 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 remark 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setRemark("解析"))); + // 测试 createTime 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, o -> o.setCreateTime(buildTime(2022, 11, 11)))); + // 准备参数 + TenantPackagePageReqVO reqVO = new TenantPackagePageReqVO(); + reqVO.setName("芋道"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setRemark("源码"); + reqVO.setCreateTime(buildBetweenTime(2022, 10, 9, 2022, 10, 11)); + + // 调用 + PageResult pageResult = tenantPackageService.getTenantPackagePage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTenantPackage, pageResult.getList().get(0)); + } + + @Test + public void testValidTenantPackage_success() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, + o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + + // 调用 + TenantPackageDO result = tenantPackageService.validTenantPackage(dbTenantPackage.getId()); + // 断言 + assertPojoEquals(dbTenantPackage, result); + } + + @Test + public void testValidTenantPackage_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.validTenantPackage(id), TENANT_PACKAGE_NOT_EXISTS); + } + + @Test + public void testValidTenantPackage_disable() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + + // 调用, 并断言异常 + assertServiceException(() -> tenantPackageService.validTenantPackage(dbTenantPackage.getId()), + TENANT_PACKAGE_DISABLE, dbTenantPackage.getName()); + } + + @Test + public void testGetTenantPackage() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class); + tenantPackageMapper.insert(dbTenantPackage);// @Sql: 先插入出一条存在的数据 + + // 调用 + TenantPackageDO result = tenantPackageService.getTenantPackage(dbTenantPackage.getId()); + // 断言 + assertPojoEquals(result, dbTenantPackage); + } + + @Test + public void testGetTenantPackageListByStatus() { + // mock 数据 + TenantPackageDO dbTenantPackage = randomPojo(TenantPackageDO.class, + o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus())); + tenantPackageMapper.insert(dbTenantPackage); + // 测试 status 不匹配 + tenantPackageMapper.insert(cloneIgnoreId(dbTenantPackage, + o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + + // 调用 + List list = tenantPackageService.getTenantPackageListByStatus( + CommonStatusEnum.ENABLE.getStatus()); + assertEquals(1, list.size()); + assertPojoEquals(dbTenantPackage, list.get(0)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/tenant/TenantServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/tenant/TenantServiceImplTest.java new file mode 100644 index 0000000..46dcec8 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/tenant/TenantServiceImplTest.java @@ -0,0 +1,484 @@ +package com.yunxi.scm.module.system.service.tenant; + +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.tenant.config.TenantProperties; +import com.yunxi.scm.framework.tenant.core.context.TenantContextHolder; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantCreateReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantExportReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantPageReqVO; +import com.yunxi.scm.module.system.controller.admin.tenant.vo.tenant.TenantUpdateReqVO; +import com.yunxi.scm.module.system.dal.dataobject.permission.MenuDO; +import com.yunxi.scm.module.system.dal.dataobject.permission.RoleDO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantPackageDO; +import com.yunxi.scm.module.system.dal.mysql.tenant.TenantMapper; +import com.yunxi.scm.module.system.enums.permission.RoleCodeEnum; +import com.yunxi.scm.module.system.enums.permission.RoleTypeEnum; +import com.yunxi.scm.module.system.service.permission.MenuService; +import com.yunxi.scm.module.system.service.permission.PermissionService; +import com.yunxi.scm.module.system.service.permission.RoleService; +import com.yunxi.scm.module.system.service.tenant.handler.TenantInfoHandler; +import com.yunxi.scm.module.system.service.tenant.handler.TenantMenuHandler; +import com.yunxi.scm.module.system.service.user.AdminUserService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; + +import javax.annotation.Resource; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO.PACKAGE_ID_SYSTEM; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static java.util.Arrays.asList; +import static java.util.Collections.singleton; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * {@link TenantServiceImpl} 的单元测试类 + * + * @author 芋道源码 + */ +@Import(TenantServiceImpl.class) +public class TenantServiceImplTest extends BaseDbUnitTest { + + @Resource + private TenantServiceImpl tenantService; + + @Resource + private TenantMapper tenantMapper; + + @MockBean + private TenantProperties tenantProperties; + @MockBean + private TenantPackageService tenantPackageService; + @MockBean + private AdminUserService userService; + @MockBean + private RoleService roleService; + @MockBean + private MenuService menuService; + @MockBean + private PermissionService permissionService; + + @BeforeEach + public void setUp() { + // 清理租户上下文 + TenantContextHolder.clear(); + } + + @Test + public void testGetTenantIdList() { + // mock 数据 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L)); + tenantMapper.insert(tenant); + + // 调用,并断言业务异常 + List result = tenantService.getTenantIdList(); + assertEquals(Collections.singletonList(1L), result); + } + + @Test + public void testValidTenant_notExists() { + assertServiceException(() -> tenantService.validTenant(randomLongId()), TENANT_NOT_EXISTS); + } + + @Test + public void testValidTenant_disable() { + // mock 数据 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.DISABLE.getStatus())); + tenantMapper.insert(tenant); + + // 调用,并断言业务异常 + assertServiceException(() -> tenantService.validTenant(1L), TENANT_DISABLE, tenant.getName()); + } + + @Test + public void testValidTenant_expired() { + // mock 数据 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setExpireTime(buildTime(2020, 2, 2))); + tenantMapper.insert(tenant); + + // 调用,并断言业务异常 + assertServiceException(() -> tenantService.validTenant(1L), TENANT_EXPIRE, tenant.getName()); + } + + @Test + public void testValidTenant_success() { + // mock 数据 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setId(1L).setStatus(CommonStatusEnum.ENABLE.getStatus()) + .setExpireTime(LocalDateTime.now().plusDays(1))); + tenantMapper.insert(tenant); + + // 调用,并断言业务异常 + tenantService.validTenant(1L); + } + + @Test + public void testCreateTenant() { + // mock 套餐 100L + TenantPackageDO tenantPackage = randomPojo(TenantPackageDO.class, o -> o.setId(100L)); + when(tenantPackageService.validTenantPackage(eq(100L))).thenReturn(tenantPackage); + // mock 角色 200L + when(roleService.createRole(argThat(role -> { + assertEquals(RoleCodeEnum.TENANT_ADMIN.getName(), role.getName()); + assertEquals(RoleCodeEnum.TENANT_ADMIN.getCode(), role.getCode()); + assertEquals(0, role.getSort()); + assertEquals("系统自动生成", role.getRemark()); + return true; + }), eq(RoleTypeEnum.SYSTEM.getType()))).thenReturn(200L); + // mock 用户 300L + when(userService.createUser(argThat(user -> { + assertEquals("yunai", user.getUsername()); + assertEquals("yuanma", user.getPassword()); + assertEquals("芋道", user.getNickname()); + assertEquals("15601691300", user.getMobile()); + return true; + }))).thenReturn(300L); + + // 准备参数 + TenantCreateReqVO reqVO = randomPojo(TenantCreateReqVO.class, o -> { + o.setContactName("芋道"); + o.setContactMobile("15601691300"); + o.setPackageId(100L); + o.setStatus(randomCommonStatus()); + o.setDomain("https://www.iocoder.cn"); + o.setUsername("yunai"); + o.setPassword("yuanma"); + }); + + // 调用 + Long tenantId = tenantService.createTenant(reqVO); + // 断言 + assertNotNull(tenantId); + // 校验记录的属性是否正确 + TenantDO tenant = tenantMapper.selectById(tenantId); + assertPojoEquals(reqVO, tenant); + assertEquals(300L, tenant.getContactUserId()); + // verify 分配权限 + verify(permissionService).assignRoleMenu(eq(200L), same(tenantPackage.getMenuIds())); + // verify 分配角色 + verify(permissionService).assignUserRole(eq(300L), eq(singleton(200L))); + } + + @Test + public void testUpdateTenant_success() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setStatus(randomCommonStatus())); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + TenantUpdateReqVO reqVO = randomPojo(TenantUpdateReqVO.class, o -> { + o.setId(dbTenant.getId()); // 设置更新的 ID + o.setStatus(randomCommonStatus()); + o.setDomain(randomString()); + }); + + // mock 套餐 + TenantPackageDO tenantPackage = randomPojo(TenantPackageDO.class, + o -> o.setMenuIds(asSet(200L, 201L))); + when(tenantPackageService.validTenantPackage(eq(reqVO.getPackageId()))).thenReturn(tenantPackage); + // mock 所有角色 + RoleDO role100 = randomPojo(RoleDO.class, o -> o.setId(100L).setCode(RoleCodeEnum.TENANT_ADMIN.getCode())); + role100.setTenantId(dbTenant.getId()); + RoleDO role101 = randomPojo(RoleDO.class, o -> o.setId(101L)); + role101.setTenantId(dbTenant.getId()); + when(roleService.getRoleListByStatus(isNull())).thenReturn(asList(role100, role101)); + // mock 每个角色的权限 + when(permissionService.getRoleMenuListByRoleId(eq(101L))).thenReturn(asSet(201L, 202L)); + + // 调用 + tenantService.updateTenant(reqVO); + // 校验是否更新正确 + TenantDO tenant = tenantMapper.selectById(reqVO.getId()); // 获取最新的 + assertPojoEquals(reqVO, tenant); + // verify 设置角色权限 + verify(permissionService).assignRoleMenu(eq(100L), eq(asSet(200L, 201L))); + verify(permissionService).assignRoleMenu(eq(101L), eq(asSet(201L))); + } + + @Test + public void testUpdateTenant_notExists() { + // 准备参数 + TenantUpdateReqVO reqVO = randomPojo(TenantUpdateReqVO.class); + + // 调用, 并断言异常 + assertServiceException(() -> tenantService.updateTenant(reqVO), TENANT_NOT_EXISTS); + } + + @Test + public void testUpdateTenant_system() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + TenantUpdateReqVO reqVO = randomPojo(TenantUpdateReqVO.class, o -> { + o.setId(dbTenant.getId()); // 设置更新的 ID + }); + + // 调用,校验业务异常 + assertServiceException(() -> tenantService.updateTenant(reqVO), TENANT_CAN_NOT_UPDATE_SYSTEM); + } + + @Test + public void testDeleteTenant_success() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, + o -> o.setStatus(randomCommonStatus())); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenant.getId(); + + // 调用 + tenantService.deleteTenant(id); + // 校验数据不存在了 + assertNull(tenantMapper.selectById(id)); + } + + @Test + public void testDeleteTenant_notExists() { + // 准备参数 + Long id = randomLongId(); + + // 调用, 并断言异常 + assertServiceException(() -> tenantService.deleteTenant(id), TENANT_NOT_EXISTS); + } + + @Test + public void testDeleteTenant_system() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenant.getId(); + + // 调用, 并断言异常 + assertServiceException(() -> tenantService.deleteTenant(id), TENANT_CAN_NOT_UPDATE_SYSTEM); + } + + @Test + public void testGetTenant() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + // 准备参数 + Long id = dbTenant.getId(); + + // 调用 + TenantDO result = tenantService.getTenant(id); + // 校验存在 + assertPojoEquals(result, dbTenant); + } + + @Test + public void testGetTenantPage() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setContactName("芋艿"); + o.setContactMobile("15601691300"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + }); + tenantMapper.insert(dbTenant); + // 测试 name 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setName(randomString()))); + // 测试 contactName 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactName(randomString()))); + // 测试 contactMobile 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactMobile(randomString()))); + // 测试 status 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + TenantPageReqVO reqVO = new TenantPageReqVO(); + reqVO.setName("芋道"); + reqVO.setContactName("艿"); + reqVO.setContactMobile("1560"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); + + // 调用 + PageResult pageResult = tenantService.getTenantPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbTenant, pageResult.getList().get(0)); + } + + @Test + public void testGetTenantList() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> { // 等会查询到 + o.setName("芋道源码"); + o.setContactName("芋艿"); + o.setContactMobile("15601691300"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + }); + tenantMapper.insert(dbTenant); + // 测试 name 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setName(randomString()))); + // 测试 contactName 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactName(randomString()))); + // 测试 contactMobile 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setContactMobile(randomString()))); + // 测试 status 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + tenantMapper.insert(cloneIgnoreId(dbTenant, o -> o.setCreateTime(buildTime(2021, 12, 12)))); + // 准备参数 + TenantExportReqVO reqVO = new TenantExportReqVO(); + reqVO.setName("芋道"); + reqVO.setContactName("艿"); + reqVO.setContactMobile("1560"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); + + // 调用 + List list = tenantService.getTenantList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbTenant, list.get(0)); + } + + @Test + public void testGetTenantByName() { + // mock 数据 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setName("芋道")); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + + // 调用 + TenantDO result = tenantService.getTenantByName("芋道"); + // 校验存在 + assertPojoEquals(result, dbTenant); + } + + @Test + public void testGetTenantListByPackageId() { + // mock 数据 + TenantDO dbTenant1 = randomPojo(TenantDO.class, o -> o.setPackageId(1L)); + tenantMapper.insert(dbTenant1);// @Sql: 先插入出一条存在的数据 + TenantDO dbTenant2 = randomPojo(TenantDO.class, o -> o.setPackageId(2L)); + tenantMapper.insert(dbTenant2);// @Sql: 先插入出一条存在的数据 + + // 调用 + List result = tenantService.getTenantListByPackageId(1L); + assertEquals(1, result.size()); + assertPojoEquals(dbTenant1, result.get(0)); + } + + @Test + public void testGetTenantCountByPackageId() { + // mock 数据 + TenantDO dbTenant1 = randomPojo(TenantDO.class, o -> o.setPackageId(1L)); + tenantMapper.insert(dbTenant1);// @Sql: 先插入出一条存在的数据 + TenantDO dbTenant2 = randomPojo(TenantDO.class, o -> o.setPackageId(2L)); + tenantMapper.insert(dbTenant2);// @Sql: 先插入出一条存在的数据 + + // 调用 + Long count = tenantService.getTenantCountByPackageId(1L); + assertEquals(1, count); + } + + @Test + public void testHandleTenantInfo_disable() { + // 准备参数 + TenantInfoHandler handler = mock(TenantInfoHandler.class); + // mock 禁用 + when(tenantProperties.getEnable()).thenReturn(false); + + // 调用 + tenantService.handleTenantInfo(handler); + // 断言 + verify(handler, never()).handle(any()); + } + + @Test + public void testHandleTenantInfo_success() { + // 准备参数 + TenantInfoHandler handler = mock(TenantInfoHandler.class); + // mock 未禁用 + when(tenantProperties.getEnable()).thenReturn(true); + // mock 租户 + TenantDO dbTenant = randomPojo(TenantDO.class); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + TenantContextHolder.setTenantId(dbTenant.getId()); + + // 调用 + tenantService.handleTenantInfo(handler); + // 断言 + verify(handler).handle(argThat(argument -> { + assertPojoEquals(dbTenant, argument); + return true; + })); + } + + @Test + public void testHandleTenantMenu_disable() { + // 准备参数 + TenantMenuHandler handler = mock(TenantMenuHandler.class); + // mock 禁用 + when(tenantProperties.getEnable()).thenReturn(false); + + // 调用 + tenantService.handleTenantMenu(handler); + // 断言 + verify(handler, never()).handle(any()); + } + + @Test // 系统租户的情况 + public void testHandleTenantMenu_system() { + // 准备参数 + TenantMenuHandler handler = mock(TenantMenuHandler.class); + // mock 未禁用 + when(tenantProperties.getEnable()).thenReturn(true); + // mock 租户 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(PACKAGE_ID_SYSTEM)); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + TenantContextHolder.setTenantId(dbTenant.getId()); + // mock 菜单 + when(menuService.getMenuList()).thenReturn(Arrays.asList(randomPojo(MenuDO.class, o -> o.setId(100L)), + randomPojo(MenuDO.class, o -> o.setId(101L)))); + + // 调用 + tenantService.handleTenantMenu(handler); + // 断言 + verify(handler).handle(asSet(100L, 101L)); + } + + @Test // 普通租户的情况 + public void testHandleTenantMenu_normal() { + // 准备参数 + TenantMenuHandler handler = mock(TenantMenuHandler.class); + // mock 未禁用 + when(tenantProperties.getEnable()).thenReturn(true); + // mock 租户 + TenantDO dbTenant = randomPojo(TenantDO.class, o -> o.setPackageId(200L)); + tenantMapper.insert(dbTenant);// @Sql: 先插入出一条存在的数据 + TenantContextHolder.setTenantId(dbTenant.getId()); + // mock 菜单 + when(tenantPackageService.getTenantPackage(eq(200L))).thenReturn(randomPojo(TenantPackageDO.class, + o -> o.setMenuIds(asSet(100L, 101L)))); + + // 调用 + tenantService.handleTenantMenu(handler); + // 断言 + verify(handler).handle(asSet(100L, 101L)); + } +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/user/AdminUserServiceImplTest.java b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/user/AdminUserServiceImplTest.java new file mode 100644 index 0000000..61b9f50 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/java/com/yunxi/scm/module/system/service/user/AdminUserServiceImplTest.java @@ -0,0 +1,784 @@ +package com.yunxi.scm.module.system.service.user; + +import cn.hutool.core.util.RandomUtil; +import com.yunxi.scm.framework.common.enums.CommonStatusEnum; +import com.yunxi.scm.framework.common.exception.ServiceException; +import com.yunxi.scm.framework.common.pojo.PageResult; +import com.yunxi.scm.framework.common.util.collection.ArrayUtils; +import com.yunxi.scm.framework.common.util.collection.CollectionUtils; +import com.yunxi.scm.framework.test.core.ut.BaseDbUnitTest; +import com.yunxi.scm.module.infra.api.file.FileApi; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdatePasswordReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.profile.UserProfileUpdateReqVO; +import com.yunxi.scm.module.system.controller.admin.user.vo.user.*; +import com.yunxi.scm.module.system.dal.dataobject.dept.DeptDO; +import com.yunxi.scm.module.system.dal.dataobject.dept.PostDO; +import com.yunxi.scm.module.system.dal.dataobject.dept.UserPostDO; +import com.yunxi.scm.module.system.dal.dataobject.tenant.TenantDO; +import com.yunxi.scm.module.system.dal.dataobject.user.AdminUserDO; +import com.yunxi.scm.module.system.dal.mysql.dept.UserPostMapper; +import com.yunxi.scm.module.system.dal.mysql.user.AdminUserMapper; +import com.yunxi.scm.module.system.enums.common.SexEnum; +import com.yunxi.scm.module.system.service.dept.DeptService; +import com.yunxi.scm.module.system.service.dept.PostService; +import com.yunxi.scm.module.system.service.permission.PermissionService; +import com.yunxi.scm.module.system.service.tenant.TenantService; +import org.junit.jupiter.api.Test; +import org.mockito.stubbing.Answer; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.crypto.password.PasswordEncoder; + +import javax.annotation.Resource; +import java.io.ByteArrayInputStream; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static cn.hutool.core.util.RandomUtil.randomBytes; +import static cn.hutool.core.util.RandomUtil.randomEle; +import static com.yunxi.scm.framework.common.util.collection.SetUtils.asSet; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; +import static com.yunxi.scm.framework.common.util.date.LocalDateTimeUtils.buildTime; +import static com.yunxi.scm.framework.common.util.object.ObjectUtils.cloneIgnoreId; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertPojoEquals; +import static com.yunxi.scm.framework.test.core.util.AssertUtils.assertServiceException; +import static com.yunxi.scm.framework.test.core.util.RandomUtils.*; +import static com.yunxi.scm.module.system.enums.ErrorCodeConstants.*; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; +import static org.assertj.core.util.Lists.newArrayList; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@Import(AdminUserServiceImpl.class) +public class AdminUserServiceImplTest extends BaseDbUnitTest { + + @Resource + private AdminUserServiceImpl userService; + + @Resource + private AdminUserMapper userMapper; + @Resource + private UserPostMapper userPostMapper; + + @MockBean + private DeptService deptService; + @MockBean + private PostService postService; + @MockBean + private PermissionService permissionService; + @MockBean + private PasswordEncoder passwordEncoder; + @MockBean + private TenantService tenantService; + @MockBean + private FileApi fileApi; + + @Test + public void testCreatUser_success() { + // 准备参数 + UserCreateReqVO reqVO = randomPojo(UserCreateReqVO.class, o -> { + o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); + o.setMobile(randomString()); + o.setPostIds(asSet(1L, 2L)); + }); + // mock 账户额度充足 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(1)); + doNothing().when(tenantService).handleTenantInfo(argThat(handler -> { + handler.handle(tenant); + return true; + })); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(reqVO.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + // mock postService 的方法 + List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> + randomPojo(PostDO.class, o -> { + o.setId(postId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + })); + when(postService.getPostList(eq(reqVO.getPostIds()), isNull())).thenReturn(posts); + // mock passwordEncoder 的方法 + when(passwordEncoder.encode(eq(reqVO.getPassword()))).thenReturn("yunxiyuanma"); + + // 调用 + Long userId = userService.createUser(reqVO); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertPojoEquals(reqVO, user, "password"); + assertEquals("yunxiyuanma", user.getPassword()); + assertEquals(CommonStatusEnum.ENABLE.getStatus(), user.getStatus()); + // 断言关联岗位 + List userPosts = userPostMapper.selectListByUserId(user.getId()); + assertEquals(1L, userPosts.get(0).getPostId()); + assertEquals(2L, userPosts.get(1).getPostId()); + } + + @Test + public void testCreatUser_max() { + // 准备参数 + UserCreateReqVO reqVO = randomPojo(UserCreateReqVO.class); + // mock 账户额度不足 + TenantDO tenant = randomPojo(TenantDO.class, o -> o.setAccountCount(-1)); + doNothing().when(tenantService).handleTenantInfo(argThat(handler -> { + handler.handle(tenant); + return true; + })); + + // 调用,并断言异常 + assertServiceException(() -> userService.createUser(reqVO), USER_COUNT_MAX, -1); + } + + @Test + public void testUpdateUser_success() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(o -> o.setPostIds(asSet(1L, 2L))); + userMapper.insert(dbUser); + userPostMapper.insert(new UserPostDO().setUserId(dbUser.getId()).setPostId(1L)); + userPostMapper.insert(new UserPostDO().setUserId(dbUser.getId()).setPostId(2L)); + // 准备参数 + UserUpdateReqVO reqVO = randomPojo(UserUpdateReqVO.class, o -> { + o.setId(dbUser.getId()); + o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); + o.setMobile(randomString()); + o.setPostIds(asSet(2L, 3L)); + }); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(reqVO.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + // mock postService 的方法 + List posts = CollectionUtils.convertList(reqVO.getPostIds(), postId -> + randomPojo(PostDO.class, o -> { + o.setId(postId); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + })); + when(postService.getPostList(eq(reqVO.getPostIds()), isNull())).thenReturn(posts); + + // 调用 + userService.updateUser(reqVO); + // 断言 + AdminUserDO user = userMapper.selectById(reqVO.getId()); + assertPojoEquals(reqVO, user); + // 断言关联岗位 + List userPosts = userPostMapper.selectListByUserId(user.getId()); + assertEquals(2L, userPosts.get(0).getPostId()); + assertEquals(3L, userPosts.get(1).getPostId()); + } + + @Test + public void testUpdateUserLogin() { + // mock 数据 + AdminUserDO user = randomAdminUserDO(o -> o.setLoginDate(null)); + userMapper.insert(user); + // 准备参数 + Long id = user.getId(); + String loginIp = randomString(); + + // 调用 + userService.updateUserLogin(id, loginIp); + // 断言 + AdminUserDO dbUser = userMapper.selectById(id); + assertEquals(loginIp, dbUser.getLoginIp()); + assertNotNull(dbUser.getLoginDate()); + } + + @Test + public void testUpdateUserProfile_success() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + UserProfileUpdateReqVO reqVO = randomPojo(UserProfileUpdateReqVO.class, o -> { + o.setMobile(randomString()); + o.setSex(RandomUtil.randomEle(SexEnum.values()).getSex()); + }); + + // 调用 + userService.updateUserProfile(userId, reqVO); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertPojoEquals(reqVO, user); + } + + @Test + public void testUpdateUserPassword_success() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(o -> o.setPassword("encode:tudou")); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + UserProfileUpdatePasswordReqVO reqVO = randomPojo(UserProfileUpdatePasswordReqVO.class, o -> { + o.setOldPassword("tudou"); + o.setNewPassword("yuanma"); + }); + // mock 方法 + when(passwordEncoder.encode(anyString())).then( + (Answer) invocationOnMock -> "encode:" + invocationOnMock.getArgument(0)); + when(passwordEncoder.matches(eq(reqVO.getOldPassword()), eq(dbUser.getPassword()))).thenReturn(true); + + // 调用 + userService.updateUserPassword(userId, reqVO); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertEquals("encode:yuanma", user.getPassword()); + } + + @Test + public void testUpdateUserAvatar_success() throws Exception { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + byte[] avatarFileBytes = randomBytes(10); + ByteArrayInputStream avatarFile = new ByteArrayInputStream(avatarFileBytes); + // mock 方法 + String avatar = randomString(); + when(fileApi.createFile(eq( avatarFileBytes))).thenReturn(avatar); + + // 调用 + userService.updateUserAvatar(userId, avatarFile); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertEquals(avatar, user.getAvatar()); + } + + @Test + public void testUpdateUserPassword02_success() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + String password = "yunxi"; + // mock 方法 + when(passwordEncoder.encode(anyString())).then( + (Answer) invocationOnMock -> "encode:" + invocationOnMock.getArgument(0)); + + // 调用 + userService.updateUserPassword(userId, password); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertEquals("encode:" + password, user.getPassword()); + } + + @Test + public void testUpdateUserStatus() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + Integer status = randomCommonStatus(); + + // 调用 + userService.updateUserStatus(userId, status); + // 断言 + AdminUserDO user = userMapper.selectById(userId); + assertEquals(status, user.getStatus()); + } + + @Test + public void testDeleteUser_success(){ + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + + // 调用数据 + userService.deleteUser(userId); + // 校验结果 + assertNull(userMapper.selectById(userId)); + // 校验调用次数 + verify(permissionService, times(1)).processUserDeleted(eq(userId)); + } + + @Test + public void testGetUserByUsername() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + String username = dbUser.getUsername(); + + // 调用 + AdminUserDO user = userService.getUserByUsername(username); + // 断言 + assertPojoEquals(dbUser, user); + } + + @Test + public void testGetUserByMobile() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + String mobile = dbUser.getMobile(); + + // 调用 + AdminUserDO user = userService.getUserByMobile(mobile); + // 断言 + assertPojoEquals(dbUser, user); + } + + @Test + public void testGetUserPage() { + // mock 数据 + AdminUserDO dbUser = initGetUserPageData(); + // 准备参数 + UserPageReqVO reqVO = new UserPageReqVO(); + reqVO.setUsername("tu"); + reqVO.setMobile("1560"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); + reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门 + // mock 方法 + List deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); + when(deptService.getChildDeptList(eq(reqVO.getDeptId()))).thenReturn(deptList); + + // 调用 + PageResult pageResult = userService.getUserPage(reqVO); + // 断言 + assertEquals(1, pageResult.getTotal()); + assertEquals(1, pageResult.getList().size()); + assertPojoEquals(dbUser, pageResult.getList().get(0)); + } + + @Test + public void testGetUserList_export() { + // mock 数据 + AdminUserDO dbUser = initGetUserPageData(); + // 准备参数 + UserExportReqVO reqVO = new UserExportReqVO(); + reqVO.setUsername("tu"); + reqVO.setMobile("1560"); + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus()); + reqVO.setCreateTime(buildBetweenTime(2020, 12, 1, 2020, 12, 24)); + reqVO.setDeptId(1L); // 其中,1L 是 2L 的父部门 + // mock 方法 + List deptList = newArrayList(randomPojo(DeptDO.class, o -> o.setId(2L))); + when(deptService.getChildDeptList(eq(reqVO.getDeptId()))).thenReturn(deptList); + + // 调用 + List list = userService.getUserList(reqVO); + // 断言 + assertEquals(1, list.size()); + assertPojoEquals(dbUser, list.get(0)); + } + + /** + * 初始化 getUserPage 方法的测试数据 + */ + private AdminUserDO initGetUserPageData() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(o -> { // 等会查询到 + o.setUsername("tudou"); + o.setMobile("15601691300"); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + o.setCreateTime(buildTime(2020, 12, 12)); + o.setDeptId(2L); + }); + userMapper.insert(dbUser); + // 测试 username 不匹配 + userMapper.insert(cloneIgnoreId(dbUser, o -> o.setUsername("dou"))); + // 测试 mobile 不匹配 + userMapper.insert(cloneIgnoreId(dbUser, o -> o.setMobile("18818260888"))); + // 测试 status 不匹配 + userMapper.insert(cloneIgnoreId(dbUser, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus()))); + // 测试 createTime 不匹配 + userMapper.insert(cloneIgnoreId(dbUser, o -> o.setCreateTime(buildTime(2020, 11, 11)))); + // 测试 dept 不匹配 + userMapper.insert(cloneIgnoreId(dbUser, o -> o.setDeptId(0L))); + return dbUser; + } + + @Test + public void testGetUser() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + Long userId = dbUser.getId(); + + // 调用 + AdminUserDO user = userService.getUser(userId); + // 断言 + assertPojoEquals(dbUser, user); + } + + @Test + public void testGetUserListByDeptIds() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(o -> o.setDeptId(1L)); + userMapper.insert(dbUser); + // 测试 deptId 不匹配 + userMapper.insert(cloneIgnoreId(dbUser, o -> o.setDeptId(2L))); + // 准备参数 + Collection deptIds = singleton(1L); + + // 调用 + List list = userService.getUserListByDeptIds(deptIds); + // 断言 + assertEquals(1, list.size()); + assertEquals(dbUser, list.get(0)); + } + + /** + * 情况一,校验不通过,导致插入失败 + */ + @Test + public void testImportUserList_01() { + // 准备参数 + UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { + }); + // mock 方法,模拟失败 + doThrow(new ServiceException(DEPT_NOT_FOUND)).when(deptService).validateDeptList(any()); + + // 调用 + UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), true); + // 断言 + assertEquals(0, respVO.getCreateUsernames().size()); + assertEquals(0, respVO.getUpdateUsernames().size()); + assertEquals(1, respVO.getFailureUsernames().size()); + assertEquals(DEPT_NOT_FOUND.getMsg(), respVO.getFailureUsernames().get(importUser.getUsername())); + } + + /** + * 情况二,不存在,进行插入 + */ + @Test + public void testImportUserList_02() { + // 准备参数 + UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 + }); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(importUser.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + // mock passwordEncoder 的方法 + when(passwordEncoder.encode(eq("yunxiyuanma"))).thenReturn("java"); + + // 调用 + UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), true); + // 断言 + assertEquals(1, respVO.getCreateUsernames().size()); + AdminUserDO user = userMapper.selectByUsername(respVO.getCreateUsernames().get(0)); + assertPojoEquals(importUser, user); + assertEquals("java", user.getPassword()); + assertEquals(0, respVO.getUpdateUsernames().size()); + assertEquals(0, respVO.getFailureUsernames().size()); + } + + /** + * 情况三,存在,但是不强制更新 + */ + @Test + public void testImportUserList_03() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 + o.setUsername(dbUser.getUsername()); + }); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(importUser.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + + // 调用 + UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), false); + // 断言 + assertEquals(0, respVO.getCreateUsernames().size()); + assertEquals(0, respVO.getUpdateUsernames().size()); + assertEquals(1, respVO.getFailureUsernames().size()); + assertEquals(USER_USERNAME_EXISTS.getMsg(), respVO.getFailureUsernames().get(importUser.getUsername())); + } + + /** + * 情况四,存在,强制更新 + */ + @Test + public void testImportUserList_04() { + // mock 数据 + AdminUserDO dbUser = randomAdminUserDO(); + userMapper.insert(dbUser); + // 准备参数 + UserImportExcelVO importUser = randomPojo(UserImportExcelVO.class, o -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 + o.setUsername(dbUser.getUsername()); + }); + // mock deptService 的方法 + DeptDO dept = randomPojo(DeptDO.class, o -> { + o.setId(importUser.getDeptId()); + o.setStatus(CommonStatusEnum.ENABLE.getStatus()); + }); + when(deptService.getDept(eq(dept.getId()))).thenReturn(dept); + + // 调用 + UserImportRespVO respVO = userService.importUserList(newArrayList(importUser), true); + // 断言 + assertEquals(0, respVO.getCreateUsernames().size()); + assertEquals(1, respVO.getUpdateUsernames().size()); + AdminUserDO user = userMapper.selectByUsername(respVO.getUpdateUsernames().get(0)); + assertPojoEquals(importUser, user); + assertEquals(0, respVO.getFailureUsernames().size()); + } + + @Test + public void testValidateUserExists_notExists() { + assertServiceException(() -> userService.validateUserExists(randomLongId()), USER_NOT_EXISTS); + } + + @Test + public void testValidateUsernameUnique_usernameExistsForCreate() { + // 准备参数 + String username = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setUsername(username))); + + // 调用,校验异常 + assertServiceException(() -> userService.validateUsernameUnique(null, username), + USER_USERNAME_EXISTS); + } + + @Test + public void testValidateUsernameUnique_usernameExistsForUpdate() { + // 准备参数 + Long id = randomLongId(); + String username = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setUsername(username))); + + // 调用,校验异常 + assertServiceException(() -> userService.validateUsernameUnique(id, username), + USER_USERNAME_EXISTS); + } + + @Test + public void testValidateEmailUnique_emailExistsForCreate() { + // 准备参数 + String email = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setEmail(email))); + + // 调用,校验异常 + assertServiceException(() -> userService.validateEmailUnique(null, email), + USER_EMAIL_EXISTS); + } + + @Test + public void testValidateEmailUnique_emailExistsForUpdate() { + // 准备参数 + Long id = randomLongId(); + String email = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setEmail(email))); + + // 调用,校验异常 + assertServiceException(() -> userService.validateEmailUnique(id, email), + USER_EMAIL_EXISTS); + } + + @Test + public void testValidateMobileUnique_mobileExistsForCreate() { + // 准备参数 + String mobile = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile))); + + // 调用,校验异常 + assertServiceException(() -> userService.validateMobileUnique(null, mobile), + USER_MOBILE_EXISTS); + } + + @Test + public void testValidateMobileUnique_mobileExistsForUpdate() { + // 准备参数 + Long id = randomLongId(); + String mobile = randomString(); + // mock 数据 + userMapper.insert(randomAdminUserDO(o -> o.setMobile(mobile))); + + // 调用,校验异常 + assertServiceException(() -> userService.validateMobileUnique(id, mobile), + USER_MOBILE_EXISTS); + } + + @Test + public void testValidateOldPassword_notExists() { + assertServiceException(() -> userService.validateOldPassword(randomLongId(), randomString()), + USER_NOT_EXISTS); + } + + @Test + public void testValidateOldPassword_passwordFailed() { + // mock 数据 + AdminUserDO user = randomAdminUserDO(); + userMapper.insert(user); + // 准备参数 + Long id = user.getId(); + String oldPassword = user.getPassword(); + + // 调用,校验异常 + assertServiceException(() -> userService.validateOldPassword(id, oldPassword), + USER_PASSWORD_FAILED); + // 校验调用 + verify(passwordEncoder, times(1)).matches(eq(oldPassword), eq(user.getPassword())); + } + + @Test + public void testUserListByPostIds() { + // 准备参数 + Collection postIds = asSet(10L, 20L); + // mock user1 数据 + AdminUserDO user1 = randomAdminUserDO(o -> o.setPostIds(asSet(10L, 30L))); + userMapper.insert(user1); + userPostMapper.insert(new UserPostDO().setUserId(user1.getId()).setPostId(10L)); + userPostMapper.insert(new UserPostDO().setUserId(user1.getId()).setPostId(30L)); + // mock user2 数据 + AdminUserDO user2 = randomAdminUserDO(o -> o.setPostIds(singleton(100L))); + userMapper.insert(user2); + userPostMapper.insert(new UserPostDO().setUserId(user2.getId()).setPostId(100L)); + + // 调用 + List result = userService.getUserListByPostIds(postIds); + // 断言 + assertEquals(1, result.size()); + assertEquals(user1, result.get(0)); + } + + @Test + public void testGetUserList() { + // mock 数据 + AdminUserDO user = randomAdminUserDO(); + userMapper.insert(user); + // 测试 id 不匹配 + userMapper.insert(randomAdminUserDO()); + // 准备参数 + Collection ids = singleton(user.getId()); + + // 调用 + List result = userService.getUserList(ids); + // 断言 + assertEquals(1, result.size()); + assertEquals(user, result.get(0)); + } + + @Test + public void testGetUserMap() { + // mock 数据 + AdminUserDO user = randomAdminUserDO(); + userMapper.insert(user); + // 测试 id 不匹配 + userMapper.insert(randomAdminUserDO()); + // 准备参数 + Collection ids = singleton(user.getId()); + + // 调用 + Map result = userService.getUserMap(ids); + // 断言 + assertEquals(1, result.size()); + assertEquals(user, result.get(user.getId())); + } + + @Test + public void testGetUserListByNickname() { + // mock 数据 + AdminUserDO user = randomAdminUserDO(o -> o.setNickname("芋头")); + userMapper.insert(user); + // 测试 nickname 不匹配 + userMapper.insert(randomAdminUserDO(o -> o.setNickname("源码"))); + // 准备参数 + String nickname = "芋"; + + // 调用 + List result = userService.getUserListByNickname(nickname); + // 断言 + assertEquals(1, result.size()); + assertEquals(user, result.get(0)); + } + + @Test + public void testGetUserListByStatus() { + // mock 数据 + AdminUserDO user = randomAdminUserDO(o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())); + userMapper.insert(user); + // 测试 status 不匹配 + userMapper.insert(randomAdminUserDO(o -> o.setStatus(CommonStatusEnum.ENABLE.getStatus()))); + // 准备参数 + Integer status = CommonStatusEnum.DISABLE.getStatus(); + + // 调用 + List result = userService.getUserListByStatus(status); + // 断言 + assertEquals(1, result.size()); + assertEquals(user, result.get(0)); + } + + @Test + public void testValidateUserList_success() { + // mock 数据 + AdminUserDO userDO = randomAdminUserDO().setStatus(CommonStatusEnum.ENABLE.getStatus()); + userMapper.insert(userDO); + // 准备参数 + List ids = singletonList(userDO.getId()); + + // 调用,无需断言 + userService.validateUserList(ids); + } + + @Test + public void testValidateUserList_notFound() { + // 准备参数 + List ids = singletonList(randomLongId()); + + // 调用, 并断言异常 + assertServiceException(() -> userService.validateUserList(ids), USER_NOT_EXISTS); + } + + @Test + public void testValidateUserList_notEnable() { + // mock 数据 + AdminUserDO userDO = randomAdminUserDO().setStatus(CommonStatusEnum.DISABLE.getStatus()); + userMapper.insert(userDO); + // 准备参数 + List ids = singletonList(userDO.getId()); + + // 调用, 并断言异常 + assertServiceException(() -> userService.validateUserList(ids), USER_IS_DISABLE, + userDO.getNickname()); + } + + // ========== 随机对象 ========== + + @SafeVarargs + private static AdminUserDO randomAdminUserDO(Consumer... consumers) { + Consumer consumer = (o) -> { + o.setStatus(randomEle(CommonStatusEnum.values()).getStatus()); // 保证 status 的范围 + o.setSex(randomEle(SexEnum.values()).getSex()); // 保证 sex 的范围 + }; + return randomPojo(AdminUserDO.class, ArrayUtils.append(consumer, consumers)); + } + +} diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/resources/application-unit-test.yaml b/yunxi-module-system/yunxi-module-system-biz/src/test/resources/application-unit-test.yaml new file mode 100644 index 0000000..b65a147 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/resources/application-unit-test.yaml @@ -0,0 +1,55 @@ +spring: + main: + lazy-initialization: true # 开启懒加载,加快速度 + banner-mode: off # 单元测试,禁用 Banner + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + datasource: + name: ruoyi-vue-pro + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + driver-class-name: org.h2.Driver + username: sa + password: + druid: + async-init: true # 单元测试,异步初始化 Druid 连接池,提升启动速度 + initial-size: 1 # 单元测试,配置为 1,提升启动速度 + sql: + init: + schema-locations: classpath:/sql/create_tables.sql + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 16379 # 端口(单元测试,使用 16379 端口) + database: 0 # 数据库索引 + + +mybatis: + lazy-initialization: true # 单元测试,设置 MyBatis Mapper 延迟加载,加速每个单元测试 + +--- #################### 定时任务相关配置 #################### + +--- #################### 配置中心相关配置 #################### + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项(单元测试,禁用 Lock4j) + +# Resilience4j 配置项 + +--- #################### 监控相关配置 #################### + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + info: + base-package: com.yunxi.scm.module + captcha: + timeout: 5m + width: 160 + height: 60 + enable: true diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/resources/logback.xml b/yunxi-module-system/yunxi-module-system-biz/src/test/resources/logback.xml new file mode 100644 index 0000000..daf756b --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/resources/logback.xml @@ -0,0 +1,4 @@ + + + + diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/resources/sql/clean.sql b/yunxi-module-system/yunxi-module-system-biz/src/test/resources/sql/clean.sql new file mode 100644 index 0000000..785e5ea --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/resources/sql/clean.sql @@ -0,0 +1,34 @@ +DELETE FROM "system_dept"; +DELETE FROM "system_dict_data"; +DELETE FROM "system_role"; +DELETE FROM "system_role_menu"; +DELETE FROM "system_menu"; +DELETE FROM "system_user_role"; +DELETE FROM "system_dict_type"; +DELETE FROM "system_user_session"; +DELETE FROM "system_post"; +DELETE FROM "system_user_post"; +DELETE FROM "system_notice"; +DELETE FROM "system_login_log"; +DELETE FROM "system_operate_log"; +DELETE FROM "system_users"; +DELETE FROM "system_sms_channel"; +DELETE FROM "system_sms_template"; +DELETE FROM "system_sms_log"; +DELETE FROM "system_sms_code"; +DELETE FROM "system_error_code"; +DELETE FROM "system_social_user"; +DELETE FROM "system_social_user_bind"; +DELETE FROM "system_tenant"; +DELETE FROM "system_tenant_package"; +DELETE FROM "system_sensitive_word"; +DELETE FROM "system_oauth2_client"; +DELETE FROM "system_oauth2_approve"; +DELETE FROM "system_oauth2_access_token"; +DELETE FROM "system_oauth2_refresh_token"; +DELETE FROM "system_oauth2_code"; +DELETE FROM "system_mail_account"; +DELETE FROM "system_mail_template"; +DELETE FROM "system_mail_log"; +DELETE FROM "system_notify_template"; +DELETE FROM "system_notify_message"; diff --git a/yunxi-module-system/yunxi-module-system-biz/src/test/resources/sql/create_tables.sql b/yunxi-module-system/yunxi-module-system-biz/src/test/resources/sql/create_tables.sql new file mode 100644 index 0000000..ca85884 --- /dev/null +++ b/yunxi-module-system/yunxi-module-system-biz/src/test/resources/sql/create_tables.sql @@ -0,0 +1,630 @@ +CREATE TABLE IF NOT EXISTS "system_dept" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(30) NOT NULL DEFAULT '', + "parent_id" bigint NOT NULL DEFAULT '0', + "sort" int NOT NULL DEFAULT '0', + "leader_user_id" bigint DEFAULT NULL, + "phone" varchar(11) DEFAULT NULL, + "email" varchar(50) DEFAULT NULL, + "status" tinyint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '部门表'; + +CREATE TABLE IF NOT EXISTS "system_dict_data" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "sort" int NOT NULL DEFAULT '0', + "label" varchar(100) NOT NULL DEFAULT '', + "value" varchar(100) NOT NULL DEFAULT '', + "dict_type" varchar(100) NOT NULL DEFAULT '', + "status" tinyint NOT NULL DEFAULT '0', + "color_type" varchar(100) NOT NULL DEFAULT '', + "css_class" varchar(100) NOT NULL DEFAULT '', + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '字典数据表'; + +CREATE TABLE IF NOT EXISTS "system_role" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(30) NOT NULL, + "code" varchar(100) NOT NULL, + "sort" int NOT NULL, + "data_scope" tinyint NOT NULL DEFAULT '1', + "data_scope_dept_ids" varchar(500) NOT NULL DEFAULT '', + "status" tinyint NOT NULL, + "type" tinyint NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '角色信息表'; + +CREATE TABLE IF NOT EXISTS "system_role_menu" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "role_id" bigint NOT NULL, + "menu_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '角色和菜单关联表'; + +CREATE TABLE IF NOT EXISTS "system_menu" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(50) NOT NULL, + "permission" varchar(100) NOT NULL DEFAULT '', + "type" tinyint NOT NULL, + "sort" int NOT NULL DEFAULT '0', + "parent_id" bigint NOT NULL DEFAULT '0', + "path" varchar(200) DEFAULT '', + "icon" varchar(100) DEFAULT '#', + "component" varchar(255) DEFAULT NULL, + "component_name" varchar(255) DEFAULT NULL, + "status" tinyint NOT NULL DEFAULT '0', + "visible" bit NOT NULL DEFAULT TRUE, + "keep_alive" bit NOT NULL DEFAULT TRUE, + "always_show" bit NOT NULL DEFAULT TRUE, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '菜单权限表'; + +CREATE TABLE IF NOT EXISTS "system_user_role" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "role_id" bigint NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp DEFAULT NULL, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp DEFAULT NULL, + "deleted" bit DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '用户和角色关联表'; + +CREATE TABLE IF NOT EXISTS "system_dict_type" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(100) NOT NULL DEFAULT '', + "type" varchar(100) NOT NULL DEFAULT '', + "status" tinyint NOT NULL DEFAULT '0', + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "deleted_time" timestamp NOT NULL, + PRIMARY KEY ("id") +) COMMENT '字典类型表'; + +CREATE TABLE IF NOT EXISTS `system_user_session` ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `token` varchar(32) NOT NULL, + `user_id` bigint DEFAULT NULL, + "user_type" tinyint NOT NULL, + `username` varchar(50) NOT NULL DEFAULT '', + `user_ip` varchar(50) DEFAULT NULL, + `user_agent` varchar(512) DEFAULT NULL, + `session_timeout` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) DEFAULT '' , + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY (`id`) +) COMMENT '用户在线 Session'; + +CREATE TABLE IF NOT EXISTS "system_post" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "code" varchar(64) NOT NULL, + "name" varchar(50) NOT NULL, + "sort" integer NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(500) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '岗位信息表'; + +CREATE TABLE IF NOT EXISTS `system_user_post`( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint DEFAULT NULL, + "post_id" bigint DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY (`id`) +) COMMENT ='用户岗位表'; + +CREATE TABLE IF NOT EXISTS "system_notice" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "title" varchar(50) NOT NULL COMMENT '公告标题', + "content" text NOT NULL COMMENT '公告内容', + "type" tinyint NOT NULL COMMENT '公告类型(1通知 2公告)', + "status" tinyint NOT NULL DEFAULT '0' COMMENT '公告状态(0正常 1关闭)', + "creator" varchar(64) DEFAULT '' COMMENT '创建者', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + "updater" varchar(64) DEFAULT '' COMMENT '更新者', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + "deleted" bit NOT NULL DEFAULT 0 COMMENT '是否删除', + "tenant_id" bigint not null default '0', + PRIMARY KEY("id") +) COMMENT '通知公告表'; + +CREATE TABLE IF NOT EXISTS `system_login_log` ( + `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `log_type` bigint(4) NOT NULL, + "user_id" bigint not null default '0', + "user_type" tinyint NOT NULL, + `trace_id` varchar(64) NOT NULL DEFAULT '', + `username` varchar(50) NOT NULL DEFAULT '', + `result` tinyint(4) NOT NULL, + `user_ip` varchar(50) NOT NULL, + `user_agent` varchar(512) NOT NULL, + `creator` varchar(64) DEFAULT '', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) DEFAULT '', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`) +) COMMENT ='系统访问记录'; + +CREATE TABLE IF NOT EXISTS `system_operate_log` ( + `id` bigint(20) NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `trace_id` varchar(64) NOT NULL DEFAULT '', + `user_id` bigint(20) NOT NULL, + "user_type" tinyint not null default '0', + `module` varchar(50) NOT NULL, + `name` varchar(50) NOT NULL, + `type` bigint(4) NOT NULL DEFAULT '0', + `content` varchar(2000) NOT NULL DEFAULT '', + `exts` varchar(512) NOT NULL DEFAULT '', + `request_method` varchar(16) DEFAULT '', + `request_url` varchar(255) DEFAULT '', + `user_ip` varchar(50) DEFAULT NULL, + `user_agent` varchar(200) DEFAULT NULL, + `java_method` varchar(512) NOT NULL DEFAULT '', + `java_method_args` varchar(8000) DEFAULT '', + `start_time` datetime NOT NULL, + `duration` int(11) NOT NULL, + `result_code` int(11) NOT NULL DEFAULT '0', + `result_msg` varchar(512) DEFAULT '', + `result_data` varchar(4000) DEFAULT '', + `creator` varchar(64) DEFAULT '', + `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) DEFAULT '', + `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT '0', + "tenant_id" bigint not null default '0', + PRIMARY KEY (`id`) +) COMMENT ='操作日志记录'; + +CREATE TABLE IF NOT EXISTS "system_users" ( + "id" bigint not null GENERATED BY DEFAULT AS IDENTITY, + "username" varchar(30) not null, + "password" varchar(100) not null default '', + "nickname" varchar(30) not null, + "remark" varchar(500) default null, + "dept_id" bigint default null, + "post_ids" varchar(255) default null, + "email" varchar(50) default '', + "mobile" varchar(11) default '', + "sex" tinyint default '0', + "avatar" varchar(100) default '', + "status" tinyint not null default '0', + "login_ip" varchar(50) default '', + "login_date" timestamp default null, + "creator" varchar(64) default '', + "create_time" timestamp not null default current_timestamp, + "updater" varchar(64) default '', + "update_time" timestamp not null default current_timestamp, + "deleted" bit not null default false, + "tenant_id" bigint not null default '0', + primary key ("id") +) comment '用户信息表'; + +CREATE TABLE IF NOT EXISTS "system_sms_channel" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "signature" varchar(10) NOT NULL, + "code" varchar(63) NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "api_key" varchar(63) NOT NULL, + "api_secret" varchar(63) DEFAULT NULL, + "callback_url" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信渠道'; + +CREATE TABLE IF NOT EXISTS "system_sms_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "type" tinyint NOT NULL, + "status" tinyint NOT NULL, + "code" varchar(63) NOT NULL, + "name" varchar(63) NOT NULL, + "content" varchar(255) NOT NULL, + "params" varchar(255) NOT NULL, + "remark" varchar(255) DEFAULT NULL, + "api_template_id" varchar(63) NOT NULL, + "channel_id" bigint NOT NULL, + "channel_code" varchar(63) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信模板'; + +CREATE TABLE IF NOT EXISTS "system_sms_log" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "channel_id" bigint NOT NULL, + "channel_code" varchar(63) NOT NULL, + "template_id" bigint NOT NULL, + "template_code" varchar(63) NOT NULL, + "template_type" tinyint NOT NULL, + "template_content" varchar(255) NOT NULL, + "template_params" varchar(255) NOT NULL, + "api_template_id" varchar(63) NOT NULL, + "mobile" varchar(11) NOT NULL, + "user_id" bigint DEFAULT '0', + "user_type" tinyint DEFAULT '0', + "send_status" tinyint NOT NULL DEFAULT '0', + "send_time" timestamp DEFAULT NULL, + "send_code" int DEFAULT NULL, + "send_msg" varchar(255) DEFAULT NULL, + "api_send_code" varchar(63) DEFAULT NULL, + "api_send_msg" varchar(255) DEFAULT NULL, + "api_request_id" varchar(255) DEFAULT NULL, + "api_serial_no" varchar(255) DEFAULT NULL, + "receive_status" tinyint NOT NULL DEFAULT '0', + "receive_time" timestamp DEFAULT NULL, + "api_receive_code" varchar(63) DEFAULT NULL, + "api_receive_msg" varchar(255) DEFAULT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信日志'; + +CREATE TABLE IF NOT EXISTS "system_sms_code" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "mobile" varchar(11) NOT NULL, + "code" varchar(11) NOT NULL, + "scene" bigint NOT NULL, + "create_ip" varchar NOT NULL, + "today_index" int NOT NULL, + "used" bit NOT NULL DEFAULT FALSE, + "used_time" timestamp DEFAULT NULL, + "used_ip" varchar NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '短信日志'; + +CREATE TABLE IF NOT EXISTS "system_error_code" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "type" tinyint NOT NULL DEFAULT '0', + "application_name" varchar(50) NOT NULL, + "code" int NOT NULL DEFAULT '0', + "message" varchar(512) NOT NULL DEFAULT '', + "memo" varchar(512) DEFAULT '', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '错误码表'; + +CREATE TABLE IF NOT EXISTS "system_social_user" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "type" tinyint NOT NULL, + "openid" varchar(64) NOT NULL, + "token" varchar(256) DEFAULT NULL, + "raw_token_info" varchar(1024) NOT NULL, + "nickname" varchar(32) NOT NULL, + "avatar" varchar(255) DEFAULT NULL, + "raw_user_info" varchar(1024) NOT NULL, + "code" varchar(64) NOT NULL, + "state" varchar(64), + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '社交用户'; + +CREATE TABLE IF NOT EXISTS "system_social_user_bind" ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "social_type" tinyint NOT NULL, + "social_user_id" number NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '社交用户的绑定'; + +CREATE TABLE IF NOT EXISTS "system_tenant" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "contact_user_id" bigint NOT NULL DEFAULT '0', + "contact_name" varchar(255) NOT NULL, + "contact_mobile" varchar(255), + "status" tinyint NOT NULL, + "domain" varchar(63) DEFAULT '', + "package_id" bigint NOT NULL, + "expire_time" timestamp NOT NULL, + "account_count" int NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '租户'; + +CREATE TABLE IF NOT EXISTS "system_tenant_package" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(30) NOT NULL, + "status" tinyint NOT NULL, + "remark" varchar(256), + "menu_ids" varchar(2048) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '租户套餐表'; + +CREATE TABLE IF NOT EXISTS "system_sensitive_word" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(255) NOT NULL, + "tags" varchar(1024) NOT NULL, + "status" bit NOT NULL DEFAULT FALSE, + "description" varchar(512), + "creator" varchar(64) DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '系统敏感词'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_client" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "client_id" varchar NOT NULL, + "secret" varchar NOT NULL, + "name" varchar NOT NULL, + "logo" varchar NOT NULL, + "description" varchar, + "status" int NOT NULL, + "access_token_validity_seconds" int NOT NULL, + "refresh_token_validity_seconds" int NOT NULL, + "redirect_uris" varchar NOT NULL, + "authorized_grant_types" varchar NOT NULL, + "scopes" varchar NOT NULL DEFAULT '', + "auto_approve_scopes" varchar NOT NULL DEFAULT '', + "authorities" varchar NOT NULL DEFAULT '', + "resource_ids" varchar NOT NULL DEFAULT '', + "additional_information" varchar NOT NULL DEFAULT '', + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 客户端表'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_approve" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "client_id" varchar NOT NULL, + "scope" varchar NOT NULL, + "approved" bit NOT NULL DEFAULT FALSE, + "expires_time" datetime NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 批准表'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_access_token" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "access_token" varchar NOT NULL, + "refresh_token" varchar NOT NULL, + "client_id" varchar NOT NULL, + "scopes" varchar NOT NULL, + "approved" bit NOT NULL DEFAULT FALSE, + "expires_time" datetime NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint NOT NULL, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 访问令牌'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_refresh_token" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "refresh_token" varchar NOT NULL, + "client_id" varchar NOT NULL, + "scopes" varchar NOT NULL, + "approved" bit NOT NULL DEFAULT FALSE, + "expires_time" datetime NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 刷新令牌'; + +CREATE TABLE IF NOT EXISTS "system_oauth2_code" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" tinyint NOT NULL, + "code" varchar NOT NULL, + "client_id" varchar NOT NULL, + "scopes" varchar NOT NULL, + "expires_time" datetime NOT NULL, + "redirect_uri" varchar NOT NULL, + "state" varchar NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OAuth2 刷新令牌'; + +CREATE TABLE IF NOT EXISTS "system_mail_account" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "mail" varchar NOT NULL, + "username" varchar NOT NULL, + "password" varchar NOT NULL, + "host" varchar NOT NULL, + "port" int NOT NULL, + "ssl_enable" bit NOT NULL, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '邮箱账号表'; + +CREATE TABLE IF NOT EXISTS "system_mail_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "code" varchar NOT NULL, + "account_id" bigint NOT NULL, + "nickname" varchar, + "title" varchar NOT NULL, + "content" varchar NOT NULL, + "params" varchar NOT NULL, + "status" varchar NOT NULL, + "remark" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '邮件模版表'; + +CREATE TABLE IF NOT EXISTS "system_mail_log" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint, + "user_type" varchar, + "to_mail" varchar NOT NULL, + "account_id" bigint NOT NULL, + "from_mail" varchar NOT NULL, + "template_id" bigint NOT NULL, + "template_code" varchar NOT NULL, + "template_nickname" varchar, + "template_title" varchar NOT NULL, + "template_content" varchar NOT NULL, + "template_params" varchar NOT NULL, + "send_status" varchar NOT NULL, + "send_time" datetime, + "send_message_id" varchar, + "send_exception" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '邮件日志表'; + +-- 将该建表 SQL 语句,添加到 yunxi-module-system-biz 模块的 test/resources/sql/create_tables.sql 文件里 +CREATE TABLE IF NOT EXISTS "system_notify_template" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar NOT NULL, + "code" varchar NOT NULL, + "nickname" varchar NOT NULL, + "content" varchar NOT NULL, + "type" varchar NOT NULL, + "params" varchar, + "status" varchar NOT NULL, + "remark" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '站内信模板表'; + +CREATE TABLE IF NOT EXISTS "system_notify_message" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "user_type" varchar NOT NULL, + "template_id" bigint NOT NULL, + "template_code" varchar NOT NULL, + "template_nickname" varchar NOT NULL, + "template_content" varchar NOT NULL, + "template_type" int NOT NULL, + "template_params" varchar NOT NULL, + "read_status" bit NOT NULL, + "read_time" varchar, + "creator" varchar DEFAULT '', + "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar DEFAULT '', + "update_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + "tenant_id" bigint not null default '0', + PRIMARY KEY ("id") +) COMMENT '站内信消息表'; diff --git a/yunxi-server/Dockerfile b/yunxi-server/Dockerfile new file mode 100644 index 0000000..7051e80 --- /dev/null +++ b/yunxi-server/Dockerfile @@ -0,0 +1,23 @@ +## AdoptOpenJDK 停止发布 OpenJDK 二进制,而 Eclipse Temurin 是它的延伸,提供更好的稳定性 +## 感谢复旦核博士的建议!灰子哥,牛皮! +FROM eclipse-temurin:8-jre + +## 创建目录,并使用它作为工作目录 +RUN mkdir -p /yunxi-server +WORKDIR /yunxi-server +## 将后端项目的 Jar 文件,复制到镜像中 +COPY ./target/yunxi-server.jar app.jar + +## 设置 TZ 时区 +ENV TZ=Asia/Shanghai +## 设置 JAVA_OPTS 环境变量,可通过 docker run -e "JAVA_OPTS=" 进行覆盖 +ENV JAVA_OPTS="-Xms512m -Xmx512m -Djava.security.egd=file:/dev/./urandom" + +## 应用参数 +ENV ARGS="" + +## 暴露后端项目的 48080 端口 +EXPOSE 48080 + +## 启动后端项目 +CMD java ${JAVA_OPTS} -jar app.jar $ARGS diff --git a/yunxi-server/pom.xml b/yunxi-server/pom.xml new file mode 100644 index 0000000..930a81d --- /dev/null +++ b/yunxi-server/pom.xml @@ -0,0 +1,129 @@ + + + + com.yunxi.scm + yunxi + ${revision} + + 4.0.0 + + yunxi-server + jar + + ${project.artifactId} + + 后端 Server 的主项目,通过引入需要 yunxi-module-xxx 的依赖, + 从而实现提供 RESTful API 给 yunxi-ui-admin、yunxi-ui-user 等前端项目。 + 本质上来说,它就是个空壳(容器)! + + https://github.com/YunaiV/ruoyi-vue-pro + + + + com.yunxi.scm + yunxi-module-member-biz + ${revision} + + + com.yunxi.scm + yunxi-module-system-biz + ${revision} + + + com.yunxi.scm + yunxi-module-infra-biz + ${revision} + + + com.yunxi.scm + yunxi-spring-boot-starter-biz-error-code + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + com.yunxi.scm + yunxi-spring-boot-starter-banner + + + + + com.yunxi.scm + yunxi-spring-boot-starter-protection + + + + + + + ${project.artifactId} + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.13 + + true + + + + + repackage + + + + + + + + diff --git a/yunxi-server/src/main/java/com/yunxi/scm/server/YunxiServerApplication.java b/yunxi-server/src/main/java/com/yunxi/scm/server/YunxiServerApplication.java new file mode 100644 index 0000000..1ff2e5f --- /dev/null +++ b/yunxi-server/src/main/java/com/yunxi/scm/server/YunxiServerApplication.java @@ -0,0 +1,34 @@ +package com.yunxi.scm.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * 项目的启动类 + * + * 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + * 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + * + * @author 芋道源码 + */ +@SuppressWarnings("SpringComponentScan") // 忽略 IDEA 无法识别 ${yunxi.info.base-package} +@SpringBootApplication(scanBasePackages = {"${yunxi.info.base-package}.server", "${yunxi.info.base-package}.module"}) +public class YunxiServerApplication { + + public static void main(String[] args) { + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + + SpringApplication.run(YunxiServerApplication.class, args); +// new SpringApplicationBuilder(YunxiServerApplication.class) +// .applicationStartup(new BufferingApplicationStartup(20480)) +// .run(args); + + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + // 如果你碰到启动的问题,请认真阅读 https://doc.iocoder.cn/quick-start/ 文章 + } + +} diff --git a/yunxi-server/src/main/java/com/yunxi/scm/server/controller/DefaultController.java b/yunxi-server/src/main/java/com/yunxi/scm/server/controller/DefaultController.java new file mode 100644 index 0000000..0b228dc --- /dev/null +++ b/yunxi-server/src/main/java/com/yunxi/scm/server/controller/DefaultController.java @@ -0,0 +1,50 @@ +package com.yunxi.scm.server.controller; + +import com.yunxi.scm.framework.common.pojo.CommonResult; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import static com.yunxi.scm.framework.common.exception.enums.GlobalErrorCodeConstants.NOT_IMPLEMENTED; + +/** + * 默认 Controller,解决部分 module 未开启时的 404 提示。 + * 例如说,/bpm/** 路径,工作流 + * + * @author 芋道源码 + */ +@RestController +public class DefaultController { + + @RequestMapping("/admin-api/bpm/**") + public CommonResult bpm404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[工作流模块 yunxi-module-bpm - 已禁用][参考 https://doc.iocoder.cn/bpm/ 开启]"); + } + + @RequestMapping("/admin-api/mp/**") + public CommonResult mp404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[微信公众号 yunxi-module-mp - 已禁用][参考 https://doc.iocoder.cn/mp/build/ 开启]"); + } + + @RequestMapping(value = {"/admin-api/product/**", // 商品中心 + "/admin-api/trade/**", // 交易中心 + "/admin-api/promotion/**"}) // 营销中心 + public CommonResult mall404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[商城系统 yunxi-module-mall - 已禁用][参考 https://doc.iocoder.cn/mall/build/ 开启]"); + } + + @RequestMapping(value = {"/admin-api/report/**"}) + public CommonResult report404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[报表模块 yunxi-module-report - 已禁用][参考 https://doc.iocoder.cn/report/ 开启]"); + } + + @RequestMapping(value = {"/admin-api/pay/**"}) + public CommonResult pay404() { + return CommonResult.error(NOT_IMPLEMENTED.getCode(), + "[支付模块 yunxi-module-pay - 已禁用][参考 https://doc.iocoder.cn/pay/build/ 开启]"); + } + +} diff --git a/yunxi-server/src/main/resources/application-dev.yaml b/yunxi-server/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..1a7b417 --- /dev/null +++ b/yunxi-server/src/main/resources/application-dev.yaml @@ -0,0 +1,197 @@ +server: + port: 48080 + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration # 排除积木报表带来的 MongoDB 的自动配置 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: yxsass + url: jdbc:mysql://112.124.64.122:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + driver-class-name: com.mysql.jdbc.Driver + username: yxsass + password: jxjitjjxJFNM5YNW + slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改 + name: yxsass + url: jdbc:mysql://112.124.64.122:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + driver-class-name: com.mysql.jdbc.Driver + username: yxsass + password: jxjitjjxJFNM5YNW + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 400-infra.server.iocoder.cn # 地址 + port: 6379 # 端口 + database: 1 # 数据库索引 +# password: 123456 # 密码,建议生产环境开启 + +--- #################### 定时任务相关配置 #################### + +# Quartz 配置项,对应 QuartzProperties 配置类 +spring: + quartz: + auto-startup: true # 测试环境,需要开启 Job + scheduler-name: schedulerName # Scheduler 名字。默认为 schedulerName + job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 + wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true + properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 + org: + quartz: + # Scheduler 相关配置 + scheduler: + instanceName: schedulerName + instanceId: AUTO # 自动生成 instance ID + # JobStore 相关配置 + jobStore: + # JobStore 实现类。可见博客:https://blog.csdn.net/weixin_42458219/article/details/122247162 + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + isClustered: true # 是集群模式 + clusterCheckinInterval: 15000 # 集群检查频率,单位:毫秒。默认为 15000,即 15 秒 + misfireThreshold: 60000 # misfire 阀值,单位:毫秒。 + # 线程池相关配置 + threadPool: + threadCount: 25 # 线程池大小。默认为 10 。 + threadPriority: 5 # 线程优先级 + class: org.quartz.simpl.SimpleThreadPool # 线程池类型 + jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置 + initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址 + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + # Spring Boot Admin Server 服务端的相关配置 + context-path: /admin # 配置 Spring + +# 日志文件配置 +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + +--- #################### 微信公众号相关配置 #################### +wx: # 参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 + mp: + # 公众号配置(必填) + app-id: wx041349c6f39b268b + secret: 5abee519483bc9f8cb37ce280e814bd0 + # 存储配置,解决 AccessToken 的跨节点的共享 + config-storage: + type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 + key-prefix: wx # Redis Key 的前缀 + http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 + miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档 + appid: wx63c280fe3248a3e7 + secret: 6f270509224a7ae1296bbf1c8cb97aed + config-storage: + type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 + key-prefix: wa # Redis Key 的前缀 + http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + pay: + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 + demo: true # 开启演示模式 + +justauth: + enabled: true + type: + DINGTALK: # 钉钉 + client-id: dingvrnreaje3yqvzhxg + client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI + ignore-check-redirect-uri: true + WECHAT_ENTERPRISE: # 企业微信 + client-id: wwd411c69a39ad2e54 + client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw + agent-id: 1000004 + ignore-check-redirect-uri: true + cache: + type: REDIS + prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: + timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟 +wx: + mp: + useRedis: false + defaultContent: \u60A8\u597D\uFF0C\u6709\u4EC0\u4E48\u95EE\u9898\uFF1F + redisConfig: + host: 127.0.0.1 + port: 6379 + password: diff --git a/yunxi-server/src/main/resources/application-local.yaml b/yunxi-server/src/main/resources/application-local.yaml new file mode 100644 index 0000000..b0ffbbf --- /dev/null +++ b/yunxi-server/src/main/resources/application-local.yaml @@ -0,0 +1,227 @@ +server: + port: 48080 + +--- #################### 数据库相关配置 #################### + +spring: + # 数据源配置项 + autoconfigure: + exclude: + - com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源 + - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration # 排除积木报表带来的 MongoDB 的自动配置 + datasource: + druid: # Druid 【监控】相关的全局配置 + web-stat-filter: + enabled: true + stat-view-servlet: + enabled: true + allow: # 设置白名单,不填则允许所有访问 + url-pattern: /druid/* + login-username: # 控制台管理用户名和密码 + login-password: + filter: + stat: + enabled: true + log-slow-sql: true # 慢 SQL 记录 + slow-sql-millis: 100 + merge-sql: true + wall: + config: + multi-statement-allow: true + dynamic: # 多数据源配置 + druid: # Druid 【连接池】相关的全局配置 + initial-size: 5 # 初始连接数 + min-idle: 10 # 最小连接池数量 + max-active: 20 # 最大连接池数量 + max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 + min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 + max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 + test-while-idle: true + test-on-borrow: false + test-on-return: false + primary: master + datasource: + master: + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.master.name} # PostgreSQL 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.master.name} # SQLServer 连接的示例 + username: root + password: 123456 + # username: sa + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W + slave: # 模拟从库,可根据自己需要修改 + name: ruoyi-vue-pro + url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例 + # url: jdbc:mysql://127.0.0.1:3306/${spring.datasource.dynamic.datasource.slave.name}?useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=CTT # MySQL Connector/J 5.X 连接的示例 + # url: jdbc:postgresql://127.0.0.1:5432/${spring.datasource.dynamic.datasource.slave.name} # PostgreSQL 连接的示例 + # url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例 + # url: jdbc:sqlserver://127.0.0.1:1433;DatabaseName=${spring.datasource.dynamic.datasource.slave.name} # SQLServer 连接的示例 + username: root + password: 123456 + # username: sa + # password: JSm:g(*%lU4ZAkz06cd52KqT3)i1?H7W + + # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优 + redis: + host: 127.0.0.1 # 地址 + port: 6379 # 端口 + database: 0 # 数据库索引 +# password: dev # 密码,建议生产环境开启 + +--- #################### 定时任务相关配置 #################### + +# Quartz 配置项,对应 QuartzProperties 配置类 +spring: + quartz: + auto-startup: false # 本地开发环境,尽量不要开启 Job + scheduler-name: schedulerName # Scheduler 名字。默认为 schedulerName + job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。 + wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true + properties: # 添加 Quartz Scheduler 附加属性,更多可以看 http://www.quartz-scheduler.org/documentation/2.4.0-SNAPSHOT/configuration.html 文档 + org: + quartz: + # Scheduler 相关配置 + scheduler: + instanceName: schedulerName + instanceId: AUTO # 自动生成 instance ID + # JobStore 相关配置 + jobStore: + # JobStore 实现类。可见博客:https://blog.csdn.net/weixin_42458219/article/details/122247162 + class: org.springframework.scheduling.quartz.LocalDataSourceJobStore + isClustered: true # 是集群模式 + clusterCheckinInterval: 15000 # 集群检查频率,单位:毫秒。默认为 15000,即 15 秒 + misfireThreshold: 60000 # misfire 阀值,单位:毫秒。 + # 线程池相关配置 + threadPool: + threadCount: 25 # 线程池大小。默认为 10 。 + threadPriority: 5 # 线程优先级 + class: org.quartz.simpl.SimpleThreadPool # 线程池类型 + jdbc: # 使用 JDBC 的 JobStore 的时候,JDBC 的配置 + initialize-schema: NEVER # 是否自动使用 SQL 初始化 Quartz 表结构。这里设置成 never ,我们手动创建表结构。 + +--- #################### 服务保障相关配置 #################### + +# Lock4j 配置项 +lock4j: + acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 + expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒 + +# Resilience4j 配置项 +resilience4j: + ratelimiter: + instances: + backendA: + limit-for-period: 1 # 每个周期内,允许的请求数。默认为 50 + limit-refresh-period: 60s # 每个周期的时长,单位:微秒。默认为 500 + timeout-duration: 1s # 被限流时,阻塞等待的时长,单位:微秒。默认为 5s + register-health-indicator: true # 是否注册到健康监测 + +--- #################### 监控相关配置 #################### + +# Actuator 监控端点的配置项 +management: + endpoints: + web: + base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator + exposure: + include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。 + +# Spring Boot Admin 配置项 +spring: + boot: + admin: + # Spring Boot Admin Client 客户端的相关配置 + client: + url: http://127.0.0.1:${server.port}/${spring.boot.admin.context-path} # 设置 Spring Boot Admin Server 地址 + instance: + service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME] + # Spring Boot Admin Server 服务端的相关配置 + context-path: /admin # 配置 Spring + +# 日志文件配置 +logging: + file: + name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径 + level: + # 配置自己写的 MyBatis Mapper 打印日志 + com.yunxi.scm.module.bpm.dal.mysql: debug + com.yunxi.scm.module.infra.dal.mysql: debug + com.yunxi.scm.module.infra.dal.mysql.job.JobLogMapper: INFO # 配置 JobLogMapper 的日志级别为 info + com.yunxi.scm.module.pay.dal.mysql: debug + com.yunxi.scm.module.pay.dal.mysql.notify.PayNotifyTaskMapper: INFO # 配置 JobLogMapper 的日志级别为 info + com.yunxi.scm.module.system.dal.mysql: debug + com.yunxi.scm.module.tool.dal.mysql: debug + com.yunxi.scm.module.member.dal.mysql: debug + com.yunxi.scm.module.trade.dal.mysql: debug + com.yunxi.scm.module.promotion.dal.mysql: debug + +debug: false + +--- #################### 微信公众号、小程序相关配置 #################### +wx: + mp: # 公众号配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md 文档 + app-id: wx041349c6f39b268b + secret: 5abee519483bc9f8cb37ce280e814bd0 + # 存储配置,解决 AccessToken 的跨节点的共享 + config-storage: + type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 + key-prefix: wx # Redis Key 的前缀 + http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 + miniapp: # 小程序配置(必填),参见 https://github.com/Wechat-Group/WxJava/blob/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md 文档 + appid: wx62056c0d5e8db250 + secret: 333ae72f41552af1e998fe1f54e1584a + config-storage: + type: RedisTemplate # 采用 RedisTemplate 操作 Redis,会自动从 Spring 中获取 + key-prefix: wa # Redis Key 的前缀 + http-client-type: HttpClient # 采用 HttpClient 请求微信公众号平台 + +--- #################### 芋道相关配置 #################### + +# 芋道配置项,设置当前项目所有自定义的配置 +yunxi: + captcha: + enable: false # 本地环境,暂时关闭图片验证码,方便登录等接口的测试; + security: + mock-enable: true + xss: + enable: false + exclude-urls: # 如下两个 url,仅仅是为了演示,去掉配置也没关系 + - ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求 + - ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求 + pay: + order-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/order # 支付渠道的【支付】回调地址 + refund-notify-url: http://yunai.natapp1.cc/admin-api/pay/notify/refund # 支付渠道的【退款】回调地址 + access-log: # 访问日志的配置项 + enable: false + error-code: # 错误码相关配置项 + enable: false + demo: false # 关闭演示模式 + +justauth: + enabled: true + type: + DINGTALK: # 钉钉 + client-id: dingvrnreaje3yqvzhxg + client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI + ignore-check-redirect-uri: true + WECHAT_ENTERPRISE: # 企业微信 + client-id: wwd411c69a39ad2e54 + client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw + agent-id: 1000004 + ignore-check-redirect-uri: true + WECHAT_MINI_APP: # 微信小程序 + client-id: ${wx.miniapp.appid} + client-secret: ${wx.miniapp.secret} + ignore-check-redirect-uri: true + ignore-check-state: true # 微信小程序,不会使用到 state,所以不进行校验 + + cache: + type: REDIS + prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE:: + timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟 + diff --git a/yunxi-server/src/main/resources/application.yaml b/yunxi-server/src/main/resources/application.yaml new file mode 100644 index 0000000..2a64942 --- /dev/null +++ b/yunxi-server/src/main/resources/application.yaml @@ -0,0 +1,213 @@ +spring: + application: + name: yunxi-server + + profiles: + active: dev + + main: + allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。 + + # Servlet 配置 + servlet: + # 文件上传相关配置项 + multipart: + max-file-size: 16MB # 单个文件大小 + max-request-size: 32MB # 设置总上传的文件大小 + mvc: + pathmatch: + matching-strategy: ANT_PATH_MATCHER # 解决 SpringFox 与 SpringBoot 2.6.x 不兼容的问题,参见 SpringFoxHandlerProviderBeanPostProcessor 类 +# throw-exception-if-no-handler-found: true # 404 错误时抛出异常,方便统一处理 +# static-path-pattern: /static/** # 静态资源路径; 注意:如果不配置,则 throw-exception-if-no-handler-found 不生效!!! TODO 芋艿:不能配置,会导致 swagger 不生效 + + # Jackson 配置项 + jackson: + serialization: + write-dates-as-timestamps: true # 设置 Date 的格式,使用时间戳 + write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401 + write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳 + fail-on-empty-beans: false # 允许序列化无属性的 Bean + + # Cache 配置项 + cache: + type: REDIS + redis: + time-to-live: 1h # 设置过期时间为 1 小时 + +--- #################### 接口文档配置 #################### + +springdoc: + api-docs: + enabled: true + path: /v3/api-docs + swagger-ui: + enabled: true + path: /swagger-ui + +knife4j: + enable: true + setting: + language: zh_cn + +# 工作流 Flowable 配置 +flowable: + # 1. false: 默认值,Flowable 启动时,对比数据库表中保存的版本,如果不匹配。将抛出异常 + # 2. true: 启动时会对数据库中所有表进行更新操作,如果表存在,不做处理,反之,自动创建表 + # 3. create_drop: 启动时自动创建表,关闭时自动删除表 + # 4. drop_create: 启动时,删除旧表,再创建新表 + database-schema-update: true # 设置为 false,可通过 https://github.com/flowable/flowable-sql 初始化 + db-history-used: true # flowable6 默认 true 生成信息表,无需手动设置 + check-process-definitions: false # 设置为 false,禁用 /resources/processes 自动部署 BPMN XML 流程 + history-level: full # full:保存历史数据的最高级别,可保存全部流程相关细节,包括流程流转各节点参数 + +# MyBatis Plus 的配置项 +mybatis-plus: + configuration: + map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。 + global-config: + db-config: + id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。 +# id-type: AUTO # 自增 ID,适合 MySQL 等直接自增的数据库 +# id-type: INPUT # 用户输入 ID,适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库 +# id-type: ASSIGN_ID # 分配 ID,默认使用雪花算法。注意,Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解 + logic-delete-value: 1 # 逻辑已删除值(默认为 1) + logic-not-delete-value: 0 # 逻辑未删除值(默认为 0) + type-aliases-package: ${yunxi.info.base-package}.module.*.dal.dataobject + encryptor: + password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成 + +--- #################### 验证码相关配置 #################### + +aj: + captcha: + jigsaw: classpath:images/jigsaw # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径 + pic-click: classpath:images/pic-click # 滑动验证,底图路径,不配置将使用默认图片;以 classpath: 开头,取 resource 目录下路径 + cache-type: redis # 缓存 local/redis... + cache-number: 1000 # local 缓存的阈值,达到这个值,清除缓存 + timing-clear: 180 # local定时清除过期缓存(单位秒),设置为0代表不执行 + type: blockPuzzle # 验证码类型 default两种都实例化。 blockPuzzle 滑块拼图 clickWord 文字点选 + water-mark: 芋道源码 # 右下角水印文字(我的水印),可使用 https://tool.chinaz.com/tools/unicode.aspx 中文转 Unicode,Linux 可能需要转 unicode + interference-options: 0 # 滑动干扰项(0/1/2) + req-frequency-limit-enable: false # 接口请求次数一分钟限制是否开启 true|false + req-get-lock-limit: 5 # 验证失败 5 次,get接口锁定 + req-get-lock-seconds: 10 # 验证失败后,锁定时间间隔 + req-get-minute-limit: 30 # get 接口一分钟内请求数限制 + req-check-minute-limit: 60 # check 接口一分钟内请求数限制 + req-verify-minute-limit: 60 # verify 接口一分钟内请求数限制 + +--- #################### 芋道相关配置 #################### + +yunxi: + info: + version: 1.0.0 + base-package: com.yunxi.scm + web: + admin-ui: + url: http://dashboard.yunxi.iocoder.cn # Admin 管理后台 UI 的地址 + security: + permit-all_urls: + - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 + websocket: + enable: true # websocket的开关 + path: /websocket/message # 路径 + maxOnlineCount: 0 # 最大连接人数 + sessionMap: true # 保存sessionMap + swagger: + title: 芋道快速开发平台 + description: 提供管理后台、用户 App 的所有功能 + version: ${yunxi.info.version} + url: ${yunxi.web.admin-ui.url} + email: xingyu4j@vip.qq.com + license: MIT + license-url: https://gitee.com/zhijiantianya/ruoyi-vue-pro/blob/master/LICENSE + captcha: + enable: true # 验证码的开关,默认为 true + codegen: + base-package: ${yunxi.info.base-package} + db-schemas: ${spring.datasource.dynamic.datasource.master.name} + front-type: 10 # 前端模版的类型,参见 CodegenFrontTypeEnum 枚举类 + error-code: # 错误码相关配置项 + constants-class-list: + - com.yunxi.scm.module.bpm.enums.ErrorCodeConstants + - com.yunxi.scm.module.infra.enums.ErrorCodeConstants + - com.yunxi.scm.module.member.enums.ErrorCodeConstants + - com.yunxi.scm.module.pay.enums.ErrorCodeConstants + - com.yunxi.scm.module.system.enums.ErrorCodeConstants + - com.yunxi.scm.module.mp.enums.ErrorCodeConstants + mq: + redis: + pubsub: + enable: false # 是否开启 Redis pubsub 广播消费,默认为 true。这里设置成 false,可以按需开启 + stream: + enable: false # 是否开启 Redis stream 集群消费,默认为 true。这里设置成 false,可以按需开启 + tenant: # 多租户相关配置项 + enable: true + ignore-urls: + - /admin-api/system/tenant/get-id-by-name # 基于名字获取租户,不许带租户编号 + - /admin-api/system/captcha/get # 获取图片验证码,和租户无关 + - /admin-api/system/captcha/check # 校验图片验证码,和租户无关 + - /admin-api/infra/file/*/get/** # 获取图片,和租户无关 + - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 + - /admin-api/pay/notify/** # 支付回调通知,不携带租户编号 + - /jmreport/* # 积木报表,无法携带租户编号 + - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,无法携带租户编号 + ignore-tables: + - system_tenant + - system_tenant_package + - system_dict_data + - system_dict_type + - system_error_code + - system_menu + - system_sms_channel + - system_sms_template + - system_sms_log + - system_sensitive_word + - system_oauth2_client + - system_mail_account + - system_mail_template + - system_mail_log + - system_notify_template + - infra_codegen_column + - infra_codegen_table + - infra_test_demo + - infra_config + - infra_file_config + - infra_file + - infra_file_content + - infra_job + - infra_job_log + - infra_job_log + - infra_data_source_config + - jimu_dict + - jimu_dict_item + - jimu_report + - jimu_report_data_source + - jimu_report_db + - jimu_report_db_field + - jimu_report_db_param + - jimu_report_link + - jimu_report_map + - jimu_report_share + - rep_demo_dxtj + - rep_demo_employee + - rep_demo_gongsi + - rep_demo_jianpiao + - tmp_report_data_1 + - tmp_report_data_income + sms-code: # 短信验证码相关的配置项 + expire-times: 10m + send-frequency: 1m + send-maximum-quantity-per-day: 10 + begin-code: 9999 # 这里配置 9999 的原因是,测试方便。 + end-code: 9999 # 这里配置 9999 的原因是,测试方便。 + trade: + order: + app-id: 1 # 商户编号 + expire-time: 2h # 支付的过期时间 + +debug: false + +#积木报表配置 +minidao : + base-package: org.jeecg.modules.jmreport.desreport.dao* + db-type: mysql diff --git a/yunxi-server/src/main/resources/logback-spring.xml b/yunxi-server/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..111baa2 --- /dev/null +++ b/yunxi-server/src/main/resources/logback-spring.xml @@ -0,0 +1,76 @@ + + + + + + + + + +       + + + ${PATTERN_DEFAULT} + + + + + + + + + + ${PATTERN_DEFAULT} + + + + ${LOG_FILE} + + + ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} + + ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} + + ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} + + ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} + + ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30} + + + + + + 0 + + 256 + + + + + + + + ${PATTERN_DEFAULT} + + + + + + + + + + + + + + + + + + + + + + diff --git a/yunxi-server/src/main/resources/static/MP_verify_DKOvVzFP7vPwwHx2.txt b/yunxi-server/src/main/resources/static/MP_verify_DKOvVzFP7vPwwHx2.txt new file mode 100644 index 0000000..ee06c0a --- /dev/null +++ b/yunxi-server/src/main/resources/static/MP_verify_DKOvVzFP7vPwwHx2.txt @@ -0,0 +1 @@ +DKOvVzFP7vPwwHx2 \ No newline at end of file diff --git a/yunxi-server/src/main/resources/static/READMD.md b/yunxi-server/src/main/resources/static/READMD.md new file mode 100644 index 0000000..2cf4668 --- /dev/null +++ b/yunxi-server/src/main/resources/static/READMD.md @@ -0,0 +1,13 @@ +## 微信公众号 + +参考文章:https://www.yuque.com/docs/share/0e2966dd-89f8-4b69-980d-b876168725df + +① 访问 social-login.html 选择【微信公众号】 + +② 微信公众号授权完成后,跳转回 social-login2.html,输入手机号 + 密码,进行绑定 + +## 微信小程序 + +参考文章:https://www.yuque.com/docs/share/88e3d30a-6830-45fc-8c25-dae485aef3aa + +① 暂时使用 mini-program-test 项目 diff --git a/yunxi-server/src/main/resources/static/pay_wx_pub.html b/yunxi-server/src/main/resources/static/pay_wx_pub.html new file mode 100644 index 0000000..b41bb4b --- /dev/null +++ b/yunxi-server/src/main/resources/static/pay_wx_pub.html @@ -0,0 +1,120 @@ + + + + + + 支付测试页 + + + + +

点击如下按钮,发起支付的测试
+
+ +
+ + + diff --git a/yunxi-server/src/test/java/com/yunxi/scm/ProjectReactor.java b/yunxi-server/src/test/java/com/yunxi/scm/ProjectReactor.java new file mode 100644 index 0000000..9f7d035 --- /dev/null +++ b/yunxi-server/src/test/java/com/yunxi/scm/ProjectReactor.java @@ -0,0 +1,146 @@ +package com.yunxi.scm; + +import cn.hutool.core.io.FileTypeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import com.yunxi.scm.framework.common.util.collection.SetUtils; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.stream.Collectors; + +import static java.io.File.separator; + +/** + * 项目修改器,一键替换 Maven 的 groupId、artifactId,项目的 package 等 + *

+ * 通过修改 groupIdNew、artifactIdNew、projectBaseDirNew 三个变量 + * + * @author 芋道源码 + */ +@Slf4j +public class ProjectReactor { + + private static final String GROUP_ID = "com.yunxi.scm"; + private static final String ARTIFACT_ID = "yunxi"; + private static final String PACKAGE_NAME = "com.yunxi.scm"; + private static final String TITLE = "云息供应链管理平台"; + + /** + * 白名单文件,不进行重写,避免出问题 + */ + private static final Set WHITE_FILE_TYPES = SetUtils.asSet("gif", "jpg", "svg", "png", // 图片 + "eot", "woff2", "ttf", "woff"); // 字体 + + public static void main(String[] args) { + long start = System.currentTimeMillis(); + String projectBaseDir = getProjectBaseDir(); + log.info("[main][原项目路劲改地址 ({})]", projectBaseDir); + + // ========== 配置,需要你手动修改 ========== + String groupIdNew = "com.yunxi.scm"; + String artifactIdNew = "yunxi"; + String packageNameNew = "com.yunxi.scm"; + String titleNew = "云息供应链管理平台"; + String projectBaseDirNew = projectBaseDir + "-new"; // 一键改名后,“新”项目所在的目录 + log.info("[main][检测新项目目录 ({})是否存在]", projectBaseDirNew); + if (FileUtil.exist(projectBaseDirNew)) { + log.error("[main][新项目目录检测 ({})已存在,请更改新的目录!程序退出]", projectBaseDirNew); + return; + } + // 如果新目录中存在 PACKAGE_NAME,ARTIFACT_ID 等关键字,路径会被替换,导致生成的文件不在预期目录 + if (StrUtil.containsAny(projectBaseDirNew, PACKAGE_NAME, ARTIFACT_ID, StrUtil.upperFirst(ARTIFACT_ID))) { + log.error("[main][新项目目录 `projectBaseDirNew` 检测 ({}) 存在冲突名称「{}」或者「{}」,请更改新的目录!程序退出]", + projectBaseDirNew, PACKAGE_NAME, ARTIFACT_ID); + return; + } + log.info("[main][完成新项目目录检测,新项目路径地址 ({})]", projectBaseDirNew); + // 获得需要复制的文件 + log.info("[main][开始获得需要重写的文件,预计需要 10-20 秒]"); + Collection files = listFiles(projectBaseDir); + log.info("[main][需要重写的文件数量:{},预计需要 15-30 秒]", files.size()); + // 写入文件 + files.forEach(file -> { + // 如果是白名单的文件类型,不进行重写,直接拷贝 + String fileType = getFileType(file); + if (WHITE_FILE_TYPES.contains(fileType)) { + copyFile(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); + return; + } + // 如果非白名单的文件类型,重写内容,在生成文件 + String content = replaceFileContent(file, groupIdNew, artifactIdNew, packageNameNew, titleNew); + writeFile(file, content, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); + }); + log.info("[main][重写完成]共耗时:{} 秒", (System.currentTimeMillis() - start) / 1000); + } + + private static String getProjectBaseDir() { + String baseDir = System.getProperty("user.dir"); + if (StrUtil.isEmpty(baseDir)) { + throw new NullPointerException("项目基础路径不存在"); + } + return baseDir; + } + + private static Collection listFiles(String projectBaseDir) { + Collection files = FileUtil.loopFiles(projectBaseDir); + // 移除 IDEA、Git 自身的文件、Node 编译出来的文件 + files = files.stream() + .filter(file -> !file.getPath().contains(separator + "target" + separator) + && !file.getPath().contains(separator + "node_modules" + separator) + && !file.getPath().contains(separator + ".idea" + separator) + && !file.getPath().contains(separator + ".git" + separator) + && !file.getPath().contains(separator + "dist" + separator) + && !file.getPath().contains(".iml") + && !file.getPath().contains(".html.gz")) + .collect(Collectors.toList()); + return files; + } + + private static String replaceFileContent(File file, String groupIdNew, + String artifactIdNew, String packageNameNew, + String titleNew) { + String content = FileUtil.readString(file, StandardCharsets.UTF_8); + // 如果是白名单的文件类型,不进行重写 + String fileType = getFileType(file); + if (WHITE_FILE_TYPES.contains(fileType)) { + return content; + } + // 执行文件内容都重写 + return content.replaceAll(GROUP_ID, groupIdNew) + .replaceAll(PACKAGE_NAME, packageNameNew) + .replaceAll(ARTIFACT_ID, artifactIdNew) // 必须放在最后替换,因为 ARTIFACT_ID 太短! + .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew)) + .replaceAll(TITLE, titleNew); + } + + private static void writeFile(File file, String fileContent, String projectBaseDir, + String projectBaseDirNew, String packageNameNew, String artifactIdNew) { + String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); + FileUtil.writeUtf8String(fileContent, newPath); + } + + private static void copyFile(File file, String projectBaseDir, + String projectBaseDirNew, String packageNameNew, String artifactIdNew) { + String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew); + FileUtil.copyFile(file, new File(newPath)); + } + + private static String buildNewFilePath(File file, String projectBaseDir, + String projectBaseDirNew, String packageNameNew, String artifactIdNew) { + return file.getPath().replace(projectBaseDir, projectBaseDirNew) // 新目录 + .replace(PACKAGE_NAME.replaceAll("\\.", Matcher.quoteReplacement(separator)), + packageNameNew.replaceAll("\\.", Matcher.quoteReplacement(separator))) + .replace(ARTIFACT_ID, artifactIdNew) // + .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew)); + } + + private static String getFileType(File file) { + return file.length() > 0 ? FileTypeUtil.getType(file) : ""; + } + +} diff --git a/yunxi-ui-admin-uniapp/.gitignore b/yunxi-ui-admin-uniapp/.gitignore new file mode 100644 index 0000000..59c9154 --- /dev/null +++ b/yunxi-ui-admin-uniapp/.gitignore @@ -0,0 +1,16 @@ +###################################################################### +# Build Tools + +/unpackage/* +/node_modules/* + +###################################################################### +# Development Tools + +/.idea/* +/.vscode/* +/.hbuilderx/* + +package-lock.json +yarn.lock + diff --git a/yunxi-ui-admin-uniapp/App.vue b/yunxi-ui-admin-uniapp/App.vue new file mode 100644 index 0000000..93318b5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/App.vue @@ -0,0 +1,34 @@ + + + diff --git a/yunxi-ui-admin-uniapp/LICENSE b/yunxi-ui-admin-uniapp/LICENSE new file mode 100644 index 0000000..84f43b5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 芋道 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/yunxi-ui-admin-uniapp/api/login.js b/yunxi-ui-admin-uniapp/api/login.js new file mode 100644 index 0000000..b79fd07 --- /dev/null +++ b/yunxi-ui-admin-uniapp/api/login.js @@ -0,0 +1,34 @@ +import request from '@/utils/request' + +// 登录方法 +export function login(username, password, captchaVerification) { + const data = { + username, + password, + captchaVerification + } + return request({ + url: '/system/auth/login', + headers: { + isToken: false + }, + 'method': 'POST', + 'data': data + }) +} + +// 获取用户详细信息 +export function getInfo() { + return request({ + url: '/system/auth/get-permission-info', + 'method': 'GET' + }) +} + +// 退出方法 +export function logout() { + return request({ + url: '/system/auth/logout', + 'method': 'POST' + }) +} diff --git a/yunxi-ui-admin-uniapp/api/system/user.js b/yunxi-ui-admin-uniapp/api/system/user.js new file mode 100644 index 0000000..59e9304 --- /dev/null +++ b/yunxi-ui-admin-uniapp/api/system/user.js @@ -0,0 +1,42 @@ +import upload from '@/utils/upload' +import request from '@/utils/request' + +// 用户密码重置 +export function updateUserPwd(oldPassword, newPassword) { + const data = { + oldPassword, + newPassword + } + return request({ + url: '/system/user/profile/update-password', + method: 'PUT', + params: data + }) +} + +// 查询用户个人信息 +export function getUserProfile() { + return request({ + url: '/system/user/profile/get', + method: 'GET' + }) +} + +// 修改用户个人信息 +export function updateUserProfile(data) { + return request({ + url: '/system/user/profile/update', + method: 'PUT', + data: data + }) +} + +// 用户头像上传 +export function uploadAvatar(data) { + return upload({ + url: '/system/user/profile/update-avatar', + method: 'PUT', + name: data.name, + filePath: data.filePath + }) +} diff --git a/yunxi-ui-admin-uniapp/components/uni-section/uni-section.vue b/yunxi-ui-admin-uniapp/components/uni-section/uni-section.vue new file mode 100644 index 0000000..9a52e0b --- /dev/null +++ b/yunxi-ui-admin-uniapp/components/uni-section/uni-section.vue @@ -0,0 +1,167 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/components/verifition/Verify.vue b/yunxi-ui-admin-uniapp/components/verifition/Verify.vue new file mode 100644 index 0000000..3fe4d88 --- /dev/null +++ b/yunxi-ui-admin-uniapp/components/verifition/Verify.vue @@ -0,0 +1,469 @@ + + + diff --git a/yunxi-ui-admin-uniapp/components/verifition/utils/ase.js b/yunxi-ui-admin-uniapp/components/verifition/utils/ase.js new file mode 100644 index 0000000..1fdceed --- /dev/null +++ b/yunxi-ui-admin-uniapp/components/verifition/utils/ase.js @@ -0,0 +1,14 @@ +import CryptoJS from 'crypto-js' +/** + * @word 要加密的内容 + * @keyWord String 服务器随机返回的关键字 + * */ +export function aesEncrypt(word, keyWord = "XwKsGlMcdPMEhR1B") { + var key = CryptoJS.enc.Utf8.parse(keyWord); + var srcs = CryptoJS.enc.Utf8.parse(word); + var encrypted = CryptoJS.AES.encrypt(srcs, key, { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7 + }); + return encrypted.toString(); +} diff --git a/yunxi-ui-admin-uniapp/components/verifition/utils/request.js b/yunxi-ui-admin-uniapp/components/verifition/utils/request.js new file mode 100644 index 0000000..e6a31b0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/components/verifition/utils/request.js @@ -0,0 +1,17 @@ +import config from '@/config' +const baseUrl = config.baseUrl +export const myRequest = (option = {}) => { + return new Promise((reslove, reject) => { + uni.request({ + url: baseUrl + option.url, + data: option.data, + method: option.method || "GET", + success: (result) => { + reslove(result) + }, + fail: (error) => { + reject(error) + } + }) + }) +} diff --git a/yunxi-ui-admin-uniapp/components/verifition/verifyPoint/verifyPoint.vue b/yunxi-ui-admin-uniapp/components/verifition/verifyPoint/verifyPoint.vue new file mode 100644 index 0000000..57d7d03 --- /dev/null +++ b/yunxi-ui-admin-uniapp/components/verifition/verifyPoint/verifyPoint.vue @@ -0,0 +1,557 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/components/verifition/verifySlider/verifySlider.vue b/yunxi-ui-admin-uniapp/components/verifition/verifySlider/verifySlider.vue new file mode 100644 index 0000000..7c75e0e --- /dev/null +++ b/yunxi-ui-admin-uniapp/components/verifition/verifySlider/verifySlider.vue @@ -0,0 +1,661 @@ + + + diff --git a/yunxi-ui-admin-uniapp/config.js b/yunxi-ui-admin-uniapp/config.js new file mode 100644 index 0000000..db477a3 --- /dev/null +++ b/yunxi-ui-admin-uniapp/config.js @@ -0,0 +1,27 @@ +// 应用全局配置 +module.exports = { + // baseUrl: 'http://api-dashboard.yunxi.iocoder.cn', + baseUrl: 'http://localhost:48080', + baseApi: '/admin-api', + // 应用信息 + appInfo: { + // 应用名称 + name: "yunxi-app", + // 应用版本 + version: "1.0.0", + // 应用logo + logo: "/static/logo.png", + // 官方网站 + site_url: "https://iocoder.cn", + // 政策协议 + agreements: [{ + title: "隐私政策", + url: "https://iocoder.cn" + }, + { + title: "用户服务协议", + url: "https://iocoder.cn" + } + ] + } +} diff --git a/yunxi-ui-admin-uniapp/main.js b/yunxi-ui-admin-uniapp/main.js new file mode 100644 index 0000000..3985b1b --- /dev/null +++ b/yunxi-ui-admin-uniapp/main.js @@ -0,0 +1,17 @@ +import Vue from 'vue' +import App from './App' +import store from './store' // store +import plugins from './plugins' // plugins +import './permission' // permission +Vue.use(plugins) + +Vue.config.productionTip = false +Vue.prototype.$store = store + +App.mpType = 'app' + +const app = new Vue({ + ...App +}) + +app.$mount() diff --git a/yunxi-ui-admin-uniapp/manifest.json b/yunxi-ui-admin-uniapp/manifest.json new file mode 100644 index 0000000..285d058 --- /dev/null +++ b/yunxi-ui-admin-uniapp/manifest.json @@ -0,0 +1,69 @@ +{ + "name" : "芋道移动端", + "appid" : "__UNI__25A9D80", + "description" : "", + "versionName" : "1.0.0", + "versionCode" : "100", + "transformPx" : false, + "app-plus" : { + "usingComponents" : true, + "nvueCompiler" : "uni-app", + "splashscreen" : { + "alwaysShowBeforeRender" : true, + "waiting" : true, + "autoclose" : true, + "delay" : 0 + }, + "modules" : {}, + "distribute" : { + "android" : { + "permissions" : [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ] + }, + "ios" : {}, + "sdkConfigs" : {} + } + }, + "quickapp" : {}, + "mp-weixin" : { + "appid" : "wxccd7e2a0911b3397", + "setting" : { + "urlCheck" : false, + "es6" : false, + "minified" : true, + "postcss" : true + }, + "optimization" : { + "subPackages" : true + }, + "usingComponents" : true + }, + "vueVersion" : "2", + "h5" : { + "template" : "static/index.html", + "devServer" : { + "port" : 9090, + "https" : false + }, + "title" : "Yunxi-App", + "router" : { + "mode" : "hash", + "base" : "./" + } + } +} diff --git a/yunxi-ui-admin-uniapp/package.json b/yunxi-ui-admin-uniapp/package.json new file mode 100644 index 0000000..e5def35 --- /dev/null +++ b/yunxi-ui-admin-uniapp/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "crypto-js": "^4.0.0" + } +} diff --git a/yunxi-ui-admin-uniapp/pages.json b/yunxi-ui-admin-uniapp/pages.json new file mode 100644 index 0000000..73c631a --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages.json @@ -0,0 +1,97 @@ +{ + "pages": [{ + "path": "pages/login", + "style": { + "navigationBarTitleText": "登录" + } + }, { + "path": "pages/index", + "style": { + "navigationBarTitleText": "芋道移动端框架", + "navigationStyle": "custom" + } + }, { + "path": "pages/work/index", + "style": { + "navigationBarTitleText": "工作台" + } + }, { + "path": "pages/mine/index", + "style": { + "navigationBarTitleText": "我的" + } + }, { + "path": "pages/mine/avatar/index", + "style": { + "navigationBarTitleText": "修改头像" + } + }, { + "path": "pages/mine/info/index", + "style": { + "navigationBarTitleText": "个人信息" + } + }, { + "path": "pages/mine/info/edit", + "style": { + "navigationBarTitleText": "编辑资料" + } + }, { + "path": "pages/mine/pwd/index", + "style": { + "navigationBarTitleText": "修改密码" + } + }, { + "path": "pages/mine/setting/index", + "style": { + "navigationBarTitleText": "应用设置" + } + }, { + "path": "pages/mine/help/index", + "style": { + "navigationBarTitleText": "常见问题" + } + }, { + "path": "pages/mine/about/index", + "style": { + "navigationBarTitleText": "关于我们" + } + }, { + "path": "pages/common/webview/index", + "style": { + "navigationBarTitleText": "浏览网页" + } + }, { + "path": "pages/common/textview/index", + "style": { + "navigationBarTitleText": "浏览文本" + } + }], + "tabBar": { + "color": "#000000", + "selectedColor": "#000000", + "borderStyle": "white", + "backgroundColor": "#ffffff", + "list": [{ + "pagePath": "pages/index", + "iconPath": "static/images/tabbar/home.png", + "selectedIconPath": "static/images/tabbar/home_.png", + "text": "首页" + }, { + "pagePath": "pages/work/index", + "iconPath": "static/images/tabbar/work.png", + "selectedIconPath": "static/images/tabbar/work_.png", + "text": "工作台" + }, { + "pagePath": "pages/mine/index", + "iconPath": "static/images/tabbar/mine.png", + "selectedIconPath": "static/images/tabbar/mine_.png", + "text": "我的" + } + ] + }, + "globalStyle": { + "navigationBarTextStyle": "black", + "navigationBarTitleText": "RuoYi", + "navigationBarBackgroundColor": "#FFFFFF" + } +} diff --git a/yunxi-ui-admin-uniapp/pages/common/textview/index.vue b/yunxi-ui-admin-uniapp/pages/common/textview/index.vue new file mode 100644 index 0000000..e9b05fb --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/common/textview/index.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/common/webview/index.vue b/yunxi-ui-admin-uniapp/pages/common/webview/index.vue new file mode 100644 index 0000000..8388c76 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/common/webview/index.vue @@ -0,0 +1,34 @@ + + + diff --git a/yunxi-ui-admin-uniapp/pages/index.vue b/yunxi-ui-admin-uniapp/pages/index.vue new file mode 100644 index 0000000..17613f0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/index.vue @@ -0,0 +1,43 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/login.vue b/yunxi-ui-admin-uniapp/pages/login.vue new file mode 100644 index 0000000..bb7908e --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/login.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/mine/about/index.vue b/yunxi-ui-admin-uniapp/pages/mine/about/index.vue new file mode 100644 index 0000000..c5dd58a --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/mine/about/index.vue @@ -0,0 +1,75 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/mine/avatar/index.vue b/yunxi-ui-admin-uniapp/pages/mine/avatar/index.vue new file mode 100644 index 0000000..7731486 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/mine/avatar/index.vue @@ -0,0 +1,631 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/mine/help/index.vue b/yunxi-ui-admin-uniapp/pages/mine/help/index.vue new file mode 100644 index 0000000..4cffe55 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/mine/help/index.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/mine/index.vue b/yunxi-ui-admin-uniapp/pages/mine/index.vue new file mode 100644 index 0000000..42e2120 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/mine/index.vue @@ -0,0 +1,198 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/mine/info/edit.vue b/yunxi-ui-admin-uniapp/pages/mine/info/edit.vue new file mode 100644 index 0000000..44d8ce4 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/mine/info/edit.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/mine/info/index.vue b/yunxi-ui-admin-uniapp/pages/mine/info/index.vue new file mode 100644 index 0000000..2e519e8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/mine/info/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/mine/pwd/index.vue b/yunxi-ui-admin-uniapp/pages/mine/pwd/index.vue new file mode 100644 index 0000000..da9567f --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/mine/pwd/index.vue @@ -0,0 +1,85 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/mine/setting/index.vue b/yunxi-ui-admin-uniapp/pages/mine/setting/index.vue new file mode 100644 index 0000000..0f9f058 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/mine/setting/index.vue @@ -0,0 +1,78 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/pages/work/index.vue b/yunxi-ui-admin-uniapp/pages/work/index.vue new file mode 100644 index 0000000..1afefc9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/pages/work/index.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/permission.js b/yunxi-ui-admin-uniapp/permission.js new file mode 100644 index 0000000..a47d947 --- /dev/null +++ b/yunxi-ui-admin-uniapp/permission.js @@ -0,0 +1,39 @@ +import { getAccessToken } from '@/utils/auth' + +// 登录页面 +const loginPage = "/pages/login" + +// 页面白名单 +const whiteList = [ + '/pages/login', '/pages/common/webview/index' +] + +// 检查地址白名单 +function checkWhite(url) { + const path = url.split('?')[0] + return whiteList.indexOf(path) !== -1 +} + +// 页面跳转验证拦截器 +let list = ["navigateTo", "redirectTo", "reLaunch", "switchTab"] +list.forEach(item => { + uni.addInterceptor(item, { + invoke(to) { + if (getAccessToken()) { + if (to.path === loginPage) { + uni.reLaunch({ url: "/" }) + } + return true + } else { + if (checkWhite(to.url)) { + return true + } + uni.reLaunch({ url: loginPage }) + return false + } + }, + fail(err) { + console.log(err) + } + }) +}) diff --git a/yunxi-ui-admin-uniapp/plugins/auth.js b/yunxi-ui-admin-uniapp/plugins/auth.js new file mode 100644 index 0000000..3b91c14 --- /dev/null +++ b/yunxi-ui-admin-uniapp/plugins/auth.js @@ -0,0 +1,60 @@ +import store from '@/store' + +function authPermission(permission) { + const all_permission = "*:*:*" + const permissions = store.getters && store.getters.permissions + if (permission && permission.length > 0) { + return permissions.some(v => { + return all_permission === v || v === permission + }) + } else { + return false + } +} + +function authRole(role) { + const super_admin = "admin" + const roles = store.getters && store.getters.roles + if (role && role.length > 0) { + return roles.some(v => { + return super_admin === v || v === role + }) + } else { + return false + } +} + +export default { + // 验证用户是否具备某权限 + hasPermi(permission) { + return authPermission(permission) + }, + // 验证用户是否含有指定权限,只需包含其中一个 + hasPermiOr(permissions) { + return permissions.some(item => { + return authPermission(item) + }) + }, + // 验证用户是否含有指定权限,必须全部拥有 + hasPermiAnd(permissions) { + return permissions.every(item => { + return authPermission(item) + }) + }, + // 验证用户是否具备某角色 + hasRole(role) { + return authRole(role) + }, + // 验证用户是否含有指定角色,只需包含其中一个 + hasRoleOr(roles) { + return roles.some(item => { + return authRole(item) + }) + }, + // 验证用户是否含有指定角色,必须全部拥有 + hasRoleAnd(roles) { + return roles.every(item => { + return authRole(item) + }) + } +} diff --git a/yunxi-ui-admin-uniapp/plugins/index.js b/yunxi-ui-admin-uniapp/plugins/index.js new file mode 100644 index 0000000..efbae15 --- /dev/null +++ b/yunxi-ui-admin-uniapp/plugins/index.js @@ -0,0 +1,14 @@ +import tab from './tab' +import auth from './auth' +import modal from './modal' + +export default { + install(Vue) { + // 页签操作 + Vue.prototype.$tab = tab + // 认证对象 + Vue.prototype.$auth = auth + // 模态框对象 + Vue.prototype.$modal = modal + } +} diff --git a/yunxi-ui-admin-uniapp/plugins/modal.js b/yunxi-ui-admin-uniapp/plugins/modal.js new file mode 100644 index 0000000..87960fd --- /dev/null +++ b/yunxi-ui-admin-uniapp/plugins/modal.js @@ -0,0 +1,74 @@ +export default { + // 消息提示 + msg(content) { + uni.showToast({ + title: content, + icon: 'none' + }) + }, + // 错误消息 + msgError(content) { + uni.showToast({ + title: content, + icon: 'error' + }) + }, + // 成功消息 + msgSuccess(content) { + uni.showToast({ + title: content, + icon: 'success' + }) + }, + // 隐藏消息 + hideMsg(content) { + uni.hideToast() + }, + // 弹出提示 + alert(content) { + uni.showModal({ + title: '提示', + content: content, + showCancel: false + }) + }, + // 确认窗体 + confirm(content) { + return new Promise((resolve, reject) => { + uni.showModal({ + title: '系统提示', + content: content, + cancelText: '取消', + confirmText: '确定', + success: function(res) { + if (res.confirm) { + resolve(res.confirm) + } + } + }) + }) + }, + // 提示信息 + showToast(option) { + if (typeof option === "object") { + uni.showToast(option) + } else { + uni.showToast({ + title: option, + icon: "none", + duration: 2500 + }) + } + }, + // 打开遮罩层 + loading(content) { + uni.showLoading({ + title: content, + icon: 'none' + }) + }, + // 关闭遮罩层 + closeLoading() { + uni.hideLoading() + } +} diff --git a/yunxi-ui-admin-uniapp/plugins/tab.js b/yunxi-ui-admin-uniapp/plugins/tab.js new file mode 100644 index 0000000..5d1b305 --- /dev/null +++ b/yunxi-ui-admin-uniapp/plugins/tab.js @@ -0,0 +1,30 @@ +export default { + // 关闭所有页面,打开到应用内的某个页面 + reLaunch(url) { + return uni.reLaunch({ + url: url + }) + }, + // 跳转到tabBar页面,并关闭其他所有非tabBar页面 + switchTab(url) { + return uni.switchTab({ + url: url + }) + }, + // 关闭当前页面,跳转到应用内的某个页面 + redirectTo(url) { + return uni.redirectTo({ + url: url + }) + }, + // 保留当前页面,跳转到应用内的某个页面 + navigateTo(url) { + return uni.navigateTo({ + url: url + }) + }, + // 关闭当前页面,返回上一页面或多级页面 + navigateBack() { + return uni.navigateBack() + } +} diff --git a/yunxi-ui-admin-uniapp/static/favicon.ico b/yunxi-ui-admin-uniapp/static/favicon.ico new file mode 100644 index 0000000..3946b9d Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/favicon.ico differ diff --git a/yunxi-ui-admin-uniapp/static/font/iconfont.css b/yunxi-ui-admin-uniapp/static/font/iconfont.css new file mode 100644 index 0000000..39aed3d --- /dev/null +++ b/yunxi-ui-admin-uniapp/static/font/iconfont.css @@ -0,0 +1,90 @@ +@font-face { + font-family: "iconfont"; + src: url('/static/font/iconfont.ttf') format('truetype'); +} + +.iconfont { + font-family: "iconfont" !important; + font-size: 16px; + display: inline-block; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-user:before { + content: "\e7ae"; +} + +.icon-password:before { + content: "\e8b2"; +} + +.icon-code:before { + content: "\e699"; +} + +.icon-setting:before { + content: "\e6cc"; +} + +.icon-share:before { + content: "\e739"; +} + +.icon-edit:before { + content: "\e60c"; +} + +.icon-version:before { + content: "\e63f"; +} + +.icon-service:before { + content: "\e6ff"; +} + +.icon-friendfill:before { + content: "\e726"; +} + +.icon-community:before { + content: "\e741"; +} + +.icon-people:before { + content: "\e736"; +} + +.icon-dianzan:before { + content: "\ec7f"; +} + +.icon-right:before { + content: "\e7eb"; +} + +.icon-logout:before { + content: "\e61d"; +} + +.icon-help:before { + content: "\e616"; +} + +.icon-github:before { + content: "\e628"; +} + +.icon-aixin:before { + content: "\e601"; +} + +.icon-clean:before { + content: "\e607"; +} + +.icon-refresh:before { + content: "\e604"; +} + diff --git a/yunxi-ui-admin-uniapp/static/font/iconfont.ttf b/yunxi-ui-admin-uniapp/static/font/iconfont.ttf new file mode 100644 index 0000000..53915ca Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/font/iconfont.ttf differ diff --git a/yunxi-ui-admin-uniapp/static/images/banner/banner01.jpg b/yunxi-ui-admin-uniapp/static/images/banner/banner01.jpg new file mode 100644 index 0000000..fdb1e16 Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/banner/banner01.jpg differ diff --git a/yunxi-ui-admin-uniapp/static/images/banner/banner02.jpg b/yunxi-ui-admin-uniapp/static/images/banner/banner02.jpg new file mode 100644 index 0000000..1086afd Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/banner/banner02.jpg differ diff --git a/yunxi-ui-admin-uniapp/static/images/banner/banner03.jpg b/yunxi-ui-admin-uniapp/static/images/banner/banner03.jpg new file mode 100644 index 0000000..092a5fc Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/banner/banner03.jpg differ diff --git a/yunxi-ui-admin-uniapp/static/images/default.jpg b/yunxi-ui-admin-uniapp/static/images/default.jpg new file mode 100644 index 0000000..aa0237b Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/default.jpg differ diff --git a/yunxi-ui-admin-uniapp/static/images/profile.jpg b/yunxi-ui-admin-uniapp/static/images/profile.jpg new file mode 100644 index 0000000..b3a940b Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/profile.jpg differ diff --git a/yunxi-ui-admin-uniapp/static/images/tabbar/home.png b/yunxi-ui-admin-uniapp/static/images/tabbar/home.png new file mode 100644 index 0000000..50acdfd Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/tabbar/home.png differ diff --git a/yunxi-ui-admin-uniapp/static/images/tabbar/home_.png b/yunxi-ui-admin-uniapp/static/images/tabbar/home_.png new file mode 100644 index 0000000..a408f71 Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/tabbar/home_.png differ diff --git a/yunxi-ui-admin-uniapp/static/images/tabbar/mine.png b/yunxi-ui-admin-uniapp/static/images/tabbar/mine.png new file mode 100644 index 0000000..f13fe44 Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/tabbar/mine.png differ diff --git a/yunxi-ui-admin-uniapp/static/images/tabbar/mine_.png b/yunxi-ui-admin-uniapp/static/images/tabbar/mine_.png new file mode 100644 index 0000000..8a0a742 Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/tabbar/mine_.png differ diff --git a/yunxi-ui-admin-uniapp/static/images/tabbar/work.png b/yunxi-ui-admin-uniapp/static/images/tabbar/work.png new file mode 100644 index 0000000..21e130d Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/tabbar/work.png differ diff --git a/yunxi-ui-admin-uniapp/static/images/tabbar/work_.png b/yunxi-ui-admin-uniapp/static/images/tabbar/work_.png new file mode 100644 index 0000000..80b979c Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/images/tabbar/work_.png differ diff --git a/yunxi-ui-admin-uniapp/static/index.html b/yunxi-ui-admin-uniapp/static/index.html new file mode 100644 index 0000000..a7af653 --- /dev/null +++ b/yunxi-ui-admin-uniapp/static/index.html @@ -0,0 +1,20 @@ + + + + + + + <%= htmlWebpackPlugin.options.title %> + + + + + + +

+ \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/static/logo.png b/yunxi-ui-admin-uniapp/static/logo.png new file mode 100644 index 0000000..b4270c0 Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/logo.png differ diff --git a/yunxi-ui-admin-uniapp/static/logo200.png b/yunxi-ui-admin-uniapp/static/logo200.png new file mode 100644 index 0000000..ffa9988 Binary files /dev/null and b/yunxi-ui-admin-uniapp/static/logo200.png differ diff --git a/yunxi-ui-admin-uniapp/static/scss/colorui.css b/yunxi-ui-admin-uniapp/static/scss/colorui.css new file mode 100644 index 0000000..fade3b2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/static/scss/colorui.css @@ -0,0 +1,3912 @@ +/* + ColorUi for uniApp v2.1.6 | by 文晓港 2019-05-31 10:44:24 + 仅供学习交流,如作它用所承受的法律责任一概与作者无关 + + *使用ColorUi开发扩展与插件时,请注明基于ColorUi开发 + + (QQ交流群:240787041) +*/ + +/* ================== + 初始化 + ==================== */ +body { + background-color: #f1f1f1; + font-size: 28upx; + color: #333333; + font-family: Helvetica Neue, Helvetica, sans-serif; +} + +view, +scroll-view, +swiper, +button, +input, +textarea, +label, +navigator, +image { + box-sizing: border-box; +} + +.round { + border-radius: 5000upx; +} + +.radius { + border-radius: 6upx; +} + +/* ================== + 图片 + ==================== */ + +image { + max-width: 100%; + display: inline-block; + position: relative; + z-index: 0; +} + +image.loading::before { + content: ""; + background-color: #f5f5f5; + display: block; + position: absolute; + width: 100%; + height: 100%; + z-index: -2; +} + +image.loading::after { + content: "\e7f1"; + font-family: "cuIcon"; + position: absolute; + top: 0; + left: 0; + width: 32upx; + height: 32upx; + line-height: 32upx; + right: 0; + bottom: 0; + z-index: -1; + font-size: 32upx; + margin: auto; + color: #ccc; + -webkit-animation: cuIcon-spin 2s infinite linear; + animation: cuIcon-spin 2s infinite linear; + display: block; +} + +.response { + width: 100%; +} + +/* ================== + 开关 + ==================== */ + +switch, +checkbox, +radio { + position: relative; +} + +switch::after, +switch::before { + font-family: "cuIcon"; + content: "\e645"; + position: absolute; + color: #ffffff !important; + top: 0%; + left: 0upx; + font-size: 26upx; + line-height: 26px; + width: 50%; + text-align: center; + pointer-events: none; + transform: scale(0, 0); + transition: all 0.3s ease-in-out 0s; + z-index: 9; + bottom: 0; + height: 26px; + margin: auto; +} + +switch::before { + content: "\e646"; + right: 0; + transform: scale(1, 1); + left: auto; +} + +switch[checked]::after, +switch.checked::after { + transform: scale(1, 1); +} + +switch[checked]::before, +switch.checked::before { + transform: scale(0, 0); +} + +/* #ifndef MP-ALIPAY */ +radio::before, +checkbox::before { + font-family: "cuIcon"; + content: "\e645"; + position: absolute; + color: #ffffff !important; + top: 50%; + margin-top: -8px; + right: 5px; + font-size: 32upx; + line-height: 16px; + pointer-events: none; + transform: scale(1, 1); + transition: all 0.3s ease-in-out 0s; + z-index: 9; +} + +radio .wx-radio-input, +checkbox .wx-checkbox-input, +radio .uni-radio-input, +checkbox .uni-checkbox-input { + margin: 0; + width: 24px; + height: 24px; +} + +checkbox.round .wx-checkbox-input, +checkbox.round .uni-checkbox-input { + border-radius: 100upx; +} + +/* #endif */ + +switch[checked]::before { + transform: scale(0, 0); +} + +switch .wx-switch-input, +switch .uni-switch-input { + border: none; + padding: 0 24px; + width: 48px; + height: 26px; + margin: 0; + border-radius: 100upx; +} + +switch .wx-switch-input:not([class*="bg-"]), +switch .uni-switch-input:not([class*="bg-"]) { + background: #8799a3 !important; +} + +switch .wx-switch-input::after, +switch .uni-switch-input::after { + margin: auto; + width: 26px; + height: 26px; + border-radius: 100upx; + left: 0upx; + top: 0upx; + bottom: 0upx; + position: absolute; + transform: scale(0.9, 0.9); + transition: all 0.1s ease-in-out 0s; +} + +switch .wx-switch-input.wx-switch-input-checked::after, +switch .uni-switch-input.uni-switch-input-checked::after { + margin: auto; + left: 22px; + box-shadow: none; + transform: scale(0.9, 0.9); +} + +radio-group { + display: inline-block; +} + + + +switch.radius .wx-switch-input::after, +switch.radius .wx-switch-input, +switch.radius .wx-switch-input::before, +switch.radius .uni-switch-input::after, +switch.radius .uni-switch-input, +switch.radius .uni-switch-input::before { + border-radius: 10upx; +} + +switch .wx-switch-input::before, +radio.radio::before, +checkbox .wx-checkbox-input::before, +radio .wx-radio-input::before, +switch .uni-switch-input::before, +radio.radio::before, +checkbox .uni-checkbox-input::before, +radio .uni-radio-input::before { + display: none; +} + +radio.radio[checked]::after, +radio.radio .uni-radio-input-checked::after { + content: ""; + background-color: transparent; + display: block; + position: absolute; + width: 8px; + height: 8px; + z-index: 999; + top: 0upx; + left: 0upx; + right: 0; + bottom: 0; + margin: auto; + border-radius: 200upx; + /* #ifndef MP */ + border: 7px solid #ffffff !important; + /* #endif */ + + /* #ifdef MP */ + border: 8px solid #ffffff !important; + /* #endif */ +} + +.switch-sex::after { + content: "\e71c"; +} + +.switch-sex::before { + content: "\e71a"; +} + +.switch-sex .wx-switch-input, +.switch-sex .uni-switch-input { + background: #e54d42 !important; + border-color: #e54d42 !important; +} + +.switch-sex[checked] .wx-switch-input, +.switch-sex.checked .uni-switch-input { + background: #0081ff !important; + border-color: #0081ff !important; +} + +switch.red[checked] .wx-switch-input.wx-switch-input-checked, +checkbox.red[checked] .wx-checkbox-input, +radio.red[checked] .wx-radio-input, +switch.red.checked .uni-switch-input.uni-switch-input-checked, +checkbox.red.checked .uni-checkbox-input, +radio.red.checked .uni-radio-input { + background-color: #e54d42 !important; + border-color: #e54d42 !important; + color: #ffffff !important; +} + +switch.orange[checked] .wx-switch-input, +checkbox.orange[checked] .wx-checkbox-input, +radio.orange[checked] .wx-radio-input, +switch.orange.checked .uni-switch-input, +checkbox.orange.checked .uni-checkbox-input, +radio.orange.checked .uni-radio-input { + background-color: #f37b1d !important; + border-color: #f37b1d !important; + color: #ffffff !important; +} + +switch.yellow[checked] .wx-switch-input, +checkbox.yellow[checked] .wx-checkbox-input, +radio.yellow[checked] .wx-radio-input, +switch.yellow.checked .uni-switch-input, +checkbox.yellow.checked .uni-checkbox-input, +radio.yellow.checked .uni-radio-input { + background-color: #fbbd08 !important; + border-color: #fbbd08 !important; + color: #333333 !important; +} + +switch.olive[checked] .wx-switch-input, +checkbox.olive[checked] .wx-checkbox-input, +radio.olive[checked] .wx-radio-input, +switch.olive.checked .uni-switch-input, +checkbox.olive.checked .uni-checkbox-input, +radio.olive.checked .uni-radio-input { + background-color: #8dc63f !important; + border-color: #8dc63f !important; + color: #ffffff !important; +} + +switch.green[checked] .wx-switch-input, +switch[checked] .wx-switch-input, +checkbox.green[checked] .wx-checkbox-input, +checkbox[checked] .wx-checkbox-input, +radio.green[checked] .wx-radio-input, +radio[checked] .wx-radio-input, +switch.green.checked .uni-switch-input, +switch.checked .uni-switch-input, +checkbox.green.checked .uni-checkbox-input, +checkbox.checked .uni-checkbox-input, +radio.green.checked .uni-radio-input, +radio.checked .uni-radio-input { + background-color: #39b54a !important; + border-color: #39b54a !important; + color: #ffffff !important; + border-color: #39B54A !important; +} + +switch.cyan[checked] .wx-switch-input, +checkbox.cyan[checked] .wx-checkbox-input, +radio.cyan[checked] .wx-radio-input, +switch.cyan.checked .uni-switch-input, +checkbox.cyan.checked .uni-checkbox-input, +radio.cyan.checked .uni-radio-input { + background-color: #1cbbb4 !important; + border-color: #1cbbb4 !important; + color: #ffffff !important; +} + +switch.blue[checked] .wx-switch-input, +checkbox.blue[checked] .wx-checkbox-input, +radio.blue[checked] .wx-radio-input, +switch.blue.checked .uni-switch-input, +checkbox.blue.checked .uni-checkbox-input, +radio.blue.checked .uni-radio-input { + background-color: #0081ff !important; + border-color: #0081ff !important; + color: #ffffff !important; +} + +switch.purple[checked] .wx-switch-input, +checkbox.purple[checked] .wx-checkbox-input, +radio.purple[checked] .wx-radio-input, +switch.purple.checked .uni-switch-input, +checkbox.purple.checked .uni-checkbox-input, +radio.purple.checked .uni-radio-input { + background-color: #6739b6 !important; + border-color: #6739b6 !important; + color: #ffffff !important; +} + +switch.mauve[checked] .wx-switch-input, +checkbox.mauve[checked] .wx-checkbox-input, +radio.mauve[checked] .wx-radio-input, +switch.mauve.checked .uni-switch-input, +checkbox.mauve.checked .uni-checkbox-input, +radio.mauve.checked .uni-radio-input { + background-color: #9c26b0 !important; + border-color: #9c26b0 !important; + color: #ffffff !important; +} + +switch.pink[checked] .wx-switch-input, +checkbox.pink[checked] .wx-checkbox-input, +radio.pink[checked] .wx-radio-input, +switch.pink.checked .uni-switch-input, +checkbox.pink.checked .uni-checkbox-input, +radio.pink.checked .uni-radio-input { + background-color: #e03997 !important; + border-color: #e03997 !important; + color: #ffffff !important; +} + +switch.brown[checked] .wx-switch-input, +checkbox.brown[checked] .wx-checkbox-input, +radio.brown[checked] .wx-radio-input, +switch.brown.checked .uni-switch-input, +checkbox.brown.checked .uni-checkbox-input, +radio.brown.checked .uni-radio-input { + background-color: #a5673f !important; + border-color: #a5673f !important; + color: #ffffff !important; +} + +switch.grey[checked] .wx-switch-input, +checkbox.grey[checked] .wx-checkbox-input, +radio.grey[checked] .wx-radio-input, +switch.grey.checked .uni-switch-input, +checkbox.grey.checked .uni-checkbox-input, +radio.grey.checked .uni-radio-input { + background-color: #8799a3 !important; + border-color: #8799a3 !important; + color: #ffffff !important; +} + +switch.gray[checked] .wx-switch-input, +checkbox.gray[checked] .wx-checkbox-input, +radio.gray[checked] .wx-radio-input, +switch.gray.checked .uni-switch-input, +checkbox.gray.checked .uni-checkbox-input, +radio.gray.checked .uni-radio-input { + background-color: #f0f0f0 !important; + border-color: #f0f0f0 !important; + color: #333333 !important; +} + +switch.black[checked] .wx-switch-input, +checkbox.black[checked] .wx-checkbox-input, +radio.black[checked] .wx-radio-input, +switch.black.checked .uni-switch-input, +checkbox.black.checked .uni-checkbox-input, +radio.black.checked .uni-radio-input { + background-color: #333333 !important; + border-color: #333333 !important; + color: #ffffff !important; +} + +switch.white[checked] .wx-switch-input, +checkbox.white[checked] .wx-checkbox-input, +radio.white[checked] .wx-radio-input, +switch.white.checked .uni-switch-input, +checkbox.white.checked .uni-checkbox-input, +radio.white.checked .uni-radio-input { + background-color: #ffffff !important; + border-color: #ffffff !important; + color: #333333 !important; +} + +/* ================== + 边框 + ==================== */ + +/* -- 实线 -- */ + +.solid, +.solid-top, +.solid-right, +.solid-bottom, +.solid-left, +.solids, +.solids-top, +.solids-right, +.solids-bottom, +.solids-left, +.dashed, +.dashed-top, +.dashed-right, +.dashed-bottom, +.dashed-left { + position: relative; +} + +.solid::after, +.solid-top::after, +.solid-right::after, +.solid-bottom::after, +.solid-left::after, +.solids::after, +.solids-top::after, +.solids-right::after, +.solids-bottom::after, +.solids-left::after, +.dashed::after, +.dashed-top::after, +.dashed-right::after, +.dashed-bottom::after, +.dashed-left::after { + content: " "; + width: 200%; + height: 200%; + position: absolute; + top: 0; + left: 0; + border-radius: inherit; + transform: scale(0.5); + transform-origin: 0 0; + pointer-events: none; + box-sizing: border-box; +} + +.solid::after { + border: 1upx solid rgba(0, 0, 0, 0.1); +} + +.solid-top::after { + border-top: 1upx solid rgba(0, 0, 0, 0.1); +} + +.solid-right::after { + border-right: 1upx solid rgba(0, 0, 0, 0.1); +} + +.solid-bottom::after { + border-bottom: 1upx solid rgba(0, 0, 0, 0.1); +} + +.solid-left::after { + border-left: 1upx solid rgba(0, 0, 0, 0.1); +} + +.solids::after { + border: 8upx solid #eee; +} + +.solids-top::after { + border-top: 8upx solid #eee; +} + +.solids-right::after { + border-right: 8upx solid #eee; +} + +.solids-bottom::after { + border-bottom: 8upx solid #eee; +} + +.solids-left::after { + border-left: 8upx solid #eee; +} + +/* -- 虚线 -- */ + +.dashed::after { + border: 1upx dashed #ddd; +} + +.dashed-top::after { + border-top: 1upx dashed #ddd; +} + +.dashed-right::after { + border-right: 1upx dashed #ddd; +} + +.dashed-bottom::after { + border-bottom: 1upx dashed #ddd; +} + +.dashed-left::after { + border-left: 1upx dashed #ddd; +} + +/* -- 阴影 -- */ + +.shadow[class*='white'] { + --ShadowSize: 0 1upx 6upx; +} + +.shadow-lg { + --ShadowSize: 0upx 40upx 100upx 0upx; +} + +.shadow-warp { + position: relative; + box-shadow: 0 0 10upx rgba(0, 0, 0, 0.1); +} + +.shadow-warp:before, +.shadow-warp:after { + position: absolute; + content: ""; + top: 20upx; + bottom: 30upx; + left: 20upx; + width: 50%; + box-shadow: 0 30upx 20upx rgba(0, 0, 0, 0.2); + transform: rotate(-3deg); + z-index: -1; +} + +.shadow-warp:after { + right: 20upx; + left: auto; + transform: rotate(3deg); +} + +.shadow-blur { + position: relative; +} + +.shadow-blur::before { + content: ""; + display: block; + background: inherit; + filter: blur(10upx); + position: absolute; + width: 100%; + height: 100%; + top: 10upx; + left: 10upx; + z-index: -1; + opacity: 0.4; + transform-origin: 0 0; + border-radius: inherit; + transform: scale(1, 1); +} + +/* ================== + 按钮 + ==================== */ + +.cu-btn { + position: relative; + border: 0upx; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + padding: 0 30upx; + font-size: 28upx; + height: 64upx; + line-height: 1; + text-align: center; + text-decoration: none; + overflow: visible; + margin-left: initial; + transform: translate(0upx, 0upx); + margin-right: initial; +} + +.cu-btn::after { + display: none; +} + +.cu-btn:not([class*="bg-"]) { + background-color: #f0f0f0; +} + +.cu-btn[class*="line"] { + background-color: transparent; +} + +.cu-btn[class*="line"]::after { + content: " "; + display: block; + width: 200%; + height: 200%; + position: absolute; + top: 0; + left: 0; + border: 1upx solid currentColor; + transform: scale(0.5); + transform-origin: 0 0; + box-sizing: border-box; + border-radius: 12upx; + z-index: 1; + pointer-events: none; +} + +.cu-btn.round[class*="line"]::after { + border-radius: 1000upx; +} + +.cu-btn[class*="lines"]::after { + border: 6upx solid currentColor; +} + +.cu-btn[class*="bg-"]::after { + display: none; +} + +.cu-btn.sm { + padding: 0 20upx; + font-size: 20upx; + height: 48upx; +} + +.cu-btn.lg { + padding: 0 40upx; + font-size: 32upx; + height: 80upx; +} + +.cu-btn.cuIcon.sm { + width: 48upx; + height: 48upx; +} + +.cu-btn.cuIcon { + width: 64upx; + height: 64upx; + border-radius: 500upx; + padding: 0; +} + +button.cuIcon.lg { + width: 80upx; + height: 80upx; +} + +.cu-btn.shadow-blur::before { + top: 4upx; + left: 4upx; + filter: blur(6upx); + opacity: 0.6; +} + +.cu-btn.button-hover { + transform: translate(1upx, 1upx); +} + +.block { + display: block; +} + +.cu-btn.block { + display: flex; +} + +.cu-btn[disabled] { + opacity: 0.6; + color: #ffffff; +} + +/* ================== + 徽章 + ==================== */ + +.cu-tag { + font-size: 24upx; + vertical-align: middle; + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + box-sizing: border-box; + padding: 0upx 16upx; + height: 48upx; + font-family: Helvetica Neue, Helvetica, sans-serif; + white-space: nowrap; +} + +.cu-tag:not([class*="bg"]):not([class*="line"]) { + background-color: #f1f1f1; +} + +.cu-tag[class*="line-"]::after { + content: " "; + width: 200%; + height: 200%; + position: absolute; + top: 0; + left: 0; + border: 1upx solid currentColor; + transform: scale(0.5); + transform-origin: 0 0; + box-sizing: border-box; + border-radius: inherit; + z-index: 1; + pointer-events: none; +} + +.cu-tag.radius[class*="line"]::after { + border-radius: 12upx; +} + +.cu-tag.round[class*="line"]::after { + border-radius: 1000upx; +} + +.cu-tag[class*="line-"]::after { + border-radius: 0; +} + +.cu-tag+.cu-tag { + margin-left: 10upx; +} + +.cu-tag.sm { + font-size: 20upx; + padding: 0upx 12upx; + height: 32upx; +} + +.cu-capsule { + display: inline-flex; + vertical-align: middle; +} + +.cu-capsule+.cu-capsule { + margin-left: 10upx; +} + +.cu-capsule .cu-tag { + margin: 0; +} + +.cu-capsule .cu-tag[class*="line-"]:last-child::after { + border-left: 0upx solid transparent; +} + +.cu-capsule .cu-tag[class*="line-"]:first-child::after { + border-right: 0upx solid transparent; +} + +.cu-capsule.radius .cu-tag:first-child { + border-top-left-radius: 6upx; + border-bottom-left-radius: 6upx; +} + +.cu-capsule.radius .cu-tag:last-child::after, +.cu-capsule.radius .cu-tag[class*="line-"] { + border-top-right-radius: 12upx; + border-bottom-right-radius: 12upx; +} + +.cu-capsule.round .cu-tag:first-child { + border-top-left-radius: 200upx; + border-bottom-left-radius: 200upx; + text-indent: 4upx; +} + +.cu-capsule.round .cu-tag:last-child::after, +.cu-capsule.round .cu-tag:last-child { + border-top-right-radius: 200upx; + border-bottom-right-radius: 200upx; + text-indent: -4upx; +} + +.cu-tag.badge { + border-radius: 200upx; + position: absolute; + top: -10upx; + right: -10upx; + font-size: 20upx; + padding: 0upx 10upx; + height: 28upx; + color: #ffffff; +} + +.cu-tag.badge:not([class*="bg-"]) { + background-color: #dd514c; +} + +.cu-tag:empty:not([class*="cuIcon-"]) { + padding: 0upx; + width: 16upx; + height: 16upx; + top: -4upx; + right: -4upx; +} + +.cu-tag[class*="cuIcon-"] { + width: 32upx; + height: 32upx; + top: -4upx; + right: -4upx; +} + +/* ================== + 头像 + ==================== */ + +.cu-avatar { + font-variant: small-caps; + margin: 0; + padding: 0; + display: inline-flex; + text-align: center; + justify-content: center; + align-items: center; + background-color: #ccc; + color: #ffffff; + white-space: nowrap; + position: relative; + width: 64upx; + height: 64upx; + background-size: cover; + background-position: center; + vertical-align: middle; + font-size: 1.5em; +} + +.cu-avatar.sm { + width: 48upx; + height: 48upx; + font-size: 1em; +} + +.cu-avatar.lg { + width: 96upx; + height: 96upx; + font-size: 2em; +} + +.cu-avatar.xl { + width: 128upx; + height: 128upx; + font-size: 2.5em; +} + +.cu-avatar .avatar-text { + font-size: 0.4em; +} + +.cu-avatar-group { + direction: rtl; + unicode-bidi: bidi-override; + padding: 0 10upx 0 40upx; + display: inline-block; +} + +.cu-avatar-group .cu-avatar { + margin-left: -30upx; + border: 4upx solid #f1f1f1; + vertical-align: middle; +} + +.cu-avatar-group .cu-avatar.sm { + margin-left: -20upx; + border: 1upx solid #f1f1f1; +} + +/* ================== + 进度条 + ==================== */ + +.cu-progress { + overflow: hidden; + height: 28upx; + background-color: #ebeef5; + display: inline-flex; + align-items: center; + width: 100%; +} + +.cu-progress+view, +.cu-progress+text { + line-height: 1; +} + +.cu-progress.xs { + height: 10upx; +} + +.cu-progress.sm { + height: 20upx; +} + +.cu-progress view { + width: 0; + height: 100%; + align-items: center; + display: flex; + justify-items: flex-end; + justify-content: space-around; + font-size: 20upx; + color: #ffffff; + transition: width 0.6s ease; +} + +.cu-progress text { + align-items: center; + display: flex; + font-size: 20upx; + color: #333333; + text-indent: 10upx; +} + +.cu-progress.text-progress { + padding-right: 60upx; +} + +.cu-progress.striped view { + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 72upx 72upx; +} + +.cu-progress.active view { + animation: progress-stripes 2s linear infinite; +} + +@keyframes progress-stripes { + from { + background-position: 72upx 0; + } + + to { + background-position: 0 0; + } +} + +/* ================== + 加载 + ==================== */ + +.cu-load { + display: block; + line-height: 3em; + text-align: center; +} + +.cu-load::before { + font-family: "cuIcon"; + display: inline-block; + margin-right: 6upx; +} + +.cu-load.loading::before { + content: "\e67a"; + animation: cuIcon-spin 2s infinite linear; +} + +.cu-load.loading::after { + content: "加载中..."; +} + +.cu-load.over::before { + content: "\e64a"; +} + +.cu-load.over::after { + content: "没有更多了"; +} + +.cu-load.erro::before { + content: "\e658"; +} + +.cu-load.erro::after { + content: "加载失败"; +} + +.cu-load.load-cuIcon::before { + font-size: 32upx; +} + +.cu-load.load-cuIcon::after { + display: none; +} + +.cu-load.load-cuIcon.over { + display: none; +} + +.cu-load.load-modal { + position: fixed; + top: 0; + right: 0; + bottom: 140upx; + left: 0; + margin: auto; + width: 260upx; + height: 260upx; + background-color: #ffffff; + border-radius: 10upx; + box-shadow: 0 0 0upx 2000upx rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; + font-size: 28upx; + z-index: 9999; + line-height: 2.4em; +} + +.cu-load.load-modal [class*="cuIcon-"] { + font-size: 60upx; +} + +.cu-load.load-modal image { + width: 70upx; + height: 70upx; +} + +.cu-load.load-modal::after { + content: ""; + position: absolute; + background-color: #ffffff; + border-radius: 50%; + width: 200upx; + height: 200upx; + font-size: 10px; + border-top: 6upx solid rgba(0, 0, 0, 0.05); + border-right: 6upx solid rgba(0, 0, 0, 0.05); + border-bottom: 6upx solid rgba(0, 0, 0, 0.05); + border-left: 6upx solid #f37b1d; + animation: cuIcon-spin 1s infinite linear; + z-index: -1; +} + +.load-progress { + pointer-events: none; + top: 0; + position: fixed; + width: 100%; + left: 0; + z-index: 2000; +} + +.load-progress.hide { + display: none; +} + +.load-progress .load-progress-bar { + position: relative; + width: 100%; + height: 4upx; + overflow: hidden; + transition: all 200ms ease 0s; +} + +.load-progress .load-progress-spinner { + position: absolute; + top: 10upx; + right: 10upx; + z-index: 2000; + display: block; +} + +.load-progress .load-progress-spinner::after { + content: ""; + display: block; + width: 24upx; + height: 24upx; + -webkit-box-sizing: border-box; + box-sizing: border-box; + border: solid 4upx transparent; + border-top-color: inherit; + border-left-color: inherit; + border-radius: 50%; + -webkit-animation: load-progress-spinner 0.4s linear infinite; + animation: load-progress-spinner 0.4s linear infinite; +} + +@-webkit-keyframes load-progress-spinner { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +@keyframes load-progress-spinner { + 0% { + -webkit-transform: rotate(0); + transform: rotate(0); + } + + 100% { + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + } +} + +/* ================== + 列表 + ==================== */ +.grayscale { + filter: grayscale(1); +} + +.cu-list+.cu-list { + margin-top: 30upx +} + +.cu-list>.cu-item { + transition: all .6s ease-in-out 0s; + transform: translateX(0upx) +} + +.cu-list>.cu-item.move-cur { + transform: translateX(-260upx) +} + +.cu-list>.cu-item .move { + position: absolute; + right: 0; + display: flex; + width: 260upx; + height: 100%; + transform: translateX(100%) +} + +.cu-list>.cu-item .move view { + display: flex; + flex: 1; + justify-content: center; + align-items: center +} + +.cu-list.menu-avatar { + overflow: hidden; +} + +.cu-list.menu-avatar>.cu-item { + position: relative; + display: flex; + padding-right: 10upx; + height: 140upx; + background-color: #ffffff; + justify-content: flex-end; + align-items: center +} + +.cu-list.menu-avatar>.cu-item>.cu-avatar { + position: absolute; + left: 30upx +} + +.cu-list.menu-avatar>.cu-item .flex .text-cut { + max-width: 510upx +} + +.cu-list.menu-avatar>.cu-item .content { + position: absolute; + left: 146upx; + width: calc(100% - 96upx - 60upx - 120upx - 20upx); + line-height: 1.6em; +} + +.cu-list.menu-avatar>.cu-item .content.flex-sub { + width: calc(100% - 96upx - 60upx - 20upx); +} + +.cu-list.menu-avatar>.cu-item .content>view:first-child { + font-size: 30upx; + display: flex; + align-items: center +} + +.cu-list.menu-avatar>.cu-item .content .cu-tag.sm { + display: inline-block; + margin-left: 10upx; + height: 28upx; + font-size: 16upx; + line-height: 32upx +} + +.cu-list.menu-avatar>.cu-item .action { + width: 100upx; + text-align: center +} + +.cu-list.menu-avatar>.cu-item .action view+view { + margin-top: 10upx +} + +.cu-list.menu-avatar.comment>.cu-item .content { + position: relative; + left: 0; + width: auto; + flex: 1; +} + +.cu-list.menu-avatar.comment>.cu-item { + padding: 30upx 30upx 30upx 120upx; + height: auto +} + +.cu-list.menu-avatar.comment .cu-avatar { + align-self: flex-start +} + +.cu-list.menu>.cu-item { + position: relative; + display: flex; + padding: 0 30upx; + min-height: 100upx; + background-color: #ffffff; + justify-content: space-between; + align-items: center +} + +.cu-list.menu>.cu-item:last-child:after { + border: none +} + +.cu-list.menu-avatar>.cu-item:after, +.cu-list.menu>.cu-item:after { + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; + width: 200%; + height: 200%; + border-bottom: 1upx solid #ddd; + border-radius: inherit; + content: " "; + transform: scale(.5); + transform-origin: 0 0; + pointer-events: none +} + +.cu-list.menu>.cu-item.grayscale { + background-color: #f5f5f5 +} + +.cu-list.menu>.cu-item.cur { + background-color: #fcf7e9 +} + +.cu-list.menu>.cu-item.arrow { + padding-right: 90upx +} + +.cu-list.menu>.cu-item.arrow:before { + position: absolute; + top: 0; + right: 30upx; + bottom: 0; + display: block; + margin: auto; + width: 30upx; + height: 30upx; + color: #8799a3; + content: "\e6a3"; + text-align: center; + font-size: 34upx; + font-family: cuIcon; + line-height: 30upx +} + +.cu-list.menu>.cu-item button.content { + padding: 0; + background-color: transparent; + justify-content: flex-start +} + +.cu-list.menu>.cu-item button.content:after { + display: none +} + +.cu-list.menu>.cu-item .cu-avatar-group .cu-avatar { + border-color: #ffffff +} + +.cu-list.menu>.cu-item .content>view:first-child { + display: flex; + align-items: center +} + +.cu-list.menu>.cu-item .content>text[class*=cuIcon] { + display: inline-block; + margin-right: 10upx; + width: 1.6em; + text-align: center +} + +.cu-list.menu>.cu-item .content>image { + display: inline-block; + margin-right: 10upx; + width: 1.6em; + height: 1.6em; + vertical-align: middle +} + +.cu-list.menu>.cu-item .content { + font-size: 30upx; + line-height: 1.6em; + flex: 1 +} + +.cu-list.menu>.cu-item .content .cu-tag.sm { + display: inline-block; + margin-left: 10upx; + height: 28upx; + font-size: 16upx; + line-height: 32upx +} + +.cu-list.menu>.cu-item .action .cu-tag:empty { + right: 10upx +} + +.cu-list.menu { + display: block; + overflow: hidden +} + +.cu-list.menu.sm-border>.cu-item:after { + left: 30upx; + width: calc(200% - 120upx) +} + +.cu-list.grid>.cu-item { + position: relative; + display: flex; + padding: 20upx 0 30upx; + transition-duration: 0s; + flex-direction: column +} + +.cu-list.grid>.cu-item:after { + position: absolute; + top: 0; + left: 0; + box-sizing: border-box; + width: 200%; + height: 200%; + border-right: 1px solid rgba(0, 0, 0, .1); + border-bottom: 1px solid rgba(0, 0, 0, .1); + border-radius: inherit; + content: " "; + transform: scale(.5); + transform-origin: 0 0; + pointer-events: none +} + +.cu-list.grid>.cu-item text { + display: block; + margin-top: 10upx; + color: #888; + font-size: 26upx; + line-height: 40upx +} + +.cu-list.grid>.cu-item [class*=cuIcon] { + position: relative; + display: block; + margin-top: 20upx; + width: 100%; + font-size: 48upx +} + +.cu-list.grid>.cu-item .cu-tag { + right: auto; + left: 50%; + margin-left: 20upx +} + +.cu-list.grid { + background-color: #ffffff; + text-align: center +} + +.cu-list.grid.no-border>.cu-item { + padding-top: 10upx; + padding-bottom: 20upx +} + +.cu-list.grid.no-border>.cu-item:after { + border: none +} + +.cu-list.grid.no-border { + padding: 20upx 10upx +} + +.cu-list.grid.col-3>.cu-item:nth-child(3n):after, +.cu-list.grid.col-4>.cu-item:nth-child(4n):after, +.cu-list.grid.col-5>.cu-item:nth-child(5n):after { + border-right-width: 0 +} + +.cu-list.card-menu { + overflow: hidden; + margin-right: 30upx; + margin-left: 30upx; + border-radius: 20upx +} + + +/* ================== + 操作条 + ==================== */ + +.cu-bar { + display: flex; + position: relative; + align-items: center; + min-height: 100upx; + justify-content: space-between; +} + +.cu-bar .action { + display: flex; + align-items: center; + height: 100%; + justify-content: center; + max-width: 100%; +} + +.cu-bar .action.border-title { + position: relative; + top: -10upx; +} + +.cu-bar .action.border-title text[class*="bg-"]:last-child { + position: absolute; + bottom: -0.5rem; + min-width: 2rem; + height: 6upx; + left: 0; +} + +.cu-bar .action.sub-title { + position: relative; + top: -0.2rem; +} + +.cu-bar .action.sub-title text { + position: relative; + z-index: 1; +} + +.cu-bar .action.sub-title text[class*="bg-"]:last-child { + position: absolute; + display: inline-block; + bottom: -0.2rem; + border-radius: 6upx; + width: 100%; + height: 0.6rem; + left: 0.6rem; + opacity: 0.3; + z-index: 0; +} + +.cu-bar .action.sub-title text[class*="text-"]:last-child { + position: absolute; + display: inline-block; + bottom: -0.7rem; + left: 0.5rem; + opacity: 0.2; + z-index: 0; + text-align: right; + font-weight: 900; + font-size: 36upx; +} + +.cu-bar.justify-center .action.border-title text:last-child, +.cu-bar.justify-center .action.sub-title text:last-child { + left: 0; + right: 0; + margin: auto; + text-align: center; +} + +.cu-bar .action:first-child { + margin-left: 30upx; + font-size: 30upx; +} + +.cu-bar .action text.text-cut { + text-align: left; + width: 100%; +} + +.cu-bar .cu-avatar:first-child { + margin-left: 20upx; +} + +.cu-bar .action:first-child>text[class*="cuIcon-"] { + margin-left: -0.3em; + margin-right: 0.3em; +} + +.cu-bar .action:last-child { + margin-right: 30upx; +} + +.cu-bar .action>text[class*="cuIcon-"], +.cu-bar .action>view[class*="cuIcon-"] { + font-size: 36upx; +} + +.cu-bar .action>text[class*="cuIcon-"]+text[class*="cuIcon-"] { + margin-left: 0.5em; +} + +.cu-bar .content { + position: absolute; + text-align: center; + width: calc(100% - 340upx); + left: 0; + right: 0; + bottom: 0; + top: 0; + margin: auto; + height: 60upx; + font-size: 32upx; + line-height: 60upx; + cursor: none; + pointer-events: none; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.cu-bar.ios .content { + bottom: 7px; + height: 30px; + font-size: 32upx; + line-height: 30px; +} + +.cu-bar.btn-group { + justify-content: space-around; +} + +.cu-bar.btn-group button { + padding: 20upx 32upx; +} + +.cu-bar.btn-group button { + flex: 1; + margin: 0 20upx; + max-width: 50%; +} + +.cu-bar .search-form { + background-color: #f5f5f5; + line-height: 64upx; + height: 64upx; + font-size: 24upx; + color: #333333; + flex: 1; + display: flex; + align-items: center; + margin: 0 30upx; +} + +.cu-bar .search-form+.action { + margin-right: 30upx; +} + +.cu-bar .search-form input { + flex: 1; + padding-right: 30upx; + height: 64upx; + line-height: 64upx; + font-size: 26upx; + background-color: transparent; +} + +.cu-bar .search-form [class*="cuIcon-"] { + margin: 0 0.5em 0 0.8em; +} + +.cu-bar .search-form [class*="cuIcon-"]::before { + top: 0upx; +} + +.cu-bar.fixed, +.nav.fixed { + position: fixed; + width: 100%; + top: 0; + z-index: 1024; + box-shadow: 0 1upx 6upx rgba(0, 0, 0, 0.1); +} + +.cu-bar.foot { + position: fixed; + width: 100%; + bottom: 0; + z-index: 1024; + box-shadow: 0 -1upx 6upx rgba(0, 0, 0, 0.1); +} + +.cu-bar.tabbar { + padding: 0; + height: calc(100upx + env(safe-area-inset-bottom) / 2); + padding-bottom: calc(env(safe-area-inset-bottom) / 2); +} + +.cu-tabbar-height { + min-height: 100upx; + height: calc(100upx + env(safe-area-inset-bottom) / 2); +} + +.cu-bar.tabbar.shadow { + box-shadow: 0 -1upx 6upx rgba(0, 0, 0, 0.1); +} + +.cu-bar.tabbar .action { + font-size: 22upx; + position: relative; + flex: 1; + text-align: center; + padding: 0; + display: block; + height: auto; + line-height: 1; + margin: 0; + background-color: inherit; + overflow: initial; +} + +.cu-bar.tabbar.shop .action { + width: 140upx; + flex: initial; +} + +.cu-bar.tabbar .action.add-action { + position: relative; + z-index: 2; + padding-top: 50upx; +} + +.cu-bar.tabbar .action.add-action [class*="cuIcon-"] { + position: absolute; + width: 70upx; + z-index: 2; + height: 70upx; + border-radius: 50%; + line-height: 70upx; + font-size: 50upx; + top: -35upx; + left: 0; + right: 0; + margin: auto; + padding: 0; +} + +.cu-bar.tabbar .action.add-action::after { + content: ""; + position: absolute; + width: 100upx; + height: 100upx; + top: -50upx; + left: 0; + right: 0; + margin: auto; + box-shadow: 0 -3upx 8upx rgba(0, 0, 0, 0.08); + border-radius: 50upx; + background-color: inherit; + z-index: 0; +} + +.cu-bar.tabbar .action.add-action::before { + content: ""; + position: absolute; + width: 100upx; + height: 30upx; + bottom: 30upx; + left: 0; + right: 0; + margin: auto; + background-color: inherit; + z-index: 1; +} + +.cu-bar.tabbar .btn-group { + flex: 1; + display: flex; + justify-content: space-around; + align-items: center; + padding: 0 10upx; +} + +.cu-bar.tabbar button.action::after { + border: 0; +} + +.cu-bar.tabbar .action [class*="cuIcon-"] { + width: 100upx; + position: relative; + display: block; + height: auto; + margin: 0 auto 10upx; + text-align: center; + font-size: 40upx; +} + +.cu-bar.tabbar .action .cuIcon-cu-image { + margin: 0 auto; +} + +.cu-bar.tabbar .action .cuIcon-cu-image image { + width: 50upx; + height: 50upx; + display: inline-block; +} + +.cu-bar.tabbar .submit { + align-items: center; + display: flex; + justify-content: center; + text-align: center; + position: relative; + flex: 2; + align-self: stretch; +} + +.cu-bar.tabbar .submit:last-child { + flex: 2.6; +} + +.cu-bar.tabbar .submit+.submit { + flex: 2; +} + +.cu-bar.tabbar.border .action::before { + content: " "; + width: 200%; + height: 200%; + position: absolute; + top: 0; + left: 0; + transform: scale(0.5); + transform-origin: 0 0; + border-right: 1upx solid rgba(0, 0, 0, 0.1); + z-index: 3; +} + +.cu-bar.tabbar.border .action:last-child:before { + display: none; +} + +.cu-bar.input { + padding-right: 20upx; + background-color: #ffffff; +} + +.cu-bar.input input { + overflow: initial; + line-height: 64upx; + height: 64upx; + min-height: 64upx; + flex: 1; + font-size: 30upx; + margin: 0 20upx; +} + +.cu-bar.input .action { + margin-left: 20upx; +} + +.cu-bar.input .action [class*="cuIcon-"] { + font-size: 48upx; +} + +.cu-bar.input input+.action { + margin-right: 20upx; + margin-left: 0upx; +} + +.cu-bar.input .action:first-child [class*="cuIcon-"] { + margin-left: 0upx; +} + +.cu-custom { + display: block; + position: relative; +} + +.cu-custom .cu-bar .content { + width: calc(100% - 440upx); +} + +/* #ifdef MP-ALIPAY */ +.cu-custom .cu-bar .action .cuIcon-back { + opacity: 0; +} + +/* #endif */ + +.cu-custom .cu-bar .content image { + height: 60upx; + width: 240upx; +} + +.cu-custom .cu-bar { + min-height: 0px; + /* #ifdef MP-WEIXIN */ + padding-right: 220upx; + /* #endif */ + /* #ifdef MP-ALIPAY */ + padding-right: 150upx; + /* #endif */ + box-shadow: 0upx 0upx 0upx; + z-index: 9999; +} + +.cu-custom .cu-bar .border-custom { + position: relative; + background: rgba(0, 0, 0, 0.15); + border-radius: 1000upx; + height: 30px; +} + +.cu-custom .cu-bar .border-custom::after { + content: " "; + width: 200%; + height: 200%; + position: absolute; + top: 0; + left: 0; + border-radius: inherit; + transform: scale(0.5); + transform-origin: 0 0; + pointer-events: none; + box-sizing: border-box; + border: 1upx solid #ffffff; + opacity: 0.5; +} + +.cu-custom .cu-bar .border-custom::before { + content: " "; + width: 1upx; + height: 110%; + position: absolute; + top: 22.5%; + left: 0; + right: 0; + margin: auto; + transform: scale(0.5); + transform-origin: 0 0; + pointer-events: none; + box-sizing: border-box; + opacity: 0.6; + background-color: #ffffff; +} + +.cu-custom .cu-bar .border-custom text { + display: block; + flex: 1; + margin: auto !important; + text-align: center; + font-size: 34upx; +} + +/* ================== + 导航栏 + ==================== */ + +.nav { + white-space: nowrap; +} + +::-webkit-scrollbar { + display: none; +} + +.nav .cu-item { + height: 90upx; + display: inline-block; + line-height: 90upx; + margin: 0 10upx; + padding: 0 20upx; +} + +.nav .cu-item.cur { + border-bottom: 4upx solid; +} + +/* ================== + 时间轴 + ==================== */ + +.cu-timeline { + display: block; + background-color: #ffffff; +} + +.cu-timeline .cu-time { + width: 120upx; + text-align: center; + padding: 20upx 0; + font-size: 26upx; + color: #888; + display: block; +} + +.cu-timeline>.cu-item { + padding: 30upx 30upx 30upx 120upx; + position: relative; + display: block; + z-index: 0; +} + +.cu-timeline>.cu-item:not([class*="text-"]) { + color: #ccc; +} + +.cu-timeline>.cu-item::after { + content: ""; + display: block; + position: absolute; + width: 1upx; + background-color: #ddd; + left: 60upx; + height: 100%; + top: 0; + z-index: 8; +} + +.cu-timeline>.cu-item::before { + font-family: "cuIcon"; + display: block; + position: absolute; + top: 36upx; + z-index: 9; + background-color: #ffffff; + width: 50upx; + height: 50upx; + text-align: center; + border: none; + line-height: 50upx; + left: 36upx; +} + +.cu-timeline>.cu-item:not([class*="cuIcon-"])::before { + content: "\e763"; +} + +.cu-timeline>.cu-item[class*="cuIcon-"]::before { + background-color: #ffffff; + width: 50upx; + height: 50upx; + text-align: center; + border: none; + line-height: 50upx; + left: 36upx; +} + +.cu-timeline>.cu-item>.content { + padding: 30upx; + border-radius: 6upx; + display: block; + line-height: 1.6; +} + +.cu-timeline>.cu-item>.content:not([class*="bg-"]) { + background-color: #f1f1f1; + color: #333333; +} + +.cu-timeline>.cu-item>.content+.content { + margin-top: 20upx; +} + +/* ================== + 聊天 + ==================== */ + +.cu-chat { + display: flex; + flex-direction: column; +} + +.cu-chat .cu-item { + display: flex; + padding: 30upx 30upx 70upx; + position: relative; +} + +.cu-chat .cu-item>.cu-avatar { + width: 80upx; + height: 80upx; +} + +.cu-chat .cu-item>.main { + max-width: calc(100% - 260upx); + margin: 0 40upx; + display: flex; + align-items: center; +} + +.cu-chat .cu-item>image { + height: 320upx; +} + +.cu-chat .cu-item>.main .content { + padding: 20upx; + border-radius: 6upx; + display: inline-flex; + max-width: 100%; + align-items: center; + font-size: 30upx; + position: relative; + min-height: 80upx; + line-height: 40upx; + text-align: left; +} + +.cu-chat .cu-item>.main .content:not([class*="bg-"]) { + background-color: #ffffff; + color: #333333; +} + +.cu-chat .cu-item .date { + position: absolute; + font-size: 24upx; + color: #8799a3; + width: calc(100% - 320upx); + bottom: 20upx; + left: 160upx; +} + +.cu-chat .cu-item .action { + padding: 0 30upx; + display: flex; + align-items: center; +} + +.cu-chat .cu-item>.main .content::after { + content: ""; + top: 27upx; + transform: rotate(45deg); + position: absolute; + z-index: 100; + display: inline-block; + overflow: hidden; + width: 24upx; + height: 24upx; + left: -12upx; + right: initial; + background-color: inherit; +} + +.cu-chat .cu-item.self>.main .content::after { + left: auto; + right: -12upx; +} + +.cu-chat .cu-item>.main .content::before { + content: ""; + top: 30upx; + transform: rotate(45deg); + position: absolute; + z-index: -1; + display: inline-block; + overflow: hidden; + width: 24upx; + height: 24upx; + left: -12upx; + right: initial; + background-color: inherit; + filter: blur(5upx); + opacity: 0.3; +} + +.cu-chat .cu-item>.main .content:not([class*="bg-"])::before { + background-color: #333333; + opacity: 0.1; +} + +.cu-chat .cu-item.self>.main .content::before { + left: auto; + right: -12upx; +} + +.cu-chat .cu-item.self { + justify-content: flex-end; + text-align: right; +} + +.cu-chat .cu-info { + display: inline-block; + margin: 20upx auto; + font-size: 24upx; + padding: 8upx 12upx; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 6upx; + color: #ffffff; + max-width: 400upx; + line-height: 1.4; +} + +/* ================== + 卡片 + ==================== */ + +.cu-card { + display: block; + overflow: hidden; +} + +.cu-card>.cu-item { + display: block; + background-color: #ffffff; + overflow: hidden; + border-radius: 10upx; + margin: 30upx; +} + +.cu-card>.cu-item.shadow-blur { + overflow: initial; +} + +.cu-card.no-card>.cu-item { + margin: 0upx; + border-radius: 0upx; +} + +.cu-card .grid.grid-square { + margin-bottom: -20upx; +} + +.cu-card.case .image { + position: relative; +} + +.cu-card.case .image image { + width: 100%; +} + +.cu-card.case .image .cu-tag { + position: absolute; + right: 0; + top: 0; +} + +.cu-card.case .image .cu-bar { + position: absolute; + bottom: 0; + width: 100%; + background-color: transparent; + padding: 0upx 30upx; +} + +.cu-card.case.no-card .image { + margin: 30upx 30upx 0; + overflow: hidden; + border-radius: 10upx; +} + +.cu-card.dynamic { + display: block; +} + +.cu-card.dynamic>.cu-item { + display: block; + background-color: #ffffff; + overflow: hidden; +} + +.cu-card.dynamic>.cu-item>.text-content { + padding: 0 30upx 0; + max-height: 6.4em; + overflow: hidden; + font-size: 30upx; + margin-bottom: 20upx; +} + +.cu-card.dynamic>.cu-item .square-img { + width: 100%; + height: 200upx; + border-radius: 6upx; +} + +.cu-card.dynamic>.cu-item .only-img { + width: 100%; + height: 320upx; + border-radius: 6upx; +} + +/* card.dynamic>.cu-item .comment { + padding: 20upx; + background-color: #f1f1f1; + margin: 0 30upx 30upx; + border-radius: 6upx; +} */ + +.cu-card.article { + display: block; +} + +.cu-card.article>.cu-item { + padding-bottom: 30upx; +} + +.cu-card.article>.cu-item .title { + font-size: 30upx; + font-weight: 900; + color: #333333; + line-height: 100upx; + padding: 0 30upx; +} + +.cu-card.article>.cu-item .content { + display: flex; + padding: 0 30upx; +} + +.cu-card.article>.cu-item .content>image { + width: 240upx; + height: 6.4em; + margin-right: 20upx; + border-radius: 6upx; +} + +.cu-card.article>.cu-item .content .desc { + flex: 1; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.cu-card.article>.cu-item .content .text-content { + font-size: 28upx; + color: #888; + height: 4.8em; + overflow: hidden; +} + +/* ================== + 表单 + ==================== */ + +.cu-form-group { + background-color: #ffffff; + padding: 1upx 30upx; + display: flex; + align-items: center; + min-height: 100upx; + justify-content: space-between; +} + +.cu-form-group+.cu-form-group { + border-top: 1upx solid #eee; +} + +.cu-form-group .title { + text-align: justify; + padding-right: 30upx; + font-size: 30upx; + position: relative; + height: 60upx; + line-height: 60upx; +} + +.cu-form-group input { + flex: 1; + font-size: 30upx; + color: #555; + padding-right: 20upx; +} + +.cu-form-group>text[class*="cuIcon-"] { + font-size: 36upx; + padding: 0; + box-sizing: border-box; +} + +.cu-form-group textarea { + margin: 32upx 0 30upx; + height: 4.6em; + width: 100%; + line-height: 1.2em; + flex: 1; + font-size: 28upx; + padding: 0; +} + +.cu-form-group.align-start .title { + height: 1em; + margin-top: 32upx; + line-height: 1em; +} + +.cu-form-group picker { + flex: 1; + padding-right: 40upx; + overflow: hidden; + position: relative; +} + +.cu-form-group picker .picker { + line-height: 100upx; + font-size: 28upx; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + width: 100%; + text-align: right; +} + +.cu-form-group picker::after { + font-family: cuIcon; + display: block; + content: "\e6a3"; + position: absolute; + font-size: 34upx; + color: #8799a3; + line-height: 100upx; + width: 60upx; + text-align: center; + top: 0; + bottom: 0; + right: -20upx; + margin: auto; +} + +.cu-form-group textarea[disabled], +.cu-form-group textarea[disabled] .placeholder { + color: transparent; +} + +/* ================== + 模态窗口 + ==================== */ + +.cu-modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1110; + opacity: 0; + outline: 0; + text-align: center; + -ms-transform: scale(1.185); + transform: scale(1.185); + backface-visibility: hidden; + perspective: 2000upx; + background: rgba(0, 0, 0, 0.6); + transition: all 0.3s ease-in-out 0s; + pointer-events: none; +} + +.cu-modal::before { + content: "\200B"; + display: inline-block; + height: 100%; + vertical-align: middle; +} + +.cu-modal.show { + opacity: 1; + transition-duration: 0.3s; + -ms-transform: scale(1); + transform: scale(1); + overflow-x: hidden; + overflow-y: auto; + pointer-events: auto; +} + +.cu-dialog { + position: relative; + display: inline-block; + vertical-align: middle; + margin-left: auto; + margin-right: auto; + width: 680upx; + max-width: 100%; + background-color: #f8f8f8; + border-radius: 10upx; + overflow: hidden; +} + +.cu-modal.bottom-modal::before { + vertical-align: bottom; +} + +.cu-modal.bottom-modal .cu-dialog { + width: 100%; + border-radius: 0; +} + +.cu-modal.bottom-modal { + margin-bottom: -1000upx; +} + +.cu-modal.bottom-modal.show { + margin-bottom: 0; +} + +.cu-modal.drawer-modal { + transform: scale(1); + display: flex; +} + +.cu-modal.drawer-modal .cu-dialog { + height: 100%; + min-width: 200upx; + border-radius: 0; + margin: initial; + transition-duration: 0.3s; +} + +.cu-modal.drawer-modal.justify-start .cu-dialog { + transform: translateX(-100%); +} + +.cu-modal.drawer-modal.justify-end .cu-dialog { + transform: translateX(100%); +} + +.cu-modal.drawer-modal.show .cu-dialog { + transform: translateX(0%); +} +.cu-modal .cu-dialog>.cu-bar:first-child .action{ + min-width: 100rpx; + margin-right: 0; + min-height: 100rpx; +} +/* ================== + 轮播 + ==================== */ +swiper .a-swiper-dot { + display: inline-block; + width: 16upx; + height: 16upx; + background: rgba(0, 0, 0, .3); + border-radius: 50%; + vertical-align: middle; +} + +swiper[class*="-dot"] .wx-swiper-dots, +swiper[class*="-dot"] .a-swiper-dots, +swiper[class*="-dot"] .uni-swiper-dots { + display: flex; + align-items: center; + width: 100%; + justify-content: center; +} + +swiper.square-dot .wx-swiper-dot, +swiper.square-dot .a-swiper-dot, +swiper.square-dot .uni-swiper-dot { + background-color: #ffffff; + opacity: 0.4; + width: 10upx; + height: 10upx; + border-radius: 20upx; + margin: 0 8upx !important; +} + +swiper.square-dot .wx-swiper-dot.wx-swiper-dot-active, +swiper.square-dot .a-swiper-dot.a-swiper-dot-active, +swiper.square-dot .uni-swiper-dot.uni-swiper-dot-active { + opacity: 1; + width: 30upx; +} + +swiper.round-dot .wx-swiper-dot, +swiper.round-dot .a-swiper-dot, +swiper.round-dot .uni-swiper-dot { + width: 10upx; + height: 10upx; + position: relative; + margin: 4upx 8upx !important; +} + +swiper.round-dot .wx-swiper-dot.wx-swiper-dot-active::after, +swiper.round-dot .a-swiper-dot.a-swiper-dot-active::after, +swiper.round-dot .uni-swiper-dot.uni-swiper-dot-active::after { + content: ""; + position: absolute; + width: 10upx; + height: 10upx; + top: 0upx; + left: 0upx; + right: 0; + bottom: 0; + margin: auto; + background-color: #ffffff; + border-radius: 20upx; +} + +swiper.round-dot .wx-swiper-dot.wx-swiper-dot-active, +swiper.round-dot .a-swiper-dot.a-swiper-dot-active, +swiper.round-dot .uni-swiper-dot.uni-swiper-dot-active { + width: 18upx; + height: 18upx; +} + +.screen-swiper { + min-height: 375upx; +} + +.screen-swiper image, +.screen-swiper video, +.swiper-item image, +.swiper-item video { + width: 100%; + display: block; + height: 100%; + margin: 0; + pointer-events: none; +} + +.card-swiper { + height: 420upx !important; +} + +.card-swiper swiper-item { + width: 610upx !important; + left: 70upx; + box-sizing: border-box; + padding: 40upx 0upx 70upx; + overflow: initial; +} + +.card-swiper swiper-item .swiper-item { + width: 100%; + display: block; + height: 100%; + border-radius: 10upx; + transform: scale(0.9); + transition: all 0.2s ease-in 0s; + overflow: hidden; +} + +.card-swiper swiper-item.cur .swiper-item { + transform: none; + transition: all 0.2s ease-in 0s; +} + + +.tower-swiper { + height: 420upx; + position: relative; + max-width: 750upx; + overflow: hidden; +} + +.tower-swiper .tower-item { + position: absolute; + width: 300upx; + height: 380upx; + top: 0; + bottom: 0; + left: 50%; + margin: auto; + transition: all 0.2s ease-in 0s; + opacity: 1; +} + +.tower-swiper .tower-item.none { + opacity: 0; +} + +.tower-swiper .tower-item .swiper-item { + width: 100%; + height: 100%; + border-radius: 6upx; + overflow: hidden; +} + +/* ================== + 步骤条 + ==================== */ + +.cu-steps { + display: flex; +} + +scroll-view.cu-steps { + display: block; + white-space: nowrap; +} + +scroll-view.cu-steps .cu-item { + display: inline-block; +} + +.cu-steps .cu-item { + flex: 1; + text-align: center; + position: relative; + min-width: 100upx; +} + +.cu-steps .cu-item:not([class*="text-"]) { + color: #8799a3; +} + +.cu-steps .cu-item [class*="cuIcon-"], +.cu-steps .cu-item .num { + display: block; + font-size: 40upx; + line-height: 80upx; +} + +.cu-steps .cu-item::before, +.cu-steps .cu-item::after, +.cu-steps.steps-arrow .cu-item::before, +.cu-steps.steps-arrow .cu-item::after { + content: ""; + display: block; + position: absolute; + height: 0px; + width: calc(100% - 80upx); + border-bottom: 1px solid #ccc; + left: calc(0px - (100% - 80upx) / 2); + top: 40upx; + z-index: 0; +} + +.cu-steps.steps-arrow .cu-item::before, +.cu-steps.steps-arrow .cu-item::after { + content: "\e6a3"; + font-family: 'cuIcon'; + height: 30upx; + border-bottom-width: 0px; + line-height: 30upx; + top: 0; + bottom: 0; + margin: auto; + color: #ccc; +} + +.cu-steps.steps-bottom .cu-item::before, +.cu-steps.steps-bottom .cu-item::after { + bottom: 40upx; + top: initial; +} + +.cu-steps .cu-item::after { + border-bottom: 1px solid currentColor; + width: 0px; + transition: all 0.3s ease-in-out 0s; +} + +.cu-steps .cu-item[class*="text-"]::after { + width: calc(100% - 80upx); + color: currentColor; +} + +.cu-steps .cu-item:first-child::before, +.cu-steps .cu-item:first-child::after { + display: none; +} + +.cu-steps .cu-item .num { + width: 40upx; + height: 40upx; + border-radius: 50%; + line-height: 40upx; + margin: 20upx auto; + font-size: 24upx; + border: 1px solid currentColor; + position: relative; + overflow: hidden; +} + +.cu-steps .cu-item[class*="text-"] .num { + background-color: currentColor; +} + +.cu-steps .cu-item .num::before, +.cu-steps .cu-item .num::after { + content: attr(data-index); + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + margin: auto; + transition: all 0.3s ease-in-out 0s; + transform: translateY(0upx); +} + +.cu-steps .cu-item[class*="text-"] .num::before { + transform: translateY(-40upx); + color: #ffffff; +} + +.cu-steps .cu-item .num::after { + transform: translateY(40upx); + color: #ffffff; + transition: all 0.3s ease-in-out 0s; +} + +.cu-steps .cu-item[class*="text-"] .num::after { + content: "\e645"; + font-family: 'cuIcon'; + color: #ffffff; + transform: translateY(0upx); +} + +.cu-steps .cu-item[class*="text-"] .num.err::after { + content: "\e646"; +} + +/* ================== + 布局 + ==================== */ + +/* -- flex弹性布局 -- */ + +.flex { + display: flex; +} + +.basis-xs { + flex-basis: 20%; +} + +.basis-sm { + flex-basis: 40%; +} + +.basis-df { + flex-basis: 50%; +} + +.basis-lg { + flex-basis: 60%; +} + +.basis-xl { + flex-basis: 80%; +} + +.flex-sub { + flex: 1; +} + +.flex-twice { + flex: 2; +} + +.flex-treble { + flex: 3; +} + +.flex-direction { + flex-direction: column; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.align-start { + align-items: flex-start; +} + +.align-end { + align-items: flex-end; +} + +.align-center { + align-items: center; +} + +.align-stretch { + align-items: stretch; +} + +.self-start { + align-self: flex-start; +} + +.self-center { + align-self: flex-center; +} + +.self-end { + align-self: flex-end; +} + +.self-stretch { + align-self: stretch; +} + +.align-stretch { + align-items: stretch; +} + +.justify-start { + justify-content: flex-start; +} + +.justify-end { + justify-content: flex-end; +} + +.justify-center { + justify-content: center; +} + +.justify-between { + justify-content: space-between; +} + +.justify-around { + justify-content: space-around; +} + +/* grid布局 */ + +.grid { + display: flex; + flex-wrap: wrap; +} + +.grid.grid-square { + overflow: hidden; +} + +.grid.grid-square .cu-tag { + position: absolute; + right: 0; + top: 0; + border-bottom-left-radius: 6upx; + padding: 6upx 12upx; + height: auto; + background-color: rgba(0, 0, 0, 0.5); +} + +.grid.grid-square>view>text[class*="cuIcon-"] { + font-size: 52upx; + position: absolute; + color: #8799a3; + margin: auto; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.grid.grid-square>view { + margin-right: 20upx; + margin-bottom: 20upx; + border-radius: 6upx; + position: relative; + overflow: hidden; +} +.grid.grid-square>view.bg-img image { + width: 100%; + height: 100%; + position: absolute; +} +.grid.col-1.grid-square>view { + padding-bottom: 100%; + height: 0; + margin-right: 0; +} + +.grid.col-2.grid-square>view { + padding-bottom: calc((100% - 20upx)/2); + height: 0; + width: calc((100% - 20upx)/2); +} + +.grid.col-3.grid-square>view { + padding-bottom: calc((100% - 40upx)/3); + height: 0; + width: calc((100% - 40upx)/3); +} + +.grid.col-4.grid-square>view { + padding-bottom: calc((100% - 60upx)/4); + height: 0; + width: calc((100% - 60upx)/4); +} + +.grid.col-5.grid-square>view { + padding-bottom: calc((100% - 80upx)/5); + height: 0; + width: calc((100% - 80upx)/5); +} + +.grid.col-2.grid-square>view:nth-child(2n), +.grid.col-3.grid-square>view:nth-child(3n), +.grid.col-4.grid-square>view:nth-child(4n), +.grid.col-5.grid-square>view:nth-child(5n) { + margin-right: 0; +} + +.grid.col-1>view { + width: 100%; +} + +.grid.col-2>view { + width: 50%; +} + +.grid.col-3>view { + width: 33.33%; +} + +.grid.col-4>view { + width: 25%; +} + +.grid.col-5>view { + width: 20%; +} + +/* -- 内外边距 -- */ + +.margin-0 { + margin: 0; +} + +.margin-xs { + margin: 10upx; +} + +.margin-sm { + margin: 20upx; +} + +.margin { + margin: 30upx; +} + +.margin-lg { + margin: 40upx; +} + +.margin-xl { + margin: 50upx; +} + +.margin-top-xs { + margin-top: 10upx; +} + +.margin-top-sm { + margin-top: 20upx; +} + +.margin-top { + margin-top: 30upx; +} + +.margin-top-lg { + margin-top: 40upx; +} + +.margin-top-xl { + margin-top: 50upx; +} + +.margin-right-xs { + margin-right: 10upx; +} + +.margin-right-sm { + margin-right: 20upx; +} + +.margin-right { + margin-right: 30upx; +} + +.margin-right-lg { + margin-right: 40upx; +} + +.margin-right-xl { + margin-right: 50upx; +} + +.margin-bottom-xs { + margin-bottom: 10upx; +} + +.margin-bottom-sm { + margin-bottom: 20upx; +} + +.margin-bottom { + margin-bottom: 30upx; +} + +.margin-bottom-lg { + margin-bottom: 40upx; +} + +.margin-bottom-xl { + margin-bottom: 50upx; +} + +.margin-left-xs { + margin-left: 10upx; +} + +.margin-left-sm { + margin-left: 20upx; +} + +.margin-left { + margin-left: 30upx; +} + +.margin-left-lg { + margin-left: 40upx; +} + +.margin-left-xl { + margin-left: 50upx; +} + +.margin-lr-xs { + margin-left: 10upx; + margin-right: 10upx; +} + +.margin-lr-sm { + margin-left: 20upx; + margin-right: 20upx; +} + +.margin-lr { + margin-left: 30upx; + margin-right: 30upx; +} + +.margin-lr-lg { + margin-left: 40upx; + margin-right: 40upx; +} + +.margin-lr-xl { + margin-left: 50upx; + margin-right: 50upx; +} + +.margin-tb-xs { + margin-top: 10upx; + margin-bottom: 10upx; +} + +.margin-tb-sm { + margin-top: 20upx; + margin-bottom: 20upx; +} + +.margin-tb { + margin-top: 30upx; + margin-bottom: 30upx; +} + +.margin-tb-lg { + margin-top: 40upx; + margin-bottom: 40upx; +} + +.margin-tb-xl { + margin-top: 50upx; + margin-bottom: 50upx; +} + +.padding-0 { + padding: 0; +} + +.padding-xs { + padding: 10upx; +} + +.padding-sm { + padding: 20upx; +} + +.padding { + padding: 30upx; +} + +.padding-lg { + padding: 40upx; +} + +.padding-xl { + padding: 50upx; +} + +.padding-top-xs { + padding-top: 10upx; +} + +.padding-top-sm { + padding-top: 20upx; +} + +.padding-top { + padding-top: 30upx; +} + +.padding-top-lg { + padding-top: 40upx; +} + +.padding-top-xl { + padding-top: 50upx; +} + +.padding-right-xs { + padding-right: 10upx; +} + +.padding-right-sm { + padding-right: 20upx; +} + +.padding-right { + padding-right: 30upx; +} + +.padding-right-lg { + padding-right: 40upx; +} + +.padding-right-xl { + padding-right: 50upx; +} + +.padding-bottom-xs { + padding-bottom: 10upx; +} + +.padding-bottom-sm { + padding-bottom: 20upx; +} + +.padding-bottom { + padding-bottom: 30upx; +} + +.padding-bottom-lg { + padding-bottom: 40upx; +} + +.padding-bottom-xl { + padding-bottom: 50upx; +} + +.padding-left-xs { + padding-left: 10upx; +} + +.padding-left-sm { + padding-left: 20upx; +} + +.padding-left { + padding-left: 30upx; +} + +.padding-left-lg { + padding-left: 40upx; +} + +.padding-left-xl { + padding-left: 50upx; +} + +.padding-lr-xs { + padding-left: 10upx; + padding-right: 10upx; +} + +.padding-lr-sm { + padding-left: 20upx; + padding-right: 20upx; +} + +.padding-lr { + padding-left: 30upx; + padding-right: 30upx; +} + +.padding-lr-lg { + padding-left: 40upx; + padding-right: 40upx; +} + +.padding-lr-xl { + padding-left: 50upx; + padding-right: 50upx; +} + +.padding-tb-xs { + padding-top: 10upx; + padding-bottom: 10upx; +} + +.padding-tb-sm { + padding-top: 20upx; + padding-bottom: 20upx; +} + +.padding-tb { + padding-top: 30upx; + padding-bottom: 30upx; +} + +.padding-tb-lg { + padding-top: 40upx; + padding-bottom: 40upx; +} + +.padding-tb-xl { + padding-top: 50upx; + padding-bottom: 50upx; +} + +/* -- 浮动 -- */ + +.cf::after, +.cf::before { + content: " "; + display: table; +} + +.cf::after { + clear: both; +} + +.fl { + float: left; +} + +.fr { + float: right; +} + +/* ================== + 背景 + ==================== */ + +.line-red::after, +.lines-red::after { + border-color: #e54d42; +} + +.line-orange::after, +.lines-orange::after { + border-color: #f37b1d; +} + +.line-yellow::after, +.lines-yellow::after { + border-color: #fbbd08; +} + +.line-olive::after, +.lines-olive::after { + border-color: #8dc63f; +} + +.line-green::after, +.lines-green::after { + border-color: #39b54a; +} + +.line-cyan::after, +.lines-cyan::after { + border-color: #1cbbb4; +} + +.line-blue::after, +.lines-blue::after { + border-color: #0081ff; +} + +.line-purple::after, +.lines-purple::after { + border-color: #6739b6; +} + +.line-mauve::after, +.lines-mauve::after { + border-color: #9c26b0; +} + +.line-pink::after, +.lines-pink::after { + border-color: #e03997; +} + +.line-brown::after, +.lines-brown::after { + border-color: #a5673f; +} + +.line-grey::after, +.lines-grey::after { + border-color: #8799a3; +} + +.line-gray::after, +.lines-gray::after { + border-color: #aaaaaa; +} + +.line-black::after, +.lines-black::after { + border-color: #333333; +} + +.line-white::after, +.lines-white::after { + border-color: #ffffff; +} + +.bg-red { + background-color: #e54d42; + color: #ffffff; +} + +.bg-orange { + background-color: #f37b1d; + color: #ffffff; +} + +.bg-yellow { + background-color: #fbbd08; + color: #333333; +} + +.bg-olive { + background-color: #8dc63f; + color: #ffffff; +} + +.bg-green { + background-color: #39b54a; + color: #ffffff; +} + +.bg-cyan { + background-color: #1cbbb4; + color: #ffffff; +} + +.bg-blue { + background-color: #0081ff; + color: #ffffff; +} + +.bg-purple { + background-color: #6739b6; + color: #ffffff; +} + +.bg-mauve { + background-color: #9c26b0; + color: #ffffff; +} + +.bg-pink { + background-color: #e03997; + color: #ffffff; +} + +.bg-brown { + background-color: #a5673f; + color: #ffffff; +} + +.bg-grey { + background-color: #8799a3; + color: #ffffff; +} + +.bg-gray { + background-color: #f0f0f0; + color: #333333; +} + +.bg-black { + background-color: #333333; + color: #ffffff; +} + +.bg-white { + background-color: #ffffff; + color: #666666; +} + +.bg-shadeTop { + background-image: linear-gradient(rgba(0, 0, 0, 1), rgba(0, 0, 0, 0.01)); + color: #ffffff; +} + +.bg-shadeBottom { + background-image: linear-gradient(rgba(0, 0, 0, 0.01), rgba(0, 0, 0, 1)); + color: #ffffff; +} + +.bg-red.light { + color: #e54d42; + background-color: #fadbd9; +} + +.bg-orange.light { + color: #f37b1d; + background-color: #fde6d2; +} + +.bg-yellow.light { + color: #fbbd08; + background-color: #fef2ced2; +} + +.bg-olive.light { + color: #8dc63f; + background-color: #e8f4d9; +} + +.bg-green.light { + color: #39b54a; + background-color: #d7f0dbff; +} + +.bg-cyan.light { + color: #1cbbb4; + background-color: #d2f1f0; +} + +.bg-blue.light { + color: #0081ff; + background-color: #cce6ff; +} + +.bg-purple.light { + color: #6739b6; + background-color: #e1d7f0; +} + +.bg-mauve.light { + color: #9c26b0; + background-color: #ebd4ef; +} + +.bg-pink.light { + color: #e03997; + background-color: #f9d7ea; +} + +.bg-brown.light { + color: #a5673f; + background-color: #ede1d9; +} + +.bg-grey.light { + color: #8799a3; + background-color: #e7ebed; +} + +.bg-gradual-red { + background-image: linear-gradient(45deg, #f43f3b, #ec008c); + color: #ffffff; +} + +.bg-gradual-orange { + background-image: linear-gradient(45deg, #ff9700, #ed1c24); + color: #ffffff; +} + +.bg-gradual-green { + background-image: linear-gradient(45deg, #39b54a, #8dc63f); + color: #ffffff; +} + +.bg-gradual-purple { + background-image: linear-gradient(45deg, #9000ff, #5e00ff); + color: #ffffff; +} + +.bg-gradual-pink { + background-image: linear-gradient(45deg, #ec008c, #6739b6); + color: #ffffff; +} + +.bg-gradual-blue { + background-image: linear-gradient(45deg, #0081ff, #1cbbb4); + color: #ffffff; +} + +.shadow[class*="-red"] { + box-shadow: 6upx 6upx 8upx rgba(204, 69, 59, 0.2); +} + +.shadow[class*="-orange"] { + box-shadow: 6upx 6upx 8upx rgba(217, 109, 26, 0.2); +} + +.shadow[class*="-yellow"] { + box-shadow: 6upx 6upx 8upx rgba(224, 170, 7, 0.2); +} + +.shadow[class*="-olive"] { + box-shadow: 6upx 6upx 8upx rgba(124, 173, 55, 0.2); +} + +.shadow[class*="-green"] { + box-shadow: 6upx 6upx 8upx rgba(48, 156, 63, 0.2); +} + +.shadow[class*="-cyan"] { + box-shadow: 6upx 6upx 8upx rgba(28, 187, 180, 0.2); +} + +.shadow[class*="-blue"] { + box-shadow: 6upx 6upx 8upx rgba(0, 102, 204, 0.2); +} + +.shadow[class*="-purple"] { + box-shadow: 6upx 6upx 8upx rgba(88, 48, 156, 0.2); +} + +.shadow[class*="-mauve"] { + box-shadow: 6upx 6upx 8upx rgba(133, 33, 150, 0.2); +} + +.shadow[class*="-pink"] { + box-shadow: 6upx 6upx 8upx rgba(199, 50, 134, 0.2); +} + +.shadow[class*="-brown"] { + box-shadow: 6upx 6upx 8upx rgba(140, 88, 53, 0.2); +} + +.shadow[class*="-grey"] { + box-shadow: 6upx 6upx 8upx rgba(114, 130, 138, 0.2); +} + +.shadow[class*="-gray"] { + box-shadow: 6upx 6upx 8upx rgba(114, 130, 138, 0.2); +} + +.shadow[class*="-black"] { + box-shadow: 6upx 6upx 8upx rgba(26, 26, 26, 0.2); +} + +.shadow[class*="-white"] { + box-shadow: 6upx 6upx 8upx rgba(26, 26, 26, 0.2); +} + +.text-shadow[class*="-red"] { + text-shadow: 6upx 6upx 8upx rgba(204, 69, 59, 0.2); +} + +.text-shadow[class*="-orange"] { + text-shadow: 6upx 6upx 8upx rgba(217, 109, 26, 0.2); +} + +.text-shadow[class*="-yellow"] { + text-shadow: 6upx 6upx 8upx rgba(224, 170, 7, 0.2); +} + +.text-shadow[class*="-olive"] { + text-shadow: 6upx 6upx 8upx rgba(124, 173, 55, 0.2); +} + +.text-shadow[class*="-green"] { + text-shadow: 6upx 6upx 8upx rgba(48, 156, 63, 0.2); +} + +.text-shadow[class*="-cyan"] { + text-shadow: 6upx 6upx 8upx rgba(28, 187, 180, 0.2); +} + +.text-shadow[class*="-blue"] { + text-shadow: 6upx 6upx 8upx rgba(0, 102, 204, 0.2); +} + +.text-shadow[class*="-purple"] { + text-shadow: 6upx 6upx 8upx rgba(88, 48, 156, 0.2); +} + +.text-shadow[class*="-mauve"] { + text-shadow: 6upx 6upx 8upx rgba(133, 33, 150, 0.2); +} + +.text-shadow[class*="-pink"] { + text-shadow: 6upx 6upx 8upx rgba(199, 50, 134, 0.2); +} + +.text-shadow[class*="-brown"] { + text-shadow: 6upx 6upx 8upx rgba(140, 88, 53, 0.2); +} + +.text-shadow[class*="-grey"] { + text-shadow: 6upx 6upx 8upx rgba(114, 130, 138, 0.2); +} + +.text-shadow[class*="-gray"] { + text-shadow: 6upx 6upx 8upx rgba(114, 130, 138, 0.2); +} + +.text-shadow[class*="-black"] { + text-shadow: 6upx 6upx 8upx rgba(26, 26, 26, 0.2); +} + +.bg-img { + background-size: cover; + background-position: center; + background-repeat: no-repeat; +} + +.bg-mask { + background-color: #333333; + position: relative; +} + +.bg-mask::after { + content: ""; + border-radius: inherit; + width: 100%; + height: 100%; + display: block; + background-color: rgba(0, 0, 0, 0.4); + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; +} + +.bg-mask view, +.bg-mask cover-view { + z-index: 5; + position: relative; +} + +.bg-video { + position: relative; +} + +.bg-video video { + display: block; + height: 100%; + width: 100%; + -o-object-fit: cover; + object-fit: cover; + position: absolute; + top: 0; + z-index: 0; + pointer-events: none; +} + +/* ================== + 文本 + ==================== */ + +.text-xs { + font-size: 20upx; +} + +.text-sm { + font-size: 24upx; +} + +.text-df { + font-size: 28upx; +} + +.text-lg { + font-size: 32upx; +} + +.text-xl { + font-size: 36upx; +} + +.text-xxl { + font-size: 44upx; +} + +.text-sl { + font-size: 80upx; +} + +.text-xsl { + font-size: 120upx; +} + +.text-Abc { + text-transform: Capitalize; +} + +.text-ABC { + text-transform: Uppercase; +} + +.text-abc { + text-transform: Lowercase; +} + +.text-price::before { + content: "¥"; + font-size: 80%; + margin-right: 4upx; +} + +.text-cut { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.text-bold { + font-weight: bold; +} + +.text-center { + text-align: center; +} + +.text-content { + line-height: 1.6; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-red, +.line-red, +.lines-red { + color: #e54d42; +} + +.text-orange, +.line-orange, +.lines-orange { + color: #f37b1d; +} + +.text-yellow, +.line-yellow, +.lines-yellow { + color: #fbbd08; +} + +.text-olive, +.line-olive, +.lines-olive { + color: #8dc63f; +} + +.text-green, +.line-green, +.lines-green { + color: #39b54a; +} + +.text-cyan, +.line-cyan, +.lines-cyan { + color: #1cbbb4; +} + +.text-blue, +.line-blue, +.lines-blue { + color: #0081ff; +} + +.text-purple, +.line-purple, +.lines-purple { + color: #6739b6; +} + +.text-mauve, +.line-mauve, +.lines-mauve { + color: #9c26b0; +} + +.text-pink, +.line-pink, +.lines-pink { + color: #e03997; +} + +.text-brown, +.line-brown, +.lines-brown { + color: #a5673f; +} + +.text-grey, +.line-grey, +.lines-grey { + color: #8799a3; +} + +.text-gray, +.line-gray, +.lines-gray { + color: #aaaaaa; +} + +.text-black, +.line-black, +.lines-black { + color: #333333; +} + +.text-white, +.line-white, +.lines-white { + color: #ffffff; +} diff --git a/yunxi-ui-admin-uniapp/static/scss/global.scss b/yunxi-ui-admin-uniapp/static/scss/global.scss new file mode 100644 index 0000000..ac636bd --- /dev/null +++ b/yunxi-ui-admin-uniapp/static/scss/global.scss @@ -0,0 +1,90 @@ +.text-center { + text-align: center; +} + +.font-13 { + font-size: 13px; +} + +.font-12 { + font-size: 12px; +} + +.font-11 { + font-size: 11px; +} + +.text-grey1 { + color: #888; +} +.text-grey2 { + color: #aaa; +} + +.list-cell-arrow::before { + content: ' '; + height: 10px; + width: 10px; + border-width: 2px 2px 0 0; + border-color: #c0c0c0; + border-style: solid; + -webkit-transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0); + transform: matrix(0.5, 0.5, -0.5, 0.5, 0, 0); + position: absolute; + top: 50%; + margin-top: -6px; + right: 30rpx; + } + + .list-cell { + position: relative; + width: 100%; + box-sizing: border-box; + background-color: #fff; + color: #333; + padding: 26rpx 30rpx; + } + + .list-cell:first-child { + border-radius: 8rpx 8rpx 0 0; + } + + .list-cell:last-child { + border-radius: 0 0 8rpx 8rpx; + } + + .list-cell::after { + content: ''; + position: absolute; + border-bottom: 1px solid #eaeef1; + -webkit-transform: scaleY(0.5) translateZ(0); + transform: scaleY(0.5) translateZ(0); + transform-origin: 0 100%; + bottom: 0; + right: 0; + left: 0; + pointer-events: none; + } + + + .menu-list { + margin: 15px 15px; + + .menu-item-box { + width: 100%; + display: flex; + align-items: center; + + .menu-icon { + color: #007AFF; + font-size: 16px; + margin-right: 5px; + } + + .text-right { + margin-left: auto; + margin-right: 34rpx; + color: #999; + } + } + } diff --git a/yunxi-ui-admin-uniapp/static/scss/index.scss b/yunxi-ui-admin-uniapp/static/scss/index.scss new file mode 100644 index 0000000..745cffa --- /dev/null +++ b/yunxi-ui-admin-uniapp/static/scss/index.scss @@ -0,0 +1,6 @@ +// global +@import "./global.scss"; +// color-ui +@import "@/static/scss/colorui.css"; +// iconfont +@import "@/static/font/iconfont.css"; \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/store/getters.js b/yunxi-ui-admin-uniapp/store/getters.js new file mode 100644 index 0000000..8854794 --- /dev/null +++ b/yunxi-ui-admin-uniapp/store/getters.js @@ -0,0 +1,8 @@ +const getters = { + token: state => state.user.token, + avatar: state => state.user.avatar, + name: state => state.user.name, + roles: state => state.user.roles, + permissions: state => state.user.permissions +} +export default getters diff --git a/yunxi-ui-admin-uniapp/store/index.js b/yunxi-ui-admin-uniapp/store/index.js new file mode 100644 index 0000000..83a9db5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/store/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue' +import Vuex from 'vuex' +import user from '@/store/modules/user' +import getters from './getters' + +Vue.use(Vuex) + +const store = new Vuex.Store({ + modules: { + user + }, + getters +}) + +export default store diff --git a/yunxi-ui-admin-uniapp/store/modules/user.js b/yunxi-ui-admin-uniapp/store/modules/user.js new file mode 100644 index 0000000..7d03c1a --- /dev/null +++ b/yunxi-ui-admin-uniapp/store/modules/user.js @@ -0,0 +1,98 @@ +import config from '@/config' +import storage from '@/utils/storage' +import constant from '@/utils/constant' +import { login, logout, getInfo } from '@/api/login' +import { setToken, removeToken } from '@/utils/auth' + +const baseUrl = config.baseUrl + +const user = { + state: { + id: 0, // 用户编号 + name: storage.get(constant.name), + avatar: storage.get(constant.avatar), + roles: storage.get(constant.roles), + permissions: storage.get(constant.permissions) + }, + + mutations: { + SET_ID: (state, id) => { + state.id = id + }, + SET_NAME: (state, name) => { + state.name = name + storage.set(constant.name, name) + }, + SET_AVATAR: (state, avatar) => { + state.avatar = avatar + storage.set(constant.avatar, avatar) + }, + SET_ROLES: (state, roles) => { + state.roles = roles + storage.set(constant.roles, roles) + }, + SET_PERMISSIONS: (state, permissions) => { + state.permissions = permissions + storage.set(constant.permissions, permissions) + } + }, + + actions: { + // 登录 + Login({ commit }, userInfo) { + const username = userInfo.username.trim() + const password = userInfo.password + const captchaVerification = userInfo.captchaVerification + return new Promise((resolve, reject) => { + login(username, password, captchaVerification).then(res => { + res = res.data; + // 设置 token + setToken(res) + resolve() + }).catch(error => { + reject(error) + }) + }) + }, + + // 获取用户信息 + GetInfo({ commit, state }) { + return new Promise((resolve, reject) => { + getInfo().then(res => { + res = res.data; // 读取 data 数据 + const user = res.user + const avatar = (user == null || user.avatar === "" || user.avatar == null) ? require("@/static/images/profile.jpg") : user.avatar + const nickname = (user == null || user.nickname === "" || user.nickname == null) ? "" : user.nickname + if (res.roles && res.roles.length > 0) { + commit('SET_ROLES', res.roles) + commit('SET_PERMISSIONS', res.permissions) + } else { + commit('SET_ROLES', ['ROLE_DEFAULT']) + } + commit('SET_NAME', nickname) + commit('SET_AVATAR', avatar) + resolve(res) + }).catch(error => { + reject(error) + }) + }) + }, + + // 退出系统 + LogOut({ commit, state }) { + return new Promise((resolve, reject) => { + logout(state.token).then(() => { + commit('SET_ROLES', []) + commit('SET_PERMISSIONS', []) + removeToken() + storage.clean() + resolve() + }).catch(error => { + reject(error) + }) + }) + } + } +} + +export default user diff --git a/yunxi-ui-admin-uniapp/uni.scss b/yunxi-ui-admin-uniapp/uni.scss new file mode 100644 index 0000000..5b30ca3 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni.scss @@ -0,0 +1,64 @@ +/** + * uni-app内置的常用样式变量 + */ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color:#333;//基本色 +$uni-text-color-inverse:#fff;//反色 +$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable:#c0c0c0; + +/* 背景颜色 */ +$uni-bg-color:#ffffff; +$uni-bg-color-grey:#f8f8f8; +$uni-bg-color-hover:#f1f1f1;//点击状态颜色 +$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色 + +/* 边框颜色 */ +$uni-border-color:#e5e5e5; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm:12px; +$uni-font-size-base:14px; +$uni-font-size-lg:16px; + +/* 图片尺寸 */ +$uni-img-size-sm:20px; +$uni-img-size-base:26px; +$uni-img-size-lg:40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2C405A; // 文章标题颜色 +$uni-font-size-title:20px; +$uni-color-subtitle: #555555; // 二级标题颜色 +$uni-font-size-subtitle:26px; +$uni-color-paragraph: #3F536E; // 文章段落颜色 +$uni-font-size-paragraph:15px; \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-badge/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-badge/changelog.md new file mode 100644 index 0000000..544ecc1 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-badge/changelog.md @@ -0,0 +1,29 @@ +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-badge](https://uniapp.dcloud.io/component/uniui/uni-badge) +## 1.1.7(2021-11-08) +- 优化 升级ui +- 修改 size 属性默认值调整为 small +- 修改 type 属性,默认值调整为 error,info 替换 default +## 1.1.6(2021-09-22) +- 修复 在字节小程序上样式不生效的 bug +## 1.1.5(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.4(2021-07-29) +- 修复 去掉 nvue 不支持css 的 align-self 属性,nvue 下不暂支持 absolute 属性 +## 1.1.3(2021-06-24) +- 优化 示例项目 +## 1.1.1(2021-05-12) +- 新增 组件示例地址 +## 1.1.0(2021-05-12) +- 新增 uni-badge 的 absolute 属性,支持定位 +- 新增 uni-badge 的 offset 属性,支持定位偏移 +- 新增 uni-badge 的 is-dot 属性,支持仅显示有一个小点 +- 新增 uni-badge 的 max-num 属性,支持自定义封顶的数字值,超过 99 显示99+ +- 优化 uni-badge 属性 custom-style, 支持以对象形式自定义样式 +## 1.0.7(2021-05-07) +- 修复 uni-badge 在 App 端,数字小于10时不是圆形的bug +- 修复 uni-badge 在父元素不是 flex 布局时,宽度缩小的bug +- 新增 uni-badge 属性 custom-style, 支持自定义样式 +## 1.0.6(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-badge/components/uni-badge/uni-badge.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-badge/components/uni-badge/uni-badge.vue new file mode 100644 index 0000000..fcbfe93 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-badge/components/uni-badge/uni-badge.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-badge/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-badge/package.json new file mode 100644 index 0000000..4e9e631 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-badge/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-badge", + "displayName": "uni-badge 数字角标", + "version": "1.2.0", + "description": "数字角标(徽章)组件,在元素周围展示消息提醒,一般用于列表、九宫格、按钮等地方。", + "keywords": [ + "", + "badge", + "uni-ui", + "uniui", + "数字角标", + "徽章" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-badge/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-badge/readme.md new file mode 100644 index 0000000..bdf175d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-badge/readme.md @@ -0,0 +1,10 @@ +## Badge 数字角标 +> **组件名:uni-badge** +> 代码块: `uBadge` + +数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景, + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-badge) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/changelog.md new file mode 100644 index 0000000..016e6ce --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/changelog.md @@ -0,0 +1,6 @@ +## 0.1.2(2022-06-08) +- 修复 微信小程序 separator 不显示问题 +## 0.1.1(2022-06-02) +- 新增 支持 uni.scss 修改颜色 +## 0.1.0(2022-04-21) +- 初始化 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/components/uni-breadcrumb-item/uni-breadcrumb-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/components/uni-breadcrumb-item/uni-breadcrumb-item.vue new file mode 100644 index 0000000..b9edbd6 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/components/uni-breadcrumb-item/uni-breadcrumb-item.vue @@ -0,0 +1,121 @@ + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/components/uni-breadcrumb/uni-breadcrumb.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/components/uni-breadcrumb/uni-breadcrumb.vue new file mode 100644 index 0000000..94493a2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/components/uni-breadcrumb/uni-breadcrumb.vue @@ -0,0 +1,41 @@ + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/package.json new file mode 100644 index 0000000..0a04e50 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-breadcrumb", + "displayName": "uni-breadcrumb 面包屑", + "version": "0.1.2", + "description": "Breadcrumb 面包屑", + "keywords": [ + "uni-breadcrumb", + "breadcrumb", + "uni-ui", + "面包屑导航", + "面包屑" +], + "repository": "", + "engines": { + "HBuilderX": "^3.1.0" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "y", + "vue3": "y" + }, + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/readme.md new file mode 100644 index 0000000..6976b8d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-breadcrumb/readme.md @@ -0,0 +1,66 @@ + +## breadcrumb 面包屑导航 +> **组件名:uni-breadcrumb** +> 代码块: `ubreadcrumb` + +显示当前页面的路径,快速返回之前的任意页面。 + +### 安装方式 + +本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。 + +如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55) + +### 基本用法 + +在 ``template`` 中使用组件 + +```html + + {{route.name}} + +``` + +```js +export default { + name: "uni-stat-breadcrumb", + data() { + return { + routes: [{ + to: '/A', + name: 'A页面' + }, { + to: '/B', + name: 'B页面' + }, { + to: '/C', + name: 'C页面' + }] + }; + } + } +``` + + +## API + +### Breadcrumb Props + +|属性名 |类型 |默认值 |说明 | +|:-: |:-: |:-: |:-: | +|separator |String |斜杠'/' |分隔符 | +|separatorClass |String | |图标分隔符 class | + +### Breadcrumb Item Props + +|属性名 |类型 |默认值 |说明 | +|:-: |:-: |:-: |:-: | +|to |String | |路由跳转页面路径 | +|replace|Boolean | |在使用 to 进行路由跳转时,启用 replace 将不会向 history 添加新记录(仅 h5 支持) | + + + + +## 组件示例 + +点击查看:[https://hellouniapp.dcloud.net.cn/pages/extUI/breadcrumb/breadcrumb](https://hellouniapp.dcloud.net.cn/pages/extUI/breadcrumb/breadcrumb) \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/changelog.md new file mode 100644 index 0000000..6df4493 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/changelog.md @@ -0,0 +1,16 @@ +## 1.4.5(2022-02-25) +- 修复 条件编译 nvue 不支持的 css 样式 +## 1.4.4(2022-02-25) +- 修复 条件编译 nvue 不支持的 css 样式 +## 1.4.3(2021-09-22) +- 修复 startDate、 endDate 属性失效的 bug +## 1.4.2(2021-08-24) +- 新增 支持国际化 +## 1.4.1(2021-08-05) +- 修复 弹出层被 tabbar 遮盖 bug +## 1.4.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.3.16(2021-05-12) +- 新增 组件示例地址 +## 1.3.15(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/calendar.js b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/calendar.js new file mode 100644 index 0000000..b8d7d6f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/calendar.js @@ -0,0 +1,546 @@ +/** +* @1900-2100区间内的公历、农历互转 +* @charset UTF-8 +* @github https://github.com/jjonline/calendar.js +* @Author Jea杨(JJonline@JJonline.Cn) +* @Time 2014-7-21 +* @Time 2016-8-13 Fixed 2033hex、Attribution Annals +* @Time 2016-9-25 Fixed lunar LeapMonth Param Bug +* @Time 2017-7-24 Fixed use getTerm Func Param Error.use solar year,NOT lunar year +* @Version 1.0.3 +* @公历转农历:calendar.solar2lunar(1987,11,01); //[you can ignore params of prefix 0] +* @农历转公历:calendar.lunar2solar(1987,09,10); //[you can ignore params of prefix 0] +*/ +/* eslint-disable */ +var calendar = { + + /** + * 农历1900-2100的润大小信息表 + * @Array Of Property + * @return Hex + */ + lunarInfo: [0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, 0x056a0, 0x09ad0, 0x055d2, // 1900-1909 + 0x04ae0, 0x0a5b6, 0x0a4d0, 0x0d250, 0x1d255, 0x0b540, 0x0d6a0, 0x0ada2, 0x095b0, 0x14977, // 1910-1919 + 0x04970, 0x0a4b0, 0x0b4b5, 0x06a50, 0x06d40, 0x1ab54, 0x02b60, 0x09570, 0x052f2, 0x04970, // 1920-1929 + 0x06566, 0x0d4a0, 0x0ea50, 0x06e95, 0x05ad0, 0x02b60, 0x186e3, 0x092e0, 0x1c8d7, 0x0c950, // 1930-1939 + 0x0d4a0, 0x1d8a6, 0x0b550, 0x056a0, 0x1a5b4, 0x025d0, 0x092d0, 0x0d2b2, 0x0a950, 0x0b557, // 1940-1949 + 0x06ca0, 0x0b550, 0x15355, 0x04da0, 0x0a5b0, 0x14573, 0x052b0, 0x0a9a8, 0x0e950, 0x06aa0, // 1950-1959 + 0x0aea6, 0x0ab50, 0x04b60, 0x0aae4, 0x0a570, 0x05260, 0x0f263, 0x0d950, 0x05b57, 0x056a0, // 1960-1969 + 0x096d0, 0x04dd5, 0x04ad0, 0x0a4d0, 0x0d4d4, 0x0d250, 0x0d558, 0x0b540, 0x0b6a0, 0x195a6, // 1970-1979 + 0x095b0, 0x049b0, 0x0a974, 0x0a4b0, 0x0b27a, 0x06a50, 0x06d40, 0x0af46, 0x0ab60, 0x09570, // 1980-1989 + 0x04af5, 0x04970, 0x064b0, 0x074a3, 0x0ea50, 0x06b58, 0x05ac0, 0x0ab60, 0x096d5, 0x092e0, // 1990-1999 + 0x0c960, 0x0d954, 0x0d4a0, 0x0da50, 0x07552, 0x056a0, 0x0abb7, 0x025d0, 0x092d0, 0x0cab5, // 2000-2009 + 0x0a950, 0x0b4a0, 0x0baa4, 0x0ad50, 0x055d9, 0x04ba0, 0x0a5b0, 0x15176, 0x052b0, 0x0a930, // 2010-2019 + 0x07954, 0x06aa0, 0x0ad50, 0x05b52, 0x04b60, 0x0a6e6, 0x0a4e0, 0x0d260, 0x0ea65, 0x0d530, // 2020-2029 + 0x05aa0, 0x076a3, 0x096d0, 0x04afb, 0x04ad0, 0x0a4d0, 0x1d0b6, 0x0d250, 0x0d520, 0x0dd45, // 2030-2039 + 0x0b5a0, 0x056d0, 0x055b2, 0x049b0, 0x0a577, 0x0a4b0, 0x0aa50, 0x1b255, 0x06d20, 0x0ada0, // 2040-2049 + /** Add By JJonline@JJonline.Cn**/ + 0x14b63, 0x09370, 0x049f8, 0x04970, 0x064b0, 0x168a6, 0x0ea50, 0x06b20, 0x1a6c4, 0x0aae0, // 2050-2059 + 0x0a2e0, 0x0d2e3, 0x0c960, 0x0d557, 0x0d4a0, 0x0da50, 0x05d55, 0x056a0, 0x0a6d0, 0x055d4, // 2060-2069 + 0x052d0, 0x0a9b8, 0x0a950, 0x0b4a0, 0x0b6a6, 0x0ad50, 0x055a0, 0x0aba4, 0x0a5b0, 0x052b0, // 2070-2079 + 0x0b273, 0x06930, 0x07337, 0x06aa0, 0x0ad50, 0x14b55, 0x04b60, 0x0a570, 0x054e4, 0x0d160, // 2080-2089 + 0x0e968, 0x0d520, 0x0daa0, 0x16aa6, 0x056d0, 0x04ae0, 0x0a9d4, 0x0a2d0, 0x0d150, 0x0f252, // 2090-2099 + 0x0d520], // 2100 + + /** + * 公历每个月份的天数普通表 + * @Array Of Property + * @return Number + */ + solarMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], + + /** + * 天干地支之天干速查表 + * @Array Of Property trans["甲","乙","丙","丁","戊","己","庚","辛","壬","癸"] + * @return Cn string + */ + Gan: ['\u7532', '\u4e59', '\u4e19', '\u4e01', '\u620a', '\u5df1', '\u5e9a', '\u8f9b', '\u58ec', '\u7678'], + + /** + * 天干地支之地支速查表 + * @Array Of Property + * @trans["子","丑","寅","卯","辰","巳","午","未","申","酉","戌","亥"] + * @return Cn string + */ + Zhi: ['\u5b50', '\u4e11', '\u5bc5', '\u536f', '\u8fb0', '\u5df3', '\u5348', '\u672a', '\u7533', '\u9149', '\u620c', '\u4ea5'], + + /** + * 天干地支之地支速查表<=>生肖 + * @Array Of Property + * @trans["鼠","牛","虎","兔","龙","蛇","马","羊","猴","鸡","狗","猪"] + * @return Cn string + */ + Animals: ['\u9f20', '\u725b', '\u864e', '\u5154', '\u9f99', '\u86c7', '\u9a6c', '\u7f8a', '\u7334', '\u9e21', '\u72d7', '\u732a'], + + /** + * 24节气速查表 + * @Array Of Property + * @trans["小寒","大寒","立春","雨水","惊蛰","春分","清明","谷雨","立夏","小满","芒种","夏至","小暑","大暑","立秋","处暑","白露","秋分","寒露","霜降","立冬","小雪","大雪","冬至"] + * @return Cn string + */ + solarTerm: ['\u5c0f\u5bd2', '\u5927\u5bd2', '\u7acb\u6625', '\u96e8\u6c34', '\u60ca\u86f0', '\u6625\u5206', '\u6e05\u660e', '\u8c37\u96e8', '\u7acb\u590f', '\u5c0f\u6ee1', '\u8292\u79cd', '\u590f\u81f3', '\u5c0f\u6691', '\u5927\u6691', '\u7acb\u79cb', '\u5904\u6691', '\u767d\u9732', '\u79cb\u5206', '\u5bd2\u9732', '\u971c\u964d', '\u7acb\u51ac', '\u5c0f\u96ea', '\u5927\u96ea', '\u51ac\u81f3'], + + /** + * 1900-2100各年的24节气日期速查表 + * @Array Of Property + * @return 0x string For splice + */ + sTermInfo: ['9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', + '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', + '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', 'b027097bd097c36b0b6fc9274c91aa', + '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd0b06bdb0722c965ce1cfcc920f', + 'b027097bd097c36b0b6fc9274c91aa', '9778397bd19801ec9210c965cc920e', '97b6b97bd19801ec95f8c965cc920f', + '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd197c36c9210c9274c91aa', + '97b6b97bd19801ec95f8c965cc920e', '97bd09801d98082c95f8e1cfcc920f', '97bd097bd097c36b0b6fc9210c8dc2', + '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec95f8c965cc920e', '97bcf97c3598082c95f8e1cfcc920f', + '97bd097bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', + '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', + '97b6b97bd19801ec9210c965cc920e', '97bcf97c3598082c95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', + '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', + '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', + '97bcf97c359801ec95f8c965cc920f', '97bd097bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', + '97b6b97bd19801ec9210c965cc920e', '97bcf97c359801ec95f8c965cc920f', '97bd097bd07f595b0b6fc920fb0722', + '9778397bd097c36b0b6fc9210c8dc2', '9778397bd19801ec9210c9274c920e', '97b6b97bd19801ec95f8c965cc920f', + '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e', + '97b6b97bd19801ec95f8c965cc920f', '97bd07f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', + '9778397bd097c36c9210c9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bd07f1487f595b0b0bc920fb0722', + '7f0e397bd097c36b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', + '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', + '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', + '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', '97bcf7f1487f531b0b0bb0b6fb0722', + '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b97bd19801ec9210c965cc920e', + '97bcf7f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', + '97b6b97bd19801ec9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', + '9778397bd097c36b0b6fc9210c91aa', '97b6b97bd197c36c9210c9274c920e', '97bcf7f0e47f531b0b0bb0b6fb0722', + '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '9778397bd097c36c9210c9274c920e', + '97b6b7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c36b0b6fc9210c8dc2', + '9778397bd097c36b0b70c9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722', + '7f0e397bd097c35b0b6fc9210c8dc2', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', + '7f0e27f1487f595b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', + '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', + '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', + '7f0e397bd097c35b0b6fc920fb0722', '9778397bd097c36b0b6fc9274c91aa', '97b6b7f0e47f531b0723b0b6fb0721', + '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9274c91aa', + '97b6b7f0e47f531b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', + '9778397bd097c36b0b6fc9210c91aa', '97b6b7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', + '7f0e397bd07f595b0b0bc920fb0722', '9778397bd097c36b0b6fc9210c8dc2', '977837f0e37f149b0723b0787b0721', + '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f5307f595b0b0bc920fb0722', '7f0e397bd097c35b0b6fc9210c8dc2', + '977837f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e37f1487f595b0b0bb0b6fb0722', + '7f0e397bd097c35b0b6fc9210c8dc2', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', + '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', '977837f0e37f14998082b0787b06bd', + '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd097c35b0b6fc920fb0722', + '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', + '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', + '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14998082b0787b06bd', + '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0b0bb0b6fb0722', '7f0e397bd07f595b0b0bc920fb0722', + '977837f0e37f14998082b0723b06bd', '7f07e7f0e37f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', + '7f0e397bd07f595b0b0bc920fb0722', '977837f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b0721', + '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f595b0b0bb0b6fb0722', '7f0e37f0e37f14898082b0723b02d5', + '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e37f1487f531b0b0bb0b6fb0722', + '7f0e37f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', + '7f0e37f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd', + '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e37f14898082b072297c35', + '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722', + '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f149b0723b0787b0721', + '7f0e27f1487f531b0b0bb0b6fb0722', '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14998082b0723b06bd', + '7f07e7f0e47f149b0723b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', '7f0e37f0e366aa89801eb072297c35', + '7ec967f0e37f14998082b0723b06bd', '7f07e7f0e37f14998083b0787b0721', '7f0e27f0e47f531b0723b0b6fb0722', + '7f0e37f0e366aa89801eb072297c35', '7ec967f0e37f14898082b0723b02d5', '7f07e7f0e37f14998082b0787b0721', + '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66aa89801e9808297c35', '665f67f0e37f14898082b0723b02d5', + '7ec967f0e37f14998082b0787b0721', '7f07e7f0e47f531b0723b0b6fb0722', '7f0e36665b66a449801e9808297c35', + '665f67f0e37f14898082b0723b02d5', '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', + '7f0e36665b66a449801e9808297c35', '665f67f0e37f14898082b072297c35', '7ec967f0e37f14998082b0787b06bd', + '7f07e7f0e47f531b0723b0b6fb0721', '7f0e26665b66a449801e9808297c35', '665f67f0e37f1489801eb072297c35', + '7ec967f0e37f14998082b0787b06bd', '7f07e7f0e47f531b0723b0b6fb0721', '7f0e27f1487f531b0b0bb0b6fb0722'], + + /** + * 数字转中文速查表 + * @Array Of Property + * @trans ['日','一','二','三','四','五','六','七','八','九','十'] + * @return Cn string + */ + nStr1: ['\u65e5', '\u4e00', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d', '\u4e03', '\u516b', '\u4e5d', '\u5341'], + + /** + * 日期转农历称呼速查表 + * @Array Of Property + * @trans ['初','十','廿','卅'] + * @return Cn string + */ + nStr2: ['\u521d', '\u5341', '\u5eff', '\u5345'], + + /** + * 月份转农历称呼速查表 + * @Array Of Property + * @trans ['正','一','二','三','四','五','六','七','八','九','十','冬','腊'] + * @return Cn string + */ + nStr3: ['\u6b63', '\u4e8c', '\u4e09', '\u56db', '\u4e94', '\u516d', '\u4e03', '\u516b', '\u4e5d', '\u5341', '\u51ac', '\u814a'], + + /** + * 返回农历y年一整年的总天数 + * @param lunar Year + * @return Number + * @eg:var count = calendar.lYearDays(1987) ;//count=387 + */ + lYearDays: function (y) { + var i; var sum = 348 + for (i = 0x8000; i > 0x8; i >>= 1) { sum += (this.lunarInfo[y - 1900] & i) ? 1 : 0 } + return (sum + this.leapDays(y)) + }, + + /** + * 返回农历y年闰月是哪个月;若y年没有闰月 则返回0 + * @param lunar Year + * @return Number (0-12) + * @eg:var leapMonth = calendar.leapMonth(1987) ;//leapMonth=6 + */ + leapMonth: function (y) { // 闰字编码 \u95f0 + return (this.lunarInfo[y - 1900] & 0xf) + }, + + /** + * 返回农历y年闰月的天数 若该年没有闰月则返回0 + * @param lunar Year + * @return Number (0、29、30) + * @eg:var leapMonthDay = calendar.leapDays(1987) ;//leapMonthDay=29 + */ + leapDays: function (y) { + if (this.leapMonth(y)) { + return ((this.lunarInfo[y - 1900] & 0x10000) ? 30 : 29) + } + return (0) + }, + + /** + * 返回农历y年m月(非闰月)的总天数,计算m为闰月时的天数请使用leapDays方法 + * @param lunar Year + * @return Number (-1、29、30) + * @eg:var MonthDay = calendar.monthDays(1987,9) ;//MonthDay=29 + */ + monthDays: function (y, m) { + if (m > 12 || m < 1) { return -1 }// 月份参数从1至12,参数错误返回-1 + return ((this.lunarInfo[y - 1900] & (0x10000 >> m)) ? 30 : 29) + }, + + /** + * 返回公历(!)y年m月的天数 + * @param solar Year + * @return Number (-1、28、29、30、31) + * @eg:var solarMonthDay = calendar.leapDays(1987) ;//solarMonthDay=30 + */ + solarDays: function (y, m) { + if (m > 12 || m < 1) { return -1 } // 若参数错误 返回-1 + var ms = m - 1 + if (ms == 1) { // 2月份的闰平规律测算后确认返回28或29 + return (((y % 4 == 0) && (y % 100 != 0) || (y % 400 == 0)) ? 29 : 28) + } else { + return (this.solarMonth[ms]) + } + }, + + /** + * 农历年份转换为干支纪年 + * @param lYear 农历年的年份数 + * @return Cn string + */ + toGanZhiYear: function (lYear) { + var ganKey = (lYear - 3) % 10 + var zhiKey = (lYear - 3) % 12 + if (ganKey == 0) ganKey = 10// 如果余数为0则为最后一个天干 + if (zhiKey == 0) zhiKey = 12// 如果余数为0则为最后一个地支 + return this.Gan[ganKey - 1] + this.Zhi[zhiKey - 1] + }, + + /** + * 公历月、日判断所属星座 + * @param cMonth [description] + * @param cDay [description] + * @return Cn string + */ + toAstro: function (cMonth, cDay) { + var s = '\u9b54\u7faf\u6c34\u74f6\u53cc\u9c7c\u767d\u7f8a\u91d1\u725b\u53cc\u5b50\u5de8\u87f9\u72ee\u5b50\u5904\u5973\u5929\u79e4\u5929\u874e\u5c04\u624b\u9b54\u7faf' + var arr = [20, 19, 21, 21, 21, 22, 23, 23, 23, 23, 22, 22] + return s.substr(cMonth * 2 - (cDay < arr[cMonth - 1] ? 2 : 0), 2) + '\u5ea7'// 座 + }, + + /** + * 传入offset偏移量返回干支 + * @param offset 相对甲子的偏移量 + * @return Cn string + */ + toGanZhi: function (offset) { + return this.Gan[offset % 10] + this.Zhi[offset % 12] + }, + + /** + * 传入公历(!)y年获得该年第n个节气的公历日期 + * @param y公历年(1900-2100);n二十四节气中的第几个节气(1~24);从n=1(小寒)算起 + * @return day Number + * @eg:var _24 = calendar.getTerm(1987,3) ;//_24=4;意即1987年2月4日立春 + */ + getTerm: function (y, n) { + if (y < 1900 || y > 2100) { return -1 } + if (n < 1 || n > 24) { return -1 } + var _table = this.sTermInfo[y - 1900] + var _info = [ + parseInt('0x' + _table.substr(0, 5)).toString(), + parseInt('0x' + _table.substr(5, 5)).toString(), + parseInt('0x' + _table.substr(10, 5)).toString(), + parseInt('0x' + _table.substr(15, 5)).toString(), + parseInt('0x' + _table.substr(20, 5)).toString(), + parseInt('0x' + _table.substr(25, 5)).toString() + ] + var _calday = [ + _info[0].substr(0, 1), + _info[0].substr(1, 2), + _info[0].substr(3, 1), + _info[0].substr(4, 2), + + _info[1].substr(0, 1), + _info[1].substr(1, 2), + _info[1].substr(3, 1), + _info[1].substr(4, 2), + + _info[2].substr(0, 1), + _info[2].substr(1, 2), + _info[2].substr(3, 1), + _info[2].substr(4, 2), + + _info[3].substr(0, 1), + _info[3].substr(1, 2), + _info[3].substr(3, 1), + _info[3].substr(4, 2), + + _info[4].substr(0, 1), + _info[4].substr(1, 2), + _info[4].substr(3, 1), + _info[4].substr(4, 2), + + _info[5].substr(0, 1), + _info[5].substr(1, 2), + _info[5].substr(3, 1), + _info[5].substr(4, 2) + ] + return parseInt(_calday[n - 1]) + }, + + /** + * 传入农历数字月份返回汉语通俗表示法 + * @param lunar month + * @return Cn string + * @eg:var cnMonth = calendar.toChinaMonth(12) ;//cnMonth='腊月' + */ + toChinaMonth: function (m) { // 月 => \u6708 + if (m > 12 || m < 1) { return -1 } // 若参数错误 返回-1 + var s = this.nStr3[m - 1] + s += '\u6708'// 加上月字 + return s + }, + + /** + * 传入农历日期数字返回汉字表示法 + * @param lunar day + * @return Cn string + * @eg:var cnDay = calendar.toChinaDay(21) ;//cnMonth='廿一' + */ + toChinaDay: function (d) { // 日 => \u65e5 + var s + switch (d) { + case 10: + s = '\u521d\u5341'; break + case 20: + s = '\u4e8c\u5341'; break + break + case 30: + s = '\u4e09\u5341'; break + break + default : + s = this.nStr2[Math.floor(d / 10)] + s += this.nStr1[d % 10] + } + return (s) + }, + + /** + * 年份转生肖[!仅能大致转换] => 精确划分生肖分界线是“立春” + * @param y year + * @return Cn string + * @eg:var animal = calendar.getAnimal(1987) ;//animal='兔' + */ + getAnimal: function (y) { + return this.Animals[(y - 4) % 12] + }, + + /** + * 传入阳历年月日获得详细的公历、农历object信息 <=>JSON + * @param y solar year + * @param m solar month + * @param d solar day + * @return JSON object + * @eg:console.log(calendar.solar2lunar(1987,11,01)); + */ + solar2lunar: function (y, m, d) { // 参数区间1900.1.31~2100.12.31 + // 年份限定、上限 + if (y < 1900 || y > 2100) { + return -1// undefined转换为数字变为NaN + } + // 公历传参最下限 + if (y == 1900 && m == 1 && d < 31) { + return -1 + } + // 未传参 获得当天 + if (!y) { + var objDate = new Date() + } else { + var objDate = new Date(y, parseInt(m) - 1, d) + } + var i; var leap = 0; var temp = 0 + // 修正ymd参数 + var y = objDate.getFullYear() + var m = objDate.getMonth() + 1 + var d = objDate.getDate() + var offset = (Date.UTC(objDate.getFullYear(), objDate.getMonth(), objDate.getDate()) - Date.UTC(1900, 0, 31)) / 86400000 + for (i = 1900; i < 2101 && offset > 0; i++) { + temp = this.lYearDays(i) + offset -= temp + } + if (offset < 0) { + offset += temp; i-- + } + + // 是否今天 + var isTodayObj = new Date() + var isToday = false + if (isTodayObj.getFullYear() == y && isTodayObj.getMonth() + 1 == m && isTodayObj.getDate() == d) { + isToday = true + } + // 星期几 + var nWeek = objDate.getDay() + var cWeek = this.nStr1[nWeek] + // 数字表示周几顺应天朝周一开始的惯例 + if (nWeek == 0) { + nWeek = 7 + } + // 农历年 + var year = i + var leap = this.leapMonth(i) // 闰哪个月 + var isLeap = false + + // 效验闰月 + for (i = 1; i < 13 && offset > 0; i++) { + // 闰月 + if (leap > 0 && i == (leap + 1) && isLeap == false) { + --i + isLeap = true; temp = this.leapDays(year) // 计算农历闰月天数 + } else { + temp = this.monthDays(year, i)// 计算农历普通月天数 + } + // 解除闰月 + if (isLeap == true && i == (leap + 1)) { isLeap = false } + offset -= temp + } + // 闰月导致数组下标重叠取反 + if (offset == 0 && leap > 0 && i == leap + 1) { + if (isLeap) { + isLeap = false + } else { + isLeap = true; --i + } + } + if (offset < 0) { + offset += temp; --i + } + // 农历月 + var month = i + // 农历日 + var day = offset + 1 + // 天干地支处理 + var sm = m - 1 + var gzY = this.toGanZhiYear(year) + + // 当月的两个节气 + // bugfix-2017-7-24 11:03:38 use lunar Year Param `y` Not `year` + var firstNode = this.getTerm(y, (m * 2 - 1))// 返回当月「节」为几日开始 + var secondNode = this.getTerm(y, (m * 2))// 返回当月「节」为几日开始 + + // 依据12节气修正干支月 + var gzM = this.toGanZhi((y - 1900) * 12 + m + 11) + if (d >= firstNode) { + gzM = this.toGanZhi((y - 1900) * 12 + m + 12) + } + + // 传入的日期的节气与否 + var isTerm = false + var Term = null + if (firstNode == d) { + isTerm = true + Term = this.solarTerm[m * 2 - 2] + } + if (secondNode == d) { + isTerm = true + Term = this.solarTerm[m * 2 - 1] + } + // 日柱 当月一日与 1900/1/1 相差天数 + var dayCyclical = Date.UTC(y, sm, 1, 0, 0, 0, 0) / 86400000 + 25567 + 10 + var gzD = this.toGanZhi(dayCyclical + d - 1) + // 该日期所属的星座 + var astro = this.toAstro(m, d) + + return { 'lYear': year, 'lMonth': month, 'lDay': day, 'Animal': this.getAnimal(year), 'IMonthCn': (isLeap ? '\u95f0' : '') + this.toChinaMonth(month), 'IDayCn': this.toChinaDay(day), 'cYear': y, 'cMonth': m, 'cDay': d, 'gzYear': gzY, 'gzMonth': gzM, 'gzDay': gzD, 'isToday': isToday, 'isLeap': isLeap, 'nWeek': nWeek, 'ncWeek': '\u661f\u671f' + cWeek, 'isTerm': isTerm, 'Term': Term, 'astro': astro } + }, + + /** + * 传入农历年月日以及传入的月份是否闰月获得详细的公历、农历object信息 <=>JSON + * @param y lunar year + * @param m lunar month + * @param d lunar day + * @param isLeapMonth lunar month is leap or not.[如果是农历闰月第四个参数赋值true即可] + * @return JSON object + * @eg:console.log(calendar.lunar2solar(1987,9,10)); + */ + lunar2solar: function (y, m, d, isLeapMonth) { // 参数区间1900.1.31~2100.12.1 + var isLeapMonth = !!isLeapMonth + var leapOffset = 0 + var leapMonth = this.leapMonth(y) + var leapDay = this.leapDays(y) + if (isLeapMonth && (leapMonth != m)) { return -1 }// 传参要求计算该闰月公历 但该年得出的闰月与传参的月份并不同 + if (y == 2100 && m == 12 && d > 1 || y == 1900 && m == 1 && d < 31) { return -1 }// 超出了最大极限值 + var day = this.monthDays(y, m) + var _day = day + // bugFix 2016-9-25 + // if month is leap, _day use leapDays method + if (isLeapMonth) { + _day = this.leapDays(y, m) + } + if (y < 1900 || y > 2100 || d > _day) { return -1 }// 参数合法性效验 + + // 计算农历的时间差 + var offset = 0 + for (var i = 1900; i < y; i++) { + offset += this.lYearDays(i) + } + var leap = 0; var isAdd = false + for (var i = 1; i < m; i++) { + leap = this.leapMonth(y) + if (!isAdd) { // 处理闰月 + if (leap <= i && leap > 0) { + offset += this.leapDays(y); isAdd = true + } + } + offset += this.monthDays(y, i) + } + // 转换闰月农历 需补充该年闰月的前一个月的时差 + if (isLeapMonth) { offset += day } + // 1900年农历正月一日的公历时间为1900年1月30日0时0分0秒(该时间也是本农历的最开始起始点) + var stmap = Date.UTC(1900, 1, 30, 0, 0, 0) + var calObj = new Date((offset + d - 31) * 86400000 + stmap) + var cY = calObj.getUTCFullYear() + var cM = calObj.getUTCMonth() + 1 + var cD = calObj.getUTCDate() + + return this.solar2lunar(cY, cM, cD) + } +} + +export default calendar diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/en.json new file mode 100644 index 0000000..fcbd13c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/en.json @@ -0,0 +1,12 @@ +{ + "uni-calender.ok": "ok", + "uni-calender.cancel": "cancel", + "uni-calender.today": "today", + "uni-calender.MON": "MON", + "uni-calender.TUE": "TUE", + "uni-calender.WED": "WED", + "uni-calender.THU": "THU", + "uni-calender.FRI": "FRI", + "uni-calender.SAT": "SAT", + "uni-calender.SUN": "SUN" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/zh-Hans.json new file mode 100644 index 0000000..1ca43de --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "uni-calender.ok": "确定", + "uni-calender.cancel": "取消", + "uni-calender.today": "今日", + "uni-calender.SUN": "日", + "uni-calender.MON": "一", + "uni-calender.TUE": "二", + "uni-calender.WED": "三", + "uni-calender.THU": "四", + "uni-calender.FRI": "五", + "uni-calender.SAT": "六" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/zh-Hant.json new file mode 100644 index 0000000..e0fe33b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/i18n/zh-Hant.json @@ -0,0 +1,12 @@ +{ + "uni-calender.ok": "確定", + "uni-calender.cancel": "取消", + "uni-calender.today": "今日", + "uni-calender.SUN": "日", + "uni-calender.MON": "一", + "uni-calender.TUE": "二", + "uni-calender.WED": "三", + "uni-calender.THU": "四", + "uni-calender.FRI": "五", + "uni-calender.SAT": "六" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/uni-calendar-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/uni-calendar-item.vue new file mode 100644 index 0000000..30bd6c8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/uni-calendar-item.vue @@ -0,0 +1,188 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue new file mode 100644 index 0000000..88381db --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/uni-calendar.vue @@ -0,0 +1,562 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/util.js b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/util.js new file mode 100644 index 0000000..2d6100b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/components/uni-calendar/util.js @@ -0,0 +1,350 @@ +import CALENDAR from './calendar.js' + +class Calendar { + constructor({ + date, + selected, + startDate, + endDate, + range + } = {}) { + // 当前日期 + this.date = this.getDate(new Date()) // 当前初入日期 + // 打点信息 + this.selected = selected || []; + // 范围开始 + this.startDate = startDate + // 范围结束 + this.endDate = endDate + this.range = range + // 多选状态 + this.cleanMultipleStatus() + // 每周日期 + this.weeks = {} + // this._getWeek(this.date.fullDate) + } + /** + * 设置日期 + * @param {Object} date + */ + setDate(date) { + this.selectDate = this.getDate(date) + this._getWeek(this.selectDate.fullDate) + } + + /** + * 清理多选状态 + */ + cleanMultipleStatus() { + this.multipleStatus = { + before: '', + after: '', + data: [] + } + } + + /** + * 重置开始日期 + */ + resetSatrtDate(startDate) { + // 范围开始 + this.startDate = startDate + + } + + /** + * 重置结束日期 + */ + resetEndDate(endDate) { + // 范围结束 + this.endDate = endDate + } + + /** + * 获取任意时间 + */ + getDate(date, AddDayCount = 0, str = 'day') { + if (!date) { + date = new Date() + } + if (typeof date !== 'object') { + date = date.replace(/-/g, '/') + } + const dd = new Date(date) + switch (str) { + case 'day': + dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期 + break + case 'month': + if (dd.getDate() === 31) { + dd.setDate(dd.getDate() + AddDayCount) + } else { + dd.setMonth(dd.getMonth() + AddDayCount) // 获取AddDayCount天后的日期 + } + break + case 'year': + dd.setFullYear(dd.getFullYear() + AddDayCount) // 获取AddDayCount天后的日期 + break + } + const y = dd.getFullYear() + const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期,不足10补0 + const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号,不足10补0 + return { + fullDate: y + '-' + m + '-' + d, + year: y, + month: m, + date: d, + day: dd.getDay() + } + } + + + /** + * 获取上月剩余天数 + */ + _getLastMonthDays(firstDay, full) { + let dateArr = [] + for (let i = firstDay; i > 0; i--) { + const beforeDate = new Date(full.year, full.month - 1, -i + 1).getDate() + dateArr.push({ + date: beforeDate, + month: full.month - 1, + lunar: this.getlunar(full.year, full.month - 1, beforeDate), + disable: true + }) + } + return dateArr + } + /** + * 获取本月天数 + */ + _currentMonthDys(dateData, full) { + let dateArr = [] + let fullDate = this.date.fullDate + for (let i = 1; i <= dateData; i++) { + let nowDate = full.year + '-' + (full.month < 10 ? + full.month : full.month) + '-' + (i < 10 ? + '0' + i : i) + // 是否今天 + let isDay = fullDate === nowDate + // 获取打点信息 + let info = this.selected && this.selected.find((item) => { + if (this.dateEqual(nowDate, item.date)) { + return item + } + }) + + // 日期禁用 + let disableBefore = true + let disableAfter = true + if (this.startDate) { + // let dateCompBefore = this.dateCompare(this.startDate, fullDate) + // disableBefore = this.dateCompare(dateCompBefore ? this.startDate : fullDate, nowDate) + disableBefore = this.dateCompare(this.startDate, nowDate) + } + + if (this.endDate) { + // let dateCompAfter = this.dateCompare(fullDate, this.endDate) + // disableAfter = this.dateCompare(nowDate, dateCompAfter ? this.endDate : fullDate) + disableAfter = this.dateCompare(nowDate, this.endDate) + } + let multiples = this.multipleStatus.data + let checked = false + let multiplesStatus = -1 + if (this.range) { + if (multiples) { + multiplesStatus = multiples.findIndex((item) => { + return this.dateEqual(item, nowDate) + }) + } + if (multiplesStatus !== -1) { + checked = true + } + } + let data = { + fullDate: nowDate, + year: full.year, + date: i, + multiple: this.range ? checked : false, + beforeMultiple: this.dateEqual(this.multipleStatus.before, nowDate), + afterMultiple: this.dateEqual(this.multipleStatus.after, nowDate), + month: full.month, + lunar: this.getlunar(full.year, full.month, i), + disable: !(disableBefore && disableAfter), + isDay + } + if (info) { + data.extraInfo = info + } + + dateArr.push(data) + } + return dateArr + } + /** + * 获取下月天数 + */ + _getNextMonthDays(surplus, full) { + let dateArr = [] + for (let i = 1; i < surplus + 1; i++) { + dateArr.push({ + date: i, + month: Number(full.month) + 1, + lunar: this.getlunar(full.year, Number(full.month) + 1, i), + disable: true + }) + } + return dateArr + } + + /** + * 获取当前日期详情 + * @param {Object} date + */ + getInfo(date) { + if (!date) { + date = new Date() + } + const dateInfo = this.canlender.find(item => item.fullDate === this.getDate(date).fullDate) + return dateInfo + } + + /** + * 比较时间大小 + */ + dateCompare(startDate, endDate) { + // 计算截止时间 + startDate = new Date(startDate.replace('-', '/').replace('-', '/')) + // 计算详细项的截止时间 + endDate = new Date(endDate.replace('-', '/').replace('-', '/')) + if (startDate <= endDate) { + return true + } else { + return false + } + } + + /** + * 比较时间是否相等 + */ + dateEqual(before, after) { + // 计算截止时间 + before = new Date(before.replace('-', '/').replace('-', '/')) + // 计算详细项的截止时间 + after = new Date(after.replace('-', '/').replace('-', '/')) + if (before.getTime() - after.getTime() === 0) { + return true + } else { + return false + } + } + + + /** + * 获取日期范围内所有日期 + * @param {Object} begin + * @param {Object} end + */ + geDateAll(begin, end) { + var arr = [] + var ab = begin.split('-') + var ae = end.split('-') + var db = new Date() + db.setFullYear(ab[0], ab[1] - 1, ab[2]) + var de = new Date() + de.setFullYear(ae[0], ae[1] - 1, ae[2]) + var unixDb = db.getTime() - 24 * 60 * 60 * 1000 + var unixDe = de.getTime() - 24 * 60 * 60 * 1000 + for (var k = unixDb; k <= unixDe;) { + k = k + 24 * 60 * 60 * 1000 + arr.push(this.getDate(new Date(parseInt(k))).fullDate) + } + return arr + } + /** + * 计算阴历日期显示 + */ + getlunar(year, month, date) { + return CALENDAR.solar2lunar(year, month, date) + } + /** + * 设置打点 + */ + setSelectInfo(data, value) { + this.selected = value + this._getWeek(data) + } + + /** + * 获取多选状态 + */ + setMultiple(fullDate) { + let { + before, + after + } = this.multipleStatus + + if (!this.range) return + if (before && after) { + this.multipleStatus.before = '' + this.multipleStatus.after = '' + this.multipleStatus.data = [] + } else { + if (!before) { + this.multipleStatus.before = fullDate + } else { + this.multipleStatus.after = fullDate + if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after); + } else { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before); + } + } + } + this._getWeek(fullDate) + } + + /** + * 获取每周数据 + * @param {Object} dateData + */ + _getWeek(dateData) { + const { + year, + month + } = this.getDate(dateData) + let firstDay = new Date(year, month - 1, 1).getDay() + let currentDay = new Date(year, month, 0).getDate() + let dates = { + lastMonthDays: this._getLastMonthDays(firstDay, this.getDate(dateData)), // 上个月末尾几天 + currentMonthDys: this._currentMonthDys(currentDay, this.getDate(dateData)), // 本月天数 + nextMonthDays: [], // 下个月开始几天 + weeks: [] + } + let canlender = [] + const surplus = 42 - (dates.lastMonthDays.length + dates.currentMonthDys.length) + dates.nextMonthDays = this._getNextMonthDays(surplus, this.getDate(dateData)) + canlender = canlender.concat(dates.lastMonthDays, dates.currentMonthDys, dates.nextMonthDays) + let weeks = {} + // 拼接数组 上个月开始几天 + 本月天数+ 下个月开始几天 + for (let i = 0; i < canlender.length; i++) { + if (i % 7 === 0) { + weeks[parseInt(i / 7)] = new Array(7) + } + weeks[parseInt(i / 7)][i % 7] = canlender[i] + } + this.canlender = canlender + this.weeks = weeks + } + + //静态方法 + // static init(date) { + // if (!this.instance) { + // this.instance = new Calendar(date); + // } + // return this.instance; + // } +} + + +export default Calendar diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/package.json new file mode 100644 index 0000000..40455c8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-calendar", + "displayName": "uni-calendar 日历", + "version": "1.4.5", + "description": "日历组件", + "keywords": [ + "uni-ui", + "uniui", + "日历", + "", + "打卡", + "日历选择" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/readme.md new file mode 100644 index 0000000..4f3ca0e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-calendar/readme.md @@ -0,0 +1,103 @@ + + +## Calendar 日历 +> **组件名:uni-calendar** +> 代码块: `uCalendar` + + +日历组件 + +> **注意事项** +> 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。 +> - 本组件农历转换使用的js是 [@1900-2100区间内的公历、农历互转](https://github.com/jjonline/calendar.js) +> - 仅支持自定义组件模式 +> - `date`属性传入的应该是一个 String ,如: 2019-06-27 ,而不是 new Date() +> - 通过 `insert` 属性来确定当前的事件是 @change 还是 @confirm 。理应合并为一个事件,但是为了区分模式,现使用两个事件,这里需要注意 +> - 弹窗模式下无法阻止后面的元素滚动,如有需要阻止,请在弹窗弹出后,手动设置滚动元素为不可滚动 + + +### 安装方式 + +本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。 + +如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55) + +### 基本用法 + +在 ``template`` 中使用组件 + +```html + + + +``` + +### 通过方法打开日历 + +需要设置 `insert` 为 `false` + +```html + + + + +``` + +```javascript + +export default { + data() { + return {}; + }, + methods: { + open(){ + this.$refs.calendar.open(); + }, + confirm(e) { + console.log(e); + } + } +}; + +``` + + +## API + +### Calendar Props + +| 属性名 | 类型 | 默认值| 说明 | +| | | +| date | String |- | 自定义当前时间,默认为今天 | +| lunar | Boolean | false | 显示农历 | +| startDate | String |- | 日期选择范围-开始日期 | +| endDate | String |- | 日期选择范围-结束日期 | +| range | Boolean | false | 范围选择 | +| insert | Boolean | false | 插入模式,可选值,ture:插入模式;false:弹窗模式;默认为插入模式 | +|clearDate |Boolean |true |弹窗模式是否清空上次选择内容 | +| selected | Array |- | 打点,期待格式[{date: '2019-06-27', info: '签到', data: { custom: '自定义信息', name: '自定义消息头',xxx:xxx... }}] | +|showMonth | Boolean | true | 是否显示月份为背景 | + +### Calendar Events + +| 事件名 | 说明 |返回值| +| | | | +| open | 弹出日历组件,`insert :false` 时生效|- | + + + + + +## 组件示例 + +点击查看:[https://hellouniapp.dcloud.net.cn/pages/extUI/calendar/calendar](https://hellouniapp.dcloud.net.cn/pages/extUI/calendar/calendar) \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-card/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-card/changelog.md new file mode 100644 index 0000000..c3cd8c4 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-card/changelog.md @@ -0,0 +1,26 @@ +## 1.3.1(2021-12-20) +- 修复 在vue页面下略缩图显示不正常的bug +## 1.3.0(2021-11-19) +- 重构插槽的用法 ,header 替换为 title +- 新增 actions 插槽 +- 新增 cover 封面图属性和插槽 +- 新增 padding 内容默认内边距离 +- 新增 margin 卡片默认外边距离 +- 新增 spacing 卡片默认内边距 +- 新增 shadow 卡片阴影属性 +- 取消 mode 属性,可使用组合插槽代替 +- 取消 note 属性 ,使用actions插槽代替 +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-card](https://uniapp.dcloud.io/component/uniui/uni-card) +## 1.2.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.8(2021-07-01) +- 优化 图文卡片无图片加载时,提供占位图标 +- 新增 header 插槽,自定义卡片头部( 图文卡片 mode="style" 时,不支持) +- 修复 thumbnail 不存在仍然占位的 bug +## 1.1.7(2021-05-12) +- 新增 组件示例地址 +## 1.1.6(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-card/components/uni-card/uni-card.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-card/components/uni-card/uni-card.vue new file mode 100644 index 0000000..38cf594 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-card/components/uni-card/uni-card.vue @@ -0,0 +1,270 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-card/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-card/package.json new file mode 100644 index 0000000..f16224d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-card/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-card", + "displayName": "uni-card 卡片", + "version": "1.3.1", + "description": "Card 组件,提供常见的卡片样式。", + "keywords": [ + "uni-ui", + "uniui", + "card", + "", + "卡片" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-icons", + "uni-scss" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-card/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-card/readme.md new file mode 100644 index 0000000..7434e71 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-card/readme.md @@ -0,0 +1,12 @@ + + +## Card 卡片 +> **组件名:uni-card** +> 代码块: `uCard` + +卡片视图组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-card) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/changelog.md new file mode 100644 index 0000000..292e4c7 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/changelog.md @@ -0,0 +1,36 @@ +## 1.4.3(2022-01-25) +- 修复 初始化的时候 ,open 属性失效的bug +## 1.4.2(2022-01-21) +- 修复 微信小程序resize后组件收起的bug +## 1.4.1(2021-11-22) +- 修复 vue3中个别scss变量无法找到的问题 +## 1.4.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-collapse](https://uniapp.dcloud.io/component/uniui/uni-collapse) +## 1.3.3(2021-08-17) +- 优化 show-arrow 属性默认为true +## 1.3.2(2021-08-17) +- 新增 show-arrow 属性,控制是否显示右侧箭头 +## 1.3.1(2021-07-30) +- 优化 vue3下小程序事件警告的问题 +## 1.3.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.2.2(2021-07-21) +- 修复 由1.2.0版本引起的 change 事件返回 undefined 的Bug +## 1.2.1(2021-07-21) +- 优化 组件示例 +## 1.2.0(2021-07-21) +- 新增 组件折叠动画 +- 新增 value\v-model 属性 ,动态修改面板折叠状态 +- 新增 title 插槽 ,可定义面板标题 +- 新增 border 属性 ,显示隐藏面板内容分隔线 +- 新增 title-border 属性 ,显示隐藏面板标题分隔线 +- 修复 resize 方法失效的Bug +- 修复 change 事件返回参数不正确的Bug +- 优化 H5、App 平台自动更具内容更新高度,无需调用 reszie() 方法 +## 1.1.7(2021-05-12) +- 新增 组件示例地址 +## 1.1.6(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.1.5(2021-02-05) +- 调整为uni_modules目录规范 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/components/uni-collapse-item/uni-collapse-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/components/uni-collapse-item/uni-collapse-item.vue new file mode 100644 index 0000000..d62a6a7 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/components/uni-collapse-item/uni-collapse-item.vue @@ -0,0 +1,402 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/components/uni-collapse/uni-collapse.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/components/uni-collapse/uni-collapse.vue new file mode 100644 index 0000000..384c39a --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/components/uni-collapse/uni-collapse.vue @@ -0,0 +1,147 @@ + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/package.json new file mode 100644 index 0000000..65349cf --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-collapse", + "displayName": "uni-collapse 折叠面板", + "version": "1.4.3", + "description": "Collapse 组件,可以折叠 / 展开的内容区域。", + "keywords": [ + "uni-ui", + "折叠", + "折叠面板", + "手风琴" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/readme.md new file mode 100644 index 0000000..bc758eb --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-collapse/readme.md @@ -0,0 +1,12 @@ + + +## Collapse 折叠面板 +> **组件名:uni-collapse** +> 代码块: `uCollapse` +> 关联组件:`uni-collapse-item`、`uni-icons`。 + + +折叠面板用来折叠/显示过长的内容或者是列表。通常是在多内容分类项使用,折叠不重要的内容,显示重要内容。点击可以展开折叠部分。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-collapse) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-combox/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-combox/changelog.md new file mode 100644 index 0000000..23c2748 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-combox/changelog.md @@ -0,0 +1,15 @@ +## 1.0.1(2021-11-23) +- 优化 label、label-width 属性 +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-combox](https://uniapp.dcloud.io/component/uniui/uni-combox) +## 0.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.0.6(2021-05-12) +- 新增 组件示例地址 +## 0.0.5(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 0.0.4(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 0.0.3(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-combox/components/uni-combox/uni-combox.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-combox/components/uni-combox/uni-combox.vue new file mode 100644 index 0000000..500b6f8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-combox/components/uni-combox/uni-combox.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-combox/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-combox/package.json new file mode 100644 index 0000000..4a05c3f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-combox/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-combox", + "displayName": "uni-combox 组合框", + "version": "1.0.1", + "description": "可以选择也可以输入的表单项 ", + "keywords": [ + "uni-ui", + "uniui", + "combox", + "组合框", + "select" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-combox/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-combox/readme.md new file mode 100644 index 0000000..ffa2cc8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-combox/readme.md @@ -0,0 +1,11 @@ + + +## Combox 组合框 +> **组件名:uni-combox** +> 代码块: `uCombox` + + +组合框组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-combox) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/changelog.md new file mode 100644 index 0000000..f25beef --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/changelog.md @@ -0,0 +1,24 @@ +## 1.2.2(2022-01-19) +- 修复 在微信小程序中样式不生效的bug +## 1.2.1(2022-01-18) +- 新增 update 方法 ,在动态更新时间后,刷新组件 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-countdown](https://uniapp.dcloud.io/component/uniui/uni-countdown) +## 1.1.3(2021-10-18) +- 重构 +- 新增 font-size 支持自定义字体大小 +## 1.1.2(2021-08-24) +- 新增 支持国际化 +## 1.1.1(2021-07-30) +- 优化 vue3下小程序事件警告的问题 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.5(2021-06-18) +- 修复 uni-countdown 重复赋值跳两秒的 bug +## 1.0.4(2021-05-12) +- 新增 组件示例地址 +## 1.0.3(2021-05-08) +- 修复 uni-countdown 不能控制倒计时的 bug +## 1.0.2(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/en.json new file mode 100644 index 0000000..06309cb --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/en.json @@ -0,0 +1,6 @@ +{ + "uni-countdown.day": "day", + "uni-countdown.h": "h", + "uni-countdown.m": "m", + "uni-countdown.s": "s" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hans.json new file mode 100644 index 0000000..358cdd1 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hans.json @@ -0,0 +1,6 @@ +{ + "uni-countdown.day": "天", + "uni-countdown.h": "时", + "uni-countdown.m": "分", + "uni-countdown.s": "秒" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hant.json new file mode 100644 index 0000000..e5a63de --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/i18n/zh-Hant.json @@ -0,0 +1,6 @@ +{ + "uni-countdown.day": "天", + "uni-countdown.h": "時", + "uni-countdown.m": "分", + "uni-countdown.s": "秒" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/uni-countdown.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/uni-countdown.vue new file mode 100644 index 0000000..1f8ef4e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/components/uni-countdown/uni-countdown.vue @@ -0,0 +1,271 @@ + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/package.json new file mode 100644 index 0000000..70e99ee --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-countdown", + "displayName": "uni-countdown 倒计时", + "version": "1.2.2", + "description": "CountDown 倒计时组件", + "keywords": [ + "uni-ui", + "uniui", + "countdown", + "倒计时" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/readme.md new file mode 100644 index 0000000..4bcb1aa --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-countdown/readme.md @@ -0,0 +1,10 @@ + + +## CountDown 倒计时 +> **组件名:uni-countdown** +> 代码块: `uCountDown` + +倒计时组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-countdown) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/changelog.md new file mode 100644 index 0000000..dbc517a --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/changelog.md @@ -0,0 +1,43 @@ +## 1.0.2(2022-06-30) +- 优化 在 uni-forms 中的依赖注入方式 +## 1.0.1(2022-02-07) +- 修复 multiple 为 true 时,v-model 的值为 null 报错的 bug +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-checkbox](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox) +## 0.2.5(2021-08-23) +- 修复 在uni-forms中 modelValue 中不存在当前字段,当前字段必填写也不参与校验的问题 +## 0.2.4(2021-08-17) +- 修复 单选 list 模式下 ,icon 为 left 时,选中图标不显示的问题 +## 0.2.3(2021-08-11) +- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 +## 0.2.2(2021-07-30) +- 优化 在uni-forms组件,与label不对齐的问题 +## 0.2.1(2021-07-27) +- 修复 单选默认值为0不能选中的Bug +## 0.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.1.11(2021-07-06) +- 优化 删除无用日志 +## 0.1.10(2021-07-05) +- 修复 由 0.1.9 引起的非 nvue 端图标不显示的问题 +## 0.1.9(2021-07-05) +- 修复 nvue 黑框样式问题 +## 0.1.8(2021-06-28) +- 修复 selectedTextColor 属性不生效的Bug +## 0.1.7(2021-06-02) +- 新增 map 属性,可以方便映射text/value属性 +## 0.1.6(2021-05-26) +- 修复 不关联服务空间的情况下组件报错的Bug +## 0.1.5(2021-05-12) +- 新增 组件示例地址 +## 0.1.4(2021-04-09) +- 修复 nvue 下无法选中的问题 +## 0.1.3(2021-03-22) +- 新增 disabled属性 +## 0.1.2(2021-02-24) +- 优化 默认颜色显示 +## 0.1.1(2021-02-24) +- 新增 支持nvue +## 0.1.0(2021-02-18) +- “暂无数据”显示居中 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue new file mode 100644 index 0000000..2e51712 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/components/uni-data-checkbox/uni-data-checkbox.vue @@ -0,0 +1,817 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/package.json new file mode 100644 index 0000000..51470a9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-data-checkbox", + "displayName": "uni-data-checkbox 数据选择器", + "version": "1.0.2", + "description": "通过数据驱动的单选框和复选框", + "keywords": [ + "uni-ui", + "checkbox", + "单选", + "多选", + "单选多选" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.1" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-load-more","uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/readme.md new file mode 100644 index 0000000..6eb253d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-checkbox/readme.md @@ -0,0 +1,18 @@ + + +## DataCheckbox 数据驱动的单选复选框 +> **组件名:uni-data-checkbox** +> 代码块: `uDataCheckbox` + + +本组件是基于uni-app基础组件checkbox的封装。本组件要解决问题包括: + +1. 数据绑定型组件:给本组件绑定一个data,会自动渲染一组候选内容。再以往,开发者需要编写不少代码实现类似功能 +2. 自动的表单校验:组件绑定了data,且符合[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)组件的表单校验规范,搭配使用会自动实现表单校验 +3. 本组件合并了单选多选 +4. 本组件有若干风格选择,如普通的单选多选框、并列button风格、tag风格。开发者可以快速选择需要的风格。但作为一个封装组件,样式代码虽然不用自己写了,却会牺牲一定的样式自定义性 + +在uniCloud开发中,`DB Schema`中配置了enum枚举等类型后,在web控制台的[自动生成表单](https://uniapp.dcloud.io/uniCloud/schema?id=autocode)功能中,会自动生成``uni-data-checkbox``组件并绑定好data + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-checkbox) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/changelog.md new file mode 100644 index 0000000..083e521 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/changelog.md @@ -0,0 +1,64 @@ +## 1.0.7(2022-07-06) +- 优化 pc端图标位置不正确的问题 +## 1.0.6(2022-07-05) +- 优化 显示样式 +## 1.0.5(2022-07-04) +- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug +## 1.0.4(2022-04-19) +- 修复 字节小程序 本地数据无法选择下一级的Bug +## 1.0.3(2022-02-25) +- 修复 nvue 不支持的 v-show 的 bug +## 1.0.2(2022-02-25) +- 修复 条件编译 nvue 不支持的 css 样式 +## 1.0.1(2021-11-23) +- 修复 由上个版本引发的map、v-model等属性不生效的bug +## 1.0.0(2021-11-19) +- 优化 组件 UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-data-picker](https://uniapp.dcloud.io/component/uniui/uni-data-picker) +## 0.4.9(2021-10-28) +- 修复 VUE2 v-model 概率无效的 bug +## 0.4.8(2021-10-27) +- 修复 v-model 概率无效的 bug +## 0.4.7(2021-10-25) +- 新增 属性 spaceInfo 服务空间配置 HBuilderX 3.2.11+ +- 修复 树型 uniCloud 数据类型为 int 时报错的 bug +## 0.4.6(2021-10-19) +- 修复 非 VUE3 v-model 为 0 时无法选中的 bug +## 0.4.5(2021-09-26) +- 新增 清除已选项的功能(通过 clearIcon 属性配置是否显示按钮),同时提供 clear 方法以供调用,二者等效 +- 修复 readonly 为 true 时报错的 bug +## 0.4.4(2021-09-26) +- 修复 上一版本造成的 map 属性失效的 bug +- 新增 ellipsis 属性,支持配置 tab 选项长度过长时是否自动省略 +## 0.4.3(2021-09-24) +- 修复 某些情况下级联未触发的 bug +## 0.4.2(2021-09-23) +- 新增 提供 show 和 hide 方法,开发者可以通过 ref 调用 +- 新增 选项内容过长自动添加省略号 +## 0.4.1(2021-09-15) +- 新增 map 属性 字段映射,将 text/value 映射到数据中的其他字段 +## 0.4.0(2021-07-13) +- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.3.5(2021-06-04) +- 修复 无法加载云端数据的问题 +## 0.3.4(2021-05-28) +- 修复 v-model 无效问题 +- 修复 loaddata 为空数据组时加载时间过长问题 +- 修复 上个版本引出的本地数据无法选择带有 children 的 2 级节点 +## 0.3.3(2021-05-12) +- 新增 组件示例地址 +## 0.3.2(2021-04-22) +- 修复 非树形数据有 where 属性查询报错的问题 +## 0.3.1(2021-04-15) +- 修复 本地数据概率无法回显时问题 +## 0.3.0(2021-04-07) +- 新增 支持云端非树形表结构数据 +- 修复 根节点 parent_field 字段等于 null 时选择界面错乱问题 +## 0.2.0(2021-03-15) +- 修复 nodeclick、popupopened、popupclosed 事件无法触发的问题 +## 0.1.9(2021-03-09) +- 修复 微信小程序某些情况下无法选择的问题 +## 0.1.8(2021-02-05) +- 优化 部分样式在 nvue 上的兼容表现 +## 0.1.7(2021-02-05) +- 调整为 uni_modules 目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js new file mode 100644 index 0000000..6ef26a2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + this.$once('hook:beforeDestroy', () => { + document.removeEventListener('keyup', listener) + }) + }, + render: () => {} +} +// #endif diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue new file mode 100644 index 0000000..4553627 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-picker/uni-data-picker.vue @@ -0,0 +1,554 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js new file mode 100644 index 0000000..c12fd54 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-picker.js @@ -0,0 +1,563 @@ +export default { + props: { + localdata: { + type: [Array, Object], + default () { + return [] + } + }, + spaceInfo: { + type: Object, + default () { + return {} + } + }, + collection: { + type: String, + default: '' + }, + action: { + type: String, + default: '' + }, + field: { + type: String, + default: '' + }, + orderby: { + type: String, + default: '' + }, + where: { + type: [String, Object], + default: '' + }, + pageData: { + type: String, + default: 'add' + }, + pageCurrent: { + type: Number, + default: 1 + }, + pageSize: { + type: Number, + default: 20 + }, + getcount: { + type: [Boolean, String], + default: false + }, + getone: { + type: [Boolean, String], + default: false + }, + gettree: { + type: [Boolean, String], + default: false + }, + manual: { + type: Boolean, + default: false + }, + value: { + type: [Array, String, Number], + default () { + return [] + } + }, + modelValue: { + type: [Array, String, Number], + default () { + return [] + } + }, + preload: { + type: Boolean, + default: false + }, + stepSearh: { + type: Boolean, + default: true + }, + selfField: { + type: String, + default: '' + }, + parentField: { + type: String, + default: '' + }, + multiple: { + type: Boolean, + default: false + }, + map: { + type: Object, + default() { + return { + text: "text", + value: "value" + } + } + } + }, + data() { + return { + loading: false, + errorMessage: '', + loadMore: { + contentdown: '', + contentrefresh: '', + contentnomore: '' + }, + dataList: [], + selected: [], + selectedIndex: 0, + page: { + current: this.pageCurrent, + size: this.pageSize, + count: 0 + } + } + }, + computed: { + isLocaldata() { + return !this.collection.length + }, + postField() { + let fields = [this.field]; + if (this.parentField) { + fields.push(`${this.parentField} as parent_value`); + } + return fields.join(','); + }, + dataValue() { + let isModelValue = Array.isArray(this.modelValue) ? (this.modelValue.length > 0) : (this.modelValue !== null || this.modelValue !== undefined) + return isModelValue ? this.modelValue : this.value + }, + hasValue() { + if (typeof this.dataValue === 'number') { + return true + } + return (this.dataValue != null) && (this.dataValue.length > 0) + } + }, + created() { + this.$watch(() => { + var al = []; + ['pageCurrent', + 'pageSize', + 'spaceInfo', + 'value', + 'modelValue', + 'localdata', + 'collection', + 'action', + 'field', + 'orderby', + 'where', + 'getont', + 'getcount', + 'gettree' + ].forEach(key => { + al.push(this[key]) + }); + return al + }, (newValue, oldValue) => { + let needReset = false + for (let i = 2; i < newValue.length; i++) { + if (newValue[i] != oldValue[i]) { + needReset = true + break + } + } + if (newValue[0] != oldValue[0]) { + this.page.current = this.pageCurrent + } + this.page.size = this.pageSize + + this.onPropsChange() + }) + this._treeData = [] + }, + methods: { + onPropsChange() { + this._treeData = [] + }, + getCommand(options = {}) { + /* eslint-disable no-undef */ + let db = uniCloud.database(this.spaceInfo) + + const action = options.action || this.action + if (action) { + db = db.action(action) + } + + const collection = options.collection || this.collection + db = db.collection(collection) + + const where = options.where || this.where + if (!(!where || !Object.keys(where).length)) { + db = db.where(where) + } + + const field = options.field || this.field + if (field) { + db = db.field(field) + } + + const orderby = options.orderby || this.orderby + if (orderby) { + db = db.orderBy(orderby) + } + + const current = options.pageCurrent !== undefined ? options.pageCurrent : this.page.current + const size = options.pageSize !== undefined ? options.pageSize : this.page.size + const getCount = options.getcount !== undefined ? options.getcount : this.getcount + const getTree = options.gettree !== undefined ? options.gettree : this.gettree + + const getOptions = { + getCount, + getTree + } + if (options.getTreePath) { + getOptions.getTreePath = options.getTreePath + } + + db = db.skip(size * (current - 1)).limit(size).get(getOptions) + + return db + }, + getNodeData(callback) { + if (this.loading) { + return + } + this.loading = true + this.getCommand({ + field: this.postField, + where: this._pathWhere() + }).then((res) => { + this.loading = false + this.selected = res.result.data + callback && callback() + }).catch((err) => { + this.loading = false + this.errorMessage = err + }) + }, + getTreePath(callback) { + if (this.loading) { + return + } + this.loading = true + + this.getCommand({ + field: this.postField, + getTreePath: { + startWith: `${this.selfField}=='${this.dataValue}'` + } + }).then((res) => { + this.loading = false + let treePath = [] + this._extractTreePath(res.result.data, treePath) + this.selected = treePath + callback && callback() + }).catch((err) => { + this.loading = false + this.errorMessage = err + }) + }, + loadData() { + if (this.isLocaldata) { + this._processLocalData() + return + } + + if (this.dataValue != null) { + this._loadNodeData((data) => { + this._treeData = data + this._updateBindData() + this._updateSelected() + }) + return + } + + if (this.stepSearh) { + this._loadNodeData((data) => { + this._treeData = data + this._updateBindData() + }) + } else { + this._loadAllData((data) => { + this._treeData = [] + this._extractTree(data, this._treeData, null) + this._updateBindData() + }) + } + }, + _loadAllData(callback) { + if (this.loading) { + return + } + this.loading = true + + this.getCommand({ + field: this.postField, + gettree: true, + startwith: `${this.selfField}=='${this.dataValue}'` + }).then((res) => { + this.loading = false + callback(res.result.data) + this.onDataChange() + }).catch((err) => { + this.loading = false + this.errorMessage = err + }) + }, + _loadNodeData(callback, pw) { + if (this.loading) { + return + } + this.loading = true + + this.getCommand({ + field: this.postField, + where: pw || this._postWhere(), + pageSize: 500 + }).then((res) => { + this.loading = false + callback(res.result.data) + this.onDataChange() + }).catch((err) => { + this.loading = false + this.errorMessage = err + }) + }, + _pathWhere() { + let result = [] + let where_field = this._getParentNameByField(); + if (where_field) { + result.push(`${where_field} == '${this.dataValue}'`) + } + + if (this.where) { + return `(${this.where}) && (${result.join(' || ')})` + } + + return result.join(' || ') + }, + _postWhere() { + let result = [] + let selected = this.selected + let parentField = this.parentField + if (parentField) { + result.push(`${parentField} == null || ${parentField} == ""`) + } + if (selected.length) { + for (var i = 0; i < selected.length - 1; i++) { + result.push(`${parentField} == '${selected[i].value}'`) + } + } + + let where = [] + if (this.where) { + where.push(`(${this.where})`) + } + if (result.length) { + where.push(`(${result.join(' || ')})`) + } + + return where.join(' && ') + }, + _nodeWhere() { + let result = [] + let selected = this.selected + if (selected.length) { + result.push(`${this.parentField} == '${selected[selected.length - 1].value}'`) + } + + if (this.where) { + return `(${this.where}) && (${result.join(' || ')})` + } + + return result.join(' || ') + }, + _getParentNameByField() { + const fields = this.field.split(','); + let where_field = null; + for (let i = 0; i < fields.length; i++) { + const items = fields[i].split('as'); + if (items.length < 2) { + continue; + } + if (items[1].trim() === 'value') { + where_field = items[0].trim(); + break; + } + } + return where_field + }, + _isTreeView() { + return (this.parentField && this.selfField) + }, + _updateSelected() { + var dl = this.dataList + var sl = this.selected + let textField = this.map.text + let valueField = this.map.value + for (var i = 0; i < sl.length; i++) { + var value = sl[i].value + var dl2 = dl[i] + for (var j = 0; j < dl2.length; j++) { + var item2 = dl2[j] + if (item2[valueField] === value) { + sl[i].text = item2[textField] + break + } + } + } + }, + _updateBindData(node) { + const { + dataList, + hasNodes + } = this._filterData(this._treeData, this.selected) + + let isleaf = this._stepSearh === false && !hasNodes + + if (node) { + node.isleaf = isleaf + } + + this.dataList = dataList + this.selectedIndex = dataList.length - 1 + + if (!isleaf && this.selected.length < dataList.length) { + this.selected.push({ + value: null, + text: "请选择" + }) + } + + return { + isleaf, + hasNodes + } + }, + _filterData(data, paths) { + let dataList = [] + let hasNodes = true + + dataList.push(data.filter((item) => { + return (item.parent_value === null || item.parent_value === undefined || item.parent_value === '') + })) + for (let i = 0; i < paths.length; i++) { + var value = paths[i].value + var nodes = data.filter((item) => { + return item.parent_value === value + }) + + if (nodes.length) { + dataList.push(nodes) + } else { + hasNodes = false + } + } + + return { + dataList, + hasNodes + } + }, + _extractTree(nodes, result, parent_value) { + let list = result || [] + let valueField = this.map.value + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + + let child = {} + for (let key in node) { + if (key !== 'children') { + child[key] = node[key] + } + } + if (parent_value !== null && parent_value !== undefined && parent_value !== '') { + child.parent_value = parent_value + } + result.push(child) + + let children = node.children + if (children) { + this._extractTree(children, result, node[valueField]) + } + } + }, + _extractTreePath(nodes, result) { + let list = result || [] + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + + let child = {} + for (let key in node) { + if (key !== 'children') { + child[key] = node[key] + } + } + result.push(child) + + let children = node.children + if (children) { + this._extractTreePath(children, result) + } + } + }, + _findNodePath(key, nodes, path = []) { + let textField = this.map.text + let valueField = this.map.value + for (let i = 0; i < nodes.length; i++) { + let node = nodes[i] + let children = node.children + let text = node[textField] + let value = node[valueField] + + path.push({ + value, + text + }) + + if (value === key) { + return path + } + + if (children) { + const p = this._findNodePath(key, children, path) + if (p.length) { + return p + } + } + + path.pop() + } + return [] + }, + _processLocalData() { + this._treeData = [] + this._extractTree(this.localdata, this._treeData) + + var inputValue = this.dataValue + if (inputValue === undefined) { + return + } + + if (Array.isArray(inputValue)) { + inputValue = inputValue[inputValue.length - 1] + if (typeof inputValue === 'object' && inputValue[this.map.value]) { + inputValue = inputValue[this.map.value] + } + } + + this.selected = this._findNodePath(inputValue, this.localdata) + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue new file mode 100644 index 0000000..065aac2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/components/uni-data-pickerview/uni-data-pickerview.vue @@ -0,0 +1,333 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/package.json new file mode 100644 index 0000000..9900380 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/package.json @@ -0,0 +1,93 @@ +{ + "id": "uni-data-picker", + "displayName": "uni-data-picker 数据驱动的picker选择器", + "version": "1.0.7", + "description": "单列、多列级联选择器,常用于省市区城市选择、公司部门选择、多级分类等场景", + "keywords": [ + "uni-ui", + "uniui", + "picker", + "级联", + "省市区", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-load-more", + "uni-icons", + "uni-scss" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/readme.md new file mode 100644 index 0000000..6cda224 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-picker/readme.md @@ -0,0 +1,22 @@ +## DataPicker 级联选择 +> **组件名:uni-data-picker** +> 代码块: `uDataPicker` +> 关联组件:`uni-data-pickerview`、`uni-load-more`。 + + +`` 是一个选择类[datacom组件](https://uniapp.dcloud.net.cn/component/datacom)。 + +支持单列、和多列级联选择。列数没有限制,如果屏幕显示不全,顶部tab区域会左右滚动。 + +候选数据支持一次性加载完毕,也支持懒加载,比如示例图中,选择了“北京”后,动态加载北京的区县数据。 + +`` 组件尤其适用于地址选择、分类选择等选择类。 + +`` 支持本地数据、云端静态数据(json),uniCloud云数据库数据。 + +`` 可以通过JQL直连uniCloud云数据库,配套[DB Schema](https://uniapp.dcloud.net.cn/uniCloud/schema),可在schema2code中自动生成前端页面,还支持服务器端校验。 + +在uniCloud数据表中新建表“uni-id-address”和“opendb-city-china”,这2个表的schema自带foreignKey关联。在“uni-id-address”表的表结构页面使用schema2code生成前端页面,会自动生成地址管理的维护页面,自动从“opendb-city-china”表包含的中国所有省市区信息里选择地址。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/changelog.md new file mode 100644 index 0000000..d5beaa3 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/changelog.md @@ -0,0 +1,16 @@ +## 0.1.6(2022-07-06) +- 修复 pc端宽度异常的bug +## 0.1.5 +- 修复 pc端宽度异常的bug +## 0.1.4(2022-07-05) +- 优化 显示样式 +## 0.1.3(2022-06-02) +- 修复 localdata 赋值不生效的 bug +- 新增 支持 uni.scss 修改颜色 +- 新增 支持选项禁用(数据选项设置 disabled: true 即禁用) +## 0.1.2(2022-05-08) +- 修复 当 value 为 0 时选择不生效的 bug +## 0.1.1(2022-05-07) +- 新增 记住上次的选项(仅 collection 存在时有效) +## 0.1.0(2022-04-22) +- 初始化 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue new file mode 100644 index 0000000..16995bd --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue @@ -0,0 +1,426 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/package.json new file mode 100644 index 0000000..1ebd8dd --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-data-select", + "displayName": "uni-data-select 下拉框选择器", + "version": "0.1.6", + "description": "通过数据驱动的下拉框选择器", + "keywords": [ + "uni-ui", + "select", + "uni-data-select", + "下拉框", + "下拉选" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.1" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-load-more"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "u", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/readme.md new file mode 100644 index 0000000..eb58de3 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-data-select/readme.md @@ -0,0 +1,8 @@ +## DataSelect 下拉框选择器 +> **组件名:uni-data-select** +> 代码块: `uDataSelect` + +当选项过多时,使用下拉菜单展示并选择内容 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-data-select) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/changelog.md new file mode 100644 index 0000000..d551d7b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/changelog.md @@ -0,0 +1,10 @@ +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-dateformat](https://uniapp.dcloud.io/component/uniui/uni-dateformat) +## 0.0.5(2021-07-08) +- 调整 默认时间不再是当前时间,而是显示'-'字符 +## 0.0.4(2021-05-12) +- 新增 组件示例地址 +## 0.0.3(2021-02-04) +- 调整为uni_modules目录规范 +- 修复 iOS 平台日期格式化出错的问题 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js new file mode 100644 index 0000000..e00d559 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/date-format.js @@ -0,0 +1,200 @@ +// yyyy-MM-dd hh:mm:ss.SSS 所有支持的类型 +function pad(str, length = 2) { + str += '' + while (str.length < length) { + str = '0' + str + } + return str.slice(-length) +} + +const parser = { + yyyy: (dateObj) => { + return pad(dateObj.year, 4) + }, + yy: (dateObj) => { + return pad(dateObj.year) + }, + MM: (dateObj) => { + return pad(dateObj.month) + }, + M: (dateObj) => { + return dateObj.month + }, + dd: (dateObj) => { + return pad(dateObj.day) + }, + d: (dateObj) => { + return dateObj.day + }, + hh: (dateObj) => { + return pad(dateObj.hour) + }, + h: (dateObj) => { + return dateObj.hour + }, + mm: (dateObj) => { + return pad(dateObj.minute) + }, + m: (dateObj) => { + return dateObj.minute + }, + ss: (dateObj) => { + return pad(dateObj.second) + }, + s: (dateObj) => { + return dateObj.second + }, + SSS: (dateObj) => { + return pad(dateObj.millisecond, 3) + }, + S: (dateObj) => { + return dateObj.millisecond + }, +} + +// 这都n年了iOS依然不认识2020-12-12,需要转换为2020/12/12 +function getDate(time) { + if (time instanceof Date) { + return time + } + switch (typeof time) { + case 'string': + { + // 2020-12-12T12:12:12.000Z、2020-12-12T12:12:12.000 + if (time.indexOf('T') > -1) { + return new Date(time) + } + return new Date(time.replace(/-/g, '/')) + } + default: + return new Date(time) + } +} + +export function formatDate(date, format = 'yyyy/MM/dd hh:mm:ss') { + if (!date && date !== 0) { + return '' + } + date = getDate(date) + const dateObj = { + year: date.getFullYear(), + month: date.getMonth() + 1, + day: date.getDate(), + hour: date.getHours(), + minute: date.getMinutes(), + second: date.getSeconds(), + millisecond: date.getMilliseconds() + } + const tokenRegExp = /yyyy|yy|MM|M|dd|d|hh|h|mm|m|ss|s|SSS|SS|S/ + let flag = true + let result = format + while (flag) { + flag = false + result = result.replace(tokenRegExp, function(matched) { + flag = true + return parser[matched](dateObj) + }) + } + return result +} + +export function friendlyDate(time, { + locale = 'zh', + threshold = [60000, 3600000], + format = 'yyyy/MM/dd hh:mm:ss' +}) { + if (time === '-') { + return time + } + if (!time && time !== 0) { + return '' + } + const localeText = { + zh: { + year: '年', + month: '月', + day: '天', + hour: '小时', + minute: '分钟', + second: '秒', + ago: '前', + later: '后', + justNow: '刚刚', + soon: '马上', + template: '{num}{unit}{suffix}' + }, + en: { + year: 'year', + month: 'month', + day: 'day', + hour: 'hour', + minute: 'minute', + second: 'second', + ago: 'ago', + later: 'later', + justNow: 'just now', + soon: 'soon', + template: '{num} {unit} {suffix}' + } + } + const text = localeText[locale] || localeText.zh + let date = getDate(time) + let ms = date.getTime() - Date.now() + let absMs = Math.abs(ms) + if (absMs < threshold[0]) { + return ms < 0 ? text.justNow : text.soon + } + if (absMs >= threshold[1]) { + return formatDate(date, format) + } + let num + let unit + let suffix = text.later + if (ms < 0) { + suffix = text.ago + ms = -ms + } + const seconds = Math.floor((ms) / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + const months = Math.floor(days / 30) + const years = Math.floor(months / 12) + switch (true) { + case years > 0: + num = years + unit = text.year + break + case months > 0: + num = months + unit = text.month + break + case days > 0: + num = days + unit = text.day + break + case hours > 0: + num = hours + unit = text.hour + break + case minutes > 0: + num = minutes + unit = text.minute + break + default: + num = seconds + unit = text.second + break + } + + if (locale === 'en') { + if (num === 1) { + num = 'a' + } else { + unit += 's' + } + } + + return text.template.replace(/{\s*num\s*}/g, num + '').replace(/{\s*unit\s*}/g, unit).replace(/{\s*suffix\s*}/g, + suffix) +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue new file mode 100644 index 0000000..c5ed030 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/components/uni-dateformat/uni-dateformat.vue @@ -0,0 +1,88 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/package.json new file mode 100644 index 0000000..786a670 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-dateformat", + "displayName": "uni-dateformat 日期格式化", + "version": "1.0.0", + "description": "日期格式化组件,可以将日期格式化为1分钟前、刚刚等形式", + "keywords": [ + "uni-ui", + "uniui", + "日期格式化", + "时间格式化", + "格式化时间", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/readme.md new file mode 100644 index 0000000..37ddb6e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-dateformat/readme.md @@ -0,0 +1,11 @@ + + +### DateFormat 日期格式化 +> **组件名:uni-dateformat** +> 代码块: `uDateformat` + + +日期格式化组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-dateformat) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/changelog.md new file mode 100644 index 0000000..5c9735a --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/changelog.md @@ -0,0 +1,93 @@ +## 2.2.6(2022-06-30) +- 优化 组件样式,调整了组件图标大小、高度、颜色等,与uni-ui风格保持一致 +## 2.2.5(2022-06-24) +- 修复 日历顶部年月及底部确认未国际化 bug +## 2.2.4(2022-03-31) +- 修复 Vue3 下动态赋值,单选类型未响应的 bug +## 2.2.3(2022-03-28) +- 修复 Vue3 下动态赋值未响应的 bug +## 2.2.2(2021-12-10) +- 修复 clear-icon 属性在小程序平台不生效的 bug +## 2.2.1(2021-12-10) +- 修复 日期范围选在小程序平台,必须多点击一次才能取消选中状态的 bug +## 2.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-datetime-picker](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker) +## 2.1.5(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 2.1.4(2021-09-10) +- 修复 hide-second 在移动端的 bug +- 修复 单选赋默认值时,赋值日期未高亮的 bug +- 修复 赋默认值时,移动端未正确显示时间的 bug +## 2.1.3(2021-09-09) +- 新增 hide-second 属性,支持只使用时分,隐藏秒 +## 2.1.2(2021-09-03) +- 优化 取消选中时(范围选)直接开始下一次选择, 避免多点一次 +- 优化 移动端支持清除按钮,同时支持通过 ref 调用组件的 clear 方法 +- 优化 调整字号大小,美化日历界面 +- 修复 因国际化导致的 placeholder 失效的 bug +## 2.1.1(2021-08-24) +- 新增 支持国际化 +- 优化 范围选择器在 pc 端过宽的问题 +## 2.1.0(2021-08-09) +- 新增 适配 vue3 +## 2.0.19(2021-08-09) +- 新增 支持作为 uni-forms 子组件相关功能 +- 修复 在 uni-forms 中使用时,选择时间报 NAN 错误的 bug +## 2.0.18(2021-08-05) +- 修复 type 属性动态赋值无效的 bug +- 修复 ‘确认’按钮被 tabbar 遮盖 bug +- 修复 组件未赋值时范围选左、右日历相同的 bug +## 2.0.17(2021-08-04) +- 修复 范围选未正确显示当前值的 bug +- 修复 h5 平台(移动端)报错 'cale' of undefined 的 bug +## 2.0.16(2021-07-21) +- 新增 return-type 属性支持返回 date 日期对象 +## 2.0.15(2021-07-14) +- 修复 单选日期类型,初始赋值后不在当前日历的 bug +- 新增 clearIcon 属性,显示框的清空按钮可配置显示隐藏(仅 pc 有效) +- 优化 移动端移除显示框的清空按钮,无实际用途 +## 2.0.14(2021-07-14) +- 修复 组件赋值为空,界面未更新的 bug +- 修复 start 和 end 不能动态赋值的 bug +- 修复 范围选类型,用户选择后再次选择右侧日历(结束日期)显示不正确的 bug +## 2.0.13(2021-07-08) +- 修复 范围选择不能动态赋值的 bug +## 2.0.12(2021-07-08) +- 修复 范围选择的初始时间在一个月内时,造成无法选择的bug +## 2.0.11(2021-07-08) +- 优化 弹出层在超出视窗边缘定位不准确的问题 +## 2.0.10(2021-07-08) +- 修复 范围起始点样式的背景色与今日样式的字体前景色融合,导致日期字体看不清的 bug +- 优化 弹出层在超出视窗边缘被遮盖的问题 +## 2.0.9(2021-07-07) +- 新增 maskClick 事件 +- 修复 特殊情况日历 rpx 布局错误的 bug,rpx -> px +- 修复 范围选择时清空返回值不合理的bug,['', ''] -> [] +## 2.0.8(2021-07-07) +- 新增 日期时间显示框支持插槽 +## 2.0.7(2021-07-01) +- 优化 添加 uni-icons 依赖 +## 2.0.6(2021-05-22) +- 修复 图标在小程序上不显示的 bug +- 优化 重命名引用组件,避免潜在组件命名冲突 +## 2.0.5(2021-05-20) +- 优化 代码目录扁平化 +## 2.0.4(2021-05-12) +- 新增 组件示例地址 +## 2.0.3(2021-05-10) +- 修复 ios 下不识别 '-' 日期格式的 bug +- 优化 pc 下弹出层添加边框和阴影 +## 2.0.2(2021-05-08) +- 修复 在 admin 中获取弹出层定位错误的bug +## 2.0.1(2021-05-08) +- 修复 type 属性向下兼容,默认值从 date 变更为 datetime +## 2.0.0(2021-04-30) +- 支持日历形式的日期+时间的范围选择 + > 注意:此版本不向后兼容,不再支持单独时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker) +## 1.0.6(2021-03-18) +- 新增 hide-second 属性,时间支持仅选择时、分 +- 修复 选择跟显示的日期不一样的 bug +- 修复 chang事件触发2次的 bug +- 修复 分、秒 end 范围错误的 bug +- 优化 更好的 nvue 适配 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue new file mode 100644 index 0000000..3d2dbea --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar-item.vue @@ -0,0 +1,185 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue new file mode 100644 index 0000000..8f7f181 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/calendar.vue @@ -0,0 +1,907 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json new file mode 100644 index 0000000..9acf1ab --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/en.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "select date", + "uni-datetime-picker.selectTime": "select time", + "uni-datetime-picker.selectDateTime": "select datetime", + "uni-datetime-picker.startDate": "start date", + "uni-datetime-picker.endDate": "end date", + "uni-datetime-picker.startTime": "start time", + "uni-datetime-picker.endTime": "end time", + "uni-datetime-picker.ok": "ok", + "uni-datetime-picker.clear": "clear", + "uni-datetime-picker.cancel": "cancel", + "uni-datetime-picker.year": "-", + "uni-datetime-picker.month": "", + "uni-calender.MON": "MON", + "uni-calender.TUE": "TUE", + "uni-calender.WED": "WED", + "uni-calender.THU": "THU", + "uni-calender.FRI": "FRI", + "uni-calender.SAT": "SAT", + "uni-calender.SUN": "SUN", + "uni-calender.confirm": "confirm" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json new file mode 100644 index 0000000..d2df5e7 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hans.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "选择日期", + "uni-datetime-picker.selectTime": "选择时间", + "uni-datetime-picker.selectDateTime": "选择日期时间", + "uni-datetime-picker.startDate": "开始日期", + "uni-datetime-picker.endDate": "结束日期", + "uni-datetime-picker.startTime": "开始时间", + "uni-datetime-picker.endTime": "结束时间", + "uni-datetime-picker.ok": "确定", + "uni-datetime-picker.clear": "清除", + "uni-datetime-picker.cancel": "取消", + "uni-datetime-picker.year": "年", + "uni-datetime-picker.month": "月", + "uni-calender.SUN": "日", + "uni-calender.MON": "一", + "uni-calender.TUE": "二", + "uni-calender.WED": "三", + "uni-calender.THU": "四", + "uni-calender.FRI": "五", + "uni-calender.SAT": "六", + "uni-calender.confirm": "确认" +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json new file mode 100644 index 0000000..d23fa3c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/i18n/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "uni-datetime-picker.selectDate": "選擇日期", + "uni-datetime-picker.selectTime": "選擇時間", + "uni-datetime-picker.selectDateTime": "選擇日期時間", + "uni-datetime-picker.startDate": "開始日期", + "uni-datetime-picker.endDate": "結束日期", + "uni-datetime-picker.startTime": "開始时间", + "uni-datetime-picker.endTime": "結束时间", + "uni-datetime-picker.ok": "確定", + "uni-datetime-picker.clear": "清除", + "uni-datetime-picker.cancel": "取消", + "uni-datetime-picker.year": "年", + "uni-datetime-picker.month": "月", + "uni-calender.SUN": "日", + "uni-calender.MON": "一", + "uni-calender.TUE": "二", + "uni-calender.WED": "三", + "uni-calender.THU": "四", + "uni-calender.FRI": "五", + "uni-calender.SAT": "六", + "uni-calender.confirm": "確認" +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/keypress.js b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/keypress.js new file mode 100644 index 0000000..9601aba --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + this.$once('hook:beforeDestroy', () => { + document.removeEventListener('keyup', listener) + }) + }, + render: () => {} +} +// #endif \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue new file mode 100644 index 0000000..699aa63 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/time-picker.vue @@ -0,0 +1,927 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue new file mode 100644 index 0000000..9bdf8bc --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/uni-datetime-picker.vue @@ -0,0 +1,1012 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js new file mode 100644 index 0000000..efa5773 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/components/uni-datetime-picker/util.js @@ -0,0 +1,410 @@ +class Calendar { + constructor({ + date, + selected, + startDate, + endDate, + range, + // multipleStatus + } = {}) { + // 当前日期 + this.date = this.getDate(new Date()) // 当前初入日期 + // 打点信息 + this.selected = selected || []; + // 范围开始 + this.startDate = startDate + // 范围结束 + this.endDate = endDate + this.range = range + // 多选状态 + this.cleanMultipleStatus() + // 每周日期 + this.weeks = {} + // this._getWeek(this.date.fullDate) + // this.multipleStatus = multipleStatus + this.lastHover = false + } + /** + * 设置日期 + * @param {Object} date + */ + setDate(date) { + this.selectDate = this.getDate(date) + this._getWeek(this.selectDate.fullDate) + } + + /** + * 清理多选状态 + */ + cleanMultipleStatus() { + this.multipleStatus = { + before: '', + after: '', + data: [] + } + } + + /** + * 重置开始日期 + */ + resetSatrtDate(startDate) { + // 范围开始 + this.startDate = startDate + + } + + /** + * 重置结束日期 + */ + resetEndDate(endDate) { + // 范围结束 + this.endDate = endDate + } + + /** + * 获取任意时间 + */ + getDate(date, AddDayCount = 0, str = 'day') { + if (!date) { + date = new Date() + } + if (typeof date !== 'object') { + date = date.replace(/-/g, '/') + } + const dd = new Date(date) + switch (str) { + case 'day': + dd.setDate(dd.getDate() + AddDayCount) // 获取AddDayCount天后的日期 + break + case 'month': + if (dd.getDate() === 31) { + dd.setDate(dd.getDate() + AddDayCount) + } else { + dd.setMonth(dd.getMonth() + AddDayCount) // 获取AddDayCount天后的日期 + } + break + case 'year': + dd.setFullYear(dd.getFullYear() + AddDayCount) // 获取AddDayCount天后的日期 + break + } + const y = dd.getFullYear() + const m = dd.getMonth() + 1 < 10 ? '0' + (dd.getMonth() + 1) : dd.getMonth() + 1 // 获取当前月份的日期,不足10补0 + const d = dd.getDate() < 10 ? '0' + dd.getDate() : dd.getDate() // 获取当前几号,不足10补0 + return { + fullDate: y + '-' + m + '-' + d, + year: y, + month: m, + date: d, + day: dd.getDay() + } + } + + + /** + * 获取上月剩余天数 + */ + _getLastMonthDays(firstDay, full) { + let dateArr = [] + for (let i = firstDay; i > 0; i--) { + const beforeDate = new Date(full.year, full.month - 1, -i + 1).getDate() + dateArr.push({ + date: beforeDate, + month: full.month - 1, + disable: true + }) + } + return dateArr + } + /** + * 获取本月天数 + */ + _currentMonthDys(dateData, full) { + let dateArr = [] + let fullDate = this.date.fullDate + for (let i = 1; i <= dateData; i++) { + let isinfo = false + let nowDate = full.year + '-' + (full.month < 10 ? + full.month : full.month) + '-' + (i < 10 ? + '0' + i : i) + // 是否今天 + let isDay = fullDate === nowDate + // 获取打点信息 + let info = this.selected && this.selected.find((item) => { + if (this.dateEqual(nowDate, item.date)) { + return item + } + }) + + // 日期禁用 + let disableBefore = true + let disableAfter = true + if (this.startDate) { + // let dateCompBefore = this.dateCompare(this.startDate, fullDate) + // disableBefore = this.dateCompare(dateCompBefore ? this.startDate : fullDate, nowDate) + disableBefore = this.dateCompare(this.startDate, nowDate) + } + + if (this.endDate) { + // let dateCompAfter = this.dateCompare(fullDate, this.endDate) + // disableAfter = this.dateCompare(nowDate, dateCompAfter ? this.endDate : fullDate) + disableAfter = this.dateCompare(nowDate, this.endDate) + } + let multiples = this.multipleStatus.data + let checked = false + let multiplesStatus = -1 + if (this.range) { + if (multiples) { + multiplesStatus = multiples.findIndex((item) => { + return this.dateEqual(item, nowDate) + }) + } + if (multiplesStatus !== -1) { + checked = true + } + } + let data = { + fullDate: nowDate, + year: full.year, + date: i, + multiple: this.range ? checked : false, + beforeMultiple: this.isLogicBefore(nowDate, this.multipleStatus.before, this.multipleStatus.after), + afterMultiple: this.isLogicAfter(nowDate, this.multipleStatus.before, this.multipleStatus.after), + month: full.month, + disable: !(disableBefore && disableAfter), + isDay, + userChecked: false + } + if (info) { + data.extraInfo = info + } + + dateArr.push(data) + } + return dateArr + } + /** + * 获取下月天数 + */ + _getNextMonthDays(surplus, full) { + let dateArr = [] + for (let i = 1; i < surplus + 1; i++) { + dateArr.push({ + date: i, + month: Number(full.month) + 1, + disable: true + }) + } + return dateArr + } + + /** + * 获取当前日期详情 + * @param {Object} date + */ + getInfo(date) { + if (!date) { + date = new Date() + } + const dateInfo = this.canlender.find(item => item.fullDate === this.getDate(date).fullDate) + return dateInfo + } + + /** + * 比较时间大小 + */ + dateCompare(startDate, endDate) { + // 计算截止时间 + startDate = new Date(startDate.replace('-', '/').replace('-', '/')) + // 计算详细项的截止时间 + endDate = new Date(endDate.replace('-', '/').replace('-', '/')) + if (startDate <= endDate) { + return true + } else { + return false + } + } + + /** + * 比较时间是否相等 + */ + dateEqual(before, after) { + // 计算截止时间 + before = new Date(before.replace('-', '/').replace('-', '/')) + // 计算详细项的截止时间 + after = new Date(after.replace('-', '/').replace('-', '/')) + if (before.getTime() - after.getTime() === 0) { + return true + } else { + return false + } + } + + /** + * 比较真实起始日期 + */ + + isLogicBefore(currentDay, before, after) { + let logicBefore = before + if (before && after) { + logicBefore = this.dateCompare(before, after) ? before : after + } + return this.dateEqual(logicBefore, currentDay) + } + + isLogicAfter(currentDay, before, after) { + let logicAfter = after + if (before && after) { + logicAfter = this.dateCompare(before, after) ? after : before + } + return this.dateEqual(logicAfter, currentDay) + } + + /** + * 获取日期范围内所有日期 + * @param {Object} begin + * @param {Object} end + */ + geDateAll(begin, end) { + var arr = [] + var ab = begin.split('-') + var ae = end.split('-') + var db = new Date() + db.setFullYear(ab[0], ab[1] - 1, ab[2]) + var de = new Date() + de.setFullYear(ae[0], ae[1] - 1, ae[2]) + var unixDb = db.getTime() - 24 * 60 * 60 * 1000 + var unixDe = de.getTime() - 24 * 60 * 60 * 1000 + for (var k = unixDb; k <= unixDe;) { + k = k + 24 * 60 * 60 * 1000 + arr.push(this.getDate(new Date(parseInt(k))).fullDate) + } + return arr + } + + /** + * 获取多选状态 + */ + setMultiple(fullDate) { + let { + before, + after + } = this.multipleStatus + if (!this.range) return + if (before && after) { + if (!this.lastHover) { + this.lastHover = true + return + } + this.multipleStatus.before = fullDate + this.multipleStatus.after = '' + this.multipleStatus.data = [] + this.multipleStatus.fulldate = '' + this.lastHover = false + } else { + if (!before) { + this.multipleStatus.before = fullDate + this.lastHover = false + } else { + this.multipleStatus.after = fullDate + if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus + .after); + } else { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus + .before); + } + this.lastHover = true + } + } + this._getWeek(fullDate) + } + + /** + * 鼠标 hover 更新多选状态 + */ + setHoverMultiple(fullDate) { + let { + before, + after + } = this.multipleStatus + + if (!this.range) return + if (this.lastHover) return + + if (!before) { + this.multipleStatus.before = fullDate + } else { + this.multipleStatus.after = fullDate + if (this.dateCompare(this.multipleStatus.before, this.multipleStatus.after)) { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.before, this.multipleStatus.after); + } else { + this.multipleStatus.data = this.geDateAll(this.multipleStatus.after, this.multipleStatus.before); + } + } + this._getWeek(fullDate) + } + + /** + * 更新默认值多选状态 + */ + setDefaultMultiple(before, after) { + this.multipleStatus.before = before + this.multipleStatus.after = after + if (before && after) { + if (this.dateCompare(before, after)) { + this.multipleStatus.data = this.geDateAll(before, after); + this._getWeek(after) + } else { + this.multipleStatus.data = this.geDateAll(after, before); + this._getWeek(before) + } + } + } + + /** + * 获取每周数据 + * @param {Object} dateData + */ + _getWeek(dateData) { + const { + fullDate, + year, + month, + date, + day + } = this.getDate(dateData) + let firstDay = new Date(year, month - 1, 1).getDay() + let currentDay = new Date(year, month, 0).getDate() + let dates = { + lastMonthDays: this._getLastMonthDays(firstDay, this.getDate(dateData)), // 上个月末尾几天 + currentMonthDys: this._currentMonthDys(currentDay, this.getDate(dateData)), // 本月天数 + nextMonthDays: [], // 下个月开始几天 + weeks: [] + } + let canlender = [] + const surplus = 42 - (dates.lastMonthDays.length + dates.currentMonthDys.length) + dates.nextMonthDays = this._getNextMonthDays(surplus, this.getDate(dateData)) + canlender = canlender.concat(dates.lastMonthDays, dates.currentMonthDys, dates.nextMonthDays) + let weeks = {} + // 拼接数组 上个月开始几天 + 本月天数+ 下个月开始几天 + for (let i = 0; i < canlender.length; i++) { + if (i % 7 === 0) { + weeks[parseInt(i / 7)] = new Array(7) + } + weeks[parseInt(i / 7)][i % 7] = canlender[i] + } + this.canlender = canlender + this.weeks = weeks + } + + //静态方法 + // static init(date) { + // if (!this.instance) { + // this.instance = new Calendar(date); + // } + // return this.instance; + // } +} + + +export default Calendar diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/package.json new file mode 100644 index 0000000..60fa1d0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-datetime-picker", + "displayName": "uni-datetime-picker 日期选择器", + "version": "2.2.6", + "description": "uni-datetime-picker 日期时间选择器,支持日历,支持范围选择", + "keywords": [ + "uni-datetime-picker", + "uni-ui", + "uniui", + "日期时间选择器", + "日期时间" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/readme.md new file mode 100644 index 0000000..162fbef --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-datetime-picker/readme.md @@ -0,0 +1,21 @@ + + +> `重要通知:组件升级更新 2.0.0 后,支持日期+时间范围选择,组件 ui 将使用日历选择日期,ui 变化较大,同时支持 PC 和 移动端。此版本不向后兼容,不再支持单独的时间选择(type=time)及相关的 hide-second 属性(时间选可使用内置组件 picker)。若仍需使用旧版本,可在插件市场下载*非uni_modules版本*,旧版本将不再维护` + +## DatetimePicker 时间选择器 + +> **组件名:uni-datetime-picker** +> 代码块: `uDatetimePicker` + + +该组件的优势是,支持**时间戳**输入和输出(起始时间、终止时间也支持时间戳),可**同时选择**日期和时间。 + +若只是需要单独选择日期和时间,不需要时间戳输入和输出,可使用原生的 picker 组件。 + +**_点击 picker 默认值规则:_** + +- 若设置初始值 value, 会显示在 picker 显示框中 +- 若无初始值 value,则初始值 value 为当前本地时间 Date.now(), 但不会显示在 picker 显示框中 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-datetime-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/changelog.md new file mode 100644 index 0000000..6d2488c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/changelog.md @@ -0,0 +1,13 @@ +## 1.2.1(2021-11-22) +- 修复 vue3中个别scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-drawer](https://uniapp.dcloud.io/component/uniui/uni-drawer) +## 1.1.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-02-04) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/components/uni-drawer/keypress.js b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/components/uni-drawer/keypress.js new file mode 100644 index 0000000..62dda46 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/components/uni-drawer/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + // this.$once('hook:beforeDestroy', () => { + // document.removeEventListener('keyup', listener) + // }) + }, + render: () => {} +} +// #endif diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/components/uni-drawer/uni-drawer.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/components/uni-drawer/uni-drawer.vue new file mode 100644 index 0000000..82331a8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/components/uni-drawer/uni-drawer.vue @@ -0,0 +1,183 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/package.json new file mode 100644 index 0000000..dd056e4 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-drawer", + "displayName": "uni-drawer 抽屉", + "version": "1.2.1", + "description": "抽屉式导航,用于展示侧滑菜单,侧滑导航。", + "keywords": [ + "uni-ui", + "uniui", + "drawer", + "抽屉", + "侧滑导航" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/readme.md new file mode 100644 index 0000000..dcf6e6b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-drawer/readme.md @@ -0,0 +1,10 @@ + + +## Drawer 抽屉 +> **组件名:uni-drawer** +> 代码块: `uDrawer` + +抽屉侧滑菜单。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-drawer) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/changelog.md new file mode 100644 index 0000000..1e8c6f9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/changelog.md @@ -0,0 +1,47 @@ +## 1.1.0(2022-06-30) +- 新增 在 uni-forms 1.4.0 中使用可以在 blur 时校验内容 +- 新增 clear 事件,点击右侧叉号图标触发 +- 新增 change 事件 ,仅在输入框失去焦点或用户按下回车时触发 +- 优化 组件样式,组件获取焦点时高亮显示,图标颜色调整等 +- +## 1.0.5(2022-06-07) +- 优化 clearable 显示策略 +## 1.0.4(2022-06-07) +- 优化 clearable 显示策略 +## 1.0.3(2022-05-20) +- 修复 关闭图标某些情况下无法取消的bug +## 1.0.2(2022-04-12) +- 修复 默认值不生效的bug +## 1.0.1(2022-04-02) +- 修复 value不能为0的bug +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-easyinput](https://uniapp.dcloud.io/component/uniui/uni-easyinput) +## 0.1.4(2021-08-20) +- 修复 在 uni-forms 的动态表单中默认值校验不通过的 bug +## 0.1.3(2021-08-11) +- 修复 在 uni-forms 中重置表单,错误信息无法清除的问题 +## 0.1.2(2021-07-30) +- 优化 vue3下事件警告的问题 +## 0.1.1 +- 优化 errorMessage 属性支持 Boolean 类型 +## 0.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.0.16(2021-06-29) +- 修复 confirmType 属性(仅 type="text" 生效)导致多行文本框无法换行的 bug +## 0.0.15(2021-06-21) +- 修复 passwordIcon 属性拼写错误的 bug +## 0.0.14(2021-06-18) +- 新增 passwordIcon 属性,当type=password时是否显示小眼睛图标 +- 修复 confirmType 属性不生效的问题 +## 0.0.13(2021-06-04) +- 修复 disabled 状态可清出内容的 bug +## 0.0.12(2021-05-12) +- 新增 组件示例地址 +## 0.0.11(2021-05-07) +- 修复 input-border 属性不生效的问题 +## 0.0.10(2021-04-30) +- 修复 ios 遮挡文字、显示一半的问题 +## 0.0.9(2021-02-05) +- 调整为uni_modules目录规范 +- 优化 兼容 nvue 页面 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/common.js b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/common.js new file mode 100644 index 0000000..df9abe1 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/common.js @@ -0,0 +1,56 @@ +/** + * @desc 函数防抖 + * @param func 目标函数 + * @param wait 延迟执行毫秒数 + * @param immediate true - 立即执行, false - 延迟执行 + */ +export const debounce = function(func, wait = 1000, immediate = true) { + let timer; + console.log(1); + return function() { + console.log(123); + let context = this, + args = arguments; + if (timer) clearTimeout(timer); + if (immediate) { + let callNow = !timer; + timer = setTimeout(() => { + timer = null; + }, wait); + if (callNow) func.apply(context, args); + } else { + timer = setTimeout(() => { + func.apply(context, args); + }, wait) + } + } +} +/** + * @desc 函数节流 + * @param func 函数 + * @param wait 延迟执行毫秒数 + * @param type 1 使用表时间戳,在时间段开始的时候触发 2 使用表定时器,在时间段结束的时候触发 + */ +export const throttle = (func, wait = 1000, type = 1) => { + let previous = 0; + let timeout; + return function() { + let context = this; + let args = arguments; + if (type === 1) { + let now = Date.now(); + + if (now - previous > wait) { + func.apply(context, args); + previous = now; + } + } else if (type === 2) { + if (!timeout) { + timeout = setTimeout(() => { + timeout = null; + func.apply(context, args) + }, wait) + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue new file mode 100644 index 0000000..5818d7f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/components/uni-easyinput/uni-easyinput.vue @@ -0,0 +1,593 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/package.json new file mode 100644 index 0000000..3cc793e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-easyinput", + "displayName": "uni-easyinput 增强输入框", + "version": "1.1.0", + "description": "Easyinput 组件是对原生input组件的增强", + "keywords": [ + "uni-ui", + "uniui", + "input", + "uni-easyinput", + "输入框" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/readme.md new file mode 100644 index 0000000..f1faf8f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-easyinput/readme.md @@ -0,0 +1,11 @@ + + +### Easyinput 增强输入框 +> **组件名:uni-easyinput** +> 代码块: `uEasyinput` + + +easyinput 组件是对原生input组件的增强 ,是专门为配合表单组件[uni-forms](https://ext.dcloud.net.cn/plugin?id=2773)而设计的,easyinput 内置了边框,图标等,同时包含 input 所有功能 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-easyinput) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fab/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-fab/changelog.md new file mode 100644 index 0000000..24e26b1 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fab/changelog.md @@ -0,0 +1,17 @@ +## 1.2.2(2021-12-29) +- 更新 组件依赖 +## 1.2.1(2021-11-19) +- 修复 阴影颜色不正确的bug +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-fab](https://uniapp.dcloud.io/component/uniui/uni-fab) +## 1.1.1(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-02-05) +- 调整为uni_modules目录规范 +- 优化 按钮背景色调整 +- 优化 兼容pc端 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fab/components/uni-fab/uni-fab.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-fab/components/uni-fab/uni-fab.vue new file mode 100644 index 0000000..bef97f1 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fab/components/uni-fab/uni-fab.vue @@ -0,0 +1,475 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fab/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-fab/package.json new file mode 100644 index 0000000..0f27daa --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fab/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-fab", + "displayName": "uni-fab 悬浮按钮", + "version": "1.2.2", + "description": "悬浮按钮 fab button ,点击可展开一个图标按钮菜单。", + "keywords": [ + "uni-ui", + "uniui", + "按钮", + "悬浮按钮", + "fab" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-icons"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fab/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-fab/readme.md new file mode 100644 index 0000000..9a444e8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fab/readme.md @@ -0,0 +1,9 @@ +## Fab 悬浮按钮 +> **组件名:uni-fab** +> 代码块: `uFab` + + +点击可展开一个图形按钮菜单 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-fab) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fav/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/changelog.md new file mode 100644 index 0000000..d8a08d4 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/changelog.md @@ -0,0 +1,19 @@ +## 1.2.1(2022-05-30) +- 新增 stat 属性 ,是否开启uni统计功能 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-fav](https://uniapp.dcloud.io/component/uniui/uni-fav) +## 1.1.1(2021-08-24) +- 新增 支持国际化 +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.6(2021-05-12) +- 新增 组件示例地址 +## 1.0.5(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.4(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.0.3(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.0.2(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/en.json new file mode 100644 index 0000000..9a0759e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/en.json @@ -0,0 +1,4 @@ +{ + "uni-fav.collect": "collect", + "uni-fav.collected": "collected" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hans.json new file mode 100644 index 0000000..67c89bf --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hans.json @@ -0,0 +1,4 @@ +{ + "uni-fav.collect": "收藏", + "uni-fav.collected": "已收藏" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hant.json new file mode 100644 index 0000000..67c89bf --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/i18n/zh-Hant.json @@ -0,0 +1,4 @@ +{ + "uni-fav.collect": "收藏", + "uni-fav.collected": "已收藏" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/uni-fav.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/uni-fav.vue new file mode 100644 index 0000000..d2c58df --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/components/uni-fav/uni-fav.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fav/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/package.json new file mode 100644 index 0000000..cc14697 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-fav", + "displayName": "uni-fav 收藏按钮", + "version": "1.2.1", + "description": " Fav 收藏组件,可自定义颜色、大小。", + "keywords": [ + "fav", + "uni-ui", + "uniui", + "收藏" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-fav/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/readme.md new file mode 100644 index 0000000..4de125d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-fav/readme.md @@ -0,0 +1,10 @@ + + +## Fav 收藏按钮 +> **组件名:uni-fav** +> 代码块: `uFav` + +用于收藏功能,可点击切换选中、不选中的状态。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-fav) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/changelog.md new file mode 100644 index 0000000..5c81026 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/changelog.md @@ -0,0 +1,63 @@ +## 1.0.2(2022-07-04) +- 修复 在uni-forms下样式不生效的bug +## 1.0.1(2021-11-23) +- 修复 参数为对象的情况下,url在某些情况显示错误的bug +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-file-picker](https://uniapp.dcloud.io/component/uniui/uni-file-picker) +## 0.2.16(2021-11-08) +- 修复 传入空对象 ,显示错误的Bug +## 0.2.15(2021-08-30) +- 修复 return-type="object" 时且存在v-model时,无法删除文件的Bug +## 0.2.14(2021-08-23) +- 新增 参数中返回 fileID 字段 +## 0.2.13(2021-08-23) +- 修复 腾讯云传入fileID 不能回显的bug +- 修复 选择图片后,不能放大的问题 +## 0.2.12(2021-08-17) +- 修复 由于 0.2.11 版本引起的不能回显图片的Bug +## 0.2.11(2021-08-16) +- 新增 clearFiles(index) 方法,可以手动删除指定文件 +- 修复 v-model 值设为 null 报错的Bug +## 0.2.10(2021-08-13) +- 修复 return-type="object" 时,无法删除文件的Bug +## 0.2.9(2021-08-03) +- 修复 auto-upload 属性失效的Bug +## 0.2.8(2021-07-31) +- 修复 fileExtname属性不指定值报错的Bug +## 0.2.7(2021-07-31) +- 修复 在某种场景下图片不回显的Bug +## 0.2.6(2021-07-30) +- 修复 return-type为object下,返回值不正确的Bug +## 0.2.5(2021-07-30) +- 修复(重要) H5 平台下如果和uni-forms组件一同使用导致页面卡死的问题 +## 0.2.3(2021-07-28) +- 优化 调整示例代码 +## 0.2.2(2021-07-27) +- 修复 vue3 下赋值错误的Bug +- 优化 h5平台下上传文件导致页面卡死的问题 +## 0.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.1.1(2021-07-02) +- 修复 sourceType 缺少默认值导致 ios 无法选择文件 +## 0.1.0(2021-06-30) +- 优化 解耦与uniCloud的强绑定关系 ,如不绑定服务空间,默认autoUpload为false且不可更改 +## 0.0.11(2021-06-30) +- 修复 由 0.0.10 版本引发的 returnType 属性失效的问题 +## 0.0.10(2021-06-29) +- 优化 文件上传后进度条消失时机 +## 0.0.9(2021-06-29) +- 修复 在uni-forms 中,删除文件 ,获取的值不对的Bug +## 0.0.8(2021-06-15) +- 修复 删除文件时无法触发 v-model 的Bug +## 0.0.7(2021-05-12) +- 新增 组件示例地址 +## 0.0.6(2021-04-09) +- 修复 选择的文件非 file-extname 字段指定的扩展名报错的Bug +## 0.0.5(2021-04-09) +- 优化 更新组件示例 +## 0.0.4(2021-04-09) +- 优化 file-extname 字段支持字符串写法,多个扩展名需要用逗号分隔 +## 0.0.3(2021-02-05) +- 调整为uni_modules目录规范 +- 修复 微信小程序不指定 fileExtname 属性选择失败的Bug diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js new file mode 100644 index 0000000..24a07f5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/choose-and-upload-file.js @@ -0,0 +1,224 @@ +'use strict'; + +const ERR_MSG_OK = 'chooseAndUploadFile:ok'; +const ERR_MSG_FAIL = 'chooseAndUploadFile:fail'; + +function chooseImage(opts) { + const { + count, + sizeType = ['original', 'compressed'], + sourceType = ['album', 'camera'], + extension + } = opts + return new Promise((resolve, reject) => { + uni.chooseImage({ + count, + sizeType, + sourceType, + extension, + success(res) { + resolve(normalizeChooseAndUploadFileRes(res, 'image')); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseImage:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function chooseVideo(opts) { + const { + camera, + compressed, + maxDuration, + sourceType = ['album', 'camera'], + extension + } = opts; + return new Promise((resolve, reject) => { + uni.chooseVideo({ + camera, + compressed, + maxDuration, + sourceType, + extension, + success(res) { + const { + tempFilePath, + duration, + size, + height, + width + } = res; + resolve(normalizeChooseAndUploadFileRes({ + errMsg: 'chooseVideo:ok', + tempFilePaths: [tempFilePath], + tempFiles: [ + { + name: (res.tempFile && res.tempFile.name) || '', + path: tempFilePath, + size, + type: (res.tempFile && res.tempFile.type) || '', + width, + height, + duration, + fileType: 'video', + cloudPath: '', + }, ], + }, 'video')); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseVideo:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function chooseAll(opts) { + const { + count, + extension + } = opts; + return new Promise((resolve, reject) => { + let chooseFile = uni.chooseFile; + if (typeof wx !== 'undefined' && + typeof wx.chooseMessageFile === 'function') { + chooseFile = wx.chooseMessageFile; + } + if (typeof chooseFile !== 'function') { + return reject({ + errMsg: ERR_MSG_FAIL + ' 请指定 type 类型,该平台仅支持选择 image 或 video。', + }); + } + chooseFile({ + type: 'all', + count, + extension, + success(res) { + resolve(normalizeChooseAndUploadFileRes(res)); + }, + fail(res) { + reject({ + errMsg: res.errMsg.replace('chooseFile:fail', ERR_MSG_FAIL), + }); + }, + }); + }); +} + +function normalizeChooseAndUploadFileRes(res, fileType) { + res.tempFiles.forEach((item, index) => { + if (!item.name) { + item.name = item.path.substring(item.path.lastIndexOf('/') + 1); + } + if (fileType) { + item.fileType = fileType; + } + item.cloudPath = + Date.now() + '_' + index + item.name.substring(item.name.lastIndexOf('.')); + }); + if (!res.tempFilePaths) { + res.tempFilePaths = res.tempFiles.map((file) => file.path); + } + return res; +} + +function uploadCloudFiles(files, max = 5, onUploadProgress) { + files = JSON.parse(JSON.stringify(files)) + const len = files.length + let count = 0 + let self = this + return new Promise(resolve => { + while (count < max) { + next() + } + + function next() { + let cur = count++ + if (cur >= len) { + !files.find(item => !item.url && !item.errMsg) && resolve(files) + return + } + const fileItem = files[cur] + const index = self.files.findIndex(v => v.uuid === fileItem.uuid) + fileItem.url = '' + delete fileItem.errMsg + + uniCloud + .uploadFile({ + filePath: fileItem.path, + cloudPath: fileItem.cloudPath, + fileType: fileItem.fileType, + onUploadProgress: res => { + res.index = index + onUploadProgress && onUploadProgress(res) + } + }) + .then(res => { + fileItem.url = res.fileID + fileItem.index = index + if (cur < len) { + next() + } + }) + .catch(res => { + fileItem.errMsg = res.errMsg || res.message + fileItem.index = index + if (cur < len) { + next() + } + }) + } + }) +} + + + + + +function uploadFiles(choosePromise, { + onChooseFile, + onUploadProgress +}) { + return choosePromise + .then((res) => { + if (onChooseFile) { + const customChooseRes = onChooseFile(res); + if (typeof customChooseRes !== 'undefined') { + return Promise.resolve(customChooseRes).then((chooseRes) => typeof chooseRes === 'undefined' ? + res : chooseRes); + } + } + return res; + }) + .then((res) => { + if (res === false) { + return { + errMsg: ERR_MSG_OK, + tempFilePaths: [], + tempFiles: [], + }; + } + return res + }) +} + +function chooseAndUploadFile(opts = { + type: 'all' +}) { + if (opts.type === 'image') { + return uploadFiles(chooseImage(opts), opts); + } + else if (opts.type === 'video') { + return uploadFiles(chooseVideo(opts), opts); + } + return uploadFiles(chooseAll(opts), opts); +} + +export { + chooseAndUploadFile, + uploadCloudFiles +}; diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue new file mode 100644 index 0000000..0928a41 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/uni-file-picker.vue @@ -0,0 +1,656 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue new file mode 100644 index 0000000..625d92e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-file.vue @@ -0,0 +1,325 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue new file mode 100644 index 0000000..2a29bc2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/upload-image.vue @@ -0,0 +1,292 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/utils.js b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/utils.js new file mode 100644 index 0000000..60aaa3e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/components/uni-file-picker/utils.js @@ -0,0 +1,109 @@ +/** + * 获取文件名和后缀 + * @param {String} name + */ +export const get_file_ext = (name) => { + const last_len = name.lastIndexOf('.') + const len = name.length + return { + name: name.substring(0, last_len), + ext: name.substring(last_len + 1, len) + } +} + +/** + * 获取扩展名 + * @param {Array} fileExtname + */ +export const get_extname = (fileExtname) => { + if (!Array.isArray(fileExtname)) { + let extname = fileExtname.replace(/(\[|\])/g, '') + return extname.split(',') + } else { + return fileExtname + } + return [] +} + +/** + * 获取文件和检测是否可选 + */ +export const get_files_and_is_max = (res, _extname) => { + let filePaths = [] + let files = [] + if(!_extname || _extname.length === 0){ + return { + filePaths, + files + } + } + res.tempFiles.forEach(v => { + let fileFullName = get_file_ext(v.name) + const extname = fileFullName.ext.toLowerCase() + if (_extname.indexOf(extname) !== -1) { + files.push(v) + filePaths.push(v.path) + } + }) + if (files.length !== res.tempFiles.length) { + uni.showToast({ + title: `当前选择了${res.tempFiles.length}个文件 ,${res.tempFiles.length - files.length} 个文件格式不正确`, + icon: 'none', + duration: 5000 + }) + } + + return { + filePaths, + files + } +} + + +/** + * 获取图片信息 + * @param {Object} filepath + */ +export const get_file_info = (filepath) => { + return new Promise((resolve, reject) => { + uni.getImageInfo({ + src: filepath, + success(res) { + resolve(res) + }, + fail(err) { + reject(err) + } + }) + }) +} +/** + * 获取封装数据 + */ +export const get_file_data = async (files, type = 'image') => { + // 最终需要上传数据库的数据 + let fileFullName = get_file_ext(files.name) + const extname = fileFullName.ext.toLowerCase() + let filedata = { + name: files.name, + uuid: files.uuid, + extname: extname || '', + cloudPath: files.cloudPath, + fileType: files.fileType, + url: files.path || files.path, + size: files.size, //单位是字节 + image: {}, + path: files.path, + video: {} + } + if (type === 'image') { + const imageinfo = await get_file_info(files.path) + delete filedata.video + filedata.image.width = imageinfo.width + filedata.image.height = imageinfo.height + filedata.image.location = imageinfo.path + } else { + delete filedata.image + } + return filedata +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/package.json new file mode 100644 index 0000000..08bd66e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-file-picker", + "displayName": "uni-file-picker 文件选择上传", + "version": "1.0.2", + "description": "文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间", + "keywords": [ + "uni-ui", + "uniui", + "图片上传", + "文件上传" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/readme.md new file mode 100644 index 0000000..c8399a5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-file-picker/readme.md @@ -0,0 +1,11 @@ + +## FilePicker 文件选择上传 + +> **组件名:uni-file-picker** +> 代码块: `uFilePicker` + + +文件选择上传组件,可以选择图片、视频等任意文件并上传到当前绑定的服务空间 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-file-picker) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-forms/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/changelog.md new file mode 100644 index 0000000..5a4bb79 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/changelog.md @@ -0,0 +1,86 @@ +## 1.4.6(2022-07-13) +- 修复 model 需要校验的值没有声明对应字段时,导致第一次不触发校验的bug +## 1.4.5(2022-07-05) +- 新增 更多表单示例 +- 优化 子表单组件过期提示的问题 +- 优化 子表单组件uni-datetime-picker、uni-data-select、uni-data-picker的显示样式 +## 1.4.4(2022-07-04) +- 更新 删除组件日志 +## 1.4.3(2022-07-04) +- 修复 由 1.4.0 引发的 label 插槽不生效的bug +## 1.4.2(2022-07-04) +- 修复 子组件找不到 setValue 报错的bug +## 1.4.1(2022-07-04) +- 修复 uni-data-picker 在 uni-forms-item 中报错的bug +- 修复 uni-data-picker 在 uni-forms-item 中宽度不正确的bug +## 1.4.0(2022-06-30) +- 【重要】组件逻辑重构,部分用法用旧版本不兼容,请注意兼容问题 +- 【重要】组件使用 Provide/Inject 方式注入依赖,提供了自定义表单组件调用 uni-forms 校验表单的能力 +- 新增 model 属性,等同于原 value/modelValue 属性,旧属性即将废弃 +- 新增 validateTrigger 属性的 blur 值,仅 uni-easyinput 生效 +- 新增 onFieldChange 方法,可以对子表单进行校验,可替代binddata方法 +- 新增 子表单的 setRules 方法,配合自定义校验函数使用 +- 新增 uni-forms-item 的 setRules 方法,配置动态表单使用可动态更新校验规则 +- 优化 动态表单校验方式,废弃拼接name的方式 +## 1.3.3(2022-06-22) +- 修复 表单校验顺序无序问题 +## 1.3.2(2021-12-09) +- +## 1.3.1(2021-11-19) +- 修复 label 插槽不生效的bug +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-forms](https://uniapp.dcloud.io/component/uniui/uni-forms) +## 1.2.7(2021-08-13) +- 修复 没有添加校验规则的字段依然报错的Bug +## 1.2.6(2021-08-11) +- 修复 重置表单错误信息无法清除的问题 +## 1.2.5(2021-08-11) +- 优化 组件文档 +## 1.2.4(2021-08-11) +- 修复 表单验证只生效一次的问题 +## 1.2.3(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.2.2(2021-07-26) +- 修复 vue2 下条件编译导致destroyed生命周期失效的Bug +- 修复 1.2.1 引起的示例在小程序平台报错的Bug +## 1.2.1(2021-07-22) +- 修复 动态校验表单,默认值为空的情况下校验失效的Bug +- 修复 不指定name属性时,运行报错的Bug +- 优化 label默认宽度从65调整至70,使required为true且四字时不换行 +- 优化 组件示例,新增动态校验示例代码 +- 优化 组件文档,使用方式更清晰 +## 1.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.2(2021-06-25) +- 修复 pattern 属性在微信小程序平台无效的问题 +## 1.1.1(2021-06-22) +- 修复 validate-trigger属性为submit且err-show-type属性为toast时不能弹出的Bug +## 1.1.0(2021-06-22) +- 修复 只写setRules方法而导致校验不生效的Bug +- 修复 由上个办法引发的错误提示文字错位的Bug +## 1.0.48(2021-06-21) +- 修复 不设置 label 属性 ,无法设置label插槽的问题 +## 1.0.47(2021-06-21) +- 修复 不设置label属性,label-width属性不生效的bug +- 修复 setRules 方法与rules属性冲突的问题 +## 1.0.46(2021-06-04) +- 修复 动态删减数据导致报错的问题 +## 1.0.45(2021-06-04) +- 新增 modelValue 属性 ,value 即将废弃 +## 1.0.44(2021-06-02) +- 新增 uni-forms-item 可以设置单独的 rules +- 新增 validate 事件增加 keepitem 参数,可以选择那些字段不过滤 +- 优化 submit 事件重命名为 validate +## 1.0.43(2021-05-12) +- 新增 组件示例地址 +## 1.0.42(2021-04-30) +- 修复 自定义检验器失效的问题 +## 1.0.41(2021-03-05) +- 更新 校验器 +- 修复 表单规则设置类型为 number 的情况下,值为0校验失败的Bug +## 1.0.40(2021-03-04) +- 修复 动态显示uni-forms-item的情况下,submit 方法获取值错误的Bug +## 1.0.39(2021-02-05) +- 调整为uni_modules目录规范 +- 修复 校验器传入 int 等类型 ,返回String类型的Bug diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue new file mode 100644 index 0000000..250ed87 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms-item/uni-forms-item.vue @@ -0,0 +1,627 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/uni-forms.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/uni-forms.vue new file mode 100644 index 0000000..ed2f6d9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/uni-forms.vue @@ -0,0 +1,397 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/utils.js b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/utils.js new file mode 100644 index 0000000..6da2421 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/utils.js @@ -0,0 +1,293 @@ +/** + * 简单处理对象拷贝 + * @param {Obejct} 被拷贝对象 + * @@return {Object} 拷贝对象 + */ +export const deepCopy = (val) => { + return JSON.parse(JSON.stringify(val)) +} +/** + * 过滤数字类型 + * @param {String} format 数字类型 + * @@return {Boolean} 返回是否为数字类型 + */ +export const typeFilter = (format) => { + return format === 'int' || format === 'double' || format === 'number' || format === 'timestamp'; +} + +/** + * 把 value 转换成指定的类型,用于处理初始值,原因是初始值需要入库不能为 undefined + * @param {String} key 字段名 + * @param {any} value 字段值 + * @param {Object} rules 表单校验规则 + */ +export const getValue = (key, value, rules) => { + const isRuleNumType = rules.find(val => val.format && typeFilter(val.format)); + const isRuleBoolType = rules.find(val => (val.format && val.format === 'boolean') || val.format === 'bool'); + // 输入类型为 number + if (!!isRuleNumType) { + if (!value && value !== 0) { + value = null + } else { + value = isNumber(Number(value)) ? Number(value) : value + } + } + + // 输入类型为 boolean + if (!!isRuleBoolType) { + value = isBoolean(value) ? value : false + } + + return value; +} + +/** + * 获取表单数据 + * @param {String|Array} name 真实名称,需要使用 realName 获取 + * @param {Object} data 原始数据 + * @param {any} value 需要设置的值 + */ +export const setDataValue = (field, formdata, value) => { + formdata[field] = value + return value || '' +} + +/** + * 获取表单数据 + * @param {String|Array} field 真实名称,需要使用 realName 获取 + * @param {Object} data 原始数据 + */ +export const getDataValue = (field, data) => { + return objGet(data, field) +} + +/** + * 获取表单类型 + * @param {String|Array} field 真实名称,需要使用 realName 获取 + */ +export const getDataValueType = (field, data) => { + const value = getDataValue(field, data) + return { + type: type(value), + value + } +} + +/** + * 获取表单可用的真实name + * @param {String|Array} name 表单name + * @@return {String} 表单可用的真实name + */ +export const realName = (name, data = {}) => { + const base_name = _basePath(name) + if (typeof base_name === 'object' && Array.isArray(base_name) && base_name.length > 1) { + const realname = base_name.reduce((a, b) => a += `#${b}`, '_formdata_') + return realname + } + return base_name[0] || name +} + +/** + * 判断是否表单可用的真实name + * @param {String|Array} name 表单name + * @@return {String} 表单可用的真实name + */ +export const isRealName = (name) => { + const reg = /^_formdata_#*/ + return reg.test(name) +} + +/** + * 获取表单数据的原始格式 + * @@return {Object|Array} object 需要解析的数据 + */ +export const rawData = (object = {}, name) => { + let newData = JSON.parse(JSON.stringify(object)) + let formData = {} + for(let i in newData){ + let path = name2arr(i) + objSet(formData,path,newData[i]) + } + return formData +} + +/** + * 真实name还原为 array + * @param {*} name + */ +export const name2arr = (name) => { + let field = name.replace('_formdata_#', '') + field = field.split('#').map(v => (isNumber(v) ? Number(v) : v)) + return field +} + +/** + * 对象中设置值 + * @param {Object|Array} object 源数据 + * @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c'] + * @param {String} value 需要设置的值 + */ +export const objSet = (object, path, value) => { + if (typeof object !== 'object') return object; + _basePath(path).reduce((o, k, i, _) => { + if (i === _.length - 1) { + // 若遍历结束直接赋值 + o[k] = value + return null + } else if (k in o) { + // 若存在对应路径,则返回找到的对象,进行下一次遍历 + return o[k] + } else { + // 若不存在对应路径,则创建对应对象,若下一路径是数字,新对象赋值为空数组,否则赋值为空对象 + o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {} + return o[k] + } + }, object) + // 返回object + return object; +} + +// 处理 path, path有三种形式:'a[0].b.c'、'a.0.b.c' 和 ['a','0','b','c'],需要统一处理成数组,便于后续使用 +function _basePath(path) { + // 若是数组,则直接返回 + if (Array.isArray(path)) return path + // 若有 '[',']',则替换成将 '[' 替换成 '.',去掉 ']' + return path.replace(/\[/g, '.').replace(/\]/g, '').split('.') +} + +/** + * 从对象中获取值 + * @param {Object|Array} object 源数据 + * @param {String| Array} path 'a.b.c' 或 ['a',0,'b','c'] + * @param {String} defaultVal 如果无法从调用链中获取值的默认值 + */ +export const objGet = (object, path, defaultVal = 'undefined') => { + // 先将path处理成统一格式 + let newPath = _basePath(path) + // 递归处理,返回最后结果 + let val = newPath.reduce((o, k) => { + return (o || {})[k] + }, object); + return !val || val !== undefined ? val : defaultVal +} + + +/** + * 是否为 number 类型 + * @param {any} num 需要判断的值 + * @return {Boolean} 是否为 number + */ +export const isNumber = (num) => { + return !isNaN(Number(num)) +} + +/** + * 是否为 boolean 类型 + * @param {any} bool 需要判断的值 + * @return {Boolean} 是否为 boolean + */ +export const isBoolean = (bool) => { + return (typeof bool === 'boolean') +} +/** + * 是否有必填字段 + * @param {Object} rules 规则 + * @return {Boolean} 是否有必填字段 + */ +export const isRequiredField = (rules) => { + let isNoField = false; + for (let i = 0; i < rules.length; i++) { + const ruleData = rules[i]; + if (ruleData.required) { + isNoField = true; + break; + } + } + return isNoField; +} + + +/** + * 获取数据类型 + * @param {Any} obj 需要获取数据类型的值 + */ +export const type = (obj) => { + var class2type = {}; + + // 生成class2type映射 + "Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) { + class2type["[object " + item + "]"] = item.toLowerCase(); + }) + if (obj == null) { + return obj + ""; + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[Object.prototype.toString.call(obj)] || "object" : + typeof obj; +} + +/** + * 判断两个值是否相等 + * @param {any} a 值 + * @param {any} b 值 + * @return {Boolean} 是否相等 + */ +export const isEqual = (a, b) => { + //如果a和b本来就全等 + if (a === b) { + //判断是否为0和-0 + return a !== 0 || 1 / a === 1 / b; + } + //判断是否为null和undefined + if (a == null || b == null) { + return a === b; + } + //接下来判断a和b的数据类型 + var classNameA = toString.call(a), + classNameB = toString.call(b); + //如果数据类型不相等,则返回false + if (classNameA !== classNameB) { + return false; + } + //如果数据类型相等,再根据不同数据类型分别判断 + switch (classNameA) { + case '[object RegExp]': + case '[object String]': + //进行字符串转换比较 + return '' + a === '' + b; + case '[object Number]': + //进行数字转换比较,判断是否为NaN + if (+a !== +a) { + return +b !== +b; + } + //判断是否为0或-0 + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + return +a === +b; + } + //如果是对象类型 + if (classNameA == '[object Object]') { + //获取a和b的属性长度 + var propsA = Object.getOwnPropertyNames(a), + propsB = Object.getOwnPropertyNames(b); + if (propsA.length != propsB.length) { + return false; + } + for (var i = 0; i < propsA.length; i++) { + var propName = propsA[i]; + //如果对应属性对应值不相等,则返回false + if (a[propName] !== b[propName]) { + return false; + } + } + return true; + } + //如果是数组类型 + if (classNameA == '[object Array]') { + if (a.toString() == b.toString()) { + return true; + } + return false; + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/validate.js b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/validate.js new file mode 100644 index 0000000..1834c6c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/components/uni-forms/validate.js @@ -0,0 +1,486 @@ +var pattern = { + email: /^\S+?@\S+?\.\S+?$/, + idcard: /^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/, + url: new RegExp( + "^(?!mailto:)(?:(?:http|https|ftp)://|//)(?:\\S+(?::\\S*)?@)?(?:(?:(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}(?:\\.(?:[0-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))|(?:(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)(?:\\.(?:[a-z\\u00a1-\\uffff0-9]+-*)*[a-z\\u00a1-\\uffff0-9]+)*(?:\\.(?:[a-z\\u00a1-\\uffff]{2,})))|localhost)(?::\\d{2,5})?(?:(/|\\?|#)[^\\s]*)?$", + 'i') +}; + +const FORMAT_MAPPING = { + "int": 'integer', + "bool": 'boolean', + "double": 'number', + "long": 'number', + "password": 'string' + // "fileurls": 'array' +} + +function formatMessage(args, resources = '') { + var defaultMessage = ['label'] + defaultMessage.forEach((item) => { + if (args[item] === undefined) { + args[item] = '' + } + }) + + let str = resources + for (let key in args) { + let reg = new RegExp('{' + key + '}') + str = str.replace(reg, args[key]) + } + return str +} + +function isEmptyValue(value, type) { + if (value === undefined || value === null) { + return true; + } + + if (typeof value === 'string' && !value) { + return true; + } + + if (Array.isArray(value) && !value.length) { + return true; + } + + if (type === 'object' && !Object.keys(value).length) { + return true; + } + + return false; +} + +const types = { + integer(value) { + return types.number(value) && parseInt(value, 10) === value; + }, + string(value) { + return typeof value === 'string'; + }, + number(value) { + if (isNaN(value)) { + return false; + } + return typeof value === 'number'; + }, + "boolean": function(value) { + return typeof value === 'boolean'; + }, + "float": function(value) { + return types.number(value) && !types.integer(value); + }, + array(value) { + return Array.isArray(value); + }, + object(value) { + return typeof value === 'object' && !types.array(value); + }, + date(value) { + return value instanceof Date; + }, + timestamp(value) { + if (!this.integer(value) || Math.abs(value).toString().length > 16) { + return false + } + return true; + }, + file(value) { + return typeof value.url === 'string'; + }, + email(value) { + return typeof value === 'string' && !!value.match(pattern.email) && value.length < 255; + }, + url(value) { + return typeof value === 'string' && !!value.match(pattern.url); + }, + pattern(reg, value) { + try { + return new RegExp(reg).test(value); + } catch (e) { + return false; + } + }, + method(value) { + return typeof value === 'function'; + }, + idcard(value) { + return typeof value === 'string' && !!value.match(pattern.idcard); + }, + 'url-https'(value) { + return this.url(value) && value.startsWith('https://'); + }, + 'url-scheme'(value) { + return value.startsWith('://'); + }, + 'url-web'(value) { + return false; + } +} + +class RuleValidator { + + constructor(message) { + this._message = message + } + + async validateRule(fieldKey, fieldValue, value, data, allData) { + var result = null + + let rules = fieldValue.rules + + let hasRequired = rules.findIndex((item) => { + return item.required + }) + if (hasRequired < 0) { + if (value === null || value === undefined) { + return result + } + if (typeof value === 'string' && !value.length) { + return result + } + } + + var message = this._message + + if (rules === undefined) { + return message['default'] + } + + for (var i = 0; i < rules.length; i++) { + let rule = rules[i] + let vt = this._getValidateType(rule) + + Object.assign(rule, { + label: fieldValue.label || `["${fieldKey}"]` + }) + + if (RuleValidatorHelper[vt]) { + result = RuleValidatorHelper[vt](rule, value, message) + if (result != null) { + break + } + } + + if (rule.validateExpr) { + let now = Date.now() + let resultExpr = rule.validateExpr(value, allData, now) + if (resultExpr === false) { + result = this._getMessage(rule, rule.errorMessage || this._message['default']) + break + } + } + + if (rule.validateFunction) { + result = await this.validateFunction(rule, value, data, allData, vt) + if (result !== null) { + break + } + } + } + + if (result !== null) { + result = message.TAG + result + } + + return result + } + + async validateFunction(rule, value, data, allData, vt) { + let result = null + try { + let callbackMessage = null + const res = await rule.validateFunction(rule, value, allData || data, (message) => { + callbackMessage = message + }) + if (callbackMessage || (typeof res === 'string' && res) || res === false) { + result = this._getMessage(rule, callbackMessage || res, vt) + } + } catch (e) { + result = this._getMessage(rule, e.message, vt) + } + return result + } + + _getMessage(rule, message, vt) { + return formatMessage(rule, message || rule.errorMessage || this._message[vt] || message['default']) + } + + _getValidateType(rule) { + var result = '' + if (rule.required) { + result = 'required' + } else if (rule.format) { + result = 'format' + } else if (rule.arrayType) { + result = 'arrayTypeFormat' + } else if (rule.range) { + result = 'range' + } else if (rule.maximum !== undefined || rule.minimum !== undefined) { + result = 'rangeNumber' + } else if (rule.maxLength !== undefined || rule.minLength !== undefined) { + result = 'rangeLength' + } else if (rule.pattern) { + result = 'pattern' + } else if (rule.validateFunction) { + result = 'validateFunction' + } + return result + } +} + +const RuleValidatorHelper = { + required(rule, value, message) { + if (rule.required && isEmptyValue(value, rule.format || typeof value)) { + return formatMessage(rule, rule.errorMessage || message.required); + } + + return null + }, + + range(rule, value, message) { + const { + range, + errorMessage + } = rule; + + let list = new Array(range.length); + for (let i = 0; i < range.length; i++) { + const item = range[i]; + if (types.object(item) && item.value !== undefined) { + list[i] = item.value; + } else { + list[i] = item; + } + } + + let result = false + if (Array.isArray(value)) { + result = (new Set(value.concat(list)).size === list.length); + } else { + if (list.indexOf(value) > -1) { + result = true; + } + } + + if (!result) { + return formatMessage(rule, errorMessage || message['enum']); + } + + return null + }, + + rangeNumber(rule, value, message) { + if (!types.number(value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + let { + minimum, + maximum, + exclusiveMinimum, + exclusiveMaximum + } = rule; + let min = exclusiveMinimum ? value <= minimum : value < minimum; + let max = exclusiveMaximum ? value >= maximum : value > maximum; + + if (minimum !== undefined && min) { + return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMinimum ? + 'exclusiveMinimum' : 'minimum' + ]) + } else if (maximum !== undefined && max) { + return formatMessage(rule, rule.errorMessage || message['number'][exclusiveMaximum ? + 'exclusiveMaximum' : 'maximum' + ]) + } else if (minimum !== undefined && maximum !== undefined && (min || max)) { + return formatMessage(rule, rule.errorMessage || message['number'].range) + } + + return null + }, + + rangeLength(rule, value, message) { + if (!types.string(value) && !types.array(value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + let min = rule.minLength; + let max = rule.maxLength; + let val = value.length; + + if (min !== undefined && val < min) { + return formatMessage(rule, rule.errorMessage || message['length'].minLength) + } else if (max !== undefined && val > max) { + return formatMessage(rule, rule.errorMessage || message['length'].maxLength) + } else if (min !== undefined && max !== undefined && (val < min || val > max)) { + return formatMessage(rule, rule.errorMessage || message['length'].range) + } + + return null + }, + + pattern(rule, value, message) { + if (!types['pattern'](rule.pattern, value)) { + return formatMessage(rule, rule.errorMessage || message.pattern.mismatch); + } + + return null + }, + + format(rule, value, message) { + var customTypes = Object.keys(types); + var format = FORMAT_MAPPING[rule.format] ? FORMAT_MAPPING[rule.format] : (rule.format || rule.arrayType); + + if (customTypes.indexOf(format) > -1) { + if (!types[format](value)) { + return formatMessage(rule, rule.errorMessage || message.typeError); + } + } + + return null + }, + + arrayTypeFormat(rule, value, message) { + if (!Array.isArray(value)) { + return formatMessage(rule, rule.errorMessage || message.typeError); + } + + for (let i = 0; i < value.length; i++) { + const element = value[i]; + let formatResult = this.format(rule, element, message) + if (formatResult !== null) { + return formatResult + } + } + + return null + } +} + +class SchemaValidator extends RuleValidator { + + constructor(schema, options) { + super(SchemaValidator.message); + + this._schema = schema + this._options = options || null + } + + updateSchema(schema) { + this._schema = schema + } + + async validate(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidate(data, false, allData) + } + return result.length ? result[0] : null + } + + async validateAll(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidate(data, true, allData) + } + return result + } + + async validateUpdate(data, allData) { + let result = this._checkFieldInSchema(data) + if (!result) { + result = await this.invokeValidateUpdate(data, false, allData) + } + return result.length ? result[0] : null + } + + async invokeValidate(data, all, allData) { + let result = [] + let schema = this._schema + for (let key in schema) { + let value = schema[key] + let errorMessage = await this.validateRule(key, value, data[key], data, allData) + if (errorMessage != null) { + result.push({ + key, + errorMessage + }) + if (!all) break + } + } + return result + } + + async invokeValidateUpdate(data, all, allData) { + let result = [] + for (let key in data) { + let errorMessage = await this.validateRule(key, this._schema[key], data[key], data, allData) + if (errorMessage != null) { + result.push({ + key, + errorMessage + }) + if (!all) break + } + } + return result + } + + _checkFieldInSchema(data) { + var keys = Object.keys(data) + var keys2 = Object.keys(this._schema) + if (new Set(keys.concat(keys2)).size === keys2.length) { + return '' + } + + var noExistFields = keys.filter((key) => { + return keys2.indexOf(key) < 0; + }) + var errorMessage = formatMessage({ + field: JSON.stringify(noExistFields) + }, SchemaValidator.message.TAG + SchemaValidator.message['defaultInvalid']) + return [{ + key: 'invalid', + errorMessage + }] + } +} + +function Message() { + return { + TAG: "", + default: '验证错误', + defaultInvalid: '提交的字段{field}在数据库中并不存在', + validateFunction: '验证无效', + required: '{label}必填', + 'enum': '{label}超出范围', + timestamp: '{label}格式无效', + whitespace: '{label}不能为空', + typeError: '{label}类型无效', + date: { + format: '{label}日期{value}格式无效', + parse: '{label}日期无法解析,{value}无效', + invalid: '{label}日期{value}无效' + }, + length: { + minLength: '{label}长度不能少于{minLength}', + maxLength: '{label}长度不能超过{maxLength}', + range: '{label}必须介于{minLength}和{maxLength}之间' + }, + number: { + minimum: '{label}不能小于{minimum}', + maximum: '{label}不能大于{maximum}', + exclusiveMinimum: '{label}不能小于等于{minimum}', + exclusiveMaximum: '{label}不能大于等于{maximum}', + range: '{label}必须介于{minimum}and{maximum}之间' + }, + pattern: { + mismatch: '{label}格式不匹配' + } + }; +} + + +SchemaValidator.message = new Message(); + +export default SchemaValidator diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-forms/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/package.json new file mode 100644 index 0000000..e69d39b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/package.json @@ -0,0 +1,91 @@ +{ + "id": "uni-forms", + "displayName": "uni-forms 表单", + "version": "1.4.6", + "description": "由输入框、选择器、单选框、多选框等控件组成,用以收集、校验、提交数据", + "keywords": [ + "uni-ui", + "表单", + "校验", + "表单校验", + "表单验证" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-forms/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/readme.md new file mode 100644 index 0000000..63d5a04 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-forms/readme.md @@ -0,0 +1,23 @@ + + +## Forms 表单 + +> **组件名:uni-forms** +> 代码块: `uForms`、`uni-forms-item` +> 关联组件:`uni-forms-item`、`uni-easyinput`、`uni-data-checkbox`、`uni-group`。 + + +uni-app的内置组件已经有了 `
`组件,用于提交表单内容。 + +然而几乎每个表单都需要做表单验证,为了方便做表单验证,减少重复开发,`uni ui` 又基于 ``组件封装了 ``组件,内置了表单验证功能。 + +`` 提供了 `rules`属性来描述校验规则、``子组件来包裹具体的表单项,以及给原生或三方组件提供了 `binddata()` 来设置表单值。 + +每个要校验的表单项,不管input还是checkbox,都必须放在``组件中,且一个``组件只能放置一个表单项。 + +``组件内部预留了显示error message的区域,默认是在表单项的底部。 + +另外,``组件下面的各个表单项,可以通过``包裹为不同的分组。同一``下的不同表单项目将聚拢在一起,同其他group保持垂直间距。``仅影响视觉效果。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-forms) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/changelog.md new file mode 100644 index 0000000..c6264c6 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/changelog.md @@ -0,0 +1,18 @@ +## 1.2.1(2022-05-30) +- 新增 stat属性,是否开启uni统计功能 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-goods-nav](https://uniapp.dcloud.io/component/uniui/uni-goods-nav) +## 1.1.1(2021-08-24) +- 新增 支持国际化 +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.5(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/en.json new file mode 100644 index 0000000..dcdba41 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/en.json @@ -0,0 +1,6 @@ +{ + "uni-goods-nav.options.shop": "shop", + "uni-goods-nav.options.cart": "cart", + "uni-goods-nav.buttonGroup.addToCart": "add to cart", + "uni-goods-nav.buttonGroup.buyNow": "buy now" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hans.json new file mode 100644 index 0000000..48ee344 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hans.json @@ -0,0 +1,6 @@ +{ + "uni-goods-nav.options.shop": "店铺", + "uni-goods-nav.options.cart": "购物车", + "uni-goods-nav.buttonGroup.addToCart": "加入购物车", + "uni-goods-nav.buttonGroup.buyNow": "立即购买" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hant.json new file mode 100644 index 0000000..d0a0255 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/i18n/zh-Hant.json @@ -0,0 +1,6 @@ +{ + "uni-goods-nav.options.shop": "店鋪", + "uni-goods-nav.options.cart": "購物車", + "uni-goods-nav.buttonGroup.addToCart": "加入購物車", + "uni-goods-nav.buttonGroup.buyNow": "立即購買" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/uni-goods-nav.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/uni-goods-nav.vue new file mode 100644 index 0000000..8a16b17 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/components/uni-goods-nav/uni-goods-nav.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/package.json new file mode 100644 index 0000000..636e45e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-goods-nav", + "displayName": "uni-goods-nav 商品导航", + "version": "1.2.1", + "description": "商品导航组件主要用于电商类应用底部导航,可自定义加入购物车,购买等操作", + "keywords": [ + "uni-ui", + "uniui", + "商品导航" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/readme.md new file mode 100644 index 0000000..07df93f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-goods-nav/readme.md @@ -0,0 +1,10 @@ + + +## GoodsNav 商品导航 +> **组件名:uni-goods-nav** +> 代码块: `uGoodsNav` + +商品加入购物车,立即购买等。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-goods-nav) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-grid/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/changelog.md new file mode 100644 index 0000000..d301166 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/changelog.md @@ -0,0 +1,13 @@ +## 1.4.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-grid](https://uniapp.dcloud.io/component/uniui/uni-grid) +## 1.3.2(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.3.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.3.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.2.4(2021-05-12) +- 新增 组件示例地址 +## 1.2.3(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-grid/components/uni-grid-item/uni-grid-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/components/uni-grid-item/uni-grid-item.vue new file mode 100644 index 0000000..20fd54e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/components/uni-grid-item/uni-grid-item.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-grid/components/uni-grid/uni-grid.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/components/uni-grid/uni-grid.vue new file mode 100644 index 0000000..96a412f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/components/uni-grid/uni-grid.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-grid/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/package.json new file mode 100644 index 0000000..ccb2c91 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-grid", + "displayName": "uni-grid 宫格", + "version": "1.4.0", + "description": "Grid 宫格组件,提供移动端常见的宫格布局,如九宫格。", + "keywords": [ + "uni-ui", + "uniui", + "九宫格", + "表格" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-icons"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-grid/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/readme.md new file mode 100644 index 0000000..0aa44cc --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-grid/readme.md @@ -0,0 +1,11 @@ + + +## Grid 宫格 +> **组件名:uni-grid** +> 代码块: `uGrid` + + +宫格组件。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-grid) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-group/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-group/changelog.md new file mode 100644 index 0000000..a7024fd --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-group/changelog.md @@ -0,0 +1,16 @@ +## 1.2.2(2022-05-30) +- 新增 stat属性,是否开启uni统计功能 +## 1.2.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-group](https://uniapp.dcloud.io/component/uniui/uni-group) +## 1.1.7(2021-11-08) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +- 优化 组件文档 +## 1.0.3(2021-05-12) +- 新增 组件示例地址 +## 1.0.2(2021-02-05) +- 调整为uni_modules目录规范 +- 优化 兼容 nvue 页面 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-group/components/uni-group/uni-group.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-group/components/uni-group/uni-group.vue new file mode 100644 index 0000000..3425ecd --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-group/components/uni-group/uni-group.vue @@ -0,0 +1,134 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-group/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-group/package.json new file mode 100644 index 0000000..ea00a08 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-group/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-group", + "displayName": "uni-group 分组", + "version": "1.2.2", + "description": "分组组件可用于将组件用于分组,添加间隔,以产生明显的区块", + "keywords": [ + "uni-ui", + "uniui", + "group", + "分组", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-group/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-group/readme.md new file mode 100644 index 0000000..bae67f4 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-group/readme.md @@ -0,0 +1,9 @@ + +## Group 分组 +> **组件名:uni-group** +> 代码块: `uGroup` + +分组组件可用于将组件分组,添加间隔,以产生明显的区块。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-group) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-icons/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/changelog.md new file mode 100644 index 0000000..6449885 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/changelog.md @@ -0,0 +1,22 @@ +## 1.3.5(2022-01-24) +- 优化 size 属性可以传入不带单位的字符串数值 +## 1.3.4(2022-01-24) +- 优化 size 支持其他单位 +## 1.3.3(2022-01-17) +- 修复 nvue 有些图标不显示的bug,兼容老版本图标 +## 1.3.2(2021-12-01) +- 优化 示例可复制图标名称 +## 1.3.1(2021-11-23) +- 优化 兼容旧组件 type 值 +## 1.3.0(2021-11-19) +- 新增 更多图标 +- 优化 自定义图标使用方式 +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons) +## 1.1.7(2021-11-08) +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.5(2021-05-12) +- 新增 组件示例地址 +## 1.1.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/icons.js b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/icons.js new file mode 100644 index 0000000..7889936 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/icons.js @@ -0,0 +1,1169 @@ +export default { + "id": "2852637", + "name": "uniui图标库", + "font_family": "uniicons", + "css_prefix_text": "uniui-", + "description": "", + "glyphs": [ + { + "icon_id": "25027049", + "name": "yanse", + "font_class": "color", + "unicode": "e6cf", + "unicode_decimal": 59087 + }, + { + "icon_id": "25027048", + "name": "wallet", + "font_class": "wallet", + "unicode": "e6b1", + "unicode_decimal": 59057 + }, + { + "icon_id": "25015720", + "name": "settings-filled", + "font_class": "settings-filled", + "unicode": "e6ce", + "unicode_decimal": 59086 + }, + { + "icon_id": "25015434", + "name": "shimingrenzheng-filled", + "font_class": "auth-filled", + "unicode": "e6cc", + "unicode_decimal": 59084 + }, + { + "icon_id": "24934246", + "name": "shop-filled", + "font_class": "shop-filled", + "unicode": "e6cd", + "unicode_decimal": 59085 + }, + { + "icon_id": "24934159", + "name": "staff-filled-01", + "font_class": "staff-filled", + "unicode": "e6cb", + "unicode_decimal": 59083 + }, + { + "icon_id": "24932461", + "name": "VIP-filled", + "font_class": "vip-filled", + "unicode": "e6c6", + "unicode_decimal": 59078 + }, + { + "icon_id": "24932462", + "name": "plus_circle_fill", + "font_class": "plus-filled", + "unicode": "e6c7", + "unicode_decimal": 59079 + }, + { + "icon_id": "24932463", + "name": "folder_add-filled", + "font_class": "folder-add-filled", + "unicode": "e6c8", + "unicode_decimal": 59080 + }, + { + "icon_id": "24932464", + "name": "yanse-filled", + "font_class": "color-filled", + "unicode": "e6c9", + "unicode_decimal": 59081 + }, + { + "icon_id": "24932465", + "name": "tune-filled", + "font_class": "tune-filled", + "unicode": "e6ca", + "unicode_decimal": 59082 + }, + { + "icon_id": "24932455", + "name": "a-rilidaka-filled", + "font_class": "calendar-filled", + "unicode": "e6c0", + "unicode_decimal": 59072 + }, + { + "icon_id": "24932456", + "name": "notification-filled", + "font_class": "notification-filled", + "unicode": "e6c1", + "unicode_decimal": 59073 + }, + { + "icon_id": "24932457", + "name": "wallet-filled", + "font_class": "wallet-filled", + "unicode": "e6c2", + "unicode_decimal": 59074 + }, + { + "icon_id": "24932458", + "name": "paihangbang-filled", + "font_class": "medal-filled", + "unicode": "e6c3", + "unicode_decimal": 59075 + }, + { + "icon_id": "24932459", + "name": "gift-filled", + "font_class": "gift-filled", + "unicode": "e6c4", + "unicode_decimal": 59076 + }, + { + "icon_id": "24932460", + "name": "fire-filled", + "font_class": "fire-filled", + "unicode": "e6c5", + "unicode_decimal": 59077 + }, + { + "icon_id": "24928001", + "name": "refreshempty", + "font_class": "refreshempty", + "unicode": "e6bf", + "unicode_decimal": 59071 + }, + { + "icon_id": "24926853", + "name": "location-ellipse", + "font_class": "location-filled", + "unicode": "e6af", + "unicode_decimal": 59055 + }, + { + "icon_id": "24926735", + "name": "person-filled", + "font_class": "person-filled", + "unicode": "e69d", + "unicode_decimal": 59037 + }, + { + "icon_id": "24926703", + "name": "personadd-filled", + "font_class": "personadd-filled", + "unicode": "e698", + "unicode_decimal": 59032 + }, + { + "icon_id": "24923351", + "name": "back", + "font_class": "back", + "unicode": "e6b9", + "unicode_decimal": 59065 + }, + { + "icon_id": "24923352", + "name": "forward", + "font_class": "forward", + "unicode": "e6ba", + "unicode_decimal": 59066 + }, + { + "icon_id": "24923353", + "name": "arrowthinright", + "font_class": "arrow-right", + "unicode": "e6bb", + "unicode_decimal": 59067 + }, + { + "icon_id": "24923353", + "name": "arrowthinright", + "font_class": "arrowthinright", + "unicode": "e6bb", + "unicode_decimal": 59067 + }, + { + "icon_id": "24923354", + "name": "arrowthinleft", + "font_class": "arrow-left", + "unicode": "e6bc", + "unicode_decimal": 59068 + }, + { + "icon_id": "24923354", + "name": "arrowthinleft", + "font_class": "arrowthinleft", + "unicode": "e6bc", + "unicode_decimal": 59068 + }, + { + "icon_id": "24923355", + "name": "arrowthinup", + "font_class": "arrow-up", + "unicode": "e6bd", + "unicode_decimal": 59069 + }, + { + "icon_id": "24923355", + "name": "arrowthinup", + "font_class": "arrowthinup", + "unicode": "e6bd", + "unicode_decimal": 59069 + }, + { + "icon_id": "24923356", + "name": "arrowthindown", + "font_class": "arrow-down", + "unicode": "e6be", + "unicode_decimal": 59070 + },{ + "icon_id": "24923356", + "name": "arrowthindown", + "font_class": "arrowthindown", + "unicode": "e6be", + "unicode_decimal": 59070 + }, + { + "icon_id": "24923349", + "name": "arrowdown", + "font_class": "bottom", + "unicode": "e6b8", + "unicode_decimal": 59064 + },{ + "icon_id": "24923349", + "name": "arrowdown", + "font_class": "arrowdown", + "unicode": "e6b8", + "unicode_decimal": 59064 + }, + { + "icon_id": "24923346", + "name": "arrowright", + "font_class": "right", + "unicode": "e6b5", + "unicode_decimal": 59061 + }, + { + "icon_id": "24923346", + "name": "arrowright", + "font_class": "arrowright", + "unicode": "e6b5", + "unicode_decimal": 59061 + }, + { + "icon_id": "24923347", + "name": "arrowup", + "font_class": "top", + "unicode": "e6b6", + "unicode_decimal": 59062 + }, + { + "icon_id": "24923347", + "name": "arrowup", + "font_class": "arrowup", + "unicode": "e6b6", + "unicode_decimal": 59062 + }, + { + "icon_id": "24923348", + "name": "arrowleft", + "font_class": "left", + "unicode": "e6b7", + "unicode_decimal": 59063 + }, + { + "icon_id": "24923348", + "name": "arrowleft", + "font_class": "arrowleft", + "unicode": "e6b7", + "unicode_decimal": 59063 + }, + { + "icon_id": "24923334", + "name": "eye", + "font_class": "eye", + "unicode": "e651", + "unicode_decimal": 58961 + }, + { + "icon_id": "24923335", + "name": "eye-filled", + "font_class": "eye-filled", + "unicode": "e66a", + "unicode_decimal": 58986 + }, + { + "icon_id": "24923336", + "name": "eye-slash", + "font_class": "eye-slash", + "unicode": "e6b3", + "unicode_decimal": 59059 + }, + { + "icon_id": "24923337", + "name": "eye-slash-filled", + "font_class": "eye-slash-filled", + "unicode": "e6b4", + "unicode_decimal": 59060 + }, + { + "icon_id": "24923305", + "name": "info-filled", + "font_class": "info-filled", + "unicode": "e649", + "unicode_decimal": 58953 + }, + { + "icon_id": "24923299", + "name": "reload-01", + "font_class": "reload", + "unicode": "e6b2", + "unicode_decimal": 59058 + }, + { + "icon_id": "24923195", + "name": "mic_slash_fill", + "font_class": "micoff-filled", + "unicode": "e6b0", + "unicode_decimal": 59056 + }, + { + "icon_id": "24923165", + "name": "map-pin-ellipse", + "font_class": "map-pin-ellipse", + "unicode": "e6ac", + "unicode_decimal": 59052 + }, + { + "icon_id": "24923166", + "name": "map-pin", + "font_class": "map-pin", + "unicode": "e6ad", + "unicode_decimal": 59053 + }, + { + "icon_id": "24923167", + "name": "location", + "font_class": "location", + "unicode": "e6ae", + "unicode_decimal": 59054 + }, + { + "icon_id": "24923064", + "name": "starhalf", + "font_class": "starhalf", + "unicode": "e683", + "unicode_decimal": 59011 + }, + { + "icon_id": "24923065", + "name": "star", + "font_class": "star", + "unicode": "e688", + "unicode_decimal": 59016 + }, + { + "icon_id": "24923066", + "name": "star-filled", + "font_class": "star-filled", + "unicode": "e68f", + "unicode_decimal": 59023 + }, + { + "icon_id": "24899646", + "name": "a-rilidaka", + "font_class": "calendar", + "unicode": "e6a0", + "unicode_decimal": 59040 + }, + { + "icon_id": "24899647", + "name": "fire", + "font_class": "fire", + "unicode": "e6a1", + "unicode_decimal": 59041 + }, + { + "icon_id": "24899648", + "name": "paihangbang", + "font_class": "medal", + "unicode": "e6a2", + "unicode_decimal": 59042 + }, + { + "icon_id": "24899649", + "name": "font", + "font_class": "font", + "unicode": "e6a3", + "unicode_decimal": 59043 + }, + { + "icon_id": "24899650", + "name": "gift", + "font_class": "gift", + "unicode": "e6a4", + "unicode_decimal": 59044 + }, + { + "icon_id": "24899651", + "name": "link", + "font_class": "link", + "unicode": "e6a5", + "unicode_decimal": 59045 + }, + { + "icon_id": "24899652", + "name": "notification", + "font_class": "notification", + "unicode": "e6a6", + "unicode_decimal": 59046 + }, + { + "icon_id": "24899653", + "name": "staff", + "font_class": "staff", + "unicode": "e6a7", + "unicode_decimal": 59047 + }, + { + "icon_id": "24899654", + "name": "VIP", + "font_class": "vip", + "unicode": "e6a8", + "unicode_decimal": 59048 + }, + { + "icon_id": "24899655", + "name": "folder_add", + "font_class": "folder-add", + "unicode": "e6a9", + "unicode_decimal": 59049 + }, + { + "icon_id": "24899656", + "name": "tune", + "font_class": "tune", + "unicode": "e6aa", + "unicode_decimal": 59050 + }, + { + "icon_id": "24899657", + "name": "shimingrenzheng", + "font_class": "auth", + "unicode": "e6ab", + "unicode_decimal": 59051 + }, + { + "icon_id": "24899565", + "name": "person", + "font_class": "person", + "unicode": "e699", + "unicode_decimal": 59033 + }, + { + "icon_id": "24899566", + "name": "email-filled", + "font_class": "email-filled", + "unicode": "e69a", + "unicode_decimal": 59034 + }, + { + "icon_id": "24899567", + "name": "phone-filled", + "font_class": "phone-filled", + "unicode": "e69b", + "unicode_decimal": 59035 + }, + { + "icon_id": "24899568", + "name": "phone", + "font_class": "phone", + "unicode": "e69c", + "unicode_decimal": 59036 + }, + { + "icon_id": "24899570", + "name": "email", + "font_class": "email", + "unicode": "e69e", + "unicode_decimal": 59038 + }, + { + "icon_id": "24899571", + "name": "personadd", + "font_class": "personadd", + "unicode": "e69f", + "unicode_decimal": 59039 + }, + { + "icon_id": "24899558", + "name": "chatboxes-filled", + "font_class": "chatboxes-filled", + "unicode": "e692", + "unicode_decimal": 59026 + }, + { + "icon_id": "24899559", + "name": "contact", + "font_class": "contact", + "unicode": "e693", + "unicode_decimal": 59027 + }, + { + "icon_id": "24899560", + "name": "chatbubble-filled", + "font_class": "chatbubble-filled", + "unicode": "e694", + "unicode_decimal": 59028 + }, + { + "icon_id": "24899561", + "name": "contact-filled", + "font_class": "contact-filled", + "unicode": "e695", + "unicode_decimal": 59029 + }, + { + "icon_id": "24899562", + "name": "chatboxes", + "font_class": "chatboxes", + "unicode": "e696", + "unicode_decimal": 59030 + }, + { + "icon_id": "24899563", + "name": "chatbubble", + "font_class": "chatbubble", + "unicode": "e697", + "unicode_decimal": 59031 + }, + { + "icon_id": "24881290", + "name": "upload-filled", + "font_class": "upload-filled", + "unicode": "e68e", + "unicode_decimal": 59022 + }, + { + "icon_id": "24881292", + "name": "upload", + "font_class": "upload", + "unicode": "e690", + "unicode_decimal": 59024 + }, + { + "icon_id": "24881293", + "name": "weixin", + "font_class": "weixin", + "unicode": "e691", + "unicode_decimal": 59025 + }, + { + "icon_id": "24881274", + "name": "compose", + "font_class": "compose", + "unicode": "e67f", + "unicode_decimal": 59007 + }, + { + "icon_id": "24881275", + "name": "qq", + "font_class": "qq", + "unicode": "e680", + "unicode_decimal": 59008 + }, + { + "icon_id": "24881276", + "name": "download-filled", + "font_class": "download-filled", + "unicode": "e681", + "unicode_decimal": 59009 + }, + { + "icon_id": "24881277", + "name": "pengyouquan", + "font_class": "pyq", + "unicode": "e682", + "unicode_decimal": 59010 + }, + { + "icon_id": "24881279", + "name": "sound", + "font_class": "sound", + "unicode": "e684", + "unicode_decimal": 59012 + }, + { + "icon_id": "24881280", + "name": "trash-filled", + "font_class": "trash-filled", + "unicode": "e685", + "unicode_decimal": 59013 + }, + { + "icon_id": "24881281", + "name": "sound-filled", + "font_class": "sound-filled", + "unicode": "e686", + "unicode_decimal": 59014 + }, + { + "icon_id": "24881282", + "name": "trash", + "font_class": "trash", + "unicode": "e687", + "unicode_decimal": 59015 + }, + { + "icon_id": "24881284", + "name": "videocam-filled", + "font_class": "videocam-filled", + "unicode": "e689", + "unicode_decimal": 59017 + }, + { + "icon_id": "24881285", + "name": "spinner-cycle", + "font_class": "spinner-cycle", + "unicode": "e68a", + "unicode_decimal": 59018 + }, + { + "icon_id": "24881286", + "name": "weibo", + "font_class": "weibo", + "unicode": "e68b", + "unicode_decimal": 59019 + }, + { + "icon_id": "24881288", + "name": "videocam", + "font_class": "videocam", + "unicode": "e68c", + "unicode_decimal": 59020 + }, + { + "icon_id": "24881289", + "name": "download", + "font_class": "download", + "unicode": "e68d", + "unicode_decimal": 59021 + }, + { + "icon_id": "24879601", + "name": "help", + "font_class": "help", + "unicode": "e679", + "unicode_decimal": 59001 + }, + { + "icon_id": "24879602", + "name": "navigate-filled", + "font_class": "navigate-filled", + "unicode": "e67a", + "unicode_decimal": 59002 + }, + { + "icon_id": "24879603", + "name": "plusempty", + "font_class": "plusempty", + "unicode": "e67b", + "unicode_decimal": 59003 + }, + { + "icon_id": "24879604", + "name": "smallcircle", + "font_class": "smallcircle", + "unicode": "e67c", + "unicode_decimal": 59004 + }, + { + "icon_id": "24879605", + "name": "minus-filled", + "font_class": "minus-filled", + "unicode": "e67d", + "unicode_decimal": 59005 + }, + { + "icon_id": "24879606", + "name": "micoff", + "font_class": "micoff", + "unicode": "e67e", + "unicode_decimal": 59006 + }, + { + "icon_id": "24879588", + "name": "closeempty", + "font_class": "closeempty", + "unicode": "e66c", + "unicode_decimal": 58988 + }, + { + "icon_id": "24879589", + "name": "clear", + "font_class": "clear", + "unicode": "e66d", + "unicode_decimal": 58989 + }, + { + "icon_id": "24879590", + "name": "navigate", + "font_class": "navigate", + "unicode": "e66e", + "unicode_decimal": 58990 + }, + { + "icon_id": "24879591", + "name": "minus", + "font_class": "minus", + "unicode": "e66f", + "unicode_decimal": 58991 + }, + { + "icon_id": "24879592", + "name": "image", + "font_class": "image", + "unicode": "e670", + "unicode_decimal": 58992 + }, + { + "icon_id": "24879593", + "name": "mic", + "font_class": "mic", + "unicode": "e671", + "unicode_decimal": 58993 + }, + { + "icon_id": "24879594", + "name": "paperplane", + "font_class": "paperplane", + "unicode": "e672", + "unicode_decimal": 58994 + }, + { + "icon_id": "24879595", + "name": "close", + "font_class": "close", + "unicode": "e673", + "unicode_decimal": 58995 + }, + { + "icon_id": "24879596", + "name": "help-filled", + "font_class": "help-filled", + "unicode": "e674", + "unicode_decimal": 58996 + }, + { + "icon_id": "24879597", + "name": "plus-filled", + "font_class": "paperplane-filled", + "unicode": "e675", + "unicode_decimal": 58997 + }, + { + "icon_id": "24879598", + "name": "plus", + "font_class": "plus", + "unicode": "e676", + "unicode_decimal": 58998 + }, + { + "icon_id": "24879599", + "name": "mic-filled", + "font_class": "mic-filled", + "unicode": "e677", + "unicode_decimal": 58999 + }, + { + "icon_id": "24879600", + "name": "image-filled", + "font_class": "image-filled", + "unicode": "e678", + "unicode_decimal": 59000 + }, + { + "icon_id": "24855900", + "name": "locked-filled", + "font_class": "locked-filled", + "unicode": "e668", + "unicode_decimal": 58984 + }, + { + "icon_id": "24855901", + "name": "info", + "font_class": "info", + "unicode": "e669", + "unicode_decimal": 58985 + }, + { + "icon_id": "24855903", + "name": "locked", + "font_class": "locked", + "unicode": "e66b", + "unicode_decimal": 58987 + }, + { + "icon_id": "24855884", + "name": "camera-filled", + "font_class": "camera-filled", + "unicode": "e658", + "unicode_decimal": 58968 + }, + { + "icon_id": "24855885", + "name": "chat-filled", + "font_class": "chat-filled", + "unicode": "e659", + "unicode_decimal": 58969 + }, + { + "icon_id": "24855886", + "name": "camera", + "font_class": "camera", + "unicode": "e65a", + "unicode_decimal": 58970 + }, + { + "icon_id": "24855887", + "name": "circle", + "font_class": "circle", + "unicode": "e65b", + "unicode_decimal": 58971 + }, + { + "icon_id": "24855888", + "name": "checkmarkempty", + "font_class": "checkmarkempty", + "unicode": "e65c", + "unicode_decimal": 58972 + }, + { + "icon_id": "24855889", + "name": "chat", + "font_class": "chat", + "unicode": "e65d", + "unicode_decimal": 58973 + }, + { + "icon_id": "24855890", + "name": "circle-filled", + "font_class": "circle-filled", + "unicode": "e65e", + "unicode_decimal": 58974 + }, + { + "icon_id": "24855891", + "name": "flag", + "font_class": "flag", + "unicode": "e65f", + "unicode_decimal": 58975 + }, + { + "icon_id": "24855892", + "name": "flag-filled", + "font_class": "flag-filled", + "unicode": "e660", + "unicode_decimal": 58976 + }, + { + "icon_id": "24855893", + "name": "gear-filled", + "font_class": "gear-filled", + "unicode": "e661", + "unicode_decimal": 58977 + }, + { + "icon_id": "24855894", + "name": "home", + "font_class": "home", + "unicode": "e662", + "unicode_decimal": 58978 + }, + { + "icon_id": "24855895", + "name": "home-filled", + "font_class": "home-filled", + "unicode": "e663", + "unicode_decimal": 58979 + }, + { + "icon_id": "24855896", + "name": "gear", + "font_class": "gear", + "unicode": "e664", + "unicode_decimal": 58980 + }, + { + "icon_id": "24855897", + "name": "smallcircle-filled", + "font_class": "smallcircle-filled", + "unicode": "e665", + "unicode_decimal": 58981 + }, + { + "icon_id": "24855898", + "name": "map-filled", + "font_class": "map-filled", + "unicode": "e666", + "unicode_decimal": 58982 + }, + { + "icon_id": "24855899", + "name": "map", + "font_class": "map", + "unicode": "e667", + "unicode_decimal": 58983 + }, + { + "icon_id": "24855825", + "name": "refresh-filled", + "font_class": "refresh-filled", + "unicode": "e656", + "unicode_decimal": 58966 + }, + { + "icon_id": "24855826", + "name": "refresh", + "font_class": "refresh", + "unicode": "e657", + "unicode_decimal": 58967 + }, + { + "icon_id": "24855808", + "name": "cloud-upload", + "font_class": "cloud-upload", + "unicode": "e645", + "unicode_decimal": 58949 + }, + { + "icon_id": "24855809", + "name": "cloud-download-filled", + "font_class": "cloud-download-filled", + "unicode": "e646", + "unicode_decimal": 58950 + }, + { + "icon_id": "24855810", + "name": "cloud-download", + "font_class": "cloud-download", + "unicode": "e647", + "unicode_decimal": 58951 + }, + { + "icon_id": "24855811", + "name": "cloud-upload-filled", + "font_class": "cloud-upload-filled", + "unicode": "e648", + "unicode_decimal": 58952 + }, + { + "icon_id": "24855813", + "name": "redo", + "font_class": "redo", + "unicode": "e64a", + "unicode_decimal": 58954 + }, + { + "icon_id": "24855814", + "name": "images-filled", + "font_class": "images-filled", + "unicode": "e64b", + "unicode_decimal": 58955 + }, + { + "icon_id": "24855815", + "name": "undo-filled", + "font_class": "undo-filled", + "unicode": "e64c", + "unicode_decimal": 58956 + }, + { + "icon_id": "24855816", + "name": "more", + "font_class": "more", + "unicode": "e64d", + "unicode_decimal": 58957 + }, + { + "icon_id": "24855817", + "name": "more-filled", + "font_class": "more-filled", + "unicode": "e64e", + "unicode_decimal": 58958 + }, + { + "icon_id": "24855818", + "name": "undo", + "font_class": "undo", + "unicode": "e64f", + "unicode_decimal": 58959 + }, + { + "icon_id": "24855819", + "name": "images", + "font_class": "images", + "unicode": "e650", + "unicode_decimal": 58960 + }, + { + "icon_id": "24855821", + "name": "paperclip", + "font_class": "paperclip", + "unicode": "e652", + "unicode_decimal": 58962 + }, + { + "icon_id": "24855822", + "name": "settings", + "font_class": "settings", + "unicode": "e653", + "unicode_decimal": 58963 + }, + { + "icon_id": "24855823", + "name": "search", + "font_class": "search", + "unicode": "e654", + "unicode_decimal": 58964 + }, + { + "icon_id": "24855824", + "name": "redo-filled", + "font_class": "redo-filled", + "unicode": "e655", + "unicode_decimal": 58965 + }, + { + "icon_id": "24841702", + "name": "list", + "font_class": "list", + "unicode": "e644", + "unicode_decimal": 58948 + }, + { + "icon_id": "24841489", + "name": "mail-open-filled", + "font_class": "mail-open-filled", + "unicode": "e63a", + "unicode_decimal": 58938 + }, + { + "icon_id": "24841491", + "name": "hand-thumbsdown-filled", + "font_class": "hand-down-filled", + "unicode": "e63c", + "unicode_decimal": 58940 + }, + { + "icon_id": "24841492", + "name": "hand-thumbsdown", + "font_class": "hand-down", + "unicode": "e63d", + "unicode_decimal": 58941 + }, + { + "icon_id": "24841493", + "name": "hand-thumbsup-filled", + "font_class": "hand-up-filled", + "unicode": "e63e", + "unicode_decimal": 58942 + }, + { + "icon_id": "24841494", + "name": "hand-thumbsup", + "font_class": "hand-up", + "unicode": "e63f", + "unicode_decimal": 58943 + }, + { + "icon_id": "24841496", + "name": "heart-filled", + "font_class": "heart-filled", + "unicode": "e641", + "unicode_decimal": 58945 + }, + { + "icon_id": "24841498", + "name": "mail-open", + "font_class": "mail-open", + "unicode": "e643", + "unicode_decimal": 58947 + }, + { + "icon_id": "24841488", + "name": "heart", + "font_class": "heart", + "unicode": "e639", + "unicode_decimal": 58937 + }, + { + "icon_id": "24839963", + "name": "loop", + "font_class": "loop", + "unicode": "e633", + "unicode_decimal": 58931 + }, + { + "icon_id": "24839866", + "name": "pulldown", + "font_class": "pulldown", + "unicode": "e632", + "unicode_decimal": 58930 + }, + { + "icon_id": "24813798", + "name": "scan", + "font_class": "scan", + "unicode": "e62a", + "unicode_decimal": 58922 + }, + { + "icon_id": "24813786", + "name": "bars", + "font_class": "bars", + "unicode": "e627", + "unicode_decimal": 58919 + }, + { + "icon_id": "24813788", + "name": "cart-filled", + "font_class": "cart-filled", + "unicode": "e629", + "unicode_decimal": 58921 + }, + { + "icon_id": "24813790", + "name": "checkbox", + "font_class": "checkbox", + "unicode": "e62b", + "unicode_decimal": 58923 + }, + { + "icon_id": "24813791", + "name": "checkbox-filled", + "font_class": "checkbox-filled", + "unicode": "e62c", + "unicode_decimal": 58924 + }, + { + "icon_id": "24813794", + "name": "shop", + "font_class": "shop", + "unicode": "e62f", + "unicode_decimal": 58927 + }, + { + "icon_id": "24813795", + "name": "headphones", + "font_class": "headphones", + "unicode": "e630", + "unicode_decimal": 58928 + }, + { + "icon_id": "24813796", + "name": "cart", + "font_class": "cart", + "unicode": "e631", + "unicode_decimal": 58929 + } + ] +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue new file mode 100644 index 0000000..86e7444 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uni-icons.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css new file mode 100644 index 0000000..2f56eab --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.css @@ -0,0 +1,663 @@ +.uniui-color:before { + content: "\e6cf"; +} + +.uniui-wallet:before { + content: "\e6b1"; +} + +.uniui-settings-filled:before { + content: "\e6ce"; +} + +.uniui-auth-filled:before { + content: "\e6cc"; +} + +.uniui-shop-filled:before { + content: "\e6cd"; +} + +.uniui-staff-filled:before { + content: "\e6cb"; +} + +.uniui-vip-filled:before { + content: "\e6c6"; +} + +.uniui-plus-filled:before { + content: "\e6c7"; +} + +.uniui-folder-add-filled:before { + content: "\e6c8"; +} + +.uniui-color-filled:before { + content: "\e6c9"; +} + +.uniui-tune-filled:before { + content: "\e6ca"; +} + +.uniui-calendar-filled:before { + content: "\e6c0"; +} + +.uniui-notification-filled:before { + content: "\e6c1"; +} + +.uniui-wallet-filled:before { + content: "\e6c2"; +} + +.uniui-medal-filled:before { + content: "\e6c3"; +} + +.uniui-gift-filled:before { + content: "\e6c4"; +} + +.uniui-fire-filled:before { + content: "\e6c5"; +} + +.uniui-refreshempty:before { + content: "\e6bf"; +} + +.uniui-location-filled:before { + content: "\e6af"; +} + +.uniui-person-filled:before { + content: "\e69d"; +} + +.uniui-personadd-filled:before { + content: "\e698"; +} + +.uniui-back:before { + content: "\e6b9"; +} + +.uniui-forward:before { + content: "\e6ba"; +} + +.uniui-arrow-right:before { + content: "\e6bb"; +} + +.uniui-arrowthinright:before { + content: "\e6bb"; +} + +.uniui-arrow-left:before { + content: "\e6bc"; +} + +.uniui-arrowthinleft:before { + content: "\e6bc"; +} + +.uniui-arrow-up:before { + content: "\e6bd"; +} + +.uniui-arrowthinup:before { + content: "\e6bd"; +} + +.uniui-arrow-down:before { + content: "\e6be"; +} + +.uniui-arrowthindown:before { + content: "\e6be"; +} + +.uniui-bottom:before { + content: "\e6b8"; +} + +.uniui-arrowdown:before { + content: "\e6b8"; +} + +.uniui-right:before { + content: "\e6b5"; +} + +.uniui-arrowright:before { + content: "\e6b5"; +} + +.uniui-top:before { + content: "\e6b6"; +} + +.uniui-arrowup:before { + content: "\e6b6"; +} + +.uniui-left:before { + content: "\e6b7"; +} + +.uniui-arrowleft:before { + content: "\e6b7"; +} + +.uniui-eye:before { + content: "\e651"; +} + +.uniui-eye-filled:before { + content: "\e66a"; +} + +.uniui-eye-slash:before { + content: "\e6b3"; +} + +.uniui-eye-slash-filled:before { + content: "\e6b4"; +} + +.uniui-info-filled:before { + content: "\e649"; +} + +.uniui-reload:before { + content: "\e6b2"; +} + +.uniui-micoff-filled:before { + content: "\e6b0"; +} + +.uniui-map-pin-ellipse:before { + content: "\e6ac"; +} + +.uniui-map-pin:before { + content: "\e6ad"; +} + +.uniui-location:before { + content: "\e6ae"; +} + +.uniui-starhalf:before { + content: "\e683"; +} + +.uniui-star:before { + content: "\e688"; +} + +.uniui-star-filled:before { + content: "\e68f"; +} + +.uniui-calendar:before { + content: "\e6a0"; +} + +.uniui-fire:before { + content: "\e6a1"; +} + +.uniui-medal:before { + content: "\e6a2"; +} + +.uniui-font:before { + content: "\e6a3"; +} + +.uniui-gift:before { + content: "\e6a4"; +} + +.uniui-link:before { + content: "\e6a5"; +} + +.uniui-notification:before { + content: "\e6a6"; +} + +.uniui-staff:before { + content: "\e6a7"; +} + +.uniui-vip:before { + content: "\e6a8"; +} + +.uniui-folder-add:before { + content: "\e6a9"; +} + +.uniui-tune:before { + content: "\e6aa"; +} + +.uniui-auth:before { + content: "\e6ab"; +} + +.uniui-person:before { + content: "\e699"; +} + +.uniui-email-filled:before { + content: "\e69a"; +} + +.uniui-phone-filled:before { + content: "\e69b"; +} + +.uniui-phone:before { + content: "\e69c"; +} + +.uniui-email:before { + content: "\e69e"; +} + +.uniui-personadd:before { + content: "\e69f"; +} + +.uniui-chatboxes-filled:before { + content: "\e692"; +} + +.uniui-contact:before { + content: "\e693"; +} + +.uniui-chatbubble-filled:before { + content: "\e694"; +} + +.uniui-contact-filled:before { + content: "\e695"; +} + +.uniui-chatboxes:before { + content: "\e696"; +} + +.uniui-chatbubble:before { + content: "\e697"; +} + +.uniui-upload-filled:before { + content: "\e68e"; +} + +.uniui-upload:before { + content: "\e690"; +} + +.uniui-weixin:before { + content: "\e691"; +} + +.uniui-compose:before { + content: "\e67f"; +} + +.uniui-qq:before { + content: "\e680"; +} + +.uniui-download-filled:before { + content: "\e681"; +} + +.uniui-pyq:before { + content: "\e682"; +} + +.uniui-sound:before { + content: "\e684"; +} + +.uniui-trash-filled:before { + content: "\e685"; +} + +.uniui-sound-filled:before { + content: "\e686"; +} + +.uniui-trash:before { + content: "\e687"; +} + +.uniui-videocam-filled:before { + content: "\e689"; +} + +.uniui-spinner-cycle:before { + content: "\e68a"; +} + +.uniui-weibo:before { + content: "\e68b"; +} + +.uniui-videocam:before { + content: "\e68c"; +} + +.uniui-download:before { + content: "\e68d"; +} + +.uniui-help:before { + content: "\e679"; +} + +.uniui-navigate-filled:before { + content: "\e67a"; +} + +.uniui-plusempty:before { + content: "\e67b"; +} + +.uniui-smallcircle:before { + content: "\e67c"; +} + +.uniui-minus-filled:before { + content: "\e67d"; +} + +.uniui-micoff:before { + content: "\e67e"; +} + +.uniui-closeempty:before { + content: "\e66c"; +} + +.uniui-clear:before { + content: "\e66d"; +} + +.uniui-navigate:before { + content: "\e66e"; +} + +.uniui-minus:before { + content: "\e66f"; +} + +.uniui-image:before { + content: "\e670"; +} + +.uniui-mic:before { + content: "\e671"; +} + +.uniui-paperplane:before { + content: "\e672"; +} + +.uniui-close:before { + content: "\e673"; +} + +.uniui-help-filled:before { + content: "\e674"; +} + +.uniui-paperplane-filled:before { + content: "\e675"; +} + +.uniui-plus:before { + content: "\e676"; +} + +.uniui-mic-filled:before { + content: "\e677"; +} + +.uniui-image-filled:before { + content: "\e678"; +} + +.uniui-locked-filled:before { + content: "\e668"; +} + +.uniui-info:before { + content: "\e669"; +} + +.uniui-locked:before { + content: "\e66b"; +} + +.uniui-camera-filled:before { + content: "\e658"; +} + +.uniui-chat-filled:before { + content: "\e659"; +} + +.uniui-camera:before { + content: "\e65a"; +} + +.uniui-circle:before { + content: "\e65b"; +} + +.uniui-checkmarkempty:before { + content: "\e65c"; +} + +.uniui-chat:before { + content: "\e65d"; +} + +.uniui-circle-filled:before { + content: "\e65e"; +} + +.uniui-flag:before { + content: "\e65f"; +} + +.uniui-flag-filled:before { + content: "\e660"; +} + +.uniui-gear-filled:before { + content: "\e661"; +} + +.uniui-home:before { + content: "\e662"; +} + +.uniui-home-filled:before { + content: "\e663"; +} + +.uniui-gear:before { + content: "\e664"; +} + +.uniui-smallcircle-filled:before { + content: "\e665"; +} + +.uniui-map-filled:before { + content: "\e666"; +} + +.uniui-map:before { + content: "\e667"; +} + +.uniui-refresh-filled:before { + content: "\e656"; +} + +.uniui-refresh:before { + content: "\e657"; +} + +.uniui-cloud-upload:before { + content: "\e645"; +} + +.uniui-cloud-download-filled:before { + content: "\e646"; +} + +.uniui-cloud-download:before { + content: "\e647"; +} + +.uniui-cloud-upload-filled:before { + content: "\e648"; +} + +.uniui-redo:before { + content: "\e64a"; +} + +.uniui-images-filled:before { + content: "\e64b"; +} + +.uniui-undo-filled:before { + content: "\e64c"; +} + +.uniui-more:before { + content: "\e64d"; +} + +.uniui-more-filled:before { + content: "\e64e"; +} + +.uniui-undo:before { + content: "\e64f"; +} + +.uniui-images:before { + content: "\e650"; +} + +.uniui-paperclip:before { + content: "\e652"; +} + +.uniui-settings:before { + content: "\e653"; +} + +.uniui-search:before { + content: "\e654"; +} + +.uniui-redo-filled:before { + content: "\e655"; +} + +.uniui-list:before { + content: "\e644"; +} + +.uniui-mail-open-filled:before { + content: "\e63a"; +} + +.uniui-hand-down-filled:before { + content: "\e63c"; +} + +.uniui-hand-down:before { + content: "\e63d"; +} + +.uniui-hand-up-filled:before { + content: "\e63e"; +} + +.uniui-hand-up:before { + content: "\e63f"; +} + +.uniui-heart-filled:before { + content: "\e641"; +} + +.uniui-mail-open:before { + content: "\e643"; +} + +.uniui-heart:before { + content: "\e639"; +} + +.uniui-loop:before { + content: "\e633"; +} + +.uniui-pulldown:before { + content: "\e632"; +} + +.uniui-scan:before { + content: "\e62a"; +} + +.uniui-bars:before { + content: "\e627"; +} + +.uniui-cart-filled:before { + content: "\e629"; +} + +.uniui-checkbox:before { + content: "\e62b"; +} + +.uniui-checkbox-filled:before { + content: "\e62c"; +} + +.uniui-shop:before { + content: "\e62f"; +} + +.uniui-headphones:before { + content: "\e630"; +} + +.uniui-cart:before { + content: "\e631"; +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf new file mode 100644 index 0000000..835f33b Binary files /dev/null and b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/components/uni-icons/uniicons.ttf differ diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-icons/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/package.json new file mode 100644 index 0000000..d1c4e77 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-icons", + "displayName": "uni-icons 图标", + "version": "1.3.5", + "description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。", + "keywords": [ + "uni-ui", + "uniui", + "icon", + "图标" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.2.14" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-icons/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/readme.md new file mode 100644 index 0000000..86234ba --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-icons/readme.md @@ -0,0 +1,8 @@ +## Icons 图标 +> **组件名:uni-icons** +> 代码块: `uIcons` + +用于展示 icons 图标 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/changelog.md new file mode 100644 index 0000000..08fa71c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/changelog.md @@ -0,0 +1,17 @@ +## 1.2.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-indexed-list](https://uniapp.dcloud.io/component/uniui/uni-indexed-list) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.11(2021-05-12) +- 新增 组件示例地址 +## 1.0.10(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.9(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.8(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 支持 PC 端 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list-item.vue new file mode 100644 index 0000000..2f13bae --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list-item.vue @@ -0,0 +1,144 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list.vue new file mode 100644 index 0000000..35e168c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/components/uni-indexed-list/uni-indexed-list.vue @@ -0,0 +1,367 @@ + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/package.json new file mode 100644 index 0000000..125c0e7 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-indexed-list", + "displayName": "uni-indexed-list 索引列表", + "version": "1.2.1", + "description": "索引列表组件,右侧带索引的列表,方便快速定位到具体内容,通常用于城市/机场选择等场景", + "keywords": [ + "uni-ui", + "索引列表", + "索引", + "列表" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/readme.md new file mode 100644 index 0000000..44ad84b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-indexed-list/readme.md @@ -0,0 +1,11 @@ + + +## IndexedList 索引列表 +> **组件名:uni-indexed-list** +> 代码块: `uIndexedList` + + +用于展示索引列表。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-indexed-list) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-link/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-link/changelog.md new file mode 100644 index 0000000..2cfbf59 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-link/changelog.md @@ -0,0 +1,17 @@ +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-link](https://uniapp.dcloud.io/component/uniui/uni-link) +## 1.1.7(2021-11-08) +## 0.0.7(2021-09-03) +- 修复 在 nvue 下不显示的 bug +## 0.0.6(2021-07-30) +- 新增 支持自定义插槽 +## 0.0.5(2021-06-21) +- 新增 download 属性,H5平台下载文件名 +## 0.0.4(2021-05-12) +- 新增 组件示例地址 +## 0.0.3(2021-03-09) +- 新增 href 属性支持 tel:|mailto: + +## 0.0.2(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-link/components/uni-link/uni-link.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-link/components/uni-link/uni-link.vue new file mode 100644 index 0000000..27c5468 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-link/components/uni-link/uni-link.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-link/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-link/package.json new file mode 100644 index 0000000..77b1986 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-link/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-link", + "displayName": "uni-link 超链接", + "version": "1.0.0", + "description": "uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打", + "keywords": [ + "uni-ui", + "uniui", + "link", + "超链接", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "y" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-link/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-link/readme.md new file mode 100644 index 0000000..7f09e94 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-link/readme.md @@ -0,0 +1,11 @@ + + +## Link 链接 +> **组件名:uni-link** +> 代码块: `uLink` + + +uni-link是一个外部网页超链接组件,在小程序内复制url,在app内打开外部浏览器,在h5端打开新网页。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-link) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-list/changelog.md new file mode 100644 index 0000000..6aa6e4e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/changelog.md @@ -0,0 +1,20 @@ +## 1.2.1(2022-03-30) +- 删除无用文件 +## 1.2.0(2021-11-23) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-list](https://uniapp.dcloud.io/component/uniui/uni-list) +## 1.1.3(2021-08-30) +- 修复 在vue3中to属性在发行应用的时候报错的bug +## 1.1.2(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.1.1(2021-07-21) +- 修复 与其他组件嵌套使用时,点击失效的Bug +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.17(2021-05-12) +- 新增 组件示例地址 +## 1.0.16(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.0.15(2021-02-05) +- 调整为uni_modules目录规范 +- 修复 uni-list-chat 角标显示不正常的问题 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-ad/uni-list-ad.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-ad/uni-list-ad.vue new file mode 100644 index 0000000..b9349c2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-ad/uni-list-ad.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.scss new file mode 100644 index 0000000..311f8d9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.scss @@ -0,0 +1,58 @@ +/** + * 这里是 uni-list 组件内置的常用样式变量 + * 如果需要覆盖样式,这里提供了基本的组件样式变量,您可以尝试修改这里的变量,去完成样式替换,而不用去修改源码 + * + */ + +// 背景色 +$background-color : #fff; +// 分割线颜色 +$divide-line-color : #e5e5e5; + +// 默认头像大小,如需要修改此值,注意同步修改 js 中的值 const avatarWidth = xx ,目前只支持方形头像 +// nvue 页面不支持修改头像大小 +$avatar-width : 45px ; + +// 头像边框 +$avatar-border-radius: 5px; +$avatar-border-color: #eee; +$avatar-border-width: 1px; + +// 标题文字样式 +$title-size : 16px; +$title-color : #3b4144; +$title-weight : normal; + +// 描述文字样式 +$note-size : 12px; +$note-color : #999; +$note-weight : normal; + +// 右侧额外内容默认样式 +$right-text-size : 12px; +$right-text-color : #999; +$right-text-weight : normal; + +// 角标样式 +// nvue 页面不支持修改圆点位置以及大小 +// 角标在左侧时,角标的位置,默认为 0 ,负数左/下移动,正数右/上移动 +$badge-left: 0px; +$badge-top: 0px; + +// 显示圆点时,圆点大小 +$dot-width: 10px; +$dot-height: 10px; + +// 显示角标时,角标大小和字体大小 +$badge-size : 18px; +$badge-font : 12px; +// 显示角标时,角标前景色 +$badge-color : #fff; +// 显示角标时,角标背景色 +$badge-background-color : #ff5a5f; +// 显示角标时,角标左右间距 +$badge-space : 6px; + +// 状态样式 +// 选中颜色 +$hover : #f5f5f5; diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.vue new file mode 100644 index 0000000..2b31008 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-chat/uni-list-chat.vue @@ -0,0 +1,538 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-item/uni-list-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-item/uni-list-item.vue new file mode 100644 index 0000000..2c7d9ea --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list-item/uni-list-item.vue @@ -0,0 +1,454 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-list.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-list.vue new file mode 100644 index 0000000..ecda676 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-list.vue @@ -0,0 +1,108 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.vue new file mode 100644 index 0000000..3b4c5a2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.wxs b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.wxs new file mode 100644 index 0000000..818a6b7 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/components/uni-list/uni-refresh.wxs @@ -0,0 +1,87 @@ +var pullDown = { + threshold: 95, + maxHeight: 200, + callRefresh: 'onrefresh', + callPullingDown: 'onpullingdown', + refreshSelector: '.uni-refresh' +}; + +function ready(newValue, oldValue, ownerInstance, instance) { + var state = instance.getState() + state.canPullDown = newValue; + // console.log(newValue); +} + +function touchStart(e, instance) { + var state = instance.getState(); + state.refreshInstance = instance.selectComponent(pullDown.refreshSelector); + state.canPullDown = (state.refreshInstance != null && state.refreshInstance != undefined); + if (!state.canPullDown) { + return + } + + // console.log("touchStart"); + + state.height = 0; + state.touchStartY = e.touches[0].pageY || e.changedTouches[0].pageY; + state.refreshInstance.setStyle({ + 'height': 0 + }); + state.refreshInstance.callMethod("onchange", true); +} + +function touchMove(e, ownerInstance) { + var instance = e.instance; + var state = instance.getState(); + if (!state.canPullDown) { + return + } + + var oldHeight = state.height; + var endY = e.touches[0].pageY || e.changedTouches[0].pageY; + var height = endY - state.touchStartY; + if (height > pullDown.maxHeight) { + return; + } + + var refreshInstance = state.refreshInstance; + refreshInstance.setStyle({ + 'height': height + 'px' + }); + + height = height < pullDown.maxHeight ? height : pullDown.maxHeight; + state.height = height; + refreshInstance.callMethod(pullDown.callPullingDown, { + height: height + }); +} + +function touchEnd(e, ownerInstance) { + var state = e.instance.getState(); + if (!state.canPullDown) { + return + } + + state.refreshInstance.callMethod("onchange", false); + + var refreshInstance = state.refreshInstance; + if (state.height > pullDown.threshold) { + refreshInstance.callMethod(pullDown.callRefresh); + return; + } + + refreshInstance.setStyle({ + 'height': 0 + }); +} + +function propObserver(newValue, oldValue, instance) { + pullDown = newValue; +} + +module.exports = { + touchmove: touchMove, + touchstart: touchStart, + touchend: touchEnd, + propObserver: propObserver +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-list/package.json new file mode 100644 index 0000000..66e8bef --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/package.json @@ -0,0 +1,91 @@ +{ + "id": "uni-list", + "displayName": "uni-list 列表", + "version": "1.2.1", + "description": "List 组件 ,帮助使用者快速构建列表。", + "keywords": [ + "", + "uni-ui", + "uniui", + "列表", + "", + "list" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-badge", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-list/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-list/readme.md new file mode 100644 index 0000000..32c2865 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-list/readme.md @@ -0,0 +1,346 @@ +## List 列表 +> **组件名:uni-list** +> 代码块: `uList`、`uListItem` +> 关联组件:`uni-list-item`、`uni-badge`、`uni-icons`、`uni-list-chat`、`uni-list-ad` + + +List 列表组件,包含基本列表样式、可扩展插槽机制、长列表性能优化、多端兼容。 + +在vue页面里,它默认使用页面级滚动。在app-nvue页面里,它默认使用原生list组件滚动。这样的长列表,在滚动出屏幕外后,系统会回收不可见区域的渲染内存资源,不会造成滚动越长手机越卡的问题。 + +uni-list组件是父容器,里面的核心是uni-list-item子组件,它代表列表中的一个可重复行,子组件可以无限循环。 + +uni-list-item有很多风格,uni-list-item组件通过内置的属性,满足一些常用的场景。当内置属性不满足需求时,可以通过扩展插槽来自定义列表内容。 + +内置属性可以覆盖的场景包括:导航列表、设置列表、小图标列表、通信录列表、聊天记录列表。 + +涉及很多大图或丰富内容的列表,比如类今日头条的新闻列表、类淘宝的电商列表,需要通过扩展插槽实现。 + +下文均有样例给出。 + +uni-list不包含下拉刷新和上拉翻页。上拉翻页另见组件:[uni-load-more](https://ext.dcloud.net.cn/plugin?id=29) + + +### 安装方式 + +本组件符合[easycom](https://uniapp.dcloud.io/collocation/pages?id=easycom)规范,`HBuilderX 2.5.5`起,只需将本组件导入项目,在页面`template`中即可直接使用,无需在页面中`import`和注册`components`。 + +如需通过`npm`方式使用`uni-ui`组件,另见文档:[https://ext.dcloud.net.cn/plugin?id=55](https://ext.dcloud.net.cn/plugin?id=55) + +> **注意事项** +> 为了避免错误使用,给大家带来不好的开发体验,请在使用组件前仔细阅读下面的注意事项,可以帮你避免一些错误。 +> - 组件需要依赖 `sass` 插件 ,请自行手动安装 +> - 组件内部依赖 `'uni-icons'` 、`uni-badge` 组件 +> - `uni-list` 和 `uni-list-item` 需要配套使用,暂不支持单独使用 `uni-list-item` +> - 只有开启点击反馈后,会有点击选中效果 +> - 使用插槽时,可以完全自定义内容 +> - note 、rightText 属性暂时没做限制,不支持文字溢出隐藏,使用时应该控制长度显示或通过默认插槽自行扩展 +> - 支付宝小程序平台需要在支付宝小程序开发者工具里开启 component2 编译模式,开启方式: 详情 --> 项目配置 --> 启用 component2 编译 +> - 如果需要修改 `switch`、`badge` 样式,请使用插槽自定义 +> - 在 `HBuilderX` 低版本中,可能会出现组件显示 `undefined` 的问题,请升级最新的 `HBuilderX` 或者 `cli` +> - 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + +### 基本用法 + +- 设置 `title` 属性,可以显示列表标题 +- 设置 `disabled` 属性,可以禁用当前项 + +```html + + + + + +``` + +### 多行内容显示 + +- 设置 `note` 属性 ,可以在第二行显示描述文本信息 + +```html + + + + + +``` + +### 右侧显示角标、switch + +- 设置 `show-badge` 属性 ,可以显示角标内容 +- 设置 `show-switch` 属性,可以显示 switch 开关 + +```html + + + + + +``` + +### 左侧显示略缩图、图标 + +- 设置 `thumb` 属性 ,可以在列表左侧显示略缩图 +- 设置 `show-extra-icon` 属性,并指定 `extra-icon` 可以在左侧显示图标 + +```html + + + + +``` + +### 开启点击反馈和右侧箭头 +- 设置 `clickable` 为 `true` ,则表示这是一个可点击的列表,会默认给一个点击效果,并可以监听 `click` 事件 +- 设置 `link` 属性,会自动开启点击反馈,并给列表右侧添加一个箭头 +- 设置 `to` 属性,可以跳转页面,`link` 的值表示跳转方式,如果不指定,默认为 `navigateTo` + +```html + + + + + + + +``` + + +### 聊天列表示例 +- 设置 `clickable` 为 `true` ,则表示这是一个可点击的列表,会默认给一个点击效果,并可以监听 `click` 事件 +- 设置 `link` 属性,会自动开启点击反馈,`link` 的值表示跳转方式,如果不指定,默认为 `navigateTo` +- 设置 `to` 属性,可以跳转页面 +- `time` 属性,通常会设置成时间显示,但是这个属性不仅仅可以设置时间,你可以传入任何文本,注意文本长度可能会影响显示 +- `avatar` 和 `avatarList` 属性同时只会有一个生效,同时设置的话,`avatarList` 属性的长度大于1 ,`avatar` 属性将失效 +- 可以通过默认插槽自定义列表右侧内容 + +```html + + + + + + + + + + + + + + + + + 刚刚 + + + + + + + +``` + +```javascript + +export default { + components: {}, + data() { + return { + avatarList: [{ + url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' + }, { + url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' + }, { + url: 'https://vkceyugu.cdn.bspapp.com/VKCEYUGU-dc-site/460d46d0-4fcc-11eb-8ff1-d5dcf8779628.png' + }] + } + } +} + +``` + + +```css + +.chat-custom-right { + flex: 1; + /* #ifndef APP-NVUE */ + display: flex; + /* #endif */ + flex-direction: column; + justify-content: space-between; + align-items: flex-end; +} + +.chat-custom-text { + font-size: 12px; + color: #999; +} + +``` + +## API + +### List Props + +属性名 |类型 |默认值 | 说明 +:-: |:-: |:-: | :-: +border |Boolean |true | 是否显示边框 + + +### ListItem Props + +属性名 |类型 |默认值 | 说明 +:-: |:-: |:-: | :-: +title |String |- | 标题 +note |String |- | 描述 +ellipsis |Number |0 | title 是否溢出隐藏,可选值,0:默认; 1:显示一行; 2:显示两行;【nvue 暂不支持】 +thumb |String |- | 左侧缩略图,若thumb有值,则不会显示扩展图标 +thumbSize |String |medium | 略缩图尺寸,可选值,lg:大图; medium:一般; sm:小图; +showBadge |Boolean |false | 是否显示数字角标 +badgeText |String |- | 数字角标内容 +badgeType |String |- | 数字角标类型,参考[uni-icons](https://ext.dcloud.net.cn/plugin?id=21) +badgeStyle |Object |- | 数字角标样式,使用uni-badge的custom-style参数 +rightText |String |- | 右侧文字内容 +disabled |Boolean |false | 是否禁用 +showArrow |Boolean |true | 是否显示箭头图标 +link |String |navigateTo | 新页面跳转方式,可选值见下表 +to |String |- | 新页面跳转地址,如填写此属性,click 会返回页面是否跳转成功 +clickable |Boolean |false | 是否开启点击反馈 +showSwitch |Boolean |false | 是否显示Switch +switchChecked |Boolean |false | Switch是否被选中 +showExtraIcon |Boolean |false | 左侧是否显示扩展图标 +extraIcon |Object |- | 扩展图标参数,格式为 ``{color: '#4cd964',size: '22',type: 'spinner'}``,参考 [uni-icons](https://ext.dcloud.net.cn/plugin?id=28) +direction | String |row | 排版方向,可选值,row:水平排列; column:垂直排列; 3个插槽是水平排还是垂直排,也受此属性控制 + + +#### Link Options + +属性名 | 说明 +:-: | :-: +navigateTo | 同 uni.navigateTo() +redirectTo | 同 uni.reLaunch() +reLaunch | 同 uni.reLaunch() +switchTab | 同 uni.switchTab() + +### ListItem Events + +事件称名 |说明 |返回参数 +:-: |:-: |:-: +click |点击 uniListItem 触发事件,需开启点击反馈 |- +switchChange |点击切换 Switch 时触发,需显示 switch |e={value:checked} + + + +### ListItem Slots + +名称 | 说明 +:-: | :-: +header | 左/上内容插槽,可完全自定义默认显示 +body | 中间内容插槽,可完全自定义中间内容 +footer | 右/下内容插槽,可完全自定义右侧内容 + + +> **通过插槽扩展** +> 需要注意的是当使用插槽时,内置样式将会失效,只保留排版样式,此时的样式需要开发者自己实现 +> 如果 `uni-list-item` 组件内置属性样式无法满足需求,可以使用插槽来自定义uni-list-item里的内容。 +> uni-list-item提供了3个可扩展的插槽:`header`、`body`、`footer` +> - 当 `direction` 属性为 `row` 时表示水平排列,此时 `header` 表示列表的左边部分,`body` 表示列表的中间部分,`footer` 表示列表的右边部分 +> - 当 `direction` 属性为 `column` 时表示垂直排列,此时 `header` 表示列表的上边部分,`body` 表示列表的中间部分,`footer` 表示列表的下边部分 +> 开发者可以只用1个插槽,也可以3个一起使用。在插槽中可自主编写view标签,实现自己所需的效果。 + + +**示例** + +```html + + + + + + + + + 自定义插槽 + + + + +``` + + + + + +### ListItemChat Props + +属性名 |类型 |默认值 | 说明 +:-: |:-: |:-: | :-: +title |String |- | 标题 +note |String |- | 描述 +clickable |Boolean |false | 是否开启点击反馈 +badgeText |String |- | 数字角标内容,设置为 `dot` 将显示圆点 +badgePositon |String |right | 角标位置 +link |String |navigateTo | 是否展示右侧箭头并开启点击反馈,可选值见下表 +clickable |Boolean |false | 是否开启点击反馈 +to |String |- | 跳转页面地址,如填写此属性,click 会返回页面是否跳转成功 +time |String |- | 右侧时间显示 +avatarCircle |Boolean |false | 是否显示圆形头像 +avatar |String |- | 头像地址,avatarCircle 不填时生效 +avatarList |Array |- | 头像组,格式为 [{url:''}] + +#### Link Options + +属性名 | 说明 +:-: | :-: +navigateTo | 同 uni.navigateTo() +redirectTo | 同 uni.reLaunch() +reLaunch | 同 uni.reLaunch() +switchTab | 同 uni.switchTab() + +### ListItemChat Slots + +名称 | 说明 +:- | :- +default | 自定义列表右侧内容(包括时间和角标显示) + +### ListItemChat Events +事件称名 | 说明 | 返回参数 +:-: | :-: | :-: +@click | 点击 uniListChat 触发事件 | {data:{}} ,如有 to 属性,会返回页面跳转信息 + + + + + + +## 基于uni-list扩展的页面模板 + +通过扩展插槽,可实现多种常见样式的列表 + +**新闻列表类** + +1. 云端一体混合布局:[https://ext.dcloud.net.cn/plugin?id=2546](https://ext.dcloud.net.cn/plugin?id=2546) +2. 云端一体垂直布局,大图模式:[https://ext.dcloud.net.cn/plugin?id=2583](https://ext.dcloud.net.cn/plugin?id=2583) +3. 云端一体垂直布局,多行图文混排:[https://ext.dcloud.net.cn/plugin?id=2584](https://ext.dcloud.net.cn/plugin?id=2584) +4. 云端一体垂直布局,多图模式:[https://ext.dcloud.net.cn/plugin?id=2585](https://ext.dcloud.net.cn/plugin?id=2585) +5. 云端一体水平布局,左图右文:[https://ext.dcloud.net.cn/plugin?id=2586](https://ext.dcloud.net.cn/plugin?id=2586) +6. 云端一体水平布局,左文右图:[https://ext.dcloud.net.cn/plugin?id=2587](https://ext.dcloud.net.cn/plugin?id=2587) +7. 云端一体垂直布局,无图模式,主标题+副标题:[https://ext.dcloud.net.cn/plugin?id=2588](https://ext.dcloud.net.cn/plugin?id=2588) + +**商品列表类** + +1. 云端一体列表/宫格视图互切:[https://ext.dcloud.net.cn/plugin?id=2651](https://ext.dcloud.net.cn/plugin?id=2651) +2. 云端一体列表(宫格模式):[https://ext.dcloud.net.cn/plugin?id=2671](https://ext.dcloud.net.cn/plugin?id=2671) +3. 云端一体列表(列表模式):[https://ext.dcloud.net.cn/plugin?id=2672](https://ext.dcloud.net.cn/plugin?id=2672) + +## 组件示例 + +点击查看:[https://hellouniapp.dcloud.net.cn/pages/extUI/list/list](https://hellouniapp.dcloud.net.cn/pages/extUI/list/list) \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/changelog.md new file mode 100644 index 0000000..8f03f1d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/changelog.md @@ -0,0 +1,19 @@ +## 1.3.3(2022-01-20) +- 新增 showText属性 ,是否显示文本 +## 1.3.2(2022-01-19) +- 修复 nvue 平台下不显示文本的bug +## 1.3.1(2022-01-19) +- 修复 微信小程序平台样式选择器报警告的问题 +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-load-more](https://uniapp.dcloud.io/component/uniui/uni-load-more) +## 1.2.1(2021-08-24) +- 新增 支持国际化 +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.8(2021-05-12) +- 新增 组件示例地址 +## 1.1.7(2021-03-30) +- 修复 uni-load-more 在首页使用时,h5 平台报 'uni is not defined' 的 bug +## 1.1.6(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/en.json new file mode 100644 index 0000000..a4f14a5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/en.json @@ -0,0 +1,5 @@ +{ + "uni-load-more.contentdown": "Pull up to show more", + "uni-load-more.contentrefresh": "loading...", + "uni-load-more.contentnomore": "No more data" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hans.json new file mode 100644 index 0000000..f15d510 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hans.json @@ -0,0 +1,5 @@ +{ + "uni-load-more.contentdown": "上拉显示更多", + "uni-load-more.contentrefresh": "正在加载...", + "uni-load-more.contentnomore": "没有更多数据了" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hant.json new file mode 100644 index 0000000..a255c6d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/i18n/zh-Hant.json @@ -0,0 +1,5 @@ +{ + "uni-load-more.contentdown": "上拉顯示更多", + "uni-load-more.contentrefresh": "正在加載...", + "uni-load-more.contentnomore": "沒有更多數據了" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/uni-load-more.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/uni-load-more.vue new file mode 100644 index 0000000..e5eff4d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/components/uni-load-more/uni-load-more.vue @@ -0,0 +1,399 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/package.json new file mode 100644 index 0000000..2fa6f04 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-load-more", + "displayName": "uni-load-more 加载更多", + "version": "1.3.3", + "description": "LoadMore 组件,常用在列表里面,做滚动加载使用。", + "keywords": [ + "uni-ui", + "uniui", + "加载更多", + "load-more" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/readme.md new file mode 100644 index 0000000..54dc1fa --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-load-more/readme.md @@ -0,0 +1,14 @@ + + +### LoadMore 加载更多 +> **组件名:uni-load-more** +> 代码块: `uLoadMore` + + +用于列表中,做滚动加载使用,展示 loading 的各种状态。 + + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-load-more) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/changelog.md new file mode 100644 index 0000000..f0f6b56 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/changelog.md @@ -0,0 +1,41 @@ +## 1.3.6(2022-06-30) +- 修复 组件示例中插槽用法无法显示内容的bug +## 1.3.5(2022-05-24) +- 新增 stat 属性 ,可开启统计title 上报 ,仅使用了title 属性且项目开启了uni统计生效 +## 1.3.4(2022-01-24) +- 更新 组件示例 +## 1.3.3(2022-01-24) +- 新增 left-width/right-width属性 ,可修改左右两侧的宽度 +## 1.3.2(2022-01-18) +- 修复 在vue下,标题不垂直居中的bug +## 1.3.1(2022-01-18) +- 修复 height 属性类型错误 +## 1.3.0(2022-01-18) +- 新增 height 属性,可修改组件高度 +- 新增 dark 属性可可开启暗黑模式 +- 优化 标题字数过多显示省略号 +- 优化 插槽,插入内容可完全覆盖 +## 1.2.1(2022-01-10) +- 修复 color 属性不生效的bug +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-nav-bar](https://uniapp.dcloud.io/component/uniui/uni-nav-bar) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.11(2021-05-12) +- 新增 组件示例地址 +## 1.0.10(2021-04-30) +- 修复 在nvue下fixed为true,宽度不能撑满的Bug +## 1.0.9(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.8(2021-04-14) +- uni-ui 修复 uni-nav-bar 当 fixed 属性为 true 时铺不满屏幕的 bug + +## 1.0.7(2021-02-25) +- 修复 easycom 下,找不到 uni-status-bar 的bug + +## 1.0.6(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.5(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue new file mode 100644 index 0000000..cbfc168 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-nav-bar.vue @@ -0,0 +1,348 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar.vue new file mode 100644 index 0000000..6a68874 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/package.json new file mode 100644 index 0000000..e3fe073 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-nav-bar", + "displayName": "uni-nav-bar 自定义导航栏", + "version": "1.3.6", + "description": "自定义导航栏组件,主要用于头部导航。", + "keywords": [ + "uni-ui", + "导航", + "导航栏", + "自定义导航栏" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/readme.md new file mode 100644 index 0000000..3934b32 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-nav-bar/readme.md @@ -0,0 +1,15 @@ + + +## NavBar 导航栏 +> **组件名:uni-nav-bar** +> 代码块: `uNavBar` + +导航栏组件,主要用于头部导航。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-nav-bar) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/changelog.md new file mode 100644 index 0000000..9ee75a0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/changelog.md @@ -0,0 +1,16 @@ +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-notice-bar](https://uniapp.dcloud.io/component/uniui/uni-notice-bar) +## 1.1.1(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.9(2021-05-12) +- 新增 组件示例地址 +## 1.0.8(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.7(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.6(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/components/uni-notice-bar/uni-notice-bar.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/components/uni-notice-bar/uni-notice-bar.vue new file mode 100644 index 0000000..1d2ac1d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/components/uni-notice-bar/uni-notice-bar.vue @@ -0,0 +1,395 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/package.json new file mode 100644 index 0000000..97719a0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-notice-bar", + "displayName": "uni-notice-bar 通告栏", + "version": "1.2.0", + "description": "NoticeBar 通告栏组件,常用于展示公告信息,可设为滚动公告", + "keywords": [ + "uni-ui", + "uniui", + "通告栏", + "公告", + "跑马灯" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/readme.md new file mode 100644 index 0000000..fb2ede2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-notice-bar/readme.md @@ -0,0 +1,13 @@ + + +## NoticeBar 通告栏 +> **组件名:uni-notice-bar** +> 代码块: `uNoticeBar` + + +通告栏组件 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-notice-bar) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/changelog.md new file mode 100644 index 0000000..5925c32 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/changelog.md @@ -0,0 +1,25 @@ +## 1.2.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-number-box](https://uniapp.dcloud.io/component/uniui/uni-number-box) +## 1.1.2(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +## 1.1.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-04-20) +- 修复 uni-number-box 浮点数运算不精确的 bug +- 修复 uni-number-box change 事件触发不正确的 bug +- 新增 uni-number-box v-model 双向绑定 +## 1.0.5(2021-02-05) +- 调整为uni_modules目录规范 + +## 1.0.7(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 支持 v-model +- 新增 支持 focus、blur 事件 +- 新增 支持 PC 端 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue new file mode 100644 index 0000000..e91c032 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/components/uni-number-box/uni-number-box.vue @@ -0,0 +1,221 @@ + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/package.json new file mode 100644 index 0000000..ad82336 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/package.json @@ -0,0 +1,85 @@ +{ + "id": "uni-number-box", + "displayName": "uni-number-box 数字输入框", + "version": "1.2.1", + "description": "NumberBox 带加减按钮的数字输入框组件,用户可以控制每次点击增加的数值,支持小数。", + "keywords": [ + "uni-ui", + "uniui", + "数字输入框" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/readme.md new file mode 100644 index 0000000..affc56f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-number-box/readme.md @@ -0,0 +1,13 @@ + + +## NumberBox 数字输入框 +> **组件名:uni-number-box** +> 代码块: `uNumberBox` + + +带加减按钮的数字输入框。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-number-box) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/changelog.md new file mode 100644 index 0000000..336c2ba --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/changelog.md @@ -0,0 +1,20 @@ +## 1.2.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-pagination](https://uniapp.dcloud.io/component/uniui/uni-pagination) +## 1.1.2(2021-10-08) +- 修复 current 、value 属性未监听,导致高亮样式失效的 bug +## 1.1.1(2021-08-20) +- 新增 支持国际化 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-05-12) +- 新增 组件示例地址 +## 1.0.6(2021-04-12) +- 新增 PC 和 移动端适配不同的 ui +## 1.0.5(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/en.json new file mode 100644 index 0000000..a57becd --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/en.json @@ -0,0 +1,4 @@ +{ + "uni-pagination.prevText": "prev", + "uni-pagination.nextText": "next" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json new file mode 100644 index 0000000..ccbba2f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/es.json @@ -0,0 +1,4 @@ +{ + "uni-pagination.prevText": "anterior", + "uni-pagination.nextText": "próxima" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/fr.json b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/fr.json new file mode 100644 index 0000000..9b5f2d9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/fr.json @@ -0,0 +1,4 @@ +{ + "uni-pagination.prevText": "précédente", + "uni-pagination.nextText": "suivante" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/index.js new file mode 100644 index 0000000..2469dd0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/index.js @@ -0,0 +1,12 @@ +import en from './en.json' +import es from './es.json' +import fr from './fr.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + es, + fr, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hans.json new file mode 100644 index 0000000..fedbe82 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hans.json @@ -0,0 +1,4 @@ +{ + "uni-pagination.prevText": "上一页", + "uni-pagination.nextText": "下一页" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hant.json new file mode 100644 index 0000000..133b340 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/i18n/zh-Hant.json @@ -0,0 +1,4 @@ +{ + "uni-pagination.prevText": "上一頁", + "uni-pagination.nextText": "下一頁" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/uni-pagination.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/uni-pagination.vue new file mode 100644 index 0000000..79db4b8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/components/uni-pagination/uni-pagination.vue @@ -0,0 +1,409 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/package.json new file mode 100644 index 0000000..adce670 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-pagination", + "displayName": "uni-pagination 分页器", + "version": "1.2.1", + "description": "Pagination 分页器组件,用于展示页码、请求数据等。", + "keywords": [ + "uni-ui", + "uniui", + "分页器", + "页码" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-icons"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/readme.md new file mode 100644 index 0000000..eefa263 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-pagination/readme.md @@ -0,0 +1,13 @@ + + +## Pagination 分页器 +> **组件名:uni-pagination** +> 代码块: `uPagination` + + +分页器组件,用于展示页码、请求数据等。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-pagination) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/changelog.md new file mode 100644 index 0000000..a9e2d66 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/changelog.md @@ -0,0 +1,60 @@ +## 1.7.9(2022-04-02) +- 修复 弹出层内部无法滚动的bug +## 1.7.8(2022-03-28) +- 修复 小程序中高度错误的bug +## 1.7.7(2022-03-17) +- 修复 快速调用open出现问题的Bug +## 1.7.6(2022-02-14) +- 修复 safeArea 属性不能设置为false的bug +## 1.7.5(2022-01-19) +- 修复 isMaskClick 失效的bug +## 1.7.4(2022-01-19) +- 新增 cancelText \ confirmText 属性 ,可自定义文本 +- 新增 maskBackgroundColor 属性 ,可以修改蒙版颜色 +- 优化 maskClick属性 更新为 isMaskClick ,解决微信小程序警告的问题 +## 1.7.3(2022-01-13) +- 修复 设置 safeArea 属性不生效的bug +## 1.7.2(2021-11-26) +- 优化 组件示例 +## 1.7.1(2021-11-26) +- 修复 vuedoc 文字错误 +## 1.7.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-popup](https://uniapp.dcloud.io/component/uniui/uni-popup) +## 1.6.2(2021-08-24) +- 新增 支持国际化 +## 1.6.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.6.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.5.0(2021-06-23) +- 新增 mask-click 遮罩层点击事件 +## 1.4.5(2021-06-22) +- 修复 nvue 平台中间弹出后,点击内容,再点击遮罩无法关闭的Bug +## 1.4.4(2021-06-18) +- 修复 H5平台中间弹出后,点击内容,再点击遮罩无法关闭的Bug +## 1.4.3(2021-06-08) +- 修复 错误的 watch 字段 +- 修复 safeArea 属性不生效的问题 +- 修复 点击内容,再点击遮罩无法关闭的Bug +## 1.4.2(2021-05-12) +- 新增 组件示例地址 +## 1.4.1(2021-04-29) +- 修复 组件内放置 input 、textarea 组件,无法聚焦的问题 +## 1.4.0 (2021-04-29) +- 新增 type 属性的 left\right 值,支持左右弹出 +- 新增 open(String:type) 方法参数 ,可以省略 type 属性 ,直接传入类型打开指定弹窗 +- 新增 backgroundColor 属性,可定义主窗口背景色,默认不显示背景色 +- 新增 safeArea 属性,是否适配底部安全区 +- 修复 App\h5\微信小程序底部安全区占位不对的Bug +- 修复 App 端弹出等待的Bug +- 优化 提升低配设备性能,优化动画卡顿问题 +- 优化 更简单的组件自定义方式 +## 1.2.9(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 +## 1.2.8(2021-02-05) +- 调整为uni_modules目录规范 +## 1.2.7(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 支持 PC 端 +- 新增 uni-popup-message 、uni-popup-dialog扩展组件支持 PC 端 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/keypress.js b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/keypress.js new file mode 100644 index 0000000..6ef26a2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + this.$once('hook:beforeDestroy', () => { + document.removeEventListener('keyup', listener) + }) + }, + render: () => {} +} +// #endif diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/uni-popup-dialog.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/uni-popup-dialog.vue new file mode 100644 index 0000000..a5d0f2a --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-dialog/uni-popup-dialog.vue @@ -0,0 +1,271 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-message/uni-popup-message.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-message/uni-popup-message.vue new file mode 100644 index 0000000..91370a8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-message/uni-popup-message.vue @@ -0,0 +1,143 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-share/uni-popup-share.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-share/uni-popup-share.vue new file mode 100644 index 0000000..5be7624 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup-share/uni-popup-share.vue @@ -0,0 +1,187 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/en.json new file mode 100644 index 0000000..7f1bd06 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/en.json @@ -0,0 +1,7 @@ +{ + "uni-popup.cancel": "cancel", + "uni-popup.ok": "ok", + "uni-popup.placeholder": "pleace enter", + "uni-popup.title": "Hint", + "uni-popup.shareTitle": "Share to" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hans.json new file mode 100644 index 0000000..5e3003c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hans.json @@ -0,0 +1,7 @@ +{ + "uni-popup.cancel": "取消", + "uni-popup.ok": "确定", + "uni-popup.placeholder": "请输入", + "uni-popup.title": "提示", + "uni-popup.shareTitle": "分享到" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hant.json new file mode 100644 index 0000000..13e39eb --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/i18n/zh-Hant.json @@ -0,0 +1,7 @@ +{ + "uni-popup.cancel": "取消", + "uni-popup.ok": "確定", + "uni-popup.placeholder": "請輸入", + "uni-popup.title": "提示", + "uni-popup.shareTitle": "分享到" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/keypress.js b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/keypress.js new file mode 100644 index 0000000..62dda46 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/keypress.js @@ -0,0 +1,45 @@ +// #ifdef H5 +export default { + name: 'Keypress', + props: { + disable: { + type: Boolean, + default: false + } + }, + mounted () { + const keyNames = { + esc: ['Esc', 'Escape'], + tab: 'Tab', + enter: 'Enter', + space: [' ', 'Spacebar'], + up: ['Up', 'ArrowUp'], + left: ['Left', 'ArrowLeft'], + right: ['Right', 'ArrowRight'], + down: ['Down', 'ArrowDown'], + delete: ['Backspace', 'Delete', 'Del'] + } + const listener = ($event) => { + if (this.disable) { + return + } + const keyName = Object.keys(keyNames).find(key => { + const keyName = $event.key + const value = keyNames[key] + return value === keyName || (Array.isArray(value) && value.includes(keyName)) + }) + if (keyName) { + // 避免和其他按键事件冲突 + setTimeout(() => { + this.$emit(keyName, {}) + }, 0) + } + } + document.addEventListener('keyup', listener) + // this.$once('hook:beforeDestroy', () => { + // document.removeEventListener('keyup', listener) + // }) + }, + render: () => {} +} +// #endif diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/popup.js b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/popup.js new file mode 100644 index 0000000..c4e5781 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/popup.js @@ -0,0 +1,26 @@ + +export default { + data() { + return { + + } + }, + created(){ + this.popup = this.getParent() + }, + methods:{ + /** + * 获取父元素实例 + */ + getParent(name = 'uniPopup') { + let parent = this.$parent; + let parentName = parent.$options.name; + while (parentName !== name) { + parent = parent.$parent; + if (!parent) return false + parentName = parent.$options.name; + } + return parent; + }, + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/uni-popup.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/uni-popup.vue new file mode 100644 index 0000000..db90c59 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/components/uni-popup/uni-popup.vue @@ -0,0 +1,474 @@ + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/package.json new file mode 100644 index 0000000..069e9ce --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/package.json @@ -0,0 +1,90 @@ +{ + "id": "uni-popup", + "displayName": "uni-popup 弹出层", + "version": "1.7.9", + "description": " Popup 组件,提供常用的弹层", + "keywords": [ + "uni-ui", + "弹出层", + "弹窗", + "popup", + "弹框" + ], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-transition" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-popup/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/readme.md new file mode 100644 index 0000000..fdad4b3 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-popup/readme.md @@ -0,0 +1,17 @@ + + +## Popup 弹出层 +> **组件名:uni-popup** +> 代码块: `uPopup` +> 关联组件:`uni-transition` + + +弹出层组件,在应用中弹出一个消息提示窗口、提示框等 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-popup) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-rate/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-rate/changelog.md new file mode 100644 index 0000000..8a98a61 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-rate/changelog.md @@ -0,0 +1,25 @@ +## 1.3.1(2022-02-25) +- 修复 条件判断 `NaN` 错误的 bug +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-rate](https://uniapp.dcloud.io/component/uniui/uni-rate) +## 1.2.2(2021-09-10) +- 优化 默认值修改为 0 颗星 +## 1.2.1(2021-07-30) +- 优化 vue3下事件警告的问题 +## 1.2.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.2(2021-05-12) +- 新增 组件示例地址 +## 1.1.1(2021-04-21) +- 修复 布局变化后 uni-rate 星星计算不准确的 bug +- 优化 添加依赖 uni-icons, 导入 uni-rate 自动下载依赖 +## 1.1.0(2021-04-16) +- 修复 uni-rate 属性 margin 值为 string 组件失效的 bug + +## 1.0.9(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.8(2021-02-05) +- 调整为uni_modules目录规范 +- 支持 pc 端 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-rate/components/uni-rate/uni-rate.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-rate/components/uni-rate/uni-rate.vue new file mode 100644 index 0000000..857f5f9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-rate/components/uni-rate/uni-rate.vue @@ -0,0 +1,361 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-rate/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-rate/package.json new file mode 100644 index 0000000..64e8e33 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-rate/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-rate", + "displayName": "uni-rate 评分", + "version": "1.3.1", + "description": "Rate 评分组件,可自定义评分星星图标的大小、间隔、评分数。", + "keywords": [ + "uni-ui", + "uniui", + "评分" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-rate/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-rate/readme.md new file mode 100644 index 0000000..eae7b5c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-rate/readme.md @@ -0,0 +1,12 @@ + + +## Rate 评分 +> **组件名:uni-rate** +> 代码块: `uRate` +> 关联组件:`uni-icons` + + +评分组件,多用于购买商品后,对商品进行评价等场景 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-rate) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-row/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-row/changelog.md new file mode 100644 index 0000000..5b465bc --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-row/changelog.md @@ -0,0 +1,10 @@ +## 1.0.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-row](https://uniapp.dcloud.io/component/uniui/uni-row) +## 0.1.0(2021-07-13) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 0.0.4(2021-05-12) +- 新增 组件示例地址 +## 0.0.3(2021-02-05) +- 调整为uni_modules目录规范 +- 新增uni-row组件 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-row/components/uni-col/uni-col.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-row/components/uni-col/uni-col.vue new file mode 100644 index 0000000..d5f3728 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-row/components/uni-col/uni-col.vue @@ -0,0 +1,317 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-row/components/uni-row/uni-row.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-row/components/uni-row/uni-row.vue new file mode 100644 index 0000000..c7d9370 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-row/components/uni-row/uni-row.vue @@ -0,0 +1,190 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-row/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-row/package.json new file mode 100644 index 0000000..3f52fa6 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-row/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-row", + "displayName": "uni-row 布局-行", + "version": "1.0.0", + "description": "流式栅格系统,随着屏幕或视口分为 24 份,可以迅速简便地创建布局。", + "keywords": [ + "uni-ui", + "uniui", + "栅格", + "布局", + "layout" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "u" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-row/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-row/readme.md new file mode 100644 index 0000000..3c9c8b9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-row/readme.md @@ -0,0 +1,10 @@ +## Layout 布局 + +> **组件名 uni-row、uni-col** +> 代码块: `uRow`、`uCol` + + +流式栅格系统,随着屏幕或视口分为 24 份,可以迅速简便地创建布局。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-row) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/changelog.md new file mode 100644 index 0000000..b863bb0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/changelog.md @@ -0,0 +1,8 @@ +## 1.0.3(2022-01-21) +- 优化 组件示例 +## 1.0.2(2021-11-22) +- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题 +## 1.0.1(2021-11-22) +- 修复 vue3中scss语法兼容问题 +## 1.0.0(2021-11-18) +- init diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/index.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/index.scss new file mode 100644 index 0000000..1744a5f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/index.scss @@ -0,0 +1 @@ +@import './styles/index.scss'; diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/package.json new file mode 100644 index 0000000..7cc0ccb --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/package.json @@ -0,0 +1,82 @@ +{ + "id": "uni-scss", + "displayName": "uni-scss 辅助样式", + "version": "1.0.3", + "description": "uni-sass是uni-ui提供的一套全局样式 ,通过一些简单的类名和sass变量,实现简单的页面布局操作,比如颜色、边距、圆角等。", + "keywords": [ + "uni-scss", + "uni-ui", + "辅助样式" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "^3.1.0" + }, + "dcloudext": { + "category": [ + "JS SDK", + "通用 SDK" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "n", + "联盟": "n" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/readme.md new file mode 100644 index 0000000..b7d1c25 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/readme.md @@ -0,0 +1,4 @@ +`uni-sass` 是 `uni-ui`提供的一套全局样式 ,通过一些简单的类名和`sass`变量,实现简单的页面布局操作,比如颜色、边距、圆角等。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-sass) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/index.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/index.scss new file mode 100644 index 0000000..ffac4fe --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/index.scss @@ -0,0 +1,7 @@ +@import './setting/_variables.scss'; +@import './setting/_border.scss'; +@import './setting/_color.scss'; +@import './setting/_space.scss'; +@import './setting/_radius.scss'; +@import './setting/_text.scss'; +@import './setting/_styles.scss'; diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_border.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_border.scss new file mode 100644 index 0000000..12a11c3 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_border.scss @@ -0,0 +1,3 @@ +.uni-border { + border: 1px $uni-border-1 solid; +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_color.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_color.scss new file mode 100644 index 0000000..1ededd9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_color.scss @@ -0,0 +1,66 @@ + +// TODO 暂时不需要 class ,需要用户使用变量实现 ,如果使用类名其实并不推荐 +// @mixin get-styles($k,$c) { +// @if $k == size or $k == weight{ +// font-#{$k}:#{$c} +// }@else{ +// #{$k}:#{$c} +// } +// } +$uni-ui-color:( + // 主色 + primary: $uni-primary, + primary-disable: $uni-primary-disable, + primary-light: $uni-primary-light, + // 辅助色 + success: $uni-success, + success-disable: $uni-success-disable, + success-light: $uni-success-light, + warning: $uni-warning, + warning-disable: $uni-warning-disable, + warning-light: $uni-warning-light, + error: $uni-error, + error-disable: $uni-error-disable, + error-light: $uni-error-light, + info: $uni-info, + info-disable: $uni-info-disable, + info-light: $uni-info-light, + // 中性色 + main-color: $uni-main-color, + base-color: $uni-base-color, + secondary-color: $uni-secondary-color, + extra-color: $uni-extra-color, + // 背景色 + bg-color: $uni-bg-color, + // 边框颜色 + border-1: $uni-border-1, + border-2: $uni-border-2, + border-3: $uni-border-3, + border-4: $uni-border-4, + // 黑色 + black:$uni-black, + // 白色 + white:$uni-white, + // 透明 + transparent:$uni-transparent +) !default; +@each $key, $child in $uni-ui-color { + .uni-#{"" + $key} { + color: $child; + } + .uni-#{"" + $key}-bg { + background-color: $child; + } +} +.uni-shadow-sm { + box-shadow: $uni-shadow-sm; +} +.uni-shadow-base { + box-shadow: $uni-shadow-base; +} +.uni-shadow-lg { + box-shadow: $uni-shadow-lg; +} +.uni-mask { + background-color:$uni-mask; +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss new file mode 100644 index 0000000..9a0428b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_radius.scss @@ -0,0 +1,55 @@ +@mixin radius($r,$d:null ,$important: false){ + $radius-value:map-get($uni-radius, $r) if($important, !important, null); + // Key exists within the $uni-radius variable + @if (map-has-key($uni-radius, $r) and $d){ + @if $d == t { + border-top-left-radius:$radius-value; + border-top-right-radius:$radius-value; + }@else if $d == r { + border-top-right-radius:$radius-value; + border-bottom-right-radius:$radius-value; + }@else if $d == b { + border-bottom-left-radius:$radius-value; + border-bottom-right-radius:$radius-value; + }@else if $d == l { + border-top-left-radius:$radius-value; + border-bottom-left-radius:$radius-value; + }@else if $d == tl { + border-top-left-radius:$radius-value; + }@else if $d == tr { + border-top-right-radius:$radius-value; + }@else if $d == br { + border-bottom-right-radius:$radius-value; + }@else if $d == bl { + border-bottom-left-radius:$radius-value; + } + }@else{ + border-radius:$radius-value; + } +} + +@each $key, $child in $uni-radius { + @if($key){ + .uni-radius-#{"" + $key} { + @include radius($key) + } + }@else{ + .uni-radius { + @include radius($key) + } + } +} + +@each $direction in t, r, b, l,tl, tr, br, bl { + @each $key, $child in $uni-radius { + @if($key){ + .uni-radius-#{"" + $direction}-#{"" + $key} { + @include radius($key,$direction,false) + } + }@else{ + .uni-radius-#{$direction} { + @include radius($key,$direction,false) + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_space.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_space.scss new file mode 100644 index 0000000..3c89528 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_space.scss @@ -0,0 +1,56 @@ + +@mixin fn($space,$direction,$size,$n) { + @if $n { + #{$space}-#{$direction}: #{$size*$uni-space-root}px + } @else { + #{$space}-#{$direction}: #{-$size*$uni-space-root}px + } +} +@mixin get-styles($direction,$i,$space,$n){ + @if $direction == t { + @include fn($space, top,$i,$n); + } + @if $direction == r { + @include fn($space, right,$i,$n); + } + @if $direction == b { + @include fn($space, bottom,$i,$n); + } + @if $direction == l { + @include fn($space, left,$i,$n); + } + @if $direction == x { + @include fn($space, left,$i,$n); + @include fn($space, right,$i,$n); + } + @if $direction == y { + @include fn($space, top,$i,$n); + @include fn($space, bottom,$i,$n); + } + @if $direction == a { + @if $n { + #{$space}:#{$i*$uni-space-root}px; + } @else { + #{$space}:#{-$i*$uni-space-root}px; + } + } +} + +@each $orientation in m,p { + $space: margin; + @if $orientation == m { + $space: margin; + } @else { + $space: padding; + } + @for $i from 0 through 16 { + @each $direction in t, r, b, l, x, y, a { + .uni-#{$orientation}#{$direction}-#{$i} { + @include get-styles($direction,$i,$space,true); + } + .uni-#{$orientation}#{$direction}-n#{$i} { + @include get-styles($direction,$i,$space,false); + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss new file mode 100644 index 0000000..689afec --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_styles.scss @@ -0,0 +1,167 @@ +/* #ifndef APP-NVUE */ + +$-color-white:#fff; +$-color-black:#000; +@mixin base-style($color) { + color: #fff; + background-color: $color; + border-color: mix($-color-black, $color, 8%); + &:not([hover-class]):active { + background: mix($-color-black, $color, 10%); + border-color: mix($-color-black, $color, 20%); + color: $-color-white; + outline: none; + } +} +@mixin is-color($color) { + @include base-style($color); + &[loading] { + @include base-style($color); + &::before { + margin-right:5px; + } + } + &[disabled] { + &, + &[loading], + &:not([hover-class]):active { + color: $-color-white; + border-color: mix(darken($color,10%), $-color-white); + background-color: mix($color, $-color-white); + } + } + +} +@mixin base-plain-style($color) { + color:$color; + background-color: mix($-color-white, $color, 90%); + border-color: mix($-color-white, $color, 70%); + &:not([hover-class]):active { + background: mix($-color-white, $color, 80%); + color: $color; + outline: none; + border-color: mix($-color-white, $color, 50%); + } +} +@mixin is-plain($color){ + &[plain] { + @include base-plain-style($color); + &[loading] { + @include base-plain-style($color); + &::before { + margin-right:5px; + } + } + &[disabled] { + &, + &:active { + color: mix($-color-white, $color, 40%); + background-color: mix($-color-white, $color, 90%); + border-color: mix($-color-white, $color, 80%); + } + } + } +} + + +.uni-btn { + margin: 5px; + color: #393939; + border:1px solid #ccc; + font-size: 16px; + font-weight: 200; + background-color: #F9F9F9; + // TODO 暂时处理边框隐藏一边的问题 + overflow: visible; + &::after{ + border: none; + } + + &:not([type]),&[type=default] { + color: #999; + &[loading] { + background: none; + &::before { + margin-right:5px; + } + } + + + + &[disabled]{ + color: mix($-color-white, #999, 60%); + &, + &[loading], + &:active { + color: mix($-color-white, #999, 60%); + background-color: mix($-color-white,$-color-black , 98%); + border-color: mix($-color-white, #999, 85%); + } + } + + &[plain] { + color: #999; + background: none; + border-color: $uni-border-1; + &:not([hover-class]):active { + background: none; + color: mix($-color-white, $-color-black, 80%); + border-color: mix($-color-white, $-color-black, 90%); + outline: none; + } + &[disabled]{ + &, + &[loading], + &:active { + background: none; + color: mix($-color-white, #999, 60%); + border-color: mix($-color-white, #999, 85%); + } + } + } + } + + &:not([hover-class]):active { + color: mix($-color-white, $-color-black, 50%); + } + + &[size=mini] { + font-size: 16px; + font-weight: 200; + border-radius: 8px; + } + + + + &.uni-btn-small { + font-size: 14px; + } + &.uni-btn-mini { + font-size: 12px; + } + + &.uni-btn-radius { + border-radius: 999px; + } + &[type=primary] { + @include is-color($uni-primary); + @include is-plain($uni-primary) + } + &[type=success] { + @include is-color($uni-success); + @include is-plain($uni-success) + } + &[type=error] { + @include is-color($uni-error); + @include is-plain($uni-error) + } + &[type=warning] { + @include is-color($uni-warning); + @include is-plain($uni-warning) + } + &[type=info] { + @include is-color($uni-info); + @include is-plain($uni-info) + } +} +/* #endif */ diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_text.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_text.scss new file mode 100644 index 0000000..a34d08f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_text.scss @@ -0,0 +1,24 @@ +@mixin get-styles($k,$c) { + @if $k == size or $k == weight{ + font-#{$k}:#{$c} + }@else{ + #{$k}:#{$c} + } +} + +@each $key, $child in $uni-headings { + /* #ifndef APP-NVUE */ + .uni-#{$key} { + @each $k, $c in $child { + @include get-styles($k,$c) + } + } + /* #endif */ + /* #ifdef APP-NVUE */ + .container .uni-#{$key} { + @each $k, $c in $child { + @include get-styles($k,$c) + } + } + /* #endif */ +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss new file mode 100644 index 0000000..557d3d7 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/setting/_variables.scss @@ -0,0 +1,146 @@ +// @use "sass:math"; +@import '../tools/functions.scss'; +// 间距基础倍数 +$uni-space-root: 2 !default; +// 边框半径默认值 +$uni-radius-root:5px !default; +$uni-radius: () !default; +// 边框半径断点 +$uni-radius: map-deep-merge( + ( + 0: 0, + // TODO 当前版本暂时不支持 sm 属性 + // 'sm': math.div($uni-radius-root, 2), + null: $uni-radius-root, + 'lg': $uni-radius-root * 2, + 'xl': $uni-radius-root * 6, + 'pill': 9999px, + 'circle': 50% + ), + $uni-radius +); +// 字体家族 +$body-font-family: 'Roboto', sans-serif !default; +// 文本 +$heading-font-family: $body-font-family !default; +$uni-headings: () !default; +$letterSpacing: -0.01562em; +$uni-headings: map-deep-merge( + ( + 'h1': ( + size: 32px, + weight: 300, + line-height: 50px, + // letter-spacing:-0.01562em + ), + 'h2': ( + size: 28px, + weight: 300, + line-height: 40px, + // letter-spacing: -0.00833em + ), + 'h3': ( + size: 24px, + weight: 400, + line-height: 32px, + // letter-spacing: normal + ), + 'h4': ( + size: 20px, + weight: 400, + line-height: 30px, + // letter-spacing: 0.00735em + ), + 'h5': ( + size: 16px, + weight: 400, + line-height: 24px, + // letter-spacing: normal + ), + 'h6': ( + size: 14px, + weight: 500, + line-height: 18px, + // letter-spacing: 0.0125em + ), + 'subtitle': ( + size: 12px, + weight: 400, + line-height: 20px, + // letter-spacing: 0.00937em + ), + 'body': ( + font-size: 14px, + font-weight: 400, + line-height: 22px, + // letter-spacing: 0.03125em + ), + 'caption': ( + 'size': 12px, + 'weight': 400, + 'line-height': 20px, + // 'letter-spacing': 0.03333em, + // 'text-transform': false + ) + ), + $uni-headings +); + + + +// 主色 +$uni-primary: #2979ff !default; +$uni-primary-disable:lighten($uni-primary,20%) !default; +$uni-primary-light: lighten($uni-primary,25%) !default; + +// 辅助色 +// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。 +$uni-success: #18bc37 !default; +$uni-success-disable:lighten($uni-success,20%) !default; +$uni-success-light: lighten($uni-success,25%) !default; + +$uni-warning: #f3a73f !default; +$uni-warning-disable:lighten($uni-warning,20%) !default; +$uni-warning-light: lighten($uni-warning,25%) !default; + +$uni-error: #e43d33 !default; +$uni-error-disable:lighten($uni-error,20%) !default; +$uni-error-light: lighten($uni-error,25%) !default; + +$uni-info: #8f939c !default; +$uni-info-disable:lighten($uni-info,20%) !default; +$uni-info-light: lighten($uni-info,25%) !default; + +// 中性色 +// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。 +$uni-main-color: #3a3a3a !default; // 主要文字 +$uni-base-color: #6a6a6a !default; // 常规文字 +$uni-secondary-color: #909399 !default; // 次要文字 +$uni-extra-color: #c7c7c7 !default; // 辅助说明 + +// 边框颜色 +$uni-border-1: #F0F0F0 !default; +$uni-border-2: #EDEDED !default; +$uni-border-3: #DCDCDC !default; +$uni-border-4: #B9B9B9 !default; + +// 常规色 +$uni-black: #000000 !default; +$uni-white: #ffffff !default; +$uni-transparent: rgba($color: #000000, $alpha: 0) !default; + +// 背景色 +$uni-bg-color: #f7f7f7 !default; + +/* 水平间距 */ +$uni-spacing-sm: 8px !default; +$uni-spacing-base: 15px !default; +$uni-spacing-lg: 30px !default; + +// 阴影 +$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5) !default; +$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2) !default; +$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5) !default; + +// 蒙版 +$uni-mask: rgba($color: #000000, $alpha: 0.4) !default; diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/tools/functions.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/tools/functions.scss new file mode 100644 index 0000000..ac6f63e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/styles/tools/functions.scss @@ -0,0 +1,19 @@ +// 合并 map +@function map-deep-merge($parent-map, $child-map){ + $result: $parent-map; + @each $key, $child in $child-map { + $parent-has-key: map-has-key($result, $key); + $parent-value: map-get($result, $key); + $parent-type: type-of($parent-value); + $child-type: type-of($child); + $parent-is-map: $parent-type == map; + $child-is-map: $child-type == map; + + @if (not $parent-has-key) or ($parent-type != $child-type) or (not ($parent-is-map and $child-is-map)){ + $result: map-merge($result, ( $key: $child )); + }@else { + $result: map-merge($result, ( $key: map-deep-merge($parent-value, $child) )); + } + } + @return $result; +}; diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/theme.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/theme.scss new file mode 100644 index 0000000..80ee62f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/theme.scss @@ -0,0 +1,31 @@ +// 间距基础倍数 +$uni-space-root: 2; +// 边框半径默认值 +$uni-radius-root:5px; +// 主色 +$uni-primary: #2979ff; +// 辅助色 +$uni-success: #4cd964; +// 警告色 +$uni-warning: #f0ad4e; +// 错误色 +$uni-error: #dd524d; +// 描述色 +$uni-info: #909399; +// 中性色 +$uni-main-color: #303133; +$uni-base-color: #606266; +$uni-secondary-color: #909399; +$uni-extra-color: #C0C4CC; +// 背景色 +$uni-bg-color: #f5f5f5; +// 边框颜色 +$uni-border-1: #DCDFE6; +$uni-border-2: #E4E7ED; +$uni-border-3: #EBEEF5; +$uni-border-4: #F2F6FC; + +// 常规色 +$uni-black: #000000; +$uni-white: #ffffff; +$uni-transparent: rgba($color: #000000, $alpha: 0); diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-scss/variables.scss b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/variables.scss new file mode 100644 index 0000000..1c062d4 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-scss/variables.scss @@ -0,0 +1,62 @@ +@import './styles/setting/_variables.scss'; +// 间距基础倍数 +$uni-space-root: 2; +// 边框半径默认值 +$uni-radius-root:5px; + +// 主色 +$uni-primary: #2979ff; +$uni-primary-disable:mix(#fff,$uni-primary,50%); +$uni-primary-light: mix(#fff,$uni-primary,80%); + +// 辅助色 +// 除了主色外的场景色,需要在不同的场景中使用(例如危险色表示危险的操作)。 +$uni-success: #18bc37; +$uni-success-disable:mix(#fff,$uni-success,50%); +$uni-success-light: mix(#fff,$uni-success,80%); + +$uni-warning: #f3a73f; +$uni-warning-disable:mix(#fff,$uni-warning,50%); +$uni-warning-light: mix(#fff,$uni-warning,80%); + +$uni-error: #e43d33; +$uni-error-disable:mix(#fff,$uni-error,50%); +$uni-error-light: mix(#fff,$uni-error,80%); + +$uni-info: #8f939c; +$uni-info-disable:mix(#fff,$uni-info,50%); +$uni-info-light: mix(#fff,$uni-info,80%); + +// 中性色 +// 中性色用于文本、背景和边框颜色。通过运用不同的中性色,来表现层次结构。 +$uni-main-color: #3a3a3a; // 主要文字 +$uni-base-color: #6a6a6a; // 常规文字 +$uni-secondary-color: #909399; // 次要文字 +$uni-extra-color: #c7c7c7; // 辅助说明 + +// 边框颜色 +$uni-border-1: #F0F0F0; +$uni-border-2: #EDEDED; +$uni-border-3: #DCDCDC; +$uni-border-4: #B9B9B9; + +// 常规色 +$uni-black: #000000; +$uni-white: #ffffff; +$uni-transparent: rgba($color: #000000, $alpha: 0); + +// 背景色 +$uni-bg-color: #f7f7f7; + +/* 水平间距 */ +$uni-spacing-sm: 8px; +$uni-spacing-base: 15px; +$uni-spacing-lg: 30px; + +// 阴影 +$uni-shadow-sm:0 0 5px rgba($color: #d8d8d8, $alpha: 0.5); +$uni-shadow-base:0 1px 8px 1px rgba($color: #a5a5a5, $alpha: 0.2); +$uni-shadow-lg:0px 1px 10px 2px rgba($color: #a5a4a4, $alpha: 0.5); + +// 蒙版 +$uni-mask: rgba($color: #000000, $alpha: 0.4); diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/changelog.md new file mode 100644 index 0000000..b41fdd3 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/changelog.md @@ -0,0 +1,33 @@ +## 1.2.3(2022-05-24) +- 新增 readonly 属性,组件只读 +## 1.2.2(2022-05-06) +- 修复 vue3 input 事件不生效的bug +## 1.2.1(2022-05-06) +- 修复 多余代码导致的bug +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-search-bar](https://uniapp.dcloud.io/component/uniui/uni-search-bar) +## 1.1.2(2021-08-30) +- 修复 value 属性与 modelValue 属性不兼容的Bug +## 1.1.1(2021-08-24) +- 新增 支持国际化 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.9(2021-05-12) +- 新增 项目示例地址 +## 1.0.8(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.7(2021-04-15) +- uni-ui 新增 uni-search-bar 的 focus 事件 + +## 1.0.6(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.5(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 支持双向绑定 +- 更改 input 事件的返回值,e={value:Number} --> e=value +- 新增 支持图标插槽 +- 新增 支持 clear、blur 事件 +- 新增 支持 focus 属性 +- 去掉组件背景色 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/en.json new file mode 100644 index 0000000..dd083a5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/en.json @@ -0,0 +1,4 @@ +{ + "uni-search-bar.cancel": "cancel", + "uni-search-bar.placeholder": "Search enter content" +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/index.js new file mode 100644 index 0000000..de7509c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/index.js @@ -0,0 +1,8 @@ +import en from './en.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hans.json new file mode 100644 index 0000000..d4e5c12 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hans.json @@ -0,0 +1,4 @@ +{ + "uni-search-bar.cancel": "cancel", + "uni-search-bar.placeholder": "请输入搜索内容" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hant.json new file mode 100644 index 0000000..318b6ef --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/i18n/zh-Hant.json @@ -0,0 +1,4 @@ +{ + "uni-search-bar.cancel": "cancel", + "uni-search-bar.placeholder": "請輸入搜索內容" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/uni-search-bar.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/uni-search-bar.vue new file mode 100644 index 0000000..5a518a8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/components/uni-search-bar/uni-search-bar.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/package.json new file mode 100644 index 0000000..9352c57 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-search-bar", + "displayName": "uni-search-bar 搜索栏", + "version": "1.2.3", + "description": "搜索栏组件,通常用于搜索商品、文章等", + "keywords": [ + "uni-ui", + "uniui", + "搜索框", + "搜索栏" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/readme.md new file mode 100644 index 0000000..253092f --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-search-bar/readme.md @@ -0,0 +1,14 @@ + + +## SearchBar 搜索栏 + +> **组件名:uni-search-bar** +> 代码块: `uSearchBar` + + +搜索栏组件 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-search-bar) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/changelog.md new file mode 100644 index 0000000..a44385d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/changelog.md @@ -0,0 +1,9 @@ +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-segmented-control](https://uniapp.dcloud.io/component/uniui/uni-segmented-control) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.5(2021-05-12) +- 新增 项目示例地址 +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/components/uni-segmented-control/uni-segmented-control.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/components/uni-segmented-control/uni-segmented-control.vue new file mode 100644 index 0000000..ddbcf88 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/components/uni-segmented-control/uni-segmented-control.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/package.json new file mode 100644 index 0000000..6cae41d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-segmented-control", + "displayName": "uni-segmented-control 分段器", + "version": "1.2.0", + "description": "分段器由至少 2 个分段控件组成,用作不同视图的显示", + "keywords": [ + "uni-ui", + "uniui", + "分段器", + "segement", + "顶部选择" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/readme.md new file mode 100644 index 0000000..3527b03 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-segmented-control/readme.md @@ -0,0 +1,13 @@ + + +## SegmentedControl 分段器 +> **组件名:uni-segmented-control** +> 代码块: `uSegmentedControl` + + +用作不同视图的显示 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-segmented-control) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-steps/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-steps/changelog.md new file mode 100644 index 0000000..cb9d367 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-steps/changelog.md @@ -0,0 +1,16 @@ +## 1.1.1(2021-11-22) +- 修复 vue3中某些scss变量无法找到的问题 +## 1.1.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-steps](https://uniapp.dcloud.io/component/uniui/uni-steps) +## 1.0.8(2021-05-12) +- 新增 项目示例地址 +## 1.0.7(2021-05-06) +- 修复 uni-steps 横向布局时,多行文字高度不合理的 bug +## 1.0.6(2021-04-21) +- 优化 添加依赖 uni-icons, 导入后自动下载依赖 +## 1.0.5(2021-02-05) +- 优化 组件引用关系,通过uni_modules引用组件 + +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-steps/components/uni-steps/uni-steps.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-steps/components/uni-steps/uni-steps.vue new file mode 100644 index 0000000..a6c8f28 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-steps/components/uni-steps/uni-steps.vue @@ -0,0 +1,269 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-steps/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-steps/package.json new file mode 100644 index 0000000..c687b40 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-steps/package.json @@ -0,0 +1,89 @@ +{ + "id": "uni-steps", + "displayName": "uni-steps 步骤条", + "version": "1.1.1", + "description": "步骤条组件,提供横向和纵向两种布局格式。", + "keywords": [ + "uni-ui", + "uniui", + "步骤条", + "时间轴" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": [ + "uni-scss", + "uni-icons" + ], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-steps/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-steps/readme.md new file mode 100644 index 0000000..da7a4bf --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-steps/readme.md @@ -0,0 +1,13 @@ + + +## Steps 步骤条 +> **组件名:uni-steps** +> 代码块: `uSteps` + + +步骤条,常用于显示进度 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-steps) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/changelog.md new file mode 100644 index 0000000..c007cb5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/changelog.md @@ -0,0 +1,41 @@ +## 1.3.7(2022-06-06) +- 修复 vue3 下使用组件不能正常运行的Bug +## 1.3.6(2022-05-31) +- 修复 h5端点击click触发两次的Bug +## 1.3.5(2022-05-23) +- 修复 isPC 找不到的Bug +## 1.3.4(2022-05-19) +- 修复 在 nvue 下 disabled 失效的bug +## 1.3.3(2022-03-31) +- 修复 按钮字体大小不能设置的bug +## 1.3.2(2022-03-16) +- 修复 h5和app端下报el错误的bug +## 1.3.1(2022-03-07) +- 修复 HBuilderX 1.4.X 版本中,h5和app端下报错的bug +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-swipe-action](https://uniapp.dcloud.io/component/uniui/uni-swipe-action) +## 1.2.4(2021-08-20) +- 优化 close-all 方法 +## 1.2.3(2021-08-20) +- 新增 close-all 方法,关闭所有已打开的组件 +## 1.2.2(2021-08-17) +- 新增 resize() 方法,在非微信小程序、h5、app-vue端出现不能滑动的问题的时候,重置组件 +- 修复 app 端偶尔出现类似 Page[x][-x,xx;-x,xx,x,x-x] 的问题 +- 优化 微信小程序、h5、app-vue 滑动逻辑,避免出现动态新增组件后不能滑动的问题 +## 1.2.1(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +- 修复 跨页面修改组件数据 ,导致不能滑动的问题 +## 1.1.10(2021-06-17) +- 修复 按钮点击执行两次的bug +## 1.1.9(2021-05-12) +- 新增 项目示例地址 +## 1.1.8(2021-03-26) +- 修复 微信小程序 nv_navigator is not defined 报错的bug +## 1.1.7(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 左侧滑动 +- 新增 插槽使用方式 +- 新增 threshold 属性,可以控制滑动缺省值 +- 优化 长列表滚动性能 +- 修复 滚动页面时触发组件滑动的Bug diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/bindingx.js b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/bindingx.js new file mode 100644 index 0000000..755c97c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/bindingx.js @@ -0,0 +1,302 @@ +let bindIngXMixins = {} + +// #ifdef APP-NVUE +const BindingX = uni.requireNativePlugin('bindingx'); +const dom = uni.requireNativePlugin('dom'); +const animation = uni.requireNativePlugin('animation'); + +bindIngXMixins = { + data() { + return {} + }, + + watch: { + show(newVal) { + if (this.autoClose) return + if (this.stop) return + this.stop = true + if (newVal) { + this.open(newVal) + } else { + this.close() + } + }, + leftOptions() { + this.getSelectorQuery() + this.init() + }, + rightOptions(newVal) { + this.init() + } + }, + created() { + this.swipeaction = this.getSwipeAction() + if (this.swipeaction.children !== undefined) { + this.swipeaction.children.push(this) + } + }, + mounted() { + this.box = this.getEl(this.$refs['selector-box--hock']) + this.selector = this.getEl(this.$refs['selector-content--hock']); + this.leftButton = this.getEl(this.$refs['selector-left-button--hock']); + this.rightButton = this.getEl(this.$refs['selector-right-button--hock']); + this.init() + }, + // beforeDestroy() { + // this.swipeaction.children.forEach((item, index) => { + // if (item === this) { + // this.swipeaction.children.splice(index, 1) + // } + // }) + // }, + methods: { + init() { + this.$nextTick(() => { + this.x = 0 + this.button = { + show: false + } + setTimeout(() => { + this.getSelectorQuery() + }, 200) + }) + }, + onClick(index, item, position) { + this.$emit('click', { + content: item, + index, + position + }) + }, + touchstart(e) { + // fix by mehaotian 禁止滑动 + if (this.disabled) return + // 每次只触发一次,避免多次监听造成闪烁 + if (this.stop) return + this.stop = true + if (this.autoClose) { + this.swipeaction.closeOther(this) + } + + const leftWidth = this.button.left.width + const rightWidth = this.button.right.width + let expression = this.range(this.x, -rightWidth, leftWidth) + let leftExpression = this.range(this.x - leftWidth, -leftWidth, 0) + let rightExpression = this.range(this.x + rightWidth, 0, rightWidth) + + this.eventpan = BindingX.bind({ + anchor: this.box, + eventType: 'pan', + props: [{ + element: this.selector, + property: 'transform.translateX', + expression + }, { + element: this.leftButton, + property: 'transform.translateX', + expression: leftExpression + }, { + element: this.rightButton, + property: 'transform.translateX', + expression: rightExpression + }, ] + }, (e) => { + // nope + if (e.state === 'end') { + this.x = e.deltaX + this.x; + this.isclick = true + this.bindTiming(e.deltaX) + } + }); + }, + touchend(e) { + if (this.isopen !== 'none' && !this.isclick) { + this.open('none') + } + }, + bindTiming(x) { + const left = this.x + const leftWidth = this.button.left.width + const rightWidth = this.button.right.width + const threshold = this.threshold + if (!this.isopen || this.isopen === 'none') { + if (left > threshold) { + this.open('left') + } else if (left < -threshold) { + this.open('right') + } else { + this.open('none') + } + } else { + if ((x > -leftWidth && x < 0) || x > rightWidth) { + if ((x > -threshold && x < 0) || (x - rightWidth > threshold)) { + this.open('left') + } else { + this.open('none') + } + } else { + if ((x < threshold && x > 0) || (x + leftWidth < -threshold)) { + this.open('right') + } else { + this.open('none') + } + } + } + }, + + /** + * 移动范围 + * @param {Object} num + * @param {Object} mix + * @param {Object} max + */ + range(num, mix, max) { + return `min(max(x+${num}, ${mix}), ${max})` + }, + + /** + * 开启swipe + */ + open(type) { + this.animation(type) + }, + + /** + * 关闭swipe + */ + close() { + this.animation('none') + }, + + /** + * 开启关闭动画 + * @param {Object} type + */ + animation(type) { + const time = 300 + const leftWidth = this.button.left.width + const rightWidth = this.button.right.width + if (this.eventpan && this.eventpan.token) { + BindingX.unbind({ + token: this.eventpan.token, + eventType: 'pan' + }) + } + + switch (type) { + case 'left': + Promise.all([ + this.move(this.selector, leftWidth), + this.move(this.leftButton, 0), + this.move(this.rightButton, rightWidth * 2) + ]).then(() => { + this.setEmit(leftWidth, type) + }) + break + case 'right': + Promise.all([ + this.move(this.selector, -rightWidth), + this.move(this.leftButton, -leftWidth * 2), + this.move(this.rightButton, 0) + ]).then(() => { + this.setEmit(-rightWidth, type) + }) + break + default: + Promise.all([ + this.move(this.selector, 0), + this.move(this.leftButton, -leftWidth), + this.move(this.rightButton, rightWidth) + ]).then(() => { + this.setEmit(0, type) + }) + + } + }, + setEmit(x, type) { + const leftWidth = this.button.left.width + const rightWidth = this.button.right.width + this.isopen = this.isopen || 'none' + this.stop = false + this.isclick = false + // 只有状态不一致才会返回结果 + if (this.isopen !== type && this.x !== x) { + if (type === 'left' && leftWidth > 0) { + this.$emit('change', 'left') + } + if (type === 'right' && rightWidth > 0) { + this.$emit('change', 'right') + } + if (type === 'none') { + this.$emit('change', 'none') + } + } + this.x = x + this.isopen = type + }, + move(ref, value) { + return new Promise((resolve, reject) => { + animation.transition(ref, { + styles: { + transform: `translateX(${value})`, + }, + duration: 150, //ms + timingFunction: 'linear', + needLayout: false, + delay: 0 //ms + }, function(res) { + resolve(res) + }) + }) + + }, + + /** + * 获取ref + * @param {Object} el + */ + getEl(el) { + return el.ref + }, + /** + * 获取节点信息 + */ + getSelectorQuery() { + Promise.all([ + this.getDom('left'), + this.getDom('right'), + ]).then((data) => { + let show = 'none' + if (this.autoClose) { + show = 'none' + } else { + show = this.show + } + + if (show === 'none') { + // this.close() + } else { + this.open(show) + } + + }) + + }, + getDom(str) { + return new Promise((resolve, reject) => { + dom.getComponentRect(this.$refs[`selector-${str}-button--hock`], (data) => { + if (data) { + this.button[str] = data.size + resolve(data) + } else { + reject() + } + }) + }) + } + } +} + +// #endif + +export default bindIngXMixins diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/isPC.js b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/isPC.js new file mode 100644 index 0000000..917cb48 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/isPC.js @@ -0,0 +1,12 @@ +export function isPC() { + var userAgentInfo = navigator.userAgent; + var Agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]; + var flag = true; + for (let v = 0; v < Agents.length - 1; v++) { + if (userAgentInfo.indexOf(Agents[v]) > 0) { + flag = false; + break; + } + } + return flag; +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpalipay.js b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpalipay.js new file mode 100644 index 0000000..43cd56b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpalipay.js @@ -0,0 +1,193 @@ +export default { + data() { + return { + x: 0, + transition: false, + width: 0, + viewWidth: 0, + swipeShow: 0 + } + }, + watch: { + show(newVal) { + if (this.autoClose) return + if (newVal && newVal !== 'none') { + this.transition = true + this.open(newVal) + } else { + this.close() + } + } + }, + created() { + this.swipeaction = this.getSwipeAction() + if (this.swipeaction.children !== undefined) { + this.swipeaction.children.push(this) + } + }, + mounted() { + this.isopen = false + setTimeout(() => { + this.getQuerySelect() + }, 50) + }, + methods: { + appTouchStart(e) { + const { + clientX + } = e.changedTouches[0] + this.clientX = clientX + this.timestamp = new Date().getTime() + }, + appTouchEnd(e, index, item, position) { + const { + clientX + } = e.changedTouches[0] + // fixed by xxxx 模拟点击事件,解决 ios 13 点击区域错位的问题 + let diff = Math.abs(this.clientX - clientX) + let time = (new Date().getTime()) - this.timestamp + if (diff < 40 && time < 300) { + this.$emit('click', { + content: item, + index, + position + }) + } + }, + /** + * 移动触发 + * @param {Object} e + */ + onChange(e) { + this.moveX = e.detail.x + this.isclose = false + }, + touchstart(e) { + this.transition = false + this.isclose = true + this.autoClose && this.swipeaction.closeOther(this) + }, + touchmove(e) {}, + touchend(e) { + // 0的位置什么都不执行 + if (this.isclose && this.isopen === 'none') return + if (this.isclose && this.isopen !== 'none') { + this.transition = true + this.close() + } else { + this.move(this.moveX + this.leftWidth) + } + }, + + /** + * 移动 + * @param {Object} moveX + */ + move(moveX) { + // 打开关闭的处理逻辑不太一样 + this.transition = true + // 未打开状态 + if (!this.isopen || this.isopen === 'none') { + if (moveX > this.threshold) { + this.open('left') + } else if (moveX < -this.threshold) { + this.open('right') + } else { + this.close() + } + } else { + if (moveX < 0 && moveX < this.rightWidth) { + const rightX = this.rightWidth + moveX + if (rightX < this.threshold) { + this.open('right') + } else { + this.close() + } + } else if (moveX > 0 && moveX < this.leftWidth) { + const leftX = this.leftWidth - moveX + if (leftX < this.threshold) { + this.open('left') + } else { + this.close() + } + } + + } + + }, + + /** + * 打开 + */ + open(type) { + this.x = this.moveX + this.animation(type) + }, + + /** + * 关闭 + */ + close() { + this.x = this.moveX + // TODO 解决 x 值不更新的问题,所以会多触发一次 nextTick ,待优化 + this.$nextTick(() => { + this.x = -this.leftWidth + if (this.isopen !== 'none') { + this.$emit('change', 'none') + } + this.isopen = 'none' + }) + }, + + /** + * 执行结束动画 + * @param {Object} type + */ + animation(type) { + this.$nextTick(() => { + if (type === 'left') { + this.x = 0 + } else { + this.x = -this.rightWidth - this.leftWidth + } + + if (this.isopen !== type) { + this.$emit('change', type) + } + this.isopen = type + }) + + }, + getSlide(x) {}, + getQuerySelect() { + const query = uni.createSelectorQuery().in(this); + query.selectAll('.movable-view--hock').boundingClientRect(data => { + this.leftWidth = data[1].width + this.rightWidth = data[2].width + this.width = data[0].width + this.viewWidth = this.width + this.rightWidth + this.leftWidth + if (this.leftWidth === 0) { + // TODO 疑似bug ,初始化的时候如果x 是0,会导致移动位置错误,所以让元素超出一点 + this.x = -0.1 + } else { + this.x = -this.leftWidth + } + this.moveX = this.x + this.$nextTick(() => { + this.swipeShow = 1 + }) + + if (!this.buttonWidth) { + this.disabledView = true + } + + if (this.autoClose) return + if (this.show !== 'none') { + this.transition = true + this.open(this.shows) + } + }).exec(); + + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpother.js b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpother.js new file mode 100644 index 0000000..9a8bcbb --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpother.js @@ -0,0 +1,259 @@ +let otherMixins = {} + +// #ifndef APP-PLUS|| MP-WEIXIN || H5 +const MIN_DISTANCE = 10; +otherMixins = { + data() { + // TODO 随机生生元素ID,解决百度小程序获取同一个元素位置信息的bug + const elClass = `Uni_${Math.ceil(Math.random() * 10e5).toString(36)}` + return { + uniShow: false, + left: 0, + buttonShow: 'none', + ani: false, + moveLeft: '', + elClass + } + }, + watch: { + show(newVal) { + if (this.autoClose) return + this.openState(newVal) + }, + left() { + this.moveLeft = `translateX(${this.left}px)` + }, + buttonShow(newVal) { + if (this.autoClose) return + this.openState(newVal) + }, + leftOptions() { + this.init() + }, + rightOptions() { + this.init() + } + }, + mounted() { + this.swipeaction = this.getSwipeAction() + if (this.swipeaction.children !== undefined) { + this.swipeaction.children.push(this) + } + this.init() + }, + methods: { + init() { + clearTimeout(this.timer) + this.timer = setTimeout(() => { + this.getSelectorQuery() + }, 100) + // 移动距离 + this.left = 0 + this.x = 0 + }, + + closeSwipe(e) { + if (!this.autoClose) return + this.swipeaction.closeOther(this) + }, + appTouchStart(e) { + const { + clientX + } = e.changedTouches[0] + this.clientX = clientX + this.timestamp = new Date().getTime() + }, + appTouchEnd(e, index, item, position) { + const { + clientX + } = e.changedTouches[0] + // fixed by xxxx 模拟点击事件,解决 ios 13 点击区域错位的问题 + let diff = Math.abs(this.clientX - clientX) + let time = (new Date().getTime()) - this.timestamp + if (diff < 40 && time < 300) { + this.$emit('click', { + content: item, + index, + position + }) + } + }, + touchstart(e) { + if (this.disabled) return + this.ani = false + this.x = this.left || 0 + this.stopTouchStart(e) + this.autoClose && this.closeSwipe() + }, + touchmove(e) { + if (this.disabled) return + // 是否可以滑动页面 + this.stopTouchMove(e); + if (this.direction !== 'horizontal') { + return; + } + this.move(this.x + this.deltaX) + return false + }, + touchend() { + if (this.disabled) return + this.moveDirection(this.left) + }, + /** + * 设置移动距离 + * @param {Object} value + */ + move(value) { + value = value || 0 + const leftWidth = this.leftWidth + const rightWidth = this.rightWidth + // 获取可滑动范围 + this.left = this.range(value, -rightWidth, leftWidth); + }, + + /** + * 获取范围 + * @param {Object} num + * @param {Object} min + * @param {Object} max + */ + range(num, min, max) { + return Math.min(Math.max(num, min), max); + }, + /** + * 移动方向判断 + * @param {Object} left + * @param {Object} value + */ + moveDirection(left) { + const threshold = this.threshold + const isopen = this.isopen || 'none' + const leftWidth = this.leftWidth + const rightWidth = this.rightWidth + if (this.deltaX === 0) { + this.openState('none') + return + } + if ((isopen === 'none' && rightWidth > 0 && -left > threshold) || (isopen !== 'none' && rightWidth > + 0 && rightWidth + + left < threshold)) { + // right + this.openState('right') + } else if ((isopen === 'none' && leftWidth > 0 && left > threshold) || (isopen !== 'none' && leftWidth > + 0 && + leftWidth - left < threshold)) { + // left + this.openState('left') + } else { + // default + this.openState('none') + } + }, + + /** + * 开启状态 + * @param {Boolean} type + */ + openState(type) { + const leftWidth = this.leftWidth + const rightWidth = this.rightWidth + let left = '' + this.isopen = this.isopen ? this.isopen : 'none' + switch (type) { + case "left": + left = leftWidth + break + case "right": + left = -rightWidth + break + default: + left = 0 + } + + + if (this.isopen !== type) { + this.throttle = true + this.$emit('change', type) + } + + this.isopen = type + // 添加动画类 + this.ani = true + this.$nextTick(() => { + this.move(left) + }) + // 设置最终移动位置,理论上只要进入到这个函数,肯定是要打开的 + }, + close() { + this.openState('none') + }, + getDirection(x, y) { + if (x > y && x > MIN_DISTANCE) { + return 'horizontal'; + } + if (y > x && y > MIN_DISTANCE) { + return 'vertical'; + } + return ''; + }, + + /** + * 重置滑动状态 + * @param {Object} event + */ + resetTouchStatus() { + this.direction = ''; + this.deltaX = 0; + this.deltaY = 0; + this.offsetX = 0; + this.offsetY = 0; + }, + + /** + * 设置滑动开始位置 + * @param {Object} event + */ + stopTouchStart(event) { + this.resetTouchStatus(); + const touch = event.touches[0]; + this.startX = touch.clientX; + this.startY = touch.clientY; + }, + + /** + * 滑动中,是否禁止打开 + * @param {Object} event + */ + stopTouchMove(event) { + const touch = event.touches[0]; + this.deltaX = touch.clientX - this.startX; + this.deltaY = touch.clientY - this.startY; + this.offsetX = Math.abs(this.deltaX); + this.offsetY = Math.abs(this.deltaY); + this.direction = this.direction || this.getDirection(this.offsetX, this.offsetY); + }, + + getSelectorQuery() { + const views = uni.createSelectorQuery().in(this) + views + .selectAll('.' + this.elClass) + .boundingClientRect(data => { + if (data.length === 0) return + let show = 'none' + if (this.autoClose) { + show = 'none' + } else { + show = this.show + } + this.leftWidth = data[0].width || 0 + this.rightWidth = data[1].width || 0 + this.buttonShow = show + }) + .exec() + } + } +} + +// #endif + +export default otherMixins diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpwxs.js b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpwxs.js new file mode 100644 index 0000000..435e0fb --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/mpwxs.js @@ -0,0 +1,83 @@ +let mpMixins = {} +let is_pc = null +// #ifdef H5 +import { + isPC +} from "./isPC" +is_pc = isPC() +// #endif +// #ifdef APP-VUE|| MP-WEIXIN || H5 + +mpMixins = { + data() { + return { + is_show: 'none' + } + }, + watch: { + show(newVal) { + this.is_show = this.show + } + }, + created() { + this.swipeaction = this.getSwipeAction() + if (this.swipeaction.children !== undefined) { + this.swipeaction.children.push(this) + } + }, + mounted() { + this.is_show = this.show + }, + methods: { + // wxs 中调用 + closeSwipe(e) { + if (!this.autoClose) return + this.swipeaction.closeOther(this) + }, + + change(e) { + this.$emit('change', e.open) + if (this.is_show !== e.open) { + this.is_show = e.open + } + }, + + appTouchStart(e) { + if (is_pc) return + const { + clientX + } = e.changedTouches[0] + this.clientX = clientX + this.timestamp = new Date().getTime() + }, + appTouchEnd(e, index, item, position) { + if (is_pc) return + const { + clientX + } = e.changedTouches[0] + // fixed by xxxx 模拟点击事件,解决 ios 13 点击区域错位的问题 + let diff = Math.abs(this.clientX - clientX) + let time = (new Date().getTime()) - this.timestamp + if (diff < 40 && time < 300) { + this.$emit('click', { + content: item, + index, + position + }) + } + }, + onClickForPC(index, item, position) { + if (!is_pc) return + // #ifdef H5 + this.$emit('click', { + content: item, + index, + position + }) + // #endif + } + } +} + +// #endif +export default mpMixins diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/render.js b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/render.js new file mode 100644 index 0000000..78f0ec6 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/render.js @@ -0,0 +1,270 @@ +const MIN_DISTANCE = 10; +export default { + showWatch(newVal, oldVal, ownerInstance, instance, self) { + var state = self.state + var $el = ownerInstance.$el || ownerInstance.$vm && ownerInstance.$vm.$el + if (!$el) return + this.getDom(instance, ownerInstance, self) + if (newVal && newVal !== 'none') { + this.openState(newVal, instance, ownerInstance, self) + return + } + + if (state.left) { + this.openState('none', instance, ownerInstance, self) + } + this.resetTouchStatus(instance, self) + }, + + /** + * 开始触摸操作 + * @param {Object} e + * @param {Object} ins + */ + touchstart(e, ownerInstance, self) { + let instance = e.instance; + let disabled = instance.getDataset().disabled + let state = self.state; + this.getDom(instance, ownerInstance, self) + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = this.getDisabledType(disabled) + if (disabled) return + // 开始触摸时移除动画类 + instance.requestAnimationFrame(function() { + instance.removeClass('ani'); + ownerInstance.callMethod('closeSwipe'); + }) + + // 记录上次的位置 + state.x = state.left || 0 + // 计算滑动开始位置 + this.stopTouchStart(e, ownerInstance, self) + }, + + /** + * 开始滑动操作 + * @param {Object} e + * @param {Object} ownerInstance + */ + touchmove(e, ownerInstance, self) { + let instance = e.instance; + // 删除之后已经那不到实例了 + if (!instance) return; + let disabled = instance.getDataset().disabled + let state = self.state + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = this.getDisabledType(disabled) + if (disabled) return + // 是否可以滑动页面 + this.stopTouchMove(e, self); + if (state.direction !== 'horizontal') { + return; + } + if (e.preventDefault) { + // 阻止页面滚动 + e.preventDefault() + } + let x = state.x + state.deltaX + this.move(x, instance, ownerInstance, self) + }, + + /** + * 结束触摸操作 + * @param {Object} e + * @param {Object} ownerInstance + */ + touchend(e, ownerInstance, self) { + let instance = e.instance; + let disabled = instance.getDataset().disabled + let state = self.state + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = this.getDisabledType(disabled) + + if (disabled) return + // 滑动过程中触摸结束,通过阙值判断是开启还是关闭 + // fixed by mehaotian 定时器解决点击按钮,touchend 触发比 click 事件时机早的问题 ,主要是 ios13 + this.moveDirection(state.left, instance, ownerInstance, self) + + }, + + /** + * 设置移动距离 + * @param {Object} value + * @param {Object} instance + * @param {Object} ownerInstance + */ + move(value, instance, ownerInstance, self) { + value = value || 0 + let state = self.state + let leftWidth = state.leftWidth + let rightWidth = state.rightWidth + // 获取可滑动范围 + state.left = this.range(value, -rightWidth, leftWidth); + instance.requestAnimationFrame(function() { + instance.setStyle({ + transform: 'translateX(' + state.left + 'px)', + '-webkit-transform': 'translateX(' + state.left + 'px)' + }) + }) + + }, + + /** + * 获取元素信息 + * @param {Object} instance + * @param {Object} ownerInstance + */ + getDom(instance, ownerInstance, self) { + var state = self.state + var $el = ownerInstance.$el || ownerInstance.$vm && ownerInstance.$vm.$el + var leftDom = $el.querySelector('.button-group--left') + var rightDom = $el.querySelector('.button-group--right') + + state.leftWidth = leftDom.offsetWidth || 0 + state.rightWidth = rightDom.offsetWidth || 0 + state.threshold = instance.getDataset().threshold + }, + + getDisabledType(value) { + return (typeof(value) === 'string' ? JSON.parse(value) : value) || false; + }, + + /** + * 获取范围 + * @param {Object} num + * @param {Object} min + * @param {Object} max + */ + range(num, min, max) { + return Math.min(Math.max(num, min), max); + }, + + + /** + * 移动方向判断 + * @param {Object} left + * @param {Object} value + * @param {Object} ownerInstance + * @param {Object} ins + */ + moveDirection(left, ins, ownerInstance, self) { + var state = self.state + var threshold = state.threshold + var position = state.position + var isopen = state.isopen || 'none' + var leftWidth = state.leftWidth + var rightWidth = state.rightWidth + if (state.deltaX === 0) { + this.openState('none', ins, ownerInstance, self) + return + } + if ((isopen === 'none' && rightWidth > 0 && -left > threshold) || (isopen !== 'none' && rightWidth > 0 && + rightWidth + + left < threshold)) { + // right + this.openState('right', ins, ownerInstance, self) + } else if ((isopen === 'none' && leftWidth > 0 && left > threshold) || (isopen !== 'none' && leftWidth > 0 && + leftWidth - left < threshold)) { + // left + this.openState('left', ins, ownerInstance, self) + } else { + // default + this.openState('none', ins, ownerInstance, self) + } + }, + + + /** + * 开启状态 + * @param {Boolean} type + * @param {Object} ins + * @param {Object} ownerInstance + */ + openState(type, ins, ownerInstance, self) { + let state = self.state + let leftWidth = state.leftWidth + let rightWidth = state.rightWidth + let left = '' + state.isopen = state.isopen ? state.isopen : 'none' + switch (type) { + case "left": + left = leftWidth + break + case "right": + left = -rightWidth + break + default: + left = 0 + } + + // && !state.throttle + + if (state.isopen !== type) { + state.throttle = true + ownerInstance.callMethod('change', { + open: type + }) + + } + + state.isopen = type + // 添加动画类 + ins.requestAnimationFrame(() => { + ins.addClass('ani'); + this.move(left, ins, ownerInstance, self) + }) + }, + + + getDirection(x, y) { + if (x > y && x > MIN_DISTANCE) { + return 'horizontal'; + } + if (y > x && y > MIN_DISTANCE) { + return 'vertical'; + } + return ''; + }, + + /** + * 重置滑动状态 + * @param {Object} event + */ + resetTouchStatus(instance, self) { + let state = self.state; + state.direction = ''; + state.deltaX = 0; + state.deltaY = 0; + state.offsetX = 0; + state.offsetY = 0; + }, + + /** + * 设置滑动开始位置 + * @param {Object} event + */ + stopTouchStart(event, ownerInstance, self) { + let instance = event.instance; + let state = self.state + this.resetTouchStatus(instance, self); + var touch = event.touches[0]; + state.startX = touch.clientX; + state.startY = touch.clientY; + }, + + /** + * 滑动中,是否禁止打开 + * @param {Object} event + */ + stopTouchMove(event, self) { + let instance = event.instance; + let state = self.state; + let touch = event.touches[0]; + + state.deltaX = touch.clientX - state.startX; + state.deltaY = touch.clientY - state.startY; + state.offsetY = Math.abs(state.deltaY); + state.offsetX = Math.abs(state.deltaX); + state.direction = state.direction || this.getDirection(state.offsetX, state.offsetY); + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/uni-swipe-action-item.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/uni-swipe-action-item.vue new file mode 100644 index 0000000..d79c297 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/uni-swipe-action-item.vue @@ -0,0 +1,347 @@ + + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/wx.wxs b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/wx.wxs new file mode 100644 index 0000000..b394244 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action-item/wx.wxs @@ -0,0 +1,341 @@ +var MIN_DISTANCE = 10; + +/** + * 判断当前是否为H5、app-vue + */ +var IS_HTML5 = false +if (typeof window === 'object') IS_HTML5 = true + +/** + * 监听页面内值的变化,主要用于动态开关swipe-action + * @param {Object} newValue + * @param {Object} oldValue + * @param {Object} ownerInstance + * @param {Object} instance + */ +function showWatch(newVal, oldVal, ownerInstance, instance) { + var state = instance.getState() + getDom(instance, ownerInstance) + if (newVal && newVal !== 'none') { + openState(newVal, instance, ownerInstance) + return + } + + if (state.left) { + openState('none', instance, ownerInstance) + } + resetTouchStatus(instance) +} + +/** + * 开始触摸操作 + * @param {Object} e + * @param {Object} ins + */ +function touchstart(e, ownerInstance) { + var instance = e.instance; + var disabled = instance.getDataset().disabled + var state = instance.getState(); + getDom(instance, ownerInstance) + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false; + if (disabled) return + // 开始触摸时移除动画类 + instance.requestAnimationFrame(function() { + instance.removeClass('ani'); + ownerInstance.callMethod('closeSwipe'); + }) + + // 记录上次的位置 + state.x = state.left || 0 + // 计算滑动开始位置 + stopTouchStart(e, ownerInstance) +} + +/** + * 开始滑动操作 + * @param {Object} e + * @param {Object} ownerInstance + */ +function touchmove(e, ownerInstance) { + var instance = e.instance; + var disabled = instance.getDataset().disabled + var state = instance.getState() + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false; + if (disabled) return + // 是否可以滑动页面 + stopTouchMove(e); + if (state.direction !== 'horizontal') { + return; + } + + if (e.preventDefault) { + // 阻止页面滚动 + e.preventDefault() + } + + move(state.x + state.deltaX, instance, ownerInstance) +} + +/** + * 结束触摸操作 + * @param {Object} e + * @param {Object} ownerInstance + */ +function touchend(e, ownerInstance) { + var instance = e.instance; + var disabled = instance.getDataset().disabled + var state = instance.getState() + // fix by mehaotian, TODO 兼容 app-vue 获取dataset为字符串 , h5 获取 为 undefined 的问题,待框架修复 + disabled = (typeof(disabled) === 'string' ? JSON.parse(disabled) : disabled) || false; + + if (disabled) return + // 滑动过程中触摸结束,通过阙值判断是开启还是关闭 + // fixed by mehaotian 定时器解决点击按钮,touchend 触发比 click 事件时机早的问题 ,主要是 ios13 + moveDirection(state.left, instance, ownerInstance) + +} + +/** + * 设置移动距离 + * @param {Object} value + * @param {Object} instance + * @param {Object} ownerInstance + */ +function move(value, instance, ownerInstance) { + value = value || 0 + var state = instance.getState() + var leftWidth = state.leftWidth + var rightWidth = state.rightWidth + // 获取可滑动范围 + state.left = range(value, -rightWidth, leftWidth); + instance.requestAnimationFrame(function() { + instance.setStyle({ + transform: 'translateX(' + state.left + 'px)', + '-webkit-transform': 'translateX(' + state.left + 'px)' + }) + }) + +} + +/** + * 获取元素信息 + * @param {Object} instance + * @param {Object} ownerInstance + */ +function getDom(instance, ownerInstance) { + var state = instance.getState() + var leftDom = ownerInstance.selectComponent('.button-group--left') + var rightDom = ownerInstance.selectComponent('.button-group--right') + var leftStyles = { + width: 0 + } + var rightStyles = { + width: 0 + } + leftStyles = leftDom.getBoundingClientRect() + rightStyles = rightDom.getBoundingClientRect() + + state.leftWidth = leftStyles.width || 0 + state.rightWidth = rightStyles.width || 0 + state.threshold = instance.getDataset().threshold +} + +/** + * 获取范围 + * @param {Object} num + * @param {Object} min + * @param {Object} max + */ +function range(num, min, max) { + return Math.min(Math.max(num, min), max); +} + + +/** + * 移动方向判断 + * @param {Object} left + * @param {Object} value + * @param {Object} ownerInstance + * @param {Object} ins + */ +function moveDirection(left, ins, ownerInstance) { + var state = ins.getState() + var threshold = state.threshold + var position = state.position + var isopen = state.isopen || 'none' + var leftWidth = state.leftWidth + var rightWidth = state.rightWidth + if (state.deltaX === 0) { + openState('none', ins, ownerInstance) + return + } + if ((isopen === 'none' && rightWidth > 0 && -left > threshold) || (isopen !== 'none' && rightWidth > 0 && + rightWidth + + left < threshold)) { + // right + openState('right', ins, ownerInstance) + } else if ((isopen === 'none' && leftWidth > 0 && left > threshold) || (isopen !== 'none' && leftWidth > 0 && + leftWidth - left < threshold)) { + // left + openState('left', ins, ownerInstance) + } else { + // default + openState('none', ins, ownerInstance) + } +} + + +/** + * 开启状态 + * @param {Boolean} type + * @param {Object} ins + * @param {Object} ownerInstance + */ +function openState(type, ins, ownerInstance) { + var state = ins.getState() + var leftWidth = state.leftWidth + var rightWidth = state.rightWidth + var left = '' + state.isopen = state.isopen ? state.isopen : 'none' + switch (type) { + case "left": + left = leftWidth + break + case "right": + left = -rightWidth + break + default: + left = 0 + } + + // && !state.throttle + + if (state.isopen !== type) { + state.throttle = true + ownerInstance.callMethod('change', { + open: type + }) + + } + + state.isopen = type + // 添加动画类 + ins.requestAnimationFrame(function() { + ins.addClass('ani'); + move(left, ins, ownerInstance) + }) + // 设置最终移动位置,理论上只要进入到这个函数,肯定是要打开的 +} + + +function getDirection(x, y) { + if (x > y && x > MIN_DISTANCE) { + return 'horizontal'; + } + if (y > x && y > MIN_DISTANCE) { + return 'vertical'; + } + return ''; +} + +/** + * 重置滑动状态 + * @param {Object} event + */ +function resetTouchStatus(instance) { + var state = instance.getState(); + state.direction = ''; + state.deltaX = 0; + state.deltaY = 0; + state.offsetX = 0; + state.offsetY = 0; +} + +/** + * 设置滑动开始位置 + * @param {Object} event + */ +function stopTouchStart(event) { + var instance = event.instance; + var state = instance.getState(); + resetTouchStatus(instance); + var touch = event.touches[0]; + if (IS_HTML5 && isPC()) { + touch = event; + } + state.startX = touch.clientX; + state.startY = touch.clientY; +} + +/** + * 滑动中,是否禁止打开 + * @param {Object} event + */ +function stopTouchMove(event) { + var instance = event.instance; + var state = instance.getState(); + var touch = event.touches[0]; + if (IS_HTML5 && isPC()) { + touch = event; + } + state.deltaX = touch.clientX - state.startX; + state.deltaY = touch.clientY - state.startY; + state.offsetY = Math.abs(state.deltaY); + state.offsetX = Math.abs(state.deltaX); + state.direction = state.direction || getDirection(state.offsetX, state.offsetY); +} + +function isPC() { + var userAgentInfo = navigator.userAgent; + var Agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]; + var flag = true; + for (var v = 0; v < Agents.length - 1; v++) { + if (userAgentInfo.indexOf(Agents[v]) > 0) { + flag = false; + break; + } + } + return flag; +} + +var movable = false + +function mousedown(e, ins) { + if (!IS_HTML5) return + if (!isPC()) return + touchstart(e, ins) + movable = true +} + +function mousemove(e, ins) { + if (!IS_HTML5) return + if (!isPC()) return + if (!movable) return + touchmove(e, ins) +} + +function mouseup(e, ins) { + if (!IS_HTML5) return + if (!isPC()) return + touchend(e, ins) + movable = false +} + +function mouseleave(e, ins) { + if (!IS_HTML5) return + if (!isPC()) return + movable = false +} + +module.exports = { + showWatch: showWatch, + touchstart: touchstart, + touchmove: touchmove, + touchend: touchend, + mousedown: mousedown, + mousemove: mousemove, + mouseup: mouseup, + mouseleave: mouseleave +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action/uni-swipe-action.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action/uni-swipe-action.vue new file mode 100644 index 0000000..4971782 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/components/uni-swipe-action/uni-swipe-action.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/package.json new file mode 100644 index 0000000..c8998d9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-swipe-action", + "displayName": "uni-swipe-action 滑动操作", + "version": "1.3.7", + "description": "SwipeAction 滑动操作操作组件", + "keywords": [ + "", + "uni-ui", + "uniui", + "滑动删除", + "侧滑删除" + ], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "y", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/readme.md new file mode 100644 index 0000000..93a5cac --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swipe-action/readme.md @@ -0,0 +1,11 @@ + + +## SwipeAction 滑动操作 +> **组件名:uni-swipe-action** +> 代码块: `uSwipeAction`、`uSwipeActionItem` + + +通过滑动触发选项的容器 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-swipe-action) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/changelog.md new file mode 100644 index 0000000..85cf54d --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/changelog.md @@ -0,0 +1,12 @@ +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-swiper-dot](https://uniapp.dcloud.io/component/uniui/uni-swiper-dot) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.6(2021-05-12) +- 新增 示例地址 +- 修复 示例项目缺少组件的Bug +## 1.0.5(2021-02-05) +- 调整为uni_modules目录规范 +- 新增 clickItem 事件,支持指示点控制轮播 +- 新增 支持 pc 可用 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/components/uni-swiper-dot/uni-swiper-dot.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/components/uni-swiper-dot/uni-swiper-dot.vue new file mode 100644 index 0000000..e5befae --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/components/uni-swiper-dot/uni-swiper-dot.vue @@ -0,0 +1,218 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/package.json new file mode 100644 index 0000000..f2dd8d2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-swiper-dot", + "displayName": "uni-swiper-dot 轮播图指示点", + "version": "1.2.0", + "description": "自定义轮播图指示点组件", + "keywords": [ + "uni-ui", + "uniui", + "轮播图指示点", + "dot", + "swiper" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/readme.md new file mode 100644 index 0000000..7d397e2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-swiper-dot/readme.md @@ -0,0 +1,11 @@ + + +## SwiperDot 轮播图指示点 +> **组件名:uni-swiper-dot** +> 代码块: `uSwiperDot` + + +自定义轮播图指示点 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-swiper-dot) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-table/changelog.md new file mode 100644 index 0000000..8233b20 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/changelog.md @@ -0,0 +1,23 @@ +## 1.2.1(2022-06-06) +- 修复 微信小程序存在无使用组件的问题 +## 1.2.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-table](https://uniapp.dcloud.io/component/uniui/uni-table) +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-07-08) +- 新增 uni-th 支持 date 日期筛选范围 +## 1.0.6(2021-07-05) +- 新增 uni-th 支持 range 筛选范围 +## 1.0.5(2021-06-28) +- 新增 uni-th 筛选功能 +## 1.0.4(2021-05-12) +- 新增 示例地址 +- 修复 示例项目缺少组件的Bug +## 1.0.3(2021-04-16) +- 新增 sortable 属性,是否开启单列排序 +- 优化 表格多选逻辑 +## 1.0.2(2021-03-22) +- uni-tr 添加 disabled 属性,用于 type=selection 时,设置某行是否可由全选按钮控制 +## 1.0.1(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-table/uni-table.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-table/uni-table.vue new file mode 100644 index 0000000..91b74fa --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-table/uni-table.vue @@ -0,0 +1,455 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tbody/uni-tbody.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tbody/uni-tbody.vue new file mode 100644 index 0000000..fbe1bdc --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tbody/uni-tbody.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-td/uni-td.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-td/uni-td.vue new file mode 100644 index 0000000..9ce93e9 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-td/uni-td.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-th/filter-dropdown.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-th/filter-dropdown.vue new file mode 100644 index 0000000..bc9a0e3 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-th/filter-dropdown.vue @@ -0,0 +1,503 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-th/uni-th.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-th/uni-th.vue new file mode 100644 index 0000000..883e3f2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-th/uni-th.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-thead/uni-thead.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-thead/uni-thead.vue new file mode 100644 index 0000000..0dd18cd --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-thead/uni-thead.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tr/table-checkbox.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tr/table-checkbox.vue new file mode 100644 index 0000000..158f3ff --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tr/table-checkbox.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tr/uni-tr.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tr/uni-tr.vue new file mode 100644 index 0000000..f9b9671 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/components/uni-tr/uni-tr.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/en.json b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/en.json new file mode 100644 index 0000000..e32023c --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/en.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "Reset", + "filter-dropdown.search": "Search", + "filter-dropdown.submit": "Submit", + "filter-dropdown.filter": "Filter", + "filter-dropdown.gt": "Greater or equal to", + "filter-dropdown.lt": "Less than or equal to", + "filter-dropdown.date": "Date" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/es.json b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/es.json new file mode 100644 index 0000000..9afd04b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/es.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "Reiniciar", + "filter-dropdown.search": "Búsqueda", + "filter-dropdown.submit": "Entregar", + "filter-dropdown.filter": "Filtrar", + "filter-dropdown.gt": "Mayor o igual a", + "filter-dropdown.lt": "Menos que o igual a", + "filter-dropdown.date": "Fecha" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/fr.json b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/fr.json new file mode 100644 index 0000000..b006237 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/fr.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "Réinitialiser", + "filter-dropdown.search": "Chercher", + "filter-dropdown.submit": "Soumettre", + "filter-dropdown.filter": "Filtre", + "filter-dropdown.gt": "Supérieur ou égal à", + "filter-dropdown.lt": "Inférieur ou égal à", + "filter-dropdown.date": "Date" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/index.js b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/index.js new file mode 100644 index 0000000..2469dd0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/index.js @@ -0,0 +1,12 @@ +import en from './en.json' +import es from './es.json' +import fr from './fr.json' +import zhHans from './zh-Hans.json' +import zhHant from './zh-Hant.json' +export default { + en, + es, + fr, + 'zh-Hans': zhHans, + 'zh-Hant': zhHant +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/zh-Hans.json b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/zh-Hans.json new file mode 100644 index 0000000..862af17 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/zh-Hans.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "重置", + "filter-dropdown.search": "搜索", + "filter-dropdown.submit": "确定", + "filter-dropdown.filter": "筛选", + "filter-dropdown.gt": "大于等于", + "filter-dropdown.lt": "小于等于", + "filter-dropdown.date": "日期范围" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/zh-Hant.json b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/zh-Hant.json new file mode 100644 index 0000000..64f8061 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/i18n/zh-Hant.json @@ -0,0 +1,9 @@ +{ + "filter-dropdown.reset": "重置", + "filter-dropdown.search": "搜索", + "filter-dropdown.submit": "確定", + "filter-dropdown.filter": "篩選", + "filter-dropdown.gt": "大於等於", + "filter-dropdown.lt": "小於等於", + "filter-dropdown.date": "日期範圍" +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-table/package.json new file mode 100644 index 0000000..f224ab7 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/package.json @@ -0,0 +1,86 @@ +{ + "id": "uni-table", + "displayName": "uni-table 表格", + "version": "1.2.1", + "description": "表格组件,多用于展示多条结构类似的数据,如", + "keywords": [ + "uni-ui", + "uniui", + "table", + "表格" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss","uni-datetime-picker"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "n" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "n", + "QQ": "y" + }, + "快应用": { + "华为": "n", + "联盟": "n" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-table/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-table/readme.md new file mode 100644 index 0000000..bb08c79 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-table/readme.md @@ -0,0 +1,13 @@ + + +## Table 表单 +> 组件名:``uni-table``,代码块: `uTable`。 + +用于展示多条结构类似的数据 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-table) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-tag/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-tag/changelog.md new file mode 100644 index 0000000..c0c5839 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-tag/changelog.md @@ -0,0 +1,21 @@ +## 2.1.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-tag](https://uniapp.dcloud.io/component/uniui/uni-tag) +## 2.0.0(2021-11-09) +- 新增 提供组件设计资源,组件样式调整 +- 移除 插槽 +- 移除 type 属性的 royal 选项 +## 1.1.1(2021-08-11) +- type 不是 default 时,size 为 small 字体大小显示不正确 +## 1.1.0(2021-07-30) +- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.0.7(2021-06-18) +- 修复 uni-tag 在字节跳动小程序上 css 类名编译错误的 bug +## 1.0.6(2021-06-04) +- 修复 未定义 sass 变量 "$uni-color-royal" 的bug +## 1.0.5(2021-05-10) +- 修复 royal 类型无效的bug +- 修复 uni-tag 宽度不自适应的bug +- 新增 uni-tag 支持属性 custom-style 自定义样式 +## 1.0.4(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-tag/components/uni-tag/uni-tag.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-tag/components/uni-tag/uni-tag.vue new file mode 100644 index 0000000..6378a0b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-tag/components/uni-tag/uni-tag.vue @@ -0,0 +1,252 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-tag/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-tag/package.json new file mode 100644 index 0000000..1878088 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-tag/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-tag", + "displayName": "uni-tag 标签", + "version": "2.1.0", + "description": "Tag 组件,用于展示1个或多个文字标签,可点击切换选中、不选中的状态。", + "keywords": [ + "uni-ui", + "uniui", + "", + "tag", + "标签" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-tag/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-tag/readme.md new file mode 100644 index 0000000..6e78ff5 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-tag/readme.md @@ -0,0 +1,13 @@ + + +## Tag 标签 +> **组件名:uni-tag** +> 代码块: `uTag` + + +用于展示1个或多个文字标签,可点击切换选中、不选中的状态 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-tag) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-title/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-title/changelog.md new file mode 100644 index 0000000..7626216 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-title/changelog.md @@ -0,0 +1,10 @@ +## 1.1.1(2022-05-19) +- 修改组件描述 +## 1.1.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-title](https://uniapp.dcloud.io/component/uniui/uni-title) +## 1.0.2(2021-05-12) +- 新增 示例地址 +- 修复 示例项目缺少组件的Bug +## 1.0.1(2021-02-05) +- 调整为uni_modules目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-title/components/uni-title/uni-title.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-title/components/uni-title/uni-title.vue new file mode 100644 index 0000000..bf4f926 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-title/components/uni-title/uni-title.vue @@ -0,0 +1,171 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-title/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-title/package.json new file mode 100644 index 0000000..2249f5a --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-title/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-title", + "displayName": "uni-title 章节标题", + "version": "1.1.1", + "description": "章节标题,通常用于记录页面标题,使用当前组件,uni-app 如果开启统计,将会自动统计页面标题", + "keywords": [ + "uni-ui", + "uniui", + "标题", + "章节", + "章节标题", + "" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-title/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-title/readme.md new file mode 100644 index 0000000..0e60b1b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-title/readme.md @@ -0,0 +1,14 @@ + + +## Title 标题 +> **组件名:uni-title** +> 代码块: `uTitle` + + +章节标题,通常用于记录页面标题,使用当前组件,uni-app 如果开启统计,将会自动统计页面标题 。 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-title) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/changelog.md new file mode 100644 index 0000000..00f1572 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/changelog.md @@ -0,0 +1,10 @@ +## 0.2.1(2022-05-09) +- 修复 content 为空时仍然弹出的bug +## 0.2.0(2022-05-07) +**注意:破坏性更新** +- 更新 text 属性变更为 content +- 更新 移除 width 属性 +## 0.1.1(2022-04-27) +- 修复 组件根 text 嵌套组件 warning +## 0.1.0(2022-04-21) +- 初始化 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue new file mode 100644 index 0000000..ffbb6fa --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/components/uni-tooltip/uni-tooltip.vue @@ -0,0 +1,68 @@ + + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/package.json new file mode 100644 index 0000000..e88ecf8 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/package.json @@ -0,0 +1,88 @@ +{ + "id": "uni-tooltip", + "displayName": "uni-tooltip 提示文字", + "version": "0.2.1", + "description": "Tooltip 提示文字", + "keywords": [ + "uni-tooltip", + "uni-ui", + "tooltip", + "tip", + "文字提示" + ], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无 ", + "data": "无", + "permissions": "无" + }, + "npmurl": "" + }, + "uni_modules": { + "dependencies": [], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "Vue": { + "vue2": "y", + "vue3": "y" + }, + "App": { + "app-vue": "y", + "app-nvue": "u" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "u", + "百度": "u", + "字节跳动": "u", + "QQ": "u", + "京东": "u" + }, + "快应用": { + "华为": "u", + "联盟": "u" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/readme.md new file mode 100644 index 0000000..faafa2e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-tooltip/readme.md @@ -0,0 +1,8 @@ +## Badge 数字角标 +> **组件名:uni-tooltip** +> 代码块: `uTooltip` + +数字角标一般和其它控件(列表、9宫格等)配合使用,用于进行数量提示,默认为实心灰色背景, + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-tooltip) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-transition/changelog.md b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/changelog.md new file mode 100644 index 0000000..b1a824b --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/changelog.md @@ -0,0 +1,20 @@ +## 1.3.1(2021-11-23) +- 修复 init 方法初始化问题 +## 1.3.0(2021-11-19) +- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource) +- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-transition](https://uniapp.dcloud.io/component/uniui/uni-transition) +## 1.2.1(2021-09-27) +- 修复 init 方法不生效的 Bug +## 1.2.0(2021-07-30) +- 组件兼容 vue3,如何创建 vue3 项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834) +## 1.1.1(2021-05-12) +- 新增 示例地址 +- 修复 示例项目缺少组件的 Bug +## 1.1.0(2021-04-22) +- 新增 通过方法自定义动画 +- 新增 custom-class 非 NVUE 平台支持自定义 class 定制样式 +- 优化 动画触发逻辑,使动画更流畅 +- 优化 支持单独的动画类型 +- 优化 文档示例 +## 1.0.2(2021-02-05) +- 调整为 uni_modules 目录规范 diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-transition/components/uni-transition/createAnimation.js b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/components/uni-transition/createAnimation.js new file mode 100644 index 0000000..5f54365 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/components/uni-transition/createAnimation.js @@ -0,0 +1,128 @@ +// const defaultOption = { +// duration: 300, +// timingFunction: 'linear', +// delay: 0, +// transformOrigin: '50% 50% 0' +// } +// #ifdef APP-NVUE +const nvueAnimation = uni.requireNativePlugin('animation') +// #endif +class MPAnimation { + constructor(options, _this) { + this.options = options + this.animation = uni.createAnimation(options) + this.currentStepAnimates = {} + this.next = 0 + this.$ = _this + + } + + _nvuePushAnimates(type, args) { + let aniObj = this.currentStepAnimates[this.next] + let styles = {} + if (!aniObj) { + styles = { + styles: {}, + config: {} + } + } else { + styles = aniObj + } + if (animateTypes1.includes(type)) { + if (!styles.styles.transform) { + styles.styles.transform = '' + } + let unit = '' + if(type === 'rotate'){ + unit = 'deg' + } + styles.styles.transform += `${type}(${args+unit}) ` + } else { + styles.styles[type] = `${args}` + } + this.currentStepAnimates[this.next] = styles + } + _animateRun(styles = {}, config = {}) { + let ref = this.$.$refs['ani'].ref + if (!ref) return + return new Promise((resolve, reject) => { + nvueAnimation.transition(ref, { + styles, + ...config + }, res => { + resolve() + }) + }) + } + + _nvueNextAnimate(animates, step = 0, fn) { + let obj = animates[step] + if (obj) { + let { + styles, + config + } = obj + this._animateRun(styles, config).then(() => { + step += 1 + this._nvueNextAnimate(animates, step, fn) + }) + } else { + this.currentStepAnimates = {} + typeof fn === 'function' && fn() + this.isEnd = true + } + } + + step(config = {}) { + // #ifndef APP-NVUE + this.animation.step(config) + // #endif + // #ifdef APP-NVUE + this.currentStepAnimates[this.next].config = Object.assign({}, this.options, config) + this.currentStepAnimates[this.next].styles.transformOrigin = this.currentStepAnimates[this.next].config.transformOrigin + this.next++ + // #endif + return this + } + + run(fn) { + // #ifndef APP-NVUE + this.$.animationData = this.animation.export() + this.$.timer = setTimeout(() => { + typeof fn === 'function' && fn() + }, this.$.durationTime) + // #endif + // #ifdef APP-NVUE + this.isEnd = false + let ref = this.$.$refs['ani'] && this.$.$refs['ani'].ref + if(!ref) return + this._nvueNextAnimate(this.currentStepAnimates, 0, fn) + this.next = 0 + // #endif + } +} + + +const animateTypes1 = ['matrix', 'matrix3d', 'rotate', 'rotate3d', 'rotateX', 'rotateY', 'rotateZ', 'scale', 'scale3d', + 'scaleX', 'scaleY', 'scaleZ', 'skew', 'skewX', 'skewY', 'translate', 'translate3d', 'translateX', 'translateY', + 'translateZ' +] +const animateTypes2 = ['opacity', 'backgroundColor'] +const animateTypes3 = ['width', 'height', 'left', 'right', 'top', 'bottom'] +animateTypes1.concat(animateTypes2, animateTypes3).forEach(type => { + MPAnimation.prototype[type] = function(...args) { + // #ifndef APP-NVUE + this.animation[type](...args) + // #endif + // #ifdef APP-NVUE + this._nvuePushAnimates(type, args) + // #endif + return this + } +}) + +export function createAnimation(option, _this) { + if(!_this) return + clearTimeout(_this.timer) + return new MPAnimation(option, _this) +} diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-transition/components/uni-transition/uni-transition.vue b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/components/uni-transition/uni-transition.vue new file mode 100644 index 0000000..0d739bd --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/components/uni-transition/uni-transition.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-transition/package.json b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/package.json new file mode 100644 index 0000000..d15fdf0 --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/package.json @@ -0,0 +1,87 @@ +{ + "id": "uni-transition", + "displayName": "uni-transition 过渡动画", + "version": "1.3.1", + "description": "元素的简单过渡动画", + "keywords": [ + "uni-ui", + "uniui", + "动画", + "过渡", + "过渡动画" +], + "repository": "https://github.com/dcloudio/uni-ui", + "engines": { + "HBuilderX": "" + }, + "directories": { + "example": "../../temps/example_temps" + }, + "dcloudext": { + "category": [ + "前端组件", + "通用组件" + ], + "sale": { + "regular": { + "price": "0.00" + }, + "sourcecode": { + "price": "0.00" + } + }, + "contact": { + "qq": "" + }, + "declaration": { + "ads": "无", + "data": "无", + "permissions": "无" + }, + "npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui" + }, + "uni_modules": { + "dependencies": ["uni-scss"], + "encrypt": [], + "platforms": { + "cloud": { + "tcb": "y", + "aliyun": "y" + }, + "client": { + "App": { + "app-vue": "y", + "app-nvue": "y" + }, + "H5-mobile": { + "Safari": "y", + "Android Browser": "y", + "微信浏览器(Android)": "y", + "QQ浏览器(Android)": "y" + }, + "H5-pc": { + "Chrome": "y", + "IE": "y", + "Edge": "y", + "Firefox": "y", + "Safari": "y" + }, + "小程序": { + "微信": "y", + "阿里": "y", + "百度": "y", + "字节跳动": "y", + "QQ": "y" + }, + "快应用": { + "华为": "u", + "联盟": "u" + }, + "Vue": { + "vue2": "y", + "vue3": "y" + } + } + } + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/uni_modules/uni-transition/readme.md b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/readme.md new file mode 100644 index 0000000..2f8a77e --- /dev/null +++ b/yunxi-ui-admin-uniapp/uni_modules/uni-transition/readme.md @@ -0,0 +1,11 @@ + + +## Transition 过渡动画 +> **组件名:uni-transition** +> 代码块: `uTransition` + + +元素过渡动画 + +### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-transition) +#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839 \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/utils/auth.js b/yunxi-ui-admin-uniapp/utils/auth.js new file mode 100644 index 0000000..cf27e27 --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/auth.js @@ -0,0 +1,22 @@ +const AccessTokenKey = 'ACCESS_TOKEN' +const RefreshTokenKey = 'REFRESH_TOKEN' + +// ========== Token 相关 ========== + +export function getAccessToken() { + return uni.getStorageSync(AccessTokenKey) +} + +export function getRefreshToken() { + return uni.getStorageSync(RefreshTokenKey) +} + +export function setToken(token) { + uni.setStorageSync(AccessTokenKey, token.accessToken) + uni.setStorageSync(RefreshTokenKey, token.refreshToken) +} + +export function removeToken() { + uni.removeStorageSync(AccessTokenKey) + uni.removeStorageSync(RefreshTokenKey) +} diff --git a/yunxi-ui-admin-uniapp/utils/common.js b/yunxi-ui-admin-uniapp/utils/common.js new file mode 100644 index 0000000..00d4137 --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/common.js @@ -0,0 +1,54 @@ +/** +* 显示消息提示框 +* @param content 提示的标题 +*/ +export function toast(content) { + uni.showToast({ + icon: 'none', + title: content + }) +} + +/** +* 显示模态弹窗 +* @param content 提示的标题 +*/ +export function showConfirm(content) { + return new Promise((resolve, reject) => { + uni.showModal({ + title: '提示', + content: content, + cancelText: '取消', + confirmText: '确定', + success: function(res) { + resolve(res) + } + }) + }) +} + +/** +* 参数处理 +* @param params 参数 +*/ +export function tansParams(params) { + let result = '' + for (const propName of Object.keys(params)) { + const value = params[propName] + var part = encodeURIComponent(propName) + "=" + if (value !== null && value !== "" && typeof (value) !== "undefined") { + if (typeof value === 'object') { + for (const key of Object.keys(value)) { + if (value[key] !== null && value[key] !== "" && typeof (value[key]) !== 'undefined') { + let params = propName + '[' + key + ']' + var subPart = encodeURIComponent(params) + "=" + result += subPart + encodeURIComponent(value[key]) + "&" + } + } + } else { + result += part + encodeURIComponent(value) + "&" + } + } + } + return result +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/utils/constant.js b/yunxi-ui-admin-uniapp/utils/constant.js new file mode 100644 index 0000000..8becd84 --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/constant.js @@ -0,0 +1,8 @@ +const constant = { + avatar: 'vuex_avatar', + name: 'vuex_name', + roles: 'vuex_roles', + permissions: 'vuex_permissions' + } + + export default constant diff --git a/yunxi-ui-admin-uniapp/utils/errorCode.js b/yunxi-ui-admin-uniapp/utils/errorCode.js new file mode 100644 index 0000000..d2111ee --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/errorCode.js @@ -0,0 +1,6 @@ +export default { + '401': '认证失败,无法访问系统资源', + '403': '当前操作没有权限', + '404': '访问资源不存在', + 'default': '系统未知错误,请反馈给管理员' +} diff --git a/yunxi-ui-admin-uniapp/utils/permission.js b/yunxi-ui-admin-uniapp/utils/permission.js new file mode 100644 index 0000000..17969f2 --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/permission.js @@ -0,0 +1,51 @@ +import store from '@/store' + +/** + * 字符权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkPermi(value) { + if (value && value instanceof Array && value.length > 0) { + const permissions = store.getters && store.getters.permissions + const permissionDatas = value + const all_permission = "*:*:*" + + const hasPermission = permissions.some(permission => { + return all_permission === permission || permissionDatas.includes(permission) + }) + + if (!hasPermission) { + return false + } + return true + } else { + console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`) + return false + } +} + +/** + * 角色权限校验 + * @param {Array} value 校验值 + * @returns {Boolean} + */ +export function checkRole(value) { + if (value && value instanceof Array && value.length > 0) { + const roles = store.getters && store.getters.roles + const permissionRoles = value + const super_admin = "admin" + + const hasRole = roles.some(role => { + return super_admin === role || permissionRoles.includes(role) + }) + + if (!hasRole) { + return false + } + return true + } else { + console.error(`need roles! Like checkRole="['admin','editor']"`) + return false + } +} \ No newline at end of file diff --git a/yunxi-ui-admin-uniapp/utils/request.js b/yunxi-ui-admin-uniapp/utils/request.js new file mode 100644 index 0000000..4799e74 --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/request.js @@ -0,0 +1,76 @@ +import store from '@/store' +import config from '@/config' +import { getAccessToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { toast, showConfirm, tansParams } from '@/utils/common' + +let timeout = 10000 +const baseUrl = config.baseUrl + config.baseApi; + +const request = config => { + // 是否需要设置 token + const isToken = (config.headers || {}).isToken === false + config.header = config.header || {} + if (getAccessToken() && !isToken) { + config.header['Authorization'] = 'Bearer ' + getAccessToken() + } + // 设置租户 TODO 芋艿:强制 1 先 + config.header['tenant-id'] = '1'; + // get请求映射params参数 + if (config.params) { + let url = config.url + '?' + tansParams(config.params) + url = url.slice(0, -1) + config.url = url + } + return new Promise((resolve, reject) => { + uni.request({ + method: config.method || 'get', + timeout: config.timeout || timeout, + url: config.baseUrl || baseUrl + config.url, + data: config.data, + // header: config.header, + header: config.header, + dataType: 'json' + }).then(response => { + let [error, res] = response + if (error) { + toast('后端接口连接异常') + reject('后端接口连接异常') + return + } + const code = res.data.code || 200 + const msg = errorCode[code] || res.data.msg || errorCode['default'] + if (code === 401) { + showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(res => { + if (res.confirm) { + store.dispatch('LogOut').then(res => { + uni.reLaunch({ url: '/pages/login' }) + }) + } + }) + reject('无效的会话,或者会话已过期,请重新登录。') + } else if (code === 500) { + toast(msg) + reject('500') + } else if (code !== 200) { + toast(msg) + reject(code) + } + resolve(res.data) + }) + .catch(error => { + let { message } = error + if (message === 'Network Error') { + message = '后端接口连接异常' + } else if (message.includes('timeout')) { + message = '系统接口请求超时' + } else if (message.includes('Request failed with status code')) { + message = '系统接口' + message.substr(message.length - 3) + '异常' + } + toast(message) + reject(error) + }) + }) +} + +export default request diff --git a/yunxi-ui-admin-uniapp/utils/ruoyi.js b/yunxi-ui-admin-uniapp/utils/ruoyi.js new file mode 100644 index 0000000..fb60217 --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/ruoyi.js @@ -0,0 +1,47 @@ +/** + * 通用js方法封装处理 + * Copyright (c) 2019 ruoyi + */ + +// 日期格式化 +export function parseTime(time, pattern) { + if (arguments.length === 0 || !time) { + return null + } + const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}' + let date + if (typeof time === 'object') { + date = time + } else { + if ((typeof time === 'string') && (/^[0-9]+$/.test(time))) { + time = parseInt(time) + } else if (typeof time === 'string') { + time = time.replace(new RegExp(/-/gm), '/').replace('T', ' ').replace(new RegExp(/\.[\d]{3}/gm),''); + } + if ((typeof time === 'number') && (time.toString().length === 10)) { + time = time * 1000 + } + date = new Date(time) + } + const formatObj = { + y: date.getFullYear(), + m: date.getMonth() + 1, + d: date.getDate(), + h: date.getHours(), + i: date.getMinutes(), + s: date.getSeconds(), + a: date.getDay() + } + const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key) => { + let value = formatObj[key] + // Note: getDay() returns 0 on Sunday + if (key === 'a') { + return ['日', '一', '二', '三', '四', '五', '六'][value] + } + if (result.length > 0 && value < 10) { + value = '0' + value + } + return value || 0 + }) + return time_str +} diff --git a/yunxi-ui-admin-uniapp/utils/storage.js b/yunxi-ui-admin-uniapp/utils/storage.js new file mode 100644 index 0000000..dd5c38b --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/storage.js @@ -0,0 +1,33 @@ +import constant from './constant' + +// 存储变量名 +let storageKey = 'storage_data' + +// 存储节点变量名 +let storageNodeKeys = [constant.avatar, constant.name, constant.roles, constant.permissions] + +// 存储的数据 +let storageData = uni.getStorageSync(storageKey) || {} + +const storage = { + set: function(key, value) { + if (storageNodeKeys.indexOf(key) != -1) { + let tmp = uni.getStorageSync(storageKey) + tmp = tmp ? tmp : {} + tmp[key] = value + uni.setStorageSync(storageKey, tmp) + } + }, + get: function(key) { + return storageData[key] || "" + }, + remove: function(key) { + delete storageData[key] + uni.setStorageSync(storageKey, storageData) + }, + clean: function() { + uni.removeStorageSync(storageKey) + } +} + +export default storage diff --git a/yunxi-ui-admin-uniapp/utils/upload.js b/yunxi-ui-admin-uniapp/utils/upload.js new file mode 100644 index 0000000..fb6b379 --- /dev/null +++ b/yunxi-ui-admin-uniapp/utils/upload.js @@ -0,0 +1,73 @@ +import store from '@/store' +import config from '@/config' +import { getAccessToken } from '@/utils/auth' +import errorCode from '@/utils/errorCode' +import { toast, showConfirm, tansParams } from '@/utils/common' + +let timeout = 10000 +const baseUrl = config.baseUrl + +const upload = config => { + // 是否需要设置 token + const isToken = (config.headers || {}).isToken === false + config.header = config.header || {} + if (getAccessToken() && !isToken) { + config.header['Authorization'] = 'Bearer ' + getAccessToken() + } + // get请求映射params参数 + if (config.params) { + let url = config.url + '?' + tansParams(config.params) + url = url.slice(0, -1) + config.url = url + } + // 设置租户 TODO 芋艿:强制 1 先 + config.header['tenant-id'] = '1'; + return new Promise((resolve, reject) => { + uni.uploadFile({ + timeout: config.timeout || timeout, + url: baseUrl + config.url, + filePath: config.filePath, + name: config.name || 'file', + header: config.header, + formData: config.formData, + method: config.method || 'post', + success: (res) => { + let result = JSON.parse(res.data) + const code = result.code || 200 + const msg = errorCode[code] || result.msg || errorCode['default'] + if (code === 200) { + resolve(result) + } else if (code == 401) { + showConfirm("登录状态已过期,您可以继续留在该页面,或者重新登录?").then(res => { + if (res.confirm) { + store.dispatch('LogOut').then(res => { + uni.reLaunch({ url: '/pages/login/login' }) + }) + } + }) + reject('无效的会话,或者会话已过期,请重新登录。') + } else if (code === 500) { + toast(msg) + reject('500') + } else if (code !== 200) { + toast(msg) + reject(code) + } + }, + fail: (error) => { + let { message } = error + if (message == 'Network Error') { + message = '后端接口连接异常' + } else if (message.includes('timeout')) { + message = '系统接口请求超时' + } else if (message.includes('Request failed with status code')) { + message = '系统接口' + message.substr(message.length - 3) + '异常' + } + toast(message) + reject(error) + } + }) + }) +} + +export default upload diff --git a/yunxi-ui-admin/.dockerignore b/yunxi-ui-admin/.dockerignore new file mode 100644 index 0000000..ddc40ed --- /dev/null +++ b/yunxi-ui-admin/.dockerignore @@ -0,0 +1,3 @@ +.dockerignore +dist +node_modules diff --git a/yunxi-ui-admin/.editorconfig b/yunxi-ui-admin/.editorconfig new file mode 100644 index 0000000..7034f9b --- /dev/null +++ b/yunxi-ui-admin/.editorconfig @@ -0,0 +1,22 @@ +# 告诉EditorConfig插件,这是根文件,不用继续往上查找 +root = true + +# 匹配全部文件 +[*] +# 设置字符集 +charset = utf-8 +# 缩进风格,可选space、tab +indent_style = space +# 缩进的空格数 +indent_size = 2 +# 结尾换行符,可选lf、cr、crlf +end_of_line = lf +# 在文件结尾插入新行 +insert_final_newline = true +# 删除一行中的前后空格 +trim_trailing_whitespace = true + +# 匹配md结尾的文件 +[*.md] +insert_final_newline = false +trim_trailing_whitespace = false diff --git a/yunxi-ui-admin/.env.dev b/yunxi-ui-admin/.env.dev new file mode 100644 index 0000000..4caddc5 --- /dev/null +++ b/yunxi-ui-admin/.env.dev @@ -0,0 +1,23 @@ +# 开发环境配置 +ENV = 'development' + +# 页面标题 +VUE_APP_TITLE = 云息供应链管理平台 + +# 云息供应链管理平台/开发环境 +VUE_APP_BASE_API = 'http://localhost:48080' + +# 路由懒加载 +VUE_CLI_BABEL_TRANSPILE_MODULES = true + +# 多租户的开关 +VUE_APP_TENANT_ENABLE = true + +# 验证码的开关 +VUE_APP_CAPTCHA_ENABLE = true + +# 文档的开关 +VUE_APP_DOC_ENABLE = true + +# 百度统计 +VUE_APP_BAIDU_CODE = fadc1bd5db1a1d6f581df60a1807f8ab diff --git a/yunxi-ui-admin/.env.front b/yunxi-ui-admin/.env.front new file mode 100644 index 0000000..8d55df4 --- /dev/null +++ b/yunxi-ui-admin/.env.front @@ -0,0 +1,23 @@ +# 开发环境配置 +ENV = 'development' + +# 页面标题 +VUE_APP_TITLE = 云息供应链管理平台 + +# 云息供应链管理平台/本地环境 +VUE_APP_BASE_API = 'http://api-dashboard.yunxi.iocoder.cn' + +# 路由懒加载 +VUE_CLI_BABEL_TRANSPILE_MODULES = true + +# 多租户的开关 +VUE_APP_TENANT_ENABLE = true + +# 验证码的开关 +VUE_APP_CAPTCHA_ENABLE = true + +# 文档的开关 +VUE_APP_DOC_ENABLE = true + +# 百度统计 +VUE_APP_BAIDU_CODE = fadc1bd5db1a1d6f581df60a1807f8ab diff --git a/yunxi-ui-admin/.env.prod b/yunxi-ui-admin/.env.prod new file mode 100644 index 0000000..a70f04d --- /dev/null +++ b/yunxi-ui-admin/.env.prod @@ -0,0 +1,25 @@ +# 生产环境配置 +NODE_ENV = 'production' + +# 页面标题 +VUE_APP_TITLE = 云息供应链管理平台 + +# 云息供应链管理平台/生产环境 +VUE_APP_BASE_API = '/prod-api' + +# 根据服务器或域名修改 +PUBLIC_PATH = 'http://my-pi.com:8888/yunxi-admin/' +# 二级部署路径 +VUE_APP_APP_NAME ='yunxi-admin' + +# 多租户的开关 +VUE_APP_TENANT_ENABLE = true + +# 验证码的开关 +VUE_APP_CAPTCHA_ENABLE = true + +# 文档的开关 +VUE_APP_DOC_ENABLE = false + +# 百度统计 +VUE_APP_BAIDU_CODE = fadc1bd5db1a1d6f581df60a1807f8ab diff --git a/yunxi-ui-admin/.env.stage b/yunxi-ui-admin/.env.stage new file mode 100644 index 0000000..ac888b0 --- /dev/null +++ b/yunxi-ui-admin/.env.stage @@ -0,0 +1,25 @@ +NODE_ENV = production + +# 页面标题 +VUE_APP_TITLE = 云息供应链管理平台 + +# 测试环境配置 +ENV = 'staging' + +# 云息供应链管理平台/测试环境 +VUE_APP_BASE_API = 'http://api-dashboard.yunxi.iocoder.cn' + +# 静态资源地址 +PUBLIC_PATH = 'http://static.yunxi.iocoder.cn/' + +# 多租户的开关 +VUE_APP_TENANT_ENABLE = true + +# 验证码的开关 +VUE_APP_CAPTCHA_ENABLE = true + +# 文档的开关 +VUE_APP_DOC_ENABLE = false + +# 百度统计 +VUE_APP_BAIDU_CODE = fadc1bd5db1a1d6f581df60a1807f8ab diff --git a/yunxi-ui-admin/.env.static b/yunxi-ui-admin/.env.static new file mode 100644 index 0000000..b09403f --- /dev/null +++ b/yunxi-ui-admin/.env.static @@ -0,0 +1,27 @@ +NODE_ENV = development + +# 测试环境配置 +ENV = 'staging' + +# 页面标题 +VUE_APP_TITLE = 云息供应链管理平台 + +# 云息供应链管理平台/测试环境 +VUE_APP_BASE_API = 'http://127.0.0.1:48080' + +# 根据服务器或域名修改 +PUBLIC_PATH = '/admin-ui-vue2/' +# 二级部署路径 +VUE_APP_APP_NAME ='/admin-ui-vue2/' + +# 多租户的开关 +VUE_APP_TENANT_ENABLE = true + +# 验证码的开关 +VUE_APP_CAPTCHA_ENABLE = true + +# 文档的开关 +VUE_APP_DOC_ENABLE = true + +# 百度统计 +VUE_APP_BAIDU_CODE = fadc1bd5db1a1d6f581df60a1807f8ab diff --git a/yunxi-ui-admin/.eslintignore b/yunxi-ui-admin/.eslintignore new file mode 100644 index 0000000..21cfec7 --- /dev/null +++ b/yunxi-ui-admin/.eslintignore @@ -0,0 +1,10 @@ +# 忽略build目录下类型为js的文件的语法检查 +build/*.js +# 忽略src/assets目录下文件的语法检查 +src/assets +# 忽略public目录下文件的语法检查 +public +# 忽略当前目录下为js的文件的语法检查 +*.js +# 忽略当前目录下为vue的文件的语法检查 +*.vue diff --git a/yunxi-ui-admin/.eslintrc.js b/yunxi-ui-admin/.eslintrc.js new file mode 100644 index 0000000..877d007 --- /dev/null +++ b/yunxi-ui-admin/.eslintrc.js @@ -0,0 +1,192 @@ +// ESlint 检查配置 +module.exports = { + root: true, + parserOptions: { + parser: 'babel-eslint', + sourceType: 'module' + }, + env: { + browser: true, + node: true, + es6: true, + }, + extends: ['plugin:vue/recommended', 'eslint:recommended'], + + // add your custom rules here + //it is base on https://github.com/vuejs/eslint-config-vue + rules: { + "vue/singleline-html-element-content-newline": "off", + "vue/multiline-html-element-content-newline":"off", + "vue/name-property-casing": ["error", "PascalCase"], + "vue/no-v-html": "off", + 'accessor-pairs': 2, + 'arrow-spacing': [2, { + 'before': true, + 'after': true + }], + 'block-spacing': [2, 'always'], + 'brace-style': [2, '1tbs', { + 'allowSingleLine': true + }], + 'camelcase': [0, { + 'properties': 'always' + }], + 'comma-dangle': [2, 'never'], + 'comma-spacing': [2, { + 'before': false, + 'after': true + }], + 'comma-style': [2, 'last'], + 'constructor-super': 2, + 'curly': [2, 'multi-line'], + 'dot-location': [2, 'property'], + 'eol-last': 2, + 'eqeqeq': ["error", "always", {"null": "ignore"}], + 'generator-star-spacing': [2, { + 'before': true, + 'after': true + }], + 'handle-callback-err': [2, '^(err|error)$'], + 'indent': [2, 2, { + 'SwitchCase': 1 + }], + 'jsx-quotes': [2, 'prefer-single'], + 'key-spacing': [2, { + 'beforeColon': false, + 'afterColon': true + }], + 'keyword-spacing': [2, { + 'before': true, + 'after': true + }], + 'new-cap': [2, { + 'newIsCap': true, + 'capIsNew': false + }], + 'new-parens': 2, + 'no-array-constructor': 2, + 'no-caller': 2, + 'no-console': 'off', + 'no-class-assign': 2, + 'no-cond-assign': 2, + 'no-const-assign': 2, + 'no-control-regex': 0, + 'no-delete-var': 2, + 'no-dupe-args': 2, + 'no-dupe-class-members': 2, + 'no-dupe-keys': 2, + 'no-duplicate-case': 2, + 'no-empty-character-class': 2, + 'no-empty-pattern': 2, + 'no-eval': 2, + 'no-ex-assign': 2, + 'no-extend-native': 2, + 'no-extra-bind': 2, + 'no-extra-boolean-cast': 2, + 'no-extra-parens': [2, 'functions'], + 'no-fallthrough': 2, + 'no-floating-decimal': 2, + 'no-func-assign': 2, + 'no-implied-eval': 2, + 'no-inner-declarations': [2, 'functions'], + 'no-invalid-regexp': 2, + 'no-irregular-whitespace': 2, + 'no-iterator': 2, + 'no-label-var': 2, + 'no-labels': [2, { + 'allowLoop': false, + 'allowSwitch': false + }], + 'no-lone-blocks': 2, + 'no-mixed-spaces-and-tabs': 2, + 'no-multi-spaces': 2, + 'no-multi-str': 2, + 'no-multiple-empty-lines': [2, { + 'max': 1 + }], + 'no-native-reassign': 2, + 'no-negated-in-lhs': 2, + 'no-new-object': 2, + 'no-new-require': 2, + 'no-new-symbol': 2, + 'no-new-wrappers': 2, + 'no-obj-calls': 2, + 'no-octal': 2, + 'no-octal-escape': 2, + 'no-path-concat': 2, + 'no-proto': 2, + 'no-redeclare': 2, + 'no-regex-spaces': 2, + 'no-return-assign': [2, 'except-parens'], + 'no-self-assign': 2, + 'no-self-compare': 2, + 'no-sequences': 2, + 'no-shadow-restricted-names': 2, + 'no-spaced-func': 2, + 'no-sparse-arrays': 2, + 'no-this-before-super': 2, + 'no-throw-literal': 2, + 'no-trailing-spaces': 2, + 'no-undef': 2, + 'no-undef-init': 2, + 'no-unexpected-multiline': 2, + 'no-unmodified-loop-condition': 2, + 'no-unneeded-ternary': [2, { + 'defaultAssignment': false + }], + 'no-unreachable': 2, + 'no-unsafe-finally': 2, + 'no-unused-vars': [2, { + 'vars': 'all', + 'args': 'none' + }], + 'no-useless-call': 2, + 'no-useless-computed-key': 2, + 'no-useless-constructor': 2, + 'no-useless-escape': 0, + 'no-whitespace-before-property': 2, + 'no-with': 2, + 'one-var': [2, { + 'initialized': 'never' + }], + 'operator-linebreak': [2, 'after', { + 'overrides': { + '?': 'before', + ':': 'before' + } + }], + 'padded-blocks': [2, 'never'], + 'quotes': [2, 'single', { + 'avoidEscape': true, + 'allowTemplateLiterals': true + }], + 'semi': [2, 'never'], + 'semi-spacing': [2, { + 'before': false, + 'after': true + }], + 'space-before-blocks': [2, 'always'], + 'space-before-function-paren': [2, 'never'], + 'space-in-parens': [2, 'never'], + 'space-infix-ops': 2, + 'space-unary-ops': [2, { + 'words': true, + 'nonwords': false + }], + 'spaced-comment': [2, 'always', { + 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] + }], + 'template-curly-spacing': [2, 'never'], + 'use-isnan': 2, + 'valid-typeof': 2, + 'wrap-iife': [2, 'any'], + 'yield-star-spacing': [2, 'both'], + 'yoda': [2, 'never'], + 'prefer-const': 2, + 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, + 'object-curly-spacing': [2, 'always', { + objectsInObjects: false + }], + 'array-bracket-spacing': [2, 'never'] + } +} diff --git a/yunxi-ui-admin/.gitignore b/yunxi-ui-admin/.gitignore new file mode 100644 index 0000000..004766c --- /dev/null +++ b/yunxi-ui-admin/.gitignore @@ -0,0 +1,22 @@ +.DS_Store +node_modules/ +dist/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +**/*.log + +tests/**/coverage/ +tests/e2e/reports +selenium-debug.log + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.local + +package-lock.json diff --git a/yunxi-ui-admin/.npmrc b/yunxi-ui-admin/.npmrc new file mode 100644 index 0000000..3f3e0a2 --- /dev/null +++ b/yunxi-ui-admin/.npmrc @@ -0,0 +1,4 @@ +phantomjs_cdnurl=http://cnpmjs.org/downloads +chromedriver_cdnurl=http://cdn.npm.taobao.org/dist/chromedriver +sass_binary_site=https://npm.taobao.org/mirrors/node-sass/ +registry=https://registry.npmmirror.com diff --git a/yunxi-ui-admin/Dockerfile b/yunxi-ui-admin/Dockerfile new file mode 100644 index 0000000..4764e74 --- /dev/null +++ b/yunxi-ui-admin/Dockerfile @@ -0,0 +1,21 @@ +FROM node:16-alpine as build-stage + +WORKDIR /admim + +COPY .npmrc package.json yarn.lock ./ +RUN --mount=type=cache,id=yarn-store,target=/root/.yarn-store \ + yarn install --frozen-lockfile + +COPY . . +ARG NODE_ENV="" +RUN env ${NODE_ENV} yarn build:prod + +## -- stage: dist => nginx -- +FROM nginx:alpine + +ENV TZ=Asia/Shanghai + +COPY ./nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build-stage /admim/dist /usr/share/nginx/html + +EXPOSE 80 diff --git a/yunxi-ui-admin/Jenkinsfile b/yunxi-ui-admin/Jenkinsfile new file mode 100644 index 0000000..6b25d01 --- /dev/null +++ b/yunxi-ui-admin/Jenkinsfile @@ -0,0 +1,44 @@ +#!groovy +pipeline { + + agent any + + tools { nodejs "nodejs" } + + parameters { + string(name: 'TAG_NAME', defaultValue: '', description: '') + } + + environment { + APP_NAME = 'yunxi-admin' + PROJECT_DIR='yunxi-admin-ui' + NGINX_WORKDIR = '/home/pi/mydata/nginx/html/' + } + + stages { + stage('检出') { + steps { + git url: "https://gitee.com/will-we/ruoyi-vue-pro.git", + branch: "devops" + } + } + + stage('构建') { + steps { + sh 'cnpm --prefix '+ "${env.PROJECT_DIR}" +' install' + sh 'cnpm --prefix '+ "${env.PROJECT_DIR}" +' run build:prod' + } + } + + stage('部署') { + steps { + sh 'rm -f ' + "${env.APP_NAME}" + '/'+ "${env.APP_NAME}" + '.tar.gz' + sh 'mkdir -p ' + "${env.NGINX_WORKDIR}" + "${env.APP_NAME}" + sh 'cp -rf ' + "${env.PROJECT_DIR}" + '/dist/. ' + "${env.NGINX_WORKDIR}" + "${env.APP_NAME}" + sh 'tar -zcvf ' + "${env.PROJECT_DIR}" + '/'+ "${env.PROJECT_DIR}" + '.tar.gz ' + "${env.PROJECT_DIR}" + '/dist/' + archiveArtifacts "${env.PROJECT_DIR}" + '/'+ "${env.PROJECT_DIR}" + '.tar.gz' + //TODO 考虑刷新缓存的问题 + } + } + } +} diff --git a/yunxi-ui-admin/babel.config.js b/yunxi-ui-admin/babel.config.js new file mode 100644 index 0000000..b99f001 --- /dev/null +++ b/yunxi-ui-admin/babel.config.js @@ -0,0 +1,13 @@ +module.exports = { + presets: [ + // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app + '@vue/cli-plugin-babel/preset' + ], + 'env': { + 'development': { + // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). + // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. + 'plugins': ['dynamic-import-node'] + } + } +} diff --git a/yunxi-ui-admin/bin/build.bat b/yunxi-ui-admin/bin/build.bat new file mode 100644 index 0000000..e74798f --- /dev/null +++ b/yunxi-ui-admin/bin/build.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [��Ϣ] ���Web���̣�����dist�ļ��� +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run build:prod + +pause \ No newline at end of file diff --git a/yunxi-ui-admin/bin/package.bat b/yunxi-ui-admin/bin/package.bat new file mode 100644 index 0000000..66bd858 --- /dev/null +++ b/yunxi-ui-admin/bin/package.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [��Ϣ] ��װWeb���̣�����node_modules�ļ��� +echo. + +%~d0 +cd %~dp0 + +cd .. +npm install --registry=https://registry.npm.taobao.org + +pause \ No newline at end of file diff --git a/yunxi-ui-admin/bin/run-web.bat b/yunxi-ui-admin/bin/run-web.bat new file mode 100644 index 0000000..322d59f --- /dev/null +++ b/yunxi-ui-admin/bin/run-web.bat @@ -0,0 +1,12 @@ +@echo off +echo. +echo [��Ϣ] ʹ�� Vue ���� Web ���̡� +echo. + +%~d0 +cd %~dp0 + +cd .. +npm run dev + +pause \ No newline at end of file diff --git a/yunxi-ui-admin/build/index.js b/yunxi-ui-admin/build/index.js new file mode 100644 index 0000000..5204895 --- /dev/null +++ b/yunxi-ui-admin/build/index.js @@ -0,0 +1,35 @@ +const { run } = require('runjs') +const chalk = require('chalk') +const config = require('../vue.config.js') +const rawArgv = process.argv.slice(2) +const args = rawArgv.join(' ') + +if (process.env.npm_config_preview || rawArgv.includes('--preview')) { + const report = rawArgv.includes('--report') + + run(`vue-cli-service build ${args}`) + + const port = 9526 + const publicPath = config.publicPath + + const connect = require('connect') + const serveStatic = require('serve-static') + const app = connect() + + app.use( + publicPath, + serveStatic('./dist', { + index: ['index.html', '/'] + }) + ) + + app.listen(port, function () { + console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) + if (report) { + console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) + } + + }) +} else { + run(`vue-cli-service build ${args}`) +} diff --git a/yunxi-ui-admin/nginx.conf b/yunxi-ui-admin/nginx.conf new file mode 100644 index 0000000..bd56cd7 --- /dev/null +++ b/yunxi-ui-admin/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80 default_server; + server_name _; ## 重要!!!修改成你的外网 IP/域名 + + gzip on; + gzip_min_length 1k; # 设置允许压缩的页面最小字节数 + gzip_buffers 4 16k; # 用来存储 gzip 的压缩结果 + gzip_http_version 1.1; # 识别 HTTP 协议版本 + gzip_comp_level 2; # 设置 gzip 的压缩比 1-9。1 压缩比最小但最快,而 9 相反 + gzip_types text/plain application/x-javascript text/css application/xml application/javascript; # 指定压缩类型 + gzip_proxied any; # 无论后端服务器的 headers 头返回什么信息,都无条件启用压缩 + + location / { ## 前端项目 + root /usr/share/nginx/html/; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + location /prod-api/ { ## 后端项目 - 管理后台 + proxy_pass http://yunxi-server:48080/; ## 重要!!!proxy_pass 需要设置为后端项目所在服务器的 IP + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header REMOTE-HOST $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} diff --git a/yunxi-ui-admin/package.json b/yunxi-ui-admin/package.json new file mode 100644 index 0000000..f34eec3 --- /dev/null +++ b/yunxi-ui-admin/package.json @@ -0,0 +1,112 @@ +{ + "name": "yunxi-ui-admin", + "version": "1.8.0-snapshot", + "description": "云息供应链管理平台", + "author": "芋道", + "license": "MIT", + "scripts": { + "local": "vue-cli-service serve --mode local", + "dev": "vue-cli-service serve --mode dev", + "front": "vue-cli-service serve --mode front", + "build:prod": "vue-cli-service build --mode prod", + "build:stage": "vue-cli-service build --mode stage", + "build:dev": "vue-cli-service build --mode dev", + "build:static": "vue-cli-service build --mode static", + "preview": "node build/index.js --preview", + "lint": "eslint --ext .js,.vue src", + "clean": "rimraf node_modules" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "src/**/*.{js,vue}": [ + "eslint --fix", + "git add" + ] + }, + "keywords": [ + "vue", + "admin", + "dashboard", + "element-ui", + "boilerplate", + "admin-template", + "management-system" + ], + "repository": { + "type": "git", + "url": "https://github.com/YunaiV/ruoyi-vue-pro" + }, + "dependencies": { + "@babel/parser": "7.18.4", + "@riophae/vue-treeselect": "0.4.0", + "axios": "0.27.2", + "benz-amr-recorder": "^1.1.5", + "bpmn-js-token-simulation": "0.10.0", + "clipboard": "2.0.8", + "core-js": "^3.26.0", + "crypto-js": "^4.0.0", + "echarts": "5.4.0", + "element-ui": "2.15.12", + "file-saver": "2.0.5", + "fuse.js": "6.6.2", + "highlight.js": "9.18.5", + "js-beautify": "1.13.0", + "jsencrypt": "3.3.1", + "min-dash": "3.5.2", + "nprogress": "0.2.0", + "qrcode.vue": "^1.7.0", + "quill": "1.3.7", + "screenfull": "5.0.2", + "sortablejs": "1.10.2", + "throttle-debounce": "2.1.0", + "vue": "2.7.14", + "vue-count-to": "1.0.13", + "vue-cropper": "0.5.8", + "vue-meta": "^2.4.0", + "vue-quill-editor": "^3.0.6", + "vue-router": "3.4.9", + "vue-video-player": "^5.0.2", + "vuedraggable": "2.24.3", + "vuex": "3.6.2", + "xml-js": "1.6.11" + }, + "devDependencies": { + "@vue/cli-plugin-babel": "4.5.18", + "@vue/cli-plugin-eslint": "4.5.18", + "@vue/cli-service": "4.5.18", + "@vue/compiler-sfc": "^3.0.1", + "@vue/eslint-config-prettier": "^5.0.0", + "babel-eslint": "10.1.0", + "bpmn-js": "8.9.0", + "bpmn-js-properties-panel": "0.46.0", + "chalk": "4.1.0", + "compression-webpack-plugin": "5.0.2", + "connect": "3.6.6", + "eslint": "7.15.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-prettier": "^3.1.0", + "eslint-plugin-vue": "9.0.0", + "fs-extra": "^8.1.0", + "lint-staged": "12.5.0", + "runjs": "4.4.2", + "sass": "1.32.13", + "sass-loader": "10.2.0", + "script-ext-html-webpack-plugin": "2.1.5", + "svg-sprite-loader": "5.1.1", + "terser-webpack-plugin": "^4.2.3", + "webpack-bundle-analyzer": "^3.9.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 5.0.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions" + ] +} diff --git a/yunxi-ui-admin/public/favicon.ico b/yunxi-ui-admin/public/favicon.ico new file mode 100644 index 0000000..e263760 Binary files /dev/null and b/yunxi-ui-admin/public/favicon.ico differ diff --git a/yunxi-ui-admin/public/html/ie.html b/yunxi-ui-admin/public/html/ie.html new file mode 100644 index 0000000..5775546 --- /dev/null +++ b/yunxi-ui-admin/public/html/ie.html @@ -0,0 +1,45 @@ + + + + + 请升级您的浏览器 + + + + + + +

请升级您的浏览器,以便我们更好的为您提供服务!

+

您正在使用 Internet Explorer 的早期版本(IE11以下版本或使用该内核的浏览器)。这意味着在升级浏览器前,您将无法访问此网站。

+
+

请注意:微软公司对Windows XP 及 Internet Explorer 早期版本的支持已经结束

+

自 2016 年 1 月 12 日起,Microsoft 不再为 IE 11 以下版本提供相应支持和更新。没有关键的浏览器安全更新,您的电脑可能易受有害病毒、间谍软件和其他恶意软件的攻击,它们可以窃取或损害您的业务数据和信息。请参阅 微软对 Internet Explorer 早期版本的支持将于 2016 年 1 月 12 日结束的说明

+
+

您可以选择更先进的浏览器

+

推荐使用以下浏览器的最新版本。如果您的电脑已有以下浏览器的最新版本则直接使用该浏览器访问即可。

+ +
+ + diff --git a/yunxi-ui-admin/public/index.html b/yunxi-ui-admin/public/index.html new file mode 100644 index 0000000..84c3c23 --- /dev/null +++ b/yunxi-ui-admin/public/index.html @@ -0,0 +1,208 @@ + + + + + + + + + <%= webpackConfig.name %> + + + + +
+
+
+
+
+
正在加载系统资源,请耐心等待
+
+
+ + diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/base/browser/ui/codicons/codicon/codicon.ttf b/yunxi-ui-admin/public/libs/monaco-editor/vs/base/browser/ui/codicons/codicon/codicon.ttf new file mode 100644 index 0000000..b546c0a Binary files /dev/null and b/yunxi-ui-admin/public/libs/monaco-editor/vs/base/browser/ui/codicons/codicon/codicon.ttf differ diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/base/worker/workerMain.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/base/worker/workerMain.js new file mode 100644 index 0000000..1a433ee --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/base/worker/workerMain.js @@ -0,0 +1,12 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/(function(){var z=["require","exports","vs/base/common/platform","vs/editor/common/core/position","vs/base/common/errors","vs/base/common/strings","vs/editor/common/core/range","vs/base/common/lifecycle","vs/base/common/stopwatch","vs/base/common/event","vs/base/common/diff/diff","vs/base/common/types","vs/base/common/uint","vs/base/common/uri","vs/base/common/arrays","vs/base/common/diff/diffChange","vs/base/common/iterator","vs/base/common/keyCodes","vs/base/common/linkedList","vs/base/common/process","vs/base/common/path","vs/base/common/cancellation","vs/base/common/hash","vs/editor/common/core/characterClassifier","vs/editor/common/core/selection","vs/editor/common/core/token","vs/editor/common/diff/diffComputer","vs/editor/common/model/wordHelper","vs/editor/common/modes/linkComputer","vs/editor/common/modes/supports/inplaceReplaceSupport","vs/editor/common/standalone/standaloneEnums","vs/editor/common/standalone/standaloneBase","vs/editor/common/viewModel/prefixSumComputer","vs/editor/common/model/mirrorTextModel","vs/base/common/worker/simpleWorker","vs/editor/common/services/editorSimpleWorker"],G=function(I){for(var t=[],p=0,P=I.length;p=0?!0:typeof process!="undefined"?process.platform==="win32":!1},p}();I.Environment=t})(X||(X={}));var X;(function(I){var t=function(){function E(u,d,l){this.type=u,this.detail=d,this.timestamp=l}return E}();I.LoaderEvent=t;var p=function(){function E(u){this._events=[new t(1,"",u)]}return E.prototype.record=function(u,d){this._events.push(new t(u,d,I.Utilities.getHighPerformanceTimestamp()))},E.prototype.getEvents=function(){return this._events},E}();I.LoaderEventRecorder=p;var P=function(){function E(){}return E.prototype.record=function(u,d){},E.prototype.getEvents=function(){return[]},E.INSTANCE=new E,E}();I.NullLoaderEventRecorder=P})(X||(X={}));var X;(function(I){var t=function(){function p(){}return p.fileUriToFilePath=function(P,E){if(E=decodeURI(E).replace(/%23/g,"#"),P){if(/^file:\/\/\//.test(E))return E.substr(8);if(/^file:\/\//.test(E))return E.substr(5)}else if(/^file:\/\//.test(E))return E.substr(7);return E},p.startsWith=function(P,E){return P.length>=E.length&&P.substr(0,E.length)===E},p.endsWith=function(P,E){return P.length>=E.length&&P.substr(P.length-E.length)===E},p.containsQueryString=function(P){return/^[^\#]*\?/gi.test(P)},p.isAbsolutePath=function(P){return/^((http:\/\/)|(https:\/\/)|(file:\/\/)|(\/))/.test(P)},p.forEachProperty=function(P,E){if(P){var u=void 0;for(u in P)P.hasOwnProperty(u)&&E(u,P[u])}},p.isEmpty=function(P){var E=!0;return p.forEachProperty(P,function(){E=!1}),E},p.recursiveClone=function(P){if(!P||typeof P!="object"||P instanceof RegExp||!Array.isArray(P)&&Object.getPrototypeOf(P)!==Object.prototype)return P;var E=Array.isArray(P)?[]:{};return p.forEachProperty(P,function(u,d){d&&typeof d=="object"?E[u]=p.recursiveClone(d):E[u]=d}),E},p.generateAnonymousModule=function(){return"===anonymous"+p.NEXT_ANONYMOUS_ID+++"==="},p.isAnonymousModule=function(P){return p.startsWith(P,"===anonymous")},p.getHighPerformanceTimestamp=function(){return this.PERFORMANCE_NOW_PROBED||(this.PERFORMANCE_NOW_PROBED=!0,this.HAS_PERFORMANCE_NOW=I.global.performance&&typeof I.global.performance.now=="function"),this.HAS_PERFORMANCE_NOW?I.global.performance.now():Date.now()},p.NEXT_ANONYMOUS_ID=1,p.PERFORMANCE_NOW_PROBED=!1,p.HAS_PERFORMANCE_NOW=!1,p}();I.Utilities=t})(X||(X={}));var X;(function(I){function t(E){if(E instanceof Error)return E;var u=new Error(E.message||String(E)||"Unknown Error");return E.stack&&(u.stack=E.stack),u}I.ensureError=t;var p=function(){function E(){}return E.validateConfigurationOptions=function(u){function d(s){if(s.phase==="loading"){console.error('Loading "'+s.moduleId+'" failed'),console.error(s),console.error("Here are the modules that depend on it:"),console.error(s.neededBy);return}if(s.phase==="factory"){console.error('The factory method of "'+s.moduleId+'" has thrown an exception'),console.error(s);return}}if(u=u||{},typeof u.baseUrl!="string"&&(u.baseUrl=""),typeof u.isBuild!="boolean"&&(u.isBuild=!1),typeof u.paths!="object"&&(u.paths={}),typeof u.config!="object"&&(u.config={}),typeof u.catchError=="undefined"&&(u.catchError=!1),typeof u.recordStats=="undefined"&&(u.recordStats=!1),typeof u.urlArgs!="string"&&(u.urlArgs=""),typeof u.onError!="function"&&(u.onError=d),Array.isArray(u.ignoreDuplicateModules)||(u.ignoreDuplicateModules=[]),u.baseUrl.length>0&&(I.Utilities.endsWith(u.baseUrl,"/")||(u.baseUrl+="/")),typeof u.cspNonce!="string"&&(u.cspNonce=""),typeof u.preferScriptTags=="undefined"&&(u.preferScriptTags=!1),Array.isArray(u.nodeModules)||(u.nodeModules=[]),u.nodeCachedData&&typeof u.nodeCachedData=="object"&&(typeof u.nodeCachedData.seed!="string"&&(u.nodeCachedData.seed="seed"),(typeof u.nodeCachedData.writeDelay!="number"||u.nodeCachedData.writeDelay<0)&&(u.nodeCachedData.writeDelay=1e3*7),!u.nodeCachedData.path||typeof u.nodeCachedData.path!="string")){var l=t(new Error("INVALID cached data configuration, 'path' MUST be set"));l.phase="configuration",u.onError(l),u.nodeCachedData=void 0}return u},E.mergeConfigurationOptions=function(u,d){u===void 0&&(u=null),d===void 0&&(d=null);var l=I.Utilities.recursiveClone(d||{});return I.Utilities.forEachProperty(u,function(s,h){s==="ignoreDuplicateModules"&&typeof l.ignoreDuplicateModules!="undefined"?l.ignoreDuplicateModules=l.ignoreDuplicateModules.concat(h):s==="paths"&&typeof l.paths!="undefined"?I.Utilities.forEachProperty(h,function(w,a){return l.paths[w]=a}):s==="config"&&typeof l.config!="undefined"?I.Utilities.forEachProperty(h,function(w,a){return l.config[w]=a}):l[s]=I.Utilities.recursiveClone(h)}),E.validateConfigurationOptions(l)},E}();I.ConfigurationOptionsUtil=p;var P=function(){function E(u,d){if(this._env=u,this.options=p.mergeConfigurationOptions(d),this._createIgnoreDuplicateModulesMap(),this._createNodeModulesMap(),this._createSortedPathsRules(),this.options.baseUrl===""){if(this.options.nodeRequire&&this.options.nodeRequire.main&&this.options.nodeRequire.main.filename&&this._env.isNode){var l=this.options.nodeRequire.main.filename,s=Math.max(l.lastIndexOf("/"),l.lastIndexOf("\\"));this.options.baseUrl=l.substring(0,s+1)}if(this.options.nodeMain&&this._env.isNode){var l=this.options.nodeMain,s=Math.max(l.lastIndexOf("/"),l.lastIndexOf("\\"));this.options.baseUrl=l.substring(0,s+1)}}}return E.prototype._createIgnoreDuplicateModulesMap=function(){this.ignoreDuplicateModulesMap={};for(var u=0;u=5)){if(_.length0?(C=_.slice(0,16),v=_.slice(16),w.record(60,h)):w.record(61,h),r()})}},l.prototype._verifyCachedData=function(s,h,w,a,S){var v=this;!a||s.cachedDataRejected||setTimeout(function(){var C=v._crypto.createHash("md5").update(h,"utf8").digest();a.equals(C)||(S.getConfig().onError(new Error("FAILED TO VERIFY CACHED DATA, deleting stale '"+w+"' now, but a RESTART IS REQUIRED")),v._fs.unlink(w,function(n){n&&S.getConfig().onError(n)}))},Math.ceil(5e3*(1+Math.random())))},l._BOM=65279,l._PREFIX="(function (require, define, __filename, __dirname) { ",l._SUFFIX=` +});`,l}();function u(l,s){if(s.__$__isRecorded)return s;var h=function(a){l.record(33,a);try{return s(a)}finally{l.record(34,a)}};return h.__$__isRecorded=!0,h}I.ensureRecordedNodeRequire=u;function d(l){return new t(l)}I.createScriptLoader=d})(X||(X={}));var X;(function(I){var t=function(){function l(s){var h=s.lastIndexOf("/");h!==-1?this.fromModulePath=s.substr(0,h+1):this.fromModulePath=""}return l._normalizeModuleId=function(s){var h=s,w;for(w=/\/\.\//;w.test(h);)h=h.replace(w,"/");for(h=h.replace(/^\.\//g,""),w=/\/(([^\/])|([^\/][^\/\.])|([^\/\.][^\/])|([^\/][^\/][^\/]+))\/\.\.\//;w.test(h);)h=h.replace(w,"/");return h=h.replace(/^(([^\/])|([^\/][^\/\.])|([^\/\.][^\/])|([^\/][^\/][^\/]+))\/\.\.\//,""),h},l.prototype.resolveModule=function(s){var h=s;return I.Utilities.isAbsolutePath(h)||(I.Utilities.startsWith(h,"./")||I.Utilities.startsWith(h,"../"))&&(h=l._normalizeModuleId(this.fromModulePath+h)),h},l.ROOT=new l(""),l}();I.ModuleIdResolver=t;var p=function(){function l(s,h,w,a,S,v){this.id=s,this.strId=h,this.dependencies=w,this._callback=a,this._errorback=S,this.moduleIdResolver=v,this.exports={},this.error=null,this.exportsPassedIn=!1,this.unresolvedDependenciesCount=this.dependencies.length,this._isComplete=!1}return l._safeInvokeFunction=function(s,h){try{return{returnedValue:s.apply(I.global,h),producedError:null}}catch(w){return{returnedValue:null,producedError:w}}},l._invokeFactory=function(s,h,w,a){return s.isBuild()&&!I.Utilities.isAnonymousModule(h)?{returnedValue:null,producedError:null}:s.shouldCatchError()?this._safeInvokeFunction(w,a):{returnedValue:w.apply(I.global,a),producedError:null}},l.prototype.complete=function(s,h,w){this._isComplete=!0;var a=null;if(this._callback)if(typeof this._callback=="function"){s.record(21,this.strId);var S=l._invokeFactory(h,this.strId,this._callback,w);a=S.producedError,s.record(22,this.strId),!a&&typeof S.returnedValue!="undefined"&&(!this.exportsPassedIn||I.Utilities.isEmpty(this.exports))&&(this.exports=S.returnedValue)}else this.exports=this._callback;if(a){var v=I.ensureError(a);v.phase="factory",v.moduleId=this.strId,this.error=v,h.onError(v)}this.dependencies=null,this._callback=null,this._errorback=null,this.moduleIdResolver=null},l.prototype.onDependencyError=function(s){return this._isComplete=!0,this.error=s,this._errorback?(this._errorback(s),!0):!1},l.prototype.isComplete=function(){return this._isComplete},l}();I.Module=p;var P=function(){function l(){this._nextId=0,this._strModuleIdToIntModuleId=new Map,this._intModuleIdToStrModuleId=[],this.getModuleId("exports"),this.getModuleId("module"),this.getModuleId("require")}return l.prototype.getMaxModuleId=function(){return this._nextId},l.prototype.getModuleId=function(s){var h=this._strModuleIdToIntModuleId.get(s);return typeof h=="undefined"&&(h=this._nextId++,this._strModuleIdToIntModuleId.set(s,h),this._intModuleIdToStrModuleId[h]=s),h},l.prototype.getStrModuleId=function(s){return this._intModuleIdToStrModuleId[s]},l}(),E=function(){function l(s){this.id=s}return l.EXPORTS=new l(0),l.MODULE=new l(1),l.REQUIRE=new l(2),l}();I.RegularDependency=E;var u=function(){function l(s,h,w){this.id=s,this.pluginId=h,this.pluginParam=w}return l}();I.PluginDependency=u;var d=function(){function l(s,h,w,a,S){S===void 0&&(S=0),this._env=s,this._scriptLoader=h,this._loaderAvailableTimestamp=S,this._defineFunc=w,this._requireFunc=a,this._moduleIdProvider=new P,this._config=new I.Configuration(this._env),this._hasDependencyCycle=!1,this._modules2=[],this._knownModules2=[],this._inverseDependencies2=[],this._inversePluginDependencies2=new Map,this._currentAnnonymousDefineCall=null,this._recorder=null,this._buildInfoPath=[],this._buildInfoDefineStack=[],this._buildInfoDependencies=[]}return l.prototype.reset=function(){return new l(this._env,this._scriptLoader,this._defineFunc,this._requireFunc,this._loaderAvailableTimestamp)},l.prototype.getGlobalAMDDefineFunc=function(){return this._defineFunc},l.prototype.getGlobalAMDRequireFunc=function(){return this._requireFunc},l._findRelevantLocationInStack=function(s,h){for(var w=function(i){return i.replace(/\\/g,"/")},a=w(s),S=h.split(/\n/),v=0;v=0){var a=h.resolveModule(s.substr(0,w)),S=h.resolveModule(s.substr(w+1)),v=this._moduleIdProvider.getModuleId(a+"!"+S),C=this._moduleIdProvider.getModuleId(a);return new u(v,C,S)}return new E(this._moduleIdProvider.getModuleId(h.resolveModule(s)))},l.prototype._normalizeDependencies=function(s,h){for(var w=[],a=0,S=0,v=s.length;S0;){var r=n.shift(),m=this._modules2[r];m&&(C=m.onDependencyError(w)||C);var _=this._inverseDependencies2[r];if(_)for(var S=0,v=_.length;S0;){var n=C.shift(),r=n.dependencies;if(r)for(var S=0,v=r.length;S=a.length)h._onLoadError(s,n);else{var r=a[v],m=h.getRecorder();if(h._config.isBuild()&&r==="empty:"){h._buildInfoPath[s]=r,h.defineModule(h._moduleIdProvider.getStrModuleId(s),[],null,null,null),h._onLoad(s);return}m.record(10,r),h._scriptLoader.load(h,r,function(){h._config.isBuild()&&(h._buildInfoPath[s]=r),m.record(11,r),h._onLoad(s)},function(_){m.record(12,r),C(_)})}};C(null)}},l.prototype._loadPluginDependency=function(s,h){var w=this;if(!(this._modules2[h.id]||this._knownModules2[h.id])){this._knownModules2[h.id]=!0;var a=function(S){w.defineModule(w._moduleIdProvider.getStrModuleId(h.id),[],S,null,null)};a.error=function(S){w._config.onError(w._createLoadError(h.id,S))},s.load(h.pluginParam,this._createRequire(t.ROOT),a,this._config.getOptionsLiteral())}},l.prototype._resolve=function(s){var h=this,w=s.dependencies;if(w)for(var a=0,S=w.length;a +`)),s.unresolvedDependenciesCount--;continue}if(this._inverseDependencies2[v.id]=this._inverseDependencies2[v.id]||[],this._inverseDependencies2[v.id].push(s.id),v instanceof u){var r=this._modules2[v.pluginId];if(r&&r.isComplete()){this._loadPluginDependency(r.exports,v);continue}var m=this._inversePluginDependencies2.get(v.pluginId);m||(m=[],this._inversePluginDependencies2.set(v.pluginId,m)),m.push(v),this._loadModule(v.pluginId);continue}this._loadModule(v.id)}s.unresolvedDependenciesCount===0&&this._onModuleComplete(s)},l.prototype._onModuleComplete=function(s){var h=this,w=this.getRecorder();if(!s.isComplete()){var a=s.dependencies,S=[];if(a)for(var v=0,C=a.length;vA===M){if(c===g)return!0;if(!c||!g||c.length!==g.length)return!1;for(let A=0,M=c.length;A0)M=R-1;else return R}return-(A+1)}t.binarySearch=u;function d(c,g){let L=0,A=c.length;if(A===0)return 0;for(;L=g.length)throw new TypeError("invalid index");let A=g[Math.floor(g.length*Math.random())],M=[],R=[],D=[];for(let T of g){const $=L(T,A);$<0?M.push(T):$>0?R.push(T):D.push(T)}return cA?c[$]=R[T++]:T>M?c[$]=R[D++]:g(R[T],R[D])<0?c[$]=R[T++]:c[$]=R[D++]}function w(c,g,L,A,M){if(!(A<=L)){const R=L+(A-L)/2|0;w(c,g,L,R,M),w(c,g,R+1,A,M),!(g(c[R],c[R+1])<=0)&&h(c,g,L,R,A,M)}}function a(c,g){const L=[];let A;for(const M of s(c.slice(0),g))!A||g(A[0],M)!==0?(A=[M],L.push(A)):A.push(M);return L}t.groupBy=a;function S(c){return c.filter(g=>!!g)}t.coalesce=S;function v(c){return!Array.isArray(c)||c.length===0}t.isFalsyOrEmpty=v;function C(c){return Array.isArray(c)&&c.length>0}t.isNonEmptyArray=C;function n(c,g){if(!g)return c.filter((A,M)=>c.indexOf(A)===M);const L=Object.create(null);return c.filter(A=>{const M=g(A);return L[M]?!1:(L[M]=!0,!0)})}t.distinct=n;function r(c){const g=new Set;return c.filter(L=>g.has(L)?!1:(g.add(L),!0))}t.distinctES6=r;function m(c,g){return c.length>0?c[0]:g}t.firstOrDefault=m;function _(c){return[].concat(...c)}t.flatten=_;function o(c,g){let L=typeof g=="number"?c:0;typeof g=="number"?L=c:(L=0,g=c);const A=[];if(L<=g)for(let M=L;Mg;M--)A.push(M);return A}t.range=o;function i(c,g,L){const A=c.slice(0,g),M=c.slice(g);return A.concat(L,M)}t.arrayInsert=i;function f(c,g){const L=c.indexOf(g);L>-1&&(c.splice(L,1),c.unshift(g))}t.pushToStart=f;function b(c,g){const L=c.indexOf(g);L>-1&&(c.splice(L,1),c.push(g))}t.pushToEnd=b;function N(c){return Array.isArray(c)?c:[c]}t.asArray=N}),V(z[15],G([0,1]),function(I,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.DiffChange=void 0;class p{constructor(E,u,d,l){this.originalStart=E,this.originalLength=u,this.modifiedStart=d,this.modifiedLength=l}getOriginalEnd(){return this.originalStart+this.originalLength}getModifiedEnd(){return this.modifiedStart+this.modifiedLength}}t.DiffChange=p}),V(z[4],G([0,1]),function(I,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.illegalState=t.illegalArgument=t.canceled=t.isPromiseCanceledError=t.transformErrorForSerialization=t.onUnexpectedExternalError=t.onUnexpectedError=t.errorHandler=t.ErrorHandler=void 0;class p{constructor(){this.listeners=[],this.unexpectedErrorHandler=function(S){setTimeout(()=>{throw S.stack?new Error(S.message+` + +`+S.stack):S},0)}}emit(S){this.listeners.forEach(v=>{v(S)})}onUnexpectedError(S){this.unexpectedErrorHandler(S),this.emit(S)}onUnexpectedExternalError(S){this.unexpectedErrorHandler(S)}}t.ErrorHandler=p,t.errorHandler=new p;function P(a){l(a)||t.errorHandler.onUnexpectedError(a)}t.onUnexpectedError=P;function E(a){l(a)||t.errorHandler.onUnexpectedExternalError(a)}t.onUnexpectedExternalError=E;function u(a){if(a instanceof Error){let{name:S,message:v}=a;const C=a.stacktrace||a.stack;return{$isError:!0,name:S,message:v,stack:C}}return a}t.transformErrorForSerialization=u;const d="Canceled";function l(a){return a instanceof Error&&a.name===d&&a.message===d}t.isPromiseCanceledError=l;function s(){const a=new Error(d);return a.name=a.message,a}t.canceled=s;function h(a){return a?new Error(`Illegal argument: ${a}`):new Error("Illegal argument")}t.illegalArgument=h;function w(a){return a?new Error(`Illegal state: ${a}`):new Error("Illegal state")}t.illegalState=w}),V(z[16],G([0,1]),function(I,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Iterable=void 0;var p;(function(P){function E(f){return f&&typeof f=="object"&&typeof f[Symbol.iterator]=="function"}P.is=E;const u=Object.freeze([]);function d(){return u}P.empty=d;function*l(f){yield f}P.single=l;function s(f){return f||u}P.from=s;function h(f){return!f||f[Symbol.iterator]().next().done===!0}P.isEmpty=h;function w(f){return f[Symbol.iterator]().next().value}P.first=w;function a(f,b){for(const N of f)if(b(N))return!0;return!1}P.some=a;function S(f,b){for(const N of f)if(b(N))return N}P.find=S;function*v(f,b){for(const N of f)b(N)&&(yield N)}P.filter=v;function*C(f,b){for(const N of f)yield b(N)}P.map=C;function*n(...f){for(const b of f)for(const N of b)yield N}P.concat=n;function*r(f){for(const b of f)for(const N of b)yield N}P.concatNested=r;function m(f,b,N){let c=N;for(const g of f)c=b(c,g);return c}P.reduce=m;function*_(f,b,N=f.length){for(b<0&&(b+=f.length),N<0?N+=f.length:N>f.length&&(N=f.length);bc===g){const c=f[Symbol.iterator](),g=b[Symbol.iterator]();for(;;){const L=c.next(),A=g.next();if(L.done!==A.done)return!1;if(L.done)return!0;if(!N(L.value,A.value))return!1}}P.equals=i})(p=t.Iterable||(t.Iterable={}))}),V(z[17],G([0,1,4]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ResolvedKeybinding=t.ResolvedKeybindingPart=t.ChordKeybinding=t.SimpleKeybinding=t.createSimpleKeybinding=t.createKeybinding=t.KeyChord=t.KeyCodeUtils=void 0;class P{constructor(){this._keyCodeToStr=[],this._strToKeyCode=Object.create(null)}define(r,m){this._keyCodeToStr[r]=m,this._strToKeyCode[m.toLowerCase()]=r}keyCodeToStr(r){return this._keyCodeToStr[r]}strToKeyCode(r){return this._strToKeyCode[r.toLowerCase()]||0}}const E=new P,u=new P,d=new P;(function(){function n(r,m,_=m,o=_){E.define(r,m),u.define(r,_),d.define(r,o)}n(0,"unknown"),n(1,"Backspace"),n(2,"Tab"),n(3,"Enter"),n(4,"Shift"),n(5,"Ctrl"),n(6,"Alt"),n(7,"PauseBreak"),n(8,"CapsLock"),n(9,"Escape"),n(10,"Space"),n(11,"PageUp"),n(12,"PageDown"),n(13,"End"),n(14,"Home"),n(15,"LeftArrow","Left"),n(16,"UpArrow","Up"),n(17,"RightArrow","Right"),n(18,"DownArrow","Down"),n(19,"Insert"),n(20,"Delete"),n(21,"0"),n(22,"1"),n(23,"2"),n(24,"3"),n(25,"4"),n(26,"5"),n(27,"6"),n(28,"7"),n(29,"8"),n(30,"9"),n(31,"A"),n(32,"B"),n(33,"C"),n(34,"D"),n(35,"E"),n(36,"F"),n(37,"G"),n(38,"H"),n(39,"I"),n(40,"J"),n(41,"K"),n(42,"L"),n(43,"M"),n(44,"N"),n(45,"O"),n(46,"P"),n(47,"Q"),n(48,"R"),n(49,"S"),n(50,"T"),n(51,"U"),n(52,"V"),n(53,"W"),n(54,"X"),n(55,"Y"),n(56,"Z"),n(57,"Meta"),n(58,"ContextMenu"),n(59,"F1"),n(60,"F2"),n(61,"F3"),n(62,"F4"),n(63,"F5"),n(64,"F6"),n(65,"F7"),n(66,"F8"),n(67,"F9"),n(68,"F10"),n(69,"F11"),n(70,"F12"),n(71,"F13"),n(72,"F14"),n(73,"F15"),n(74,"F16"),n(75,"F17"),n(76,"F18"),n(77,"F19"),n(78,"NumLock"),n(79,"ScrollLock"),n(80,";",";","OEM_1"),n(81,"=","=","OEM_PLUS"),n(82,",",",","OEM_COMMA"),n(83,"-","-","OEM_MINUS"),n(84,".",".","OEM_PERIOD"),n(85,"/","/","OEM_2"),n(86,"`","`","OEM_3"),n(110,"ABNT_C1"),n(111,"ABNT_C2"),n(87,"[","[","OEM_4"),n(88,"\\","\\","OEM_5"),n(89,"]","]","OEM_6"),n(90,"'","'","OEM_7"),n(91,"OEM_8"),n(92,"OEM_102"),n(93,"NumPad0"),n(94,"NumPad1"),n(95,"NumPad2"),n(96,"NumPad3"),n(97,"NumPad4"),n(98,"NumPad5"),n(99,"NumPad6"),n(100,"NumPad7"),n(101,"NumPad8"),n(102,"NumPad9"),n(103,"NumPad_Multiply"),n(104,"NumPad_Add"),n(105,"NumPad_Separator"),n(106,"NumPad_Subtract"),n(107,"NumPad_Decimal"),n(108,"NumPad_Divide")})();var l;(function(n){function r(f){return E.keyCodeToStr(f)}n.toString=r;function m(f){return E.strToKeyCode(f)}n.fromString=m;function _(f){return u.keyCodeToStr(f)}n.toUserSettingsUS=_;function o(f){return d.keyCodeToStr(f)}n.toUserSettingsGeneral=o;function i(f){return u.strToKeyCode(f)||d.strToKeyCode(f)}n.fromUserSettings=i})(l=t.KeyCodeUtils||(t.KeyCodeUtils={}));function s(n,r){const m=(r&65535)<<16>>>0;return(n|m)>>>0}t.KeyChord=s;function h(n,r){if(n===0)return null;const m=(n&65535)>>>0,_=(n&4294901760)>>>16;return _!==0?new S([w(m,r),w(_,r)]):new S([w(m,r)])}t.createKeybinding=h;function w(n,r){const m=!!(n&2048),_=!!(n&256),o=r===2?_:m,i=!!(n&1024),f=!!(n&512),b=r===2?m:_,N=n&255;return new a(o,i,f,b,N)}t.createSimpleKeybinding=w;class a{constructor(r,m,_,o,i){this.ctrlKey=r,this.shiftKey=m,this.altKey=_,this.metaKey=o,this.keyCode=i}equals(r){return this.ctrlKey===r.ctrlKey&&this.shiftKey===r.shiftKey&&this.altKey===r.altKey&&this.metaKey===r.metaKey&&this.keyCode===r.keyCode}isModifierKey(){return this.keyCode===0||this.keyCode===5||this.keyCode===57||this.keyCode===6||this.keyCode===4}toChord(){return new S([this])}isDuplicateModifierCase(){return this.ctrlKey&&this.keyCode===5||this.shiftKey&&this.keyCode===4||this.altKey&&this.keyCode===6||this.metaKey&&this.keyCode===57}}t.SimpleKeybinding=a;class S{constructor(r){if(r.length===0)throw p.illegalArgument("parts");this.parts=r}}t.ChordKeybinding=S;class v{constructor(r,m,_,o,i,f){this.ctrlKey=r,this.shiftKey=m,this.altKey=_,this.metaKey=o,this.keyLabel=i,this.keyAriaLabel=f}}t.ResolvedKeybindingPart=v;class C{}t.ResolvedKeybinding=C}),V(z[7],G([0,1,16]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.ImmortalReference=t.MutableDisposable=t.Disposable=t.DisposableStore=t.toDisposable=t.combinedDisposable=t.dispose=t.isDisposable=t.MultiDisposeError=t.trackDisposable=void 0;const P=!1;let E=null;if(P){const r="__is_disposable_tracked__";E=new class{trackDisposable(m){const _=new Error("Potentially leaked disposable").stack;setTimeout(()=>{m[r]||console.log(_)},3e3)}markTracked(m){if(m&&m!==v.None)try{m[r]=!0}catch(_){}}}}function u(r){!E||E.markTracked(r)}function d(r){return E&&E.trackDisposable(r),r}t.trackDisposable=d;class l extends Error{constructor(m){super(`Encountered errors while disposing of store. Errors: [${m.join(", ")}]`);this.errors=m}}t.MultiDisposeError=l;function s(r){return typeof r.dispose=="function"&&r.dispose.length===0}t.isDisposable=s;function h(r){if(p.Iterable.is(r)){let m=[];for(const _ of r)if(_){u(_);try{_.dispose()}catch(o){m.push(o)}}if(m.length===1)throw m[0];if(m.length>1)throw new l(m);return Array.isArray(r)?[]:r}else if(r)return u(r),r.dispose(),r}t.dispose=h;function w(...r){return r.forEach(u),a(()=>h(r))}t.combinedDisposable=w;function a(r){const m=d({dispose:()=>{u(m),r()}});return m}t.toDisposable=a;class S{constructor(){this._toDispose=new Set,this._isDisposed=!1}dispose(){this._isDisposed||(u(this),this._isDisposed=!0,this.clear())}clear(){try{h(this._toDispose.values())}finally{this._toDispose.clear()}}add(m){if(!m)return m;if(m===this)throw new Error("Cannot register a disposable on itself!");return u(m),this._isDisposed?S.DISABLE_DISPOSED_WARNING||console.warn(new Error("Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!").stack):this._toDispose.add(m),m}}t.DisposableStore=S,S.DISABLE_DISPOSED_WARNING=!1;class v{constructor(){this._store=new S,d(this)}dispose(){u(this),this._store.dispose()}_register(m){if(m===this)throw new Error("Cannot register a disposable on itself!");return this._store.add(m)}}t.Disposable=v,v.None=Object.freeze({dispose(){}});class C{constructor(){this._isDisposed=!1,d(this)}get value(){return this._isDisposed?void 0:this._value}set value(m){var _;this._isDisposed||m===this._value||((_=this._value)===null||_===void 0||_.dispose(),m&&u(m),this._value=m)}clear(){this.value=void 0}dispose(){var m;this._isDisposed=!0,u(this),(m=this._value)===null||m===void 0||m.dispose(),this._value=void 0}}t.MutableDisposable=C;class n{constructor(m){this.object=m}dispose(){}}t.ImmortalReference=n}),V(z[18],G([0,1]),function(I,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.LinkedList=void 0;class p{constructor(u){this.element=u,this.next=p.Undefined,this.prev=p.Undefined}}p.Undefined=new p(void 0);class P{constructor(){this._first=p.Undefined,this._last=p.Undefined,this._size=0}get size(){return this._size}isEmpty(){return this._first===p.Undefined}clear(){this._first=p.Undefined,this._last=p.Undefined,this._size=0}unshift(u){return this._insert(u,!1)}push(u){return this._insert(u,!0)}_insert(u,d){const l=new p(u);if(this._first===p.Undefined)this._first=l,this._last=l;else if(d){const h=this._last;this._last=l,l.prev=h,h.next=l}else{const h=this._first;this._first=l,l.next=h,h.prev=l}this._size+=1;let s=!1;return()=>{s||(s=!0,this._remove(l))}}shift(){if(this._first!==p.Undefined){const u=this._first.element;return this._remove(this._first),u}}pop(){if(this._last!==p.Undefined){const u=this._last.element;return this._remove(this._last),u}}_remove(u){if(u.prev!==p.Undefined&&u.next!==p.Undefined){const d=u.prev;d.next=u.next,u.next.prev=d}else u.prev===p.Undefined&&u.next===p.Undefined?(this._first=p.Undefined,this._last=p.Undefined):u.next===p.Undefined?(this._last=this._last.prev,this._last.next=p.Undefined):u.prev===p.Undefined&&(this._first=this._first.next,this._first.prev=p.Undefined);this._size-=1}*[Symbol.iterator](){let u=this._first;for(;u!==p.Undefined;)yield u.element,u=u.next}}t.LinkedList=P}),V(z[2],G([0,1]),function(I,t){"use strict";var p;Object.defineProperty(t,"__esModule",{value:!0}),t.isLittleEndian=t.OS=t.setImmediate=t.globals=t.userAgent=t.isIOS=t.isWeb=t.isNative=t.isLinux=t.isMacintosh=t.isWindows=t.isPreferringBrowserCodeLoad=t.browserCodeLoadingCacheStrategy=t.isElectronSandboxed=void 0;const P="en";let E=!1,u=!1,d=!1,l=!1,s=!1,h=!1,w=!1,a,S=P,v,C;const n=typeof self=="object"?self:typeof global=="object"?global:{};let r;typeof process!="undefined"?r=process:typeof n.vscode!="undefined"&&(r=n.vscode.process);const m=typeof((p=r==null?void 0:r.versions)===null||p===void 0?void 0:p.electron)=="string"&&r.type==="renderer";if(t.isElectronSandboxed=m&&(r==null?void 0:r.sandboxed),t.browserCodeLoadingCacheStrategy=(()=>{if(t.isElectronSandboxed)return"bypassHeatCheck";const b=r==null?void 0:r.env.ENABLE_VSCODE_BROWSER_CODE_LOADING;if(typeof b=="string")return b==="none"||b==="code"||b==="bypassHeatCheck"||b==="bypassHeatCheckAndEagerCompile"?b:"bypassHeatCheck"})(),t.isPreferringBrowserCodeLoad=typeof t.browserCodeLoadingCacheStrategy=="string",typeof navigator=="object"&&!m)C=navigator.userAgent,E=C.indexOf("Windows")>=0,u=C.indexOf("Macintosh")>=0,w=(C.indexOf("Macintosh")>=0||C.indexOf("iPad")>=0||C.indexOf("iPhone")>=0)&&!!navigator.maxTouchPoints&&navigator.maxTouchPoints>0,d=C.indexOf("Linux")>=0,h=!0,a=navigator.language,S=a;else if(typeof r=="object"){E=r.platform==="win32",u=r.platform==="darwin",d=r.platform==="linux",l=d&&!!r.env.SNAP&&!!r.env.SNAP_REVISION,a=P,S=P;const b=r.env.VSCODE_NLS_CONFIG;if(b)try{const N=JSON.parse(b),c=N.availableLanguages["*"];a=N.locale,S=c||P,v=N._translationsConfigFile}catch(N){}s=!0}else console.error("Unable to resolve platform.");let _=0;u?_=1:E?_=3:d&&(_=2),t.isWindows=E,t.isMacintosh=u,t.isLinux=d,t.isNative=s,t.isWeb=h,t.isIOS=w,t.userAgent=C,t.globals=n,t.setImmediate=function(){if(t.globals.setImmediate)return t.globals.setImmediate.bind(t.globals);if(typeof t.globals.postMessage=="function"&&!t.globals.importScripts){let c=[];t.globals.addEventListener("message",L=>{if(L.data&&L.data.vscodeSetImmediateId)for(let A=0,M=c.length;A{const A=++g;c.push({id:A,callback:L}),t.globals.postMessage({vscodeSetImmediateId:A},"*")}}if(r&&typeof r.nextTick=="function")return r.nextTick.bind(r);const N=Promise.resolve();return c=>N.then(c)}(),t.OS=u||w?2:E?1:3;let o=!0,i=!1;function f(){if(!i){i=!0;const b=new Uint8Array(2);b[0]=1,b[1]=2,o=new Uint16Array(b.buffer)[0]===(2<<8)+1}return o}t.isLittleEndian=f}),V(z[19],G([0,1,2]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.platform=t.env=t.cwd=void 0;let P;typeof process!="undefined"?P=process:typeof p.globals.vscode!="undefined"?P={get platform(){return p.globals.vscode.process.platform},get env(){return p.globals.vscode.process.env},nextTick(E){return p.setImmediate(E)},cwd(){return p.globals.vscode.process.env.VSCODE_CWD||p.globals.vscode.process.execPath.substr(0,p.globals.vscode.process.execPath.lastIndexOf(p.globals.vscode.process.platform==="win32"?"\\":"/"))}}:P={get platform(){return p.isWindows?"win32":p.isMacintosh?"darwin":"linux"},nextTick(E){return p.setImmediate(E)},get env(){return Object.create(null)},cwd(){return"/"}},t.cwd=P.cwd,t.env=P.env,t.platform=P.platform}),V(z[20],G([0,1,19]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.sep=t.extname=t.basename=t.dirname=t.relative=t.resolve=t.normalize=t.posix=t.win32=void 0;const P=65,E=97,u=90,d=122,l=46,s=47,h=92,w=58,a=63;class S extends Error{constructor(i,f,b){let N;typeof f=="string"&&f.indexOf("not ")===0?(N="must not be",f=f.replace(/^not /,"")):N="must be";const c=i.indexOf(".")!==-1?"property":"argument";let g=`The "${i}" ${c} ${N} of type ${f}`;g+=`. Received type ${typeof b}`,super(g),this.code="ERR_INVALID_ARG_TYPE"}}function v(o,i){if(typeof o!="string")throw new S(i,"string",o)}function C(o){return o===s||o===h}function n(o){return o===s}function r(o){return o>=P&&o<=u||o>=E&&o<=d}function m(o,i,f,b){let N="",c=0,g=-1,L=0,A=0;for(let M=0;M<=o.length;++M){if(M2){const R=N.lastIndexOf(f);R===-1?(N="",c=0):(N=N.slice(0,R),c=N.length-1-N.lastIndexOf(f)),g=M,L=0;continue}else if(N.length!==0){N="",c=0,g=M,L=0;continue}}i&&(N+=N.length>0?`${f}..`:"..",c=2)}else N.length>0?N+=`${f}${o.slice(g+1,M)}`:N=o.slice(g+1,M),c=M-g-1;g=M,L=0}else A===l&&L!==-1?++L:L=-1}return N}function _(o,i){if(i===null||typeof i!="object")throw new S("pathObject","Object",i);const f=i.dir||i.root,b=i.base||`${i.name||""}${i.ext||""}`;return f?f===i.root?`${f}${b}`:`${f}${o}${b}`:b}t.win32={resolve(...o){let i="",f="",b=!1;for(let N=o.length-1;N>=-1;N--){let c;if(N>=0){if(c=o[N],v(c,"path"),c.length===0)continue}else i.length===0?c=p.cwd():(c=p.env[`=${i}`]||p.cwd(),(c===void 0||c.slice(0,2).toLowerCase()!==i.toLowerCase()&&c.charCodeAt(2)===h)&&(c=`${i}\\`));const g=c.length;let L=0,A="",M=!1;const R=c.charCodeAt(0);if(g===1)C(R)&&(L=1,M=!0);else if(C(R))if(M=!0,C(c.charCodeAt(1))){let D=2,T=D;for(;D2&&C(c.charCodeAt(2))&&(M=!0,L=3));if(A.length>0)if(i.length>0){if(A.toLowerCase()!==i.toLowerCase())continue}else i=A;if(b){if(i.length>0)break}else if(f=`${c.slice(L)}\\${f}`,b=M,M&&i.length>0)break}return f=m(f,!b,"\\",C),b?`${i}\\${f}`:`${i}${f}`||"."},normalize(o){v(o,"path");const i=o.length;if(i===0)return".";let f=0,b,N=!1;const c=o.charCodeAt(0);if(i===1)return n(c)?"\\":o;if(C(c))if(N=!0,C(o.charCodeAt(1))){let L=2,A=L;for(;L2&&C(o.charCodeAt(2))&&(N=!0,f=3));let g=f0&&C(o.charCodeAt(i-1))&&(g+="\\"),b===void 0?N?`\\${g}`:g:N?`${b}\\${g}`:`${b}${g}`},isAbsolute(o){v(o,"path");const i=o.length;if(i===0)return!1;const f=o.charCodeAt(0);return C(f)||i>2&&r(f)&&o.charCodeAt(1)===w&&C(o.charCodeAt(2))},join(...o){if(o.length===0)return".";let i,f;for(let c=0;c0&&(i===void 0?i=f=g:i+=`\\${g}`)}if(i===void 0)return".";let b=!0,N=0;if(typeof f=="string"&&C(f.charCodeAt(0))){++N;const c=f.length;c>1&&C(f.charCodeAt(1))&&(++N,c>2&&(C(f.charCodeAt(2))?++N:b=!1))}if(b){for(;N=2&&(i=`\\${i.slice(N)}`)}return t.win32.normalize(i)},relative(o,i){if(v(o,"from"),v(i,"to"),o===i)return"";const f=t.win32.resolve(o),b=t.win32.resolve(i);if(f===b||(o=f.toLowerCase(),i=b.toLowerCase(),o===i))return"";let N=0;for(;NN&&o.charCodeAt(c-1)===h;)c--;const g=c-N;let L=0;for(;LL&&i.charCodeAt(A-1)===h;)A--;const M=A-L,R=gR){if(i.charCodeAt(L+T)===h)return b.slice(L+T+1);if(T===2)return b.slice(L+T)}g>R&&(o.charCodeAt(N+T)===h?D=T:T===2&&(D=3)),D===-1&&(D=0)}let $="";for(T=N+D+1;T<=c;++T)(T===c||o.charCodeAt(T)===h)&&($+=$.length===0?"..":"\\..");return L+=D,$.length>0?`${$}${b.slice(L,A)}`:(b.charCodeAt(L)===h&&++L,b.slice(L,A))},toNamespacedPath(o){if(typeof o!="string")return o;if(o.length===0)return"";const i=t.win32.resolve(o);if(i.length<=2)return o;if(i.charCodeAt(0)===h){if(i.charCodeAt(1)===h){const f=i.charCodeAt(2);if(f!==a&&f!==l)return`\\\\?\\UNC\\${i.slice(2)}`}}else if(r(i.charCodeAt(0))&&i.charCodeAt(1)===w&&i.charCodeAt(2)===h)return`\\\\?\\${i}`;return o},dirname(o){v(o,"path");const i=o.length;if(i===0)return".";let f=-1,b=0;const N=o.charCodeAt(0);if(i===1)return C(N)?o:".";if(C(N)){if(f=b=1,C(o.charCodeAt(1))){let L=2,A=L;for(;L2&&C(o.charCodeAt(2))?3:2,b=f);let c=-1,g=!0;for(let L=i-1;L>=b;--L)if(C(o.charCodeAt(L))){if(!g){c=L;break}}else g=!1;if(c===-1){if(f===-1)return".";c=f}return o.slice(0,c)},basename(o,i){i!==void 0&&v(i,"ext"),v(o,"path");let f=0,b=-1,N=!0,c;if(o.length>=2&&r(o.charCodeAt(0))&&o.charCodeAt(1)===w&&(f=2),i!==void 0&&i.length>0&&i.length<=o.length){if(i===o)return"";let g=i.length-1,L=-1;for(c=o.length-1;c>=f;--c){const A=o.charCodeAt(c);if(C(A)){if(!N){f=c+1;break}}else L===-1&&(N=!1,L=c+1),g>=0&&(A===i.charCodeAt(g)?--g==-1&&(b=c):(g=-1,b=L))}return f===b?b=L:b===-1&&(b=o.length),o.slice(f,b)}for(c=o.length-1;c>=f;--c)if(C(o.charCodeAt(c))){if(!N){f=c+1;break}}else b===-1&&(N=!1,b=c+1);return b===-1?"":o.slice(f,b)},extname(o){v(o,"path");let i=0,f=-1,b=0,N=-1,c=!0,g=0;o.length>=2&&o.charCodeAt(1)===w&&r(o.charCodeAt(0))&&(i=b=2);for(let L=o.length-1;L>=i;--L){const A=o.charCodeAt(L);if(C(A)){if(!c){b=L+1;break}continue}N===-1&&(c=!1,N=L+1),A===l?f===-1?f=L:g!==1&&(g=1):f!==-1&&(g=-1)}return f===-1||N===-1||g===0||g===1&&f===N-1&&f===b+1?"":o.slice(f,N)},format:_.bind(null,"\\"),parse(o){v(o,"path");const i={root:"",dir:"",base:"",ext:"",name:""};if(o.length===0)return i;const f=o.length;let b=0,N=o.charCodeAt(0);if(f===1)return C(N)?(i.root=i.dir=o,i):(i.base=i.name=o,i);if(C(N)){if(b=1,C(o.charCodeAt(1))){let D=2,T=D;for(;D0&&(i.root=o.slice(0,b));let c=-1,g=b,L=-1,A=!0,M=o.length-1,R=0;for(;M>=b;--M){if(N=o.charCodeAt(M),C(N)){if(!A){g=M+1;break}continue}L===-1&&(A=!1,L=M+1),N===l?c===-1?c=M:R!==1&&(R=1):c!==-1&&(R=-1)}return L!==-1&&(c===-1||R===0||R===1&&c===L-1&&c===g+1?i.base=i.name=o.slice(g,L):(i.name=o.slice(g,c),i.base=o.slice(g,L),i.ext=o.slice(c,L))),g>0&&g!==b?i.dir=o.slice(0,g-1):i.dir=i.root,i},sep:"\\",delimiter:";",win32:null,posix:null},t.posix={resolve(...o){let i="",f=!1;for(let b=o.length-1;b>=-1&&!f;b--){const N=b>=0?o[b]:p.cwd();v(N,"path"),N.length!==0&&(i=`${N}/${i}`,f=N.charCodeAt(0)===s)}return i=m(i,!f,"/",n),f?`/${i}`:i.length>0?i:"."},normalize(o){if(v(o,"path"),o.length===0)return".";const i=o.charCodeAt(0)===s,f=o.charCodeAt(o.length-1)===s;return o=m(o,!i,"/",n),o.length===0?i?"/":f?"./":".":(f&&(o+="/"),i?`/${o}`:o)},isAbsolute(o){return v(o,"path"),o.length>0&&o.charCodeAt(0)===s},join(...o){if(o.length===0)return".";let i;for(let f=0;f0&&(i===void 0?i=b:i+=`/${b}`)}return i===void 0?".":t.posix.normalize(i)},relative(o,i){if(v(o,"from"),v(i,"to"),o===i||(o=t.posix.resolve(o),i=t.posix.resolve(i),o===i))return"";const f=1,b=o.length,N=b-f,c=1,g=i.length-c,L=NL){if(i.charCodeAt(c+M)===s)return i.slice(c+M+1);if(M===0)return i.slice(c+M)}else N>L&&(o.charCodeAt(f+M)===s?A=M:M===0&&(A=0));let R="";for(M=f+A+1;M<=b;++M)(M===b||o.charCodeAt(M)===s)&&(R+=R.length===0?"..":"/..");return`${R}${i.slice(c+A)}`},toNamespacedPath(o){return o},dirname(o){if(v(o,"path"),o.length===0)return".";const i=o.charCodeAt(0)===s;let f=-1,b=!0;for(let N=o.length-1;N>=1;--N)if(o.charCodeAt(N)===s){if(!b){f=N;break}}else b=!1;return f===-1?i?"/":".":i&&f===1?"//":o.slice(0,f)},basename(o,i){i!==void 0&&v(i,"ext"),v(o,"path");let f=0,b=-1,N=!0,c;if(i!==void 0&&i.length>0&&i.length<=o.length){if(i===o)return"";let g=i.length-1,L=-1;for(c=o.length-1;c>=0;--c){const A=o.charCodeAt(c);if(A===s){if(!N){f=c+1;break}}else L===-1&&(N=!1,L=c+1),g>=0&&(A===i.charCodeAt(g)?--g==-1&&(b=c):(g=-1,b=L))}return f===b?b=L:b===-1&&(b=o.length),o.slice(f,b)}for(c=o.length-1;c>=0;--c)if(o.charCodeAt(c)===s){if(!N){f=c+1;break}}else b===-1&&(N=!1,b=c+1);return b===-1?"":o.slice(f,b)},extname(o){v(o,"path");let i=-1,f=0,b=-1,N=!0,c=0;for(let g=o.length-1;g>=0;--g){const L=o.charCodeAt(g);if(L===s){if(!N){f=g+1;break}continue}b===-1&&(N=!1,b=g+1),L===l?i===-1?i=g:c!==1&&(c=1):i!==-1&&(c=-1)}return i===-1||b===-1||c===0||c===1&&i===b-1&&i===f+1?"":o.slice(i,b)},format:_.bind(null,"/"),parse(o){v(o,"path");const i={root:"",dir:"",base:"",ext:"",name:""};if(o.length===0)return i;const f=o.charCodeAt(0)===s;let b;f?(i.root="/",b=1):b=0;let N=-1,c=0,g=-1,L=!0,A=o.length-1,M=0;for(;A>=b;--A){const R=o.charCodeAt(A);if(R===s){if(!L){c=A+1;break}continue}g===-1&&(L=!1,g=A+1),R===l?N===-1?N=A:M!==1&&(M=1):N!==-1&&(M=-1)}if(g!==-1){const R=c===0&&f?1:c;N===-1||M===0||M===1&&N===g-1&&N===c+1?i.base=i.name=o.slice(R,g):(i.name=o.slice(R,N),i.base=o.slice(R,g),i.ext=o.slice(N,g))}return c>0?i.dir=o.slice(0,c-1):f&&(i.dir="/"),i},sep:"/",delimiter:":",win32:null,posix:null},t.posix.win32=t.win32.win32=t.win32,t.posix.posix=t.win32.posix=t.posix,t.normalize=p.platform==="win32"?t.win32.normalize:t.posix.normalize,t.resolve=p.platform==="win32"?t.win32.resolve:t.posix.resolve,t.relative=p.platform==="win32"?t.win32.relative:t.posix.relative,t.dirname=p.platform==="win32"?t.win32.dirname:t.posix.dirname,t.basename=p.platform==="win32"?t.win32.basename:t.posix.basename,t.extname=p.platform==="win32"?t.win32.extname:t.posix.extname,t.sep=p.platform==="win32"?t.win32.sep:t.posix.sep}),V(z[8],G([0,1,2]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.StopWatch=void 0;const P=p.globals.performance&&typeof p.globals.performance.now=="function";class E{constructor(d){this._highResolution=P&&d,this._startTime=this._now(),this._stopTime=-1}static create(d=!0){return new E(d)}stop(){this._stopTime=this._now()}elapsed(){return this._stopTime!==-1?this._stopTime-this._startTime:this._now()-this._startTime}_now(){return this._highResolution?p.globals.performance.now():Date.now()}}t.StopWatch=E}),V(z[9],G([0,1,4,7,18,8]),function(I,t,p,P,E,u){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Relay=t.EventBufferer=t.PauseableEmitter=t.Emitter=t.Event=void 0;var d;(function(C){C.None=()=>P.Disposable.None;function n(k){return(F,O=null,e)=>{let H=!1,B;return B=k(Q=>{if(!H)return B?B.dispose():H=!0,F.call(O,Q)},null,e),H&&B.dispose(),B}}C.once=n;function r(k,F){return b((O,e=null,H)=>k(B=>O.call(e,F(B)),null,H))}C.map=r;function m(k,F){return b((O,e=null,H)=>k(B=>{F(B),O.call(e,B)},null,H))}C.forEach=m;function _(k,F){return b((O,e=null,H)=>k(B=>F(B)&&O.call(e,B),null,H))}C.filter=_;function o(k){return k}C.signal=o;function i(...k){return(F,O=null,e)=>P.combinedDisposable(...k.map(H=>H(B=>F.call(O,B),null,e)))}C.any=i;function f(k,F,O){let e=O;return r(k,H=>(e=F(e,H),e))}C.reduce=f;function b(k){let F;const O=new w({onFirstListenerAdd(){F=k(O.fire,O)},onLastListenerRemove(){F.dispose()}});return O.event}C.snapshot=b;function N(k,F,O=100,e=!1,H){let B,Q,Z,ie=0;const re=new w({leakWarningThreshold:H,onFirstListenerAdd(){B=k(se=>{ie++,Q=F(Q,se),e&&!Z&&(re.fire(Q),Q=void 0),clearTimeout(Z),Z=setTimeout(()=>{const le=Q;Q=void 0,Z=void 0,(!e||ie>1)&&re.fire(le),ie=0},O)})},onLastListenerRemove(){B.dispose()}});return re.event}C.debounce=N;function c(k){const F=new Date().getTime();return r(n(k),O=>new Date().getTime()-F)}C.stopwatch=c;function g(k){let F=!0,O;return _(k,e=>{const H=F||e!==O;return F=!1,O=e,H})}C.latch=g;function L(k,F=!1,O=[]){let e=O.slice(),H=k(Z=>{e?e.push(Z):Q.fire(Z)});const B=()=>{e&&e.forEach(Z=>Q.fire(Z)),e=null},Q=new w({onFirstListenerAdd(){H||(H=k(Z=>Q.fire(Z)))},onFirstListenerDidAdd(){e&&(F?setTimeout(B):B())},onLastListenerRemove(){H&&H.dispose(),H=null}});return Q.event}C.buffer=L;class A{constructor(F){this.event=F}map(F){return new A(r(this.event,F))}forEach(F){return new A(m(this.event,F))}filter(F){return new A(_(this.event,F))}reduce(F,O){return new A(f(this.event,F,O))}latch(){return new A(g(this.event))}debounce(F,O=100,e=!1,H){return new A(N(this.event,F,O,e,H))}on(F,O,e){return this.event(F,O,e)}once(F,O,e){return n(this.event)(F,O,e)}}function M(k){return new A(k)}C.chain=M;function R(k,F,O=e=>e){const e=(...Z)=>Q.fire(O(...Z)),H=()=>k.on(F,e),B=()=>k.removeListener(F,e),Q=new w({onFirstListenerAdd:H,onLastListenerRemove:B});return Q.event}C.fromNodeEventEmitter=R;function D(k,F,O=e=>e){const e=(...Z)=>Q.fire(O(...Z)),H=()=>k.addEventListener(F,e),B=()=>k.removeEventListener(F,e),Q=new w({onFirstListenerAdd:H,onLastListenerRemove:B});return Q.event}C.fromDOMEventEmitter=D;function T(k){const F=new w;let O=!1;return k.then(void 0,()=>null).then(()=>{O?F.fire(void 0):setTimeout(()=>F.fire(void 0),0)}),O=!0,F.event}C.fromPromise=T;function $(k){return new Promise(F=>n(k)(F))}C.toPromise=$})(d=t.Event||(t.Event={}));class l{constructor(n){this._listenerCount=0,this._invocationCount=0,this._elapsedOverall=0,this._name=`${n}_${l._idPool++}`}start(n){this._stopWatch=new u.StopWatch(!0),this._listenerCount=n}stop(){if(this._stopWatch){const n=this._stopWatch.elapsed();this._elapsedOverall+=n,this._invocationCount+=1,console.info(`did FIRE ${this._name}: elapsed_ms: ${n.toFixed(5)}, listener: ${this._listenerCount} (elapsed_overall: ${this._elapsedOverall.toFixed(2)}, invocations: ${this._invocationCount})`),this._stopWatch=void 0}}}l._idPool=0;let s=-1;class h{constructor(n,r=Math.random().toString(18).slice(2,5)){this.customThreshold=n,this.name=r,this._warnCountdown=0}dispose(){this._stacks&&this._stacks.clear()}check(n){let r=s;if(typeof this.customThreshold=="number"&&(r=this.customThreshold),!(r<=0||n{const o=this._stacks.get(m)||0;this._stacks.set(m,o-1)}}}}class w{constructor(n){var r;this._disposed=!1,this._options=n,this._leakageMon=s>0?new h(this._options&&this._options.leakWarningThreshold):void 0,this._perfMon=((r=this._options)===null||r===void 0?void 0:r._profName)?new l(this._options._profName):void 0}get event(){return this._event||(this._event=(n,r,m)=>{var _;this._listeners||(this._listeners=new E.LinkedList);const o=this._listeners.isEmpty();o&&this._options&&this._options.onFirstListenerAdd&&this._options.onFirstListenerAdd(this);const i=this._listeners.push(r?[n,r]:n);o&&this._options&&this._options.onFirstListenerDidAdd&&this._options.onFirstListenerDidAdd(this),this._options&&this._options.onListenerDidAdd&&this._options.onListenerDidAdd(this,n,r);const f=(_=this._leakageMon)===null||_===void 0?void 0:_.check(this._listeners.size);let b;return b={dispose:()=>{f&&f(),b.dispose=w._noop,this._disposed||(i(),this._options&&this._options.onLastListenerRemove&&(this._listeners&&!this._listeners.isEmpty()||this._options.onLastListenerRemove(this)))}},m instanceof P.DisposableStore?m.add(b):Array.isArray(m)&&m.push(b),b}),this._event}fire(n){var r,m;if(this._listeners){this._deliveryQueue||(this._deliveryQueue=new E.LinkedList);for(let _ of this._listeners)this._deliveryQueue.push([_,n]);for((r=this._perfMon)===null||r===void 0||r.start(this._deliveryQueue.size);this._deliveryQueue.size>0;){const[_,o]=this._deliveryQueue.shift();try{typeof _=="function"?_.call(void 0,o):_[0].call(_[1],o)}catch(i){p.onUnexpectedError(i)}}(m=this._perfMon)===null||m===void 0||m.stop()}}dispose(){var n,r,m;(n=this._listeners)===null||n===void 0||n.clear(),(r=this._deliveryQueue)===null||r===void 0||r.clear(),(m=this._leakageMon)===null||m===void 0||m.dispose(),this._disposed=!0}}t.Emitter=w,w._noop=function(){};class a extends w{constructor(n){super(n);this._isPaused=0,this._eventQueue=new E.LinkedList,this._mergeFn=n==null?void 0:n.merge}pause(){this._isPaused++}resume(){if(this._isPaused!==0&&--this._isPaused==0)if(this._mergeFn){const n=Array.from(this._eventQueue);this._eventQueue.clear(),super.fire(this._mergeFn(n))}else for(;!this._isPaused&&this._eventQueue.size!==0;)super.fire(this._eventQueue.shift())}fire(n){this._listeners&&(this._isPaused!==0?this._eventQueue.push(n):super.fire(n))}}t.PauseableEmitter=a;class S{constructor(){this.buffers=[]}wrapEvent(n){return(r,m,_)=>n(o=>{const i=this.buffers[this.buffers.length-1];i?i.push(()=>r.call(m,o)):r.call(m,o)},void 0,_)}bufferEvents(n){const r=[];this.buffers.push(r);const m=n();return this.buffers.pop(),r.forEach(_=>_()),m}}t.EventBufferer=S;class v{constructor(){this.listening=!1,this.inputEvent=d.None,this.inputEventListener=P.Disposable.None,this.emitter=new w({onFirstListenerDidAdd:()=>{this.listening=!0,this.inputEventListener=this.inputEvent(this.emitter.fire,this.emitter)},onLastListenerRemove:()=>{this.listening=!1,this.inputEventListener.dispose()}}),this.event=this.emitter.event}set input(n){this.inputEvent=n,this.listening&&(this.inputEventListener.dispose(),this.inputEventListener=n(this.emitter.fire,this.emitter))}dispose(){this.inputEventListener.dispose(),this.emitter.dispose()}}t.Relay=v}),V(z[21],G([0,1,9]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CancellationTokenSource=t.CancellationToken=void 0;const P=Object.freeze(function(l,s){const h=setTimeout(l.bind(s),0);return{dispose(){clearTimeout(h)}}});var E;(function(l){function s(h){return h===l.None||h===l.Cancelled||h instanceof u?!0:!h||typeof h!="object"?!1:typeof h.isCancellationRequested=="boolean"&&typeof h.onCancellationRequested=="function"}l.isCancellationToken=s,l.None=Object.freeze({isCancellationRequested:!1,onCancellationRequested:p.Event.None}),l.Cancelled=Object.freeze({isCancellationRequested:!0,onCancellationRequested:P})})(E=t.CancellationToken||(t.CancellationToken={}));class u{constructor(){this._isCancelled=!1,this._emitter=null}cancel(){this._isCancelled||(this._isCancelled=!0,this._emitter&&(this._emitter.fire(void 0),this.dispose()))}get isCancellationRequested(){return this._isCancelled}get onCancellationRequested(){return this._isCancelled?P:(this._emitter||(this._emitter=new p.Emitter),this._emitter.event)}dispose(){this._emitter&&(this._emitter.dispose(),this._emitter=null)}}class d{constructor(s){this._token=void 0,this._parentListener=void 0,this._parentListener=s&&s.onCancellationRequested(this.cancel,this)}get token(){return this._token||(this._token=new u),this._token}cancel(){this._token?this._token instanceof u&&this._token.cancel():this._token=E.Cancelled}dispose(s=!1){s&&this.cancel(),this._parentListener&&this._parentListener.dispose(),this._token?this._token instanceof u&&this._token.dispose():this._token=E.None}}t.CancellationTokenSource=d}),V(z[5],G([0,1]),function(I,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.breakBetweenGraphemeBreakType=t.getGraphemeBreakType=t.singleLetterHash=t.containsUppercaseCharacter=t.startsWithUTF8BOM=t.UTF8_BOM_CHARACTER=t.isEmojiImprecise=t.isFullWidthCharacter=t.containsFullWidthCharacter=t.containsUnusualLineTerminators=t.UNUSUAL_LINE_TERMINATORS=t.isBasicASCII=t.containsEmoji=t.containsRTL=t.decodeUTF8=t.prevCharLength=t.nextCharLength=t.getNextCodePoint=t.computeCodePoint=t.isLowSurrogate=t.isHighSurrogate=t.commonSuffixLength=t.commonPrefixLength=t.startsWithIgnoreCase=t.equalsIgnoreCase=t.isUpperAsciiLetter=t.isLowerAsciiLetter=t.compareSubstringIgnoreCase=t.compareIgnoreCase=t.compareSubstring=t.compare=t.lastNonWhitespaceIndex=t.getLeadingWhitespace=t.firstNonWhitespaceIndex=t.splitLines=t.regExpFlags=t.regExpLeadsToEndlessLoop=t.createRegExp=t.stripWildcards=t.convertSimple2RegExpPattern=t.rtrim=t.ltrim=t.trim=t.escapeRegExpCharacters=t.escape=t.format=t.isFalsyOrWhitespace=void 0;function p(y){return!y||typeof y!="string"?!0:y.trim().length===0}t.isFalsyOrWhitespace=p;const P=/{(\d+)}/g;function E(y,...U){return U.length===0?y:y.replace(P,function(W,q){const j=parseInt(q,10);return isNaN(j)||j<0||j>=U.length?W:U[j]})}t.format=E;function u(y){return y.replace(/[<>&]/g,function(U){switch(U){case"<":return"<";case">":return">";case"&":return"&";default:return U}})}t.escape=u;function d(y){return y.replace(/[\\\{\}\*\+\?\|\^\$\.\[\]\(\)]/g,"\\$&")}t.escapeRegExpCharacters=d;function l(y,U=" "){const W=s(y,U);return h(W,U)}t.trim=l;function s(y,U){if(!y||!U)return y;const W=U.length;if(W===0||y.length===0)return y;let q=0;for(;y.indexOf(U,q)===q;)q=q+W;return y.substring(q)}t.ltrim=s;function h(y,U){if(!y||!U)return y;const W=U.length,q=y.length;if(W===0||q===0)return y;let j=q,Y=-1;for(;Y=y.lastIndexOf(U,j-1),!(Y===-1||Y+W!==j);){if(Y===0)return"";j=Y}return y.substring(0,j)}t.rtrim=h;function w(y){return y.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g,"\\$&").replace(/[\*]/g,".*")}t.convertSimple2RegExpPattern=w;function a(y){return y.replace(/\*/g,"")}t.stripWildcards=a;function S(y,U,W={}){if(!y)throw new Error("Cannot create regex from empty string");U||(y=d(y)),W.wholeWord&&(/\B/.test(y.charAt(0))||(y="\\b"+y),/\B/.test(y.charAt(y.length-1))||(y=y+"\\b"));let q="";return W.global&&(q+="g"),W.matchCase||(q+="i"),W.multiline&&(q+="m"),W.unicode&&(q+="u"),new RegExp(y,q)}t.createRegExp=S;function v(y){return y.source==="^"||y.source==="^$"||y.source==="$"||y.source==="^\\s*$"?!1:!!(y.exec("")&&y.lastIndex===0)}t.regExpLeadsToEndlessLoop=v;function C(y){return(y.global?"g":"")+(y.ignoreCase?"i":"")+(y.multiline?"m":"")+(y.unicode?"u":"")}t.regExpFlags=C;function n(y){return y.split(/\r\n|\r|\n/)}t.splitLines=n;function r(y){for(let U=0,W=y.length;U=0;W--){const q=y.charCodeAt(W);if(q!==32&&q!==9)return W}return-1}t.lastNonWhitespaceIndex=_;function o(y,U){return yU?1:0}t.compare=o;function i(y,U,W=0,q=y.length,j=0,Y=U.length){for(;Wte)return 1}const J=q-W,x=Y-j;return Jx?1:0}t.compareSubstring=i;function f(y,U){return b(y,U,0,y.length,0,U.length)}t.compareIgnoreCase=f;function b(y,U,W=0,q=y.length,j=0,Y=U.length){for(;Wx?1:0}t.compareSubstringIgnoreCase=b;function N(y){return y>=97&&y<=122}t.isLowerAsciiLetter=N;function c(y){return y>=65&&y<=90}t.isUpperAsciiLetter=c;function g(y){return N(y)||c(y)}function L(y,U){return y.length===U.length&&A(y,U)}t.equalsIgnoreCase=L;function A(y,U,W=y.length){for(let q=0;qy.length?!1:A(y,U,W)}t.startsWithIgnoreCase=M;function R(y,U){let W,q=Math.min(y.length,U.length);for(W=0;W1){const q=y.charCodeAt(U-2);if(T(q))return k(q,W)}return W}function e(y,U){const W=ee.getInstance(),q=U,j=y.length,Y=F(y,j,U);U+=Y>=65536?2:1;let J=W.getGraphemeBreakType(Y);for(;U=65536?2:1,J=K}return U-q}t.nextCharLength=e;function H(y,U){const W=ee.getInstance(),q=U,j=O(y,U);U-=j>=65536?2:1;let Y=W.getGraphemeBreakType(j);for(;U>0;){const J=O(y,U),x=W.getGraphemeBreakType(J);if(ue(x,Y))break;U-=J>=65536?2:1,Y=x}return q-U}t.prevCharLength=H;function B(y){const U=y.byteLength,W=[];let q=0;for(;q=240&&q+3>>0|(y[q++]&63)<<12>>>0|(y[q++]&63)<<6>>>0|(y[q++]&63)<<0>>>0:j>=224&&q+2>>0|(y[q++]&63)<<6>>>0|(y[q++]&63)<<0>>>0:j>=192&&q+1>>0|(y[q++]&63)<<0>>>0:Y=y[q++],Y>=0&&Y<=55295||Y>=57344&&Y<=65535)W.push(String.fromCharCode(Y));else if(Y>=65536&&Y<=1114111){const J=Y-65536,x=55296+((J&1047552)>>>10),K=56320+((J&1023)>>>0);W.push(String.fromCharCode(x)),W.push(String.fromCharCode(K))}else W.push(String.fromCharCode(65533))}return W.join("")}t.decodeUTF8=B;const Q=/(?:[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05F4\u0608\u060B\u060D\u061B-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u0710\u0712-\u072F\u074D-\u07A5\u07B1-\u07EA\u07F4\u07F5\u07FA-\u0815\u081A\u0824\u0828\u0830-\u0858\u085E-\u08BD\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFD3D\uFD50-\uFDFC\uFE70-\uFEFC]|\uD802[\uDC00-\uDD1B\uDD20-\uDE00\uDE10-\uDE33\uDE40-\uDEE4\uDEEB-\uDF35\uDF40-\uDFFF]|\uD803[\uDC00-\uDCFF]|\uD83A[\uDC00-\uDCCF\uDD00-\uDD43\uDD50-\uDFFF]|\uD83B[\uDC00-\uDEBB])/;function Z(y){return Q.test(y)}t.containsRTL=Z;const ie=/(?:[\u231A\u231B\u23F0\u23F3\u2600-\u27BF\u2B50\u2B55]|\uD83C[\uDDE6-\uDDFF\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F\uDE80-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD00-\uDDFF\uDE70-\uDED6])/;function re(y){return ie.test(y)}t.containsEmoji=re;const se=/^[\t\n\r\x20-\x7E]*$/;function le(y){return se.test(y)}t.isBasicASCII=le,t.UNUSUAL_LINE_TERMINATORS=/[\u2028\u2029]/;function he(y){return t.UNUSUAL_LINE_TERMINATORS.test(y)}t.containsUnusualLineTerminators=he;function de(y){for(let U=0,W=y.length;U=11904&&y<=55215||y>=63744&&y<=64255||y>=65281&&y<=65374}t.isFullWidthCharacter=ae;function me(y){return y>=127462&&y<=127487||y===8986||y===8987||y===9200||y===9203||y>=9728&&y<=10175||y===11088||y===11093||y>=127744&&y<=128591||y>=128640&&y<=128764||y>=128992&&y<=129003||y>=129280&&y<=129535||y>=129648&&y<=129750}t.isEmojiImprecise=me,t.UTF8_BOM_CHARACTER=String.fromCharCode(65279);function ge(y){return!!(y&&y.length>0&&y.charCodeAt(0)===65279)}t.startsWithUTF8BOM=ge;function _e(y,U=!1){return y?(U&&(y=y.replace(/\\./g,"")),y.toLowerCase()!==y):!1}t.containsUppercaseCharacter=_e;function ve(y){const U=90-65+1;return y=y%(2*U),yW[3*j+1])j=2*j+1;else return W[3*j+2];return 0}}ee._INSTANCE=null;function be(){return JSON.parse("[0,0,0,51592,51592,11,44424,44424,11,72251,72254,5,7150,7150,7,48008,48008,11,55176,55176,11,128420,128420,14,3276,3277,5,9979,9980,14,46216,46216,11,49800,49800,11,53384,53384,11,70726,70726,5,122915,122916,5,129320,129327,14,2558,2558,5,5906,5908,5,9762,9763,14,43360,43388,8,45320,45320,11,47112,47112,11,48904,48904,11,50696,50696,11,52488,52488,11,54280,54280,11,70082,70083,1,71350,71350,7,73111,73111,5,127892,127893,14,128726,128727,14,129473,129474,14,2027,2035,5,2901,2902,5,3784,3789,5,6754,6754,5,8418,8420,5,9877,9877,14,11088,11088,14,44008,44008,5,44872,44872,11,45768,45768,11,46664,46664,11,47560,47560,11,48456,48456,11,49352,49352,11,50248,50248,11,51144,51144,11,52040,52040,11,52936,52936,11,53832,53832,11,54728,54728,11,69811,69814,5,70459,70460,5,71096,71099,7,71998,71998,5,72874,72880,5,119149,119149,7,127374,127374,14,128335,128335,14,128482,128482,14,128765,128767,14,129399,129400,14,129680,129685,14,1476,1477,5,2377,2380,7,2759,2760,5,3137,3140,7,3458,3459,7,4153,4154,5,6432,6434,5,6978,6978,5,7675,7679,5,9723,9726,14,9823,9823,14,9919,9923,14,10035,10036,14,42736,42737,5,43596,43596,5,44200,44200,11,44648,44648,11,45096,45096,11,45544,45544,11,45992,45992,11,46440,46440,11,46888,46888,11,47336,47336,11,47784,47784,11,48232,48232,11,48680,48680,11,49128,49128,11,49576,49576,11,50024,50024,11,50472,50472,11,50920,50920,11,51368,51368,11,51816,51816,11,52264,52264,11,52712,52712,11,53160,53160,11,53608,53608,11,54056,54056,11,54504,54504,11,54952,54952,11,68108,68111,5,69933,69940,5,70197,70197,7,70498,70499,7,70845,70845,5,71229,71229,5,71727,71735,5,72154,72155,5,72344,72345,5,73023,73029,5,94095,94098,5,121403,121452,5,126981,127182,14,127538,127546,14,127990,127990,14,128391,128391,14,128445,128449,14,128500,128505,14,128752,128752,14,129160,129167,14,129356,129356,14,129432,129442,14,129648,129651,14,129751,131069,14,173,173,4,1757,1757,1,2274,2274,1,2494,2494,5,2641,2641,5,2876,2876,5,3014,3016,7,3262,3262,7,3393,3396,5,3570,3571,7,3968,3972,5,4228,4228,7,6086,6086,5,6679,6680,5,6912,6915,5,7080,7081,5,7380,7392,5,8252,8252,14,9096,9096,14,9748,9749,14,9784,9786,14,9833,9850,14,9890,9894,14,9938,9938,14,9999,9999,14,10085,10087,14,12349,12349,14,43136,43137,7,43454,43456,7,43755,43755,7,44088,44088,11,44312,44312,11,44536,44536,11,44760,44760,11,44984,44984,11,45208,45208,11,45432,45432,11,45656,45656,11,45880,45880,11,46104,46104,11,46328,46328,11,46552,46552,11,46776,46776,11,47000,47000,11,47224,47224,11,47448,47448,11,47672,47672,11,47896,47896,11,48120,48120,11,48344,48344,11,48568,48568,11,48792,48792,11,49016,49016,11,49240,49240,11,49464,49464,11,49688,49688,11,49912,49912,11,50136,50136,11,50360,50360,11,50584,50584,11,50808,50808,11,51032,51032,11,51256,51256,11,51480,51480,11,51704,51704,11,51928,51928,11,52152,52152,11,52376,52376,11,52600,52600,11,52824,52824,11,53048,53048,11,53272,53272,11,53496,53496,11,53720,53720,11,53944,53944,11,54168,54168,11,54392,54392,11,54616,54616,11,54840,54840,11,55064,55064,11,65438,65439,5,69633,69633,5,69837,69837,1,70018,70018,7,70188,70190,7,70368,70370,7,70465,70468,7,70712,70719,5,70835,70840,5,70850,70851,5,71132,71133,5,71340,71340,7,71458,71461,5,71985,71989,7,72002,72002,7,72193,72202,5,72281,72283,5,72766,72766,7,72885,72886,5,73104,73105,5,92912,92916,5,113824,113827,4,119173,119179,5,121505,121519,5,125136,125142,5,127279,127279,14,127489,127490,14,127570,127743,14,127900,127901,14,128254,128254,14,128369,128370,14,128400,128400,14,128425,128432,14,128468,128475,14,128489,128494,14,128715,128720,14,128745,128745,14,128759,128760,14,129004,129023,14,129296,129304,14,129340,129342,14,129388,129392,14,129404,129407,14,129454,129455,14,129485,129487,14,129659,129663,14,129719,129727,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2363,2363,7,2402,2403,5,2507,2508,7,2622,2624,7,2691,2691,7,2786,2787,5,2881,2884,5,3006,3006,5,3072,3072,5,3170,3171,5,3267,3268,7,3330,3331,7,3406,3406,1,3538,3540,5,3655,3662,5,3897,3897,5,4038,4038,5,4184,4185,5,4352,4447,8,6068,6069,5,6155,6157,5,6448,6449,7,6742,6742,5,6783,6783,5,6966,6970,5,7042,7042,7,7143,7143,7,7212,7219,5,7412,7412,5,8206,8207,4,8294,8303,4,8596,8601,14,9410,9410,14,9742,9742,14,9757,9757,14,9770,9770,14,9794,9794,14,9828,9828,14,9855,9855,14,9882,9882,14,9900,9903,14,9929,9933,14,9963,9967,14,9987,9988,14,10006,10006,14,10062,10062,14,10175,10175,14,11744,11775,5,42607,42607,5,43043,43044,7,43263,43263,5,43444,43445,7,43569,43570,5,43698,43700,5,43766,43766,5,44032,44032,11,44144,44144,11,44256,44256,11,44368,44368,11,44480,44480,11,44592,44592,11,44704,44704,11,44816,44816,11,44928,44928,11,45040,45040,11,45152,45152,11,45264,45264,11,45376,45376,11,45488,45488,11,45600,45600,11,45712,45712,11,45824,45824,11,45936,45936,11,46048,46048,11,46160,46160,11,46272,46272,11,46384,46384,11,46496,46496,11,46608,46608,11,46720,46720,11,46832,46832,11,46944,46944,11,47056,47056,11,47168,47168,11,47280,47280,11,47392,47392,11,47504,47504,11,47616,47616,11,47728,47728,11,47840,47840,11,47952,47952,11,48064,48064,11,48176,48176,11,48288,48288,11,48400,48400,11,48512,48512,11,48624,48624,11,48736,48736,11,48848,48848,11,48960,48960,11,49072,49072,11,49184,49184,11,49296,49296,11,49408,49408,11,49520,49520,11,49632,49632,11,49744,49744,11,49856,49856,11,49968,49968,11,50080,50080,11,50192,50192,11,50304,50304,11,50416,50416,11,50528,50528,11,50640,50640,11,50752,50752,11,50864,50864,11,50976,50976,11,51088,51088,11,51200,51200,11,51312,51312,11,51424,51424,11,51536,51536,11,51648,51648,11,51760,51760,11,51872,51872,11,51984,51984,11,52096,52096,11,52208,52208,11,52320,52320,11,52432,52432,11,52544,52544,11,52656,52656,11,52768,52768,11,52880,52880,11,52992,52992,11,53104,53104,11,53216,53216,11,53328,53328,11,53440,53440,11,53552,53552,11,53664,53664,11,53776,53776,11,53888,53888,11,54000,54000,11,54112,54112,11,54224,54224,11,54336,54336,11,54448,54448,11,54560,54560,11,54672,54672,11,54784,54784,11,54896,54896,11,55008,55008,11,55120,55120,11,64286,64286,5,66272,66272,5,68900,68903,5,69762,69762,7,69817,69818,5,69927,69931,5,70003,70003,5,70070,70078,5,70094,70094,7,70194,70195,7,70206,70206,5,70400,70401,5,70463,70463,7,70475,70477,7,70512,70516,5,70722,70724,5,70832,70832,5,70842,70842,5,70847,70848,5,71088,71089,7,71102,71102,7,71219,71226,5,71231,71232,5,71342,71343,7,71453,71455,5,71463,71467,5,71737,71738,5,71995,71996,5,72000,72000,7,72145,72147,7,72160,72160,5,72249,72249,7,72273,72278,5,72330,72342,5,72752,72758,5,72850,72871,5,72882,72883,5,73018,73018,5,73031,73031,5,73109,73109,5,73461,73462,7,94031,94031,5,94192,94193,7,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,126976,126979,14,127184,127231,14,127344,127345,14,127405,127461,14,127514,127514,14,127561,127567,14,127778,127779,14,127896,127896,14,127985,127986,14,127995,127999,5,128326,128328,14,128360,128366,14,128378,128378,14,128394,128397,14,128405,128406,14,128422,128423,14,128435,128443,14,128453,128464,14,128479,128480,14,128484,128487,14,128496,128498,14,128640,128709,14,128723,128724,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129096,129103,14,129292,129292,14,129311,129311,14,129329,129330,14,129344,129349,14,129360,129374,14,129394,129394,14,129402,129402,14,129413,129425,14,129445,129450,14,129466,129471,14,129483,129483,14,129511,129535,14,129653,129655,14,129667,129670,14,129705,129711,14,129731,129743,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2307,2307,7,2366,2368,7,2382,2383,7,2434,2435,7,2497,2500,5,2519,2519,5,2563,2563,7,2631,2632,5,2677,2677,5,2750,2752,7,2763,2764,7,2817,2817,5,2879,2879,5,2891,2892,7,2914,2915,5,3008,3008,5,3021,3021,5,3076,3076,5,3146,3149,5,3202,3203,7,3264,3265,7,3271,3272,7,3298,3299,5,3390,3390,5,3402,3404,7,3426,3427,5,3535,3535,5,3544,3550,7,3635,3635,7,3763,3763,7,3893,3893,5,3953,3966,5,3981,3991,5,4145,4145,7,4157,4158,5,4209,4212,5,4237,4237,5,4520,4607,10,5970,5971,5,6071,6077,5,6089,6099,5,6277,6278,5,6439,6440,5,6451,6456,7,6683,6683,5,6744,6750,5,6765,6770,7,6846,6846,5,6964,6964,5,6972,6972,5,7019,7027,5,7074,7077,5,7083,7085,5,7146,7148,7,7154,7155,7,7222,7223,5,7394,7400,5,7416,7417,5,8204,8204,5,8233,8233,4,8288,8292,4,8413,8416,5,8482,8482,14,8986,8987,14,9193,9203,14,9654,9654,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9775,14,9792,9792,14,9800,9811,14,9825,9826,14,9831,9831,14,9852,9853,14,9872,9873,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9936,9936,14,9941,9960,14,9974,9974,14,9982,9985,14,9992,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10145,10145,14,11013,11015,14,11503,11505,5,12334,12335,5,12951,12951,14,42612,42621,5,43014,43014,5,43047,43047,7,43204,43205,5,43335,43345,5,43395,43395,7,43450,43451,7,43561,43566,5,43573,43574,5,43644,43644,5,43710,43711,5,43758,43759,7,44005,44005,5,44012,44012,7,44060,44060,11,44116,44116,11,44172,44172,11,44228,44228,11,44284,44284,11,44340,44340,11,44396,44396,11,44452,44452,11,44508,44508,11,44564,44564,11,44620,44620,11,44676,44676,11,44732,44732,11,44788,44788,11,44844,44844,11,44900,44900,11,44956,44956,11,45012,45012,11,45068,45068,11,45124,45124,11,45180,45180,11,45236,45236,11,45292,45292,11,45348,45348,11,45404,45404,11,45460,45460,11,45516,45516,11,45572,45572,11,45628,45628,11,45684,45684,11,45740,45740,11,45796,45796,11,45852,45852,11,45908,45908,11,45964,45964,11,46020,46020,11,46076,46076,11,46132,46132,11,46188,46188,11,46244,46244,11,46300,46300,11,46356,46356,11,46412,46412,11,46468,46468,11,46524,46524,11,46580,46580,11,46636,46636,11,46692,46692,11,46748,46748,11,46804,46804,11,46860,46860,11,46916,46916,11,46972,46972,11,47028,47028,11,47084,47084,11,47140,47140,11,47196,47196,11,47252,47252,11,47308,47308,11,47364,47364,11,47420,47420,11,47476,47476,11,47532,47532,11,47588,47588,11,47644,47644,11,47700,47700,11,47756,47756,11,47812,47812,11,47868,47868,11,47924,47924,11,47980,47980,11,48036,48036,11,48092,48092,11,48148,48148,11,48204,48204,11,48260,48260,11,48316,48316,11,48372,48372,11,48428,48428,11,48484,48484,11,48540,48540,11,48596,48596,11,48652,48652,11,48708,48708,11,48764,48764,11,48820,48820,11,48876,48876,11,48932,48932,11,48988,48988,11,49044,49044,11,49100,49100,11,49156,49156,11,49212,49212,11,49268,49268,11,49324,49324,11,49380,49380,11,49436,49436,11,49492,49492,11,49548,49548,11,49604,49604,11,49660,49660,11,49716,49716,11,49772,49772,11,49828,49828,11,49884,49884,11,49940,49940,11,49996,49996,11,50052,50052,11,50108,50108,11,50164,50164,11,50220,50220,11,50276,50276,11,50332,50332,11,50388,50388,11,50444,50444,11,50500,50500,11,50556,50556,11,50612,50612,11,50668,50668,11,50724,50724,11,50780,50780,11,50836,50836,11,50892,50892,11,50948,50948,11,51004,51004,11,51060,51060,11,51116,51116,11,51172,51172,11,51228,51228,11,51284,51284,11,51340,51340,11,51396,51396,11,51452,51452,11,51508,51508,11,51564,51564,11,51620,51620,11,51676,51676,11,51732,51732,11,51788,51788,11,51844,51844,11,51900,51900,11,51956,51956,11,52012,52012,11,52068,52068,11,52124,52124,11,52180,52180,11,52236,52236,11,52292,52292,11,52348,52348,11,52404,52404,11,52460,52460,11,52516,52516,11,52572,52572,11,52628,52628,11,52684,52684,11,52740,52740,11,52796,52796,11,52852,52852,11,52908,52908,11,52964,52964,11,53020,53020,11,53076,53076,11,53132,53132,11,53188,53188,11,53244,53244,11,53300,53300,11,53356,53356,11,53412,53412,11,53468,53468,11,53524,53524,11,53580,53580,11,53636,53636,11,53692,53692,11,53748,53748,11,53804,53804,11,53860,53860,11,53916,53916,11,53972,53972,11,54028,54028,11,54084,54084,11,54140,54140,11,54196,54196,11,54252,54252,11,54308,54308,11,54364,54364,11,54420,54420,11,54476,54476,11,54532,54532,11,54588,54588,11,54644,54644,11,54700,54700,11,54756,54756,11,54812,54812,11,54868,54868,11,54924,54924,11,54980,54980,11,55036,55036,11,55092,55092,11,55148,55148,11,55216,55238,9,65056,65071,5,65529,65531,4,68097,68099,5,68159,68159,5,69446,69456,5,69688,69702,5,69808,69810,7,69815,69816,7,69821,69821,1,69888,69890,5,69932,69932,7,69957,69958,7,70016,70017,5,70067,70069,7,70079,70080,7,70089,70092,5,70095,70095,5,70191,70193,5,70196,70196,5,70198,70199,5,70367,70367,5,70371,70378,5,70402,70403,7,70462,70462,5,70464,70464,5,70471,70472,7,70487,70487,5,70502,70508,5,70709,70711,7,70720,70721,7,70725,70725,7,70750,70750,5,70833,70834,7,70841,70841,7,70843,70844,7,70846,70846,7,70849,70849,7,71087,71087,5,71090,71093,5,71100,71101,5,71103,71104,5,71216,71218,7,71227,71228,7,71230,71230,7,71339,71339,5,71341,71341,5,71344,71349,5,71351,71351,5,71456,71457,7,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,94180,94180,5,113821,113822,5,119141,119141,5,119143,119145,5,119150,119154,5,119163,119170,5,119210,119213,5,121344,121398,5,121461,121461,5,121499,121503,5,122880,122886,5,122907,122913,5,122918,122922,5,123628,123631,5,125252,125258,5,126980,126980,14,127183,127183,14,127245,127247,14,127340,127343,14,127358,127359,14,127377,127386,14,127462,127487,6,127491,127503,14,127535,127535,14,127548,127551,14,127568,127569,14,127744,127777,14,127780,127891,14,127894,127895,14,127897,127899,14,127902,127984,14,127987,127989,14,127991,127994,14,128000,128253,14,128255,128317,14,128329,128334,14,128336,128359,14,128367,128368,14,128371,128377,14,128379,128390,14,128392,128393,14,128398,128399,14,128401,128404,14,128407,128419,14,128421,128421,14,128424,128424,14,128433,128434,14,128444,128444,14,128450,128452,14,128465,128467,14,128476,128478,14,128481,128481,14,128483,128483,14,128488,128488,14,128495,128495,14,128499,128499,14,128506,128591,14,128710,128714,14,128721,128722,14,128725,128725,14,128728,128735,14,128742,128744,14,128746,128746,14,128749,128751,14,128753,128754,14,128756,128758,14,128761,128761,14,128763,128764,14,128884,128895,14,128992,129003,14,129036,129039,14,129114,129119,14,129198,129279,14,129293,129295,14,129305,129310,14,129312,129319,14,129328,129328,14,129331,129338,14,129343,129343,14,129351,129355,14,129357,129359,14,129375,129387,14,129393,129393,14,129395,129398,14,129401,129401,14,129403,129403,14,129408,129412,14,129426,129431,14,129443,129444,14,129451,129453,14,129456,129465,14,129472,129472,14,129475,129482,14,129484,129484,14,129488,129510,14,129536,129647,14,129652,129652,14,129656,129658,14,129664,129666,14,129671,129679,14,129686,129704,14,129712,129718,14,129728,129730,14,129744,129750,14,917504,917504,4,917506,917535,4,917632,917759,4,918000,921599,4,0,9,4,11,12,4,14,31,4,169,169,14,174,174,14,1155,1159,5,1425,1469,5,1473,1474,5,1479,1479,5,1552,1562,5,1611,1631,5,1750,1756,5,1759,1764,5,1770,1773,5,1809,1809,5,1958,1968,5,2045,2045,5,2075,2083,5,2089,2093,5,2259,2273,5,2275,2306,5,2362,2362,5,2364,2364,5,2369,2376,5,2381,2381,5,2385,2391,5,2433,2433,5,2492,2492,5,2495,2496,7,2503,2504,7,2509,2509,5,2530,2531,5,2561,2562,5,2620,2620,5,2625,2626,5,2635,2637,5,2672,2673,5,2689,2690,5,2748,2748,5,2753,2757,5,2761,2761,7,2765,2765,5,2810,2815,5,2818,2819,7,2878,2878,5,2880,2880,7,2887,2888,7,2893,2893,5,2903,2903,5,2946,2946,5,3007,3007,7,3009,3010,7,3018,3020,7,3031,3031,5,3073,3075,7,3134,3136,5,3142,3144,5,3157,3158,5,3201,3201,5,3260,3260,5,3263,3263,5,3266,3266,5,3270,3270,5,3274,3275,7,3285,3286,5,3328,3329,5,3387,3388,5,3391,3392,7,3398,3400,7,3405,3405,5,3415,3415,5,3457,3457,5,3530,3530,5,3536,3537,7,3542,3542,5,3551,3551,5,3633,3633,5,3636,3642,5,3761,3761,5,3764,3772,5,3864,3865,5,3895,3895,5,3902,3903,7,3967,3967,7,3974,3975,5,3993,4028,5,4141,4144,5,4146,4151,5,4155,4156,7,4182,4183,7,4190,4192,5,4226,4226,5,4229,4230,5,4253,4253,5,4448,4519,9,4957,4959,5,5938,5940,5,6002,6003,5,6070,6070,7,6078,6085,7,6087,6088,7,6109,6109,5,6158,6158,4,6313,6313,5,6435,6438,7,6441,6443,7,6450,6450,5,6457,6459,5,6681,6682,7,6741,6741,7,6743,6743,7,6752,6752,5,6757,6764,5,6771,6780,5,6832,6845,5,6847,6848,5,6916,6916,7,6965,6965,5,6971,6971,7,6973,6977,7,6979,6980,7,7040,7041,5,7073,7073,7,7078,7079,7,7082,7082,7,7142,7142,5,7144,7145,5,7149,7149,5,7151,7153,5,7204,7211,7,7220,7221,7,7376,7378,5,7393,7393,7,7405,7405,5,7415,7415,7,7616,7673,5,8203,8203,4,8205,8205,13,8232,8232,4,8234,8238,4,8265,8265,14,8293,8293,4,8400,8412,5,8417,8417,5,8421,8432,5,8505,8505,14,8617,8618,14,9000,9000,14,9167,9167,14,9208,9210,14,9642,9643,14,9664,9664,14,9728,9732,14,9735,9741,14,9743,9744,14,9746,9746,14,9750,9751,14,9753,9756,14,9758,9759,14,9761,9761,14,9764,9765,14,9767,9769,14,9771,9773,14,9776,9783,14,9787,9791,14,9793,9793,14,9795,9799,14,9812,9822,14,9824,9824,14,9827,9827,14,9829,9830,14,9832,9832,14,9851,9851,14,9854,9854,14,9856,9861,14,9874,9876,14,9878,9879,14,9881,9881,14,9883,9884,14,9888,9889,14,9895,9895,14,9898,9899,14,9904,9905,14,9917,9918,14,9924,9925,14,9928,9928,14,9934,9935,14,9937,9937,14,9939,9940,14,9961,9962,14,9968,9973,14,9975,9978,14,9981,9981,14,9986,9986,14,9989,9989,14,9998,9998,14,10000,10001,14,10004,10004,14,10013,10013,14,10024,10024,14,10052,10052,14,10060,10060,14,10067,10069,14,10083,10084,14,10133,10135,14,10160,10160,14,10548,10549,14,11035,11036,14,11093,11093,14,11647,11647,5,12330,12333,5,12336,12336,14,12441,12442,5,12953,12953,14,42608,42610,5,42654,42655,5,43010,43010,5,43019,43019,5,43045,43046,5,43052,43052,5,43188,43203,7,43232,43249,5,43302,43309,5,43346,43347,7,43392,43394,5,43443,43443,5,43446,43449,5,43452,43453,5,43493,43493,5,43567,43568,7,43571,43572,7,43587,43587,5,43597,43597,7,43696,43696,5,43703,43704,5,43713,43713,5,43756,43757,5,43765,43765,7,44003,44004,7,44006,44007,7,44009,44010,7,44013,44013,5,44033,44059,12,44061,44087,12,44089,44115,12,44117,44143,12,44145,44171,12,44173,44199,12,44201,44227,12,44229,44255,12,44257,44283,12,44285,44311,12,44313,44339,12,44341,44367,12,44369,44395,12,44397,44423,12,44425,44451,12,44453,44479,12,44481,44507,12,44509,44535,12,44537,44563,12,44565,44591,12,44593,44619,12,44621,44647,12,44649,44675,12,44677,44703,12,44705,44731,12,44733,44759,12,44761,44787,12,44789,44815,12,44817,44843,12,44845,44871,12,44873,44899,12,44901,44927,12,44929,44955,12,44957,44983,12,44985,45011,12,45013,45039,12,45041,45067,12,45069,45095,12,45097,45123,12,45125,45151,12,45153,45179,12,45181,45207,12,45209,45235,12,45237,45263,12,45265,45291,12,45293,45319,12,45321,45347,12,45349,45375,12,45377,45403,12,45405,45431,12,45433,45459,12,45461,45487,12,45489,45515,12,45517,45543,12,45545,45571,12,45573,45599,12,45601,45627,12,45629,45655,12,45657,45683,12,45685,45711,12,45713,45739,12,45741,45767,12,45769,45795,12,45797,45823,12,45825,45851,12,45853,45879,12,45881,45907,12,45909,45935,12,45937,45963,12,45965,45991,12,45993,46019,12,46021,46047,12,46049,46075,12,46077,46103,12,46105,46131,12,46133,46159,12,46161,46187,12,46189,46215,12,46217,46243,12,46245,46271,12,46273,46299,12,46301,46327,12,46329,46355,12,46357,46383,12,46385,46411,12,46413,46439,12,46441,46467,12,46469,46495,12,46497,46523,12,46525,46551,12,46553,46579,12,46581,46607,12,46609,46635,12,46637,46663,12,46665,46691,12,46693,46719,12,46721,46747,12,46749,46775,12,46777,46803,12,46805,46831,12,46833,46859,12,46861,46887,12,46889,46915,12,46917,46943,12,46945,46971,12,46973,46999,12,47001,47027,12,47029,47055,12,47057,47083,12,47085,47111,12,47113,47139,12,47141,47167,12,47169,47195,12,47197,47223,12,47225,47251,12,47253,47279,12,47281,47307,12,47309,47335,12,47337,47363,12,47365,47391,12,47393,47419,12,47421,47447,12,47449,47475,12,47477,47503,12,47505,47531,12,47533,47559,12,47561,47587,12,47589,47615,12,47617,47643,12,47645,47671,12,47673,47699,12,47701,47727,12,47729,47755,12,47757,47783,12,47785,47811,12,47813,47839,12,47841,47867,12,47869,47895,12,47897,47923,12,47925,47951,12,47953,47979,12,47981,48007,12,48009,48035,12,48037,48063,12,48065,48091,12,48093,48119,12,48121,48147,12,48149,48175,12,48177,48203,12,48205,48231,12,48233,48259,12,48261,48287,12,48289,48315,12,48317,48343,12,48345,48371,12,48373,48399,12,48401,48427,12,48429,48455,12,48457,48483,12,48485,48511,12,48513,48539,12,48541,48567,12,48569,48595,12,48597,48623,12,48625,48651,12,48653,48679,12,48681,48707,12,48709,48735,12,48737,48763,12,48765,48791,12,48793,48819,12,48821,48847,12,48849,48875,12,48877,48903,12,48905,48931,12,48933,48959,12,48961,48987,12,48989,49015,12,49017,49043,12,49045,49071,12,49073,49099,12,49101,49127,12,49129,49155,12,49157,49183,12,49185,49211,12,49213,49239,12,49241,49267,12,49269,49295,12,49297,49323,12,49325,49351,12,49353,49379,12,49381,49407,12,49409,49435,12,49437,49463,12,49465,49491,12,49493,49519,12,49521,49547,12,49549,49575,12,49577,49603,12,49605,49631,12,49633,49659,12,49661,49687,12,49689,49715,12,49717,49743,12,49745,49771,12,49773,49799,12,49801,49827,12,49829,49855,12,49857,49883,12,49885,49911,12,49913,49939,12,49941,49967,12,49969,49995,12,49997,50023,12,50025,50051,12,50053,50079,12,50081,50107,12,50109,50135,12,50137,50163,12,50165,50191,12,50193,50219,12,50221,50247,12,50249,50275,12,50277,50303,12,50305,50331,12,50333,50359,12,50361,50387,12,50389,50415,12,50417,50443,12,50445,50471,12,50473,50499,12,50501,50527,12,50529,50555,12,50557,50583,12,50585,50611,12,50613,50639,12,50641,50667,12,50669,50695,12,50697,50723,12,50725,50751,12,50753,50779,12,50781,50807,12,50809,50835,12,50837,50863,12,50865,50891,12,50893,50919,12,50921,50947,12,50949,50975,12,50977,51003,12,51005,51031,12,51033,51059,12,51061,51087,12,51089,51115,12,51117,51143,12,51145,51171,12,51173,51199,12,51201,51227,12,51229,51255,12,51257,51283,12,51285,51311,12,51313,51339,12,51341,51367,12,51369,51395,12,51397,51423,12,51425,51451,12,51453,51479,12,51481,51507,12,51509,51535,12,51537,51563,12,51565,51591,12,51593,51619,12,51621,51647,12,51649,51675,12,51677,51703,12,51705,51731,12,51733,51759,12,51761,51787,12,51789,51815,12,51817,51843,12,51845,51871,12,51873,51899,12,51901,51927,12,51929,51955,12,51957,51983,12,51985,52011,12,52013,52039,12,52041,52067,12,52069,52095,12,52097,52123,12,52125,52151,12,52153,52179,12,52181,52207,12,52209,52235,12,52237,52263,12,52265,52291,12,52293,52319,12,52321,52347,12,52349,52375,12,52377,52403,12,52405,52431,12,52433,52459,12,52461,52487,12,52489,52515,12,52517,52543,12,52545,52571,12,52573,52599,12,52601,52627,12,52629,52655,12,52657,52683,12,52685,52711,12,52713,52739,12,52741,52767,12,52769,52795,12,52797,52823,12,52825,52851,12,52853,52879,12,52881,52907,12,52909,52935,12,52937,52963,12,52965,52991,12,52993,53019,12,53021,53047,12,53049,53075,12,53077,53103,12,53105,53131,12,53133,53159,12,53161,53187,12,53189,53215,12,53217,53243,12,53245,53271,12,53273,53299,12,53301,53327,12,53329,53355,12,53357,53383,12,53385,53411,12,53413,53439,12,53441,53467,12,53469,53495,12,53497,53523,12,53525,53551,12,53553,53579,12,53581,53607,12,53609,53635,12,53637,53663,12,53665,53691,12,53693,53719,12,53721,53747,12,53749,53775,12,53777,53803,12,53805,53831,12,53833,53859,12,53861,53887,12,53889,53915,12,53917,53943,12,53945,53971,12,53973,53999,12,54001,54027,12,54029,54055,12,54057,54083,12,54085,54111,12,54113,54139,12,54141,54167,12,54169,54195,12,54197,54223,12,54225,54251,12,54253,54279,12,54281,54307,12,54309,54335,12,54337,54363,12,54365,54391,12,54393,54419,12,54421,54447,12,54449,54475,12,54477,54503,12,54505,54531,12,54533,54559,12,54561,54587,12,54589,54615,12,54617,54643,12,54645,54671,12,54673,54699,12,54701,54727,12,54729,54755,12,54757,54783,12,54785,54811,12,54813,54839,12,54841,54867,12,54869,54895,12,54897,54923,12,54925,54951,12,54953,54979,12,54981,55007,12,55009,55035,12,55037,55063,12,55065,55091,12,55093,55119,12,55121,55147,12,55149,55175,12,55177,55203,12,55243,55291,10,65024,65039,5,65279,65279,4,65520,65528,4,66045,66045,5,66422,66426,5,68101,68102,5,68152,68154,5,68325,68326,5,69291,69292,5,69632,69632,7,69634,69634,7,69759,69761,5]")}}),V(z[22],G([0,1,5]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.StringSHA1=t.toHexString=t.stringHash=t.doHash=t.hash=void 0;function P(n){return E(n,0)}t.hash=P;function E(n,r){switch(typeof n){case"object":return n===null?u(349,r):Array.isArray(n)?s(n,r):h(n,r);case"string":return l(n,r);case"boolean":return d(n,r);case"number":return u(n,r);case"undefined":return u(937,r);default:return u(617,r)}}t.doHash=E;function u(n,r){return(r<<5)-r+n|0}function d(n,r){return u(n?433:863,r)}function l(n,r){r=u(149417,r);for(let m=0,_=n.length;m<_;m++)r=u(n.charCodeAt(m),r);return r}t.stringHash=l;function s(n,r){return r=u(104579,r),n.reduce((m,_)=>E(_,m),r)}function h(n,r){return r=u(181387,r),Object.keys(n).sort().reduce((m,_)=>(m=l(_,m),E(n[_],m)),r)}function w(n,r,m=32){const _=m-r,o=~((1<<_)-1);return(n<>>_)>>>0}function a(n,r=0,m=n.byteLength,_=0){for(let o=0;om.toString(16).padStart(2,"0")).join(""):S((n>>>0).toString(16),r/4)}t.toHexString=v;class C{constructor(){this._h0=1732584193,this._h1=4023233417,this._h2=2562383102,this._h3=271733878,this._h4=3285377520,this._buff=new Uint8Array(64+3),this._buffDV=new DataView(this._buff.buffer),this._buffLen=0,this._totalLen=0,this._leftoverHighSurrogate=0,this._finished=!1}update(r){const m=r.length;if(m!==0){const _=this._buff;let o=this._buffLen,i=this._leftoverHighSurrogate,f,b;for(i!==0?(f=i,b=-1,i=0):(f=r.charCodeAt(0),b=0);;){let N=f;if(p.isHighSurrogate(f))if(b+1>>6,r[m++]=128|(_&63)>>>0):_<65536?(r[m++]=224|(_&61440)>>>12,r[m++]=128|(_&4032)>>>6,r[m++]=128|(_&63)>>>0):(r[m++]=240|(_&1835008)>>>18,r[m++]=128|(_&258048)>>>12,r[m++]=128|(_&4032)>>>6,r[m++]=128|(_&63)>>>0),m>=64&&(this._step(),m-=64,this._totalLen+=64,r[0]=r[64+0],r[1]=r[64+1],r[2]=r[64+2]),m}digest(){return this._finished||(this._finished=!0,this._leftoverHighSurrogate&&(this._leftoverHighSurrogate=0,this._buffLen=this._push(this._buff,this._buffLen,65533)),this._totalLen+=this._buffLen,this._wrapUp()),v(this._h0)+v(this._h1)+v(this._h2)+v(this._h3)+v(this._h4)}_wrapUp(){this._buff[this._buffLen++]=128,a(this._buff,this._buffLen),this._buffLen>56&&(this._step(),a(this._buff));const r=8*this._totalLen;this._buffDV.setUint32(56,Math.floor(r/4294967296),!1),this._buffDV.setUint32(60,r%4294967296,!1),this._step()}_step(){const r=C._bigBlock32,m=this._buffDV;for(let L=0;L<64;L+=4)r.setUint32(L,m.getUint32(L,!1),!1);for(let L=64;L<320;L+=4)r.setUint32(L,w(r.getUint32(L-12,!1)^r.getUint32(L-32,!1)^r.getUint32(L-56,!1)^r.getUint32(L-64,!1),1),!1);let _=this._h0,o=this._h1,i=this._h2,f=this._h3,b=this._h4,N,c,g;for(let L=0;L<80;L++)L<20?(N=o&i|~o&f,c=1518500249):L<40?(N=o^i^f,c=1859775393):L<60?(N=o&i|o&f|i&f,c=2400959708):(N=o^i^f,c=3395469782),g=w(_,5)+N+b+c+r.getUint32(L*4,!1)&4294967295,b=f,f=i,i=w(o,30),o=_,_=g;this._h0=this._h0+_&4294967295,this._h1=this._h1+o&4294967295,this._h2=this._h2+i&4294967295,this._h3=this._h3+f&4294967295,this._h4=this._h4+b&4294967295}}t.StringSHA1=C,C._bigBlock32=new DataView(new ArrayBuffer(320))}),V(z[10],G([0,1,15,22]),function(I,t,p,P){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.LcsDiff=t.MyArray=t.Debug=t.stringDiff=t.StringDiffSequence=void 0;class E{constructor(a){this.source=a}getElements(){const a=this.source,S=new Int32Array(a.length);for(let v=0,C=a.length;v0||this.m_modifiedCount>0)&&this.m_changes.push(new p.DiffChange(this.m_originalStart,this.m_originalCount,this.m_modifiedStart,this.m_modifiedCount)),this.m_originalCount=0,this.m_modifiedCount=0,this.m_originalStart=1073741824,this.m_modifiedStart=1073741824}AddOriginalElement(a,S){this.m_originalStart=Math.min(this.m_originalStart,a),this.m_modifiedStart=Math.min(this.m_modifiedStart,S),this.m_originalCount++}AddModifiedElement(a,S){this.m_originalStart=Math.min(this.m_originalStart,a),this.m_modifiedStart=Math.min(this.m_modifiedStart,S),this.m_modifiedCount++}getChanges(){return(this.m_originalCount>0||this.m_modifiedCount>0)&&this.MarkNextChange(),this.m_changes}getReverseChanges(){return(this.m_originalCount>0||this.m_modifiedCount>0)&&this.MarkNextChange(),this.m_changes.reverse(),this.m_changes}}class h{constructor(a,S,v=null){this.ContinueProcessingPredicate=v;const[C,n,r]=h._getElements(a),[m,_,o]=h._getElements(S);this._hasStrings=r&&o,this._originalStringElements=C,this._originalElementsOrHash=n,this._modifiedStringElements=m,this._modifiedElementsOrHash=_,this.m_forwardHistory=[],this.m_reverseHistory=[]}static _isStringArray(a){return a.length>0&&typeof a[0]=="string"}static _getElements(a){const S=a.getElements();if(h._isStringArray(S)){const v=new Int32Array(S.length);for(let C=0,n=S.length;C=a&&C>=v&&this.ElementsAreEqual(S,C);)S--,C--;if(a>S||v>C){let f;return v<=C?(d.Assert(a===S+1,"originalStart should only be one more than originalEnd"),f=[new p.DiffChange(a,0,v,C-v+1)]):a<=S?(d.Assert(v===C+1,"modifiedStart should only be one more than modifiedEnd"),f=[new p.DiffChange(a,S-a+1,v,0)]):(d.Assert(a===S+1,"originalStart should only be one more than originalEnd"),d.Assert(v===C+1,"modifiedStart should only be one more than modifiedEnd"),f=[]),f}const r=[0],m=[0],_=this.ComputeRecursionPoint(a,S,v,C,r,m,n),o=r[0],i=m[0];if(_!==null)return _;if(!n[0]){const f=this.ComputeDiffRecursive(a,o,v,i,n);let b=[];return n[0]?b=[new p.DiffChange(o+1,S-(o+1)+1,i+1,C-(i+1)+1)]:b=this.ComputeDiffRecursive(o+1,S,i+1,C,n),this.ConcatenateChanges(f,b)}return[new p.DiffChange(a,S-a+1,v,C-v+1)]}WALKTRACE(a,S,v,C,n,r,m,_,o,i,f,b,N,c,g,L,A,M){let R=null,D=null,T=new s,$=S,k=v,F=N[0]-L[0]-C,O=-1073741824,e=this.m_forwardHistory.length-1;do{const H=F+a;H===$||H=0&&(o=this.m_forwardHistory[e],a=o[0],$=1,k=o.length-1)}while(--e>=-1);if(R=T.getReverseChanges(),M[0]){let H=N[0]+1,B=L[0]+1;if(R!==null&&R.length>0){const Q=R[R.length-1];H=Math.max(H,Q.getOriginalEnd()),B=Math.max(B,Q.getModifiedEnd())}D=[new p.DiffChange(H,b-H+1,B,g-B+1)]}else{T=new s,$=r,k=m,F=N[0]-L[0]-_,O=1073741824,e=A?this.m_reverseHistory.length-1:this.m_reverseHistory.length-2;do{const H=F+n;H===$||H=i[H+1]?(f=i[H+1]-1,c=f-F-_,f>O&&T.MarkNextChange(),O=f+1,T.AddOriginalElement(f+1,c+1),F=H+1-n):(f=i[H-1],c=f-F-_,f>O&&T.MarkNextChange(),O=f,T.AddModifiedElement(f+1,c+1),F=H-1-n),e>=0&&(i=this.m_reverseHistory[e],n=i[0],$=1,k=i.length-1)}while(--e>=-1);D=T.getChanges()}return this.ConcatenateChanges(R,D)}ComputeRecursionPoint(a,S,v,C,n,r,m){let _=0,o=0,i=0,f=0,b=0,N=0;a--,v--,n[0]=0,r[0]=0,this.m_forwardHistory=[],this.m_reverseHistory=[];const c=S-a+(C-v),g=c+1,L=new Int32Array(g),A=new Int32Array(g),M=C-v,R=S-a,D=a-v,T=S-C,k=(R-M)%2==0;L[M]=a,A[R]=S,m[0]=!1;for(let F=1;F<=c/2+1;F++){let O=0,e=0;i=this.ClipDiagonalBound(M-F,F,M,g),f=this.ClipDiagonalBound(M+F,F,M,g);for(let B=i;B<=f;B+=2){B===i||BO+e&&(O=_,e=o),!k&&Math.abs(B-R)<=F-1&&_>=A[B])return n[0]=_,r[0]=o,Q<=A[B]&&1447>0&&F<=1447+1?this.WALKTRACE(M,i,f,D,R,b,N,T,L,A,_,S,n,o,C,r,k,m):null}const H=(O-a+(e-v)-F)/2;if(this.ContinueProcessingPredicate!==null&&!this.ContinueProcessingPredicate(O,H))return m[0]=!0,n[0]=O,r[0]=e,H>0&&1447>0&&F<=1447+1?this.WALKTRACE(M,i,f,D,R,b,N,T,L,A,_,S,n,o,C,r,k,m):(a++,v++,[new p.DiffChange(a,S-a+1,v,C-v+1)]);b=this.ClipDiagonalBound(R-F,F,R,g),N=this.ClipDiagonalBound(R+F,F,R,g);for(let B=b;B<=N;B+=2){B===b||B=A[B+1]?_=A[B+1]-1:_=A[B-1],o=_-(B-R)-T;const Q=_;for(;_>a&&o>v&&this.ElementsAreEqual(_,o);)_--,o--;if(A[B]=_,k&&Math.abs(B-M)<=F&&_<=L[B])return n[0]=_,r[0]=o,Q>=L[B]&&1447>0&&F<=1447+1?this.WALKTRACE(M,i,f,D,R,b,N,T,L,A,_,S,n,o,C,r,k,m):null}if(F<=1447){let B=new Int32Array(f-i+2);B[0]=M-i+1,l.Copy2(L,i,B,1,f-i+1),this.m_forwardHistory.push(B),B=new Int32Array(N-b+2),B[0]=R-b+1,l.Copy2(A,b,B,1,N-b+1),this.m_reverseHistory.push(B)}}return this.WALKTRACE(M,i,f,D,R,b,N,T,L,A,_,S,n,o,C,r,k,m)}PrettifyChanges(a){for(let S=0;S0,m=v.modifiedLength>0;for(;v.originalStart+v.originalLength=0;S--){const v=a[S];let C=0,n=0;if(S>0){const i=a[S-1];i.originalLength>0&&(C=i.originalStart+i.originalLength),i.modifiedLength>0&&(n=i.modifiedStart+i.modifiedLength)}const r=v.originalLength>0,m=v.modifiedLength>0;let _=0,o=this._boundaryScore(v.originalStart,v.originalLength,v.modifiedStart,v.modifiedLength);for(let i=1;;i++){const f=v.originalStart-i,b=v.modifiedStart-i;if(fo&&(o=N,_=i)}v.originalStart-=_,v.modifiedStart-=_}if(this._hasStrings)for(let S=1,v=a.length;S0&&N>_&&(_=N,o=f,i=b)}return _>0?[o,i]:null}_contiguousSequenceScore(a,S,v){let C=0;for(let n=0;n=this._originalElementsOrHash.length-1?!0:this._hasStrings&&/^\s*$/.test(this._originalStringElements[a])}_OriginalRegionIsBoundary(a,S){if(this._OriginalIsBoundary(a)||this._OriginalIsBoundary(a-1))return!0;if(S>0){const v=a+S;if(this._OriginalIsBoundary(v-1)||this._OriginalIsBoundary(v))return!0}return!1}_ModifiedIsBoundary(a){return a<=0||a>=this._modifiedElementsOrHash.length-1?!0:this._hasStrings&&/^\s*$/.test(this._modifiedStringElements[a])}_ModifiedRegionIsBoundary(a,S){if(this._ModifiedIsBoundary(a)||this._ModifiedIsBoundary(a-1))return!0;if(S>0){const v=a+S;if(this._ModifiedIsBoundary(v-1)||this._ModifiedIsBoundary(v))return!0}return!1}_boundaryScore(a,S,v,C){const n=this._OriginalRegionIsBoundary(a,S)?1:0,r=this._ModifiedRegionIsBoundary(v,C)?1:0;return n+r}ConcatenateChanges(a,S){let v=[];if(a.length===0||S.length===0)return S.length>0?S:a;if(this.ChangesOverlap(a[a.length-1],S[0],v)){const C=new Array(a.length+S.length-1);return l.Copy(a,0,C,0,a.length-1),C[a.length-1]=v[0],l.Copy(S,1,C,a.length,S.length-1),C}else{const C=new Array(a.length+S.length);return l.Copy(a,0,C,0,a.length),l.Copy(S,0,C,a.length,S.length),C}}ChangesOverlap(a,S,v){if(d.Assert(a.originalStart<=S.originalStart,"Left change is not less than or equal to right change"),d.Assert(a.modifiedStart<=S.modifiedStart,"Left change is not less than or equal to right change"),a.originalStart+a.originalLength>=S.originalStart||a.modifiedStart+a.modifiedLength>=S.modifiedStart){const C=a.originalStart;let n=a.originalLength;const r=a.modifiedStart;let m=a.modifiedLength;return a.originalStart+a.originalLength>=S.originalStart&&(n=S.originalStart+S.originalLength-a.originalStart),a.modifiedStart+a.modifiedLength>=S.modifiedStart&&(m=S.modifiedStart+S.modifiedLength-a.modifiedStart),v[0]=new p.DiffChange(C,n,r,m),!0}else return v[0]=null,!1}ClipDiagonalBound(a,S,v,C){if(a>=0&&afunction(){const N=Array.prototype.slice.call(arguments,0);return o(b,N)};let f={};for(const b of _)f[b]=i(b);return f}t.createProxyObject=r;function m(_){return _===null?void 0:_}t.withNullAsUndefined=m}),V(z[12],G([0,1]),function(I,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.toUint32=t.toUint8=void 0;function p(E){return E<0?0:E>255?255:E|0}t.toUint8=p;function P(E){return E<0?0:E>4294967295?4294967295:E|0}t.toUint32=P}),V(z[13],G([0,1,2,20]),function(I,t,p,P){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.uriToFsPath=t.URI=void 0;const E=/^\w[\w\d+.-]*$/,u=/^\//,d=/^\/\//;function l(c,g){if(!c.scheme&&g)throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${c.authority}", path: "${c.path}", query: "${c.query}", fragment: "${c.fragment}"}`);if(c.scheme&&!E.test(c.scheme))throw new Error("[UriError]: Scheme contains illegal characters.");if(c.path){if(c.authority){if(!u.test(c.path))throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character')}else if(d.test(c.path))throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")')}}function s(c,g){return!c&&!g?"file":c}function h(c,g){switch(c){case"https":case"http":case"file":g?g[0]!==a&&(g=a+g):g=a;break}return g}const w="",a="/",S=/^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;class v{constructor(g,L,A,M,R,D=!1){typeof g=="object"?(this.scheme=g.scheme||w,this.authority=g.authority||w,this.path=g.path||w,this.query=g.query||w,this.fragment=g.fragment||w):(this.scheme=s(g,D),this.authority=L||w,this.path=h(this.scheme,A||w),this.query=M||w,this.fragment=R||w,l(this,D))}static isUri(g){return g instanceof v?!0:g?typeof g.authority=="string"&&typeof g.fragment=="string"&&typeof g.path=="string"&&typeof g.query=="string"&&typeof g.scheme=="string"&&typeof g.fsPath=="string"&&typeof g.with=="function"&&typeof g.toString=="function":!1}get fsPath(){return o(this,!1)}with(g){if(!g)return this;let{scheme:L,authority:A,path:M,query:R,fragment:D}=g;return L===void 0?L=this.scheme:L===null&&(L=w),A===void 0?A=this.authority:A===null&&(A=w),M===void 0?M=this.path:M===null&&(M=w),R===void 0?R=this.query:R===null&&(R=w),D===void 0?D=this.fragment:D===null&&(D=w),L===this.scheme&&A===this.authority&&M===this.path&&R===this.query&&D===this.fragment?this:new n(L,A,M,R,D)}static parse(g,L=!1){const A=S.exec(g);return A?new n(A[2]||w,N(A[4]||w),N(A[5]||w),N(A[7]||w),N(A[9]||w),L):new n(w,w,w,w,w)}static file(g){let L=w;if(p.isWindows&&(g=g.replace(/\\/g,a)),g[0]===a&&g[1]===a){const A=g.indexOf(a,2);A===-1?(L=g.substring(2),g=a):(L=g.substring(2,A),g=g.substring(A)||a)}return new n("file",L,g,w,w)}static from(g){return new n(g.scheme,g.authority,g.path,g.query,g.fragment)}static joinPath(g,...L){if(!g.path)throw new Error("[UriError]: cannot call joinPath on URI without path");let A;return p.isWindows&&g.scheme==="file"?A=v.file(P.win32.join(o(g,!0),...L)).path:A=P.posix.join(g.path,...L),g.with({path:A})}toString(g=!1){return i(this,g)}toJSON(){return this}static revive(g){if(g){if(g instanceof v)return g;{const L=new n(g);return L._formatted=g.external,L._fsPath=g._sep===C?g.fsPath:null,L}}else return g}}t.URI=v;const C=p.isWindows?1:void 0;class n extends v{constructor(){super(...arguments);this._formatted=null,this._fsPath=null}get fsPath(){return this._fsPath||(this._fsPath=o(this,!1)),this._fsPath}toString(g=!1){return g?i(this,!0):(this._formatted||(this._formatted=i(this,!1)),this._formatted)}toJSON(){const g={$mid:1};return this._fsPath&&(g.fsPath=this._fsPath,g._sep=C),this._formatted&&(g.external=this._formatted),this.path&&(g.path=this.path),this.scheme&&(g.scheme=this.scheme),this.authority&&(g.authority=this.authority),this.query&&(g.query=this.query),this.fragment&&(g.fragment=this.fragment),g}}const r={[58]:"%3A",[47]:"%2F",[63]:"%3F",[35]:"%23",[91]:"%5B",[93]:"%5D",[64]:"%40",[33]:"%21",[36]:"%24",[38]:"%26",[39]:"%27",[40]:"%28",[41]:"%29",[42]:"%2A",[43]:"%2B",[44]:"%2C",[59]:"%3B",[61]:"%3D",[32]:"%20"};function m(c,g){let L,A=-1;for(let M=0;M=97&&R<=122||R>=65&&R<=90||R>=48&&R<=57||R===45||R===46||R===95||R===126||g&&R===47)A!==-1&&(L+=encodeURIComponent(c.substring(A,M)),A=-1),L!==void 0&&(L+=c.charAt(M));else{L===void 0&&(L=c.substr(0,M));const D=r[R];D!==void 0?(A!==-1&&(L+=encodeURIComponent(c.substring(A,M)),A=-1),L+=D):A===-1&&(A=M)}}return A!==-1&&(L+=encodeURIComponent(c.substring(A))),L!==void 0?L:c}function _(c){let g;for(let L=0;L1&&c.scheme==="file"?L=`//${c.authority}${c.path}`:c.path.charCodeAt(0)===47&&(c.path.charCodeAt(1)>=65&&c.path.charCodeAt(1)<=90||c.path.charCodeAt(1)>=97&&c.path.charCodeAt(1)<=122)&&c.path.charCodeAt(2)===58?g?L=c.path.substr(1):L=c.path[1].toLowerCase()+c.path.substr(2):L=c.path,p.isWindows&&(L=L.replace(/\//g,"\\")),L}t.uriToFsPath=o;function i(c,g){const L=g?_:m;let A="",{scheme:M,authority:R,path:D,query:T,fragment:$}=c;if(M&&(A+=M,A+=":"),(R||M==="file")&&(A+=a,A+=a),R){let k=R.indexOf("@");if(k!==-1){const F=R.substr(0,k);R=R.substr(k+1),k=F.indexOf(":"),k===-1?A+=L(F,!1):(A+=L(F.substr(0,k),!1),A+=":",A+=L(F.substr(k+1),!1)),A+="@"}R=R.toLowerCase(),k=R.indexOf(":"),k===-1?A+=L(R,!1):(A+=L(R.substr(0,k),!1),A+=R.substr(k))}if(D){if(D.length>=3&&D.charCodeAt(0)===47&&D.charCodeAt(2)===58){const k=D.charCodeAt(1);k>=65&&k<=90&&(D=`/${String.fromCharCode(k+32)}:${D.substr(3)}`)}else if(D.length>=2&&D.charCodeAt(1)===58){const k=D.charCodeAt(0);k>=65&&k<=90&&(D=`${String.fromCharCode(k+32)}:${D.substr(2)}`)}A+=L(D,!0)}return T&&(A+="?",A+=L(T,!1)),$&&(A+="#",A+=g?$:m($,!1)),A}function f(c){try{return decodeURIComponent(c)}catch(g){return c.length>3?c.substr(0,3)+f(c.substr(3)):c}}const b=/(%[0-9A-Za-z][0-9A-Za-z])+/g;function N(c){return c.match(b)?c.replace(b,g=>f(g)):c}}),V(z[34],G([0,1,4,7,2,11]),function(I,t,p,P,E,u){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.create=t.SimpleWorkerServer=t.SimpleWorkerClient=t.logOnceWebWorkerWarning=void 0;const d="$initialize";let l=!1;function s(v){!E.isWeb||(l||(l=!0,console.warn("Could not create web worker(s). Falling back to loading web worker code in main thread, which might cause UI freezes. Please see https://github.com/microsoft/monaco-editor#faq")),console.warn(v.message))}t.logOnceWebWorkerWarning=s;class h{constructor(C){this._workerId=-1,this._handler=C,this._lastSentReq=0,this._pendingReplies=Object.create(null)}setWorkerId(C){this._workerId=C}sendMessage(C,n){let r=String(++this._lastSentReq);return new Promise((m,_)=>{this._pendingReplies[r]={resolve:m,reject:_},this._send({vsWorker:this._workerId,req:r,method:C,args:n})})}handleMessage(C){!C||!C.vsWorker||this._workerId!==-1&&C.vsWorker!==this._workerId||this._handleMessage(C)}_handleMessage(C){if(C.seq){let _=C;if(!this._pendingReplies[_.seq]){console.warn("Got reply to unknown seq");return}let o=this._pendingReplies[_.seq];if(delete this._pendingReplies[_.seq],_.err){let i=_.err;_.err.$isError&&(i=new Error,i.name=_.err.name,i.message=_.err.message,i.stack=_.err.stack),o.reject(i);return}o.resolve(_.res);return}let n=C,r=n.req;this._handler.handleMessage(n.method,n.args).then(_=>{this._send({vsWorker:this._workerId,seq:r,res:_,err:void 0})},_=>{_.detail instanceof Error&&(_.detail=p.transformErrorForSerialization(_.detail)),this._send({vsWorker:this._workerId,seq:r,res:void 0,err:p.transformErrorForSerialization(_)})})}_send(C){let n=[];if(C.req){const r=C;for(let m=0;m{this._protocol.handleMessage(f)},f=>{m&&m(f)})),this._protocol=new h({sendMessage:(f,b)=>{this._worker.postMessage(f,b)},handleMessage:(f,b)=>{if(typeof r[f]!="function")return Promise.reject(new Error("Missing method "+f+" on main thread host."));try{return Promise.resolve(r[f].apply(r,b))}catch(N){return Promise.reject(N)}}}),this._protocol.setWorkerId(this._worker.getId());let _=null;typeof self.require!="undefined"&&typeof self.require.getConfig=="function"?_=self.require.getConfig():typeof self.requirejs!="undefined"&&(_=self.requirejs.s.contexts._.config);const o=u.getAllMethodNames(r);this._onModuleLoaded=this._protocol.sendMessage(d,[this._worker.getId(),JSON.parse(JSON.stringify(_)),n,o]);const i=(f,b)=>this._request(f,b);this._lazyProxy=new Promise((f,b)=>{m=b,this._onModuleLoaded.then(N=>{f(u.createProxyObject(N,i))},N=>{b(N),this._onError("Worker failed to load "+n,N)})})}getProxyObject(){return this._lazyProxy}_request(C,n){return new Promise((r,m)=>{this._onModuleLoaded.then(()=>{this._protocol.sendMessage(C,n).then(r,m)},m)})}_onError(C,n){console.error(C),console.info(n)}}t.SimpleWorkerClient=w;class a{constructor(C,n){this._requestHandlerFactory=n,this._requestHandler=null,this._protocol=new h({sendMessage:(r,m)=>{C(r,m)},handleMessage:(r,m)=>this._handleMessage(r,m)})}onmessage(C){this._protocol.handleMessage(C)}_handleMessage(C,n){if(C===d)return this.initialize(n[0],n[1],n[2],n[3]);if(!this._requestHandler||typeof this._requestHandler[C]!="function")return Promise.reject(new Error("Missing requestHandler or method: "+C));try{return Promise.resolve(this._requestHandler[C].apply(this._requestHandler,n))}catch(r){return Promise.reject(r)}}initialize(C,n,r,m){this._protocol.setWorkerId(C);const _=(i,f)=>this._protocol.sendMessage(i,f),o=u.createProxyObject(m,_);return this._requestHandlerFactory?(this._requestHandler=this._requestHandlerFactory(o),Promise.resolve(u.getAllMethodNames(this._requestHandler))):(n&&(typeof n.baseUrl!="undefined"&&delete n.baseUrl,typeof n.paths!="undefined"&&typeof n.paths.vs!="undefined"&&delete n.paths.vs,typeof n.trustedTypesPolicy!==void 0&&delete n.trustedTypesPolicy,n.catchError=!0,self.require.config(n)),new Promise((i,f)=>{self.require([r],b=>{if(this._requestHandler=b.create(o),!this._requestHandler){f(new Error("No RequestHandler!"));return}i(u.getAllMethodNames(this._requestHandler))},f)}))}}t.SimpleWorkerServer=a;function S(v){return new a(v,null)}t.create=S}),V(z[23],G([0,1,12]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CharacterSet=t.CharacterClassifier=void 0;class P{constructor(d){let l=p.toUint8(d);this._defaultValue=l,this._asciiMap=P._createAsciiMap(l),this._map=new Map}static _createAsciiMap(d){let l=new Uint8Array(256);for(let s=0;s<256;s++)l[s]=d;return l}set(d,l){let s=p.toUint8(l);d>=0&&d<256?this._asciiMap[d]=s:this._map.set(d,s)}get(d){return d>=0&&d<256?this._asciiMap[d]:this._map.get(d)||this._defaultValue}}t.CharacterClassifier=P;class E{constructor(){this._actual=new P(0)}add(d){this._actual.set(d,1)}has(d){return this._actual.get(d)===1}}t.CharacterSet=E}),V(z[3],G([0,1]),function(I,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Position=void 0;class p{constructor(E,u){this.lineNumber=E,this.column=u}with(E=this.lineNumber,u=this.column){return E===this.lineNumber&&u===this.column?this:new p(E,u)}delta(E=0,u=0){return this.with(this.lineNumber+E,this.column+u)}equals(E){return p.equals(this,E)}static equals(E,u){return!E&&!u?!0:!!E&&!!u&&E.lineNumber===u.lineNumber&&E.column===u.column}isBefore(E){return p.isBefore(this,E)}static isBefore(E,u){return E.lineNumberl||u===l&&d>s?(this.startLineNumber=l,this.startColumn=s,this.endLineNumber=u,this.endColumn=d):(this.startLineNumber=u,this.startColumn=d,this.endLineNumber=l,this.endColumn=s)}isEmpty(){return P.isEmpty(this)}static isEmpty(u){return u.startLineNumber===u.endLineNumber&&u.startColumn===u.endColumn}containsPosition(u){return P.containsPosition(this,u)}static containsPosition(u,d){return!(d.lineNumberu.endLineNumber||d.lineNumber===u.startLineNumber&&d.columnu.endColumn)}containsRange(u){return P.containsRange(this,u)}static containsRange(u,d){return!(d.startLineNumberu.endLineNumber||d.endLineNumber>u.endLineNumber||d.startLineNumber===u.startLineNumber&&d.startColumnu.endColumn)}strictContainsRange(u){return P.strictContainsRange(this,u)}static strictContainsRange(u,d){return!(d.startLineNumberu.endLineNumber||d.endLineNumber>u.endLineNumber||d.startLineNumber===u.startLineNumber&&d.startColumn<=u.startColumn||d.endLineNumber===u.endLineNumber&&d.endColumn>=u.endColumn)}plusRange(u){return P.plusRange(this,u)}static plusRange(u,d){let l,s,h,w;return d.startLineNumberu.endLineNumber?(h=d.endLineNumber,w=d.endColumn):d.endLineNumber===u.endLineNumber?(h=d.endLineNumber,w=Math.max(d.endColumn,u.endColumn)):(h=u.endLineNumber,w=u.endColumn),new P(l,s,h,w)}intersectRanges(u){return P.intersectRanges(this,u)}static intersectRanges(u,d){let l=u.startLineNumber,s=u.startColumn,h=u.endLineNumber,w=u.endColumn,a=d.startLineNumber,S=d.startColumn,v=d.endLineNumber,C=d.endColumn;return lv?(h=v,w=C):h===v&&(w=Math.min(w,C)),l>h||l===h&&s>w?null:new P(l,s,h,w)}equalsRange(u){return P.equalsRange(this,u)}static equalsRange(u,d){return!!u&&!!d&&u.startLineNumber===d.startLineNumber&&u.startColumn===d.startColumn&&u.endLineNumber===d.endLineNumber&&u.endColumn===d.endColumn}getEndPosition(){return P.getEndPosition(this)}static getEndPosition(u){return new p.Position(u.endLineNumber,u.endColumn)}getStartPosition(){return P.getStartPosition(this)}static getStartPosition(u){return new p.Position(u.startLineNumber,u.startColumn)}toString(){return"["+this.startLineNumber+","+this.startColumn+" -> "+this.endLineNumber+","+this.endColumn+"]"}setEndPosition(u,d){return new P(this.startLineNumber,this.startColumn,u,d)}setStartPosition(u,d){return new P(u,d,this.endLineNumber,this.endColumn)}collapseToStart(){return P.collapseToStart(this)}static collapseToStart(u){return new P(u.startLineNumber,u.startColumn,u.startLineNumber,u.startColumn)}static fromPositions(u,d=u){return new P(u.lineNumber,u.column,d.lineNumber,d.column)}static lift(u){return u?new P(u.startLineNumber,u.startColumn,u.endLineNumber,u.endColumn):null}static isIRange(u){return u&&typeof u.startLineNumber=="number"&&typeof u.startColumn=="number"&&typeof u.endLineNumber=="number"&&typeof u.endColumn=="number"}static areIntersectingOrTouching(u,d){return!(u.endLineNumberu.startLineNumber}}t.Range=P}),V(z[24],G([0,1,3,6]),function(I,t,p,P){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Selection=void 0;class E extends P.Range{constructor(d,l,s,h){super(d,l,s,h);this.selectionStartLineNumber=d,this.selectionStartColumn=l,this.positionLineNumber=s,this.positionColumn=h}toString(){return"["+this.selectionStartLineNumber+","+this.selectionStartColumn+" -> "+this.positionLineNumber+","+this.positionColumn+"]"}equalsSelection(d){return E.selectionsEqual(this,d)}static selectionsEqual(d,l){return d.selectionStartLineNumber===l.selectionStartLineNumber&&d.selectionStartColumn===l.selectionStartColumn&&d.positionLineNumber===l.positionLineNumber&&d.positionColumn===l.positionColumn}getDirection(){return this.selectionStartLineNumber===this.startLineNumber&&this.selectionStartColumn===this.startColumn?0:1}setEndPosition(d,l){return this.getDirection()===0?new E(this.startLineNumber,this.startColumn,d,l):new E(d,l,this.startLineNumber,this.startColumn)}getPosition(){return new p.Position(this.positionLineNumber,this.positionColumn)}setStartPosition(d,l){return this.getDirection()===0?new E(d,l,this.endLineNumber,this.endColumn):new E(this.endLineNumber,this.endColumn,d,l)}static fromPositions(d,l=d){return new E(d.lineNumber,d.column,l.lineNumber,l.column)}static liftSelection(d){return new E(d.selectionStartLineNumber,d.selectionStartColumn,d.positionLineNumber,d.positionColumn)}static selectionsArrEqual(d,l){if(d&&!l||!d&&l)return!1;if(!d&&!l)return!0;if(d.length!==l.length)return!1;for(let s=0,h=d.length;s0&&m.originalLength<20&&m.modifiedLength>0&&m.modifiedLength<20&&i()){const M=_.createCharSequence(r,m.originalStart,m.originalStart+m.originalLength-1),R=o.createCharSequence(r,m.modifiedStart,m.modifiedStart+m.modifiedLength-1);let D=u(M,R,i,!0).changes;b&&(D=h(D)),A=[];for(let T=0,$=D.length;T<$;T++)A.push(s.createFromDiffChange(D[T],M,R))}return new w(N,c,g,L,A)}}class a{constructor(r,m,_){this.shouldComputeCharChanges=_.shouldComputeCharChanges,this.shouldPostProcessCharChanges=_.shouldPostProcessCharChanges,this.shouldIgnoreTrimWhitespace=_.shouldIgnoreTrimWhitespace,this.shouldMakePrettyDiff=_.shouldMakePrettyDiff,this.originalLines=r,this.modifiedLines=m,this.original=new d(r),this.modified=new d(m),this.continueLineDiff=C(_.maxComputationTime),this.continueCharDiff=C(_.maxComputationTime===0?0:Math.min(_.maxComputationTime,5e3))}computeDiff(){if(this.original.lines.length===1&&this.original.lines[0].length===0)return this.modified.lines.length===1&&this.modified.lines[0].length===0?{quitEarly:!1,changes:[]}:{quitEarly:!1,changes:[{originalStartLineNumber:1,originalEndLineNumber:1,modifiedStartLineNumber:1,modifiedEndLineNumber:this.modified.lines.length,charChanges:[{modifiedEndColumn:0,modifiedEndLineNumber:0,modifiedStartColumn:0,modifiedStartLineNumber:0,originalEndColumn:0,originalEndLineNumber:0,originalStartColumn:0,originalStartLineNumber:0}]}]};if(this.modified.lines.length===1&&this.modified.lines[0].length===0)return{quitEarly:!1,changes:[{originalStartLineNumber:1,originalEndLineNumber:this.original.lines.length,modifiedStartLineNumber:1,modifiedEndLineNumber:1,charChanges:[{modifiedEndColumn:0,modifiedEndLineNumber:0,modifiedStartColumn:0,modifiedStartLineNumber:0,originalEndColumn:0,originalEndLineNumber:0,originalStartColumn:0,originalStartLineNumber:0}]}]};const r=u(this.original,this.modified,this.continueLineDiff,this.shouldMakePrettyDiff),m=r.changes,_=r.quitEarly;if(this.shouldIgnoreTrimWhitespace){const b=[];for(let N=0,c=m.length;N1&&D>1;){const T=A.charCodeAt(R-2),$=M.charCodeAt(D-2);if(T!==$)break;R--,D--}(R>1||D>1)&&this._pushTrimWhitespaceCharChange(o,i+1,1,R,f+1,1,D)}{let R=v(A,1),D=v(M,1);const T=A.length+1,$=M.length+1;for(;R!0;const r=Date.now();return()=>Date.now()-r/?";function p(l=""){let s="(-?\\d*\\.\\d\\w*)|([^";for(const h of t.USUAL_WORD_SEPARATORS)l.indexOf(h)>=0||(s+="\\"+h);return s+="\\s]+)",new RegExp(s,"g")}t.DEFAULT_WORD_REGEXP=p();function P(l){let s=t.DEFAULT_WORD_REGEXP;if(l&&l instanceof RegExp)if(l.global)s=l;else{let h="g";l.ignoreCase&&(h+="i"),l.multiline&&(h+="m"),l.unicode&&(h+="u"),s=new RegExp(l.source,h)}return s.lastIndex=0,s}t.ensureValidWordDefinition=P;const E={maxLen:1e3,windowSize:15,timeBudget:150};function u(l,s,h,w,a=E){if(h.length>a.maxLen){let r=l-a.maxLen/2;return r<0?r=0:w+=r,h=h.substring(r,l+a.maxLen/2),u(l,s,h,w,a)}const S=Date.now(),v=l-1-w;let C=-1,n=null;for(let r=1;!(Date.now()-S>=a.timeBudget);r++){const m=v-a.windowSize*r;s.lastIndex=Math.max(0,m);const _=d(s,h,v,C);if(!_&&n||(n=_,m<=0))break;C=m}if(n){let r={word:n[0],startColumn:w+1+n.index,endColumn:w+1+n.index+n[0].length};return s.lastIndex=0,r}return null}t.getWordAtText=u;function d(l,s,h,w){let a;for(;a=l.exec(s);){const S=a.index||0;if(S<=h&&l.lastIndex>=h)return a;if(w>0&&S>w)return null}return null}}),V(z[28],G([0,1,23]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.computeLinks=t.LinkComputer=t.StateMachine=t.Uint8Matrix=void 0;class P{constructor(S,v,C){const n=new Uint8Array(S*v);for(let r=0,m=S*v;rv&&(v=o),_>C&&(C=_),i>C&&(C=i)}v++,C++;let n=new P(C,v,0);for(let r=0,m=S.length;r=this._maxCharCode?0:this._states.get(S,v)}}t.StateMachine=E;let u=null;function d(){return u===null&&(u=new E([[1,104,2],[1,72,2],[1,102,6],[1,70,6],[2,116,3],[2,84,3],[3,116,4],[3,84,4],[4,112,5],[4,80,5],[5,115,9],[5,83,9],[5,58,10],[6,105,7],[6,73,7],[7,108,8],[7,76,8],[8,101,9],[8,69,9],[9,58,10],[10,47,11],[11,47,12]])),u}let l=null;function s(){if(l===null){l=new p.CharacterClassifier(0);const a=` <>'"\u3001\u3002\uFF61\uFF64\uFF0C\uFF0E\uFF1A\uFF1B\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\uFF08\uFF3B\uFF5B\uFF62\uFF63\uFF5D\uFF3D\uFF09\u3015\u3011\u300F\u300D\u300B\u3009\u201D\u2019\uFF40\uFF5E\u2026`;for(let v=0;vn);if(n>0){const _=v.charCodeAt(n-1),o=v.charCodeAt(m);(_===40&&o===41||_===91&&o===93||_===123&&o===125)&&m--}return{range:{startLineNumber:C,startColumn:n+1,endLineNumber:C,endColumn:m+2},url:v.substring(n,m+1)}}static computeLinks(S,v=d()){const C=s();let n=[];for(let r=1,m=S.getLineCount();r<=m;r++){const _=S.getLineContent(r),o=_.length;let i=0,f=0,b=0,N=1,c=!1,g=!1,L=!1,A=!1;for(;i=0?(l+=d?1:-1,l<0?l=E.length-1:l%=E.length,E[l]):null}}t.BasicInplaceReplace=p,p.INSTANCE=new p}),V(z[30],G([0,1]),function(I,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.WrappingIndent=t.TrackedRangeStickiness=t.TextEditorCursorStyle=t.TextEditorCursorBlinkingStyle=t.SymbolTag=t.SymbolKind=t.SignatureHelpTriggerKind=t.SelectionDirection=t.ScrollbarVisibility=t.ScrollType=t.RenderMinimap=t.RenderLineNumbersType=t.OverviewRulerLane=t.OverlayWidgetPositionPreference=t.MouseTargetType=t.MinimapPosition=t.MarkerTag=t.MarkerSeverity=t.KeyCode=t.InlineHintKind=t.IndentAction=t.EndOfLineSequence=t.EndOfLinePreference=t.EditorOption=t.EditorAutoIndentStrategy=t.DocumentHighlightKind=t.DefaultEndOfLine=t.CursorChangeReason=t.ContentWidgetPositionPreference=t.CompletionTriggerKind=t.CompletionItemTag=t.CompletionItemKind=t.CompletionItemInsertTextRule=t.AccessibilitySupport=void 0;var p;(function(e){e[e.Unknown=0]="Unknown",e[e.Disabled=1]="Disabled",e[e.Enabled=2]="Enabled"})(p=t.AccessibilitySupport||(t.AccessibilitySupport={}));var P;(function(e){e[e.KeepWhitespace=1]="KeepWhitespace",e[e.InsertAsSnippet=4]="InsertAsSnippet"})(P=t.CompletionItemInsertTextRule||(t.CompletionItemInsertTextRule={}));var E;(function(e){e[e.Method=0]="Method",e[e.Function=1]="Function",e[e.Constructor=2]="Constructor",e[e.Field=3]="Field",e[e.Variable=4]="Variable",e[e.Class=5]="Class",e[e.Struct=6]="Struct",e[e.Interface=7]="Interface",e[e.Module=8]="Module",e[e.Property=9]="Property",e[e.Event=10]="Event",e[e.Operator=11]="Operator",e[e.Unit=12]="Unit",e[e.Value=13]="Value",e[e.Constant=14]="Constant",e[e.Enum=15]="Enum",e[e.EnumMember=16]="EnumMember",e[e.Keyword=17]="Keyword",e[e.Text=18]="Text",e[e.Color=19]="Color",e[e.File=20]="File",e[e.Reference=21]="Reference",e[e.Customcolor=22]="Customcolor",e[e.Folder=23]="Folder",e[e.TypeParameter=24]="TypeParameter",e[e.User=25]="User",e[e.Issue=26]="Issue",e[e.Snippet=27]="Snippet"})(E=t.CompletionItemKind||(t.CompletionItemKind={}));var u;(function(e){e[e.Deprecated=1]="Deprecated"})(u=t.CompletionItemTag||(t.CompletionItemTag={}));var d;(function(e){e[e.Invoke=0]="Invoke",e[e.TriggerCharacter=1]="TriggerCharacter",e[e.TriggerForIncompleteCompletions=2]="TriggerForIncompleteCompletions"})(d=t.CompletionTriggerKind||(t.CompletionTriggerKind={}));var l;(function(e){e[e.EXACT=0]="EXACT",e[e.ABOVE=1]="ABOVE",e[e.BELOW=2]="BELOW"})(l=t.ContentWidgetPositionPreference||(t.ContentWidgetPositionPreference={}));var s;(function(e){e[e.NotSet=0]="NotSet",e[e.ContentFlush=1]="ContentFlush",e[e.RecoverFromMarkers=2]="RecoverFromMarkers",e[e.Explicit=3]="Explicit",e[e.Paste=4]="Paste",e[e.Undo=5]="Undo",e[e.Redo=6]="Redo"})(s=t.CursorChangeReason||(t.CursorChangeReason={}));var h;(function(e){e[e.LF=1]="LF",e[e.CRLF=2]="CRLF"})(h=t.DefaultEndOfLine||(t.DefaultEndOfLine={}));var w;(function(e){e[e.Text=0]="Text",e[e.Read=1]="Read",e[e.Write=2]="Write"})(w=t.DocumentHighlightKind||(t.DocumentHighlightKind={}));var a;(function(e){e[e.None=0]="None",e[e.Keep=1]="Keep",e[e.Brackets=2]="Brackets",e[e.Advanced=3]="Advanced",e[e.Full=4]="Full"})(a=t.EditorAutoIndentStrategy||(t.EditorAutoIndentStrategy={}));var S;(function(e){e[e.acceptSuggestionOnCommitCharacter=0]="acceptSuggestionOnCommitCharacter",e[e.acceptSuggestionOnEnter=1]="acceptSuggestionOnEnter",e[e.accessibilitySupport=2]="accessibilitySupport",e[e.accessibilityPageSize=3]="accessibilityPageSize",e[e.ariaLabel=4]="ariaLabel",e[e.autoClosingBrackets=5]="autoClosingBrackets",e[e.autoClosingOvertype=6]="autoClosingOvertype",e[e.autoClosingQuotes=7]="autoClosingQuotes",e[e.autoIndent=8]="autoIndent",e[e.automaticLayout=9]="automaticLayout",e[e.autoSurround=10]="autoSurround",e[e.codeLens=11]="codeLens",e[e.codeLensFontFamily=12]="codeLensFontFamily",e[e.codeLensFontSize=13]="codeLensFontSize",e[e.colorDecorators=14]="colorDecorators",e[e.columnSelection=15]="columnSelection",e[e.comments=16]="comments",e[e.contextmenu=17]="contextmenu",e[e.copyWithSyntaxHighlighting=18]="copyWithSyntaxHighlighting",e[e.cursorBlinking=19]="cursorBlinking",e[e.cursorSmoothCaretAnimation=20]="cursorSmoothCaretAnimation",e[e.cursorStyle=21]="cursorStyle",e[e.cursorSurroundingLines=22]="cursorSurroundingLines",e[e.cursorSurroundingLinesStyle=23]="cursorSurroundingLinesStyle",e[e.cursorWidth=24]="cursorWidth",e[e.disableLayerHinting=25]="disableLayerHinting",e[e.disableMonospaceOptimizations=26]="disableMonospaceOptimizations",e[e.dragAndDrop=27]="dragAndDrop",e[e.emptySelectionClipboard=28]="emptySelectionClipboard",e[e.extraEditorClassName=29]="extraEditorClassName",e[e.fastScrollSensitivity=30]="fastScrollSensitivity",e[e.find=31]="find",e[e.fixedOverflowWidgets=32]="fixedOverflowWidgets",e[e.folding=33]="folding",e[e.foldingStrategy=34]="foldingStrategy",e[e.foldingHighlight=35]="foldingHighlight",e[e.unfoldOnClickAfterEndOfLine=36]="unfoldOnClickAfterEndOfLine",e[e.fontFamily=37]="fontFamily",e[e.fontInfo=38]="fontInfo",e[e.fontLigatures=39]="fontLigatures",e[e.fontSize=40]="fontSize",e[e.fontWeight=41]="fontWeight",e[e.formatOnPaste=42]="formatOnPaste",e[e.formatOnType=43]="formatOnType",e[e.glyphMargin=44]="glyphMargin",e[e.gotoLocation=45]="gotoLocation",e[e.hideCursorInOverviewRuler=46]="hideCursorInOverviewRuler",e[e.highlightActiveIndentGuide=47]="highlightActiveIndentGuide",e[e.hover=48]="hover",e[e.inDiffEditor=49]="inDiffEditor",e[e.letterSpacing=50]="letterSpacing",e[e.lightbulb=51]="lightbulb",e[e.lineDecorationsWidth=52]="lineDecorationsWidth",e[e.lineHeight=53]="lineHeight",e[e.lineNumbers=54]="lineNumbers",e[e.lineNumbersMinChars=55]="lineNumbersMinChars",e[e.linkedEditing=56]="linkedEditing",e[e.links=57]="links",e[e.matchBrackets=58]="matchBrackets",e[e.minimap=59]="minimap",e[e.mouseStyle=60]="mouseStyle",e[e.mouseWheelScrollSensitivity=61]="mouseWheelScrollSensitivity",e[e.mouseWheelZoom=62]="mouseWheelZoom",e[e.multiCursorMergeOverlapping=63]="multiCursorMergeOverlapping",e[e.multiCursorModifier=64]="multiCursorModifier",e[e.multiCursorPaste=65]="multiCursorPaste",e[e.occurrencesHighlight=66]="occurrencesHighlight",e[e.overviewRulerBorder=67]="overviewRulerBorder",e[e.overviewRulerLanes=68]="overviewRulerLanes",e[e.padding=69]="padding",e[e.parameterHints=70]="parameterHints",e[e.peekWidgetDefaultFocus=71]="peekWidgetDefaultFocus",e[e.definitionLinkOpensInPeek=72]="definitionLinkOpensInPeek",e[e.quickSuggestions=73]="quickSuggestions",e[e.quickSuggestionsDelay=74]="quickSuggestionsDelay",e[e.readOnly=75]="readOnly",e[e.renameOnType=76]="renameOnType",e[e.renderControlCharacters=77]="renderControlCharacters",e[e.renderIndentGuides=78]="renderIndentGuides",e[e.renderFinalNewline=79]="renderFinalNewline",e[e.renderLineHighlight=80]="renderLineHighlight",e[e.renderLineHighlightOnlyWhenFocus=81]="renderLineHighlightOnlyWhenFocus",e[e.renderValidationDecorations=82]="renderValidationDecorations",e[e.renderWhitespace=83]="renderWhitespace",e[e.revealHorizontalRightPadding=84]="revealHorizontalRightPadding",e[e.roundedSelection=85]="roundedSelection",e[e.rulers=86]="rulers",e[e.scrollbar=87]="scrollbar",e[e.scrollBeyondLastColumn=88]="scrollBeyondLastColumn",e[e.scrollBeyondLastLine=89]="scrollBeyondLastLine",e[e.scrollPredominantAxis=90]="scrollPredominantAxis",e[e.selectionClipboard=91]="selectionClipboard",e[e.selectionHighlight=92]="selectionHighlight",e[e.selectOnLineNumbers=93]="selectOnLineNumbers",e[e.showFoldingControls=94]="showFoldingControls",e[e.showUnused=95]="showUnused",e[e.snippetSuggestions=96]="snippetSuggestions",e[e.smartSelect=97]="smartSelect",e[e.smoothScrolling=98]="smoothScrolling",e[e.stickyTabStops=99]="stickyTabStops",e[e.stopRenderingLineAfter=100]="stopRenderingLineAfter",e[e.suggest=101]="suggest",e[e.suggestFontSize=102]="suggestFontSize",e[e.suggestLineHeight=103]="suggestLineHeight",e[e.suggestOnTriggerCharacters=104]="suggestOnTriggerCharacters",e[e.suggestSelection=105]="suggestSelection",e[e.tabCompletion=106]="tabCompletion",e[e.tabIndex=107]="tabIndex",e[e.unusualLineTerminators=108]="unusualLineTerminators",e[e.useTabStops=109]="useTabStops",e[e.wordSeparators=110]="wordSeparators",e[e.wordWrap=111]="wordWrap",e[e.wordWrapBreakAfterCharacters=112]="wordWrapBreakAfterCharacters",e[e.wordWrapBreakBeforeCharacters=113]="wordWrapBreakBeforeCharacters",e[e.wordWrapColumn=114]="wordWrapColumn",e[e.wordWrapOverride1=115]="wordWrapOverride1",e[e.wordWrapOverride2=116]="wordWrapOverride2",e[e.wrappingIndent=117]="wrappingIndent",e[e.wrappingStrategy=118]="wrappingStrategy",e[e.showDeprecated=119]="showDeprecated",e[e.inlineHints=120]="inlineHints",e[e.editorClassName=121]="editorClassName",e[e.pixelRatio=122]="pixelRatio",e[e.tabFocusMode=123]="tabFocusMode",e[e.layoutInfo=124]="layoutInfo",e[e.wrappingInfo=125]="wrappingInfo"})(S=t.EditorOption||(t.EditorOption={}));var v;(function(e){e[e.TextDefined=0]="TextDefined",e[e.LF=1]="LF",e[e.CRLF=2]="CRLF"})(v=t.EndOfLinePreference||(t.EndOfLinePreference={}));var C;(function(e){e[e.LF=0]="LF",e[e.CRLF=1]="CRLF"})(C=t.EndOfLineSequence||(t.EndOfLineSequence={}));var n;(function(e){e[e.None=0]="None",e[e.Indent=1]="Indent",e[e.IndentOutdent=2]="IndentOutdent",e[e.Outdent=3]="Outdent"})(n=t.IndentAction||(t.IndentAction={}));var r;(function(e){e[e.Other=0]="Other",e[e.Type=1]="Type",e[e.Parameter=2]="Parameter"})(r=t.InlineHintKind||(t.InlineHintKind={}));var m;(function(e){e[e.Unknown=0]="Unknown",e[e.Backspace=1]="Backspace",e[e.Tab=2]="Tab",e[e.Enter=3]="Enter",e[e.Shift=4]="Shift",e[e.Ctrl=5]="Ctrl",e[e.Alt=6]="Alt",e[e.PauseBreak=7]="PauseBreak",e[e.CapsLock=8]="CapsLock",e[e.Escape=9]="Escape",e[e.Space=10]="Space",e[e.PageUp=11]="PageUp",e[e.PageDown=12]="PageDown",e[e.End=13]="End",e[e.Home=14]="Home",e[e.LeftArrow=15]="LeftArrow",e[e.UpArrow=16]="UpArrow",e[e.RightArrow=17]="RightArrow",e[e.DownArrow=18]="DownArrow",e[e.Insert=19]="Insert",e[e.Delete=20]="Delete",e[e.KEY_0=21]="KEY_0",e[e.KEY_1=22]="KEY_1",e[e.KEY_2=23]="KEY_2",e[e.KEY_3=24]="KEY_3",e[e.KEY_4=25]="KEY_4",e[e.KEY_5=26]="KEY_5",e[e.KEY_6=27]="KEY_6",e[e.KEY_7=28]="KEY_7",e[e.KEY_8=29]="KEY_8",e[e.KEY_9=30]="KEY_9",e[e.KEY_A=31]="KEY_A",e[e.KEY_B=32]="KEY_B",e[e.KEY_C=33]="KEY_C",e[e.KEY_D=34]="KEY_D",e[e.KEY_E=35]="KEY_E",e[e.KEY_F=36]="KEY_F",e[e.KEY_G=37]="KEY_G",e[e.KEY_H=38]="KEY_H",e[e.KEY_I=39]="KEY_I",e[e.KEY_J=40]="KEY_J",e[e.KEY_K=41]="KEY_K",e[e.KEY_L=42]="KEY_L",e[e.KEY_M=43]="KEY_M",e[e.KEY_N=44]="KEY_N",e[e.KEY_O=45]="KEY_O",e[e.KEY_P=46]="KEY_P",e[e.KEY_Q=47]="KEY_Q",e[e.KEY_R=48]="KEY_R",e[e.KEY_S=49]="KEY_S",e[e.KEY_T=50]="KEY_T",e[e.KEY_U=51]="KEY_U",e[e.KEY_V=52]="KEY_V",e[e.KEY_W=53]="KEY_W",e[e.KEY_X=54]="KEY_X",e[e.KEY_Y=55]="KEY_Y",e[e.KEY_Z=56]="KEY_Z",e[e.Meta=57]="Meta",e[e.ContextMenu=58]="ContextMenu",e[e.F1=59]="F1",e[e.F2=60]="F2",e[e.F3=61]="F3",e[e.F4=62]="F4",e[e.F5=63]="F5",e[e.F6=64]="F6",e[e.F7=65]="F7",e[e.F8=66]="F8",e[e.F9=67]="F9",e[e.F10=68]="F10",e[e.F11=69]="F11",e[e.F12=70]="F12",e[e.F13=71]="F13",e[e.F14=72]="F14",e[e.F15=73]="F15",e[e.F16=74]="F16",e[e.F17=75]="F17",e[e.F18=76]="F18",e[e.F19=77]="F19",e[e.NumLock=78]="NumLock",e[e.ScrollLock=79]="ScrollLock",e[e.US_SEMICOLON=80]="US_SEMICOLON",e[e.US_EQUAL=81]="US_EQUAL",e[e.US_COMMA=82]="US_COMMA",e[e.US_MINUS=83]="US_MINUS",e[e.US_DOT=84]="US_DOT",e[e.US_SLASH=85]="US_SLASH",e[e.US_BACKTICK=86]="US_BACKTICK",e[e.US_OPEN_SQUARE_BRACKET=87]="US_OPEN_SQUARE_BRACKET",e[e.US_BACKSLASH=88]="US_BACKSLASH",e[e.US_CLOSE_SQUARE_BRACKET=89]="US_CLOSE_SQUARE_BRACKET",e[e.US_QUOTE=90]="US_QUOTE",e[e.OEM_8=91]="OEM_8",e[e.OEM_102=92]="OEM_102",e[e.NUMPAD_0=93]="NUMPAD_0",e[e.NUMPAD_1=94]="NUMPAD_1",e[e.NUMPAD_2=95]="NUMPAD_2",e[e.NUMPAD_3=96]="NUMPAD_3",e[e.NUMPAD_4=97]="NUMPAD_4",e[e.NUMPAD_5=98]="NUMPAD_5",e[e.NUMPAD_6=99]="NUMPAD_6",e[e.NUMPAD_7=100]="NUMPAD_7",e[e.NUMPAD_8=101]="NUMPAD_8",e[e.NUMPAD_9=102]="NUMPAD_9",e[e.NUMPAD_MULTIPLY=103]="NUMPAD_MULTIPLY",e[e.NUMPAD_ADD=104]="NUMPAD_ADD",e[e.NUMPAD_SEPARATOR=105]="NUMPAD_SEPARATOR",e[e.NUMPAD_SUBTRACT=106]="NUMPAD_SUBTRACT",e[e.NUMPAD_DECIMAL=107]="NUMPAD_DECIMAL",e[e.NUMPAD_DIVIDE=108]="NUMPAD_DIVIDE",e[e.KEY_IN_COMPOSITION=109]="KEY_IN_COMPOSITION",e[e.ABNT_C1=110]="ABNT_C1",e[e.ABNT_C2=111]="ABNT_C2",e[e.MAX_VALUE=112]="MAX_VALUE"})(m=t.KeyCode||(t.KeyCode={}));var _;(function(e){e[e.Hint=1]="Hint",e[e.Info=2]="Info",e[e.Warning=4]="Warning",e[e.Error=8]="Error"})(_=t.MarkerSeverity||(t.MarkerSeverity={}));var o;(function(e){e[e.Unnecessary=1]="Unnecessary",e[e.Deprecated=2]="Deprecated"})(o=t.MarkerTag||(t.MarkerTag={}));var i;(function(e){e[e.Inline=1]="Inline",e[e.Gutter=2]="Gutter"})(i=t.MinimapPosition||(t.MinimapPosition={}));var f;(function(e){e[e.UNKNOWN=0]="UNKNOWN",e[e.TEXTAREA=1]="TEXTAREA",e[e.GUTTER_GLYPH_MARGIN=2]="GUTTER_GLYPH_MARGIN",e[e.GUTTER_LINE_NUMBERS=3]="GUTTER_LINE_NUMBERS",e[e.GUTTER_LINE_DECORATIONS=4]="GUTTER_LINE_DECORATIONS",e[e.GUTTER_VIEW_ZONE=5]="GUTTER_VIEW_ZONE",e[e.CONTENT_TEXT=6]="CONTENT_TEXT",e[e.CONTENT_EMPTY=7]="CONTENT_EMPTY",e[e.CONTENT_VIEW_ZONE=8]="CONTENT_VIEW_ZONE",e[e.CONTENT_WIDGET=9]="CONTENT_WIDGET",e[e.OVERVIEW_RULER=10]="OVERVIEW_RULER",e[e.SCROLLBAR=11]="SCROLLBAR",e[e.OVERLAY_WIDGET=12]="OVERLAY_WIDGET",e[e.OUTSIDE_EDITOR=13]="OUTSIDE_EDITOR"})(f=t.MouseTargetType||(t.MouseTargetType={}));var b;(function(e){e[e.TOP_RIGHT_CORNER=0]="TOP_RIGHT_CORNER",e[e.BOTTOM_RIGHT_CORNER=1]="BOTTOM_RIGHT_CORNER",e[e.TOP_CENTER=2]="TOP_CENTER"})(b=t.OverlayWidgetPositionPreference||(t.OverlayWidgetPositionPreference={}));var N;(function(e){e[e.Left=1]="Left",e[e.Center=2]="Center",e[e.Right=4]="Right",e[e.Full=7]="Full"})(N=t.OverviewRulerLane||(t.OverviewRulerLane={}));var c;(function(e){e[e.Off=0]="Off",e[e.On=1]="On",e[e.Relative=2]="Relative",e[e.Interval=3]="Interval",e[e.Custom=4]="Custom"})(c=t.RenderLineNumbersType||(t.RenderLineNumbersType={}));var g;(function(e){e[e.None=0]="None",e[e.Text=1]="Text",e[e.Blocks=2]="Blocks"})(g=t.RenderMinimap||(t.RenderMinimap={}));var L;(function(e){e[e.Smooth=0]="Smooth",e[e.Immediate=1]="Immediate"})(L=t.ScrollType||(t.ScrollType={}));var A;(function(e){e[e.Auto=1]="Auto",e[e.Hidden=2]="Hidden",e[e.Visible=3]="Visible"})(A=t.ScrollbarVisibility||(t.ScrollbarVisibility={}));var M;(function(e){e[e.LTR=0]="LTR",e[e.RTL=1]="RTL"})(M=t.SelectionDirection||(t.SelectionDirection={}));var R;(function(e){e[e.Invoke=1]="Invoke",e[e.TriggerCharacter=2]="TriggerCharacter",e[e.ContentChange=3]="ContentChange"})(R=t.SignatureHelpTriggerKind||(t.SignatureHelpTriggerKind={}));var D;(function(e){e[e.File=0]="File",e[e.Module=1]="Module",e[e.Namespace=2]="Namespace",e[e.Package=3]="Package",e[e.Class=4]="Class",e[e.Method=5]="Method",e[e.Property=6]="Property",e[e.Field=7]="Field",e[e.Constructor=8]="Constructor",e[e.Enum=9]="Enum",e[e.Interface=10]="Interface",e[e.Function=11]="Function",e[e.Variable=12]="Variable",e[e.Constant=13]="Constant",e[e.String=14]="String",e[e.Number=15]="Number",e[e.Boolean=16]="Boolean",e[e.Array=17]="Array",e[e.Object=18]="Object",e[e.Key=19]="Key",e[e.Null=20]="Null",e[e.EnumMember=21]="EnumMember",e[e.Struct=22]="Struct",e[e.Event=23]="Event",e[e.Operator=24]="Operator",e[e.TypeParameter=25]="TypeParameter"})(D=t.SymbolKind||(t.SymbolKind={}));var T;(function(e){e[e.Deprecated=1]="Deprecated"})(T=t.SymbolTag||(t.SymbolTag={}));var $;(function(e){e[e.Hidden=0]="Hidden",e[e.Blink=1]="Blink",e[e.Smooth=2]="Smooth",e[e.Phase=3]="Phase",e[e.Expand=4]="Expand",e[e.Solid=5]="Solid"})($=t.TextEditorCursorBlinkingStyle||(t.TextEditorCursorBlinkingStyle={}));var k;(function(e){e[e.Line=1]="Line",e[e.Block=2]="Block",e[e.Underline=3]="Underline",e[e.LineThin=4]="LineThin",e[e.BlockOutline=5]="BlockOutline",e[e.UnderlineThin=6]="UnderlineThin"})(k=t.TextEditorCursorStyle||(t.TextEditorCursorStyle={}));var F;(function(e){e[e.AlwaysGrowsWhenTypingAtEdges=0]="AlwaysGrowsWhenTypingAtEdges",e[e.NeverGrowsWhenTypingAtEdges=1]="NeverGrowsWhenTypingAtEdges",e[e.GrowsOnlyWhenTypingBefore=2]="GrowsOnlyWhenTypingBefore",e[e.GrowsOnlyWhenTypingAfter=3]="GrowsOnlyWhenTypingAfter"})(F=t.TrackedRangeStickiness||(t.TrackedRangeStickiness={}));var O;(function(e){e[e.None=0]="None",e[e.Same=1]="Same",e[e.Indent=2]="Indent",e[e.DeepIndent=3]="DeepIndent"})(O=t.WrappingIndent||(t.WrappingIndent={}))}),V(z[31],G([0,1,21,9,17,13,3,6,24,25,30]),function(I,t,p,P,E,u,d,l,s,h,w){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.createMonacoBaseAPI=t.KeyMod=void 0;class a{static chord(C,n){return E.KeyChord(C,n)}}t.KeyMod=a,a.CtrlCmd=2048,a.Shift=1024,a.Alt=512,a.WinCtrl=256;function S(){return{editor:void 0,languages:void 0,CancellationTokenSource:p.CancellationTokenSource,Emitter:P.Emitter,KeyCode:w.KeyCode,KeyMod:a,Position:d.Position,Range:l.Range,Selection:s.Selection,SelectionDirection:w.SelectionDirection,MarkerSeverity:w.MarkerSeverity,MarkerTag:w.MarkerTag,Uri:u.URI,Token:h.Token}}t.createMonacoBaseAPI=S}),V(z[32],G([0,1,12]),function(I,t,p){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.PrefixSumComputer=t.PrefixSumIndexOfResult=void 0;class P{constructor(d,l){this.index=d,this.remainder=l}}t.PrefixSumIndexOfResult=P;class E{constructor(d){this.values=d,this.prefixSum=new Uint32Array(d.length),this.prefixSumValidIndex=new Int32Array(1),this.prefixSumValidIndex[0]=-1}insertValues(d,l){d=p.toUint32(d);const s=this.values,h=this.prefixSum,w=l.length;return w===0?!1:(this.values=new Uint32Array(s.length+w),this.values.set(s.subarray(0,d),0),this.values.set(s.subarray(d),d+w),this.values.set(l,d),d-1=0&&this.prefixSum.set(h.subarray(0,this.prefixSumValidIndex[0]+1)),!0)}changeValue(d,l){return d=p.toUint32(d),l=p.toUint32(l),this.values[d]===l?!1:(this.values[d]=l,d-1=s.length)return!1;let w=s.length-d;return l>=w&&(l=w),l===0?!1:(this.values=new Uint32Array(s.length-l),this.values.set(s.subarray(0,d),0),this.values.set(s.subarray(d+l),d),this.prefixSum=new Uint32Array(this.values.length),d-1=0&&this.prefixSum.set(h.subarray(0,this.prefixSumValidIndex[0]+1)),!0)}getTotalValue(){return this.values.length===0?0:this._getAccumulatedValue(this.values.length-1)}getAccumulatedValue(d){return d<0?0:(d=p.toUint32(d),this._getAccumulatedValue(d))}_getAccumulatedValue(d){if(d<=this.prefixSumValidIndex[0])return this.prefixSum[d];let l=this.prefixSumValidIndex[0]+1;l===0&&(this.prefixSum[0]=this.values[0],l++),d>=this.values.length&&(d=this.values.length-1);for(let s=l;s<=d;s++)this.prefixSum[s]=this.prefixSum[s-1]+this.values[s];return this.prefixSumValidIndex[0]=Math.max(this.prefixSumValidIndex[0],d),this.prefixSum[d]}getIndexOf(d){d=Math.floor(d),this.getTotalValue();let l=0,s=this.values.length-1,h=0,w=0,a=0;for(;l<=s;)if(h=l+(s-l)/2|0,w=this.prefixSum[h],a=w-this.values[h],d=w)l=h+1;else break;return new P(h,d-a)}}t.PrefixSumComputer=E}),V(z[33],G([0,1,5,3,32]),function(I,t,p,P,E){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.MirrorTextModel=void 0;class u{constructor(l,s,h,w){this._uri=l,this._lines=s,this._eol=h,this._versionId=w,this._lineStarts=null,this._cachedTextValue=null}dispose(){this._lines.length=0}getText(){return this._cachedTextValue===null&&(this._cachedTextValue=this._lines.join(this._eol)),this._cachedTextValue}onEvents(l){l.eol&&l.eol!==this._eol&&(this._eol=l.eol,this._lineStarts=null);const s=l.changes;for(const h of s)this._acceptDeleteRange(h.range),this._acceptInsertText(new P.Position(h.range.startLineNumber,h.range.startColumn),h.text);this._versionId=l.versionId,this._cachedTextValue=null}_ensureLineStarts(){if(!this._lineStarts){const l=this._eol.length,s=this._lines.length,h=new Uint32Array(s);for(let w=0;wthis._lines.length)f=this._lines.length,b=this._lines[f-1].length+1,N=!0;else{let c=this._lines[f-1].length+1;b<1?(b=1,N=!0):b>c&&(b=c,N=!0)}return N?{lineNumber:f,column:b}:i}}class m{constructor(i,f){this._host=i,this._models=Object.create(null),this._foreignModuleFactory=f,this._foreignModule=null}dispose(){this._models=Object.create(null)}_getModel(i){return this._models[i]}_getModels(){let i=[];return Object.keys(this._models).forEach(f=>i.push(this._models[f])),i}acceptNewModel(i){this._models[i.url]=new r(u.URI.parse(i.url),i.lines,i.EOL,i.versionId)}acceptModelChanged(i,f){!this._models[i]||this._models[i].onEvents(f)}acceptRemovedModel(i){!this._models[i]||delete this._models[i]}computeDiff(i,f,b,N){return ne(this,void 0,void 0,function*(){const c=this._getModel(i),g=this._getModel(f);if(!c||!g)return null;const L=c.getLinesContent(),A=g.getLinesContent(),R=new s.DiffComputer(L,A,{shouldComputeCharChanges:!0,shouldPostProcessCharChanges:!0,shouldIgnoreTrimWhitespace:b,shouldMakePrettyDiff:!0,maxComputationTime:N}).computeDiff(),D=R.changes.length>0?!1:this._modelsAreIdentical(c,g);return{quitEarly:R.quitEarly,identical:D,changes:R.changes}})}_modelsAreIdentical(i,f){const b=i.getLineCount(),N=f.getLineCount();if(b!==N)return!1;for(let c=1;c<=b;c++){const g=i.getLineContent(c),L=f.getLineContent(c);if(g!==L)return!1}return!0}computeMoreMinimalEdits(i,f){return ne(this,void 0,void 0,function*(){const b=this._getModel(i);if(!b)return f;const N=[];let c;f=p.mergeSort(f,(g,L)=>{if(g.range&&L.range)return l.Range.compareRangesUsingStarts(g.range,L.range);let A=g.range?0:1,M=L.range?0:1;return A-M});for(let{range:g,text:L,eol:A}of f)if(typeof A=="number"&&(c=A),!(l.Range.isEmpty(g)&&!L)){const M=b.getValueInRange(g);if(L=L.replace(/\r\n|\n|\r/g,b.eol),M!==L){if(Math.max(L.length,M.length)>m._diffLimit){N.push({range:g,text:L});continue}const R=P.stringDiff(M,L,!1),D=b.offsetAt(l.Range.lift(g).getStartPosition());for(const T of R){const $=b.positionAt(D+T.originalStart),k=b.positionAt(D+T.originalStart+T.originalLength),F={text:L.substr(T.modifiedStart,T.modifiedLength),range:{startLineNumber:$.lineNumber,startColumn:$.column,endLineNumber:k.lineNumber,endColumn:k.column}};b.getValueInRange(F.range)!==F.text&&N.push(F)}}}return typeof c=="number"&&N.push({eol:c,text:"",range:{startLineNumber:0,startColumn:0,endLineNumber:0,endColumn:0}}),N})}computeLinks(i){return ne(this,void 0,void 0,function*(){let f=this._getModel(i);return f?a.computeLinks(f):null})}textualSuggest(i,f,b,N){return ne(this,void 0,void 0,function*(){const c=new n.StopWatch(!0),g=new RegExp(b,N),L=new Set;e:for(let A of i){const M=this._getModel(A);if(!!M){for(let R of M.words(g))if(!(R===f||!isNaN(Number(R)))&&(L.add(R),L.size>m._suggestionsLimit))break e}}return{words:Array.from(L),duration:c.elapsed()}})}computeWordRanges(i,f,b,N){return ne(this,void 0,void 0,function*(){let c=this._getModel(i);if(!c)return Object.create(null);const g=new RegExp(b,N),L=Object.create(null);for(let A=f.startLineNumber;Athis._host.fhr(L,A);let g={host:C.createProxyObject(b,N),getMirrorModels:()=>this._getModels()};return this._foreignModuleFactory?(this._foreignModule=this._foreignModuleFactory(g,f),Promise.resolve(C.getAllMethodNames(this._foreignModule))):new Promise((L,A)=>{I([i],M=>{this._foreignModule=M.create(g,f),L(C.getAllMethodNames(this._foreignModule))},A)})}fmr(i,f){if(!this._foreignModule||typeof this._foreignModule[i]!="function")return Promise.reject(new Error("Missing requestHandler or method: "+i));try{return Promise.resolve(this._foreignModule[i].apply(this._foreignModule,f))}catch(b){return Promise.reject(b)}}}t.EditorSimpleWorker=m,m._diffLimit=1e5,m._suggestionsLimit=1e4;function _(o){return new m(o,null)}t.create=_,typeof importScripts=="function"&&(E.globals.monaco=v.createMonacoBaseAPI())});"use strict";(function(){var I,t;const p=self.MonacoEnvironment,P=p&&p.baseUrl?p.baseUrl:"../../../",E=typeof((I=self.trustedTypes)===null||I===void 0?void 0:I.createPolicy)=="function"?(t=self.trustedTypes)===null||t===void 0?void 0:t.createPolicy("amdLoader",{createScriptURL:s=>s}):void 0;if(typeof self.define!="function"||!self.define.amd){let s=P+"vs/loader.js";E&&(s=E.createScriptURL(s)),importScripts(s)}require.config({baseUrl:P,catchError:!0,trustedTypesPolicy:E});let u=function(s){require([s],function(h){setTimeout(function(){let w=h.create((a,S)=>{self.postMessage(a,S)},null);for(self.onmessage=a=>w.onmessage(a.data);l.length>0;)self.onmessage(l.shift())},0)})},d=!0,l=[];self.onmessage=s=>{if(!d){l.push(s);return}d=!1,u(s.data)}})()}).call(this); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/abap/abap.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/abap/abap.js new file mode 100644 index 0000000..efe87c5 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/abap/abap.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/abap/abap",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"*"},brackets:[["[","]"],["(",")"]]};n.language={defaultToken:"invalid",ignoreCase:!0,tokenPostfix:".abap",keywords:["abap-source","abbreviated","abstract","accept","accepting","according","activation","actual","add","add-corresponding","adjacent","after","alias","aliases","align","all","allocate","alpha","analysis","analyzer","append","appendage","appending","application","archive","area","arithmetic","as","ascending","aspect","assert","assign","assigned","assigning","association","asynchronous","at","attributes","authority","authority-check","avg","back","background","backup","backward","badi","base","before","begin","big","binary","bintohex","bit","black","blank","blanks","blob","block","blocks","blue","bound","boundaries","bounds","boxed","break-point","buffer","by","bypassing","byte","byte-order","call","calling","case","cast","casting","catch","center","centered","chain","chain-input","chain-request","change","changing","channels","character","char-to-hex","check","checkbox","ci_","circular","class","class-coding","class-data","class-events","class-methods","class-pool","cleanup","clear","client","clob","clock","close","coalesce","code","coding","col_background","col_group","col_heading","col_key","col_negative","col_normal","col_positive","col_total","collect","color","column","columns","comment","comments","commit","common","communication","comparing","component","components","compression","compute","concat","concat_with_space","concatenate","cond","condition","connect","connection","constants","context","contexts","continue","control","controls","conv","conversion","convert","copies","copy","corresponding","country","cover","cpi","create","creating","critical","currency","currency_conversion","current","cursor","cursor-selection","customer","customer-function","dangerous","data","database","datainfo","dataset","date","dats_add_days","dats_add_months","dats_days_between","dats_is_valid","daylight","dd/mm/yy","dd/mm/yyyy","ddmmyy","deallocate","decimal_shift","decimals","declarations","deep","default","deferred","define","defining","definition","delete","deleting","demand","department","descending","describe","destination","detail","dialog","directory","disconnect","display","display-mode","distinct","divide","divide-corresponding","division","do","dummy","duplicate","duplicates","duration","during","dynamic","dynpro","edit","editor-call","else","elseif","empty","enabled","enabling","encoding","end","endat","endcase","endcatch","endchain","endclass","enddo","endenhancement","end-enhancement-section","endexec","endform","endfunction","endian","endif","ending","endinterface","end-lines","endloop","endmethod","endmodule","end-of-definition","end-of-editing","end-of-file","end-of-page","end-of-selection","endon","endprovide","endselect","end-test-injection","end-test-seam","endtry","endwhile","endwith","engineering","enhancement","enhancement-point","enhancements","enhancement-section","entries","entry","enum","environment","errormessage","errors","escaping","event","events","exact","except","exception","exceptions","exception-table","exclude","excluding","exec","execute","exists","exit","exit-command","expand","expanding","expiration","explicit","exponent","export","exporting","extend","extended","extension","extract","fail","fetch","field","field-groups","fields","field-symbol","field-symbols","file","filter","filters","filter-table","final","first","first-line","fixed-point","fkeq","fkge","flush","font","for","form","format","forward","found","frame","frames","free","friends","from","function","functionality","function-pool","further","gaps","generate","get","giving","gkeq","gkge","global","grant","green","group","groups","handle","handler","harmless","hashed","having","hdb","header","headers","heading","head-lines","help-id","help-request","hextobin","hide","high","hint","hold","hotspot","icon","id","identification","identifier","ids","if","ignore","ignoring","immediately","implementation","implementations","implemented","implicit","import","importing","inactive","incl","include","includes","including","increment","index","index-line","infotypes","inheriting","init","initial","initialization","inner","inout","input","instance","instances","instr","intensified","interface","interface-pool","interfaces","internal","intervals","into","inverse","inverted-date","is","iso","job","join","keep","keeping","kernel","key","keys","keywords","kind","language","last","late","layout","leading","leave","left","left-justified","leftplus","leftspace","legacy","length","let","level","levels","like","line","line-count","linefeed","line-selection","line-size","list","listbox","list-processing","little","llang","load","load-of-program","lob","local","locale","locator","logfile","logical","log-point","long","loop","low","lower","lpad","lpi","ltrim","mail","main","major-id","mapping","margin","mark","mask","matchcode","max","maximum","medium","members","memory","mesh","message","message-id","messages","messaging","method","methods","min","minimum","minor-id","mm/dd/yy","mm/dd/yyyy","mmddyy","mode","modif","modifier","modify","module","move","move-corresponding","multiply","multiply-corresponding","name","nametab","native","nested","nesting","new","new-line","new-page","new-section","next","no","node","nodes","no-display","no-extension","no-gap","no-gaps","no-grouping","no-heading","non-unicode","non-unique","no-scrolling","no-sign","no-title","no-topofpage","no-zero","null","number","object","objects","obligatory","occurrence","occurrences","occurs","of","off","offset","ole","on","only","open","option","optional","options","order","other","others","out","outer","output","output-length","overflow","overlay","pack","package","pad","padding","page","pages","parameter","parameters","parameter-table","part","partially","pattern","percentage","perform","performing","person","pf1","pf10","pf11","pf12","pf13","pf14","pf15","pf2","pf3","pf4","pf5","pf6","pf7","pf8","pf9","pf-status","pink","places","pool","pos_high","pos_low","position","pragmas","precompiled","preferred","preserving","primary","print","print-control","priority","private","procedure","process","program","property","protected","provide","public","push","pushbutton","put","queue-only","quickinfo","radiobutton","raise","raising","range","ranges","read","reader","read-only","receive","received","receiver","receiving","red","redefinition","reduce","reduced","ref","reference","refresh","regex","reject","remote","renaming","replacement","replacing","report","request","requested","reserve","reset","resolution","respecting","responsible","result","results","resumable","resume","retry","return","returncode","returning","returns","right","right-justified","rightplus","rightspace","risk","rmc_communication_failure","rmc_invalid_status","rmc_system_failure","role","rollback","rows","rpad","rtrim","run","sap","sap-spool","saving","scale_preserving","scale_preserving_scientific","scan","scientific","scientific_with_leading_zero","scroll","scroll-boundary","scrolling","search","secondary","seconds","section","select","selection","selections","selection-screen","selection-set","selection-sets","selection-table","select-options","send","separate","separated","set","shared","shift","short","shortdump-id","sign_as_postfix","single","size","skip","skipping","smart","some","sort","sortable","sorted","source","specified","split","spool","spots","sql","sqlscript","stable","stamp","standard","starting","start-of-editing","start-of-selection","state","statement","statements","static","statics","statusinfo","step-loop","stop","structure","structures","style","subkey","submatches","submit","subroutine","subscreen","subtract","subtract-corresponding","suffix","sum","summary","summing","supplied","supply","suppress","switch","switchstates","symbol","syncpoints","syntax","syntax-check","syntax-trace","system-call","system-exceptions","system-exit","tab","tabbed","tables","tableview","tabstrip","target","task","tasks","test","testing","test-injection","test-seam","text","textpool","then","throw","time","times","timestamp","timezone","tims_is_valid","title","titlebar","title-lines","to","tokenization","tokens","top-lines","top-of-page","trace-file","trace-table","trailing","transaction","transfer","transformation","transporting","trmac","truncate","truncation","try","tstmp_add_seconds","tstmp_current_utctimestamp","tstmp_is_valid","tstmp_seconds_between","type","type-pool","type-pools","types","uline","unassign","under","unicode","union","unique","unit_conversion","unix","unpack","until","unwind","up","update","upper","user","user-command","using","utf-8","valid","value","value-request","values","vary","varying","verification-message","version","via","view","visible","wait","warning","when","whenever","where","while","width","window","windows","with","with-heading","without","with-title","word","work","write","writer","xml","xsd","yellow","yes","yymmdd","zero","zone","abs","acos","asin","atan","bit-set","boolc","boolx","ceil","char_off","charlen","cmax","cmin","concat_lines_of","condense","contains","contains_any_not_of","contains_any_of","cos","cosh","count","count_any_not_of","count_any_of","dbmaxlen","distance","escape","exp","find","find_any_not_of","find_any_of","find_end","floor","frac","from_mixed","insert","ipow","line_exists","line_index","lines","log","log10","match","matches","nmax","nmin","numofchar","repeat","replace","rescale","reverse","round","segment","shift_left","shift_right","sign","sin","sinh","sqrt","strlen","substring","substring_after","substring_before","substring_from","substring_to","tan","tanh","to_lower","to_mixed","to_upper","translate","trunc","utclong_add","utclong_current","utclong_diff","xsdbool","xstrlen"],typeKeywords:["b","c","d","decfloat16","decfloat34","f","i","int8","n","p","s","string","t","utclong","x","xstring","any","clike","csequence","decfloat","numeric","simple","xsequence","table","hashed","index","sorted","standard","accp","char","clnt","cuky","curr","dats","dec","df16_dec","df16_raw","df34_dec","df34_raw","fltp","int1","int2","int4","lang","lchr","lraw","numc","quan","raw","rawstring","sstring","tims","unit","df16_scl","df34_scl","prec","varc","abap_bool","space","me","syst","sy","screen"],operators:[" +"," -","/","*","**","div","mod","=","#","@","&","&&","bit-and","bit-not","bit-or","bit-xor","m","o","z","and","equiv","not","or"," < "," > ","<=",">=","<>","><","=<","=>","between","bt","byte-ca","byte-cn","byte-co","byte-cs","byte-na","byte-ns","ca","cn","co","cp","cs","eq","ge","gt","in","le","lt","na","nb","ne","np","ns"],symbols:/[=>/,"identifier"],{include:"@whitespace"},[/[:,.]/,"delimiter"],[/[{}()\[\]]/,"@brackets"],[/@symbols/,{cases:{"@operators":"operator","@default":""}}],[/'/,{token:"string",bracket:"@open",next:"@stringquote"}],[/`/,{token:"string",bracket:"@open",next:"@stringping"}],[/\|/,{token:"string",bracket:"@open",next:"@stringtemplate"}],[/\d+/,"number"]],stringtemplate:[[/[^\\\|]+/,"string"],[/\\\|/,"string"],[/\|/,{token:"string",bracket:"@close",next:"@pop"}]],stringping:[[/[^\\`]+/,"string"],[/`/,{token:"string",bracket:"@close",next:"@pop"}]],stringquote:[[/[^\\']+/,"string"],[/'/,{token:"string",bracket:"@close",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,""],[/^\*.*$/,"comment"],[/\".*$/,"comment"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/apex/apex.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/apex/apex.js new file mode 100644 index 0000000..8c75f02 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/apex/apex.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/apex/apex",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"<",close:">"}],folding:{markers:{start:new RegExp("^\\s*//\\s*(?:(?:#?region\\b)|(?:))")}}};var s=[];["abstract","activate","and","any","array","as","asc","assert","autonomous","begin","bigdecimal","blob","boolean","break","bulk","by","case","cast","catch","char","class","collect","commit","const","continue","convertcurrency","decimal","default","delete","desc","do","double","else","end","enum","exception","exit","export","extends","false","final","finally","float","for","from","future","get","global","goto","group","having","hint","if","implements","import","in","inner","insert","instanceof","int","interface","into","join","last_90_days","last_month","last_n_days","last_week","like","limit","list","long","loop","map","merge","native","new","next_90_days","next_month","next_n_days","next_week","not","null","nulls","number","object","of","on","or","outer","override","package","parallel","pragma","private","protected","public","retrieve","return","returning","rollback","savepoint","search","select","set","short","sort","stat","static","strictfp","super","switch","synchronized","system","testmethod","then","this","this_month","this_week","throw","throws","today","tolabel","tomorrow","transaction","transient","trigger","true","try","type","undelete","update","upsert","using","virtual","void","volatile","webservice","when","where","while","yesterday"].forEach((function(e){s.push(e),s.push(e.toUpperCase()),s.push(function(e){return e.charAt(0).toUpperCase()+e.substr(1)}(e))})),t.language={defaultToken:"",tokenPostfix:".apex",keywords:s,operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/@\s*[a-zA-Z_\$][\w\$]*/,"annotation"],[/(@digits)[eE]([\-+]?(@digits))?[fFdD]?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?[fFdD]?/,"number.float"],[/(@digits)[fFdD]/,"number.float"],[/(@digits)[lL]?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string",'@string."'],[/'/,"string","@string.'"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@apexdoc"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],apexdoc:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/["']/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/azcli/azcli.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/azcli/azcli.js new file mode 100644 index 0000000..2ee40f6 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/azcli/azcli.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/azcli/azcli",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"#"}},t.language={defaultToken:"keyword",ignoreCase:!0,tokenPostfix:".azcli",str:/[^#\s]/,tokenizer:{root:[{include:"@comment"},[/\s-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":{token:"key.identifier",next:"@type"}}}],[/^-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":{token:"key.identifier",next:"@type"}}}]],type:[{include:"@comment"},[/-+@str*\s*/,{cases:{"@eos":{token:"key.identifier",next:"@popall"},"@default":"key.identifier"}}],[/@str+\s*/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}]],comment:[[/#.*$/,{cases:{"@eos":{token:"comment",next:"@popall"}}}]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/bat/bat.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/bat/bat.js new file mode 100644 index 0000000..b12e11b --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/bat/bat.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/bat/bat",["require","exports"],(function(e,s){"use strict";Object.defineProperty(s,"__esModule",{value:!0}),s.language=s.conf=void 0,s.conf={comments:{lineComment:"REM"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],surroundingPairs:[{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],folding:{markers:{start:new RegExp("^\\s*(::\\s*|REM\\s+)#region"),end:new RegExp("^\\s*(::\\s*|REM\\s+)#endregion")}}},s.language={defaultToken:"",ignoreCase:!0,tokenPostfix:".bat",brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"}],keywords:/call|defined|echo|errorlevel|exist|for|goto|if|pause|set|shift|start|title|not|pushd|popd/,symbols:/[=>"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"}]},o.language={defaultToken:"",tokenPostfix:".cameligo",ignoreCase:!0,brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],keywords:["abs","begin","Bytes","Crypto","Current","else","end","failwith","false","fun","if","in","let","let%entry","let%init","List","list","Map","map","match","match%nat","mod","not","operation","Operation","of","Set","set","sender","source","String","then","true","type","with"],typeKeywords:["int","unit","string","tz"],operators:["=",">","<","<=",">=","<>",":",":=","and","mod","or","+","-","*","/","@","&","^","%","->","<-"],symbols:/[=><:@\^&|+\-*\/\^%]+/,tokenizer:{root:[[/[a-zA-Z_][\w]*/,{cases:{"@keywords":{token:"keyword.$0"},"@default":"identifier"}}],{include:"@whitespace"},[/[{}()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/\$[0-9a-fA-F]{1,16}/,"number.hex"],[/\d+/,"number"],[/[;,.]/,"delimiter"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/'/,"string","@string"],[/'[^\\']'/,"string"],[/'/,"string.invalid"],[/\#\d+/,"string"]],comment:[[/[^\(\*]+/,"comment"],[/\*\)/,"comment","@pop"],[/\(\*/,"comment"]],string:[[/[^\\']+/,"string"],[/\\./,"string.escape.invalid"],[/'/,{token:"string.quote",bracket:"@close",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,"white"],[/\(\*/,"comment","@comment"],[/\/\/.*$/,"comment"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/clojure/clojure.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/clojure/clojure.js new file mode 100644 index 0000000..24e807d --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/clojure/clojure.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/clojure/clojure",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:";;"},brackets:[["[","]"],["(",")"],["{","}"]],autoClosingPairs:[{open:"[",close:"]"},{open:'"',close:'"'},{open:"(",close:")"},{open:"{",close:"}"}],surroundingPairs:[{open:"[",close:"]"},{open:'"',close:'"'},{open:"(",close:")"},{open:"{",close:"}"}]},t.language={defaultToken:"",ignoreCase:!0,tokenPostfix:".clj",brackets:[{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"{",close:"}",token:"delimiter.curly"}],constants:["true","false","nil"],numbers:/^(?:[+\-]?\d+(?:(?:N|(?:[eE][+\-]?\d+))|(?:\.?\d*(?:M|(?:[eE][+\-]?\d+))?)|\/\d+|[xX][0-9a-fA-F]+|r[0-9a-zA-Z]+)?(?=[\\\[\]\s"#'(),;@^`{}~]|$))/,characters:/^(?:\\(?:backspace|formfeed|newline|return|space|tab|o[0-7]{3}|u[0-9A-Fa-f]{4}|x[0-9A-Fa-f]{4}|.)?(?=[\\\[\]\s"(),;@^`{}~]|$))/,escapes:/^\\(?:["'\\bfnrt]|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,qualifiedSymbols:/^(?:(?:[^\\\/\[\]\d\s"#'(),;@^`{}~][^\\\[\]\s"(),;@^`{}~]*(?:\.[^\\\/\[\]\d\s"#'(),;@^`{}~][^\\\[\]\s"(),;@^`{}~]*)*\/)?(?:\/|[^\\\/\[\]\d\s"#'(),;@^`{}~][^\\\[\]\s"(),;@^`{}~]*)*(?=[\\\[\]\s"(),;@^`{}~]|$))/,specialForms:[".","catch","def","do","if","monitor-enter","monitor-exit","new","quote","recur","set!","throw","try","var"],coreSymbols:["*","*'","*1","*2","*3","*agent*","*allow-unresolved-vars*","*assert*","*clojure-version*","*command-line-args*","*compile-files*","*compile-path*","*compiler-options*","*data-readers*","*default-data-reader-fn*","*e","*err*","*file*","*flush-on-newline*","*fn-loader*","*in*","*math-context*","*ns*","*out*","*print-dup*","*print-length*","*print-level*","*print-meta*","*print-namespace-maps*","*print-readably*","*read-eval*","*reader-resolver*","*source-path*","*suppress-read*","*unchecked-math*","*use-context-classloader*","*verbose-defrecords*","*warn-on-reflection*","+","+'","-","-'","->","->>","->ArrayChunk","->Eduction","->Vec","->VecNode","->VecSeq","-cache-protocol-fn","-reset-methods","..","/","<","<=","=","==",">",">=","EMPTY-NODE","Inst","StackTraceElement->vec","Throwable->map","accessor","aclone","add-classpath","add-watch","agent","agent-error","agent-errors","aget","alength","alias","all-ns","alter","alter-meta!","alter-var-root","amap","ancestors","and","any?","apply","areduce","array-map","as->","aset","aset-boolean","aset-byte","aset-char","aset-double","aset-float","aset-int","aset-long","aset-short","assert","assoc","assoc!","assoc-in","associative?","atom","await","await-for","await1","bases","bean","bigdec","bigint","biginteger","binding","bit-and","bit-and-not","bit-clear","bit-flip","bit-not","bit-or","bit-set","bit-shift-left","bit-shift-right","bit-test","bit-xor","boolean","boolean-array","boolean?","booleans","bound-fn","bound-fn*","bound?","bounded-count","butlast","byte","byte-array","bytes","bytes?","case","cast","cat","char","char-array","char-escape-string","char-name-string","char?","chars","chunk","chunk-append","chunk-buffer","chunk-cons","chunk-first","chunk-next","chunk-rest","chunked-seq?","class","class?","clear-agent-errors","clojure-version","coll?","comment","commute","comp","comparator","compare","compare-and-set!","compile","complement","completing","concat","cond","cond->","cond->>","condp","conj","conj!","cons","constantly","construct-proxy","contains?","count","counted?","create-ns","create-struct","cycle","dec","dec'","decimal?","declare","dedupe","default-data-readers","definline","definterface","defmacro","defmethod","defmulti","defn","defn-","defonce","defprotocol","defrecord","defstruct","deftype","delay","delay?","deliver","denominator","deref","derive","descendants","destructure","disj","disj!","dissoc","dissoc!","distinct","distinct?","doall","dorun","doseq","dosync","dotimes","doto","double","double-array","double?","doubles","drop","drop-last","drop-while","eduction","empty","empty?","ensure","ensure-reduced","enumeration-seq","error-handler","error-mode","eval","even?","every-pred","every?","ex-data","ex-info","extend","extend-protocol","extend-type","extenders","extends?","false?","ffirst","file-seq","filter","filterv","find","find-keyword","find-ns","find-protocol-impl","find-protocol-method","find-var","first","flatten","float","float-array","float?","floats","flush","fn","fn?","fnext","fnil","for","force","format","frequencies","future","future-call","future-cancel","future-cancelled?","future-done?","future?","gen-class","gen-interface","gensym","get","get-in","get-method","get-proxy-class","get-thread-bindings","get-validator","group-by","halt-when","hash","hash-combine","hash-map","hash-ordered-coll","hash-set","hash-unordered-coll","ident?","identical?","identity","if-let","if-not","if-some","ifn?","import","in-ns","inc","inc'","indexed?","init-proxy","inst-ms","inst-ms*","inst?","instance?","int","int-array","int?","integer?","interleave","intern","interpose","into","into-array","ints","io!","isa?","iterate","iterator-seq","juxt","keep","keep-indexed","key","keys","keyword","keyword?","last","lazy-cat","lazy-seq","let","letfn","line-seq","list","list*","list?","load","load-file","load-reader","load-string","loaded-libs","locking","long","long-array","longs","loop","macroexpand","macroexpand-1","make-array","make-hierarchy","map","map-entry?","map-indexed","map?","mapcat","mapv","max","max-key","memfn","memoize","merge","merge-with","meta","method-sig","methods","min","min-key","mix-collection-hash","mod","munge","name","namespace","namespace-munge","nat-int?","neg-int?","neg?","newline","next","nfirst","nil?","nnext","not","not-any?","not-empty","not-every?","not=","ns","ns-aliases","ns-imports","ns-interns","ns-map","ns-name","ns-publics","ns-refers","ns-resolve","ns-unalias","ns-unmap","nth","nthnext","nthrest","num","number?","numerator","object-array","odd?","or","parents","partial","partition","partition-all","partition-by","pcalls","peek","persistent!","pmap","pop","pop!","pop-thread-bindings","pos-int?","pos?","pr","pr-str","prefer-method","prefers","primitives-classnames","print","print-ctor","print-dup","print-method","print-simple","print-str","printf","println","println-str","prn","prn-str","promise","proxy","proxy-call-with-super","proxy-mappings","proxy-name","proxy-super","push-thread-bindings","pvalues","qualified-ident?","qualified-keyword?","qualified-symbol?","quot","rand","rand-int","rand-nth","random-sample","range","ratio?","rational?","rationalize","re-find","re-groups","re-matcher","re-matches","re-pattern","re-seq","read","read-line","read-string","reader-conditional","reader-conditional?","realized?","record?","reduce","reduce-kv","reduced","reduced?","reductions","ref","ref-history-count","ref-max-history","ref-min-history","ref-set","refer","refer-clojure","reify","release-pending-sends","rem","remove","remove-all-methods","remove-method","remove-ns","remove-watch","repeat","repeatedly","replace","replicate","require","reset!","reset-meta!","reset-vals!","resolve","rest","restart-agent","resultset-seq","reverse","reversible?","rseq","rsubseq","run!","satisfies?","second","select-keys","send","send-off","send-via","seq","seq?","seqable?","seque","sequence","sequential?","set","set-agent-send-executor!","set-agent-send-off-executor!","set-error-handler!","set-error-mode!","set-validator!","set?","short","short-array","shorts","shuffle","shutdown-agents","simple-ident?","simple-keyword?","simple-symbol?","slurp","some","some->","some->>","some-fn","some?","sort","sort-by","sorted-map","sorted-map-by","sorted-set","sorted-set-by","sorted?","special-symbol?","spit","split-at","split-with","str","string?","struct","struct-map","subs","subseq","subvec","supers","swap!","swap-vals!","symbol","symbol?","sync","tagged-literal","tagged-literal?","take","take-last","take-nth","take-while","test","the-ns","thread-bound?","time","to-array","to-array-2d","trampoline","transduce","transient","tree-seq","true?","type","unchecked-add","unchecked-add-int","unchecked-byte","unchecked-char","unchecked-dec","unchecked-dec-int","unchecked-divide-int","unchecked-double","unchecked-float","unchecked-inc","unchecked-inc-int","unchecked-int","unchecked-long","unchecked-multiply","unchecked-multiply-int","unchecked-negate","unchecked-negate-int","unchecked-remainder-int","unchecked-short","unchecked-subtract","unchecked-subtract-int","underive","unquote","unquote-splicing","unreduced","unsigned-bit-shift-right","update","update-in","update-proxy","uri?","use","uuid?","val","vals","var-get","var-set","var?","vary-meta","vec","vector","vector-of","vector?","volatile!","volatile?","vreset!","vswap!","when","when-first","when-let","when-not","when-some","while","with-bindings","with-bindings*","with-in-str","with-loading-context","with-local-vars","with-meta","with-open","with-out-str","with-precision","with-redefs","with-redefs-fn","xml-seq","zero?","zipmap"],tokenizer:{root:[{include:"@whitespace"},[/@numbers/,"number"],[/@characters/,"string"],{include:"@string"},[/[()\[\]{}]/,"@brackets"],[/\/#"(?:\.|(?:")|[^"\n])*"\/g/,"regexp"],[/[#'@^`~]/,"meta"],[/@qualifiedSymbols/,{cases:{"^:.+$":"constant","@specialForms":"keyword","@coreSymbols":"keyword","@constants":"constant","@default":"identifier"}}]],whitespace:[[/[\s,]+/,"white"],[/;.*$/,"comment"],[/\(comment\b/,"comment","@comment"]],comment:[[/\(/,"comment","@push"],[/\)/,"comment","@pop"],[/[^()]/,"comment"]],string:[[/"/,"string","@multiLineString"]],multiLineString:[[/"/,"string","@popall"],[/@escapes/,"string.escape"],[/./,"string"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/coffee/coffee.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/coffee/coffee.js new file mode 100644 index 0000000..7809426 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/coffee/coffee.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/coffee/coffee",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\#%\^\&\*\(\)\=\$\-\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{blockComment:["###","###"],lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*#region\\b"),end:new RegExp("^\\s*#endregion\\b")}}},n.language={defaultToken:"",ignoreCase:!0,tokenPostfix:".coffee",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"}],regEx:/\/(?!\/\/)(?:[^\/\\]|\\.)*\/[igm]*/,keywords:["and","or","is","isnt","not","on","yes","@","no","off","true","false","null","this","new","delete","typeof","in","instanceof","return","throw","break","continue","debugger","if","else","switch","for","while","do","try","catch","finally","class","extends","super","undefined","then","unless","until","loop","of","by","when"],symbols:/[=>"}],keywords:["abstract","amp","array","auto","bool","break","case","catch","char","class","const","constexpr","const_cast","continue","cpu","decltype","default","delegate","delete","do","double","dynamic_cast","each","else","enum","event","explicit","export","extern","false","final","finally","float","for","friend","gcnew","generic","goto","if","in","initonly","inline","int","interface","interior_ptr","internal","literal","long","mutable","namespace","new","noexcept","nullptr","__nullptr","operator","override","partial","pascal","pin_ptr","private","property","protected","public","ref","register","reinterpret_cast","restrict","return","safe_cast","sealed","short","signed","sizeof","static","static_assert","static_cast","struct","switch","template","this","thread_local","throw","tile_static","true","try","typedef","typeid","typename","union","unsigned","using","virtual","void","volatile","wchar_t","where","while","_asm","_based","_cdecl","_declspec","_fastcall","_if_exists","_if_not_exists","_inline","_multiple_inheritance","_pascal","_single_inheritance","_stdcall","_virtual_inheritance","_w64","__abstract","__alignof","__asm","__assume","__based","__box","__builtin_alignof","__cdecl","__clrcall","__declspec","__delegate","__event","__except","__fastcall","__finally","__forceinline","__gc","__hook","__identifier","__if_exists","__if_not_exists","__inline","__int128","__int16","__int32","__int64","__int8","__interface","__leave","__m128","__m128d","__m128i","__m256","__m256d","__m256i","__m64","__multiple_inheritance","__newslot","__nogc","__noop","__nounwind","__novtordisp","__pascal","__pin","__pragma","__property","__ptr32","__ptr64","__raise","__restrict","__resume","__sealed","__single_inheritance","__stdcall","__super","__thiscall","__try","__try_cast","__typeof","__unaligned","__unhook","__uuidof","__value","__virtual_inheritance","__w64","__wchar_t"],operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/,"number.float"],[/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/,"number.hex"],[/0[0-7']*[0-7](@integersuffix)/,"number.octal"],[/0[bB][0-1']*[0-1](@integersuffix)/,"number.binary"],[/\d[\d']*\d(@integersuffix)/,"number"],[/\d(@integersuffix)/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@doccomment"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],doccomment:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],raw:[[/(.*)(\))(?:([^ ()\\\t"]*))(\")/,{cases:{"$3==$S2":["string.raw","string.raw.end","string.raw.end",{token:"string.raw.end",next:"@pop"}],"@default":["string.raw","string.raw","string.raw","string.raw"]}}],[/.*/,"string.raw"]],annotation:[{include:"@whitespace"},[/using|alignas/,"keyword"],[/[a-zA-Z0-9_]+/,"annotation"],[/[,:]/,"delimiter"],[/[()]/,"@brackets"],[/\]\s*\]/,{token:"annotation",next:"@pop"}]],include:[[/(\s*)(<)([^<>]*)(>)/,["","keyword.directive.include.begin","string.include.identifier",{token:"keyword.directive.include.end",next:"@pop"}]],[/(\s*)(")([^"]*)(")/,["","keyword.directive.include.begin","string.include.identifier",{token:"keyword.directive.include.end",next:"@pop"}]]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/csharp/csharp.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/csharp/csharp.js new file mode 100644 index 0000000..06b57d4 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/csharp/csharp.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/csharp/csharp",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],folding:{markers:{start:new RegExp("^\\s*#region\\b"),end:new RegExp("^\\s*#endregion\\b")}}},t.language={defaultToken:"",tokenPostfix:".cs",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],keywords:["extern","alias","using","bool","decimal","sbyte","byte","short","ushort","int","uint","long","ulong","char","float","double","object","dynamic","string","assembly","is","as","ref","out","this","base","new","typeof","void","checked","unchecked","default","delegate","var","const","if","else","switch","case","while","do","for","foreach","in","break","continue","goto","return","throw","try","catch","finally","lock","yield","from","let","where","join","on","equals","into","orderby","ascending","descending","select","group","by","namespace","partial","class","field","event","method","param","public","protected","internal","private","abstract","sealed","static","struct","readonly","volatile","virtual","override","params","get","set","add","remove","operator","true","false","implicit","explicit","interface","enum","null","async","await","fixed","sizeof","stackalloc","unsafe","nameof","when"],namespaceFollows:["namespace","using"],parenFollows:["if","for","while","switch","foreach","using","catch","when"],operators:["=","??","||","&&","|","^","&","==","!=","<=",">=","<<","+","-","*","/","%","!","~","++","--","+=","-=","*=","/=","%=","&=","|=","^=","<<=",">>=",">>","=>"],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/[0-9_]*\.[0-9_]+([eE][\-+]?\d+)?[fFdD]?/,"number.float"],[/0[xX][0-9a-fA-F_]+/,"number.hex"],[/0[bB][01_]+/,"number.hex"],[/[0-9_]+/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,{token:"string.quote",next:"@string"}],[/\$\@"/,{token:"string.quote",next:"@litinterpstring"}],[/\@"/,{token:"string.quote",next:"@litstring"}],[/\$"/,{token:"string.quote",next:"@interpolatedstring"}],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],qualified:[[/[a-zA-Z_][\w]*/,{cases:{"@keywords":{token:"keyword.$0"},"@default":"identifier"}}],[/\./,"delimiter"],["","","@pop"]],namespace:[{include:"@whitespace"},[/[A-Z]\w*/,"namespace"],[/[\.=]/,"delimiter"],["","","@pop"]],comment:[[/[^\/*]+/,"comment"],["\\*/","comment","@pop"],[/[\/*]/,"comment"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,{token:"string.quote",next:"@pop"}]],litstring:[[/[^"]+/,"string"],[/""/,"string.escape"],[/"/,{token:"string.quote",next:"@pop"}]],litinterpstring:[[/[^"{]+/,"string"],[/""/,"string.escape"],[/{{/,"string.escape"],[/}}/,"string.escape"],[/{/,{token:"string.quote",next:"root.litinterpstring"}],[/"/,{token:"string.quote",next:"@pop"}]],interpolatedstring:[[/[^\\"{]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/{{/,"string.escape"],[/}}/,"string.escape"],[/{/,{token:"string.quote",next:"root.interpolatedstring"}],[/"/,{token:"string.quote",next:"@pop"}]],whitespace:[[/^[ \t\v\f]*#((r)|(load))(?=\s)/,"directive.csx"],[/^[ \t\v\f]*#\w.*$/,"namespace.cpp"],[/[ \t\v\f\r\n]+/,""],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/csp/csp.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/csp/csp.js new file mode 100644 index 0000000..05010bd --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/csp/csp.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/csp/csp",["require","exports"],(function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.language=e.conf=void 0,e.conf={brackets:[],autoClosingPairs:[],surroundingPairs:[]},e.language={keywords:[],typeKeywords:[],tokenPostfix:".csp",operators:[],symbols:/[=>",token:"delimiter.angle"}],tokenizer:{root:[{include:"@selector"}],selector:[{include:"@comments"},{include:"@import"},{include:"@strings"},["[@](keyframes|-webkit-keyframes|-moz-keyframes|-o-keyframes)",{token:"keyword",next:"@keyframedeclaration"}],["[@](page|content|font-face|-moz-document)",{token:"keyword"}],["[@](charset|namespace)",{token:"keyword",next:"@declarationbody"}],["(url-prefix)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],["(url)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],{include:"@selectorname"},["[\\*]","tag"],["[>\\+,]","delimiter"],["\\[",{token:"delimiter.bracket",next:"@selectorattribute"}],["{",{token:"delimiter.bracket",next:"@selectorbody"}]],selectorbody:[{include:"@comments"},["[*_]?@identifier@ws:(?=(\\s|\\d|[^{;}]*[;}]))","attribute.name","@rulevalue"],["}",{token:"delimiter.bracket",next:"@pop"}]],selectorname:[["(\\.|#(?=[^{])|%|(@identifier)|:)+","tag"]],selectorattribute:[{include:"@term"},["]",{token:"delimiter.bracket",next:"@pop"}]],term:[{include:"@comments"},["(url-prefix)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],["(url)(\\()",["attribute.value",{token:"delimiter.parenthesis",next:"@urldeclaration"}]],{include:"@functioninvocation"},{include:"@numbers"},{include:"@name"},["([<>=\\+\\-\\*\\/\\^\\|\\~,])","delimiter"],[",","delimiter"]],rulevalue:[{include:"@comments"},{include:"@strings"},{include:"@term"},["!important","keyword"],[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}]],warndebug:[["[@](warn|debug)",{token:"keyword",next:"@declarationbody"}]],import:[["[@](import)",{token:"keyword",next:"@declarationbody"}]],urldeclaration:[{include:"@strings"},["[^)\r\n]+","string"],["\\)",{token:"delimiter.parenthesis",next:"@pop"}]],parenthizedterm:[{include:"@term"},["\\)",{token:"delimiter.parenthesis",next:"@pop"}]],declarationbody:[{include:"@term"},[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}]],comments:[["\\/\\*","comment","@comment"],["\\/\\/+.*","comment"]],comment:[["\\*\\/","comment","@pop"],[/[^*/]+/,"comment"],[/./,"comment"]],name:[["@identifier","attribute.value"]],numbers:[["-?(\\d*\\.)?\\d+([eE][\\-+]?\\d+)?",{token:"attribute.value.number",next:"@units"}],["#[0-9a-fA-F_]+(?!\\w)","attribute.value.hex"]],units:[["(em|ex|ch|rem|vmin|vmax|vw|vh|vm|cm|mm|in|px|pt|pc|deg|grad|rad|turn|s|ms|Hz|kHz|%)?","attribute.value.unit","@pop"]],keyframedeclaration:[["@identifier","attribute.value"],["{",{token:"delimiter.bracket",switchTo:"@keyframebody"}]],keyframebody:[{include:"@term"},["{",{token:"delimiter.bracket",next:"@selectorbody"}],["}",{token:"delimiter.bracket",next:"@pop"}]],functioninvocation:[["@identifier\\(",{token:"attribute.value",next:"@functionarguments"}]],functionarguments:[["\\$@identifier@ws:","attribute.name"],["[,]","delimiter"],{include:"@term"},["\\)",{token:"attribute.value",next:"@pop"}]],strings:[['~?"',{token:"string",next:"@stringenddoublequote"}],["~?'",{token:"string",next:"@stringendquote"}]],stringenddoublequote:[["\\\\.","string"],['"',{token:"string",next:"@pop"}],[/[^\\"]+/,"string"],[".","string"]],stringendquote:[["\\\\.","string"],["'",{token:"string",next:"@pop"}],[/[^\\']+/,"string"],[".","string"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/dart/dart.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/dart/dart.js new file mode 100644 index 0000000..6ad018a --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/dart/dart.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/dart/dart",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]},{open:"`",close:"`",notIn:["string","comment"]},{open:"/**",close:" */",notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"},{open:"(",close:")"},{open:'"',close:'"'},{open:"`",close:"`"}],folding:{markers:{start:/^\s*\s*#?region\b/,end:/^\s*\s*#?endregion\b/}}},n.language={defaultToken:"invalid",tokenPostfix:".dart",keywords:["abstract","dynamic","implements","show","as","else","import","static","assert","enum","in","super","async","export","interface","switch","await","extends","is","sync","break","external","library","this","case","factory","mixin","throw","catch","false","new","true","class","final","null","try","const","finally","on","typedef","continue","for","operator","var","covariant","Function","part","void","default","get","rethrow","while","deferred","hide","return","with","do","if","set","yield"],typeKeywords:["int","double","String","bool"],operators:["+","-","*","/","~/","%","++","--","==","!=",">","<",">=","<=","=","-=","/=","%=",">>=","^=","+=","*=","~/=","<<=","&=","!=","||","&&","&","|","^","~","<<",">>","!",">>>","??","?",":","|="],symbols:/[=>](?!@symbols)/,"@brackets"],[/!(?=([^=]|$))/,"delimiter"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/(@digits)[eE]([\-+]?(@digits))?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/,"number.float"],[/0[xX](@hexdigits)n?/,"number.hex"],[/0[oO]?(@octaldigits)n?/,"number.octal"],[/0[bB](@binarydigits)n?/,"number.binary"],[/(@digits)n?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string_double"],[/'/,"string","@string_single"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@jsdoc"],[/\/\*/,"comment","@comment"],[/\/\/\/.*$/,"comment.doc"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],jsdoc:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],regexp:[[/(\{)(\d+(?:,\d*)?)(\})/,["regexp.escape.control","regexp.escape.control","regexp.escape.control"]],[/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/,["regexp.escape.control",{token:"regexp.escape.control",next:"@regexrange"}]],[/(\()(\?:|\?=|\?!)/,["regexp.escape.control","regexp.escape.control"]],[/[()]/,"regexp.escape.control"],[/@regexpctl/,"regexp.escape.control"],[/[^\\\/]/,"regexp"],[/@regexpesc/,"regexp.escape"],[/\\\./,"regexp.invalid"],[/(\/)([gimsuy]*)/,[{token:"regexp",bracket:"@close",next:"@pop"},"keyword.other"]]],regexrange:[[/-/,"regexp.escape.control"],[/\^/,"regexp.invalid"],[/@regexpesc/,"regexp.escape"],[/[^\]]/,"regexp"],[/\]/,{token:"regexp.escape.control",next:"@pop",bracket:"@close"}]],string_double:[[/[^\\"\$]+/,"string"],[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"],[/\$\w+/,"identifier"]],string_single:[[/[^\\'\$]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/'/,"string","@pop"],[/\$\w+/,"identifier"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/dockerfile/dockerfile.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/dockerfile/dockerfile.js new file mode 100644 index 0000000..2dcc929 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/dockerfile/dockerfile.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/dockerfile/dockerfile",["require","exports"],(function(e,o){"use strict";Object.defineProperty(o,"__esModule",{value:!0}),o.language=o.conf=void 0,o.conf={brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},o.language={defaultToken:"",tokenPostfix:".dockerfile",variable:/\${?[\w]+}?/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},[/(ONBUILD)(\s+)/,["keyword",""]],[/(ENV)(\s+)([\w]+)/,["keyword","",{token:"variable",next:"@arguments"}]],[/(FROM|MAINTAINER|RUN|EXPOSE|ENV|ADD|ARG|VOLUME|LABEL|USER|WORKDIR|COPY|CMD|STOPSIGNAL|SHELL|HEALTHCHECK|ENTRYPOINT)/,{token:"keyword",next:"@arguments"}]],arguments:[{include:"@whitespace"},{include:"@strings"},[/(@variable)/,{cases:{"@eos":{token:"variable",next:"@popall"},"@default":"variable"}}],[/\\/,{cases:{"@eos":"","@default":""}}],[/./,{cases:{"@eos":{token:"",next:"@popall"},"@default":""}}]],whitespace:[[/\s+/,{cases:{"@eos":{token:"",next:"@popall"},"@default":""}}]],comment:[[/(^#.*$)/,"comment","@popall"]],strings:[[/\\'$/,"","@popall"],[/\\'/,""],[/'$/,"string","@popall"],[/'/,"string","@stringBody"],[/"$/,"string","@popall"],[/"/,"string","@dblStringBody"]],stringBody:[[/[^\\\$']/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/\\./,"string.escape"],[/'$/,"string","@popall"],[/'/,"string","@pop"],[/(@variable)/,"variable"],[/\\$/,"string"],[/$/,"string","@popall"]],dblStringBody:[[/[^\\\$"]/,{cases:{"@eos":{token:"string",next:"@popall"},"@default":"string"}}],[/\\./,"string.escape"],[/"$/,"string","@popall"],[/"/,"string","@pop"],[/(@variable)/,"variable"],[/\\$/,"string"],[/$/,"string","@popall"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ecl/ecl.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ecl/ecl.js new file mode 100644 index 0000000..c97e21c --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ecl/ecl.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/ecl/ecl",["require","exports"],(function(e,o){"use strict";Object.defineProperty(o,"__esModule",{value:!0}),o.language=o.conf=void 0,o.conf={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}]},o.language={defaultToken:"",tokenPostfix:".ecl",ignoreCase:!0,brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],pounds:["append","break","declare","demangle","end","for","getdatatype","if","inmodule","loop","mangle","onwarning","option","set","stored","uniquename"].join("|"),keywords:["__compressed__","after","all","and","any","as","atmost","before","beginc","best","between","case","cluster","compressed","compression","const","counter","csv","default","descend","embed","encoding","encrypt","end","endc","endembed","endmacro","enum","escape","except","exclusive","expire","export","extend","fail","few","fileposition","first","flat","forward","from","full","function","functionmacro","group","grouped","heading","hole","ifblock","import","in","inner","interface","internal","joined","keep","keyed","last","left","limit","linkcounted","literal","little_endian","load","local","locale","lookup","lzw","macro","many","maxcount","maxlength","min skew","module","mofn","multiple","named","namespace","nocase","noroot","noscan","nosort","not","noxpath","of","onfail","only","opt","or","outer","overwrite","packed","partition","penalty","physicallength","pipe","prefetch","quote","record","repeat","retry","return","right","right1","right2","rows","rowset","scan","scope","self","separator","service","shared","skew","skip","smart","soapaction","sql","stable","store","terminator","thor","threshold","timelimit","timeout","token","transform","trim","type","unicodeorder","unordered","unsorted","unstable","update","use","validate","virtual","whole","width","wild","within","wnotrim","xml","xpath"],functions:["abs","acos","aggregate","allnodes","apply","ascii","asin","assert","asstring","atan","atan2","ave","build","buildindex","case","catch","choose","choosen","choosesets","clustersize","combine","correlation","cos","cosh","count","covariance","cron","dataset","dedup","define","denormalize","dictionary","distribute","distributed","distribution","ebcdic","enth","error","evaluate","event","eventextra","eventname","exists","exp","fail","failcode","failmessage","fetch","fromunicode","fromxml","getenv","getisvalid","global","graph","group","hash","hash32","hash64","hashcrc","hashmd5","having","httpcall","httpheader","if","iff","index","intformat","isvalid","iterate","join","keydiff","keypatch","keyunicode","length","library","limit","ln","loadxml","local","log","loop","map","matched","matchlength","matchposition","matchtext","matchunicode","max","merge","mergejoin","min","nofold","nolocal","nonempty","normalize","nothor","notify","output","parallel","parse","pipe","power","preload","process","project","pull","random","range","rank","ranked","realformat","recordof","regexfind","regexreplace","regroup","rejected","rollup","round","roundup","row","rowdiff","sample","sequential","set","sin","sinh","sizeof","soapcall","sort","sorted","sqrt","stepped","stored","sum","table","tan","tanh","thisnode","topn","tounicode","toxml","transfer","transform","trim","truncate","typeof","ungroup","unicodeorder","variance","wait","which","workunit","xmldecode","xmlencode","xmltext","xmlunicode"],typesint:["integer","unsigned"].join("|"),typesnum:["data","qstring","string","unicode","utf8","varstring","varunicode"],typesone:["ascii","big_endian","boolean","data","decimal","ebcdic","grouped","integer","linkcounted","pattern","qstring","real","record","rule","set of","streamed","string","token","udecimal","unicode","unsigned","utf8","varstring","varunicode"].join("|"),operators:["+","-","/",":=","<","<>","=",">","\\","and","in","not","or"],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/[0-9_]*\.[0-9_]+([eE][\-+]?\d+)?/,"number.float"],[/0[xX][0-9a-fA-F_]+/,"number.hex"],[/0[bB][01]+/,"number.hex"],[/[0-9_]+/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\v\f\r\n]+/,""],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],string:[[/[^\\']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/'/,"string","@pop"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/fsharp/fsharp.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/fsharp/fsharp.js new file mode 100644 index 0000000..b4cdd38 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/fsharp/fsharp.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/fsharp/fsharp",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"//",blockComment:["(*","*)"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*//\\s*#region\\b|^\\s*\\(\\*\\s*#region(.*)\\*\\)"),end:new RegExp("^\\s*//\\s*#endregion\\b|^\\s*\\(\\*\\s*#endregion\\s*\\*\\)")}}},n.language={defaultToken:"",tokenPostfix:".fs",keywords:["abstract","and","atomic","as","assert","asr","base","begin","break","checked","component","const","constraint","constructor","continue","class","default","delegate","do","done","downcast","downto","elif","else","end","exception","eager","event","external","extern","false","finally","for","fun","function","fixed","functor","global","if","in","include","inherit","inline","interface","internal","land","lor","lsl","lsr","lxor","lazy","let","match","member","mod","module","mutable","namespace","method","mixin","new","not","null","of","open","or","object","override","private","parallel","process","protected","pure","public","rec","return","static","sealed","struct","sig","then","to","true","tailcall","trait","try","type","upcast","use","val","void","virtual","volatile","when","while","with","yield"],symbols:/[=>\]/,"annotation"],[/^#(if|else|endif)/,"keyword"],[/[{}()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/@symbols/,"delimiter"],[/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/,"number.float"],[/0x[0-9a-fA-F]+LF/,"number.float"],[/0x[0-9a-fA-F]+(@integersuffix)/,"number.hex"],[/0b[0-1]+(@integersuffix)/,"number.bin"],[/\d+(@integersuffix)/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"""/,"string",'@string."""'],[/"/,"string",'@string."'],[/\@"/,{token:"string.quote",next:"@litstring"}],[/'[^\\']'B?/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\(\*(?!\))/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^*(]+/,"comment"],[/\*\)/,"comment","@pop"],[/\*/,"comment"],[/\(\*\)/,"comment"],[/\(/,"comment"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/("""|"B?)/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}]],litstring:[[/[^"]+/,"string"],[/""/,"string.escape"],[/"/,{token:"string.quote",next:"@pop"}]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/go/go.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/go/go.js new file mode 100644 index 0000000..bedb5be --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/go/go.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/go/go",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"`",close:"`",notIn:["string"]},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"`",close:"`"},{open:'"',close:'"'},{open:"'",close:"'"}]},n.language={defaultToken:"",tokenPostfix:".go",keywords:["break","case","chan","const","continue","default","defer","else","fallthrough","for","func","go","goto","if","import","interface","map","package","range","return","select","struct","switch","type","var","bool","true","false","uint8","uint16","uint32","uint64","int8","int16","int32","int64","float32","float64","complex64","complex128","byte","rune","uint","int","uintptr","string","nil"],operators:["+","-","*","/","%","&","|","^","<<",">>","&^","+=","-=","*=","/=","%=","&=","|=","^=","<<=",">>=","&^=","&&","||","<-","++","--","==","<",">","=","!","!=","<=",">=",":=","...","(",")","","]","{","}",",",";",".",":"],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\d+[eE]([\-+]?\d+)?/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/0[xX][0-9a-fA-F']*[0-9a-fA-F]/,"number.hex"],[/0[0-7']*[0-7]/,"number.octal"],[/0[bB][0-1']*[0-1]/,"number.binary"],[/\d[\d']*/,"number"],[/\d/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"],[/`/,"string","@rawstring"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@doccomment"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],doccomment:[[/[^\/*]+/,"comment.doc"],[/\/\*/,"comment.doc.invalid"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],rawstring:[[/[^\`]/,"string"],[/`/,"string","@pop"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/graphql/graphql.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/graphql/graphql.js new file mode 100644 index 0000000..dfcd6d0 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/graphql/graphql.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/graphql/graphql",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"""',close:'"""',notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"""',close:'"""'},{open:'"',close:'"'}],folding:{offSide:!0}},n.language={defaultToken:"invalid",tokenPostfix:".gql",keywords:["null","true","false","query","mutation","subscription","extend","schema","directive","scalar","type","interface","union","enum","input","implements","fragment","on"],typeKeywords:["Int","Float","String","Boolean","ID"],directiveLocations:["SCHEMA","SCALAR","OBJECT","FIELD_DEFINITION","ARGUMENT_DEFINITION","INTERFACE","UNION","ENUM","ENUM_VALUE","INPUT_OBJECT","INPUT_FIELD_DEFINITION","QUERY","MUTATION","SUBSCRIPTION","FIELD","FRAGMENT_DEFINITION","FRAGMENT_SPREAD","INLINE_FRAGMENT","VARIABLE_DEFINITION"],operators:["=","!","?",":","&","|"],symbols:/[=!?:&|]+/,escapes:/\\(?:["\\\/bfnrt]|u[0-9A-Fa-f]{4})/,tokenizer:{root:[[/[a-z_][\w$]*/,{cases:{"@keywords":"keyword","@default":"key.identifier"}}],[/[$][\w$]*/,{cases:{"@keywords":"keyword","@default":"argument.identifier"}}],[/[A-Z][\w\$]*/,{cases:{"@typeKeywords":"keyword","@default":"type.identifier"}}],{include:"@whitespace"},[/[{}()\[\]]/,"@brackets"],[/@symbols/,{cases:{"@operators":"operator","@default":""}}],[/@\s*[a-zA-Z_\$][\w\$]*/,{token:"annotation",log:"annotation token: $0"}],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/0[xX][0-9a-fA-F]+/,"number.hex"],[/\d+/,"number"],[/[;,.]/,"delimiter"],[/"""/,{token:"string",next:"@mlstring",nextEmbedded:"markdown"}],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,{token:"string.quote",bracket:"@open",next:"@string"}]],mlstring:[[/[^"]+/,"string"],['"""',{token:"string",next:"@pop",nextEmbedded:"@pop"}]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,{token:"string.quote",bracket:"@close",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,""],[/#.*$/,"comment"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/handlebars/handlebars.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/handlebars/handlebars.js new file mode 100644 index 0000000..3225fb9 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/handlebars/handlebars.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/handlebars/handlebars",["require","exports","../fillers/monaco-editor-core"],(function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0;var a=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"];t.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["{{!--","--}}"]},brackets:[["\x3c!--","--\x3e"],["<",">"],["{{","}}"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"<",close:">"},{open:'"',close:'"'},{open:"'",close:"'"}],onEnterRules:[{beforeText:new RegExp("<(?!(?:"+a.join("|")+"))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),afterText:/^<\/(\w[\w\d]*)\s*>$/i,action:{indentAction:n.languages.IndentAction.IndentOutdent}},{beforeText:new RegExp("<(?!(?:"+a.join("|")+"))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),action:{indentAction:n.languages.IndentAction.Indent}}]},t.language={defaultToken:"",tokenPostfix:"",tokenizer:{root:[[/\{\{!--/,"comment.block.start.handlebars","@commentBlock"],[/\{\{!/,"comment.start.handlebars","@comment"],[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.root"}],[/)/,["delimiter.html","tag.html","delimiter.html"]],[/(<)(script)/,["delimiter.html",{token:"tag.html",next:"@script"}]],[/(<)(style)/,["delimiter.html",{token:"tag.html",next:"@style"}]],[/(<)([:\w]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/(<\/)(\w+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/]+/,"metatag.content.html"],[/>/,"metatag.html","@pop"]],comment:[[/\}\}/,"comment.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentBlock:[[/--\}\}/,"comment.block.end.handlebars","@pop"],[/./,"comment.content.handlebars"]],commentHtml:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.comment"}],[/-->/,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/\{\{/,{token:"@rematch",switchTo:"@handlebarsInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],handlebarsInSimpleState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3"}],{include:"handlebarsRoot"}],handlebarsInEmbeddedState:[[/\{\{\{?/,"delimiter.handlebars"],[/\}\}\}?/,{token:"delimiter.handlebars",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],{include:"handlebarsRoot"}],handlebarsRoot:[[/"[^"]*"/,"string.handlebars"],[/[#/][^\s}]+/,"keyword.helper.handlebars"],[/else\b/,"keyword.helper.handlebars"],[/[\s]+/],[/[^}]/,"variable.parameter.handlebars"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/hcl/hcl.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/hcl/hcl.js new file mode 100644 index 0000000..4859378 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/hcl/hcl.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/hcl/hcl",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"',notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}]},t.language={defaultToken:"",tokenPostfix:".hcl",keywords:["var","local","path","for_each","any","string","number","bool","true","false","null","if ","else ","endif ","for ","in","endfor"],operators:["=",">=","<=","==","!=","+","-","*","/","%","&&","||","!","<",">","?","...",":"],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"operator","@default":""}}],[/\d*\d+[eE]([\-+]?\d+)?/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/\d[\d']*/,"number"],[/\d/,"number"],[/[;,.]/,"delimiter"],[/"/,"string","@string"],[/'/,"invalid"]],heredoc:[[/<<[-]*\s*["]?([\w\-]+)["]?/,{token:"string.heredoc.delimiter",next:"@heredocBody.$1"}]],heredocBody:[[/([\w\-]+)$/,{cases:{"$1==$S2":[{token:"string.heredoc.delimiter",next:"@popall"}],"@default":"string.heredoc"}}],[/./,"string.heredoc"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"],[/#.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],string:[[/\$\{/,{token:"delimiter",next:"@stringExpression"}],[/[^\\"\$]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@popall"]],stringInsideExpression:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],stringExpression:[[/\}/,{token:"delimiter",next:"@pop"}],[/"/,"string","@stringInsideExpression"],{include:"@terraform"}]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/html/html.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/html/html.js new file mode 100644 index 0000000..32ac8c8 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/html/html.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/html/html",["require","exports","../fillers/monaco-editor-core"],(function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0;var i=["area","base","br","col","embed","hr","img","input","keygen","link","menuitem","meta","param","source","track","wbr"];t.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\$\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\s]+)/g,comments:{blockComment:["\x3c!--","--\x3e"]},brackets:[["\x3c!--","--\x3e"],["<",">"],["{","}"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">"}],onEnterRules:[{beforeText:new RegExp("<(?!(?:"+i.join("|")+"))([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$","i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:n.languages.IndentAction.IndentOutdent}},{beforeText:new RegExp("<(?!(?:"+i.join("|")+"))(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),action:{indentAction:n.languages.IndentAction.Indent}}],folding:{markers:{start:new RegExp("^\\s*\x3c!--\\s*#region\\b.*--\x3e"),end:new RegExp("^\\s*\x3c!--\\s*#endregion\\b.*--\x3e")}}},t.language={defaultToken:"",tokenPostfix:".html",ignoreCase:!0,tokenizer:{root:[[/)/,["delimiter","tag","","delimiter"]],[/(<)(script)/,["delimiter",{token:"tag",next:"@script"}]],[/(<)(style)/,["delimiter",{token:"tag",next:"@style"}]],[/(<)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/(<\/)((?:[\w\-]+:)?[\w\-]+)/,["delimiter",{token:"tag",next:"@otherTag"}]],[/]+/,"metatag.content"],[/>/,"metatag","@pop"]],comment:[[/-->/,"comment","@pop"],[/[^-]+/,"comment.content"],[/./,"comment.content"]],otherTag:[[/\/?>/,"delimiter","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],scriptAfterType:[[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/>/,{token:"delimiter",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]],style:[[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter","tag",{token:"delimiter",next:"@pop"}]]],styleAfterType:[[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/>/,{token:"delimiter",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ini/ini.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ini/ini.js new file mode 100644 index 0000000..8f19f7e --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ini/ini.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/ini/ini",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},n.language={defaultToken:"",tokenPostfix:".ini",escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/^\[[^\]]*\]/,"metatag"],[/(^\w+)(\s*)(\=)/,["key","","delimiter"]],{include:"@whitespace"},[/\d+/,"number"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string",'@string."'],[/'/,"string","@string.'"]],whitespace:[[/[ \t\r\n]+/,""],[/^\s*[#;].*$/,"comment"]],string:[[/[^\\"']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/["']/,{cases:{"$#==$S2":{token:"string",next:"@pop"},"@default":"string"}}]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/java/java.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/java/java.js new file mode 100644 index 0000000..83e516e --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/java/java.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/java/java",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"<",close:">"}],folding:{markers:{start:new RegExp("^\\s*//\\s*(?:(?:#?region\\b)|(?:))")}}},t.language={defaultToken:"",tokenPostfix:".java",keywords:["abstract","continue","for","new","switch","assert","default","goto","package","synchronized","boolean","do","if","private","this","break","double","implements","protected","throw","byte","else","import","public","throws","case","enum","instanceof","return","transient","catch","extends","int","short","try","char","final","interface","static","void","class","finally","long","strictfp","volatile","const","float","native","super","while","true","false"],operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/@\s*[a-zA-Z_\$][\w\$]*/,"annotation"],[/(@digits)[eE]([\-+]?(@digits))?[fFdD]?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?[fFdD]?/,"number.float"],[/0[xX](@hexdigits)[Ll]?/,"number.hex"],[/0(@octaldigits)[Ll]?/,"number.octal"],[/0[bB](@binarydigits)[Ll]?/,"number.binary"],[/(@digits)[fFdD]/,"number.float"],[/(@digits)[lL]?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@javadoc"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],javadoc:[[/[^\/*]+/,"comment.doc"],[/\/\*/,"comment.doc.invalid"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/javascript/javascript.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/javascript/javascript.js new file mode 100644 index 0000000..66dc825 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/javascript/javascript.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/typescript/typescript",["require","exports","../fillers/monaco-editor-core"],(function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],onEnterRules:[{beforeText:/^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,afterText:/^\s*\*\/$/,action:{indentAction:n.languages.IndentAction.IndentOutdent,appendText:" * "}},{beforeText:/^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,action:{indentAction:n.languages.IndentAction.None,appendText:" * "}},{beforeText:/^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,action:{indentAction:n.languages.IndentAction.None,appendText:"* "}},{beforeText:/^(\t|(\ \ ))*\ \*\/\s*$/,action:{indentAction:n.languages.IndentAction.None,removeText:1}}],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string","comment"]},{open:"`",close:"`",notIn:["string","comment"]},{open:"/**",close:" */",notIn:["string"]}],folding:{markers:{start:new RegExp("^\\s*//\\s*#?region\\b"),end:new RegExp("^\\s*//\\s*#?endregion\\b")}}},t.language={defaultToken:"invalid",tokenPostfix:".ts",keywords:["abstract","any","as","asserts","bigint","boolean","break","case","catch","class","continue","const","constructor","debugger","declare","default","delete","do","else","enum","export","extends","false","finally","for","from","function","get","if","implements","import","in","infer","instanceof","interface","is","keyof","let","module","namespace","never","new","null","number","object","package","private","protected","public","readonly","require","global","return","set","static","string","super","switch","symbol","this","throw","true","try","type","typeof","undefined","unique","unknown","var","void","while","with","yield","async","await","of"],operators:["<=",">=","==","!=","===","!==","=>","+","-","**","*","/","%","++","--","<<",">",">>>","&","|","^","!","~","&&","||","??","?",":","=","+=","-=","*=","**=","/=","%=","<<=",">>=",">>>=","&=","|=","^=","@"],symbols:/[=>](?!@symbols)/,"@brackets"],[/!(?=([^=]|$))/,"delimiter"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/(@digits)[eE]([\-+]?(@digits))?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/,"number.float"],[/0[xX](@hexdigits)n?/,"number.hex"],[/0[oO]?(@octaldigits)n?/,"number.octal"],[/0[bB](@binarydigits)n?/,"number.binary"],[/(@digits)n?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string_double"],[/'/,"string","@string_single"],[/`/,"string","@string_backtick"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@jsdoc"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],jsdoc:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],regexp:[[/(\{)(\d+(?:,\d*)?)(\})/,["regexp.escape.control","regexp.escape.control","regexp.escape.control"]],[/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/,["regexp.escape.control",{token:"regexp.escape.control",next:"@regexrange"}]],[/(\()(\?:|\?=|\?!)/,["regexp.escape.control","regexp.escape.control"]],[/[()]/,"regexp.escape.control"],[/@regexpctl/,"regexp.escape.control"],[/[^\\\/]/,"regexp"],[/@regexpesc/,"regexp.escape"],[/\\\./,"regexp.invalid"],[/(\/)([gimsuy]*)/,[{token:"regexp",bracket:"@close",next:"@pop"},"keyword.other"]]],regexrange:[[/-/,"regexp.escape.control"],[/\^/,"regexp.invalid"],[/@regexpesc/,"regexp.escape"],[/[^\]]/,"regexp"],[/\]/,{token:"regexp.escape.control",next:"@pop",bracket:"@close"}]],string_double:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],string_single:[[/[^\\']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/'/,"string","@pop"]],string_backtick:[[/\$\{/,{token:"delimiter.bracket",next:"@bracketCounting"}],[/[^\\`$]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/`/,"string","@pop"]],bracketCounting:[[/\{/,"delimiter.bracket","@bracketCounting"],[/\}/,"delimiter.bracket","@pop"],{include:"common"}]}}})),define("vs/basic-languages/javascript/javascript",["require","exports","../typescript/typescript"],(function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf=n.conf,t.language={defaultToken:"invalid",tokenPostfix:".js",keywords:["break","case","catch","class","continue","const","constructor","debugger","default","delete","do","else","export","extends","false","finally","for","from","function","get","if","import","in","instanceof","let","new","null","return","set","super","switch","symbol","this","throw","true","try","typeof","undefined","var","void","while","with","yield","async","await","of"],typeKeywords:[],operators:n.language.operators,symbols:n.language.symbols,escapes:n.language.escapes,digits:n.language.digits,octaldigits:n.language.octaldigits,binarydigits:n.language.binarydigits,hexdigits:n.language.hexdigits,regexpctl:n.language.regexpctl,regexpesc:n.language.regexpesc,tokenizer:n.language.tokenizer}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/julia/julia.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/julia/julia.js new file mode 100644 index 0000000..3d27f05 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/julia/julia.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/julia/julia",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},t.language={tokenPostfix:".julia",keywords:["begin","while","if","for","try","return","break","continue","function","macro","quote","let","local","global","const","do","struct","module","baremodule","using","import","export","end","else","elseif","catch","finally","mutable","primitive","abstract","type","in","isa","where","new"],types:["LinRange","LineNumberNode","LinearIndices","LoadError","MIME","Matrix","Method","MethodError","Missing","MissingException","Module","NTuple","NamedTuple","Nothing","Number","OrdinalRange","OutOfMemoryError","OverflowError","Pair","PartialQuickSort","PermutedDimsArray","Pipe","Ptr","QuoteNode","Rational","RawFD","ReadOnlyMemoryError","Real","ReentrantLock","Ref","Regex","RegexMatch","RoundingMode","SegmentationFault","Set","Signed","Some","StackOverflowError","StepRange","StepRangeLen","StridedArray","StridedMatrix","StridedVecOrMat","StridedVector","String","StringIndexError","SubArray","SubString","SubstitutionString","Symbol","SystemError","Task","Text","TextDisplay","Timer","Tuple","Type","TypeError","TypeVar","UInt","UInt128","UInt16","UInt32","UInt64","UInt8","UndefInitializer","AbstractArray","UndefKeywordError","AbstractChannel","UndefRefError","AbstractChar","UndefVarError","AbstractDict","Union","AbstractDisplay","UnionAll","AbstractFloat","UnitRange","AbstractIrrational","Unsigned","AbstractMatrix","AbstractRange","Val","AbstractSet","Vararg","AbstractString","VecElement","AbstractUnitRange","VecOrMat","AbstractVecOrMat","Vector","AbstractVector","VersionNumber","Any","WeakKeyDict","ArgumentError","WeakRef","Array","AssertionError","BigFloat","BigInt","BitArray","BitMatrix","BitSet","BitVector","Bool","BoundsError","CapturedException","CartesianIndex","CartesianIndices","Cchar","Cdouble","Cfloat","Channel","Char","Cint","Cintmax_t","Clong","Clonglong","Cmd","Colon","Complex","ComplexF16","ComplexF32","ComplexF64","CompositeException","Condition","Cptrdiff_t","Cshort","Csize_t","Cssize_t","Cstring","Cuchar","Cuint","Cuintmax_t","Culong","Culonglong","Cushort","Cvoid","Cwchar_t","Cwstring","DataType","DenseArray","DenseMatrix","DenseVecOrMat","DenseVector","Dict","DimensionMismatch","Dims","DivideError","DomainError","EOFError","Enum","ErrorException","Exception","ExponentialBackOff","Expr","Float16","Float32","Float64","Function","GlobalRef","HTML","IO","IOBuffer","IOContext","IOStream","IdDict","IndexCartesian","IndexLinear","IndexStyle","InexactError","InitError","Int","Int128","Int16","Int32","Int64","Int8","Integer","InterruptException","InvalidStateException","Irrational","KeyError"],keywordops:["<:",">:",":","=>","...",".","->","?"],allops:/[^\w\d\s()\[\]{}"'#]+/,constants:["true","false","nothing","missing","undef","Inf","pi","NaN","π","ℯ","ans","PROGRAM_FILE","ARGS","C_NULL","VERSION","DEPOT_PATH","LOAD_PATH"],operators:["!","!=","!==","%","&","*","+","-","/","//","<","<<","<=","==","===","=>",">",">=",">>",">>>","\\","^","|","|>","~","÷","∈","∉","∋","∌","∘","√","∛","∩","∪","≈","≉","≠","≡","≢","≤","≥","⊆","⊇","⊈","⊉","⊊","⊋","⊻"],brackets:[{open:"(",close:")",token:"delimiter.parenthesis"},{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"}],ident:/π|ℯ|\b(?!\d)\w+\b/,escape:/(?:[abefnrstv\\"'\n\r]|[0-7]{1,3}|x[0-9A-Fa-f]{1,2}|u[0-9A-Fa-f]{4})/,escapes:/\\(?:C\-(@escape|.)|c(@escape|.)|@escape)/,tokenizer:{root:[[/(::)\s*|\b(isa)\s+/,"keyword","@typeanno"],[/\b(isa)(\s*\(@ident\s*,\s*)/,["keyword",{token:"",next:"@typeanno"}]],[/\b(type|struct)[ \t]+/,"keyword","@typeanno"],[/^\s*:@ident[!?]?/,"metatag"],[/(return)(\s*:@ident[!?]?)/,["keyword","metatag"]],[/(\(|\[|\{|@allops)(\s*:@ident[!?]?)/,["","metatag"]],[/:\(/,"metatag","@quote"],[/r"""/,"regexp.delim","@tregexp"],[/r"/,"regexp.delim","@sregexp"],[/raw"""/,"string.delim","@rtstring"],[/[bv]?"""/,"string.delim","@dtstring"],[/raw"/,"string.delim","@rsstring"],[/[bv]?"/,"string.delim","@dsstring"],[/(@ident)\{/,{cases:{"$1@types":{token:"type",next:"@gen"},"@default":{token:"type",next:"@gen"}}}],[/@ident[!?'']?(?=\.?\()/,{cases:{"@types":"type","@keywords":"keyword","@constants":"variable","@default":"keyword.flow"}}],[/@ident[!?']?/,{cases:{"@types":"type","@keywords":"keyword","@constants":"variable","@default":"identifier"}}],[/\$\w+/,"key"],[/\$\(/,"key","@paste"],[/@@ident/,"annotation"],{include:"@whitespace"},[/'(?:@escapes|.)'/,"string.character"],[/[()\[\]{}]/,"@brackets"],[/@allops/,{cases:{"@keywordops":"keyword","@operators":"operator"}}],[/[;,]/,"delimiter"],[/0[xX][0-9a-fA-F](_?[0-9a-fA-F])*/,"number.hex"],[/0[_oO][0-7](_?[0-7])*/,"number.octal"],[/0[bB][01](_?[01])*/,"number.binary"],[/[+\-]?\d+(\.\d+)?(im?|[eE][+\-]?\d+(\.\d+)?)?/,"number"]],typeanno:[[/[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*\{/,"type","@gen"],[/([a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*)(\s*<:\s*)/,["type","keyword"]],[/[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*/,"type","@pop"],["","","@pop"]],gen:[[/[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*\{/,"type","@push"],[/[a-zA-Z_]\w*(?:\.[a-zA-Z_]\w*)*/,"type"],[/<:/,"keyword"],[/(\})(\s*<:\s*)/,["type",{token:"keyword",next:"@pop"}]],[/\}/,"type","@pop"],{include:"@root"}],quote:[[/\$\(/,"key","@paste"],[/\(/,"@brackets","@paren"],[/\)/,"metatag","@pop"],{include:"@root"}],paste:[[/:\(/,"metatag","@quote"],[/\(/,"@brackets","@paren"],[/\)/,"key","@pop"],{include:"@root"}],paren:[[/\$\(/,"key","@paste"],[/:\(/,"metatag","@quote"],[/\(/,"@brackets","@push"],[/\)/,"@brackets","@pop"],{include:"@root"}],sregexp:[[/^.*/,"invalid"],[/[^\\"()\[\]{}]/,"regexp"],[/[()\[\]{}]/,"@brackets"],[/\\./,"operator.scss"],[/"[imsx]*/,"regexp.delim","@pop"]],tregexp:[[/[^\\"()\[\]{}]/,"regexp"],[/[()\[\]{}]/,"@brackets"],[/\\./,"operator.scss"],[/"(?!"")/,"string"],[/"""[imsx]*/,"regexp.delim","@pop"]],rsstring:[[/^.*/,"invalid"],[/[^\\"]/,"string"],[/\\./,"string.escape"],[/"/,"string.delim","@pop"]],rtstring:[[/[^\\"]/,"string"],[/\\./,"string.escape"],[/"(?!"")/,"string"],[/"""/,"string.delim","@pop"]],dsstring:[[/^.*/,"invalid"],[/[^\\"\$]/,"string"],[/\$/,"","@interpolated"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string.delim","@pop"]],dtstring:[[/[^\\"\$]/,"string"],[/\$/,"","@interpolated"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"(?!"")/,"string"],[/"""/,"string.delim","@pop"]],interpolated:[[/\(/,{token:"",switchTo:"@interpolated_compound"}],[/[a-zA-Z_]\w*/,"identifier"],["","","@pop"]],interpolated_compound:[[/\)/,"","@pop"],{include:"@root"}],whitespace:[[/[ \t\r\n]+/,""],[/#=/,"comment","@multi_comment"],[/#.*$/,"comment"]],multi_comment:[[/#=/,"comment","@push"],[/=#/,"comment","@pop"],[/=(?!#)|#(?!=)/,"comment"],[/[^#=]+/,"comment"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/kotlin/kotlin.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/kotlin/kotlin.js new file mode 100644 index 0000000..f256842 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/kotlin/kotlin.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/kotlin/kotlin",["require","exports"],(function(e,i){"use strict";Object.defineProperty(i,"__esModule",{value:!0}),i.language=i.conf=void 0,i.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"<",close:">"}],folding:{markers:{start:new RegExp("^\\s*//\\s*(?:(?:#?region\\b)|(?:))")}}},i.language={defaultToken:"",tokenPostfix:".kt",keywords:["as","as?","break","class","continue","do","else","false","for","fun","if","in","!in","interface","is","!is","null","object","package","return","super","this","throw","true","try","typealias","val","var","when","while","by","catch","constructor","delegate","dynamic","field","file","finally","get","import","init","param","property","receiver","set","setparam","where","actual","abstract","annotation","companion","const","crossinline","data","enum","expect","external","final","infix","inline","inner","internal","lateinit","noinline","open","operator","out","override","private","protected","public","reified","sealed","suspend","tailrec","vararg","field","it"],operators:["+","-","*","/","%","=","+=","-=","*=","/=","%=","++","--","&&","||","!","==","!=","===","!==",">","<","<=",">=","[","]","!!","?.","?:","::","..",":","?","->","@",";","$","_"],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/@\s*[a-zA-Z_\$][\w\$]*/,"annotation"],[/(@digits)[eE]([\-+]?(@digits))?[fFdD]?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?[fFdD]?/,"number.float"],[/0[xX](@hexdigits)[Ll]?/,"number.hex"],[/0(@octaldigits)[Ll]?/,"number.octal"],[/0[bB](@binarydigits)[Ll]?/,"number.binary"],[/(@digits)[fFdD]/,"number.float"],[/(@digits)[lL]?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"""/,"string","@multistring"],[/"/,"string","@string"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@javadoc"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\/\*/,"comment","@comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],javadoc:[[/[^\/*]+/,"comment.doc"],[/\/\*/,"comment.doc","@push"],[/\/\*/,"comment.doc.invalid"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],multistring:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"""/,"string","@pop"],[/./,"string"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/less/less.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/less/less.js new file mode 100644 index 0000000..edcf04d --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/less/less.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/less/less",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={wordPattern:/(#?-?\d*\.\d\w*%?)|([@#!.:]?[\w-?]+%?)|[@#!.]/g,comments:{blockComment:["/*","*/"],lineComment:"//"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]},{open:"'",close:"'",notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/"),end:new RegExp("^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/")}}},t.language={defaultToken:"",tokenPostfix:".less",identifier:"-?-?([a-zA-Z]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))([\\w\\-]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))*",identifierPlus:"-?-?([a-zA-Z:.]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))([\\w\\-:.]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))*",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.bracket"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],tokenizer:{root:[{include:"@nestedJSBegin"},["[ \\t\\r\\n]+",""],{include:"@comments"},{include:"@keyword"},{include:"@strings"},{include:"@numbers"},["[*_]?[a-zA-Z\\-\\s]+(?=:.*(;|(\\\\$)))","attribute.name","@attribute"],["url(\\-prefix)?\\(",{token:"tag",next:"@urldeclaration"}],["[{}()\\[\\]]","@brackets"],["[,:;]","delimiter"],["#@identifierPlus","tag.id"],["&","tag"],["\\.@identifierPlus(?=\\()","tag.class","@attribute"],["\\.@identifierPlus","tag.class"],["@identifierPlus","tag"],{include:"@operators"},["@(@identifier(?=[:,\\)]))","variable","@attribute"],["@(@identifier)","variable"],["@","key","@atRules"]],nestedJSBegin:[["``","delimiter.backtick"],["`",{token:"delimiter.backtick",next:"@nestedJSEnd",nextEmbedded:"text/javascript"}]],nestedJSEnd:[["`",{token:"delimiter.backtick",next:"@pop",nextEmbedded:"@pop"}]],operators:[["[<>=\\+\\-\\*\\/\\^\\|\\~]","operator"]],keyword:[["(@[\\s]*import|![\\s]*important|true|false|when|iscolor|isnumber|isstring|iskeyword|isurl|ispixel|ispercentage|isem|hue|saturation|lightness|alpha|lighten|darken|saturate|desaturate|fadein|fadeout|fade|spin|mix|round|ceil|floor|percentage)\\b","keyword"]],urldeclaration:[{include:"@strings"},["[^)\r\n]+","string"],["\\)",{token:"tag",next:"@pop"}]],attribute:[{include:"@nestedJSBegin"},{include:"@comments"},{include:"@strings"},{include:"@numbers"},{include:"@keyword"},["[a-zA-Z\\-]+(?=\\()","attribute.value","@attribute"],[">","operator","@pop"],["@identifier","attribute.value"],{include:"@operators"},["@(@identifier)","variable"],["[)\\}]","@brackets","@pop"],["[{}()\\[\\]>]","@brackets"],["[;]","delimiter","@pop"],["[,=:]","delimiter"],["\\s",""],[".","attribute.value"]],comments:[["\\/\\*","comment","@comment"],["\\/\\/+.*","comment"]],comment:[["\\*\\/","comment","@pop"],[".","comment"]],numbers:[["(\\d*\\.)?\\d+([eE][\\-+]?\\d+)?",{token:"attribute.value.number",next:"@units"}],["#[0-9a-fA-F_]+(?!\\w)","attribute.value.hex"]],units:[["(em|ex|ch|rem|vmin|vmax|vw|vh|vm|cm|mm|in|px|pt|pc|deg|grad|rad|turn|s|ms|Hz|kHz|%)?","attribute.value.unit","@pop"]],strings:[['~?"',{token:"string.delimiter",next:"@stringsEndDoubleQuote"}],["~?'",{token:"string.delimiter",next:"@stringsEndQuote"}]],stringsEndDoubleQuote:[['\\\\"',"string"],['"',{token:"string.delimiter",next:"@popall"}],[".","string"]],stringsEndQuote:[["\\\\'","string"],["'",{token:"string.delimiter",next:"@popall"}],[".","string"]],atRules:[{include:"@comments"},{include:"@strings"},["[()]","delimiter"],["[\\{;]","delimiter","@pop"],[".","key"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/lexon/lexon.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/lexon/lexon.js new file mode 100644 index 0000000..f52f8ec --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/lexon/lexon.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/lexon/lexon",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"COMMENT"},brackets:[["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:":",close:"."}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"`",close:"`"},{open:'"',close:'"'},{open:"'",close:"'"},{open:":",close:"."}],folding:{markers:{start:new RegExp("^\\s*(::\\s*|COMMENT\\s+)#region"),end:new RegExp("^\\s*(::\\s*|COMMENT\\s+)#endregion")}}},t.language={tokenPostfix:".lexon",ignoreCase:!0,keywords:["lexon","lex","clause","terms","contracts","may","pay","pays","appoints","into","to"],typeKeywords:["amount","person","key","time","date","asset","text"],operators:["less","greater","equal","le","gt","or","and","add","added","subtract","subtracted","multiply","multiplied","times","divide","divided","is","be","certified"],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,"delimiter"],[/\d*\.\d*\.\d*/,"number.semver"],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/0[xX][0-9a-fA-F]+/,"number.hex"],[/\d+/,"number"],[/[;,.]/,"delimiter"]],quoted_identifier:[[/[^\\"]+/,"identifier"],[/"/,{token:"identifier.quote",bracket:"@close",next:"@pop"}]],space_identifier_until_period:[[":","delimiter"],[" ",{token:"white",next:"@identifier_rest"}]],identifier_until_period:[{include:"@whitespace"},[":",{token:"delimiter",next:"@identifier_rest"}],[/[^\\.]+/,"identifier"],[/\./,{token:"delimiter",bracket:"@close",next:"@pop"}]],identifier_rest:[[/[^\\.]+/,"identifier"],[/\./,{token:"delimiter",bracket:"@close",next:"@pop"}]],semver:[{include:"@whitespace"},[":","delimiter"],[/\d*\.\d*\.\d*/,{token:"number.semver",bracket:"@close",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,"white"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/lua/lua.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/lua/lua.js new file mode 100644 index 0000000..da40199 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/lua/lua.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/lua/lua",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"--",blockComment:["--[[","]]"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},n.language={defaultToken:"",tokenPostfix:".lua",keywords:["and","break","do","else","elseif","end","false","for","function","goto","if","in","local","nil","not","or","repeat","return","then","true","until","while"],brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.array",open:"[",close:"]"},{token:"delimiter.parenthesis",open:"(",close:")"}],operators:["+","-","*","/","%","^","#","==","~=","<=",">=","<",">","=",";",":",",",".","..","..."],symbols:/[=>"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]}]},o.language={defaultToken:"",tokenPostfix:".m3",brackets:[{token:"delimiter.curly",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"}],keywords:["AND","ANY","ARRAY","AS","BEGIN","BITS","BRANDED","BY","CASE","CONST","DIV","DO","ELSE","ELSIF","END","EVAL","EXCEPT","EXCEPTION","EXIT","EXPORTS","FINALLY","FOR","FROM","GENERIC","IF","IMPORT","IN","INTERFACE","LOCK","LOOP","METHODS","MOD","MODULE","NOT","OBJECT","OF","OR","OVERRIDES","PROCEDURE","RAISE","RAISES","READONLY","RECORD","REF","REPEAT","RETURN","REVEAL","SET","THEN","TO","TRY","TYPE","TYPECASE","UNSAFE","UNTIL","UNTRACED","VALUE","VAR","WHILE","WITH"],reservedConstNames:["ABS","ADR","ADRSIZE","BITSIZE","BYTESIZE","CEILING","DEC","DISPOSE","FALSE","FIRST","FLOAT","FLOOR","INC","ISTYPE","LAST","LOOPHOLE","MAX","MIN","NARROW","NEW","NIL","NUMBER","ORD","ROUND","SUBARRAY","TRUE","TRUNC","TYPECODE","VAL"],reservedTypeNames:["ADDRESS","ANY","BOOLEAN","CARDINAL","CHAR","EXTENDED","INTEGER","LONGCARD","LONGINT","LONGREAL","MUTEX","NULL","REAL","REFANY","ROOT","TEXT"],operators:["+","-","*","/","&","^","."],relations:["=","#","<","<=",">",">=","<:",":"],delimiters:["|","..","=>",",",";",":="],symbols:/[>=<#.,:;+\-*/&^]+/,escapes:/\\(?:[\\fnrt"']|[0-7]{3})/,tokenizer:{root:[[/_\w*/,"invalid"],[/[a-zA-Z][a-zA-Z0-9_]*/,{cases:{"@keywords":{token:"keyword.$0"},"@reservedConstNames":{token:"constant.reserved.$0"},"@reservedTypeNames":{token:"type.reserved.$0"},"@default":"identifier"}}],{include:"@whitespace"},[/[{}()\[\]]/,"@brackets"],[/[0-9]+\.[0-9]+(?:[DdEeXx][\+\-]?[0-9]+)?/,"number.float"],[/[0-9]+(?:\_[0-9a-fA-F]+)?L?/,"number"],[/@symbols/,{cases:{"@operators":"operators","@relations":"operators","@delimiters":"delimiter","@default":"invalid"}}],[/'[^\\']'/,"string.char"],[/(')(@escapes)(')/,["string.char","string.escape","string.char"]],[/'/,"invalid"],[/"([^"\\]|\\.)*$/,"invalid"],[/"/,"string.text","@text"]],text:[[/[^\\"]+/,"string.text"],[/@escapes/,"string.escape"],[/\\./,"invalid"],[/"/,"string.text","@pop"]],comment:[[/\(\*/,"comment","@push"],[/\*\)/,"comment","@pop"],[/./,"comment"]],pragma:[[/<\*/,"keyword.pragma","@push"],[/\*>/,"keyword.pragma","@pop"],[/./,"keyword.pragma"]],whitespace:[[/[ \t\r\n]+/,"white"],[/\(\*/,"comment","@comment"],[/<\*/,"keyword.pragma","@pragma"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/markdown/markdown.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/markdown/markdown.js new file mode 100644 index 0000000..dc020ee --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/markdown/markdown.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/markdown/markdown",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{blockComment:["\x3c!--","--\x3e"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">",notIn:["string"]}],surroundingPairs:[{open:"(",close:")"},{open:"[",close:"]"},{open:"`",close:"`"}],folding:{markers:{start:new RegExp("^\\s*\x3c!--\\s*#?region\\b.*--\x3e"),end:new RegExp("^\\s*\x3c!--\\s*#?endregion\\b.*--\x3e")}}},t.language={defaultToken:"",tokenPostfix:".md",control:/[\\`*_\[\]{}()#+\-\.!]/,noncontrol:/[^\\`*_\[\]{}()#+\-\.!]/,escapes:/\\(?:@control)/,jsescapes:/\\(?:[btnfr\\"']|[0-7][0-7]?|[0-3][0-7]{2})/,empty:["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param"],tokenizer:{root:[[/^\s*\|/,"@rematch","@table_header"],[/^(\s{0,3})(#+)((?:[^\\#]|@escapes)+)((?:#+)?)/,["white","keyword","keyword","keyword"]],[/^\s*(=+|\-+)\s*$/,"keyword"],[/^\s*((\*[ ]?)+)\s*$/,"meta.separator"],[/^\s*>+/,"comment"],[/^\s*([\*\-+:]|\d+\.)\s/,"keyword"],[/^(\t|[ ]{4})[^ ].*$/,"string"],[/^\s*~~~\s*((?:\w|[\/\-#])+)?\s*$/,{token:"string",next:"@codeblock"}],[/^\s*```\s*((?:\w|[\/\-#])+).*$/,{token:"string",next:"@codeblockgh",nextEmbedded:"$1"}],[/^\s*```\s*$/,{token:"string",next:"@codeblock"}],{include:"@linecontent"}],table_header:[{include:"@table_common"},[/[^\|]+/,"keyword.table.header"]],table_body:[{include:"@table_common"},{include:"@linecontent"}],table_common:[[/\s*[\-:]+\s*/,{token:"keyword",switchTo:"table_body"}],[/^\s*\|/,"keyword.table.left"],[/^\s*[^\|]/,"@rematch","@pop"],[/^\s*$/,"@rematch","@pop"],[/\|/,{cases:{"@eos":"keyword.table.right","@default":"keyword.table.middle"}}]],codeblock:[[/^\s*~~~\s*$/,{token:"string",next:"@pop"}],[/^\s*```\s*$/,{token:"string",next:"@pop"}],[/.*$/,"variable.source"]],codeblockgh:[[/```\s*$/,{token:"variable.source",next:"@pop",nextEmbedded:"@pop"}],[/[^`]+/,"variable.source"]],linecontent:[[/&\w+;/,"string.escape"],[/@escapes/,"escape"],[/\b__([^\\_]|@escapes|_(?!_))+__\b/,"strong"],[/\*\*([^\\*]|@escapes|\*(?!\*))+\*\*/,"strong"],[/\b_[^_]+_\b/,"emphasis"],[/\*([^\\*]|@escapes)+\*/,"emphasis"],[/`([^\\`]|@escapes)+`/,"variable"],[/\{+[^}]+\}+/,"string.target"],[/(!?\[)((?:[^\]\\]|@escapes)*)(\]\([^\)]+\))/,["string.link","","string.link"]],[/(!?\[)((?:[^\]\\]|@escapes)*)(\])/,"string.link"],{include:"html"}],html:[[/<(\w+)\/>/,"tag"],[/<(\w+)/,{cases:{"@empty":{token:"tag",next:"@tag.$1"},"@default":{token:"tag",next:"@tag.$1"}}}],[/<\/(\w+)\s*>/,{token:"tag"}],[//,"comment","@pop"],[//,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\?((php)|=)?/,{token:"@rematch",switchTo:"@phpInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],phpInSimpleState:[[/<\?((php)|=)?/,"metatag.php"],[/\?>/,{token:"metatag.php",switchTo:"@$S2.$S3"}],{include:"phpRoot"}],phpInEmbeddedState:[[/<\?((php)|=)?/,"metatag.php"],[/\?>/,{token:"metatag.php",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],{include:"phpRoot"}],phpRoot:[[/[a-zA-Z_]\w*/,{cases:{"@phpKeywords":{token:"keyword.php"},"@phpCompileTimeConstants":{token:"constant.php"},"@default":"identifier.php"}}],[/[$a-zA-Z_]\w*/,{cases:{"@phpPreDefinedVariables":{token:"variable.predefined.php"},"@default":"variable.php"}}],[/[{}]/,"delimiter.bracket.php"],[/[\[\]]/,"delimiter.array.php"],[/[()]/,"delimiter.parenthesis.php"],[/[ \t\r\n]+/],[/(#|\/\/)$/,"comment.php"],[/(#|\/\/)/,"comment.php","@phpLineComment"],[/\/\*/,"comment.php","@phpComment"],[/"/,"string.php","@phpDoubleQuoteString"],[/'/,"string.php","@phpSingleQuoteString"],[/[\+\-\*\%\&\|\^\~\!\=\<\>\/\?\;\:\.\,\@]/,"delimiter.php"],[/\d*\d+[eE]([\-+]?\d+)?/,"number.float.php"],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float.php"],[/0[xX][0-9a-fA-F']*[0-9a-fA-F]/,"number.hex.php"],[/0[0-7']*[0-7]/,"number.octal.php"],[/0[bB][0-1']*[0-1]/,"number.binary.php"],[/\d[\d']*/,"number.php"],[/\d/,"number.php"]],phpComment:[[/\*\//,"comment.php","@pop"],[/[^*]+/,"comment.php"],[/./,"comment.php"]],phpLineComment:[[/\?>/,{token:"@rematch",next:"@pop"}],[/.$/,"comment.php","@pop"],[/[^?]+$/,"comment.php","@pop"],[/[^?]+/,"comment.php"],[/./,"comment.php"]],phpDoubleQuoteString:[[/[^\\"]+/,"string.php"],[/@escapes/,"string.escape.php"],[/\\./,"string.escape.invalid.php"],[/"/,"string.php","@pop"]],phpSingleQuoteString:[[/[^\\']+/,"string.php"],[/@escapes/,"string.escape.php"],[/\\./,"string.escape.invalid.php"],[/'/,"string.php","@pop"]]},phpKeywords:["abstract","and","array","as","break","callable","case","catch","cfunction","class","clone","const","continue","declare","default","do","else","elseif","enddeclare","endfor","endforeach","endif","endswitch","endwhile","extends","false","final","for","foreach","function","global","goto","if","implements","interface","instanceof","insteadof","namespace","new","null","object","old_function","or","private","protected","public","resource","static","switch","throw","trait","try","true","use","var","while","xor","die","echo","empty","exit","eval","include","include_once","isset","list","require","require_once","return","print","unset","yield","__construct"],phpCompileTimeConstants:["__CLASS__","__DIR__","__FILE__","__LINE__","__NAMESPACE__","__METHOD__","__FUNCTION__","__TRAIT__"],phpPreDefinedVariables:["$GLOBALS","$_SERVER","$_GET","$_POST","$_FILES","$_REQUEST","$_SESSION","$_ENV","$_COOKIE","$php_errormsg","$HTTP_RAW_POST_DATA","$http_response_header","$argc","$argv"],escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/postiats/postiats.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/postiats/postiats.js new file mode 100644 index 0000000..df397c1 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/postiats/postiats.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/postiats/postiats",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"//",blockComment:["(*","*)"]},brackets:[["{","}"],["[","]"],["(",")"],["<",">"]],autoClosingPairs:[{open:'"',close:'"',notIn:["string","comment"]},{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]}]},t.language={tokenPostfix:".pats",defaultToken:"invalid",keywords:["abstype","abst0ype","absprop","absview","absvtype","absviewtype","absvt0ype","absviewt0ype","as","and","assume","begin","classdec","datasort","datatype","dataprop","dataview","datavtype","dataviewtype","do","end","extern","extype","extvar","exception","fn","fnx","fun","prfn","prfun","praxi","castfn","if","then","else","ifcase","in","infix","infixl","infixr","prefix","postfix","implmnt","implement","primplmnt","primplement","import","let","local","macdef","macrodef","nonfix","symelim","symintr","overload","of","op","rec","sif","scase","sortdef","sta","stacst","stadef","static","staload","dynload","try","tkindef","typedef","propdef","viewdef","vtypedef","viewtypedef","prval","var","prvar","when","where","with","withtype","withprop","withview","withvtype","withviewtype"],keywords_dlr:["$delay","$ldelay","$arrpsz","$arrptrsize","$d2ctype","$effmask","$effmask_ntm","$effmask_exn","$effmask_ref","$effmask_wrt","$effmask_all","$extern","$extkind","$extype","$extype_struct","$extval","$extfcall","$extmcall","$literal","$myfilename","$mylocation","$myfunction","$lst","$lst_t","$lst_vt","$list","$list_t","$list_vt","$rec","$rec_t","$rec_vt","$record","$record_t","$record_vt","$tup","$tup_t","$tup_vt","$tuple","$tuple_t","$tuple_vt","$break","$continue","$raise","$showtype","$vcopyenv_v","$vcopyenv_vt","$tempenver","$solver_assert","$solver_verify"],keywords_srp:["#if","#ifdef","#ifndef","#then","#elif","#elifdef","#elifndef","#else","#endif","#error","#prerr","#print","#assert","#undef","#define","#include","#require","#pragma","#codegen2","#codegen3"],irregular_keyword_list:["val+","val-","val","case+","case-","case","addr@","addr","fold@","free@","fix@","fix","lam@","lam","llam@","llam","viewt@ype+","viewt@ype-","viewt@ype","viewtype+","viewtype-","viewtype","view+","view-","view@","view","type+","type-","type","vtype+","vtype-","vtype","vt@ype+","vt@ype-","vt@ype","viewt@ype+","viewt@ype-","viewt@ype","viewtype+","viewtype-","viewtype","prop+","prop-","prop","type+","type-","type","t@ype","t@ype+","t@ype-","abst@ype","abstype","absviewt@ype","absvt@ype","for*","for","while*","while"],keywords_types:["bool","double","byte","int","short","char","void","unit","long","float","string","strptr"],keywords_effects:["0","fun","clo","prf","funclo","cloptr","cloref","ref","ntm","1"],operators:["@","!","|","`",":","$",".","=","#","~","..","...","=>","=<>","=/=>","=>>","=/=>>","<",">","><",".<",">.",".<>.","->","-<>"],brackets:[{open:",(",close:")",token:"delimiter.parenthesis"},{open:"`(",close:")",token:"delimiter.parenthesis"},{open:"%(",close:")",token:"delimiter.parenthesis"},{open:"'(",close:")",token:"delimiter.parenthesis"},{open:"'{",close:"}",token:"delimiter.parenthesis"},{open:"@(",close:")",token:"delimiter.parenthesis"},{open:"@{",close:"}",token:"delimiter.brace"},{open:"@[",close:"]",token:"delimiter.square"},{open:"#[",close:"]",token:"delimiter.square"},{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],symbols:/[=>]/,digit:/[0-9]/,digitseq0:/@digit*/,xdigit:/[0-9A-Za-z]/,xdigitseq0:/@xdigit*/,INTSP:/[lLuU]/,FLOATSP:/[fFlL]/,fexponent:/[eE][+-]?[0-9]+/,fexponent_bin:/[pP][+-]?[0-9]+/,deciexp:/\.[0-9]*@fexponent?/,hexiexp:/\.[0-9a-zA-Z]*@fexponent_bin?/,irregular_keywords:/val[+-]?|case[+-]?|addr\@?|fold\@|free\@|fix\@?|lam\@?|llam\@?|prop[+-]?|type[+-]?|view[+-@]?|viewt@?ype[+-]?|t@?ype[+-]?|v(iew)?t@?ype[+-]?|abst@?ype|absv(iew)?t@?ype|for\*?|while\*?/,ESCHAR:/[ntvbrfa\\\?'"\(\[\{]/,start:"root",tokenizer:{root:[{regex:/[ \t\r\n]+/,action:{token:""}},{regex:/\(\*\)/,action:{token:"invalid"}},{regex:/\(\*/,action:{token:"comment",next:"lexing_COMMENT_block_ml"}},{regex:/\(/,action:"@brackets"},{regex:/\)/,action:"@brackets"},{regex:/\[/,action:"@brackets"},{regex:/\]/,action:"@brackets"},{regex:/\{/,action:"@brackets"},{regex:/\}/,action:"@brackets"},{regex:/,\(/,action:"@brackets"},{regex:/,/,action:{token:"delimiter.comma"}},{regex:/;/,action:{token:"delimiter.semicolon"}},{regex:/@\(/,action:"@brackets"},{regex:/@\[/,action:"@brackets"},{regex:/@\{/,action:"@brackets"},{regex:/:/,action:{token:"@rematch",next:"@pop"}}],lexing_EXTCODE:[{regex:/^%}/,action:{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}},{regex:/[^%]+/,action:""}],lexing_DQUOTE:[{regex:/"/,action:{token:"string.quote",next:"@pop"}},{regex:/(\{\$)(@IDENTFST@IDENTRST*)(\})/,action:[{token:"string.escape"},{token:"identifier"},{token:"string.escape"}]},{regex:/\\$/,action:{token:"string.escape"}},{regex:/\\(@ESCHAR|[xX]@xdigit+|@digit+)/,action:{token:"string.escape"}},{regex:/[^\\"]+/,action:{token:"string"}}]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/powerquery/powerquery.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/powerquery/powerquery.js new file mode 100644 index 0000000..eee02c5 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/powerquery/powerquery.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/powerquery/powerquery",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["[","]"],["(",")"],["{","}"]],autoClosingPairs:[{open:'"',close:'"',notIn:["string","comment","identifier"]},{open:"[",close:"]",notIn:["string","comment","identifier"]},{open:"(",close:")",notIn:["string","comment","identifier"]},{open:"{",close:"}",notIn:["string","comment","identifier"]}]},t.language={defaultToken:"",tokenPostfix:".pq",ignoreCase:!1,brackets:[{open:"[",close:"]",token:"delimiter.square"},{open:"{",close:"}",token:"delimiter.brackets"},{open:"(",close:")",token:"delimiter.parenthesis"}],operatorKeywords:["and","not","or"],keywords:["as","each","else","error","false","if","in","is","let","meta","otherwise","section","shared","then","true","try","type"],constructors:["#binary","#date","#datetime","#datetimezone","#duration","#table","#time"],constants:["#infinity","#nan","#sections","#shared"],typeKeywords:["action","any","anynonnull","none","null","logical","number","time","date","datetime","datetimezone","duration","text","binary","list","record","table","function"],builtinFunctions:["Access.Database","Action.Return","Action.Sequence","Action.Try","ActiveDirectory.Domains","AdoDotNet.DataSource","AdoDotNet.Query","AdobeAnalytics.Cubes","AnalysisServices.Database","AnalysisServices.Databases","AzureStorage.BlobContents","AzureStorage.Blobs","AzureStorage.Tables","Binary.Buffer","Binary.Combine","Binary.Compress","Binary.Decompress","Binary.End","Binary.From","Binary.FromList","Binary.FromText","Binary.InferContentType","Binary.Length","Binary.ToList","Binary.ToText","BinaryFormat.7BitEncodedSignedInteger","BinaryFormat.7BitEncodedUnsignedInteger","BinaryFormat.Binary","BinaryFormat.Byte","BinaryFormat.ByteOrder","BinaryFormat.Choice","BinaryFormat.Decimal","BinaryFormat.Double","BinaryFormat.Group","BinaryFormat.Length","BinaryFormat.List","BinaryFormat.Null","BinaryFormat.Record","BinaryFormat.SignedInteger16","BinaryFormat.SignedInteger32","BinaryFormat.SignedInteger64","BinaryFormat.Single","BinaryFormat.Text","BinaryFormat.Transform","BinaryFormat.UnsignedInteger16","BinaryFormat.UnsignedInteger32","BinaryFormat.UnsignedInteger64","Byte.From","Character.FromNumber","Character.ToNumber","Combiner.CombineTextByDelimiter","Combiner.CombineTextByEachDelimiter","Combiner.CombineTextByLengths","Combiner.CombineTextByPositions","Combiner.CombineTextByRanges","Comparer.Equals","Comparer.FromCulture","Comparer.Ordinal","Comparer.OrdinalIgnoreCase","Csv.Document","Cube.AddAndExpandDimensionColumn","Cube.AddMeasureColumn","Cube.ApplyParameter","Cube.AttributeMemberId","Cube.AttributeMemberProperty","Cube.CollapseAndRemoveColumns","Cube.Dimensions","Cube.DisplayFolders","Cube.Measures","Cube.Parameters","Cube.Properties","Cube.PropertyKey","Cube.ReplaceDimensions","Cube.Transform","Currency.From","DB2.Database","Date.AddDays","Date.AddMonths","Date.AddQuarters","Date.AddWeeks","Date.AddYears","Date.Day","Date.DayOfWeek","Date.DayOfWeekName","Date.DayOfYear","Date.DaysInMonth","Date.EndOfDay","Date.EndOfMonth","Date.EndOfQuarter","Date.EndOfWeek","Date.EndOfYear","Date.From","Date.FromText","Date.IsInCurrentDay","Date.IsInCurrentMonth","Date.IsInCurrentQuarter","Date.IsInCurrentWeek","Date.IsInCurrentYear","Date.IsInNextDay","Date.IsInNextMonth","Date.IsInNextNDays","Date.IsInNextNMonths","Date.IsInNextNQuarters","Date.IsInNextNWeeks","Date.IsInNextNYears","Date.IsInNextQuarter","Date.IsInNextWeek","Date.IsInNextYear","Date.IsInPreviousDay","Date.IsInPreviousMonth","Date.IsInPreviousNDays","Date.IsInPreviousNMonths","Date.IsInPreviousNQuarters","Date.IsInPreviousNWeeks","Date.IsInPreviousNYears","Date.IsInPreviousQuarter","Date.IsInPreviousWeek","Date.IsInPreviousYear","Date.IsInYearToDate","Date.IsLeapYear","Date.Month","Date.MonthName","Date.QuarterOfYear","Date.StartOfDay","Date.StartOfMonth","Date.StartOfQuarter","Date.StartOfWeek","Date.StartOfYear","Date.ToRecord","Date.ToText","Date.WeekOfMonth","Date.WeekOfYear","Date.Year","DateTime.AddZone","DateTime.Date","DateTime.FixedLocalNow","DateTime.From","DateTime.FromFileTime","DateTime.FromText","DateTime.IsInCurrentHour","DateTime.IsInCurrentMinute","DateTime.IsInCurrentSecond","DateTime.IsInNextHour","DateTime.IsInNextMinute","DateTime.IsInNextNHours","DateTime.IsInNextNMinutes","DateTime.IsInNextNSeconds","DateTime.IsInNextSecond","DateTime.IsInPreviousHour","DateTime.IsInPreviousMinute","DateTime.IsInPreviousNHours","DateTime.IsInPreviousNMinutes","DateTime.IsInPreviousNSeconds","DateTime.IsInPreviousSecond","DateTime.LocalNow","DateTime.Time","DateTime.ToRecord","DateTime.ToText","DateTimeZone.FixedLocalNow","DateTimeZone.FixedUtcNow","DateTimeZone.From","DateTimeZone.FromFileTime","DateTimeZone.FromText","DateTimeZone.LocalNow","DateTimeZone.RemoveZone","DateTimeZone.SwitchZone","DateTimeZone.ToLocal","DateTimeZone.ToRecord","DateTimeZone.ToText","DateTimeZone.ToUtc","DateTimeZone.UtcNow","DateTimeZone.ZoneHours","DateTimeZone.ZoneMinutes","Decimal.From","Diagnostics.ActivityId","Diagnostics.Trace","DirectQueryCapabilities.From","Double.From","Duration.Days","Duration.From","Duration.FromText","Duration.Hours","Duration.Minutes","Duration.Seconds","Duration.ToRecord","Duration.ToText","Duration.TotalDays","Duration.TotalHours","Duration.TotalMinutes","Duration.TotalSeconds","Embedded.Value","Error.Record","Excel.CurrentWorkbook","Excel.Workbook","Exchange.Contents","Expression.Constant","Expression.Evaluate","Expression.Identifier","Facebook.Graph","File.Contents","Folder.Contents","Folder.Files","Function.From","Function.Invoke","Function.InvokeAfter","Function.IsDataSource","GoogleAnalytics.Accounts","Guid.From","HdInsight.Containers","HdInsight.Contents","HdInsight.Files","Hdfs.Contents","Hdfs.Files","Informix.Database","Int16.From","Int32.From","Int64.From","Int8.From","ItemExpression.From","Json.Document","Json.FromValue","Lines.FromBinary","Lines.FromText","Lines.ToBinary","Lines.ToText","List.Accumulate","List.AllTrue","List.Alternate","List.AnyTrue","List.Average","List.Buffer","List.Combine","List.Contains","List.ContainsAll","List.ContainsAny","List.Count","List.Covariance","List.DateTimeZones","List.DateTimes","List.Dates","List.Difference","List.Distinct","List.Durations","List.FindText","List.First","List.FirstN","List.Generate","List.InsertRange","List.Intersect","List.IsDistinct","List.IsEmpty","List.Last","List.LastN","List.MatchesAll","List.MatchesAny","List.Max","List.MaxN","List.Median","List.Min","List.MinN","List.Mode","List.Modes","List.NonNullCount","List.Numbers","List.PositionOf","List.PositionOfAny","List.Positions","List.Product","List.Random","List.Range","List.RemoveFirstN","List.RemoveItems","List.RemoveLastN","List.RemoveMatchingItems","List.RemoveNulls","List.RemoveRange","List.Repeat","List.ReplaceMatchingItems","List.ReplaceRange","List.ReplaceValue","List.Reverse","List.Select","List.Single","List.SingleOrDefault","List.Skip","List.Sort","List.StandardDeviation","List.Sum","List.Times","List.Transform","List.TransformMany","List.Union","List.Zip","Logical.From","Logical.FromText","Logical.ToText","MQ.Queue","MySQL.Database","Number.Abs","Number.Acos","Number.Asin","Number.Atan","Number.Atan2","Number.BitwiseAnd","Number.BitwiseNot","Number.BitwiseOr","Number.BitwiseShiftLeft","Number.BitwiseShiftRight","Number.BitwiseXor","Number.Combinations","Number.Cos","Number.Cosh","Number.Exp","Number.Factorial","Number.From","Number.FromText","Number.IntegerDivide","Number.IsEven","Number.IsNaN","Number.IsOdd","Number.Ln","Number.Log","Number.Log10","Number.Mod","Number.Permutations","Number.Power","Number.Random","Number.RandomBetween","Number.Round","Number.RoundAwayFromZero","Number.RoundDown","Number.RoundTowardZero","Number.RoundUp","Number.Sign","Number.Sin","Number.Sinh","Number.Sqrt","Number.Tan","Number.Tanh","Number.ToText","OData.Feed","Odbc.DataSource","Odbc.Query","OleDb.DataSource","OleDb.Query","Oracle.Database","Percentage.From","PostgreSQL.Database","RData.FromBinary","Record.AddField","Record.Combine","Record.Field","Record.FieldCount","Record.FieldNames","Record.FieldOrDefault","Record.FieldValues","Record.FromList","Record.FromTable","Record.HasFields","Record.RemoveFields","Record.RenameFields","Record.ReorderFields","Record.SelectFields","Record.ToList","Record.ToTable","Record.TransformFields","Replacer.ReplaceText","Replacer.ReplaceValue","RowExpression.Column","RowExpression.From","Salesforce.Data","Salesforce.Reports","SapBusinessWarehouse.Cubes","SapHana.Database","SharePoint.Contents","SharePoint.Files","SharePoint.Tables","Single.From","Soda.Feed","Splitter.SplitByNothing","Splitter.SplitTextByAnyDelimiter","Splitter.SplitTextByDelimiter","Splitter.SplitTextByEachDelimiter","Splitter.SplitTextByLengths","Splitter.SplitTextByPositions","Splitter.SplitTextByRanges","Splitter.SplitTextByRepeatedLengths","Splitter.SplitTextByWhitespace","Sql.Database","Sql.Databases","SqlExpression.SchemaFrom","SqlExpression.ToExpression","Sybase.Database","Table.AddColumn","Table.AddIndexColumn","Table.AddJoinColumn","Table.AddKey","Table.AggregateTableColumn","Table.AlternateRows","Table.Buffer","Table.Column","Table.ColumnCount","Table.ColumnNames","Table.ColumnsOfType","Table.Combine","Table.CombineColumns","Table.Contains","Table.ContainsAll","Table.ContainsAny","Table.DemoteHeaders","Table.Distinct","Table.DuplicateColumn","Table.ExpandListColumn","Table.ExpandRecordColumn","Table.ExpandTableColumn","Table.FillDown","Table.FillUp","Table.FilterWithDataTable","Table.FindText","Table.First","Table.FirstN","Table.FirstValue","Table.FromColumns","Table.FromList","Table.FromPartitions","Table.FromRecords","Table.FromRows","Table.FromValue","Table.Group","Table.HasColumns","Table.InsertRows","Table.IsDistinct","Table.IsEmpty","Table.Join","Table.Keys","Table.Last","Table.LastN","Table.MatchesAllRows","Table.MatchesAnyRows","Table.Max","Table.MaxN","Table.Min","Table.MinN","Table.NestedJoin","Table.Partition","Table.PartitionValues","Table.Pivot","Table.PositionOf","Table.PositionOfAny","Table.PrefixColumns","Table.Profile","Table.PromoteHeaders","Table.Range","Table.RemoveColumns","Table.RemoveFirstN","Table.RemoveLastN","Table.RemoveMatchingRows","Table.RemoveRows","Table.RemoveRowsWithErrors","Table.RenameColumns","Table.ReorderColumns","Table.Repeat","Table.ReplaceErrorValues","Table.ReplaceKeys","Table.ReplaceMatchingRows","Table.ReplaceRelationshipIdentity","Table.ReplaceRows","Table.ReplaceValue","Table.ReverseRows","Table.RowCount","Table.Schema","Table.SelectColumns","Table.SelectRows","Table.SelectRowsWithErrors","Table.SingleRow","Table.Skip","Table.Sort","Table.SplitColumn","Table.ToColumns","Table.ToList","Table.ToRecords","Table.ToRows","Table.TransformColumnNames","Table.TransformColumnTypes","Table.TransformColumns","Table.TransformRows","Table.Transpose","Table.Unpivot","Table.UnpivotOtherColumns","Table.View","Table.ViewFunction","TableAction.DeleteRows","TableAction.InsertRows","TableAction.UpdateRows","Tables.GetRelationships","Teradata.Database","Text.AfterDelimiter","Text.At","Text.BeforeDelimiter","Text.BetweenDelimiters","Text.Clean","Text.Combine","Text.Contains","Text.End","Text.EndsWith","Text.Format","Text.From","Text.FromBinary","Text.Insert","Text.Length","Text.Lower","Text.Middle","Text.NewGuid","Text.PadEnd","Text.PadStart","Text.PositionOf","Text.PositionOfAny","Text.Proper","Text.Range","Text.Remove","Text.RemoveRange","Text.Repeat","Text.Replace","Text.ReplaceRange","Text.Select","Text.Split","Text.SplitAny","Text.Start","Text.StartsWith","Text.ToBinary","Text.ToList","Text.Trim","Text.TrimEnd","Text.TrimStart","Text.Upper","Time.EndOfHour","Time.From","Time.FromText","Time.Hour","Time.Minute","Time.Second","Time.StartOfHour","Time.ToRecord","Time.ToText","Type.AddTableKey","Type.ClosedRecord","Type.Facets","Type.ForFunction","Type.ForRecord","Type.FunctionParameters","Type.FunctionRequiredParameters","Type.FunctionReturn","Type.Is","Type.IsNullable","Type.IsOpenRecord","Type.ListItem","Type.NonNullable","Type.OpenRecord","Type.RecordFields","Type.ReplaceFacets","Type.ReplaceTableKeys","Type.TableColumn","Type.TableKeys","Type.TableRow","Type.TableSchema","Type.Union","Uri.BuildQueryString","Uri.Combine","Uri.EscapeDataString","Uri.Parts","Value.Add","Value.As","Value.Compare","Value.Divide","Value.Equals","Value.Firewall","Value.FromText","Value.Is","Value.Metadata","Value.Multiply","Value.NativeQuery","Value.NullableEquals","Value.RemoveMetadata","Value.ReplaceMetadata","Value.ReplaceType","Value.Subtract","Value.Type","ValueAction.NativeStatement","ValueAction.Replace","Variable.Value","Web.Contents","Web.Page","WebAction.Request","Xml.Document","Xml.Tables"],builtinConstants:["BinaryEncoding.Base64","BinaryEncoding.Hex","BinaryOccurrence.Optional","BinaryOccurrence.Repeating","BinaryOccurrence.Required","ByteOrder.BigEndian","ByteOrder.LittleEndian","Compression.Deflate","Compression.GZip","CsvStyle.QuoteAfterDelimiter","CsvStyle.QuoteAlways","Culture.Current","Day.Friday","Day.Monday","Day.Saturday","Day.Sunday","Day.Thursday","Day.Tuesday","Day.Wednesday","ExtraValues.Error","ExtraValues.Ignore","ExtraValues.List","GroupKind.Global","GroupKind.Local","JoinAlgorithm.Dynamic","JoinAlgorithm.LeftHash","JoinAlgorithm.LeftIndex","JoinAlgorithm.PairwiseHash","JoinAlgorithm.RightHash","JoinAlgorithm.RightIndex","JoinAlgorithm.SortMerge","JoinKind.FullOuter","JoinKind.Inner","JoinKind.LeftAnti","JoinKind.LeftOuter","JoinKind.RightAnti","JoinKind.RightOuter","JoinSide.Left","JoinSide.Right","MissingField.Error","MissingField.Ignore","MissingField.UseNull","Number.E","Number.Epsilon","Number.NaN","Number.NegativeInfinity","Number.PI","Number.PositiveInfinity","Occurrence.All","Occurrence.First","Occurrence.Last","Occurrence.Optional","Occurrence.Repeating","Occurrence.Required","Order.Ascending","Order.Descending","Precision.Decimal","Precision.Double","QuoteStyle.Csv","QuoteStyle.None","RelativePosition.FromEnd","RelativePosition.FromStart","RoundingMode.AwayFromZero","RoundingMode.Down","RoundingMode.ToEven","RoundingMode.TowardZero","RoundingMode.Up","SapHanaDistribution.All","SapHanaDistribution.Connection","SapHanaDistribution.Off","SapHanaDistribution.Statement","SapHanaRangeOperator.Equals","SapHanaRangeOperator.GreaterThan","SapHanaRangeOperator.GreaterThanOrEquals","SapHanaRangeOperator.LessThan","SapHanaRangeOperator.LessThanOrEquals","SapHanaRangeOperator.NotEquals","TextEncoding.Ascii","TextEncoding.BigEndianUnicode","TextEncoding.Unicode","TextEncoding.Utf16","TextEncoding.Utf8","TextEncoding.Windows","TraceLevel.Critical","TraceLevel.Error","TraceLevel.Information","TraceLevel.Verbose","TraceLevel.Warning","WebMethod.Delete","WebMethod.Get","WebMethod.Head","WebMethod.Patch","WebMethod.Post","WebMethod.Put"],builtinTypes:["Action.Type","Any.Type","Binary.Type","BinaryEncoding.Type","BinaryOccurrence.Type","Byte.Type","ByteOrder.Type","Character.Type","Compression.Type","CsvStyle.Type","Currency.Type","Date.Type","DateTime.Type","DateTimeZone.Type","Day.Type","Decimal.Type","Double.Type","Duration.Type","ExtraValues.Type","Function.Type","GroupKind.Type","Guid.Type","Int16.Type","Int32.Type","Int64.Type","Int8.Type","JoinAlgorithm.Type","JoinKind.Type","JoinSide.Type","List.Type","Logical.Type","MissingField.Type","None.Type","Null.Type","Number.Type","Occurrence.Type","Order.Type","Password.Type","Percentage.Type","Precision.Type","QuoteStyle.Type","Record.Type","RelativePosition.Type","RoundingMode.Type","SapHanaDistribution.Type","SapHanaRangeOperator.Type","Single.Type","Table.Type","Text.Type","TextEncoding.Type","Time.Type","TraceLevel.Type","Type.Type","Uri.Type","WebMethod.Type"],tokenizer:{root:[[/#"[\w \.]+"/,"identifier.quote"],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float"],[/0[xX][0-9a-fA-F]+/,"number.hex"],[/\d+([eE][\-+]?\d+)?/,"number"],[/(#?[a-z]+)\b/,{cases:{"@typeKeywords":"type","@keywords":"keyword","@constants":"constant","@constructors":"constructor","@operatorKeywords":"operators","@default":"identifier"}}],[/\b([A-Z][a-zA-Z0-9]+\.Type)\b/,{cases:{"@builtinTypes":"type","@default":"identifier"}}],[/\b([A-Z][a-zA-Z0-9]+\.[A-Z][a-zA-Z0-9]+)\b/,{cases:{"@builtinFunctions":"keyword.function","@builtinConstants":"constant","@default":"identifier"}}],[/\b([a-zA-Z_][\w\.]*)\b/,"identifier"],{include:"@whitespace"},{include:"@comments"},{include:"@strings"},[/[{}()\[\]]/,"@brackets"],[/([=\+<>\-\*&@\?\/!])|([<>]=)|(<>)|(=>)|(\.\.\.)|(\.\.)/,"operators"],[/[,;]/,"delimiter"]],whitespace:[[/\s+/,"white"]],comments:[["\\/\\*","comment","@comment"],["\\/\\/+.*","comment"]],comment:[["\\*\\/","comment","@pop"],[".","comment"]],strings:[['"',"string","@string"]],string:[['""',"string.escape"],['"',"string","@pop"],[".","string"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/powershell/powershell.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/powershell/powershell.js new file mode 100644 index 0000000..7505fc9 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/powershell/powershell.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/powershell/powershell",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\#%\^\&\*\(\)\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"#",blockComment:["<#","#>"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*#region\\b"),end:new RegExp("^\\s*#endregion\\b")}}},n.language={defaultToken:"",ignoreCase:!0,tokenPostfix:".ps1",brackets:[{token:"delimiter.curly",open:"{",close:"}"},{token:"delimiter.square",open:"[",close:"]"},{token:"delimiter.parenthesis",open:"(",close:")"}],keywords:["begin","break","catch","class","continue","data","define","do","dynamicparam","else","elseif","end","exit","filter","finally","for","foreach","from","function","if","in","param","process","return","switch","throw","trap","try","until","using","var","while","workflow","parallel","sequence","inlinescript","configuration"],helpKeywords:/SYNOPSIS|DESCRIPTION|PARAMETER|EXAMPLE|INPUTS|OUTPUTS|NOTES|LINK|COMPONENT|ROLE|FUNCTIONALITY|FORWARDHELPTARGETNAME|FORWARDHELPCATEGORY|REMOTEHELPRUNSPACE|EXTERNALHELP/,symbols:/[=>/,"comment","@pop"],[/(\.)(@helpKeywords)(?!\w)/,{token:"comment.keyword.$2"}],[/[\.#]/,"comment"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/pug/pug.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/pug/pug.js new file mode 100644 index 0000000..8212279 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/pug/pug.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/pug/pug",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"//"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:'"',close:'"',notIn:["string","comment"]},{open:"'",close:"'",notIn:["string","comment"]},{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]}],folding:{offSide:!0}},t.language={defaultToken:"",tokenPostfix:".pug",ignoreCase:!0,brackets:[{token:"delimiter.curly",open:"{",close:"}"},{token:"delimiter.array",open:"[",close:"]"},{token:"delimiter.parenthesis",open:"(",close:")"}],keywords:["append","block","case","default","doctype","each","else","extends","for","if","in","include","mixin","typeof","unless","var","when"],tags:["a","abbr","acronym","address","area","article","aside","audio","b","base","basefont","bdi","bdo","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","command","datalist","dd","del","details","dfn","div","dl","dt","em","embed","fieldset","figcaption","figure","font","footer","form","frame","frameset","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","keygen","kbd","label","li","link","map","mark","menu","meta","meter","nav","noframes","noscript","object","ol","optgroup","option","output","p","param","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strike","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","title","tr","tracks","tt","u","ul","video","wbr"],symbols:/[\+\-\*\%\&\|\!\=\/\.\,\:]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[[/^(\s*)([a-zA-Z_-][\w-]*)/,{cases:{"$2@tags":{cases:{"@eos":["","tag"],"@default":["",{token:"tag",next:"@tag.$1"}]}},"$2@keywords":["",{token:"keyword.$2"}],"@default":["",""]}}],[/^(\s*)(#[a-zA-Z_-][\w-]*)/,{cases:{"@eos":["","tag.id"],"@default":["",{token:"tag.id",next:"@tag.$1"}]}}],[/^(\s*)(\.[a-zA-Z_-][\w-]*)/,{cases:{"@eos":["","tag.class"],"@default":["",{token:"tag.class",next:"@tag.$1"}]}}],[/^(\s*)(\|.*)$/,""],{include:"@whitespace"},[/[a-zA-Z_$][\w$]*/,{cases:{"@keywords":{token:"keyword.$0"},"@default":""}}],[/[{}()\[\]]/,"@brackets"],[/@symbols/,"delimiter"],[/\d+\.\d+([eE][\-+]?\d+)?/,"number.float"],[/\d+/,"number"],[/"/,"string",'@string."'],[/'/,"string","@string.'"]],tag:[[/(\.)(\s*$)/,[{token:"delimiter",next:"@blockText.$S2."},""]],[/\s+/,{token:"",next:"@simpleText"}],[/#[a-zA-Z_-][\w-]*/,{cases:{"@eos":{token:"tag.id",next:"@pop"},"@default":"tag.id"}}],[/\.[a-zA-Z_-][\w-]*/,{cases:{"@eos":{token:"tag.class",next:"@pop"},"@default":"tag.class"}}],[/\(/,{token:"delimiter.parenthesis",next:"@attributeList"}]],simpleText:[[/[^#]+$/,{token:"",next:"@popall"}],[/[^#]+/,{token:""}],[/(#{)([^}]*)(})/,{cases:{"@eos":["interpolation.delimiter","interpolation",{token:"interpolation.delimiter",next:"@popall"}],"@default":["interpolation.delimiter","interpolation","interpolation.delimiter"]}}],[/#$/,{token:"",next:"@popall"}],[/#/,""]],attributeList:[[/\s+/,""],[/(\w+)(\s*=\s*)("|')/,["attribute.name","delimiter",{token:"attribute.value",next:"@value.$3"}]],[/\w+/,"attribute.name"],[/,/,{cases:{"@eos":{token:"attribute.delimiter",next:"@popall"},"@default":"attribute.delimiter"}}],[/\)$/,{token:"delimiter.parenthesis",next:"@popall"}],[/\)/,{token:"delimiter.parenthesis",next:"@pop"}]],whitespace:[[/^(\s*)(\/\/.*)$/,{token:"comment",next:"@blockText.$1.comment"}],[/[ \t\r\n]+/,""],[//,{token:"comment",next:"@pop"}],[//,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.otherTag"}],[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/]],script:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.script"}],[/type/,"attribute.name","@scriptAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.scriptAfterType"}],[/=/,"delimiter","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.scriptAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.text/javascript",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.scriptWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInEmbeddedState.scriptEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],style:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.style"}],[/type/,"attribute.name","@styleAfterType"],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.styleAfterType"}],[/=/,"delimiter","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.styleAfterTypeEquals"}],[/"([^"]*)"/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.text/css",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInSimpleState.styleWithCustomType.$S2"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value"],[/'([^']*)'/,"attribute.value"],[/[\w\-]+/,"attribute.name"],[/=/,"delimiter"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/@[^@]/,{token:"@rematch",switchTo:"@razorInEmbeddedState.styleEmbedded.$S2",nextEmbedded:"@pop"}],[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}]],razorInSimpleState:[[/@\*/,"comment.cs","@razorBlockCommentTopLevel"],[/@[{(]/,"metatag.cs","@razorRootTopLevel"],[/(@)(\s*[\w]+)/,["metatag.cs",{token:"identifier.cs",switchTo:"@$S2.$S3"}]],[/[})]/,{token:"metatag.cs",switchTo:"@$S2.$S3"}],[/\*@/,{token:"comment.cs",switchTo:"@$S2.$S3"}]],razorInEmbeddedState:[[/@\*/,"comment.cs","@razorBlockCommentTopLevel"],[/@[{(]/,"metatag.cs","@razorRootTopLevel"],[/(@)(\s*[\w]+)/,["metatag.cs",{token:"identifier.cs",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}]],[/[})]/,{token:"metatag.cs",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}],[/\*@/,{token:"comment.cs",switchTo:"@$S2.$S3",nextEmbedded:"$S3"}]],razorBlockCommentTopLevel:[[/\*@/,"@rematch","@pop"],[/[^*]+/,"comment.cs"],[/./,"comment.cs"]],razorBlockComment:[[/\*@/,"comment.cs","@pop"],[/[^*]+/,"comment.cs"],[/./,"comment.cs"]],razorRootTopLevel:[[/\{/,"delimiter.bracket.cs","@razorRoot"],[/\(/,"delimiter.parenthesis.cs","@razorRoot"],[/[})]/,"@rematch","@pop"],{include:"razorCommon"}],razorRoot:[[/\{/,"delimiter.bracket.cs","@razorRoot"],[/\(/,"delimiter.parenthesis.cs","@razorRoot"],[/\}/,"delimiter.bracket.cs","@pop"],[/\)/,"delimiter.parenthesis.cs","@pop"],{include:"razorCommon"}],razorCommon:[[/[a-zA-Z_]\w*/,{cases:{"@razorKeywords":{token:"keyword.cs"},"@default":"identifier.cs"}}],[/[\[\]]/,"delimiter.array.cs"],[/[ \t\r\n]+/],[/\/\/.*$/,"comment.cs"],[/@\*/,"comment.cs","@razorBlockComment"],[/"([^"]*)"/,"string.cs"],[/'([^']*)'/,"string.cs"],[/(<)(\w+)(\/>)/,["delimiter.html","tag.html","delimiter.html"]],[/(<)(\w+)(>)/,["delimiter.html","tag.html","delimiter.html"]],[/(<\/)(\w+)(>)/,["delimiter.html","tag.html","delimiter.html"]],[/[\+\-\*\%\&\|\^\~\!\=\<\>\/\?\;\:\.\,]/,"delimiter.cs"],[/\d*\d+[eE]([\-+]?\d+)?/,"number.float.cs"],[/\d*\.\d+([eE][\-+]?\d+)?/,"number.float.cs"],[/0[xX][0-9a-fA-F']*[0-9a-fA-F]/,"number.hex.cs"],[/0[0-7']*[0-7]/,"number.octal.cs"],[/0[bB][0-1']*[0-1]/,"number.binary.cs"],[/\d[\d']*/,"number.cs"],[/\d/,"number.cs"]]},razorKeywords:["abstract","as","async","await","base","bool","break","by","byte","case","catch","char","checked","class","const","continue","decimal","default","delegate","do","double","descending","explicit","event","extern","else","enum","false","finally","fixed","float","for","foreach","from","goto","group","if","implicit","in","int","interface","internal","into","is","lock","long","nameof","new","null","namespace","object","operator","out","override","orderby","params","private","protected","public","readonly","ref","return","switch","struct","sbyte","sealed","short","sizeof","stackalloc","static","string","select","this","throw","true","try","typeof","uint","ulong","unchecked","unsafe","ushort","using","var","virtual","volatile","void","when","while","where","yield","model","inject"],escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/redis/redis.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/redis/redis.js new file mode 100644 index 0000000..64b62ae --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/redis/redis.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/redis/redis",["require","exports"],(function(E,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.language=e.conf=void 0,e.conf={brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},e.language={defaultToken:"",tokenPostfix:".redis",ignoreCase:!0,brackets:[{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"}],keywords:["APPEND","AUTH","BGREWRITEAOF","BGSAVE","BITCOUNT","BITFIELD","BITOP","BITPOS","BLPOP","BRPOP","BRPOPLPUSH","CLIENT","KILL","LIST","GETNAME","PAUSE","REPLY","SETNAME","CLUSTER","ADDSLOTS","COUNT-FAILURE-REPORTS","COUNTKEYSINSLOT","DELSLOTS","FAILOVER","FORGET","GETKEYSINSLOT","INFO","KEYSLOT","MEET","NODES","REPLICATE","RESET","SAVECONFIG","SET-CONFIG-EPOCH","SETSLOT","SLAVES","SLOTS","COMMAND","COUNT","GETKEYS","CONFIG","GET","REWRITE","SET","RESETSTAT","DBSIZE","DEBUG","OBJECT","SEGFAULT","DECR","DECRBY","DEL","DISCARD","DUMP","ECHO","EVAL","EVALSHA","EXEC","EXISTS","EXPIRE","EXPIREAT","FLUSHALL","FLUSHDB","GEOADD","GEOHASH","GEOPOS","GEODIST","GEORADIUS","GEORADIUSBYMEMBER","GETBIT","GETRANGE","GETSET","HDEL","HEXISTS","HGET","HGETALL","HINCRBY","HINCRBYFLOAT","HKEYS","HLEN","HMGET","HMSET","HSET","HSETNX","HSTRLEN","HVALS","INCR","INCRBY","INCRBYFLOAT","KEYS","LASTSAVE","LINDEX","LINSERT","LLEN","LPOP","LPUSH","LPUSHX","LRANGE","LREM","LSET","LTRIM","MGET","MIGRATE","MONITOR","MOVE","MSET","MSETNX","MULTI","PERSIST","PEXPIRE","PEXPIREAT","PFADD","PFCOUNT","PFMERGE","PING","PSETEX","PSUBSCRIBE","PUBSUB","PTTL","PUBLISH","PUNSUBSCRIBE","QUIT","RANDOMKEY","READONLY","READWRITE","RENAME","RENAMENX","RESTORE","ROLE","RPOP","RPOPLPUSH","RPUSH","RPUSHX","SADD","SAVE","SCARD","SCRIPT","FLUSH","LOAD","SDIFF","SDIFFSTORE","SELECT","SETBIT","SETEX","SETNX","SETRANGE","SHUTDOWN","SINTER","SINTERSTORE","SISMEMBER","SLAVEOF","SLOWLOG","SMEMBERS","SMOVE","SORT","SPOP","SRANDMEMBER","SREM","STRLEN","SUBSCRIBE","SUNION","SUNIONSTORE","SWAPDB","SYNC","TIME","TOUCH","TTL","TYPE","UNSUBSCRIBE","UNLINK","UNWATCH","WAIT","WATCH","ZADD","ZCARD","ZCOUNT","ZINCRBY","ZINTERSTORE","ZLEXCOUNT","ZRANGE","ZRANGEBYLEX","ZREVRANGEBYLEX","ZRANGEBYSCORE","ZRANK","ZREM","ZREMRANGEBYLEX","ZREMRANGEBYRANK","ZREMRANGEBYSCORE","ZREVRANGE","ZREVRANGEBYSCORE","ZREVRANK","ZSCORE","ZUNIONSTORE","SCAN","SSCAN","HSCAN","ZSCAN"],operators:[],builtinFunctions:[],builtinVariables:[],pseudoColumns:[],tokenizer:{root:[{include:"@whitespace"},{include:"@pseudoColumns"},{include:"@numbers"},{include:"@strings"},{include:"@scopes"},[/[;,.]/,"delimiter"],[/[()]/,"@brackets"],[/[\w@#$]+/,{cases:{"@keywords":"keyword","@operators":"operator","@builtinVariables":"predefined","@builtinFunctions":"predefined","@default":"identifier"}}],[/[<>=!%&+\-*/|~^]/,"operator"]],whitespace:[[/\s+/,"white"]],pseudoColumns:[[/[$][A-Za-z_][\w@#$]*/,{cases:{"@pseudoColumns":"predefined","@default":"identifier"}}]],numbers:[[/0[xX][0-9a-fA-F]*/,"number"],[/[$][+-]*\d*(\.\d*)?/,"number"],[/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/,"number"]],strings:[[/'/,{token:"string",next:"@string"}],[/"/,{token:"string.double",next:"@stringDouble"}]],string:[[/[^']+/,"string"],[/''/,"string"],[/'/,{token:"string",next:"@pop"}]],stringDouble:[[/[^"]+/,"string.double"],[/""/,"string.double"],[/"/,{token:"string.double",next:"@pop"}]],scopes:[]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/redshift/redshift.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/redshift/redshift.js new file mode 100644 index 0000000..a9829cc --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/redshift/redshift.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/redshift/redshift",["require","exports"],(function(e,_){"use strict";Object.defineProperty(_,"__esModule",{value:!0}),_.language=_.conf=void 0,_.conf={comments:{lineComment:"--",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},_.language={defaultToken:"",tokenPostfix:".sql",ignoreCase:!0,brackets:[{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"}],keywords:["AES128","AES256","ALL","ALLOWOVERWRITE","ANALYSE","ANALYZE","AND","ANY","ARRAY","AS","ASC","AUTHORIZATION","BACKUP","BETWEEN","BINARY","BLANKSASNULL","BOTH","BY","BYTEDICT","BZIP2","CASE","CAST","CHECK","COLLATE","COLUMN","CONSTRAINT","CREATE","CREDENTIALS","CROSS","CURRENT_DATE","CURRENT_TIME","CURRENT_TIMESTAMP","CURRENT_USER","CURRENT_USER_ID","DEFAULT","DEFERRABLE","DEFLATE","DEFRAG","DELTA","DELTA32K","DESC","DISABLE","DISTINCT","DO","ELSE","EMPTYASNULL","ENABLE","ENCODE","ENCRYPT","ENCRYPTION","END","EXCEPT","EXPLICIT","FALSE","FOR","FOREIGN","FREEZE","FROM","FULL","GLOBALDICT256","GLOBALDICT64K","GRANT","GROUP","GZIP","HAVING","IDENTITY","IGNORE","ILIKE","IN","INITIALLY","INNER","INTERSECT","INTO","IS","ISNULL","JOIN","LEADING","LEFT","LIKE","LIMIT","LOCALTIME","LOCALTIMESTAMP","LUN","LUNS","LZO","LZOP","MINUS","MOSTLY13","MOSTLY32","MOSTLY8","NATURAL","NEW","NOT","NOTNULL","NULL","NULLS","OFF","OFFLINE","OFFSET","OID","OLD","ON","ONLY","OPEN","OR","ORDER","OUTER","OVERLAPS","PARALLEL","PARTITION","PERCENT","PERMISSIONS","PLACING","PRIMARY","RAW","READRATIO","RECOVER","REFERENCES","RESPECT","REJECTLOG","RESORT","RESTORE","RIGHT","SELECT","SESSION_USER","SIMILAR","SNAPSHOT","SOME","SYSDATE","SYSTEM","TABLE","TAG","TDES","TEXT255","TEXT32K","THEN","TIMESTAMP","TO","TOP","TRAILING","TRUE","TRUNCATECOLUMNS","UNION","UNIQUE","USER","USING","VERBOSE","WALLET","WHEN","WHERE","WITH","WITHOUT"],operators:["AND","BETWEEN","IN","LIKE","NOT","OR","IS","NULL","INTERSECT","UNION","INNER","JOIN","LEFT","OUTER","RIGHT"],builtinFunctions:["current_schema","current_schemas","has_database_privilege","has_schema_privilege","has_table_privilege","age","current_time","current_timestamp","localtime","isfinite","now","ascii","get_bit","get_byte","set_bit","set_byte","to_ascii","approximate percentile_disc","avg","count","listagg","max","median","min","percentile_cont","stddev_samp","stddev_pop","sum","var_samp","var_pop","bit_and","bit_or","bool_and","bool_or","cume_dist","first_value","lag","last_value","lead","nth_value","ratio_to_report","dense_rank","ntile","percent_rank","rank","row_number","case","coalesce","decode","greatest","least","nvl","nvl2","nullif","add_months","at time zone","convert_timezone","current_date","date_cmp","date_cmp_timestamp","date_cmp_timestamptz","date_part_year","dateadd","datediff","date_part","date_trunc","extract","getdate","interval_cmp","last_day","months_between","next_day","sysdate","timeofday","timestamp_cmp","timestamp_cmp_date","timestamp_cmp_timestamptz","timestamptz_cmp","timestamptz_cmp_date","timestamptz_cmp_timestamp","timezone","to_timestamp","trunc","abs","acos","asin","atan","atan2","cbrt","ceil","ceiling","checksum","cos","cot","degrees","dexp","dlog1","dlog10","exp","floor","ln","log","mod","pi","power","radians","random","round","sin","sign","sqrt","tan","to_hex","bpcharcmp","btrim","bttext_pattern_cmp","char_length","character_length","charindex","chr","concat","crc32","func_sha1","initcap","left and rights","len","length","lower","lpad and rpads","ltrim","md5","octet_length","position","quote_ident","quote_literal","regexp_count","regexp_instr","regexp_replace","regexp_substr","repeat","replace","replicate","reverse","rtrim","split_part","strpos","strtol","substring","textlen","translate","trim","upper","cast","convert","to_char","to_date","to_number","json_array_length","json_extract_array_element_text","json_extract_path_text","current_setting","pg_cancel_backend","pg_terminate_backend","set_config","current_database","current_user","current_user_id","pg_backend_pid","pg_last_copy_count","pg_last_copy_id","pg_last_query_id","pg_last_unload_count","session_user","slice_num","user","version","abbrev","acosd","any","area","array_agg","array_append","array_cat","array_dims","array_fill","array_length","array_lower","array_ndims","array_position","array_positions","array_prepend","array_remove","array_replace","array_to_json","array_to_string","array_to_tsvector","array_upper","asind","atan2d","atand","bit","bit_length","bound_box","box","brin_summarize_new_values","broadcast","cardinality","center","circle","clock_timestamp","col_description","concat_ws","convert_from","convert_to","corr","cosd","cotd","covar_pop","covar_samp","current_catalog","current_query","current_role","currval","cursor_to_xml","diameter","div","encode","enum_first","enum_last","enum_range","every","family","format","format_type","generate_series","generate_subscripts","get_current_ts_config","gin_clean_pending_list","grouping","has_any_column_privilege","has_column_privilege","has_foreign_data_wrapper_privilege","has_function_privilege","has_language_privilege","has_sequence_privilege","has_server_privilege","has_tablespace_privilege","has_type_privilege","height","host","hostmask","inet_client_addr","inet_client_port","inet_merge","inet_same_family","inet_server_addr","inet_server_port","isclosed","isempty","isopen","json_agg","json_object","json_object_agg","json_populate_record","json_populate_recordset","json_to_record","json_to_recordset","jsonb_agg","jsonb_object_agg","justify_days","justify_hours","justify_interval","lastval","left","line","localtimestamp","lower_inc","lower_inf","lpad","lseg","make_date","make_interval","make_time","make_timestamp","make_timestamptz","masklen","mode","netmask","network","nextval","npoints","num_nonnulls","num_nulls","numnode","obj_description","overlay","parse_ident","path","pclose","percentile_disc","pg_advisory_lock","pg_advisory_lock_shared","pg_advisory_unlock","pg_advisory_unlock_all","pg_advisory_unlock_shared","pg_advisory_xact_lock","pg_advisory_xact_lock_shared","pg_backup_start_time","pg_blocking_pids","pg_client_encoding","pg_collation_is_visible","pg_column_size","pg_conf_load_time","pg_control_checkpoint","pg_control_init","pg_control_recovery","pg_control_system","pg_conversion_is_visible","pg_create_logical_replication_slot","pg_create_physical_replication_slot","pg_create_restore_point","pg_current_xlog_flush_location","pg_current_xlog_insert_location","pg_current_xlog_location","pg_database_size","pg_describe_object","pg_drop_replication_slot","pg_export_snapshot","pg_filenode_relation","pg_function_is_visible","pg_get_constraintdef","pg_get_expr","pg_get_function_arguments","pg_get_function_identity_arguments","pg_get_function_result","pg_get_functiondef","pg_get_indexdef","pg_get_keywords","pg_get_object_address","pg_get_owned_sequence","pg_get_ruledef","pg_get_serial_sequence","pg_get_triggerdef","pg_get_userbyid","pg_get_viewdef","pg_has_role","pg_identify_object","pg_identify_object_as_address","pg_index_column_has_property","pg_index_has_property","pg_indexam_has_property","pg_indexes_size","pg_is_in_backup","pg_is_in_recovery","pg_is_other_temp_schema","pg_is_xlog_replay_paused","pg_last_committed_xact","pg_last_xact_replay_timestamp","pg_last_xlog_receive_location","pg_last_xlog_replay_location","pg_listening_channels","pg_logical_emit_message","pg_logical_slot_get_binary_changes","pg_logical_slot_get_changes","pg_logical_slot_peek_binary_changes","pg_logical_slot_peek_changes","pg_ls_dir","pg_my_temp_schema","pg_notification_queue_usage","pg_opclass_is_visible","pg_operator_is_visible","pg_opfamily_is_visible","pg_options_to_table","pg_postmaster_start_time","pg_read_binary_file","pg_read_file","pg_relation_filenode","pg_relation_filepath","pg_relation_size","pg_reload_conf","pg_replication_origin_create","pg_replication_origin_drop","pg_replication_origin_oid","pg_replication_origin_progress","pg_replication_origin_session_is_setup","pg_replication_origin_session_progress","pg_replication_origin_session_reset","pg_replication_origin_session_setup","pg_replication_origin_xact_reset","pg_replication_origin_xact_setup","pg_rotate_logfile","pg_size_bytes","pg_size_pretty","pg_sleep","pg_sleep_for","pg_sleep_until","pg_start_backup","pg_stat_file","pg_stop_backup","pg_switch_xlog","pg_table_is_visible","pg_table_size","pg_tablespace_databases","pg_tablespace_location","pg_tablespace_size","pg_total_relation_size","pg_trigger_depth","pg_try_advisory_lock","pg_try_advisory_lock_shared","pg_try_advisory_xact_lock","pg_try_advisory_xact_lock_shared","pg_ts_config_is_visible","pg_ts_dict_is_visible","pg_ts_parser_is_visible","pg_ts_template_is_visible","pg_type_is_visible","pg_typeof","pg_xact_commit_timestamp","pg_xlog_location_diff","pg_xlog_replay_pause","pg_xlog_replay_resume","pg_xlogfile_name","pg_xlogfile_name_offset","phraseto_tsquery","plainto_tsquery","point","polygon","popen","pqserverversion","query_to_xml","querytree","quote_nullable","radius","range_merge","regexp_matches","regexp_split_to_array","regexp_split_to_table","regr_avgx","regr_avgy","regr_count","regr_intercept","regr_r2","regr_slope","regr_sxx","regr_sxy","regr_syy","right","row_security_active","row_to_json","rpad","scale","set_masklen","setseed","setval","setweight","shobj_description","sind","sprintf","statement_timestamp","stddev","string_agg","string_to_array","strip","substr","table_to_xml","table_to_xml_and_xmlschema","tand","text","to_json","to_regclass","to_regnamespace","to_regoper","to_regoperator","to_regproc","to_regprocedure","to_regrole","to_regtype","to_tsquery","to_tsvector","transaction_timestamp","ts_debug","ts_delete","ts_filter","ts_headline","ts_lexize","ts_parse","ts_rank","ts_rank_cd","ts_rewrite","ts_stat","ts_token_type","tsquery_phrase","tsvector_to_array","tsvector_update_trigger","tsvector_update_trigger_column","txid_current","txid_current_snapshot","txid_snapshot_xip","txid_snapshot_xmax","txid_snapshot_xmin","txid_visible_in_snapshot","unnest","upper_inc","upper_inf","variance","width","width_bucket","xml_is_well_formed","xml_is_well_formed_content","xml_is_well_formed_document","xmlagg","xmlcomment","xmlconcat","xmlelement","xmlexists","xmlforest","xmlparse","xmlpi","xmlroot","xmlserialize","xpath","xpath_exists"],builtinVariables:[],pseudoColumns:[],tokenizer:{root:[{include:"@comments"},{include:"@whitespace"},{include:"@pseudoColumns"},{include:"@numbers"},{include:"@strings"},{include:"@complexIdentifiers"},{include:"@scopes"},[/[;,.]/,"delimiter"],[/[()]/,"@brackets"],[/[\w@#$]+/,{cases:{"@keywords":"keyword","@operators":"operator","@builtinVariables":"predefined","@builtinFunctions":"predefined","@default":"identifier"}}],[/[<>=!%&+\-*/|~^]/,"operator"]],whitespace:[[/\s+/,"white"]],comments:[[/--+.*/,"comment"],[/\/\*/,{token:"comment.quote",next:"@comment"}]],comment:[[/[^*/]+/,"comment"],[/\*\//,{token:"comment.quote",next:"@pop"}],[/./,"comment"]],pseudoColumns:[[/[$][A-Za-z_][\w@#$]*/,{cases:{"@pseudoColumns":"predefined","@default":"identifier"}}]],numbers:[[/0[xX][0-9a-fA-F]*/,"number"],[/[$][+-]*\d*(\.\d*)?/,"number"],[/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/,"number"]],strings:[[/'/,{token:"string",next:"@string"}]],string:[[/[^']+/,"string"],[/''/,"string"],[/'/,{token:"string",next:"@pop"}]],complexIdentifiers:[[/"/,{token:"identifier.quote",next:"@quotedIdentifier"}]],quotedIdentifier:[[/[^"]+/,"identifier"],[/""/,"identifier"],[/"/,{token:"identifier.quote",next:"@pop"}]],scopes:[]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/restructuredtext/restructuredtext.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/restructuredtext/restructuredtext.js new file mode 100644 index 0000000..694d9a2 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/restructuredtext/restructuredtext.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/restructuredtext/restructuredtext",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:"<",close:">",notIn:["string"]}],surroundingPairs:[{open:"(",close:")"},{open:"[",close:"]"},{open:"`",close:"`"}],folding:{markers:{start:new RegExp("^\\s*\x3c!--\\s*#?region\\b.*--\x3e"),end:new RegExp("^\\s*\x3c!--\\s*#?endregion\\b.*--\x3e")}}},n.language={defaultToken:"",tokenPostfix:".rst",control:/[\\`*_\[\]{}()#+\-\.!]/,escapes:/\\(?:@control)/,empty:["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param"],alphanumerics:/[A-Za-z0-9]/,alphanumericsplus:/[A-Za-z0-9-_+:.]/,simpleRefNameWithoutBq:/(?:@alphanumerics@alphanumericsplus*@alphanumerics)+|(?:@alphanumerics+)/,simpleRefName:/(?:`@simpleRefNameWithoutBq`|@simpleRefNameWithoutBq)/,phrase:/@simpleRefName(?:\s@simpleRefName)*/,citationName:/[A-Za-z][A-Za-z0-9-_.]*/,blockLiteralStart:/(?:[!"#$%&'()*+,-./:;<=>?@\[\]^_`{|}~]|[\s])/,precedingChars:/(?:[ -:/'"<([{])/,followingChars:/(?:[ -.,:;!?/'")\]}>]|$)/,punctuation:/(=|-|~|`|#|"|\^|\+|\*|:|\.|'|_|\+)/,tokenizer:{root:[[/^(@punctuation{3,}$){1,1}?/,"keyword"],[/^\s*([\*\-+‣•]|[a-zA-Z0-9]+\.|\([a-zA-Z0-9]+\)|[a-zA-Z0-9]+\))\s/,"keyword"],[/([ ]::)\s*$/,"keyword","@blankLineOfLiteralBlocks"],[/(::)\s*$/,"keyword","@blankLineOfLiteralBlocks"],{include:"@tables"},{include:"@explicitMarkupBlocks"},{include:"@inlineMarkup"}],explicitMarkupBlocks:[{include:"@citations"},{include:"@footnotes"},[/^(\.\.\s)(@simpleRefName)(::\s)(.*)$/,[{token:"",next:"subsequentLines"},"keyword","",""]],[/^(\.\.)(\s+)(_)(@simpleRefName)(:)(\s+)(.*)/,[{token:"",next:"hyperlinks"},"","","string.link","","","string.link"]],[/^((?:(?:\.\.)(?:\s+))?)(__)(:)(\s+)(.*)/,[{token:"",next:"subsequentLines"},"","","","string.link"]],[/^(__\s+)(.+)/,["","string.link"]],[/^(\.\.)( \|)([^| ]+[^|]*[^| ]*)(\| )(@simpleRefName)(:: .*)/,[{token:"",next:"subsequentLines"},"","string.link","","keyword",""],"@rawBlocks"],[/(\|)([^| ]+[^|]*[^| ]*)(\|_{0,2})/,["","string.link",""]],[/^(\.\.)([ ].*)$/,[{token:"",next:"@comments"},"comment"]]],inlineMarkup:[{include:"@citationsReference"},{include:"@footnotesReference"},[/(@simpleRefName)(_{1,2})/,["string.link",""]],[/(`)([^<`]+\s+)(<)(.*)(>)(`)(_)/,["","string.link","","string.link","","",""]],[/\*\*([^\\*]|\*(?!\*))+\*\*/,"strong"],[/\*[^*]+\*/,"emphasis"],[/(``)((?:[^`]|\`(?!`))+)(``)/,["","keyword",""]],[/(__\s+)(.+)/,["","keyword"]],[/(:)((?:@simpleRefNameWithoutBq)?)(:`)([^`]+)(`)/,["","keyword","","",""]],[/(`)([^`]+)(`:)((?:@simpleRefNameWithoutBq)?)(:)/,["","","","keyword",""]],[/(`)([^`]+)(`)/,""],[/(_`)(@phrase)(`)/,["","string.link",""]]],citations:[[/^(\.\.\s+\[)((?:@citationName))(\]\s+)(.*)/,[{token:"",next:"@subsequentLines"},"string.link","",""]]],citationsReference:[[/(\[)(@citationName)(\]_)/,["","string.link",""]]],footnotes:[[/^(\.\.\s+\[)((?:[0-9]+))(\]\s+.*)/,[{token:"",next:"@subsequentLines"},"string.link",""]],[/^(\.\.\s+\[)((?:#@simpleRefName?))(\]\s+)(.*)/,[{token:"",next:"@subsequentLines"},"string.link","",""]],[/^(\.\.\s+\[)((?:\*))(\]\s+)(.*)/,[{token:"",next:"@subsequentLines"},"string.link","",""]]],footnotesReference:[[/(\[)([0-9]+)(\])(_)/,["","string.link","",""]],[/(\[)(#@simpleRefName?)(\])(_)/,["","string.link","",""]],[/(\[)(\*)(\])(_)/,["","string.link","",""]]],blankLineOfLiteralBlocks:[[/^$/,"","@subsequentLinesOfLiteralBlocks"],[/^.*$/,"","@pop"]],subsequentLinesOfLiteralBlocks:[[/(@blockLiteralStart+)(.*)/,["keyword",""]],[/^(?!blockLiteralStart)/,"","@popall"]],subsequentLines:[[/^[\s]+.*/,""],[/^(?!\s)/,"","@pop"]],hyperlinks:[[/^[\s]+.*/,"string.link"],[/^(?!\s)/,"","@pop"]],comments:[[/^[\s]+.*/,"comment"],[/^(?!\s)/,"","@pop"]],tables:[[/\+-[+-]+/,"keyword"],[/\+=[+=]+/,"keyword"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ruby/ruby.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ruby/ruby.js new file mode 100644 index 0000000..1ed764a --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/ruby/ruby.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/ruby/ruby",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"#",blockComment:["=begin","=end"]},brackets:[["(",")"],["{","}"],["[","]"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],indentationRules:{increaseIndentPattern:new RegExp("^\\s*((begin|class|(private|protected)\\s+def|def|else|elsif|ensure|for|if|module|rescue|unless|until|when|while|case)|([^#]*\\sdo\\b)|([^#]*=\\s*(case|if|unless)))\\b([^#\\{;]|(\"|'|/).*\\4)*(#.*)?$"),decreaseIndentPattern:new RegExp("^\\s*([}\\]]([,)]?\\s*(#|$)|\\.[a-zA-Z_]\\w*\\b)|(end|rescue|ensure|else|elsif|when)\\b)")}},t.language={tokenPostfix:".ruby",keywords:["__LINE__","__ENCODING__","__FILE__","BEGIN","END","alias","and","begin","break","case","class","def","defined?","do","else","elsif","end","ensure","for","false","if","in","module","next","nil","not","or","redo","rescue","retry","return","self","super","then","true","undef","unless","until","when","while","yield"],keywordops:["::","..","...","?",":","=>"],builtins:["require","public","private","include","extend","attr_reader","protected","private_class_method","protected_class_method","new"],declarations:["module","class","def","case","do","begin","for","if","while","until","unless"],linedecls:["def","case","do","begin","for","if","while","until","unless"],operators:["^","&","|","<=>","==","===","!~","=~",">",">=","<","<=","<<",">>","+","-","*","/","%","**","~","+@","-@","[]","[]=","`","+=","-=","*=","**=","/=","^=","%=","<<=",">>=","&=","&&=","||=","|="],brackets:[{open:"(",close:")",token:"delimiter.parenthesis"},{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"}],symbols:/[=>"}],[/%([qws])(@delim)/,{token:"string.$1.delim",switchTo:"@qstring.$1.$2.$2"}],[/%r\(/,{token:"regexp.delim",switchTo:"@pregexp.(.)"}],[/%r\[/,{token:"regexp.delim",switchTo:"@pregexp.[.]"}],[/%r\{/,{token:"regexp.delim",switchTo:"@pregexp.{.}"}],[/%r"}],[/%r(@delim)/,{token:"regexp.delim",switchTo:"@pregexp.$1.$1"}],[/%(x|W|Q?)\(/,{token:"string.$1.delim",switchTo:"@qqstring.$1.(.)"}],[/%(x|W|Q?)\[/,{token:"string.$1.delim",switchTo:"@qqstring.$1.[.]"}],[/%(x|W|Q?)\{/,{token:"string.$1.delim",switchTo:"@qqstring.$1.{.}"}],[/%(x|W|Q?)"}],[/%(x|W|Q?)(@delim)/,{token:"string.$1.delim",switchTo:"@qqstring.$1.$2.$2"}],[/%([rqwsxW]|Q?)./,{token:"invalid",next:"@pop"}],[/./,{token:"invalid",next:"@pop"}]],qstring:[[/\\$/,"string.$S2.escape"],[/\\./,"string.$S2.escape"],[/./,{cases:{"$#==$S4":{token:"string.$S2.delim",next:"@pop"},"$#==$S3":{token:"string.$S2.delim",next:"@push"},"@default":"string.$S2"}}]],qqstring:[[/#/,"string.$S2.escape","@interpolated"],{include:"@qstring"}],whitespace:[[/[ \t\r\n]+/,""],[/^\s*=begin\b/,"comment","@comment"],[/#.*$/,"comment"]],comment:[[/[^=]+/,"comment"],[/^\s*=begin\b/,"comment.invalid"],[/^\s*=end\b.*/,"comment","@pop"],[/[=]/,"comment"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/rust/rust.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/rust/rust.js new file mode 100644 index 0000000..c74e57a --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/rust/rust.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/rust/rust",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*#pragma\\s+region\\b"),end:new RegExp("^\\s*#pragma\\s+endregion\\b")}}},t.language={tokenPostfix:".rust",defaultToken:"invalid",keywords:["as","async","await","box","break","const","continue","crate","dyn","else","enum","extern","false","fn","for","if","impl","in","let","loop","match","mod","move","mut","pub","ref","return","self","static","struct","super","trait","true","try","type","unsafe","use","where","while","catch","default","union","static","abstract","alignof","become","do","final","macro","offsetof","override","priv","proc","pure","sizeof","typeof","unsized","virtual","yield"],typeKeywords:["Self","m32","m64","m128","f80","f16","f128","int","uint","float","char","bool","u8","u16","u32","u64","f32","f64","i8","i16","i32","i64","str","Option","Either","c_float","c_double","c_void","FILE","fpos_t","DIR","dirent","c_char","c_schar","c_uchar","c_short","c_ushort","c_int","c_uint","c_long","c_ulong","size_t","ptrdiff_t","clock_t","time_t","c_longlong","c_ulonglong","intptr_t","uintptr_t","off_t","dev_t","ino_t","pid_t","mode_t","ssize_t"],constants:["true","false","Some","None","Left","Right","Ok","Err"],supportConstants:["EXIT_FAILURE","EXIT_SUCCESS","RAND_MAX","EOF","SEEK_SET","SEEK_CUR","SEEK_END","_IOFBF","_IONBF","_IOLBF","BUFSIZ","FOPEN_MAX","FILENAME_MAX","L_tmpnam","TMP_MAX","O_RDONLY","O_WRONLY","O_RDWR","O_APPEND","O_CREAT","O_EXCL","O_TRUNC","S_IFIFO","S_IFCHR","S_IFBLK","S_IFDIR","S_IFREG","S_IFMT","S_IEXEC","S_IWRITE","S_IREAD","S_IRWXU","S_IXUSR","S_IWUSR","S_IRUSR","F_OK","R_OK","W_OK","X_OK","STDIN_FILENO","STDOUT_FILENO","STDERR_FILENO"],supportMacros:["format!","print!","println!","panic!","format_args!","unreachable!","write!","writeln!"],operators:["!","!=","%","%=","&","&=","&&","*","*=","+","+=","-","-=","->",".","..","...","/","/=",":",";","<<","<<=","<","<=","=","==","=>",">",">=",">>",">>=","@","^","^=","|","|=","||","_","?","#"],escapes:/\\([nrt0\"''\\]|x\h{2}|u\{\h{1,6}\})/,delimiters:/[,]/,symbols:/[\#\!\%\&\*\+\-\.\/\:\;\<\=\>\@\^\|_\?]+/,intSuffixes:/[iu](8|16|32|64|128|size)/,floatSuffixes:/f(32|64)/,tokenizer:{root:[[/[a-zA-Z][a-zA-Z0-9_]*!?|_[a-zA-Z0-9_]+/,{cases:{"@typeKeywords":"keyword.type","@keywords":"keyword","@supportConstants":"keyword","@supportMacros":"keyword","@constants":"keyword","@default":"identifier"}}],[/\$/,"identifier"],[/'[a-zA-Z_][a-zA-Z0-9_]*(?=[^\'])/,"identifier"],[/'\S'/,"string.byteliteral"],[/"/,{token:"string.quote",bracket:"@open",next:"@string"}],{include:"@numbers"},{include:"@whitespace"},[/@delimiters/,{cases:{"@keywords":"keyword","@default":"delimiter"}}],[/[{}()\[\]<>]/,"@brackets"],[/@symbols/,{cases:{"@operators":"operator","@default":""}}]],whitespace:[[/[ \t\r\n]+/,"white"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\/\*/,"comment","@push"],["\\*/","comment","@pop"],[/[\/*]/,"comment"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,{token:"string.quote",bracket:"@close",next:"@pop"}]],numbers:[[/(0o[0-7_]+)(@intSuffixes)?/,{token:"number"}],[/(0b[0-1_]+)(@intSuffixes)?/,{token:"number"}],[/[\d][\d_]*(\.[\d][\d_]*)?[eE][+-][\d_]+(@floatSuffixes)?/,{token:"number"}],[/\b(\d\.?[\d_]*)(@floatSuffixes)?\b/,{token:"number"}],[/(0x[\da-fA-F]+)_?(@intSuffixes)?/,{token:"number"}],[/[\d][\d_]*(@intSuffixes?)?/,{token:"number"}]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sb/sb.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sb/sb.js new file mode 100644 index 0000000..5e2241e --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sb/sb.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/sb/sb",["require","exports"],(function(e,o){"use strict";Object.defineProperty(o,"__esModule",{value:!0}),o.language=o.conf=void 0,o.conf={comments:{lineComment:"'"},brackets:[["(",")"],["[","]"],["If","EndIf"],["While","EndWhile"],["For","EndFor"],["Sub","EndSub"]],autoClosingPairs:[{open:'"',close:'"',notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]}]},o.language={defaultToken:"",tokenPostfix:".sb",ignoreCase:!0,brackets:[{token:"delimiter.array",open:"[",close:"]"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"keyword.tag-if",open:"If",close:"EndIf"},{token:"keyword.tag-while",open:"While",close:"EndWhile"},{token:"keyword.tag-for",open:"For",close:"EndFor"},{token:"keyword.tag-sub",open:"Sub",close:"EndSub"}],keywords:["Else","ElseIf","EndFor","EndIf","EndSub","EndWhile","For","Goto","If","Step","Sub","Then","To","While"],tagwords:["If","Sub","While","For"],operators:[">","<","<>","<=",">=","And","Or","+","-","*","/","="],identifier:/[a-zA-Z_][\w]*/,symbols:/[=><:+\-*\/%\.,]+/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[{include:"@whitespace"},[/(@identifier)(?=[.])/,"type"],[/@identifier/,{cases:{"@keywords":{token:"keyword.$0"},"@operators":"operator","@default":"variable.name"}}],[/([.])(@identifier)/,{cases:{$2:["delimiter","type.member"],"@default":""}}],[/\d*\.\d+/,"number.float"],[/\d+/,"number"],[/[()\[\]]/,"@brackets"],[/@symbols/,{cases:{"@operators":"operator","@default":"delimiter"}}],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"]],whitespace:[[/[ \t\r\n]+/,""],[/(\').*$/,"comment"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"C?/,"string","@pop"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scala/scala.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scala/scala.js new file mode 100644 index 0000000..b75d85f --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scala/scala.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/scala/scala",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={wordPattern:/(unary_[@~!#%^&*()\-=+\\|:<>\/?]+)|([a-zA-Z_$][\w$]*?_=)|(`[^`]+`)|([a-zA-Z_$][\w$]*)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*//\\s*(?:(?:#?region\\b)|(?:))")}}},t.language={tokenPostfix:".scala",keywords:["asInstanceOf","catch","class","classOf","def","do","else","extends","finally","for","foreach","forSome","if","import","isInstanceOf","macro","match","new","object","package","return","throw","trait","try","type","until","val","var","while","with","yield","given","enum","then"],softKeywords:["as","export","extension","end","derives","on"],constants:["true","false","null","this","super"],modifiers:["abstract","final","implicit","lazy","override","private","protected","sealed"],softModifiers:["inline","opaque","open","transparent","using"],name:/(?:[a-z_$][\w$]*|`[^`]+`)/,type:/(?:[A-Z][\w$]*)/,symbols:/[=>))/,["@brackets","white","variable"]],[/@name/,{cases:{"@keywords":"keyword","@softKeywords":"keyword","@modifiers":"keyword.modifier","@softModifiers":"keyword.modifier","@constants":{token:"constant",next:"@allowMethod"},"@default":{token:"identifier",next:"@allowMethod"}}}],[/@type/,"type","@allowMethod"],{include:"@whitespace"},[/@[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)*/,"annotation"],[/[{(]/,"@brackets"],[/[})]/,"@brackets","@allowMethod"],[/\[/,"operator.square"],[/](?!\s*(?:va[rl]|def|type)\b)/,"operator.square","@allowMethod"],[/]/,"operator.square"],[/([=-]>|<-|>:|<:|:>|<%)(?=[\s\w()[\]{},\."'`])/,"keyword"],[/@symbols/,"operator"],[/[;,\.]/,"delimiter"],[/'[a-zA-Z$][\w$]*(?!')/,"attribute.name"],[/'[^\\']'/,"string","@allowMethod"],[/(')(@escapes)(')/,["string","string.escape",{token:"string",next:"@allowMethod"}]],[/'/,"string.invalid"]],import:[[/;/,"delimiter","@pop"],[/^|$/,"","@pop"],[/[ \t]+/,"white"],[/[\n\r]+/,"white","@pop"],[/\/\*/,"comment","@comment"],[/@name|@type/,"type"],[/[(){}]/,"@brackets"],[/[[\]]/,"operator.square"],[/[\.,]/,"delimiter"]],allowMethod:[[/^|$/,"","@pop"],[/[ \t]+/,"white"],[/[\n\r]+/,"white","@pop"],[/\/\*/,"comment","@comment"],[/(?==>[\s\w([{])/,"keyword","@pop"],[/(@name|@symbols)(?=[ \t]*[[({"'`]|[ \t]+(?:[+-]?\.?\d|\w))/,{cases:{"@keywords":{token:"keyword",next:"@pop"},"->|<-|>:|<:|<%":{token:"keyword",next:"@pop"},"@default":{token:"@rematch",next:"@pop"}}}],["","","@pop"]],comment:[[/[^\/*]+/,"comment"],[/\/\*/,"comment","@push"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],case:[[/\b_\*/,"key"],[/\b(_|true|false|null|this|super)\b/,"keyword","@allowMethod"],[/\bif\b|=>/,"keyword","@pop"],[/`[^`]+`/,"identifier","@allowMethod"],[/@name/,"variable","@allowMethod"],[/:::?|\||@(?![a-z_$])/,"keyword"],{include:"@root"}],vardef:[[/\b_\*/,"key"],[/\b(_|true|false|null|this|super)\b/,"keyword"],[/@name/,"variable"],[/:::?|\||@(?![a-z_$])/,"keyword"],[/=|:(?!:)/,"operator","@pop"],[/$/,"white","@pop"],{include:"@root"}],string:[[/[^\\"\n\r]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,{token:"string.quote",bracket:"@close",switchTo:"@allowMethod"}]],stringt:[[/[^\\"\n\r]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"(?=""")/,"string"],[/"""/,{token:"string.quote",bracket:"@close",switchTo:"@allowMethod"}],[/"/,"string"]],fstring:[[/@escapes/,"string.escape"],[/"/,{token:"string.quote",bracket:"@close",switchTo:"@allowMethod"}],[/\$\$/,"string"],[/(\$)([a-z_]\w*)/,["operator","identifier"]],[/\$\{/,"operator","@interp"],[/%%/,"string"],[/(%)([\-#+ 0,(])(\d+|\.\d+|\d+\.\d+)(@fstring_conv)/,["metatag","keyword.modifier","number","metatag"]],[/(%)(\d+|\.\d+|\d+\.\d+)(@fstring_conv)/,["metatag","number","metatag"]],[/(%)([\-#+ 0,(])(@fstring_conv)/,["metatag","keyword.modifier","metatag"]],[/(%)(@fstring_conv)/,["metatag","metatag"]],[/./,"string"]],fstringt:[[/@escapes/,"string.escape"],[/"(?=""")/,"string"],[/"""/,{token:"string.quote",bracket:"@close",switchTo:"@allowMethod"}],[/\$\$/,"string"],[/(\$)([a-z_]\w*)/,["operator","identifier"]],[/\$\{/,"operator","@interp"],[/%%/,"string"],[/(%)([\-#+ 0,(])(\d+|\.\d+|\d+\.\d+)(@fstring_conv)/,["metatag","keyword.modifier","number","metatag"]],[/(%)(\d+|\.\d+|\d+\.\d+)(@fstring_conv)/,["metatag","number","metatag"]],[/(%)([\-#+ 0,(])(@fstring_conv)/,["metatag","keyword.modifier","metatag"]],[/(%)(@fstring_conv)/,["metatag","metatag"]],[/./,"string"]],sstring:[[/@escapes/,"string.escape"],[/"/,{token:"string.quote",bracket:"@close",switchTo:"@allowMethod"}],[/\$\$/,"string"],[/(\$)([a-z_]\w*)/,["operator","identifier"]],[/\$\{/,"operator","@interp"],[/./,"string"]],sstringt:[[/@escapes/,"string.escape"],[/"(?=""")/,"string"],[/"""/,{token:"string.quote",bracket:"@close",switchTo:"@allowMethod"}],[/\$\$/,"string"],[/(\$)([a-z_]\w*)/,["operator","identifier"]],[/\$\{/,"operator","@interp"],[/./,"string"]],interp:[[/{/,"operator","@push"],[/}/,"operator","@pop"],{include:"@root"}],rawstring:[[/[^"]/,"string"],[/"/,{token:"string.quote",bracket:"@close",switchTo:"@allowMethod"}]],rawstringt:[[/[^"]/,"string"],[/"(?=""")/,"string"],[/"""/,{token:"string.quote",bracket:"@close",switchTo:"@allowMethod"}],[/"/,"string"]],whitespace:[[/[ \t\r\n]+/,"white"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scheme/scheme.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scheme/scheme.js new file mode 100644 index 0000000..c93fe95 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scheme/scheme.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/scheme/scheme",["require","exports"],(function(e,o){"use strict";Object.defineProperty(o,"__esModule",{value:!0}),o.language=o.conf=void 0,o.conf={comments:{lineComment:";",blockComment:["#|","|#"]},brackets:[["(",")"],["{","}"],["[","]"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'}]},o.language={defaultToken:"",ignoreCase:!0,tokenPostfix:".scheme",brackets:[{open:"(",close:")",token:"delimiter.parenthesis"},{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"}],keywords:["case","do","let","loop","if","else","when","cons","car","cdr","cond","lambda","lambda*","syntax-rules","format","set!","quote","eval","append","list","list?","member?","load"],constants:["#t","#f"],operators:["eq?","eqv?","equal?","and","or","not","null?"],tokenizer:{root:[[/#[xXoObB][0-9a-fA-F]+/,"number.hex"],[/[+-]?\d+(?:(?:\.\d*)?(?:[eE][+-]?\d+)?)?/,"number.float"],[/(?:\b(?:(define|define-syntax|define-macro))\b)(\s+)((?:\w|\-|\!|\?)*)/,["keyword","white","variable"]],{include:"@whitespace"},{include:"@strings"},[/[a-zA-Z_#][a-zA-Z0-9_\-\?\!\*]*/,{cases:{"@keywords":"keyword","@constants":"constant","@operators":"operators","@default":"identifier"}}]],comment:[[/[^\|#]+/,"comment"],[/#\|/,"comment","@push"],[/\|#/,"comment","@pop"],[/[\|#]/,"comment"]],whitespace:[[/[ \t\r\n]+/,"white"],[/#\|/,"comment","@comment"],[/;.*$/,"comment"]],strings:[[/"$/,"string","@popall"],[/"(?=.)/,"string","@multiLineString"]],multiLineString:[[/[^\\"]+$/,"string","@popall"],[/[^\\"]+/,"string"],[/\\./,"string.escape"],[/"/,"string","@popall"],[/\\$/,"string"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scss/scss.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scss/scss.js new file mode 100644 index 0000000..b8e8a5a --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/scss/scss.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/scss/scss",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={wordPattern:/(#?-?\d*\.\d\w*%?)|([@$#!.:]?[\w-?]+%?)|[@#!.]/g,comments:{blockComment:["/*","*/"],lineComment:"//"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]},{open:"'",close:"'",notIn:["string","comment"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{markers:{start:new RegExp("^\\s*\\/\\*\\s*#region\\b\\s*(.*?)\\s*\\*\\/"),end:new RegExp("^\\s*\\/\\*\\s*#endregion\\b.*\\*\\/")}}},t.language={defaultToken:"",tokenPostfix:".scss",ws:"[ \t\n\r\f]*",identifier:"-?-?([a-zA-Z]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))([\\w\\-]|(\\\\(([0-9a-fA-F]{1,6}\\s?)|[^[0-9a-fA-F])))*",brackets:[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.bracket"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}],tokenizer:{root:[{include:"@selector"}],selector:[{include:"@comments"},{include:"@import"},{include:"@variabledeclaration"},{include:"@warndebug"},["[@](include)",{token:"keyword",next:"@includedeclaration"}],["[@](keyframes|-webkit-keyframes|-moz-keyframes|-o-keyframes)",{token:"keyword",next:"@keyframedeclaration"}],["[@](page|content|font-face|-moz-document)",{token:"keyword"}],["[@](charset|namespace)",{token:"keyword",next:"@declarationbody"}],["[@](function)",{token:"keyword",next:"@functiondeclaration"}],["[@](mixin)",{token:"keyword",next:"@mixindeclaration"}],["url(\\-prefix)?\\(",{token:"meta",next:"@urldeclaration"}],{include:"@controlstatement"},{include:"@selectorname"},["[&\\*]","tag"],["[>\\+,]","delimiter"],["\\[",{token:"delimiter.bracket",next:"@selectorattribute"}],["{",{token:"delimiter.curly",next:"@selectorbody"}]],selectorbody:[["[*_]?@identifier@ws:(?=(\\s|\\d|[^{;}]*[;}]))","attribute.name","@rulevalue"],{include:"@selector"},["[@](extend)",{token:"keyword",next:"@extendbody"}],["[@](return)",{token:"keyword",next:"@declarationbody"}],["}",{token:"delimiter.curly",next:"@pop"}]],selectorname:[["#{",{token:"meta",next:"@variableinterpolation"}],["(\\.|#(?=[^{])|%|(@identifier)|:)+","tag"]],selectorattribute:[{include:"@term"},["]",{token:"delimiter.bracket",next:"@pop"}]],term:[{include:"@comments"},["url(\\-prefix)?\\(",{token:"meta",next:"@urldeclaration"}],{include:"@functioninvocation"},{include:"@numbers"},{include:"@strings"},{include:"@variablereference"},["(and\\b|or\\b|not\\b)","operator"],{include:"@name"},["([<>=\\+\\-\\*\\/\\^\\|\\~,])","operator"],[",","delimiter"],["!default","literal"],["\\(",{token:"delimiter.parenthesis",next:"@parenthizedterm"}]],rulevalue:[{include:"@term"},["!important","literal"],[";","delimiter","@pop"],["{",{token:"delimiter.curly",switchTo:"@nestedproperty"}],["(?=})",{token:"",next:"@pop"}]],nestedproperty:[["[*_]?@identifier@ws:","attribute.name","@rulevalue"],{include:"@comments"},["}",{token:"delimiter.curly",next:"@pop"}]],warndebug:[["[@](warn|debug)",{token:"keyword",next:"@declarationbody"}]],import:[["[@](import)",{token:"keyword",next:"@declarationbody"}]],variabledeclaration:[["\\$@identifier@ws:","variable.decl","@declarationbody"]],urldeclaration:[{include:"@strings"},["[^)\r\n]+","string"],["\\)",{token:"meta",next:"@pop"}]],parenthizedterm:[{include:"@term"},["\\)",{token:"delimiter.parenthesis",next:"@pop"}]],declarationbody:[{include:"@term"},[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}]],extendbody:[{include:"@selectorname"},["!optional","literal"],[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}]],variablereference:[["\\$@identifier","variable.ref"],["\\.\\.\\.","operator"],["#{",{token:"meta",next:"@variableinterpolation"}]],variableinterpolation:[{include:"@variablereference"},["}",{token:"meta",next:"@pop"}]],comments:[["\\/\\*","comment","@comment"],["\\/\\/+.*","comment"]],comment:[["\\*\\/","comment","@pop"],[".","comment"]],name:[["@identifier","attribute.value"]],numbers:[["(\\d*\\.)?\\d+([eE][\\-+]?\\d+)?",{token:"number",next:"@units"}],["#[0-9a-fA-F_]+(?!\\w)","number.hex"]],units:[["(em|ex|ch|rem|vmin|vmax|vw|vh|vm|cm|mm|in|px|pt|pc|deg|grad|rad|turn|s|ms|Hz|kHz|%)?","number","@pop"]],functiondeclaration:[["@identifier@ws\\(",{token:"meta",next:"@parameterdeclaration"}],["{",{token:"delimiter.curly",switchTo:"@functionbody"}]],mixindeclaration:[["@identifier@ws\\(",{token:"meta",next:"@parameterdeclaration"}],["@identifier","meta"],["{",{token:"delimiter.curly",switchTo:"@selectorbody"}]],parameterdeclaration:[["\\$@identifier@ws:","variable.decl"],["\\.\\.\\.","operator"],[",","delimiter"],{include:"@term"},["\\)",{token:"meta",next:"@pop"}]],includedeclaration:[{include:"@functioninvocation"},["@identifier","meta"],[";","delimiter","@pop"],["(?=})",{token:"",next:"@pop"}],["{",{token:"delimiter.curly",switchTo:"@selectorbody"}]],keyframedeclaration:[["@identifier","meta"],["{",{token:"delimiter.curly",switchTo:"@keyframebody"}]],keyframebody:[{include:"@term"},["{",{token:"delimiter.curly",next:"@selectorbody"}],["}",{token:"delimiter.curly",next:"@pop"}]],controlstatement:[["[@](if|else|for|while|each|media)",{token:"keyword.flow",next:"@controlstatementdeclaration"}]],controlstatementdeclaration:[["(in|from|through|if|to)\\b",{token:"keyword.flow"}],{include:"@term"},["{",{token:"delimiter.curly",switchTo:"@selectorbody"}]],functionbody:[["[@](return)",{token:"keyword"}],{include:"@variabledeclaration"},{include:"@term"},{include:"@controlstatement"},[";","delimiter"],["}",{token:"delimiter.curly",next:"@pop"}]],functioninvocation:[["@identifier\\(",{token:"meta",next:"@functionarguments"}]],functionarguments:[["\\$@identifier@ws:","attribute.name"],["[,]","delimiter"],{include:"@term"},["\\)",{token:"meta",next:"@pop"}]],strings:[['~?"',{token:"string.delimiter",next:"@stringenddoublequote"}],["~?'",{token:"string.delimiter",next:"@stringendquote"}]],stringenddoublequote:[["\\\\.","string"],['"',{token:"string.delimiter",next:"@pop"}],[".","string"]],stringendquote:[["\\\\.","string"],["'",{token:"string.delimiter",next:"@pop"}],[".","string"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/shell/shell.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/shell/shell.js new file mode 100644 index 0000000..84f7362 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/shell/shell.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/shell/shell",["require","exports"],(function(e,r){"use strict";Object.defineProperty(r,"__esModule",{value:!0}),r.language=r.conf=void 0,r.conf={comments:{lineComment:"#"},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"`",close:"`"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"`",close:"`"}]},r.language={defaultToken:"",ignoreCase:!0,tokenPostfix:".shell",brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"}],keywords:["if","then","do","else","elif","while","until","for","in","esac","fi","fin","fil","done","exit","set","unset","export","function"],builtins:["ab","awk","bash","beep","cat","cc","cd","chown","chmod","chroot","clear","cp","curl","cut","diff","echo","find","gawk","gcc","get","git","grep","hg","kill","killall","ln","ls","make","mkdir","openssl","mv","nc","node","npm","ping","ps","restart","rm","rmdir","sed","service","sh","shopt","shred","source","sort","sleep","ssh","start","stop","su","sudo","svn","tee","telnet","top","touch","vi","vim","wall","wc","wget","who","write","yes","zsh"],symbols:/[=>"]],autoClosingPairs:[{open:'"',close:'"',notIn:["string","comment"]},{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]}]},e.language={defaultToken:"",tokenPostfix:".sol",brackets:[{token:"delimiter.curly",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"},{token:"delimiter.angle",open:"<",close:">"}],keywords:["pragma","solidity","contract","library","using","struct","function","modifier","constructor","address","string","bool","Int","Uint","Byte","Fixed","Ufixed","int","int8","int16","int24","int32","int40","int48","int56","int64","int72","int80","int88","int96","int104","int112","int120","int128","int136","int144","int152","int160","int168","int176","int184","int192","int200","int208","int216","int224","int232","int240","int248","int256","uint","uint8","uint16","uint24","uint32","uint40","uint48","uint56","uint64","uint72","uint80","uint88","uint96","uint104","uint112","uint120","uint128","uint136","uint144","uint152","uint160","uint168","uint176","uint184","uint192","uint200","uint208","uint216","uint224","uint232","uint240","uint248","uint256","byte","bytes","bytes1","bytes2","bytes3","bytes4","bytes5","bytes6","bytes7","bytes8","bytes9","bytes10","bytes11","bytes12","bytes13","bytes14","bytes15","bytes16","bytes17","bytes18","bytes19","bytes20","bytes21","bytes22","bytes23","bytes24","bytes25","bytes26","bytes27","bytes28","bytes29","bytes30","bytes31","bytes32","fixed","fixed0x8","fixed0x16","fixed0x24","fixed0x32","fixed0x40","fixed0x48","fixed0x56","fixed0x64","fixed0x72","fixed0x80","fixed0x88","fixed0x96","fixed0x104","fixed0x112","fixed0x120","fixed0x128","fixed0x136","fixed0x144","fixed0x152","fixed0x160","fixed0x168","fixed0x176","fixed0x184","fixed0x192","fixed0x200","fixed0x208","fixed0x216","fixed0x224","fixed0x232","fixed0x240","fixed0x248","fixed0x256","fixed8x8","fixed8x16","fixed8x24","fixed8x32","fixed8x40","fixed8x48","fixed8x56","fixed8x64","fixed8x72","fixed8x80","fixed8x88","fixed8x96","fixed8x104","fixed8x112","fixed8x120","fixed8x128","fixed8x136","fixed8x144","fixed8x152","fixed8x160","fixed8x168","fixed8x176","fixed8x184","fixed8x192","fixed8x200","fixed8x208","fixed8x216","fixed8x224","fixed8x232","fixed8x240","fixed8x248","fixed16x8","fixed16x16","fixed16x24","fixed16x32","fixed16x40","fixed16x48","fixed16x56","fixed16x64","fixed16x72","fixed16x80","fixed16x88","fixed16x96","fixed16x104","fixed16x112","fixed16x120","fixed16x128","fixed16x136","fixed16x144","fixed16x152","fixed16x160","fixed16x168","fixed16x176","fixed16x184","fixed16x192","fixed16x200","fixed16x208","fixed16x216","fixed16x224","fixed16x232","fixed16x240","fixed24x8","fixed24x16","fixed24x24","fixed24x32","fixed24x40","fixed24x48","fixed24x56","fixed24x64","fixed24x72","fixed24x80","fixed24x88","fixed24x96","fixed24x104","fixed24x112","fixed24x120","fixed24x128","fixed24x136","fixed24x144","fixed24x152","fixed24x160","fixed24x168","fixed24x176","fixed24x184","fixed24x192","fixed24x200","fixed24x208","fixed24x216","fixed24x224","fixed24x232","fixed32x8","fixed32x16","fixed32x24","fixed32x32","fixed32x40","fixed32x48","fixed32x56","fixed32x64","fixed32x72","fixed32x80","fixed32x88","fixed32x96","fixed32x104","fixed32x112","fixed32x120","fixed32x128","fixed32x136","fixed32x144","fixed32x152","fixed32x160","fixed32x168","fixed32x176","fixed32x184","fixed32x192","fixed32x200","fixed32x208","fixed32x216","fixed32x224","fixed40x8","fixed40x16","fixed40x24","fixed40x32","fixed40x40","fixed40x48","fixed40x56","fixed40x64","fixed40x72","fixed40x80","fixed40x88","fixed40x96","fixed40x104","fixed40x112","fixed40x120","fixed40x128","fixed40x136","fixed40x144","fixed40x152","fixed40x160","fixed40x168","fixed40x176","fixed40x184","fixed40x192","fixed40x200","fixed40x208","fixed40x216","fixed48x8","fixed48x16","fixed48x24","fixed48x32","fixed48x40","fixed48x48","fixed48x56","fixed48x64","fixed48x72","fixed48x80","fixed48x88","fixed48x96","fixed48x104","fixed48x112","fixed48x120","fixed48x128","fixed48x136","fixed48x144","fixed48x152","fixed48x160","fixed48x168","fixed48x176","fixed48x184","fixed48x192","fixed48x200","fixed48x208","fixed56x8","fixed56x16","fixed56x24","fixed56x32","fixed56x40","fixed56x48","fixed56x56","fixed56x64","fixed56x72","fixed56x80","fixed56x88","fixed56x96","fixed56x104","fixed56x112","fixed56x120","fixed56x128","fixed56x136","fixed56x144","fixed56x152","fixed56x160","fixed56x168","fixed56x176","fixed56x184","fixed56x192","fixed56x200","fixed64x8","fixed64x16","fixed64x24","fixed64x32","fixed64x40","fixed64x48","fixed64x56","fixed64x64","fixed64x72","fixed64x80","fixed64x88","fixed64x96","fixed64x104","fixed64x112","fixed64x120","fixed64x128","fixed64x136","fixed64x144","fixed64x152","fixed64x160","fixed64x168","fixed64x176","fixed64x184","fixed64x192","fixed72x8","fixed72x16","fixed72x24","fixed72x32","fixed72x40","fixed72x48","fixed72x56","fixed72x64","fixed72x72","fixed72x80","fixed72x88","fixed72x96","fixed72x104","fixed72x112","fixed72x120","fixed72x128","fixed72x136","fixed72x144","fixed72x152","fixed72x160","fixed72x168","fixed72x176","fixed72x184","fixed80x8","fixed80x16","fixed80x24","fixed80x32","fixed80x40","fixed80x48","fixed80x56","fixed80x64","fixed80x72","fixed80x80","fixed80x88","fixed80x96","fixed80x104","fixed80x112","fixed80x120","fixed80x128","fixed80x136","fixed80x144","fixed80x152","fixed80x160","fixed80x168","fixed80x176","fixed88x8","fixed88x16","fixed88x24","fixed88x32","fixed88x40","fixed88x48","fixed88x56","fixed88x64","fixed88x72","fixed88x80","fixed88x88","fixed88x96","fixed88x104","fixed88x112","fixed88x120","fixed88x128","fixed88x136","fixed88x144","fixed88x152","fixed88x160","fixed88x168","fixed96x8","fixed96x16","fixed96x24","fixed96x32","fixed96x40","fixed96x48","fixed96x56","fixed96x64","fixed96x72","fixed96x80","fixed96x88","fixed96x96","fixed96x104","fixed96x112","fixed96x120","fixed96x128","fixed96x136","fixed96x144","fixed96x152","fixed96x160","fixed104x8","fixed104x16","fixed104x24","fixed104x32","fixed104x40","fixed104x48","fixed104x56","fixed104x64","fixed104x72","fixed104x80","fixed104x88","fixed104x96","fixed104x104","fixed104x112","fixed104x120","fixed104x128","fixed104x136","fixed104x144","fixed104x152","fixed112x8","fixed112x16","fixed112x24","fixed112x32","fixed112x40","fixed112x48","fixed112x56","fixed112x64","fixed112x72","fixed112x80","fixed112x88","fixed112x96","fixed112x104","fixed112x112","fixed112x120","fixed112x128","fixed112x136","fixed112x144","fixed120x8","fixed120x16","fixed120x24","fixed120x32","fixed120x40","fixed120x48","fixed120x56","fixed120x64","fixed120x72","fixed120x80","fixed120x88","fixed120x96","fixed120x104","fixed120x112","fixed120x120","fixed120x128","fixed120x136","fixed128x8","fixed128x16","fixed128x24","fixed128x32","fixed128x40","fixed128x48","fixed128x56","fixed128x64","fixed128x72","fixed128x80","fixed128x88","fixed128x96","fixed128x104","fixed128x112","fixed128x120","fixed128x128","fixed136x8","fixed136x16","fixed136x24","fixed136x32","fixed136x40","fixed136x48","fixed136x56","fixed136x64","fixed136x72","fixed136x80","fixed136x88","fixed136x96","fixed136x104","fixed136x112","fixed136x120","fixed144x8","fixed144x16","fixed144x24","fixed144x32","fixed144x40","fixed144x48","fixed144x56","fixed144x64","fixed144x72","fixed144x80","fixed144x88","fixed144x96","fixed144x104","fixed144x112","fixed152x8","fixed152x16","fixed152x24","fixed152x32","fixed152x40","fixed152x48","fixed152x56","fixed152x64","fixed152x72","fixed152x80","fixed152x88","fixed152x96","fixed152x104","fixed160x8","fixed160x16","fixed160x24","fixed160x32","fixed160x40","fixed160x48","fixed160x56","fixed160x64","fixed160x72","fixed160x80","fixed160x88","fixed160x96","fixed168x8","fixed168x16","fixed168x24","fixed168x32","fixed168x40","fixed168x48","fixed168x56","fixed168x64","fixed168x72","fixed168x80","fixed168x88","fixed176x8","fixed176x16","fixed176x24","fixed176x32","fixed176x40","fixed176x48","fixed176x56","fixed176x64","fixed176x72","fixed176x80","fixed184x8","fixed184x16","fixed184x24","fixed184x32","fixed184x40","fixed184x48","fixed184x56","fixed184x64","fixed184x72","fixed192x8","fixed192x16","fixed192x24","fixed192x32","fixed192x40","fixed192x48","fixed192x56","fixed192x64","fixed200x8","fixed200x16","fixed200x24","fixed200x32","fixed200x40","fixed200x48","fixed200x56","fixed208x8","fixed208x16","fixed208x24","fixed208x32","fixed208x40","fixed208x48","fixed216x8","fixed216x16","fixed216x24","fixed216x32","fixed216x40","fixed224x8","fixed224x16","fixed224x24","fixed224x32","fixed232x8","fixed232x16","fixed232x24","fixed240x8","fixed240x16","fixed248x8","ufixed","ufixed0x8","ufixed0x16","ufixed0x24","ufixed0x32","ufixed0x40","ufixed0x48","ufixed0x56","ufixed0x64","ufixed0x72","ufixed0x80","ufixed0x88","ufixed0x96","ufixed0x104","ufixed0x112","ufixed0x120","ufixed0x128","ufixed0x136","ufixed0x144","ufixed0x152","ufixed0x160","ufixed0x168","ufixed0x176","ufixed0x184","ufixed0x192","ufixed0x200","ufixed0x208","ufixed0x216","ufixed0x224","ufixed0x232","ufixed0x240","ufixed0x248","ufixed0x256","ufixed8x8","ufixed8x16","ufixed8x24","ufixed8x32","ufixed8x40","ufixed8x48","ufixed8x56","ufixed8x64","ufixed8x72","ufixed8x80","ufixed8x88","ufixed8x96","ufixed8x104","ufixed8x112","ufixed8x120","ufixed8x128","ufixed8x136","ufixed8x144","ufixed8x152","ufixed8x160","ufixed8x168","ufixed8x176","ufixed8x184","ufixed8x192","ufixed8x200","ufixed8x208","ufixed8x216","ufixed8x224","ufixed8x232","ufixed8x240","ufixed8x248","ufixed16x8","ufixed16x16","ufixed16x24","ufixed16x32","ufixed16x40","ufixed16x48","ufixed16x56","ufixed16x64","ufixed16x72","ufixed16x80","ufixed16x88","ufixed16x96","ufixed16x104","ufixed16x112","ufixed16x120","ufixed16x128","ufixed16x136","ufixed16x144","ufixed16x152","ufixed16x160","ufixed16x168","ufixed16x176","ufixed16x184","ufixed16x192","ufixed16x200","ufixed16x208","ufixed16x216","ufixed16x224","ufixed16x232","ufixed16x240","ufixed24x8","ufixed24x16","ufixed24x24","ufixed24x32","ufixed24x40","ufixed24x48","ufixed24x56","ufixed24x64","ufixed24x72","ufixed24x80","ufixed24x88","ufixed24x96","ufixed24x104","ufixed24x112","ufixed24x120","ufixed24x128","ufixed24x136","ufixed24x144","ufixed24x152","ufixed24x160","ufixed24x168","ufixed24x176","ufixed24x184","ufixed24x192","ufixed24x200","ufixed24x208","ufixed24x216","ufixed24x224","ufixed24x232","ufixed32x8","ufixed32x16","ufixed32x24","ufixed32x32","ufixed32x40","ufixed32x48","ufixed32x56","ufixed32x64","ufixed32x72","ufixed32x80","ufixed32x88","ufixed32x96","ufixed32x104","ufixed32x112","ufixed32x120","ufixed32x128","ufixed32x136","ufixed32x144","ufixed32x152","ufixed32x160","ufixed32x168","ufixed32x176","ufixed32x184","ufixed32x192","ufixed32x200","ufixed32x208","ufixed32x216","ufixed32x224","ufixed40x8","ufixed40x16","ufixed40x24","ufixed40x32","ufixed40x40","ufixed40x48","ufixed40x56","ufixed40x64","ufixed40x72","ufixed40x80","ufixed40x88","ufixed40x96","ufixed40x104","ufixed40x112","ufixed40x120","ufixed40x128","ufixed40x136","ufixed40x144","ufixed40x152","ufixed40x160","ufixed40x168","ufixed40x176","ufixed40x184","ufixed40x192","ufixed40x200","ufixed40x208","ufixed40x216","ufixed48x8","ufixed48x16","ufixed48x24","ufixed48x32","ufixed48x40","ufixed48x48","ufixed48x56","ufixed48x64","ufixed48x72","ufixed48x80","ufixed48x88","ufixed48x96","ufixed48x104","ufixed48x112","ufixed48x120","ufixed48x128","ufixed48x136","ufixed48x144","ufixed48x152","ufixed48x160","ufixed48x168","ufixed48x176","ufixed48x184","ufixed48x192","ufixed48x200","ufixed48x208","ufixed56x8","ufixed56x16","ufixed56x24","ufixed56x32","ufixed56x40","ufixed56x48","ufixed56x56","ufixed56x64","ufixed56x72","ufixed56x80","ufixed56x88","ufixed56x96","ufixed56x104","ufixed56x112","ufixed56x120","ufixed56x128","ufixed56x136","ufixed56x144","ufixed56x152","ufixed56x160","ufixed56x168","ufixed56x176","ufixed56x184","ufixed56x192","ufixed56x200","ufixed64x8","ufixed64x16","ufixed64x24","ufixed64x32","ufixed64x40","ufixed64x48","ufixed64x56","ufixed64x64","ufixed64x72","ufixed64x80","ufixed64x88","ufixed64x96","ufixed64x104","ufixed64x112","ufixed64x120","ufixed64x128","ufixed64x136","ufixed64x144","ufixed64x152","ufixed64x160","ufixed64x168","ufixed64x176","ufixed64x184","ufixed64x192","ufixed72x8","ufixed72x16","ufixed72x24","ufixed72x32","ufixed72x40","ufixed72x48","ufixed72x56","ufixed72x64","ufixed72x72","ufixed72x80","ufixed72x88","ufixed72x96","ufixed72x104","ufixed72x112","ufixed72x120","ufixed72x128","ufixed72x136","ufixed72x144","ufixed72x152","ufixed72x160","ufixed72x168","ufixed72x176","ufixed72x184","ufixed80x8","ufixed80x16","ufixed80x24","ufixed80x32","ufixed80x40","ufixed80x48","ufixed80x56","ufixed80x64","ufixed80x72","ufixed80x80","ufixed80x88","ufixed80x96","ufixed80x104","ufixed80x112","ufixed80x120","ufixed80x128","ufixed80x136","ufixed80x144","ufixed80x152","ufixed80x160","ufixed80x168","ufixed80x176","ufixed88x8","ufixed88x16","ufixed88x24","ufixed88x32","ufixed88x40","ufixed88x48","ufixed88x56","ufixed88x64","ufixed88x72","ufixed88x80","ufixed88x88","ufixed88x96","ufixed88x104","ufixed88x112","ufixed88x120","ufixed88x128","ufixed88x136","ufixed88x144","ufixed88x152","ufixed88x160","ufixed88x168","ufixed96x8","ufixed96x16","ufixed96x24","ufixed96x32","ufixed96x40","ufixed96x48","ufixed96x56","ufixed96x64","ufixed96x72","ufixed96x80","ufixed96x88","ufixed96x96","ufixed96x104","ufixed96x112","ufixed96x120","ufixed96x128","ufixed96x136","ufixed96x144","ufixed96x152","ufixed96x160","ufixed104x8","ufixed104x16","ufixed104x24","ufixed104x32","ufixed104x40","ufixed104x48","ufixed104x56","ufixed104x64","ufixed104x72","ufixed104x80","ufixed104x88","ufixed104x96","ufixed104x104","ufixed104x112","ufixed104x120","ufixed104x128","ufixed104x136","ufixed104x144","ufixed104x152","ufixed112x8","ufixed112x16","ufixed112x24","ufixed112x32","ufixed112x40","ufixed112x48","ufixed112x56","ufixed112x64","ufixed112x72","ufixed112x80","ufixed112x88","ufixed112x96","ufixed112x104","ufixed112x112","ufixed112x120","ufixed112x128","ufixed112x136","ufixed112x144","ufixed120x8","ufixed120x16","ufixed120x24","ufixed120x32","ufixed120x40","ufixed120x48","ufixed120x56","ufixed120x64","ufixed120x72","ufixed120x80","ufixed120x88","ufixed120x96","ufixed120x104","ufixed120x112","ufixed120x120","ufixed120x128","ufixed120x136","ufixed128x8","ufixed128x16","ufixed128x24","ufixed128x32","ufixed128x40","ufixed128x48","ufixed128x56","ufixed128x64","ufixed128x72","ufixed128x80","ufixed128x88","ufixed128x96","ufixed128x104","ufixed128x112","ufixed128x120","ufixed128x128","ufixed136x8","ufixed136x16","ufixed136x24","ufixed136x32","ufixed136x40","ufixed136x48","ufixed136x56","ufixed136x64","ufixed136x72","ufixed136x80","ufixed136x88","ufixed136x96","ufixed136x104","ufixed136x112","ufixed136x120","ufixed144x8","ufixed144x16","ufixed144x24","ufixed144x32","ufixed144x40","ufixed144x48","ufixed144x56","ufixed144x64","ufixed144x72","ufixed144x80","ufixed144x88","ufixed144x96","ufixed144x104","ufixed144x112","ufixed152x8","ufixed152x16","ufixed152x24","ufixed152x32","ufixed152x40","ufixed152x48","ufixed152x56","ufixed152x64","ufixed152x72","ufixed152x80","ufixed152x88","ufixed152x96","ufixed152x104","ufixed160x8","ufixed160x16","ufixed160x24","ufixed160x32","ufixed160x40","ufixed160x48","ufixed160x56","ufixed160x64","ufixed160x72","ufixed160x80","ufixed160x88","ufixed160x96","ufixed168x8","ufixed168x16","ufixed168x24","ufixed168x32","ufixed168x40","ufixed168x48","ufixed168x56","ufixed168x64","ufixed168x72","ufixed168x80","ufixed168x88","ufixed176x8","ufixed176x16","ufixed176x24","ufixed176x32","ufixed176x40","ufixed176x48","ufixed176x56","ufixed176x64","ufixed176x72","ufixed176x80","ufixed184x8","ufixed184x16","ufixed184x24","ufixed184x32","ufixed184x40","ufixed184x48","ufixed184x56","ufixed184x64","ufixed184x72","ufixed192x8","ufixed192x16","ufixed192x24","ufixed192x32","ufixed192x40","ufixed192x48","ufixed192x56","ufixed192x64","ufixed200x8","ufixed200x16","ufixed200x24","ufixed200x32","ufixed200x40","ufixed200x48","ufixed200x56","ufixed208x8","ufixed208x16","ufixed208x24","ufixed208x32","ufixed208x40","ufixed208x48","ufixed216x8","ufixed216x16","ufixed216x24","ufixed216x32","ufixed216x40","ufixed224x8","ufixed224x16","ufixed224x24","ufixed224x32","ufixed232x8","ufixed232x16","ufixed232x24","ufixed240x8","ufixed240x16","ufixed248x8","event","enum","let","mapping","private","public","external","inherited","payable","true","false","var","import","constant","if","else","for","else","for","while","do","break","continue","throw","returns","return","suicide","new","is","this","super"],operators:["=",">","<","!","~","?",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/,"number.float"],[/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/,"number.hex"],[/0[0-7']*[0-7](@integersuffix)/,"number.octal"],[/0[bB][0-1']*[0-1](@integersuffix)/,"number.binary"],[/\d[\d']*\d(@integersuffix)/,"number"],[/\d(@integersuffix)/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@doccomment"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],doccomment:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sophia/sophia.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sophia/sophia.js new file mode 100644 index 0000000..1518e61 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sophia/sophia.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/sophia/sophia",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"],["<",">"]],autoClosingPairs:[{open:'"',close:'"',notIn:["string","comment"]},{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]}]},t.language={defaultToken:"",tokenPostfix:".aes",brackets:[{token:"delimiter.curly",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"},{token:"delimiter.angle",open:"<",close:">"}],keywords:["contract","library","entrypoint","function","stateful","state","hash","signature","tuple","list","address","string","bool","int","record","datatype","type","option","oracle","oracle_query","Call","Bits","Bytes","Oracle","String","Crypto","Address","Auth","Chain","None","Some","bits","bytes","event","let","map","private","public","true","false","var","if","else","throw"],operators:["=",">","<","!","~","?","::",":","==","<=",">=","!=","&&","||","++","--","+","-","*","/","&","|","^","%","<<",">>",">>>","+=","-=","*=","/=","&=","|=","^=","%=","<<=",">>=",">>>="],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/\d*\d+[eE]([\-+]?\d+)?(@floatsuffix)/,"number.float"],[/\d*\.\d+([eE][\-+]?\d+)?(@floatsuffix)/,"number.float"],[/0[xX][0-9a-fA-F']*[0-9a-fA-F](@integersuffix)/,"number.hex"],[/0[0-7']*[0-7](@integersuffix)/,"number.octal"],[/0[bB][0-1']*[0-1](@integersuffix)/,"number.binary"],[/\d[\d']*\d(@integersuffix)/,"number"],[/\d(@integersuffix)/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"],[/'[^\\']'/,"string"],[/(')(@escapes)(')/,["string","string.escape","string"]],[/'/,"string.invalid"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@doccomment"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],doccomment:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sql/sql.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sql/sql.js new file mode 100644 index 0000000..06ccffc --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/sql/sql.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/sql/sql",["require","exports"],(function(E,T){"use strict";Object.defineProperty(T,"__esModule",{value:!0}),T.language=T.conf=void 0,T.conf={comments:{lineComment:"--",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},T.language={defaultToken:"",tokenPostfix:".sql",ignoreCase:!0,brackets:[{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"}],keywords:["ABORT_AFTER_WAIT","ABSENT","ABSOLUTE","ACCENT_SENSITIVITY","ACTION","ACTIVATION","ACTIVE","ADD","ADDRESS","ADMIN","AES","AES_128","AES_192","AES_256","AFFINITY","AFTER","AGGREGATE","ALGORITHM","ALL_CONSTRAINTS","ALL_ERRORMSGS","ALL_INDEXES","ALL_LEVELS","ALL_SPARSE_COLUMNS","ALLOW_CONNECTIONS","ALLOW_MULTIPLE_EVENT_LOSS","ALLOW_PAGE_LOCKS","ALLOW_ROW_LOCKS","ALLOW_SINGLE_EVENT_LOSS","ALLOW_SNAPSHOT_ISOLATION","ALLOWED","ALTER","ANONYMOUS","ANSI_DEFAULTS","ANSI_NULL_DEFAULT","ANSI_NULL_DFLT_OFF","ANSI_NULL_DFLT_ON","ANSI_NULLS","ANSI_PADDING","ANSI_WARNINGS","APPEND","APPLICATION","APPLICATION_LOG","ARITHABORT","ARITHIGNORE","AS","ASC","ASSEMBLY","ASYMMETRIC","ASYNCHRONOUS_COMMIT","AT","ATOMIC","ATTACH","ATTACH_REBUILD_LOG","AUDIT","AUDIT_GUID","AUTHENTICATION","AUTHORIZATION","AUTO","AUTO_CLEANUP","AUTO_CLOSE","AUTO_CREATE_STATISTICS","AUTO_SHRINK","AUTO_UPDATE_STATISTICS","AUTO_UPDATE_STATISTICS_ASYNC","AUTOMATED_BACKUP_PREFERENCE","AUTOMATIC","AVAILABILITY","AVAILABILITY_MODE","BACKUP","BACKUP_PRIORITY","BASE64","BATCHSIZE","BEGIN","BEGIN_DIALOG","BIGINT","BINARY","BINDING","BIT","BLOCKERS","BLOCKSIZE","BOUNDING_BOX","BREAK","BROKER","BROKER_INSTANCE","BROWSE","BUCKET_COUNT","BUFFER","BUFFERCOUNT","BULK","BULK_LOGGED","BY","CACHE","CALL","CALLED","CALLER","CAP_CPU_PERCENT","CASCADE","CASE","CATALOG","CATCH","CELLS_PER_OBJECT","CERTIFICATE","CHANGE_RETENTION","CHANGE_TRACKING","CHANGES","CHAR","CHARACTER","CHECK","CHECK_CONSTRAINTS","CHECK_EXPIRATION","CHECK_POLICY","CHECKALLOC","CHECKCATALOG","CHECKCONSTRAINTS","CHECKDB","CHECKFILEGROUP","CHECKIDENT","CHECKPOINT","CHECKTABLE","CLASSIFIER_FUNCTION","CLEANTABLE","CLEANUP","CLEAR","CLOSE","CLUSTER","CLUSTERED","CODEPAGE","COLLATE","COLLECTION","COLUMN","COLUMN_SET","COLUMNS","COLUMNSTORE","COLUMNSTORE_ARCHIVE","COMMIT","COMMITTED","COMPATIBILITY_LEVEL","COMPRESSION","COMPUTE","CONCAT","CONCAT_NULL_YIELDS_NULL","CONFIGURATION","CONNECT","CONSTRAINT","CONTAINMENT","CONTENT","CONTEXT","CONTINUE","CONTINUE_AFTER_ERROR","CONTRACT","CONTRACT_NAME","CONTROL","CONVERSATION","COOKIE","COPY_ONLY","COUNTER","CPU","CREATE","CREATE_NEW","CREATION_DISPOSITION","CREDENTIAL","CRYPTOGRAPHIC","CUBE","CURRENT","CURRENT_DATE","CURSOR","CURSOR_CLOSE_ON_COMMIT","CURSOR_DEFAULT","CYCLE","DATA","DATA_COMPRESSION","DATA_PURITY","DATABASE","DATABASE_DEFAULT","DATABASE_MIRRORING","DATABASE_SNAPSHOT","DATAFILETYPE","DATE","DATE_CORRELATION_OPTIMIZATION","DATEFIRST","DATEFORMAT","DATETIME","DATETIME2","DATETIMEOFFSET","DAY","DAYOFYEAR","DAYS","DB_CHAINING","DBCC","DBREINDEX","DDL_DATABASE_LEVEL_EVENTS","DEADLOCK_PRIORITY","DEALLOCATE","DEC","DECIMAL","DECLARE","DECRYPTION","DEFAULT","DEFAULT_DATABASE","DEFAULT_FULLTEXT_LANGUAGE","DEFAULT_LANGUAGE","DEFAULT_SCHEMA","DEFINITION","DELAY","DELAYED_DURABILITY","DELETE","DELETED","DENSITY_VECTOR","DENY","DEPENDENTS","DES","DESC","DESCRIPTION","DESX","DHCP","DIAGNOSTICS","DIALOG","DIFFERENTIAL","DIRECTORY_NAME","DISABLE","DISABLE_BROKER","DISABLED","DISK","DISTINCT","DISTRIBUTED","DOCUMENT","DOUBLE","DROP","DROP_EXISTING","DROPCLEANBUFFERS","DUMP","DURABILITY","DYNAMIC","EDITION","ELEMENTS","ELSE","EMERGENCY","EMPTY","EMPTYFILE","ENABLE","ENABLE_BROKER","ENABLED","ENCRYPTION","END","ENDPOINT","ENDPOINT_URL","ERRLVL","ERROR","ERROR_BROKER_CONVERSATIONS","ERRORFILE","ESCAPE","ESTIMATEONLY","EVENT","EVENT_RETENTION_MODE","EXEC","EXECUTABLE","EXECUTE","EXIT","EXPAND","EXPIREDATE","EXPIRY_DATE","EXPLICIT","EXTENDED_LOGICAL_CHECKS","EXTENSION","EXTERNAL","EXTERNAL_ACCESS","FAIL_OPERATION","FAILOVER","FAILOVER_MODE","FAILURE_CONDITION_LEVEL","FALSE","FAN_IN","FAST","FAST_FORWARD","FETCH","FIELDTERMINATOR","FILE","FILEGROUP","FILEGROWTH","FILELISTONLY","FILENAME","FILEPATH","FILESTREAM","FILESTREAM_ON","FILETABLE_COLLATE_FILENAME","FILETABLE_DIRECTORY","FILETABLE_FULLPATH_UNIQUE_CONSTRAINT_NAME","FILETABLE_NAMESPACE","FILETABLE_PRIMARY_KEY_CONSTRAINT_NAME","FILETABLE_STREAMID_UNIQUE_CONSTRAINT_NAME","FILLFACTOR","FILTERING","FIRE_TRIGGERS","FIRST","FIRSTROW","FLOAT","FMTONLY","FOLLOWING","FOR","FORCE","FORCE_FAILOVER_ALLOW_DATA_LOSS","FORCE_SERVICE_ALLOW_DATA_LOSS","FORCED","FORCEPLAN","FORCESCAN","FORCESEEK","FOREIGN","FORMATFILE","FORMSOF","FORWARD_ONLY","FREE","FREEPROCCACHE","FREESESSIONCACHE","FREESYSTEMCACHE","FROM","FULL","FULLSCAN","FULLTEXT","FUNCTION","GB","GEOGRAPHY_AUTO_GRID","GEOGRAPHY_GRID","GEOMETRY_AUTO_GRID","GEOMETRY_GRID","GET","GLOBAL","GO","GOTO","GOVERNOR","GRANT","GRIDS","GROUP","GROUP_MAX_REQUESTS","HADR","HASH","HASHED","HAVING","HEADERONLY","HEALTH_CHECK_TIMEOUT","HELP","HIERARCHYID","HIGH","HINT","HISTOGRAM","HOLDLOCK","HONOR_BROKER_PRIORITY","HOUR","HOURS","IDENTITY","IDENTITY_INSERT","IDENTITY_VALUE","IDENTITYCOL","IF","IGNORE_CONSTRAINTS","IGNORE_DUP_KEY","IGNORE_NONCLUSTERED_COLUMNSTORE_INDEX","IGNORE_TRIGGERS","IMAGE","IMMEDIATE","IMPERSONATE","IMPLICIT_TRANSACTIONS","IMPORTANCE","INCLUDE","INCREMENT","INCREMENTAL","INDEX","INDEXDEFRAG","INFINITE","INFLECTIONAL","INIT","INITIATOR","INPUT","INPUTBUFFER","INSENSITIVE","INSERT","INSERTED","INSTEAD","INT","INTEGER","INTO","IO","IP","ISABOUT","ISOLATION","JOB","KB","KEEP","KEEP_CDC","KEEP_NULLS","KEEP_REPLICATION","KEEPDEFAULTS","KEEPFIXED","KEEPIDENTITY","KEEPNULLS","KERBEROS","KEY","KEY_SOURCE","KEYS","KEYSET","KILL","KILOBYTES_PER_BATCH","LABELONLY","LANGUAGE","LAST","LASTROW","LEVEL","LEVEL_1","LEVEL_2","LEVEL_3","LEVEL_4","LIFETIME","LIMIT","LINENO","LIST","LISTENER","LISTENER_IP","LISTENER_PORT","LOAD","LOADHISTORY","LOB_COMPACTION","LOCAL","LOCAL_SERVICE_NAME","LOCK_ESCALATION","LOCK_TIMEOUT","LOGIN","LOGSPACE","LOOP","LOW","MANUAL","MARK","MARK_IN_USE_FOR_REMOVAL","MASTER","MAX_CPU_PERCENT","MAX_DISPATCH_LATENCY","MAX_DOP","MAX_DURATION","MAX_EVENT_SIZE","MAX_FILES","MAX_IOPS_PER_VOLUME","MAX_MEMORY","MAX_MEMORY_PERCENT","MAX_QUEUE_READERS","MAX_ROLLOVER_FILES","MAX_SIZE","MAXDOP","MAXERRORS","MAXLENGTH","MAXRECURSION","MAXSIZE","MAXTRANSFERSIZE","MAXVALUE","MB","MEDIADESCRIPTION","MEDIANAME","MEDIAPASSWORD","MEDIUM","MEMBER","MEMORY_OPTIMIZED","MEMORY_OPTIMIZED_DATA","MEMORY_OPTIMIZED_ELEVATE_TO_SNAPSHOT","MEMORY_PARTITION_MODE","MERGE","MESSAGE","MESSAGE_FORWARD_SIZE","MESSAGE_FORWARDING","MICROSECOND","MILLISECOND","MIN_CPU_PERCENT","MIN_IOPS_PER_VOLUME","MIN_MEMORY_PERCENT","MINUTE","MINUTES","MINVALUE","MIRROR","MIRROR_ADDRESS","MODIFY","MONEY","MONTH","MOVE","MULTI_USER","MUST_CHANGE","NAME","NANOSECOND","NATIONAL","NATIVE_COMPILATION","NCHAR","NEGOTIATE","NESTED_TRIGGERS","NEW_ACCOUNT","NEW_BROKER","NEW_PASSWORD","NEWNAME","NEXT","NO","NO_BROWSETABLE","NO_CHECKSUM","NO_COMPRESSION","NO_EVENT_LOSS","NO_INFOMSGS","NO_TRUNCATE","NO_WAIT","NOCHECK","NOCOUNT","NOEXEC","NOEXPAND","NOFORMAT","NOINDEX","NOINIT","NOLOCK","NON","NON_TRANSACTED_ACCESS","NONCLUSTERED","NONE","NORECOMPUTE","NORECOVERY","NORESEED","NORESET","NOREWIND","NORMAL","NOSKIP","NOTIFICATION","NOTRUNCATE","NOUNLOAD","NOWAIT","NTEXT","NTLM","NUMANODE","NUMERIC","NUMERIC_ROUNDABORT","NVARCHAR","OBJECT","OF","OFF","OFFLINE","OFFSET","OFFSETS","OLD_ACCOUNT","OLD_PASSWORD","ON","ON_FAILURE","ONLINE","ONLY","OPEN","OPEN_EXISTING","OPENTRAN","OPTIMISTIC","OPTIMIZE","OPTION","ORDER","OUT","OUTPUT","OUTPUTBUFFER","OVER","OVERRIDE","OWNER","OWNERSHIP","PAD_INDEX","PAGE","PAGE_VERIFY","PAGECOUNT","PAGLOCK","PARAMETERIZATION","PARSEONLY","PARTIAL","PARTITION","PARTITIONS","PARTNER","PASSWORD","PATH","PER_CPU","PER_NODE","PERCENT","PERMISSION_SET","PERSISTED","PHYSICAL_ONLY","PLAN","POISON_MESSAGE_HANDLING","POOL","POPULATION","PORT","PRECEDING","PRECISION","PRIMARY","PRIMARY_ROLE","PRINT","PRIOR","PRIORITY","PRIORITY_LEVEL","PRIVATE","PRIVILEGES","PROC","PROCCACHE","PROCEDURE","PROCEDURE_NAME","PROCESS","PROFILE","PROPERTY","PROPERTY_DESCRIPTION","PROPERTY_INT_ID","PROPERTY_SET_GUID","PROVIDER","PROVIDER_KEY_NAME","PUBLIC","PUT","QUARTER","QUERY","QUERY_GOVERNOR_COST_LIMIT","QUEUE","QUEUE_DELAY","QUOTED_IDENTIFIER","RAISERROR","RANGE","RAW","RC2","RC4","RC4_128","READ","READ_COMMITTED_SNAPSHOT","READ_ONLY","READ_ONLY_ROUTING_LIST","READ_ONLY_ROUTING_URL","READ_WRITE","READ_WRITE_FILEGROUPS","READCOMMITTED","READCOMMITTEDLOCK","READONLY","READPAST","READTEXT","READUNCOMMITTED","READWRITE","REAL","REBUILD","RECEIVE","RECOMPILE","RECONFIGURE","RECOVERY","RECURSIVE","RECURSIVE_TRIGGERS","REFERENCES","REGENERATE","RELATED_CONVERSATION","RELATED_CONVERSATION_GROUP","RELATIVE","REMOTE","REMOTE_PROC_TRANSACTIONS","REMOTE_SERVICE_NAME","REMOVE","REORGANIZE","REPAIR_ALLOW_DATA_LOSS","REPAIR_FAST","REPAIR_REBUILD","REPEATABLE","REPEATABLEREAD","REPLICA","REPLICATION","REQUEST_MAX_CPU_TIME_SEC","REQUEST_MAX_MEMORY_GRANT_PERCENT","REQUEST_MEMORY_GRANT_TIMEOUT_SEC","REQUIRED","RESAMPLE","RESEED","RESERVE_DISK_SPACE","RESET","RESOURCE","RESTART","RESTORE","RESTRICT","RESTRICTED_USER","RESULT","RESUME","RETAINDAYS","RETENTION","RETURN","RETURNS","REVERT","REVOKE","REWIND","REWINDONLY","ROBUST","ROLE","ROLLBACK","ROLLUP","ROOT","ROUTE","ROW","ROWCOUNT","ROWGUIDCOL","ROWLOCK","ROWS","ROWS_PER_BATCH","ROWTERMINATOR","ROWVERSION","RSA_1024","RSA_2048","RSA_512","RULE","SAFE","SAFETY","SAMPLE","SAVE","SCHEDULER","SCHEMA","SCHEMA_AND_DATA","SCHEMA_ONLY","SCHEMABINDING","SCHEME","SCROLL","SCROLL_LOCKS","SEARCH","SECOND","SECONDARY","SECONDARY_ONLY","SECONDARY_ROLE","SECONDS","SECRET","SECURITY_LOG","SECURITYAUDIT","SELECT","SELECTIVE","SELF","SEND","SENT","SEQUENCE","SERIALIZABLE","SERVER","SERVICE","SERVICE_BROKER","SERVICE_NAME","SESSION","SESSION_TIMEOUT","SET","SETS","SETUSER","SHOW_STATISTICS","SHOWCONTIG","SHOWPLAN","SHOWPLAN_ALL","SHOWPLAN_TEXT","SHOWPLAN_XML","SHRINKDATABASE","SHRINKFILE","SHUTDOWN","SID","SIGNATURE","SIMPLE","SINGLE_BLOB","SINGLE_CLOB","SINGLE_NCLOB","SINGLE_USER","SINGLETON","SIZE","SKIP","SMALLDATETIME","SMALLINT","SMALLMONEY","SNAPSHOT","SORT_IN_TEMPDB","SOURCE","SPARSE","SPATIAL","SPATIAL_WINDOW_MAX_CELLS","SPECIFICATION","SPLIT","SQL","SQL_VARIANT","SQLPERF","STANDBY","START","START_DATE","STARTED","STARTUP_STATE","STAT_HEADER","STATE","STATEMENT","STATIC","STATISTICAL_SEMANTICS","STATISTICS","STATISTICS_INCREMENTAL","STATISTICS_NORECOMPUTE","STATS","STATS_STREAM","STATUS","STATUSONLY","STOP","STOP_ON_ERROR","STOPAT","STOPATMARK","STOPBEFOREMARK","STOPLIST","STOPPED","SUBJECT","SUBSCRIPTION","SUPPORTED","SUSPEND","SWITCH","SYMMETRIC","SYNCHRONOUS_COMMIT","SYNONYM","SYSNAME","SYSTEM","TABLE","TABLERESULTS","TABLESAMPLE","TABLOCK","TABLOCKX","TAKE","TAPE","TARGET","TARGET_RECOVERY_TIME","TB","TCP","TEXT","TEXTIMAGE_ON","TEXTSIZE","THEN","THESAURUS","THROW","TIES","TIME","TIMEOUT","TIMER","TIMESTAMP","TINYINT","TO","TOP","TORN_PAGE_DETECTION","TRACEOFF","TRACEON","TRACESTATUS","TRACK_CAUSALITY","TRACK_COLUMNS_UPDATED","TRAN","TRANSACTION","TRANSFER","TRANSFORM_NOISE_WORDS","TRIGGER","TRIPLE_DES","TRIPLE_DES_3KEY","TRUE","TRUNCATE","TRUNCATEONLY","TRUSTWORTHY","TRY","TSQL","TWO_DIGIT_YEAR_CUTOFF","TYPE","TYPE_WARNING","UNBOUNDED","UNCHECKED","UNCOMMITTED","UNDEFINED","UNIQUE","UNIQUEIDENTIFIER","UNKNOWN","UNLIMITED","UNLOAD","UNSAFE","UPDATE","UPDATETEXT","UPDATEUSAGE","UPDLOCK","URL","USE","USED","USER","USEROPTIONS","USING","VALID_XML","VALIDATION","VALUE","VALUES","VARBINARY","VARCHAR","VARYING","VERIFYONLY","VERSION","VIEW","VIEW_METADATA","VIEWS","VISIBILITY","WAIT_AT_LOW_PRIORITY","WAITFOR","WEEK","WEIGHT","WELL_FORMED_XML","WHEN","WHERE","WHILE","WINDOWS","WITH","WITHIN","WITHOUT","WITNESS","WORK","WORKLOAD","WRITETEXT","XACT_ABORT","XLOCK","XMAX","XMIN","XML","XMLDATA","XMLNAMESPACES","XMLSCHEMA","XQUERY","XSINIL","YEAR","YMAX","YMIN"],operators:["ALL","AND","ANY","BETWEEN","EXISTS","IN","LIKE","NOT","OR","SOME","EXCEPT","INTERSECT","UNION","APPLY","CROSS","FULL","INNER","JOIN","LEFT","OUTER","RIGHT","CONTAINS","FREETEXT","IS","NULL","PIVOT","UNPIVOT","MATCHED"],builtinFunctions:["AVG","CHECKSUM_AGG","COUNT","COUNT_BIG","GROUPING","GROUPING_ID","MAX","MIN","SUM","STDEV","STDEVP","VAR","VARP","CUME_DIST","FIRST_VALUE","LAG","LAST_VALUE","LEAD","PERCENTILE_CONT","PERCENTILE_DISC","PERCENT_RANK","COLLATE","COLLATIONPROPERTY","TERTIARY_WEIGHTS","FEDERATION_FILTERING_VALUE","CAST","CONVERT","PARSE","TRY_CAST","TRY_CONVERT","TRY_PARSE","ASYMKEY_ID","ASYMKEYPROPERTY","CERTPROPERTY","CERT_ID","CRYPT_GEN_RANDOM","DECRYPTBYASYMKEY","DECRYPTBYCERT","DECRYPTBYKEY","DECRYPTBYKEYAUTOASYMKEY","DECRYPTBYKEYAUTOCERT","DECRYPTBYPASSPHRASE","ENCRYPTBYASYMKEY","ENCRYPTBYCERT","ENCRYPTBYKEY","ENCRYPTBYPASSPHRASE","HASHBYTES","IS_OBJECTSIGNED","KEY_GUID","KEY_ID","KEY_NAME","SIGNBYASYMKEY","SIGNBYCERT","SYMKEYPROPERTY","VERIFYSIGNEDBYCERT","VERIFYSIGNEDBYASYMKEY","CURSOR_STATUS","DATALENGTH","IDENT_CURRENT","IDENT_INCR","IDENT_SEED","IDENTITY","SQL_VARIANT_PROPERTY","CURRENT_TIMESTAMP","DATEADD","DATEDIFF","DATEFROMPARTS","DATENAME","DATEPART","DATETIME2FROMPARTS","DATETIMEFROMPARTS","DATETIMEOFFSETFROMPARTS","DAY","EOMONTH","GETDATE","GETUTCDATE","ISDATE","MONTH","SMALLDATETIMEFROMPARTS","SWITCHOFFSET","SYSDATETIME","SYSDATETIMEOFFSET","SYSUTCDATETIME","TIMEFROMPARTS","TODATETIMEOFFSET","YEAR","CHOOSE","COALESCE","IIF","NULLIF","ABS","ACOS","ASIN","ATAN","ATN2","CEILING","COS","COT","DEGREES","EXP","FLOOR","LOG","LOG10","PI","POWER","RADIANS","RAND","ROUND","SIGN","SIN","SQRT","SQUARE","TAN","APP_NAME","APPLOCK_MODE","APPLOCK_TEST","ASSEMBLYPROPERTY","COL_LENGTH","COL_NAME","COLUMNPROPERTY","DATABASE_PRINCIPAL_ID","DATABASEPROPERTYEX","DB_ID","DB_NAME","FILE_ID","FILE_IDEX","FILE_NAME","FILEGROUP_ID","FILEGROUP_NAME","FILEGROUPPROPERTY","FILEPROPERTY","FULLTEXTCATALOGPROPERTY","FULLTEXTSERVICEPROPERTY","INDEX_COL","INDEXKEY_PROPERTY","INDEXPROPERTY","OBJECT_DEFINITION","OBJECT_ID","OBJECT_NAME","OBJECT_SCHEMA_NAME","OBJECTPROPERTY","OBJECTPROPERTYEX","ORIGINAL_DB_NAME","PARSENAME","SCHEMA_ID","SCHEMA_NAME","SCOPE_IDENTITY","SERVERPROPERTY","STATS_DATE","TYPE_ID","TYPE_NAME","TYPEPROPERTY","DENSE_RANK","NTILE","RANK","ROW_NUMBER","PUBLISHINGSERVERNAME","OPENDATASOURCE","OPENQUERY","OPENROWSET","OPENXML","CERTENCODED","CERTPRIVATEKEY","CURRENT_USER","HAS_DBACCESS","HAS_PERMS_BY_NAME","IS_MEMBER","IS_ROLEMEMBER","IS_SRVROLEMEMBER","LOGINPROPERTY","ORIGINAL_LOGIN","PERMISSIONS","PWDENCRYPT","PWDCOMPARE","SESSION_USER","SESSIONPROPERTY","SUSER_ID","SUSER_NAME","SUSER_SID","SUSER_SNAME","SYSTEM_USER","USER","USER_ID","USER_NAME","ASCII","CHAR","CHARINDEX","CONCAT","DIFFERENCE","FORMAT","LEFT","LEN","LOWER","LTRIM","NCHAR","PATINDEX","QUOTENAME","REPLACE","REPLICATE","REVERSE","RIGHT","RTRIM","SOUNDEX","SPACE","STR","STUFF","SUBSTRING","UNICODE","UPPER","BINARY_CHECKSUM","CHECKSUM","CONNECTIONPROPERTY","CONTEXT_INFO","CURRENT_REQUEST_ID","ERROR_LINE","ERROR_NUMBER","ERROR_MESSAGE","ERROR_PROCEDURE","ERROR_SEVERITY","ERROR_STATE","FORMATMESSAGE","GETANSINULL","GET_FILESTREAM_TRANSACTION_CONTEXT","HOST_ID","HOST_NAME","ISNULL","ISNUMERIC","MIN_ACTIVE_ROWVERSION","NEWID","NEWSEQUENTIALID","ROWCOUNT_BIG","XACT_STATE","TEXTPTR","TEXTVALID","COLUMNS_UPDATED","EVENTDATA","TRIGGER_NESTLEVEL","UPDATE","CHANGETABLE","CHANGE_TRACKING_CONTEXT","CHANGE_TRACKING_CURRENT_VERSION","CHANGE_TRACKING_IS_COLUMN_IN_MASK","CHANGE_TRACKING_MIN_VALID_VERSION","CONTAINSTABLE","FREETEXTTABLE","SEMANTICKEYPHRASETABLE","SEMANTICSIMILARITYDETAILSTABLE","SEMANTICSIMILARITYTABLE","FILETABLEROOTPATH","GETFILENAMESPACEPATH","GETPATHLOCATOR","PATHNAME","GET_TRANSMISSION_STATUS"],builtinVariables:["@@DATEFIRST","@@DBTS","@@LANGID","@@LANGUAGE","@@LOCK_TIMEOUT","@@MAX_CONNECTIONS","@@MAX_PRECISION","@@NESTLEVEL","@@OPTIONS","@@REMSERVER","@@SERVERNAME","@@SERVICENAME","@@SPID","@@TEXTSIZE","@@VERSION","@@CURSOR_ROWS","@@FETCH_STATUS","@@DATEFIRST","@@PROCID","@@ERROR","@@IDENTITY","@@ROWCOUNT","@@TRANCOUNT","@@CONNECTIONS","@@CPU_BUSY","@@IDLE","@@IO_BUSY","@@PACKET_ERRORS","@@PACK_RECEIVED","@@PACK_SENT","@@TIMETICKS","@@TOTAL_ERRORS","@@TOTAL_READ","@@TOTAL_WRITE"],pseudoColumns:["$ACTION","$IDENTITY","$ROWGUID","$PARTITION"],tokenizer:{root:[{include:"@comments"},{include:"@whitespace"},{include:"@pseudoColumns"},{include:"@numbers"},{include:"@strings"},{include:"@complexIdentifiers"},{include:"@scopes"},[/[;,.]/,"delimiter"],[/[()]/,"@brackets"],[/[\w@#$]+/,{cases:{"@keywords":"keyword","@operators":"operator","@builtinVariables":"predefined","@builtinFunctions":"predefined","@default":"identifier"}}],[/[<>=!%&+\-*/|~^]/,"operator"]],whitespace:[[/\s+/,"white"]],comments:[[/--+.*/,"comment"],[/\/\*/,{token:"comment.quote",next:"@comment"}]],comment:[[/[^*/]+/,"comment"],[/\*\//,{token:"comment.quote",next:"@pop"}],[/./,"comment"]],pseudoColumns:[[/[$][A-Za-z_][\w@#$]*/,{cases:{"@pseudoColumns":"predefined","@default":"identifier"}}]],numbers:[[/0[xX][0-9a-fA-F]*/,"number"],[/[$][+-]*\d*(\.\d*)?/,"number"],[/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/,"number"]],strings:[[/N'/,{token:"string",next:"@string"}],[/'/,{token:"string",next:"@string"}]],string:[[/[^']+/,"string"],[/''/,"string"],[/'/,{token:"string",next:"@pop"}]],complexIdentifiers:[[/\[/,{token:"identifier.quote",next:"@bracketedIdentifier"}],[/"/,{token:"identifier.quote",next:"@quotedIdentifier"}]],bracketedIdentifier:[[/[^\]]+/,"identifier"],[/]]/,"identifier"],[/]/,{token:"identifier.quote",next:"@pop"}]],quotedIdentifier:[[/[^"]+/,"identifier"],[/""/,"identifier"],[/"/,{token:"identifier.quote",next:"@pop"}]],scopes:[[/BEGIN\s+(DISTRIBUTED\s+)?TRAN(SACTION)?\b/i,"keyword"],[/BEGIN\s+TRY\b/i,{token:"keyword.try"}],[/END\s+TRY\b/i,{token:"keyword.try"}],[/BEGIN\s+CATCH\b/i,{token:"keyword.catch"}],[/END\s+CATCH\b/i,{token:"keyword.catch"}],[/(BEGIN|CASE)\b/i,{token:"keyword.block"}],[/END\b/i,{token:"keyword.block"}],[/WHEN\b/i,{token:"keyword.choice"}],[/THEN\b/i,{token:"keyword.choice"}]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/st/st.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/st/st.js new file mode 100644 index 0000000..ffb986f --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/st/st.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/st/st",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"//",blockComment:["(*","*)"]},brackets:[["{","}"],["[","]"],["(",")"],["var","end_var"],["var_input","end_var"],["var_output","end_var"],["var_in_out","end_var"],["var_temp","end_var"],["var_global","end_var"],["var_access","end_var"],["var_external","end_var"],["type","end_type"],["struct","end_struct"],["program","end_program"],["function","end_function"],["function_block","end_function_block"],["action","end_action"],["step","end_step"],["initial_step","end_step"],["transaction","end_transaction"],["configuration","end_configuration"],["tcp","end_tcp"],["recource","end_recource"],["channel","end_channel"],["library","end_library"],["folder","end_folder"],["binaries","end_binaries"],["includes","end_includes"],["sources","end_sources"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"},{open:"/*",close:"*/"},{open:"'",close:"'",notIn:["string_sq"]},{open:'"',close:'"',notIn:["string_dq"]},{open:"var_input",close:"end_var"},{open:"var_output",close:"end_var"},{open:"var_in_out",close:"end_var"},{open:"var_temp",close:"end_var"},{open:"var_global",close:"end_var"},{open:"var_access",close:"end_var"},{open:"var_external",close:"end_var"},{open:"type",close:"end_type"},{open:"struct",close:"end_struct"},{open:"program",close:"end_program"},{open:"function",close:"end_function"},{open:"function_block",close:"end_function_block"},{open:"action",close:"end_action"},{open:"step",close:"end_step"},{open:"initial_step",close:"end_step"},{open:"transaction",close:"end_transaction"},{open:"configuration",close:"end_configuration"},{open:"tcp",close:"end_tcp"},{open:"recource",close:"end_recource"},{open:"channel",close:"end_channel"},{open:"library",close:"end_library"},{open:"folder",close:"end_folder"},{open:"binaries",close:"end_binaries"},{open:"includes",close:"end_includes"},{open:"sources",close:"end_sources"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"var",close:"end_var"},{open:"var_input",close:"end_var"},{open:"var_output",close:"end_var"},{open:"var_in_out",close:"end_var"},{open:"var_temp",close:"end_var"},{open:"var_global",close:"end_var"},{open:"var_access",close:"end_var"},{open:"var_external",close:"end_var"},{open:"type",close:"end_type"},{open:"struct",close:"end_struct"},{open:"program",close:"end_program"},{open:"function",close:"end_function"},{open:"function_block",close:"end_function_block"},{open:"action",close:"end_action"},{open:"step",close:"end_step"},{open:"initial_step",close:"end_step"},{open:"transaction",close:"end_transaction"},{open:"configuration",close:"end_configuration"},{open:"tcp",close:"end_tcp"},{open:"recource",close:"end_recource"},{open:"channel",close:"end_channel"},{open:"library",close:"end_library"},{open:"folder",close:"end_folder"},{open:"binaries",close:"end_binaries"},{open:"includes",close:"end_includes"},{open:"sources",close:"end_sources"}],folding:{markers:{start:new RegExp("^\\s*#pragma\\s+region\\b"),end:new RegExp("^\\s*#pragma\\s+endregion\\b")}}},n.language={defaultToken:"",tokenPostfix:".st",ignoreCase:!0,brackets:[{token:"delimiter.curly",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"}],keywords:["if","end_if","elsif","else","case","of","to","__try","__catch","__finally","do","with","by","while","repeat","end_while","end_repeat","end_case","for","end_for","task","retain","non_retain","constant","with","at","exit","return","interval","priority","address","port","on_channel","then","iec","file","uses","version","packagetype","displayname","copyright","summary","vendor","common_source","from","extends"],constant:["false","true","null"],defineKeywords:["var","var_input","var_output","var_in_out","var_temp","var_global","var_access","var_external","end_var","type","end_type","struct","end_struct","program","end_program","function","end_function","function_block","end_function_block","interface","end_interface","method","end_method","property","end_property","namespace","end_namespace","configuration","end_configuration","tcp","end_tcp","resource","end_resource","channel","end_channel","library","end_library","folder","end_folder","binaries","end_binaries","includes","end_includes","sources","end_sources","action","end_action","step","initial_step","end_step","transaction","end_transaction"],typeKeywords:["int","sint","dint","lint","usint","uint","udint","ulint","real","lreal","time","date","time_of_day","date_and_time","string","bool","byte","word","dword","array","pointer","lword"],operators:["=",">","<",":",":=","<=",">=","<>","&","+","-","*","**","MOD","^","or","and","not","xor","abs","acos","asin","atan","cos","exp","expt","ln","log","sin","sqrt","tan","sel","max","min","limit","mux","shl","shr","rol","ror","indexof","sizeof","adr","adrinst","bitadr","is_valid","ref","ref_to"],builtinVariables:[],builtinFunctions:["sr","rs","tp","ton","tof","eq","ge","le","lt","ne","round","trunc","ctd","сtu","ctud","r_trig","f_trig","move","concat","delete","find","insert","left","len","replace","right","rtc"],symbols:/[=>`?!+*\\\/]/,operatorstart:/[\/=\-+!*%<>&|^~?\u00A1-\u00A7\u00A9\u00AB\u00AC\u00AE\u00B0-\u00B1\u00B6\u00BB\u00BF\u00D7\u00F7\u2016-\u2017\u2020-\u2027\u2030-\u203E\u2041-\u2053\u2055-\u205E\u2190-\u23FF\u2500-\u2775\u2794-\u2BFF\u2E00-\u2E7F\u3001-\u3003\u3008-\u3030]/,operatorend:/[\u0300-\u036F\u1DC0-\u1DFF\u20D0-\u20FF\uFE00-\uFE0F\uFE20-\uFE2F\uE0100-\uE01EF]/,operators:/(@operatorstart)((@operatorstart)|(@operatorend))*/,escapes:/\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,tokenizer:{root:[{include:"@whitespace"},{include:"@comment"},{include:"@attribute"},{include:"@literal"},{include:"@keyword"},{include:"@invokedmethod"},{include:"@symbol"}],whitespace:[[/\s+/,"white"],[/"""/,"string.quote","@endDblDocString"]],endDblDocString:[[/[^"]+/,"string"],[/\\"/,"string"],[/"""/,"string.quote","@popall"],[/"/,"string"]],symbol:[[/[{}()\[\]]/,"@brackets"],[/[<>](?!@symbols)/,"@brackets"],[/[.]/,"delimiter"],[/@operators/,"operator"],[/@symbols/,"operator"]],comment:[[/\/\/\/.*$/,"comment.doc"],[/\/\*\*/,"comment.doc","@commentdocbody"],[/\/\/.*$/,"comment"],[/\/\*/,"comment","@commentbody"]],commentdocbody:[[/\/\*/,"comment","@commentbody"],[/\*\//,"comment.doc","@pop"],[/\:[a-zA-Z]+\:/,"comment.doc.param"],[/./,"comment.doc"]],commentbody:[[/\/\*/,"comment","@commentbody"],[/\*\//,"comment","@pop"],[/./,"comment"]],attribute:[[/\@@identifier/,{cases:{"@attributes":"keyword.control","@default":""}}]],literal:[[/"/,{token:"string.quote",next:"@stringlit"}],[/0[b]([01]_?)+/,"number.binary"],[/0[o]([0-7]_?)+/,"number.octal"],[/0[x]([0-9a-fA-F]_?)+([pP][\-+](\d_?)+)?/,"number.hex"],[/(\d_?)*\.(\d_?)+([eE][\-+]?(\d_?)+)?/,"number.float"],[/(\d_?)+/,"number"]],stringlit:[[/\\\(/,{token:"operator",next:"@interpolatedexpression"}],[/@escapes/,"string"],[/\\./,"string.escape.invalid"],[/"/,{token:"string.quote",next:"@pop"}],[/./,"string"]],interpolatedexpression:[[/\(/,{token:"operator",next:"@interpolatedexpression"}],[/\)/,{token:"operator",next:"@pop"}],{include:"@literal"},{include:"@keyword"},{include:"@symbol"}],keyword:[[/`/,{token:"operator",next:"@escapedkeyword"}],[/@identifier/,{cases:{"@keywords":"keyword","[A-Z][a-zA-Z0-9$]*":"type.identifier","@default":"identifier"}}]],escapedkeyword:[[/`/,{token:"operator",next:"@pop"}],[/./,"identifier"]],invokedmethod:[[/([.])(@identifier)/,{cases:{$2:["delimeter","type.identifier"],"@default":""}}]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/systemverilog/systemverilog.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/systemverilog/systemverilog.js new file mode 100644 index 0000000..1e107cd --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/systemverilog/systemverilog.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/systemverilog/systemverilog",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"],["begin","end"],["case","endcase"],["casex","endcase"],["casez","endcase"],["checker","endchecker"],["class","endclass"],["clocking","endclocking"],["config","endconfig"],["function","endfunction"],["generate","endgenerate"],["group","endgroup"],["interface","endinterface"],["module","endmodule"],["package","endpackage"],["primitive","endprimitive"],["program","endprogram"],["property","endproperty"],["specify","endspecify"],["sequence","endsequence"],["table","endtable"],["task","endtask"]],autoClosingPairs:[{open:"[",close:"]"},{open:"{",close:"}"},{open:"(",close:")"},{open:"'",close:"'",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string"]}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],folding:{offSide:!1,markers:{start:new RegExp("^(?:\\s*|.*(?!\\/[\\/\\*])[^\\w])(?:begin|case(x|z)?|class|clocking|config|covergroup|function|generate|interface|module|package|primitive|property|program|sequence|specify|table|task)\\b"),end:new RegExp("^(?:\\s*|.*(?!\\/[\\/\\*])[^\\w])(?:end|endcase|endclass|endclocking|endconfig|endgroup|endfunction|endgenerate|endinterface|endmodule|endpackage|endprimitive|endproperty|endprogram|endsequence|endspecify|endtable|endtask)\\b")}}},n.language={defaultToken:"",tokenPostfix:".sv",brackets:[{token:"delimiter.curly",open:"{",close:"}"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.square",open:"[",close:"]"},{token:"delimiter.angle",open:"<",close:">"}],keywords:["accept_on","alias","always","always_comb","always_ff","always_latch","and","assert","assign","assume","automatic","before","begin","bind","bins","binsof","bit","break","buf","bufif0","bufif1","byte","case","casex","casez","cell","chandle","checker","class","clocking","cmos","config","const","constraint","context","continue","cover","covergroup","coverpoint","cross","deassign","default","defparam","design","disable","dist","do","edge","else","end","endcase","endchecker","endclass","endclocking","endconfig","endfunction","endgenerate","endgroup","endinterface","endmodule","endpackage","endprimitive","endprogram","endproperty","endspecify","endsequence","endtable","endtask","enum","event","eventually","expect","export","extends","extern","final","first_match","for","force","foreach","forever","fork","forkjoin","function","generate","genvar","global","highz0","highz1","if","iff","ifnone","ignore_bins","illegal_bins","implements","implies","import","incdir","include","initial","inout","input","inside","instance","int","integer","interconnect","interface","intersect","join","join_any","join_none","large","let","liblist","library","local","localparam","logic","longint","macromodule","matches","medium","modport","module","nand","negedge","nettype","new","nexttime","nmos","nor","noshowcancelled","not","notif0","notif1","null","or","output","package","packed","parameter","pmos","posedge","primitive","priority","program","property","protected","pull0","pull1","pulldown","pullup","pulsestyle_ondetect","pulsestyle_onevent","pure","rand","randc","randcase","randsequence","rcmos","real","realtime","ref","reg","reject_on","release","repeat","restrict","return","rnmos","rpmos","rtran","rtranif0","rtranif1","s_always","s_eventually","s_nexttime","s_until","s_until_with","scalared","sequence","shortint","shortreal","showcancelled","signed","small","soft","solve","specify","specparam","static","string","strong","strong0","strong1","struct","super","supply0","supply1","sync_accept_on","sync_reject_on","table","tagged","task","this","throughout","time","timeprecision","timeunit","tran","tranif0","tranif1","tri","tri0","tri1","triand","trior","trireg","type","typedef","union","unique","unique0","unsigned","until","until_with","untyped","use","uwire","var","vectored","virtual","void","wait","wait_order","wand","weak","weak0","weak1","while","wildcard","wire","with","within","wor","xnor","xor"],builtin_gates:["and","nand","nor","or","xor","xnor","buf","not","bufif0","bufif1","notif1","notif0","cmos","nmos","pmos","rcmos","rnmos","rpmos","tran","tranif1","tranif0","rtran","rtranif1","rtranif0"],operators:["=","+=","-=","*=","/=","%=","&=","|=","^=","<<=",">>+","<<<=",">>>=","?",":","+","-","!","~","&","~&","|","~|","^","~^","^~","+","-","*","/","%","==","!=","===","!==","==?","!=?","&&","||","**","<","<=",">",">=","&","|","^",">>","<<",">>>","<<<","++","--","->","<->","inside","dist","::","+:","-:","*>","&&&","|->","|=>","#=#"],symbols:/[=>](?!@symbols)/,"@brackets"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],{include:"@numbers"},[/[;,.]/,"delimiter"],{include:"@strings"}],identifier_or_keyword:[[/@identifier/,{cases:{"@keywords":{token:"keyword.$0"},"@default":"identifier"}}]],numbers:[[/\d+?[\d_]*(?:\.[\d_]+)?[eE][\-+]?\d+/,"number.float"],[/\d+?[\d_]*\.[\d_]+(?:\s*@timeunits)?/,"number.float"],[/(?:\d+?[\d_]*\s*)?'[sS]?[dD]\s*[0-9xXzZ?]+?[0-9xXzZ?_]*/,"number"],[/(?:\d+?[\d_]*\s*)?'[sS]?[bB]\s*[0-1xXzZ?]+?[0-1xXzZ?_]*/,"number.binary"],[/(?:\d+?[\d_]*\s*)?'[sS]?[oO]\s*[0-7xXzZ?]+?[0-7xXzZ?_]*/,"number.octal"],[/(?:\d+?[\d_]*\s*)?'[sS]?[hH]\s*[0-9a-fA-FxXzZ?]+?[0-9a-fA-FxXzZ?_]*/,"number.hex"],[/1step/,"number"],[/[\dxXzZ]+?[\dxXzZ_]*(?:\s*@timeunits)?/,"number"],[/'[01xXzZ]+/,"number"]],module_instance:[{include:"@whitespace"},[/(#?)(\()/,["",{token:"@brackets",next:"@port_connection"}]],[/@identifier\s*[;={}\[\],]/,{token:"@rematch",next:"@pop"}],[/@symbols|[;={}\[\],]/,{token:"@rematch",next:"@pop"}],[/@identifier/,"type"],[/;/,"delimiter","@pop"]],port_connection:[{include:"@identifier_or_keyword"},{include:"@whitespace"},[/@systemcall/,"variable.predefined"],{include:"@numbers"},{include:"@strings"},[/[,]/,"delimiter"],[/\(/,"@brackets","@port_connection"],[/\)/,"@brackets","@pop"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],strings:[[/"([^"\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string"]],string:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],include:[[/(\s*)(")([\w*\/*]*)(.\w*)(")/,["","string.include.identifier","string.include.identifier","string.include.identifier",{token:"string.include.identifier",next:"@pop"}]],[/(\s*)(<)([\w*\/*]*)(.\w*)(>)/,["","string.include.identifier","string.include.identifier","string.include.identifier",{token:"string.include.identifier",next:"@pop"}]]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/tcl/tcl.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/tcl/tcl.js new file mode 100644 index 0000000..a00f67c --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/tcl/tcl.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/tcl/tcl",["require","exports"],(function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={brackets:[["{","}"],["[","]"],["(",")"]],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}]},t.language={tokenPostfix:".tcl",specialFunctions:["set","unset","rename","variable","proc","coroutine","foreach","incr","append","lappend","linsert","lreplace"],mainFunctions:["if","then","elseif","else","case","switch","while","for","break","continue","return","package","namespace","catch","exit","eval","expr","uplevel","upvar"],builtinFunctions:["file","info","concat","join","lindex","list","llength","lrange","lsearch","lsort","split","array","parray","binary","format","regexp","regsub","scan","string","subst","dict","cd","clock","exec","glob","pid","pwd","close","eof","fblocked","fconfigure","fcopy","fileevent","flush","gets","open","puts","read","seek","socket","tell","interp","after","auto_execok","auto_load","auto_mkindex","auto_reset","bgerror","error","global","history","load","source","time","trace","unknown","unset","update","vwait","winfo","wm","bind","event","pack","place","grid","font","bell","clipboard","destroy","focus","grab","lower","option","raise","selection","send","tk","tkwait","tk_bisque","tk_focusNext","tk_focusPrev","tk_focusFollowsMouse","tk_popup","tk_setPalette"],symbols:/[=>\/\s]+)/g,comments:{blockComment:["{#","#}"]},brackets:[["{#","#}"],["{%","%}"],["{{","}}"],["(",")"],["[","]"],["\x3c!--","--\x3e"],["<",">"]],autoClosingPairs:[{open:"{# ",close:" #}"},{open:"{% ",close:" %}"},{open:"{{ ",close:" }}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"'},{open:"'",close:"'"}],surroundingPairs:[{open:'"',close:'"'},{open:"'",close:"'"},{open:"<",close:">"}]},e.language={defaultToken:"",tokenPostfix:"",ignoreCase:!0,keywords:["apply","autoescape","block","deprecated","do","embed","extends","flush","for","from","if","import","include","macro","sandbox","set","use","verbatim","with","endapply","endautoescape","endblock","endembed","endfor","endif","endmacro","endsandbox","endset","endwith","true","false"],tokenizer:{root:[[/\s+/],[/{#/,"comment.twig","@commentState"],[/{%[-~]?/,"delimiter.twig","@blockState"],[/{{[-~]?/,"delimiter.twig","@variableState"],[/)/,["delimiter.html","tag.html","","delimiter.html"]],[/(<)(script)/,["delimiter.html",{token:"tag.html",next:"@script"}]],[/(<)(style)/,["delimiter.html",{token:"tag.html",next:"@style"}]],[/(<)((?:[\w\-]+:)?[\w\-]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/(<\/)((?:[\w\-]+:)?[\w\-]+)/,["delimiter.html",{token:"tag.html",next:"@otherTag"}]],[/|>=|<=/,"operators.twig"],[/(starts with|ends with|matches)(\s+)/,["operators.twig",""]],[/(in)(\s+)/,["operators.twig",""]],[/(is)(\s+)/,["operators.twig",""]],[/\||~|:|\.{1,2}|\?{1,2}/,"operators.twig"],[/[^\W\d][\w]*/,{cases:{"@keywords":"keyword.twig","@default":"variable.twig"}}],[/\d+(\.\d+)?/,"number.twig"],[/\(|\)|\[|\]|{|}|,/,"delimiter.twig"],[/"([^#"\\]*(?:\\.[^#"\\]*)*)"|\'([^\'\\]*(?:\\.[^\'\\]*)*)\'/,"string.twig"],[/"/,"string.twig","@stringState"],[/=>/,"operators.twig"],[/=/,"operators.twig"]],doctype:[[/[^>]+/,"metatag.content.html"],[/>/,"metatag.html","@pop"]],comment:[[/-->/,"comment.html","@pop"],[/[^-]+/,"comment.content.html"],[/./,"comment.content.html"]],otherTag:[[/\/?>/,"delimiter.html","@pop"],[/"([^"]*)"/,"attribute.value.html"],[/'([^']*)'/,"attribute.value.html"],[/[\w\-]+/,"attribute.name.html"],[/=/,"delimiter.html"],[/[ \t\r\n]+/]],script:[[/type/,"attribute.name.html","@scriptAfterType"],[/"([^"]*)"/,"attribute.value.html"],[/'([^']*)'/,"attribute.value.html"],[/[\w\-]+/,"attribute.name.html"],[/=/,"delimiter.html"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/(<\/)(script\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],scriptAfterType:[[/=/,"delimiter.html","@scriptAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value.html",switchTo:"@scriptWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value.html",switchTo:"@scriptWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@scriptEmbedded",nextEmbedded:"text/javascript"}],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptWithCustomType:[[/>/,{token:"delimiter.html",next:"@scriptEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value.html"],[/'([^']*)'/,"attribute.value.html"],[/[\w\-]+/,"attribute.name.html"],[/=/,"delimiter.html"],[/[ \t\r\n]+/],[/<\/script\s*>/,{token:"@rematch",next:"@pop"}]],scriptEmbedded:[[/<\/script/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]],style:[[/type/,"attribute.name.html","@styleAfterType"],[/"([^"]*)"/,"attribute.value.html"],[/'([^']*)'/,"attribute.value.html"],[/[\w\-]+/,"attribute.name.html"],[/=/,"delimiter.html"],[/>/,{token:"delimiter.html",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/(<\/)(style\s*)(>)/,["delimiter.html","tag.html",{token:"delimiter.html",next:"@pop"}]]],styleAfterType:[[/=/,"delimiter.html","@styleAfterTypeEquals"],[/>/,{token:"delimiter.html",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleAfterTypeEquals:[[/"([^"]*)"/,{token:"attribute.value.html",switchTo:"@styleWithCustomType.$1"}],[/'([^']*)'/,{token:"attribute.value.html",switchTo:"@styleWithCustomType.$1"}],[/>/,{token:"delimiter.html",next:"@styleEmbedded",nextEmbedded:"text/css"}],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleWithCustomType:[[/>/,{token:"delimiter.html",next:"@styleEmbedded.$S2",nextEmbedded:"$S2"}],[/"([^"]*)"/,"attribute.value.html"],[/'([^']*)'/,"attribute.value.html"],[/[\w\-]+/,"attribute.name.html"],[/=/,"delimiter.html"],[/[ \t\r\n]+/],[/<\/style\s*>/,{token:"@rematch",next:"@pop"}]],styleEmbedded:[[/<\/style/,{token:"@rematch",next:"@pop",nextEmbedded:"@pop"}],[/[^<]+/,""]]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/typescript/typescript.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/typescript/typescript.js new file mode 100644 index 0000000..42ccd45 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/typescript/typescript.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/typescript/typescript",["require","exports","../fillers/monaco-editor-core"],(function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.language=t.conf=void 0,t.conf={wordPattern:/(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g,comments:{lineComment:"//",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"]],onEnterRules:[{beforeText:/^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,afterText:/^\s*\*\/$/,action:{indentAction:n.languages.IndentAction.IndentOutdent,appendText:" * "}},{beforeText:/^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/,action:{indentAction:n.languages.IndentAction.None,appendText:" * "}},{beforeText:/^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/,action:{indentAction:n.languages.IndentAction.None,appendText:"* "}},{beforeText:/^(\t|(\ \ ))*\ \*\/\s*$/,action:{indentAction:n.languages.IndentAction.None,removeText:1}}],autoClosingPairs:[{open:"{",close:"}"},{open:"[",close:"]"},{open:"(",close:")"},{open:'"',close:'"',notIn:["string"]},{open:"'",close:"'",notIn:["string","comment"]},{open:"`",close:"`",notIn:["string","comment"]},{open:"/**",close:" */",notIn:["string"]}],folding:{markers:{start:new RegExp("^\\s*//\\s*#?region\\b"),end:new RegExp("^\\s*//\\s*#?endregion\\b")}}},t.language={defaultToken:"invalid",tokenPostfix:".ts",keywords:["abstract","any","as","asserts","bigint","boolean","break","case","catch","class","continue","const","constructor","debugger","declare","default","delete","do","else","enum","export","extends","false","finally","for","from","function","get","if","implements","import","in","infer","instanceof","interface","is","keyof","let","module","namespace","never","new","null","number","object","package","private","protected","public","readonly","require","global","return","set","static","string","super","switch","symbol","this","throw","true","try","type","typeof","undefined","unique","unknown","var","void","while","with","yield","async","await","of"],operators:["<=",">=","==","!=","===","!==","=>","+","-","**","*","/","%","++","--","<<",">",">>>","&","|","^","!","~","&&","||","??","?",":","=","+=","-=","*=","**=","/=","%=","<<=",">>=",">>>=","&=","|=","^=","@"],symbols:/[=>](?!@symbols)/,"@brackets"],[/!(?=([^=]|$))/,"delimiter"],[/@symbols/,{cases:{"@operators":"delimiter","@default":""}}],[/(@digits)[eE]([\-+]?(@digits))?/,"number.float"],[/(@digits)\.(@digits)([eE][\-+]?(@digits))?/,"number.float"],[/0[xX](@hexdigits)n?/,"number.hex"],[/0[oO]?(@octaldigits)n?/,"number.octal"],[/0[bB](@binarydigits)n?/,"number.binary"],[/(@digits)n?/,"number"],[/[;,.]/,"delimiter"],[/"([^"\\]|\\.)*$/,"string.invalid"],[/'([^'\\]|\\.)*$/,"string.invalid"],[/"/,"string","@string_double"],[/'/,"string","@string_single"],[/`/,"string","@string_backtick"]],whitespace:[[/[ \t\r\n]+/,""],[/\/\*\*(?!\/)/,"comment.doc","@jsdoc"],[/\/\*/,"comment","@comment"],[/\/\/.*$/,"comment"]],comment:[[/[^\/*]+/,"comment"],[/\*\//,"comment","@pop"],[/[\/*]/,"comment"]],jsdoc:[[/[^\/*]+/,"comment.doc"],[/\*\//,"comment.doc","@pop"],[/[\/*]/,"comment.doc"]],regexp:[[/(\{)(\d+(?:,\d*)?)(\})/,["regexp.escape.control","regexp.escape.control","regexp.escape.control"]],[/(\[)(\^?)(?=(?:[^\]\\\/]|\\.)+)/,["regexp.escape.control",{token:"regexp.escape.control",next:"@regexrange"}]],[/(\()(\?:|\?=|\?!)/,["regexp.escape.control","regexp.escape.control"]],[/[()]/,"regexp.escape.control"],[/@regexpctl/,"regexp.escape.control"],[/[^\\\/]/,"regexp"],[/@regexpesc/,"regexp.escape"],[/\\\./,"regexp.invalid"],[/(\/)([gimsuy]*)/,[{token:"regexp",bracket:"@close",next:"@pop"},"keyword.other"]]],regexrange:[[/-/,"regexp.escape.control"],[/\^/,"regexp.invalid"],[/@regexpesc/,"regexp.escape"],[/[^\]]/,"regexp"],[/\]/,{token:"regexp.escape.control",next:"@pop",bracket:"@close"}]],string_double:[[/[^\\"]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/"/,"string","@pop"]],string_single:[[/[^\\']+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/'/,"string","@pop"]],string_backtick:[[/\$\{/,{token:"delimiter.bracket",next:"@bracketCounting"}],[/[^\\`$]+/,"string"],[/@escapes/,"string.escape"],[/\\./,"string.escape.invalid"],[/`/,"string","@pop"]],bracketCounting:[[/\{/,"delimiter.bracket","@bracketCounting"],[/\}/,"delimiter.bracket","@pop"],{include:"common"}]}}})); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/vb/vb.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/vb/vb.js new file mode 100644 index 0000000..e09b2f8 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/basic-languages/vb/vb.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-languages version: 2.3.0(57af10ae0184db4e0f7f9a92ff972629c39ccb53) + * Released under the MIT license + * https://github.com/Microsoft/monaco-languages/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/basic-languages/vb/vb",["require","exports"],(function(e,n){"use strict";Object.defineProperty(n,"__esModule",{value:!0}),n.language=n.conf=void 0,n.conf={comments:{lineComment:"'",blockComment:["/*","*/"]},brackets:[["{","}"],["[","]"],["(",")"],["<",">"],["addhandler","end addhandler"],["class","end class"],["enum","end enum"],["event","end event"],["function","end function"],["get","end get"],["if","end if"],["interface","end interface"],["module","end module"],["namespace","end namespace"],["operator","end operator"],["property","end property"],["raiseevent","end raiseevent"],["removehandler","end removehandler"],["select","end select"],["set","end set"],["structure","end structure"],["sub","end sub"],["synclock","end synclock"],["try","end try"],["while","end while"],["with","end with"],["using","end using"],["do","loop"],["for","next"]],autoClosingPairs:[{open:"{",close:"}",notIn:["string","comment"]},{open:"[",close:"]",notIn:["string","comment"]},{open:"(",close:")",notIn:["string","comment"]},{open:'"',close:'"',notIn:["string","comment"]},{open:"<",close:">",notIn:["string","comment"]}],folding:{markers:{start:new RegExp("^\\s*#Region\\b"),end:new RegExp("^\\s*#End Region\\b")}}},n.language={defaultToken:"",tokenPostfix:".vb",ignoreCase:!0,brackets:[{token:"delimiter.bracket",open:"{",close:"}"},{token:"delimiter.array",open:"[",close:"]"},{token:"delimiter.parenthesis",open:"(",close:")"},{token:"delimiter.angle",open:"<",close:">"},{token:"keyword.tag-addhandler",open:"addhandler",close:"end addhandler"},{token:"keyword.tag-class",open:"class",close:"end class"},{token:"keyword.tag-enum",open:"enum",close:"end enum"},{token:"keyword.tag-event",open:"event",close:"end event"},{token:"keyword.tag-function",open:"function",close:"end function"},{token:"keyword.tag-get",open:"get",close:"end get"},{token:"keyword.tag-if",open:"if",close:"end if"},{token:"keyword.tag-interface",open:"interface",close:"end interface"},{token:"keyword.tag-module",open:"module",close:"end module"},{token:"keyword.tag-namespace",open:"namespace",close:"end namespace"},{token:"keyword.tag-operator",open:"operator",close:"end operator"},{token:"keyword.tag-property",open:"property",close:"end property"},{token:"keyword.tag-raiseevent",open:"raiseevent",close:"end raiseevent"},{token:"keyword.tag-removehandler",open:"removehandler",close:"end removehandler"},{token:"keyword.tag-select",open:"select",close:"end select"},{token:"keyword.tag-set",open:"set",close:"end set"},{token:"keyword.tag-structure",open:"structure",close:"end structure"},{token:"keyword.tag-sub",open:"sub",close:"end sub"},{token:"keyword.tag-synclock",open:"synclock",close:"end synclock"},{token:"keyword.tag-try",open:"try",close:"end try"},{token:"keyword.tag-while",open:"while",close:"end while"},{token:"keyword.tag-with",open:"with",close:"end with"},{token:"keyword.tag-using",open:"using",close:"end using"},{token:"keyword.tag-do",open:"do",close:"loop"},{token:"keyword.tag-for",open:"for",close:"next"}],keywords:["AddHandler","AddressOf","Alias","And","AndAlso","As","Async","Boolean","ByRef","Byte","ByVal","Call","Case","Catch","CBool","CByte","CChar","CDate","CDbl","CDec","Char","CInt","Class","CLng","CObj","Const","Continue","CSByte","CShort","CSng","CStr","CType","CUInt","CULng","CUShort","Date","Decimal","Declare","Default","Delegate","Dim","DirectCast","Do","Double","Each","Else","ElseIf","End","EndIf","Enum","Erase","Error","Event","Exit","False","Finally","For","Friend","Function","Get","GetType","GetXMLNamespace","Global","GoSub","GoTo","Handles","If","Implements","Imports","In","Inherits","Integer","Interface","Is","IsNot","Let","Lib","Like","Long","Loop","Me","Mod","Module","MustInherit","MustOverride","MyBase","MyClass","NameOf","Namespace","Narrowing","New","Next","Not","Nothing","NotInheritable","NotOverridable","Object","Of","On","Operator","Option","Optional","Or","OrElse","Out","Overloads","Overridable","Overrides","ParamArray","Partial","Private","Property","Protected","Public","RaiseEvent","ReadOnly","ReDim","RemoveHandler","Resume","Return","SByte","Select","Set","Shadows","Shared","Short","Single","Static","Step","Stop","String","Structure","Sub","SyncLock","Then","Throw","To","True","Try","TryCast","TypeOf","UInteger","ULong","UShort","Using","Variant","Wend","When","While","Widening","With","WithEvents","WriteOnly","Xor"],tagwords:["If","Sub","Select","Try","Class","Enum","Function","Get","Interface","Module","Namespace","Operator","Set","Structure","Using","While","With","Do","Loop","For","Next","Property","Continue","AddHandler","RemoveHandler","Event","RaiseEvent","SyncLock"],symbols:/[=>"]],autoClosingPairs:[{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],surroundingPairs:[{open:"<",close:">"},{open:"'",close:"'"},{open:'"',close:'"'}],onEnterRules:[{beforeText:new RegExp("<([_:\\w][_:\\w-.\\d]*)([^/>]*(?!/)>)[^<]*$","i"),afterText:/^<\/([_:\w][_:\w-.\d]*)\s*>$/i,action:{indentAction:n.languages.IndentAction.IndentOutdent}},{beforeText:new RegExp("<(\\w[\\w\\d]*)([^/>]*(?!/)>)[^<]*$","i"),action:{indentAction:n.languages.IndentAction.Indent}}]},t.language={defaultToken:"",tokenPostfix:".xml",ignoreCase:!0,qualifiedName:/(?:[\w\.\-]+:)?[\w\.\-]+/,tokenizer:{root:[[/[^<&]+/,""],{include:"@whitespace"},[/(<)(@qualifiedName)/,[{token:"delimiter"},{token:"tag",next:"@tag"}]],[/(<\/)(@qualifiedName)(\s*)(>)/,[{token:"delimiter"},{token:"tag"},"",{token:"delimiter"}]],[/(<\?)(@qualifiedName)/,[{token:"delimiter"},{token:"metatag",next:"@tag"}]],[/(<\!)(@qualifiedName)/,[{token:"delimiter"},{token:"metatag",next:"@tag"}]],[/<\!\[CDATA\[/,{token:"delimiter.cdata",next:"@cdata"}],[/&\w+;/,"string.escape"]],cdata:[[/[^\]]+/,""],[/\]\]>/,{token:"delimiter.cdata",next:"@pop"}],[/\]/,""]],tag:[[/[ \t\r\n]+/,""],[/(@qualifiedName)(\s*=\s*)("[^"]*"|'[^']*')/,["attribute.name","","attribute.value"]],[/(@qualifiedName)(\s*=\s*)("[^">?\/]*|'[^'>?\/]*)(?=[\?\/]\>)/,["attribute.name","","attribute.value"]],[/(@qualifiedName)(\s*=\s*)("[^">]*|'[^'>]*)/,["attribute.name","","attribute.value"]],[/@qualifiedName/,"attribute.name"],[/\?>/,{token:"delimiter",next:"@pop"}],[/(\/)(>)/,[{token:"tag"},{token:"delimiter",next:"@pop"}]],[/>/,{token:"delimiter",next:"@pop"}]],whitespace:[[/[ \t\r\n]+/,""],[//,{token:"comment",next:"@pop"}],[/");y>=0&&(u.comment&&u.comment(a.substring(4,y)),a=a.substring(y+3),n=!1)}function _(){if(!!n){var y,L=a.indexOf("<");L>=0?(y=a.substring(0,L),a=a.substring(L)):(y=a,a=""),u.chars&&u.chars(y)}}function f(y,L,I,k){var E={},T=M(L),O=S.voids[T]||!!k;I.replace(g,A),O||r.push(T),u.start&&u.start(T,E,O);function A(B,F,D,R,W){D===void 0&&R===void 0&&W===void 0?E[F]=void 0:E[F]=N.decode(D||R||W||"")}}function v(y,L){var I,k=0,E=M(L);if(E)for(k=r.length-1;k>=0&&r[k]!==E;k--);if(k>=0){for(I=r.length-1;I>=k;I--)u.end&&u.end(r[I]);r.length=k}}}e.exports=s},{"./attributes":1,"./elements":3,"./lowercase":5,he:9}],8:[function(q,e,b){"use strict";var N=q("he"),M=q("./lowercase"),w=q("./attributes"),S=q("./elements");function C(d,g){var p,c,o=g||{};return l(),{start:a,end:u,chars:i};function s(h){d.push(h)}function a(h,m,_){var f=M(h);if(c.ignoring){n(f);return}if((o.allowedTags||[]).indexOf(f)===-1){n(f);return}if(o.filter&&!o.filter({tag:f,attrs:m})){n(f);return}s("<"),s(f),Object.keys(m).forEach(v),s(_?"/>":">");function v(y){var L=m[y],I=(o.allowedClasses||{})[f]||[],k=(o.allowedAttributes||{})[f]||[];k=k.concat((o.allowedAttributes||{})["*"]||[]);var E,T=M(y);T==="class"&&k.indexOf(T)===-1?(L=L.split(" ").filter(O).join(" ").trim(),E=L.length):E=k.indexOf(T)!==-1&&(w.uris[T]!==!0||r(L)),E&&(s(" "),s(y),typeof L=="string"&&(s('="'),s(N.encode(L)),s('"')));function O(A){return I&&I.indexOf(A)!==-1}}}function u(h){var m=M(h),_=(o.allowedTags||[]).indexOf(m)!==-1;_&&c.ignoring===!1?(s("")):t(m)}function r(h){var m=h[0];if(m==="#"||m==="/")return!0;var _=h.indexOf(":");if(_===-1)return!0;var f=h.indexOf("?");if(f!==-1&&_>f)return!0;var v=h.indexOf("#");if(v!==-1&&_>v)return!0;return o.allowedSchemes.some(y);function y(L){return h.indexOf(L+":")===0}}function i(h){c.ignoring===!1&&s(o.transformText?o.transformText(h):h)}function n(h){S.voids[h]||(c.ignoring===!1?c={ignoring:h,depth:1}:c.ignoring===h&&c.depth++)}function t(h){c.ignoring===h&&--c.depth<=0&&l()}function l(){c={ignoring:!1,depth:0}}}e.exports=C},{"./attributes":1,"./elements":3,"./lowercase":5,he:9}],9:[function(q,e,b){"use strict";var N={"&":"&","<":"<",">":">",'"':""","'":"'"},M={"&":"&","<":"<",">":">",""":'"',"'":"'"},w=/(&|<|>|"|')/g,S=/[&<>"']/g;function C(c){return N[c]}function d(c){return M[c]}function g(c){return c==null?"":String(c).replace(S,C)}function p(c){return c==null?"":String(c).replace(w,d)}g.options=p.options={},e.exports={encode:g,escape:g,decode:p,unescape:p,version:"1.0.0-browser"}},{}],10:[function(q,e,b){"use strict";function N(w){return w.reduce(M,{})}function M(w,S){return w[S]=!0,w}e.exports=N},{}]},{},[4]),define("vs/base/common/insane/insane",function(){return{insane:kt}}),define(Q[54],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Iterable=void 0;var b;(function(N){function M(h){return h&&typeof h=="object"&&typeof h[Symbol.iterator]=="function"}N.is=M;const w=Object.freeze([]);function S(){return w}N.empty=S;function*C(h){yield h}N.single=C;function d(h){return h||w}N.from=d;function g(h){return!h||h[Symbol.iterator]().next().done===!0}N.isEmpty=g;function p(h){return h[Symbol.iterator]().next().value}N.first=p;function c(h,m){for(const _ of h)if(m(_))return!0;return!1}N.some=c;function o(h,m){for(const _ of h)if(m(_))return _}N.find=o;function*s(h,m){for(const _ of h)m(_)&&(yield _)}N.filter=s;function*a(h,m){for(const _ of h)yield m(_)}N.map=a;function*u(...h){for(const m of h)for(const _ of m)yield _}N.concat=u;function*r(h){for(const m of h)for(const _ of m)yield _}N.concatNested=r;function i(h,m,_){let f=_;for(const v of h)f=m(f,v);return f}N.reduce=i;function*n(h,m,_=h.length){for(m<0&&(m+=h.length),_<0?_+=h.length:_>h.length&&(_=h.length);m<_;m++)yield h[m]}N.slice=n;function t(h,m=Number.POSITIVE_INFINITY){const _=[];if(m===0)return[_,h];const f=h[Symbol.iterator]();for(let v=0;vf===v){const f=h[Symbol.iterator](),v=m[Symbol.iterator]();for(;;){const y=f.next(),L=v.next();if(y.done!==L.done)return!1;if(y.done)return!0;if(!_(y.value,L.value))return!1}}N.equals=l})(b=e.Iterable||(e.Iterable={}))}),define(Q[39],J([0,1,12]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ResolvedKeybinding=e.ResolvedKeybindingPart=e.ChordKeybinding=e.SimpleKeybinding=e.createSimpleKeybinding=e.createKeybinding=e.KeyChord=e.KeyCodeUtils=void 0;class N{constructor(){this._keyCodeToStr=[],this._strToKeyCode=Object.create(null)}define(r,i){this._keyCodeToStr[r]=i,this._strToKeyCode[i.toLowerCase()]=r}keyCodeToStr(r){return this._keyCodeToStr[r]}strToKeyCode(r){return this._strToKeyCode[r.toLowerCase()]||0}}const M=new N,w=new N,S=new N;(function(){function u(r,i,n=i,t=n){M.define(r,i),w.define(r,n),S.define(r,t)}u(0,"unknown"),u(1,"Backspace"),u(2,"Tab"),u(3,"Enter"),u(4,"Shift"),u(5,"Ctrl"),u(6,"Alt"),u(7,"PauseBreak"),u(8,"CapsLock"),u(9,"Escape"),u(10,"Space"),u(11,"PageUp"),u(12,"PageDown"),u(13,"End"),u(14,"Home"),u(15,"LeftArrow","Left"),u(16,"UpArrow","Up"),u(17,"RightArrow","Right"),u(18,"DownArrow","Down"),u(19,"Insert"),u(20,"Delete"),u(21,"0"),u(22,"1"),u(23,"2"),u(24,"3"),u(25,"4"),u(26,"5"),u(27,"6"),u(28,"7"),u(29,"8"),u(30,"9"),u(31,"A"),u(32,"B"),u(33,"C"),u(34,"D"),u(35,"E"),u(36,"F"),u(37,"G"),u(38,"H"),u(39,"I"),u(40,"J"),u(41,"K"),u(42,"L"),u(43,"M"),u(44,"N"),u(45,"O"),u(46,"P"),u(47,"Q"),u(48,"R"),u(49,"S"),u(50,"T"),u(51,"U"),u(52,"V"),u(53,"W"),u(54,"X"),u(55,"Y"),u(56,"Z"),u(57,"Meta"),u(58,"ContextMenu"),u(59,"F1"),u(60,"F2"),u(61,"F3"),u(62,"F4"),u(63,"F5"),u(64,"F6"),u(65,"F7"),u(66,"F8"),u(67,"F9"),u(68,"F10"),u(69,"F11"),u(70,"F12"),u(71,"F13"),u(72,"F14"),u(73,"F15"),u(74,"F16"),u(75,"F17"),u(76,"F18"),u(77,"F19"),u(78,"NumLock"),u(79,"ScrollLock"),u(80,";",";","OEM_1"),u(81,"=","=","OEM_PLUS"),u(82,",",",","OEM_COMMA"),u(83,"-","-","OEM_MINUS"),u(84,".",".","OEM_PERIOD"),u(85,"/","/","OEM_2"),u(86,"`","`","OEM_3"),u(110,"ABNT_C1"),u(111,"ABNT_C2"),u(87,"[","[","OEM_4"),u(88,"\\","\\","OEM_5"),u(89,"]","]","OEM_6"),u(90,"'","'","OEM_7"),u(91,"OEM_8"),u(92,"OEM_102"),u(93,"NumPad0"),u(94,"NumPad1"),u(95,"NumPad2"),u(96,"NumPad3"),u(97,"NumPad4"),u(98,"NumPad5"),u(99,"NumPad6"),u(100,"NumPad7"),u(101,"NumPad8"),u(102,"NumPad9"),u(103,"NumPad_Multiply"),u(104,"NumPad_Add"),u(105,"NumPad_Separator"),u(106,"NumPad_Subtract"),u(107,"NumPad_Decimal"),u(108,"NumPad_Divide")})();var C;(function(u){function r(h){return M.keyCodeToStr(h)}u.toString=r;function i(h){return M.strToKeyCode(h)}u.fromString=i;function n(h){return w.keyCodeToStr(h)}u.toUserSettingsUS=n;function t(h){return S.keyCodeToStr(h)}u.toUserSettingsGeneral=t;function l(h){return w.strToKeyCode(h)||S.strToKeyCode(h)}u.fromUserSettings=l})(C=e.KeyCodeUtils||(e.KeyCodeUtils={}));function d(u,r){const i=(r&65535)<<16>>>0;return(u|i)>>>0}e.KeyChord=d;function g(u,r){if(u===0)return null;const i=(u&65535)>>>0,n=(u&4294901760)>>>16;return n!==0?new o([p(i,r),p(n,r)]):new o([p(i,r)])}e.createKeybinding=g;function p(u,r){const i=!!(u&2048),n=!!(u&256),t=r===2?n:i,l=!!(u&1024),h=!!(u&512),m=r===2?i:n,_=u&255;return new c(t,l,h,m,_)}e.createSimpleKeybinding=p;class c{constructor(r,i,n,t,l){this.ctrlKey=r,this.shiftKey=i,this.altKey=n,this.metaKey=t,this.keyCode=l}equals(r){return this.ctrlKey===r.ctrlKey&&this.shiftKey===r.shiftKey&&this.altKey===r.altKey&&this.metaKey===r.metaKey&&this.keyCode===r.keyCode}isModifierKey(){return this.keyCode===0||this.keyCode===5||this.keyCode===57||this.keyCode===6||this.keyCode===4}toChord(){return new o([this])}isDuplicateModifierCase(){return this.ctrlKey&&this.keyCode===5||this.shiftKey&&this.keyCode===4||this.altKey&&this.keyCode===6||this.metaKey&&this.keyCode===57}}e.SimpleKeybinding=c;class o{constructor(r){if(r.length===0)throw b.illegalArgument("parts");this.parts=r}}e.ChordKeybinding=o;class s{constructor(r,i,n,t,l,h){this.ctrlKey=r,this.shiftKey=i,this.altKey=n,this.metaKey=t,this.keyLabel=l,this.keyAriaLabel=h}}e.ResolvedKeybindingPart=s;class a{}e.ResolvedKeybinding=a}),define(Q[150],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Lazy=void 0;class b{constructor(M){this.executor=M,this._didRun=!1}getValue(){if(!this._didRun)try{this._value=this.executor()}catch(M){this._error=M}finally{this._didRun=!0}if(this._error)throw this._error;return this._value}get rawValue(){return this._value}}e.Lazy=b}),define(Q[2],J([0,1,54]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ImmortalReference=e.MutableDisposable=e.Disposable=e.DisposableStore=e.toDisposable=e.combinedDisposable=e.dispose=e.isDisposable=e.MultiDisposeError=e.trackDisposable=void 0;const N=!1;let M=null;if(N){const r="__is_disposable_tracked__";M=new class{trackDisposable(i){const n=new Error("Potentially leaked disposable").stack;setTimeout(()=>{i[r]||console.log(n)},3e3)}markTracked(i){if(i&&i!==s.None)try{i[r]=!0}catch(n){}}}}function w(r){!M||M.markTracked(r)}function S(r){return M&&M.trackDisposable(r),r}e.trackDisposable=S;class C extends Error{constructor(i){super(`Encountered errors while disposing of store. Errors: [${i.join(", ")}]`);this.errors=i}}e.MultiDisposeError=C;function d(r){return typeof r.dispose=="function"&&r.dispose.length===0}e.isDisposable=d;function g(r){if(b.Iterable.is(r)){let i=[];for(const n of r)if(n){w(n);try{n.dispose()}catch(t){i.push(t)}}if(i.length===1)throw i[0];if(i.length>1)throw new C(i);return Array.isArray(r)?[]:r}else if(r)return w(r),r.dispose(),r}e.dispose=g;function p(...r){return r.forEach(w),c(()=>g(r))}e.combinedDisposable=p;function c(r){const i=S({dispose:()=>{w(i),r()}});return i}e.toDisposable=c;class o{constructor(){this._toDispose=new Set,this._isDisposed=!1}dispose(){this._isDisposed||(w(this),this._isDisposed=!0,this.clear())}clear(){try{g(this._toDispose.values())}finally{this._toDispose.clear()}}add(i){if(!i)return i;if(i===this)throw new Error("Cannot register a disposable on itself!");return w(i),this._isDisposed?o.DISABLE_DISPOSED_WARNING||console.warn(new Error("Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!").stack):this._toDispose.add(i),i}}e.DisposableStore=o,o.DISABLE_DISPOSED_WARNING=!1;class s{constructor(){this._store=new o,S(this)}dispose(){w(this),this._store.dispose()}_register(i){if(i===this)throw new Error("Cannot register a disposable on itself!");return this._store.add(i)}}e.Disposable=s,s.None=Object.freeze({dispose(){}});class a{constructor(){this._isDisposed=!1,S(this)}get value(){return this._isDisposed?void 0:this._value}set value(i){var n;this._isDisposed||i===this._value||((n=this._value)===null||n===void 0||n.dispose(),i&&w(i),this._value=i)}clear(){this.value=void 0}dispose(){var i;this._isDisposed=!0,w(this),(i=this._value)===null||i===void 0||i.dispose(),this._value=void 0}}e.MutableDisposable=a;class u{constructor(i){this.object=i}dispose(){}}e.ImmortalReference=u}),define(Q[71],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LinkedList=void 0;class b{constructor(w){this.element=w,this.next=b.Undefined,this.prev=b.Undefined}}b.Undefined=new b(void 0);class N{constructor(){this._first=b.Undefined,this._last=b.Undefined,this._size=0}get size(){return this._size}isEmpty(){return this._first===b.Undefined}clear(){this._first=b.Undefined,this._last=b.Undefined,this._size=0}unshift(w){return this._insert(w,!1)}push(w){return this._insert(w,!0)}_insert(w,S){const C=new b(w);if(this._first===b.Undefined)this._first=C,this._last=C;else if(S){const g=this._last;this._last=C,C.prev=g,g.next=C}else{const g=this._first;this._first=C,C.next=g,g.prev=C}this._size+=1;let d=!1;return()=>{d||(d=!0,this._remove(C))}}shift(){if(this._first!==b.Undefined){const w=this._first.element;return this._remove(this._first),w}}pop(){if(this._last!==b.Undefined){const w=this._last.element;return this._remove(this._last),w}}_remove(w){if(w.prev!==b.Undefined&&w.next!==b.Undefined){const S=w.prev;S.next=w.next,w.next.prev=S}else w.prev===b.Undefined&&w.next===b.Undefined?(this._first=b.Undefined,this._last=b.Undefined):w.next===b.Undefined?(this._last=this._last.prev,this._last.next=b.Undefined):w.prev===b.Undefined&&(this._first=this._first.next,this._first.prev=b.Undefined);this._size-=1}*[Symbol.iterator](){let w=this._first;for(;w!==b.Undefined;)yield w.element,w=w.next}}e.LinkedList=N}),function(q,e){typeof exports=="object"&&typeof module!="undefined"?module.exports=e():typeof define=="function"&&define.amd?define("vs/base/common/marked/marked",e):(q=typeof globalThis!="undefined"?globalThis:q||self,q.marked=e())}(this,function(){"use strict";function q(Se,we){for(var ye=0;yeSe.length)&&(we=Se.length);for(var ye=0,fe=new Array(we);ye=Se.length?{done:!0}:{done:!1,value:Se[fe++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance. +In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}return ye=Se[Symbol.iterator](),ye.next.bind(ye)}function w(Se){var we={exports:{}};return Se(we,we.exports),we.exports}var S=w(function(Se){function we(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:null,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tokenizer:null,walkTokens:null,xhtml:!1}}function ye(fe){Se.exports.defaults=fe}Se.exports={defaults:we(),getDefaults:we,changeDefaults:ye}}),C=/[&<>"']/,d=/[&<>"']/g,g=/[<>"']|&(?!#?\w+;)/,p=/[<>"']|&(?!#?\w+;)/g,c={"&":"&","<":"<",">":">",'"':""","'":"'"},o=function(we){return c[we]};function s(Se,we){if(we){if(C.test(Se))return Se.replace(d,o)}else if(g.test(Se))return Se.replace(p,o);return Se}var a=/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig;function u(Se){return Se.replace(a,function(we,ye){return ye=ye.toLowerCase(),ye==="colon"?":":ye.charAt(0)==="#"?ye.charAt(1)==="x"?String.fromCharCode(parseInt(ye.substring(2),16)):String.fromCharCode(+ye.substring(1)):""})}var r=/(^|[^\[])\^/g;function i(Se,we){Se=Se.source||Se,we=we||"";var ye={replace:function(de,ge){return ge=ge.source||ge,ge=ge.replace(r,"$1"),Se=Se.replace(de,ge),ye},getRegex:function(){return new RegExp(Se,we)}};return ye}var n=/[^\w:]/g,t=/^$|^[a-z][a-z0-9+.-]*:|^[?#]/i;function l(Se,we,ye){if(Se){var fe;try{fe=decodeURIComponent(u(ye)).replace(n,"").toLowerCase()}catch(de){return null}if(fe.indexOf("javascript:")===0||fe.indexOf("vbscript:")===0||fe.indexOf("data:")===0)return null}we&&!t.test(ye)&&(ye=v(we,ye));try{ye=encodeURI(ye).replace(/%25/g,"%")}catch(de){return null}return ye}var h={},m=/^[^:]+:\/*[^/]*$/,_=/^([^:]+:)[\s\S]*$/,f=/^([^:]+:\/*[^/]*)[\s\S]*$/;function v(Se,we){h[" "+Se]||(m.test(Se)?h[" "+Se]=Se+"/":h[" "+Se]=k(Se,"/",!0)),Se=h[" "+Se];var ye=Se.indexOf(":")===-1;return we.substring(0,2)==="//"?ye?we:Se.replace(_,"$1")+we:we.charAt(0)==="/"?ye?we:Se.replace(f,"$1")+we:Se+we}var y={exec:function(){}};function L(Se){for(var we=1,ye,fe;we=0&&ve[Ne]==="\\";)ke=!ke;return ke?"|":" |"}),fe=ye.split(/ \|/),de=0;if(fe.length>we)fe.splice(we);else for(;fe.length1;)we&1&&(ye+=Se),we>>=1,Se+=Se;return ye+Se}var A={escape:s,unescape:u,edit:i,cleanUrl:l,resolveUrl:v,noopTest:y,merge:L,splitCells:I,rtrim:k,findClosingBracket:E,checkSanitizeDeprecation:T,repeatString:O},B=S.defaults,F=A.rtrim,D=A.splitCells,R=A.escape,W=A.findClosingBracket;function x(Se,we,ye){var fe=we.href,de=we.title?R(we.title):null,ge=Se[1].replace(/\\([\[\]])/g,"$1");return Se[0].charAt(0)!=="!"?{type:"link",raw:ye,href:fe,title:de,text:ge}:{type:"image",raw:ye,href:fe,title:de,text:R(ge)}}function K(Se,we){var ye=Se.match(/^(\s+)(?:```)/);if(ye===null)return we;var fe=ye[1];return we.split(` +`).map(function(de){var ge=de.match(/^\s+/);if(ge===null)return de;var pe=ge[0];return pe.length>=fe.length?de.slice(fe.length):de}).join(` +`)}var Y=function(){function Se(ye){this.options=ye||B}var we=Se.prototype;return we.space=function(fe){var de=this.rules.block.newline.exec(fe);if(de)return de[0].length>1?{type:"space",raw:de[0]}:{raw:` +`}},we.code=function(fe){var de=this.rules.block.code.exec(fe);if(de){var ge=de[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:de[0],codeBlockStyle:"indented",text:this.options.pedantic?ge:F(ge,` +`)}}},we.fences=function(fe){var de=this.rules.block.fences.exec(fe);if(de){var ge=de[0],pe=K(ge,de[3]||"");return{type:"code",raw:ge,lang:de[2]?de[2].trim():de[2],text:pe}}},we.heading=function(fe){var de=this.rules.block.heading.exec(fe);if(de){var ge=de[2].trim();if(/#$/.test(ge)){var pe=F(ge,"#");(this.options.pedantic||!pe||/ $/.test(pe))&&(ge=pe.trim())}return{type:"heading",raw:de[0],depth:de[1].length,text:ge}}},we.nptable=function(fe){var de=this.rules.block.nptable.exec(fe);if(de){var ge={type:"table",header:D(de[1].replace(/^ *| *\| *$/g,"")),align:de[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:de[3]?de[3].replace(/\n$/,"").split(` +`):[],raw:de[0]};if(ge.header.length===ge.align.length){var pe=ge.align.length,ve;for(ve=0;ve ?/gm,"");return{type:"blockquote",raw:de[0],text:ge}}},we.list=function(fe){var de=this.rules.block.list.exec(fe);if(de){var ge=de[0],pe=de[2],ve=pe.length>1,ke={type:"list",raw:ge,ordered:ve,start:ve?+pe.slice(0,-1):"",loose:!1,items:[]},Ne=de[0].match(this.rules.block.item),Te=!1,Oe,Fe,Pe,xe,We,ze,Ke,Be,He=Ne.length;Pe=this.rules.block.listItemStart.exec(Ne[0]);for(var Ve=0;VePe[1].length:xe[1].length>Pe[0].length||xe[1].length>3){Ne.splice(Ve,2,Ne[Ve]+` +`+Ne[Ve+1]),Ve--,He--;continue}else(!this.options.pedantic||this.options.smartLists?xe[2][xe[2].length-1]!==pe[pe.length-1]:ve===(xe[2].length===1))&&(We=Ne.slice(Ve+1).join(` +`),ke.raw=ke.raw.substring(0,ke.raw.length-We.length),Ve=He-1);Pe=xe}Fe=Oe.length,Oe=Oe.replace(/^ *([*+-]|\d+[.)]) ?/,""),~Oe.indexOf(` + `)&&(Fe-=Oe.length,Oe=this.options.pedantic?Oe.replace(/^ {1,4}/gm,""):Oe.replace(new RegExp("^ {1,"+Fe+"}","gm"),"")),ze=Te||/\n\n(?!\s*$)/.test(Oe),Ve!==He-1&&(Te=Oe.charAt(Oe.length-1)===` +`,ze||(ze=Te)),ze&&(ke.loose=!0),this.options.gfm&&(Ke=/^\[[ xX]\] /.test(Oe),Be=void 0,Ke&&(Be=Oe[1]!==" ",Oe=Oe.replace(/^\[[ xX]\] +/,""))),ke.items.push({type:"list_item",raw:ge,task:Ke,checked:Be,loose:ze,text:Oe})}return ke}},we.html=function(fe){var de=this.rules.block.html.exec(fe);if(de)return{type:this.options.sanitize?"paragraph":"html",raw:de[0],pre:!this.options.sanitizer&&(de[1]==="pre"||de[1]==="script"||de[1]==="style"),text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(de[0]):R(de[0]):de[0]}},we.def=function(fe){var de=this.rules.block.def.exec(fe);if(de){de[3]&&(de[3]=de[3].substring(1,de[3].length-1));var ge=de[1].toLowerCase().replace(/\s+/g," ");return{tag:ge,raw:de[0],href:de[2],title:de[3]}}},we.table=function(fe){var de=this.rules.block.table.exec(fe);if(de){var ge={type:"table",header:D(de[1].replace(/^ *| *\| *$/g,"")),align:de[2].replace(/^ *|\| *$/g,"").split(/ *\| */),cells:de[3]?de[3].replace(/\n$/,"").split(` +`):[]};if(ge.header.length===ge.align.length){ge.raw=de[0];var pe=ge.align.length,ve;for(ve=0;ve/i.test(pe[0])&&(de=!1),!ge&&/^<(pre|code|kbd|script)(\s|>)/i.test(pe[0])?ge=!0:ge&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(pe[0])&&(ge=!1),{type:this.options.sanitize?"text":"html",raw:pe[0],inLink:de,inRawBlock:ge,text:this.options.sanitize?this.options.sanitizer?this.options.sanitizer(pe[0]):R(pe[0]):pe[0]}},we.link=function(fe){var de=this.rules.inline.link.exec(fe);if(de){var ge=de[2].trim();if(!this.options.pedantic&&/^$/.test(ge))return;var pe=F(ge.slice(0,-1),"\\");if((ge.length-pe.length)%2==0)return}else{var ve=W(de[2],"()");if(ve>-1){var ke=de[0].indexOf("!")===0?5:4,Ne=ke+de[1].length+ve;de[2]=de[2].substring(0,ve),de[0]=de[0].substring(0,Ne).trim(),de[3]=""}}var Te=de[2],Oe="";if(this.options.pedantic){var Fe=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(Te);Fe&&(Te=Fe[1],Oe=Fe[3])}else Oe=de[3]?de[3].slice(1,-1):"";return Te=Te.trim(),/^$/.test(ge)?Te=Te.slice(1):Te=Te.slice(1,-1)),x(de,{href:Te&&Te.replace(this.rules.inline._escapes,"$1"),title:Oe&&Oe.replace(this.rules.inline._escapes,"$1")},de[0])}},we.reflink=function(fe,de){var ge;if((ge=this.rules.inline.reflink.exec(fe))||(ge=this.rules.inline.nolink.exec(fe))){var pe=(ge[2]||ge[1]).replace(/\s+/g," ");if(pe=de[pe.toLowerCase()],!pe||!pe.href){var ve=ge[0].charAt(0);return{type:"text",raw:ve,text:ve}}return x(ge,pe,ge[0])}},we.emStrong=function(fe,de,ge){ge===void 0&&(ge="");var pe=this.rules.inline.emStrong.lDelim.exec(fe);if(!!pe&&!(pe[3]&&ge.match(/(?:[0-9A-Za-z\xAA\xB2\xB3\xB5\xB9\xBA\xBC-\xBE\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0560-\u0588\u05D0-\u05EA\u05EF-\u05F2\u0620-\u064A\u0660-\u0669\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07C0-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08C7\u0904-\u0939\u093D\u0950\u0958-\u0961\u0966-\u096F\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09E6-\u09F1\u09F4-\u09F9\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A66-\u0A6F\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AE6-\u0AEF\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B66-\u0B6F\u0B71-\u0B77\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0BE6-\u0BF2\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C66-\u0C6F\u0C78-\u0C7E\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CE6-\u0CEF\u0CF1\u0CF2\u0D04-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D58-\u0D61\u0D66-\u0D78\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DE6-\u0DEF\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E86-\u0E8A\u0E8C-\u0EA3\u0EA5\u0EA7-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F20-\u0F33\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F-\u1049\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u1090-\u1099\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1369-\u137C\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1820-\u1878\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19DA\u1A00-\u1A16\u1A20-\u1A54\u1A80-\u1A89\u1A90-\u1A99\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B50-\u1B59\u1B83-\u1BA0\u1BAE-\u1BE5\u1C00-\u1C23\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1C90-\u1CBA\u1CBD-\u1CBF\u1CE9-\u1CEC\u1CEE-\u1CF3\u1CF5\u1CF6\u1CFA\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2070\u2071\u2074-\u2079\u207F-\u2089\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2150-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2CFD\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312F\u3131-\u318E\u3192-\u3195\u31A0-\u31BF\u31F0-\u31FF\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\u3400-\u4DBF\u4E00-\u9FFC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7BF\uA7C2-\uA7CA\uA7F5-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA830-\uA835\uA840-\uA873\uA882-\uA8B3\uA8D0-\uA8D9\uA8F2-\uA8F7\uA8FB\uA8FD\uA8FE\uA900-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF-\uA9D9\uA9E0-\uA9E4\uA9E6-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA50-\uAA59\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB69\uAB70-\uABE2\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD07-\uDD33\uDD40-\uDD78\uDD8A\uDD8B\uDE80-\uDE9C\uDEA0-\uDED0\uDEE1-\uDEFB\uDF00-\uDF23\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC58-\uDC76\uDC79-\uDC9E\uDCA7-\uDCAF\uDCE0-\uDCF2\uDCF4\uDCF5\uDCFB-\uDD1B\uDD20-\uDD39\uDD80-\uDDB7\uDDBC-\uDDCF\uDDD2-\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE35\uDE40-\uDE48\uDE60-\uDE7E\uDE80-\uDE9F\uDEC0-\uDEC7\uDEC9-\uDEE4\uDEEB-\uDEEF\uDF00-\uDF35\uDF40-\uDF55\uDF58-\uDF72\uDF78-\uDF91\uDFA9-\uDFAF]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2\uDCFA-\uDD23\uDD30-\uDD39\uDE60-\uDE7E\uDE80-\uDEA9\uDEB0\uDEB1\uDF00-\uDF27\uDF30-\uDF45\uDF51-\uDF54\uDFB0-\uDFCB\uDFE0-\uDFF6]|\uD804[\uDC03-\uDC37\uDC52-\uDC6F\uDC83-\uDCAF\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD03-\uDD26\uDD36-\uDD3F\uDD44\uDD47\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDD0-\uDDDA\uDDDC\uDDE1-\uDDF4\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDEF0-\uDEF9\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC50-\uDC59\uDC5F-\uDC61\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE50-\uDE59\uDE80-\uDEAA\uDEB8\uDEC0-\uDEC9\uDF00-\uDF1A\uDF30-\uDF3B]|\uD806[\uDC00-\uDC2B\uDCA0-\uDCF2\uDCFF-\uDD06\uDD09\uDD0C-\uDD13\uDD15\uDD16\uDD18-\uDD2F\uDD3F\uDD41\uDD50-\uDD59\uDDA0-\uDDA7\uDDAA-\uDDD0\uDDE1\uDDE3\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE89\uDE9D\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC50-\uDC6C\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46\uDD50-\uDD59\uDD60-\uDD65\uDD67\uDD68\uDD6A-\uDD89\uDD98\uDDA0-\uDDA9\uDEE0-\uDEF2\uDFB0\uDFC0-\uDFD4]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD822\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879\uD880-\uD883][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF50-\uDF59\uDF5B-\uDF61\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDE40-\uDE96\uDF00-\uDF4A\uDF50\uDF93-\uDF9F\uDFE0\uDFE1\uDFE3]|\uD821[\uDC00-\uDFF7]|\uD823[\uDC00-\uDCD5\uDD00-\uDD08]|\uD82C[\uDC00-\uDD1E\uDD50-\uDD52\uDD64-\uDD67\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD834[\uDEE0-\uDEF3\uDF60-\uDF78]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD838[\uDD00-\uDD2C\uDD37-\uDD3D\uDD40-\uDD49\uDD4E\uDEC0-\uDEEB\uDEF0-\uDEF9]|\uD83A[\uDC00-\uDCC4\uDCC7-\uDCCF\uDD00-\uDD43\uDD4B\uDD50-\uDD59]|\uD83B[\uDC71-\uDCAB\uDCAD-\uDCAF\uDCB1-\uDCB4\uDD01-\uDD2D\uDD2F-\uDD3D\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD83C[\uDD00-\uDD0C]|\uD83E[\uDFF0-\uDFF9]|\uD869[\uDC00-\uDEDD\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uD884[\uDC00-\uDF4A])/))){var ve=pe[1]||pe[2]||"";if(!ve||ve&&(ge===""||this.rules.inline.punctuation.exec(ge))){var ke=pe[0].length-1,Ne,Te,Oe=ke,Fe=0,Pe=pe[0][0]==="*"?this.rules.inline.emStrong.rDelimAst:this.rules.inline.emStrong.rDelimUnd;for(Pe.lastIndex=0,de=de.slice(-1*fe.length+ke);(pe=Pe.exec(de))!=null;)if(Ne=pe[1]||pe[2]||pe[3]||pe[4]||pe[5]||pe[6],!!Ne){if(Te=Ne.length,pe[3]||pe[4]){Oe+=Te;continue}else if((pe[5]||pe[6])&&ke%3&&!((ke+Te)%3)){Fe+=Te;continue}if(Oe-=Te,!(Oe>0)){if(Oe+Fe-Te<=0&&!de.slice(Pe.lastIndex).match(Pe)&&(Te=Math.min(Te,Te+Oe+Fe)),Math.min(ke,Te)%2)return{type:"em",raw:fe.slice(0,ke+pe.index+Te+1),text:fe.slice(1,ke+pe.index+Te)};if(Math.min(ke,Te)%2==0)return{type:"strong",raw:fe.slice(0,ke+pe.index+Te+1),text:fe.slice(2,ke+pe.index+Te-1)}}}}}},we.codespan=function(fe){var de=this.rules.inline.code.exec(fe);if(de){var ge=de[2].replace(/\n/g," "),pe=/[^ ]/.test(ge),ve=/^ /.test(ge)&&/ $/.test(ge);return pe&&ve&&(ge=ge.substring(1,ge.length-1)),ge=R(ge,!0),{type:"codespan",raw:de[0],text:ge}}},we.br=function(fe){var de=this.rules.inline.br.exec(fe);if(de)return{type:"br",raw:de[0]}},we.del=function(fe){var de=this.rules.inline.del.exec(fe);if(de)return{type:"del",raw:de[0],text:de[2]}},we.autolink=function(fe,de){var ge=this.rules.inline.autolink.exec(fe);if(ge){var pe,ve;return ge[2]==="@"?(pe=R(this.options.mangle?de(ge[1]):ge[1]),ve="mailto:"+pe):(pe=R(ge[1]),ve=pe),{type:"link",raw:ge[0],text:pe,href:ve,tokens:[{type:"text",raw:pe,text:pe}]}}},we.url=function(fe,de){var ge;if(ge=this.rules.inline.url.exec(fe)){var pe,ve;if(ge[2]==="@")pe=R(this.options.mangle?de(ge[0]):ge[0]),ve="mailto:"+pe;else{var ke;do ke=ge[0],ge[0]=this.rules.inline._backpedal.exec(ge[0])[0];while(ke!==ge[0]);pe=R(ge[0]),ge[1]==="www."?ve="http://"+pe:ve=pe}return{type:"link",raw:ge[0],text:pe,href:ve,tokens:[{type:"text",raw:pe,text:pe}]}}},we.inlineText=function(fe,de,ge){var pe=this.rules.inline.text.exec(fe);if(pe){var ve;return de?ve=this.options.sanitize?this.options.sanitizer?this.options.sanitizer(pe[0]):R(pe[0]):pe[0]:ve=R(this.options.smartypants?ge(pe[0]):pe[0]),{type:"text",raw:pe[0],text:ve}}},Se}(),ee=A.noopTest,se=A.edit,ne=A.merge,le={newline:/^(?: *(?:\n|$))+/,code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,fences:/^ {0,3}(`{3,}(?=[^`\n]*\n)|~{3,})([^\n]*)\n(?:|([\s\S]*?)\n)(?: {0,3}\1[~`]* *(?:\n+|$)|$)/,hr:/^ {0,3}((?:- *){3,}|(?:_ *){3,}|(?:\* *){3,})(?:\n+|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,blockquote:/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/,list:/^( {0,3})(bull) [\s\S]+?(?:hr|def|\n{2,}(?! )(?! {0,3}bull )\n*|\s*$)/,html:"^ {0,3}(?:<(script|pre|style)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:\\n{2,}|$)|<(?!script|pre|style)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:\\n{2,}|$))",def:/^ {0,3}\[(label)\]: *\n? *]+)>?(?:(?: +\n? *| *\n *)(title))? *(?:\n+|$)/,nptable:ee,table:ee,lheading:/^([^\n]+)\n {0,3}(=+|-+) *(?:\n+|$)/,_paragraph:/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html| +\n)[^\n]+)*)/,text:/^[^\n]+/};le._label=/(?!\s*\])(?:\\[\[\]]|[^\[\]])+/,le._title=/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/,le.def=se(le.def).replace("label",le._label).replace("title",le._title).getRegex(),le.bullet=/(?:[*+-]|\d{1,9}[.)])/,le.item=/^( *)(bull) ?[^\n]*(?:\n(?! *bull ?)[^\n]*)*/,le.item=se(le.item,"gm").replace(/bull/g,le.bullet).getRegex(),le.listItemStart=se(/^( *)(bull)/).replace("bull",le.bullet).getRegex(),le.list=se(le.list).replace(/bull/g,le.bullet).replace("hr","\\n+(?=\\1?(?:(?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$))").replace("def","\\n+(?="+le.def.source+")").getRegex(),le._tag="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|section|source|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",le._comment=/|$)/,le.html=se(le.html,"i").replace("comment",le._comment).replace("tag",le._tag).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),le.paragraph=se(le._paragraph).replace("hr",le.hr).replace("heading"," {0,3}#{1,6} ").replace("|lheading","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",le._tag).getRegex(),le.blockquote=se(le.blockquote).replace("paragraph",le.paragraph).getRegex(),le.normal=ne({},le),le.gfm=ne({},le.normal,{nptable:"^ *([^|\\n ].*\\|.*)\\n {0,3}([-:]+ *\\|[-| :]*)(?:\\n((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)",table:"^ *\\|(.+)\\n {0,3}\\|?( *[-:]+[-| :]*)(?:\\n *((?:(?!\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)"}),le.gfm.nptable=se(le.gfm.nptable).replace("hr",le.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",le._tag).getRegex(),le.gfm.table=se(le.gfm.table).replace("hr",le.hr).replace("heading"," {0,3}#{1,6} ").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|!--)").replace("tag",le._tag).getRegex(),le.pedantic=ne({},le.normal,{html:se(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",le._comment).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:ee,paragraph:se(le.normal._paragraph).replace("hr",le.hr).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",le.lheading).replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").getRegex()});var X={escape:/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,autolink:/^<(scheme:[^\s\x00-\x1f<>]*|email)>/,url:ee,tag:"^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^",link:/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/,reflink:/^!?\[(label)\]\[(?!\s*\])((?:\\[\[\]]?|[^\[\]\\])+)\]/,nolink:/^!?\[(?!\s*\])((?:\[[^\[\]]*\]|\\[\[\]]|[^\[\]])*)\](?:\[\])?/,reflinkSearch:"reflink|nolink(?!\\()",emStrong:{lDelim:/^(?:\*+(?:([punct_])|[^\s*]))|^_+(?:([punct*])|([^\s_]))/,rDelimAst:/\_\_[^_]*?\*[^_]*?\_\_|[punct_](\*+)(?=[\s]|$)|[^punct*_\s](\*+)(?=[punct_\s]|$)|[punct_\s](\*+)(?=[^punct*_\s])|[\s](\*+)(?=[punct_])|[punct_](\*+)(?=[punct_])|[^punct*_\s](\*+)(?=[^punct*_\s])/,rDelimUnd:/\*\*[^*]*?\_[^*]*?\*\*|[punct*](\_+)(?=[\s]|$)|[^punct*_\s](\_+)(?=[punct*\s]|$)|[punct*\s](\_+)(?=[^punct*_\s])|[\s](\_+)(?=[punct*])|[punct*](\_+)(?=[punct*])/},code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,br:/^( {2,}|\\)\n(?!\s*$)/,del:ee,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\?@\\[\\]`^{|}~",X.punctuation=se(X.punctuation).replace(/punctuation/g,X._punctuation).getRegex(),X.blockSkip=/\[[^\]]*?\]\([^\)]*?\)|`[^`]*?`|<[^>]*?>/g,X.escapedEmSt=/\\\*|\\_/g,X._comment=se(le._comment).replace("(?:-->|$)","-->").getRegex(),X.emStrong.lDelim=se(X.emStrong.lDelim).replace(/punct/g,X._punctuation).getRegex(),X.emStrong.rDelimAst=se(X.emStrong.rDelimAst,"g").replace(/punct/g,X._punctuation).getRegex(),X.emStrong.rDelimUnd=se(X.emStrong.rDelimUnd,"g").replace(/punct/g,X._punctuation).getRegex(),X._escapes=/\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/g,X._scheme=/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/,X._email=/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/,X.autolink=se(X.autolink).replace("scheme",X._scheme).replace("email",X._email).getRegex(),X._attribute=/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/,X.tag=se(X.tag).replace("comment",X._comment).replace("attribute",X._attribute).getRegex(),X._label=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,X._href=/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/,X._title=/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/,X.link=se(X.link).replace("label",X._label).replace("href",X._href).replace("title",X._title).getRegex(),X.reflink=se(X.reflink).replace("label",X._label).getRegex(),X.reflinkSearch=se(X.reflinkSearch,"g").replace("reflink",X.reflink).replace("nolink",X.nolink).getRegex(),X.normal=ne({},X),X.pedantic=ne({},X.normal,{strong:{start:/^__|\*\*/,middle:/^__(?=\S)([\s\S]*?\S)__(?!_)|^\*\*(?=\S)([\s\S]*?\S)\*\*(?!\*)/,endAst:/\*\*(?!\*)/g,endUnd:/__(?!_)/g},em:{start:/^_|\*/,middle:/^()\*(?=\S)([\s\S]*?\S)\*(?!\*)|^_(?=\S)([\s\S]*?\S)_(?!_)/,endAst:/\*(?!\*)/g,endUnd:/_(?!_)/g},link:se(/^!?\[(label)\]\((.*?)\)/).replace("label",X._label).getRegex(),reflink:se(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",X._label).getRegex()}),X.gfm=ne({},X.normal,{escape:se(X.escape).replace("])","~|])").getRegex(),_extended_email:/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/,url:/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,_backpedal:/(?:[^?!.,:;*_~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])([\s\S]*?[^\s~])\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\.5&&(fe="x"+fe.toString(16)),we+="&#"+fe+";";return we}var oe=function(){function Se(ye){this.tokens=[],this.tokens.links=Object.create(null),this.options=ye||P,this.options.tokenizer=this.options.tokenizer||new Y,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options;var fe={block:V.normal,inline:U.normal};this.options.pedantic?(fe.block=V.pedantic,fe.inline=U.pedantic):this.options.gfm&&(fe.block=V.gfm,this.options.breaks?fe.inline=U.breaks:fe.inline=U.gfm),this.tokenizer.rules=fe}Se.lex=function(fe,de){var ge=new Se(de);return ge.lex(fe)},Se.lexInline=function(fe,de){var ge=new Se(de);return ge.inlineTokens(fe)};var we=Se.prototype;return we.lex=function(fe){return fe=fe.replace(/\r\n|\r/g,` +`).replace(/\t/g," "),this.blockTokens(fe,this.tokens,!0),this.inline(this.tokens),this.tokens},we.blockTokens=function(fe,de,ge){de===void 0&&(de=[]),ge===void 0&&(ge=!0),this.options.pedantic&&(fe=fe.replace(/^ +$/gm,""));for(var pe,ve,ke,Ne;fe;){if(pe=this.tokenizer.space(fe)){fe=fe.substring(pe.raw.length),pe.type&&de.push(pe);continue}if(pe=this.tokenizer.code(fe)){fe=fe.substring(pe.raw.length),Ne=de[de.length-1],Ne&&Ne.type==="paragraph"?(Ne.raw+=` +`+pe.raw,Ne.text+=` +`+pe.text):de.push(pe);continue}if(pe=this.tokenizer.fences(fe)){fe=fe.substring(pe.raw.length),de.push(pe);continue}if(pe=this.tokenizer.heading(fe)){fe=fe.substring(pe.raw.length),de.push(pe);continue}if(pe=this.tokenizer.nptable(fe)){fe=fe.substring(pe.raw.length),de.push(pe);continue}if(pe=this.tokenizer.hr(fe)){fe=fe.substring(pe.raw.length),de.push(pe);continue}if(pe=this.tokenizer.blockquote(fe)){fe=fe.substring(pe.raw.length),pe.tokens=this.blockTokens(pe.text,[],ge),de.push(pe);continue}if(pe=this.tokenizer.list(fe)){for(fe=fe.substring(pe.raw.length),ke=pe.items.length,ve=0;ve0)for(;(Te=this.tokenizer.rules.inline.reflinkSearch.exec(Ne))!=null;)Pe.includes(Te[0].slice(Te[0].lastIndexOf("[")+1,-1))&&(Ne=Ne.slice(0,Te.index)+"["+H("a",Te[0].length-2)+"]"+Ne.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(Te=this.tokenizer.rules.inline.blockSkip.exec(Ne))!=null;)Ne=Ne.slice(0,Te.index)+"["+H("a",Te[0].length-2)+"]"+Ne.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;(Te=this.tokenizer.rules.inline.escapedEmSt.exec(Ne))!=null;)Ne=Ne.slice(0,Te.index)+"++"+Ne.slice(this.tokenizer.rules.inline.escapedEmSt.lastIndex);for(;fe;){if(Oe||(Fe=""),Oe=!1,ve=this.tokenizer.escape(fe)){fe=fe.substring(ve.raw.length),de.push(ve);continue}if(ve=this.tokenizer.tag(fe,ge,pe)){fe=fe.substring(ve.raw.length),ge=ve.inLink,pe=ve.inRawBlock;var xe=de[de.length-1];xe&&ve.type==="text"&&xe.type==="text"?(xe.raw+=ve.raw,xe.text+=ve.text):de.push(ve);continue}if(ve=this.tokenizer.link(fe)){fe=fe.substring(ve.raw.length),ve.type==="link"&&(ve.tokens=this.inlineTokens(ve.text,[],!0,pe)),de.push(ve);continue}if(ve=this.tokenizer.reflink(fe,this.tokens.links)){fe=fe.substring(ve.raw.length);var We=de[de.length-1];ve.type==="link"?(ve.tokens=this.inlineTokens(ve.text,[],!0,pe),de.push(ve)):We&&ve.type==="text"&&We.type==="text"?(We.raw+=ve.raw,We.text+=ve.text):de.push(ve);continue}if(ve=this.tokenizer.emStrong(fe,Ne,Fe)){fe=fe.substring(ve.raw.length),ve.tokens=this.inlineTokens(ve.text,[],ge,pe),de.push(ve);continue}if(ve=this.tokenizer.codespan(fe)){fe=fe.substring(ve.raw.length),de.push(ve);continue}if(ve=this.tokenizer.br(fe)){fe=fe.substring(ve.raw.length),de.push(ve);continue}if(ve=this.tokenizer.del(fe)){fe=fe.substring(ve.raw.length),ve.tokens=this.inlineTokens(ve.text,[],ge,pe),de.push(ve);continue}if(ve=this.tokenizer.autolink(fe,ie)){fe=fe.substring(ve.raw.length),de.push(ve);continue}if(!ge&&(ve=this.tokenizer.url(fe,ie))){fe=fe.substring(ve.raw.length),de.push(ve);continue}if(ve=this.tokenizer.inlineText(fe,pe,$)){fe=fe.substring(ve.raw.length),ve.raw.slice(-1)!=="_"&&(Fe=ve.raw.slice(-1)),Oe=!0,ke=de[de.length-1],ke&&ke.type==="text"?(ke.raw+=ve.raw,ke.text+=ve.text):de.push(ve);continue}if(fe){var ze="Infinite loop on byte: "+fe.charCodeAt(0);if(this.options.silent){console.error(ze);break}else throw new Error(ze)}}return de},e(Se,null,[{key:"rules",get:function(){return{block:V,inline:U}}}]),Se}(),ae=S.defaults,G=A.cleanUrl,j=A.escape,te=function(){function Se(ye){this.options=ye||ae}var we=Se.prototype;return we.code=function(fe,de,ge){var pe=(de||"").match(/\S*/)[0];if(this.options.highlight){var ve=this.options.highlight(fe,pe);ve!=null&&ve!==fe&&(ge=!0,fe=ve)}return fe=fe.replace(/\n$/,"")+` +`,pe?'
'+(ge?fe:j(fe,!0))+`
+`:"
"+(ge?fe:j(fe,!0))+`
+`},we.blockquote=function(fe){return`
+`+fe+`
+`},we.html=function(fe){return fe},we.heading=function(fe,de,ge,pe){return this.options.headerIds?"'+fe+" +`:""+fe+" +`},we.hr=function(){return this.options.xhtml?`
+`:`
+`},we.list=function(fe,de,ge){var pe=de?"ol":"ul",ve=de&&ge!==1?' start="'+ge+'"':"";return"<"+pe+ve+`> +`+fe+" +`},we.listitem=function(fe){return"
  • "+fe+`
  • +`},we.checkbox=function(fe){return" "},we.paragraph=function(fe){return"

    "+fe+`

    +`},we.table=function(fe,de){return de&&(de="

    "+de+""),`
    + +`+fe+` +`+de+`
    +`},we.tablerow=function(fe){return` +`+fe+` +`},we.tablecell=function(fe,de){var ge=de.header?"th":"td",pe=de.align?"<"+ge+' align="'+de.align+'">':"<"+ge+">";return pe+fe+" +`},we.strong=function(fe){return""+fe+""},we.em=function(fe){return""+fe+""},we.codespan=function(fe){return""+fe+""},we.br=function(){return this.options.xhtml?"
    ":"
    "},we.del=function(fe){return""+fe+""},we.link=function(fe,de,ge){if(fe=G(this.options.sanitize,this.options.baseUrl,fe),fe===null)return ge;var pe='",pe},we.image=function(fe,de,ge){if(fe=G(this.options.sanitize,this.options.baseUrl,fe),fe===null)return ge;var pe=''+ge+'":">",pe},we.text=function(fe){return fe},Se}(),Z=function(){function Se(){}var we=Se.prototype;return we.strong=function(fe){return fe},we.em=function(fe){return fe},we.codespan=function(fe){return fe},we.del=function(fe){return fe},we.html=function(fe){return fe},we.text=function(fe){return fe},we.link=function(fe,de,ge){return""+ge},we.image=function(fe,de,ge){return""+ge},we.br=function(){return""},Se}(),ue=function(){function Se(){this.seen={}}var we=Se.prototype;return we.serialize=function(fe){return fe.toLowerCase().trim().replace(/<[!\/a-z].*?>/ig,"").replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,./:;<=>?@[\]^`{|}~]/g,"").replace(/\s/g,"-")},we.getNextSafeSlug=function(fe,de){var ge=fe,pe=0;if(this.seen.hasOwnProperty(ge)){pe=this.seen[fe];do pe++,ge=fe+"-"+pe;while(this.seen.hasOwnProperty(ge))}return de||(this.seen[fe]=pe,this.seen[ge]=0),ge},we.slug=function(fe,de){de===void 0&&(de={});var ge=this.serialize(fe);return this.getNextSafeSlug(ge,de.dryrun)},Se}(),he=S.defaults,re=A.unescape,ce=function(){function Se(ye){this.options=ye||he,this.options.renderer=this.options.renderer||new te,this.renderer=this.options.renderer,this.renderer.options=this.options,this.textRenderer=new Z,this.slugger=new ue}Se.parse=function(fe,de){var ge=new Se(de);return ge.parse(fe)},Se.parseInline=function(fe,de){var ge=new Se(de);return ge.parseInline(fe)};var we=Se.prototype;return we.parse=function(fe,de){de===void 0&&(de=!0);var ge="",pe,ve,ke,Ne,Te,Oe,Fe,Pe,xe,We,ze,Ke,Be,He,Ve,Ue,Ye,je,Xe=fe.length;for(pe=0;pe0&&Ve.tokens[0].type==="text"?(Ve.tokens[0].text=je+" "+Ve.tokens[0].text,Ve.tokens[0].tokens&&Ve.tokens[0].tokens.length>0&&Ve.tokens[0].tokens[0].type==="text"&&(Ve.tokens[0].tokens[0].text=je+" "+Ve.tokens[0].tokens[0].text)):Ve.tokens.unshift({type:"text",text:je}):He+=je),He+=this.parse(Ve.tokens,Be),xe+=this.renderer.listitem(He,Ye,Ue);ge+=this.renderer.list(xe,ze,Ke);continue}case"html":{ge+=this.renderer.html(We.text);continue}case"paragraph":{ge+=this.renderer.paragraph(this.parseInline(We.tokens));continue}case"text":{for(xe=We.tokens?this.parseInline(We.tokens):We.text;pe+1An error occurred:

    "+be(ke.message+"",!0)+"
    ";throw ke}}Ee.options=Ee.setOptions=function(Se){return me(Ee.defaults,Se),De(Ee.defaults),Ee},Ee.getDefaults=Le,Ee.defaults=Re,Ee.use=function(Se){var we=me({},Se);if(Se.renderer&&function(){var fe=Ee.defaults.renderer||new te,de=function(ve){var ke=fe[ve];fe[ve]=function(){for(var Ne=arguments.length,Te=new Array(Ne),Oe=0;OeAn error occurred:

    "+be(fe.message+"",!0)+"
    ";throw fe}},Ee.Parser=ce,Ee.parser=ce.parse,Ee.Renderer=te,Ee.TextRenderer=Z,Ee.Lexer=oe,Ee.lexer=oe.lex,Ee.Tokenizer=Y,Ee.Slugger=ue,Ee.parse=Ee;var Ae=Ee;return Ae}),define(Q[278],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ArrayNavigator=void 0;class b{constructor(M,w=0,S=M.length,C=w-1){this.items=M,this.start=w,this.end=S,this.index=C}current(){return this.index===this.start-1||this.index===this.end?null:this.items[this.index]}next(){return this.index=Math.min(this.index+1,this.end),this.current()}previous(){return this.index=Math.max(this.index-1,this.start-1),this.current()}first(){return this.index=this.start,this.current()}last(){return this.index=this.end-1,this.current()}}e.ArrayNavigator=b}),define(Q[279],J([0,1,278]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.HistoryNavigator=void 0;class N{constructor(w=[],S=10){this._initialize(w),this._limit=S,this._onChange()}add(w){this._history.delete(w),this._history.add(w),this._onChange()}next(){return this._currentPosition()!==this._elements.length-1?this._navigator.next():null}previous(){return this._currentPosition()!==0?this._navigator.previous():null}current(){return this._navigator.current()}first(){return this._navigator.first()}last(){return this._navigator.last()}has(w){return this._history.has(w)}_onChange(){this._reduceToLimit();const w=this._elements;this._navigator=new b.ArrayNavigator(w,0,w.length,w.length)}_reduceToLimit(){const w=this._elements;w.length>this._limit&&this._initialize(w.slice(w.length-this._limit))}_currentPosition(){const w=this._navigator.current();return w?this._elements.indexOf(w):-1}_initialize(w){this._history=new Set;for(const S of w)this._history.add(S)}get _elements(){const w=[];return this._history.forEach(S=>w.push(S)),w}}e.HistoryNavigator=N}),define(Q[100],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MovingAverage=e.clamp=void 0;function b(M,w,S){return Math.min(Math.max(M,w),S)}e.clamp=b;class N{constructor(){this._n=1,this._val=0}update(w){return this._val=this._val+(w-this._val)/this._n,this._n+=1,this}get value(){return this._val}}e.MovingAverage=N}),define(Q[17],J([0,1]),function(q,e){"use strict";var b;Object.defineProperty(e,"__esModule",{value:!0}),e.isLittleEndian=e.OS=e.setImmediate=e.globals=e.userAgent=e.isIOS=e.isWeb=e.isNative=e.isLinux=e.isMacintosh=e.isWindows=e.isPreferringBrowserCodeLoad=e.browserCodeLoadingCacheStrategy=e.isElectronSandboxed=void 0;const N="en";let M=!1,w=!1,S=!1,C=!1,d=!1,g=!1,p=!1,c,o=N,s,a;const u=typeof self=="object"?self:typeof global=="object"?global:{};let r;typeof process!="undefined"?r=process:typeof u.vscode!="undefined"&&(r=u.vscode.process);const i=typeof((b=r==null?void 0:r.versions)===null||b===void 0?void 0:b.electron)=="string"&&r.type==="renderer";if(e.isElectronSandboxed=i&&(r==null?void 0:r.sandboxed),e.browserCodeLoadingCacheStrategy=(()=>{if(e.isElectronSandboxed)return"bypassHeatCheck";const m=r==null?void 0:r.env.ENABLE_VSCODE_BROWSER_CODE_LOADING;if(typeof m=="string")return m==="none"||m==="code"||m==="bypassHeatCheck"||m==="bypassHeatCheckAndEagerCompile"?m:"bypassHeatCheck"})(),e.isPreferringBrowserCodeLoad=typeof e.browserCodeLoadingCacheStrategy=="string",typeof navigator=="object"&&!i)a=navigator.userAgent,M=a.indexOf("Windows")>=0,w=a.indexOf("Macintosh")>=0,p=(a.indexOf("Macintosh")>=0||a.indexOf("iPad")>=0||a.indexOf("iPhone")>=0)&&!!navigator.maxTouchPoints&&navigator.maxTouchPoints>0,S=a.indexOf("Linux")>=0,g=!0,c=navigator.language,o=c;else if(typeof r=="object"){M=r.platform==="win32",w=r.platform==="darwin",S=r.platform==="linux",C=S&&!!r.env.SNAP&&!!r.env.SNAP_REVISION,c=N,o=N;const m=r.env.VSCODE_NLS_CONFIG;if(m)try{const _=JSON.parse(m),f=_.availableLanguages["*"];c=_.locale,o=f||N,s=_._translationsConfigFile}catch(_){}d=!0}else console.error("Unable to resolve platform.");let n=0;w?n=1:M?n=3:S&&(n=2),e.isWindows=M,e.isMacintosh=w,e.isLinux=S,e.isNative=d,e.isWeb=g,e.isIOS=p,e.userAgent=a,e.globals=u,e.setImmediate=function(){if(e.globals.setImmediate)return e.globals.setImmediate.bind(e.globals);if(typeof e.globals.postMessage=="function"&&!e.globals.importScripts){let f=[];e.globals.addEventListener("message",y=>{if(y.data&&y.data.vscodeSetImmediateId)for(let L=0,I=f.length;L{const L=++v;f.push({id:L,callback:y}),e.globals.postMessage({vscodeSetImmediateId:L},"*")}}if(r&&typeof r.nextTick=="function")return r.nextTick.bind(r);const _=Promise.resolve();return f=>_.then(f)}(),e.OS=w||p?2:M?1:3;let t=!0,l=!1;function h(){if(!l){l=!0;const m=new Uint8Array(2);m[0]=1,m[1]=2,t=new Uint16Array(m.buffer)[0]===(2<<8)+1}return t}e.isLittleEndian=h}),define(Q[280],J([0,1,17]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.platform=e.env=e.cwd=void 0;let N;typeof process!="undefined"?N=process:typeof b.globals.vscode!="undefined"?N={get platform(){return b.globals.vscode.process.platform},get env(){return b.globals.vscode.process.env},nextTick(M){return b.setImmediate(M)},cwd(){return b.globals.vscode.process.env.VSCODE_CWD||b.globals.vscode.process.execPath.substr(0,b.globals.vscode.process.execPath.lastIndexOf(b.globals.vscode.process.platform==="win32"?"\\":"/"))}}:N={get platform(){return b.isWindows?"win32":b.isMacintosh?"darwin":"linux"},nextTick(M){return b.setImmediate(M)},get env(){return Object.create(null)},cwd(){return"/"}},e.cwd=N.cwd,e.env=N.env,e.platform=N.platform}),define(Q[72],J([0,1,280]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.sep=e.extname=e.basename=e.dirname=e.relative=e.resolve=e.normalize=e.posix=e.win32=void 0;const N=65,M=97,w=90,S=122,C=46,d=47,g=92,p=58,c=63;class o extends Error{constructor(l,h,m){let _;typeof h=="string"&&h.indexOf("not ")===0?(_="must not be",h=h.replace(/^not /,"")):_="must be";const f=l.indexOf(".")!==-1?"property":"argument";let v=`The "${l}" ${f} ${_} of type ${h}`;v+=`. Received type ${typeof m}`,super(v),this.code="ERR_INVALID_ARG_TYPE"}}function s(t,l){if(typeof t!="string")throw new o(l,"string",t)}function a(t){return t===d||t===g}function u(t){return t===d}function r(t){return t>=N&&t<=w||t>=M&&t<=S}function i(t,l,h,m){let _="",f=0,v=-1,y=0,L=0;for(let I=0;I<=t.length;++I){if(I2){const k=_.lastIndexOf(h);k===-1?(_="",f=0):(_=_.slice(0,k),f=_.length-1-_.lastIndexOf(h)),v=I,y=0;continue}else if(_.length!==0){_="",f=0,v=I,y=0;continue}}l&&(_+=_.length>0?`${h}..`:"..",f=2)}else _.length>0?_+=`${h}${t.slice(v+1,I)}`:_=t.slice(v+1,I),f=I-v-1;v=I,y=0}else L===C&&y!==-1?++y:y=-1}return _}function n(t,l){if(l===null||typeof l!="object")throw new o("pathObject","Object",l);const h=l.dir||l.root,m=l.base||`${l.name||""}${l.ext||""}`;return h?h===l.root?`${h}${m}`:`${h}${t}${m}`:m}e.win32={resolve(...t){let l="",h="",m=!1;for(let _=t.length-1;_>=-1;_--){let f;if(_>=0){if(f=t[_],s(f,"path"),f.length===0)continue}else l.length===0?f=b.cwd():(f=b.env[`=${l}`]||b.cwd(),(f===void 0||f.slice(0,2).toLowerCase()!==l.toLowerCase()&&f.charCodeAt(2)===g)&&(f=`${l}\\`));const v=f.length;let y=0,L="",I=!1;const k=f.charCodeAt(0);if(v===1)a(k)&&(y=1,I=!0);else if(a(k))if(I=!0,a(f.charCodeAt(1))){let E=2,T=E;for(;E2&&a(f.charCodeAt(2))&&(I=!0,y=3));if(L.length>0)if(l.length>0){if(L.toLowerCase()!==l.toLowerCase())continue}else l=L;if(m){if(l.length>0)break}else if(h=`${f.slice(y)}\\${h}`,m=I,I&&l.length>0)break}return h=i(h,!m,"\\",a),m?`${l}\\${h}`:`${l}${h}`||"."},normalize(t){s(t,"path");const l=t.length;if(l===0)return".";let h=0,m,_=!1;const f=t.charCodeAt(0);if(l===1)return u(f)?"\\":t;if(a(f))if(_=!0,a(t.charCodeAt(1))){let y=2,L=y;for(;y2&&a(t.charCodeAt(2))&&(_=!0,h=3));let v=h0&&a(t.charCodeAt(l-1))&&(v+="\\"),m===void 0?_?`\\${v}`:v:_?`${m}\\${v}`:`${m}${v}`},isAbsolute(t){s(t,"path");const l=t.length;if(l===0)return!1;const h=t.charCodeAt(0);return a(h)||l>2&&r(h)&&t.charCodeAt(1)===p&&a(t.charCodeAt(2))},join(...t){if(t.length===0)return".";let l,h;for(let f=0;f0&&(l===void 0?l=h=v:l+=`\\${v}`)}if(l===void 0)return".";let m=!0,_=0;if(typeof h=="string"&&a(h.charCodeAt(0))){++_;const f=h.length;f>1&&a(h.charCodeAt(1))&&(++_,f>2&&(a(h.charCodeAt(2))?++_:m=!1))}if(m){for(;_=2&&(l=`\\${l.slice(_)}`)}return e.win32.normalize(l)},relative(t,l){if(s(t,"from"),s(l,"to"),t===l)return"";const h=e.win32.resolve(t),m=e.win32.resolve(l);if(h===m||(t=h.toLowerCase(),l=m.toLowerCase(),t===l))return"";let _=0;for(;__&&t.charCodeAt(f-1)===g;)f--;const v=f-_;let y=0;for(;yy&&l.charCodeAt(L-1)===g;)L--;const I=L-y,k=vk){if(l.charCodeAt(y+T)===g)return m.slice(y+T+1);if(T===2)return m.slice(y+T)}v>k&&(t.charCodeAt(_+T)===g?E=T:T===2&&(E=3)),E===-1&&(E=0)}let O="";for(T=_+E+1;T<=f;++T)(T===f||t.charCodeAt(T)===g)&&(O+=O.length===0?"..":"\\..");return y+=E,O.length>0?`${O}${m.slice(y,L)}`:(m.charCodeAt(y)===g&&++y,m.slice(y,L))},toNamespacedPath(t){if(typeof t!="string")return t;if(t.length===0)return"";const l=e.win32.resolve(t);if(l.length<=2)return t;if(l.charCodeAt(0)===g){if(l.charCodeAt(1)===g){const h=l.charCodeAt(2);if(h!==c&&h!==C)return`\\\\?\\UNC\\${l.slice(2)}`}}else if(r(l.charCodeAt(0))&&l.charCodeAt(1)===p&&l.charCodeAt(2)===g)return`\\\\?\\${l}`;return t},dirname(t){s(t,"path");const l=t.length;if(l===0)return".";let h=-1,m=0;const _=t.charCodeAt(0);if(l===1)return a(_)?t:".";if(a(_)){if(h=m=1,a(t.charCodeAt(1))){let y=2,L=y;for(;y2&&a(t.charCodeAt(2))?3:2,m=h);let f=-1,v=!0;for(let y=l-1;y>=m;--y)if(a(t.charCodeAt(y))){if(!v){f=y;break}}else v=!1;if(f===-1){if(h===-1)return".";f=h}return t.slice(0,f)},basename(t,l){l!==void 0&&s(l,"ext"),s(t,"path");let h=0,m=-1,_=!0,f;if(t.length>=2&&r(t.charCodeAt(0))&&t.charCodeAt(1)===p&&(h=2),l!==void 0&&l.length>0&&l.length<=t.length){if(l===t)return"";let v=l.length-1,y=-1;for(f=t.length-1;f>=h;--f){const L=t.charCodeAt(f);if(a(L)){if(!_){h=f+1;break}}else y===-1&&(_=!1,y=f+1),v>=0&&(L===l.charCodeAt(v)?--v==-1&&(m=f):(v=-1,m=y))}return h===m?m=y:m===-1&&(m=t.length),t.slice(h,m)}for(f=t.length-1;f>=h;--f)if(a(t.charCodeAt(f))){if(!_){h=f+1;break}}else m===-1&&(_=!1,m=f+1);return m===-1?"":t.slice(h,m)},extname(t){s(t,"path");let l=0,h=-1,m=0,_=-1,f=!0,v=0;t.length>=2&&t.charCodeAt(1)===p&&r(t.charCodeAt(0))&&(l=m=2);for(let y=t.length-1;y>=l;--y){const L=t.charCodeAt(y);if(a(L)){if(!f){m=y+1;break}continue}_===-1&&(f=!1,_=y+1),L===C?h===-1?h=y:v!==1&&(v=1):h!==-1&&(v=-1)}return h===-1||_===-1||v===0||v===1&&h===_-1&&h===m+1?"":t.slice(h,_)},format:n.bind(null,"\\"),parse(t){s(t,"path");const l={root:"",dir:"",base:"",ext:"",name:""};if(t.length===0)return l;const h=t.length;let m=0,_=t.charCodeAt(0);if(h===1)return a(_)?(l.root=l.dir=t,l):(l.base=l.name=t,l);if(a(_)){if(m=1,a(t.charCodeAt(1))){let E=2,T=E;for(;E0&&(l.root=t.slice(0,m));let f=-1,v=m,y=-1,L=!0,I=t.length-1,k=0;for(;I>=m;--I){if(_=t.charCodeAt(I),a(_)){if(!L){v=I+1;break}continue}y===-1&&(L=!1,y=I+1),_===C?f===-1?f=I:k!==1&&(k=1):f!==-1&&(k=-1)}return y!==-1&&(f===-1||k===0||k===1&&f===y-1&&f===v+1?l.base=l.name=t.slice(v,y):(l.name=t.slice(v,f),l.base=t.slice(v,y),l.ext=t.slice(f,y))),v>0&&v!==m?l.dir=t.slice(0,v-1):l.dir=l.root,l},sep:"\\",delimiter:";",win32:null,posix:null},e.posix={resolve(...t){let l="",h=!1;for(let m=t.length-1;m>=-1&&!h;m--){const _=m>=0?t[m]:b.cwd();s(_,"path"),_.length!==0&&(l=`${_}/${l}`,h=_.charCodeAt(0)===d)}return l=i(l,!h,"/",u),h?`/${l}`:l.length>0?l:"."},normalize(t){if(s(t,"path"),t.length===0)return".";const l=t.charCodeAt(0)===d,h=t.charCodeAt(t.length-1)===d;return t=i(t,!l,"/",u),t.length===0?l?"/":h?"./":".":(h&&(t+="/"),l?`/${t}`:t)},isAbsolute(t){return s(t,"path"),t.length>0&&t.charCodeAt(0)===d},join(...t){if(t.length===0)return".";let l;for(let h=0;h0&&(l===void 0?l=m:l+=`/${m}`)}return l===void 0?".":e.posix.normalize(l)},relative(t,l){if(s(t,"from"),s(l,"to"),t===l||(t=e.posix.resolve(t),l=e.posix.resolve(l),t===l))return"";const h=1,m=t.length,_=m-h,f=1,v=l.length-f,y=_y){if(l.charCodeAt(f+I)===d)return l.slice(f+I+1);if(I===0)return l.slice(f+I)}else _>y&&(t.charCodeAt(h+I)===d?L=I:I===0&&(L=0));let k="";for(I=h+L+1;I<=m;++I)(I===m||t.charCodeAt(I)===d)&&(k+=k.length===0?"..":"/..");return`${k}${l.slice(f+L)}`},toNamespacedPath(t){return t},dirname(t){if(s(t,"path"),t.length===0)return".";const l=t.charCodeAt(0)===d;let h=-1,m=!0;for(let _=t.length-1;_>=1;--_)if(t.charCodeAt(_)===d){if(!m){h=_;break}}else m=!1;return h===-1?l?"/":".":l&&h===1?"//":t.slice(0,h)},basename(t,l){l!==void 0&&s(l,"ext"),s(t,"path");let h=0,m=-1,_=!0,f;if(l!==void 0&&l.length>0&&l.length<=t.length){if(l===t)return"";let v=l.length-1,y=-1;for(f=t.length-1;f>=0;--f){const L=t.charCodeAt(f);if(L===d){if(!_){h=f+1;break}}else y===-1&&(_=!1,y=f+1),v>=0&&(L===l.charCodeAt(v)?--v==-1&&(m=f):(v=-1,m=y))}return h===m?m=y:m===-1&&(m=t.length),t.slice(h,m)}for(f=t.length-1;f>=0;--f)if(t.charCodeAt(f)===d){if(!_){h=f+1;break}}else m===-1&&(_=!1,m=f+1);return m===-1?"":t.slice(h,m)},extname(t){s(t,"path");let l=-1,h=0,m=-1,_=!0,f=0;for(let v=t.length-1;v>=0;--v){const y=t.charCodeAt(v);if(y===d){if(!_){h=v+1;break}continue}m===-1&&(_=!1,m=v+1),y===C?l===-1?l=v:f!==1&&(f=1):l!==-1&&(f=-1)}return l===-1||m===-1||f===0||f===1&&l===m-1&&l===h+1?"":t.slice(l,m)},format:n.bind(null,"/"),parse(t){s(t,"path");const l={root:"",dir:"",base:"",ext:"",name:""};if(t.length===0)return l;const h=t.charCodeAt(0)===d;let m;h?(l.root="/",m=1):m=0;let _=-1,f=0,v=-1,y=!0,L=t.length-1,I=0;for(;L>=m;--L){const k=t.charCodeAt(L);if(k===d){if(!y){f=L+1;break}continue}v===-1&&(y=!1,v=L+1),k===C?_===-1?_=L:I!==1&&(I=1):_!==-1&&(I=-1)}if(v!==-1){const k=f===0&&h?1:f;_===-1||I===0||I===1&&_===v-1&&_===f+1?l.base=l.name=t.slice(k,v):(l.name=t.slice(k,_),l.base=t.slice(k,v),l.ext=t.slice(_,v))}return f>0?l.dir=t.slice(0,f-1):h&&(l.dir="/"),l},sep:"/",delimiter:":",win32:null,posix:null},e.posix.win32=e.win32.win32=e.win32,e.posix.posix=e.win32.posix=e.posix,e.normalize=b.platform==="win32"?e.win32.normalize:e.posix.normalize,e.resolve=b.platform==="win32"?e.win32.resolve:e.posix.resolve,e.relative=b.platform==="win32"?e.win32.relative:e.posix.relative,e.dirname=b.platform==="win32"?e.win32.dirname:e.posix.dirname,e.basename=b.platform==="win32"?e.win32.basename:e.posix.basename,e.extname=b.platform==="win32"?e.win32.extname:e.posix.extname,e.sep=b.platform==="win32"?e.win32.sep:e.posix.sep}),define(Q[120],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Range=void 0;var b;(function(N){function M(d,g){if(d.start>=g.end||g.start>=d.end)return{start:0,end:0};const p=Math.max(d.start,g.start),c=Math.min(d.end,g.end);return c-p<=0?{start:0,end:0}:{start:p,end:c}}N.intersect=M;function w(d){return d.end-d.start<=0}N.isEmpty=w;function S(d,g){return!w(M(d,g))}N.intersects=S;function C(d,g){const p=[],c={start:d.start,end:Math.min(g.start,d.end)},o={start:Math.max(g.end,d.start),end:d.end};return w(c)||p.push(c),w(o)||p.push(o),p}N.relativeComplement=C})(b=e.Range||(e.Range={}))}),define(Q[281],J([0,1,120]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RangeMap=e.consolidate=e.shift=e.groupIntersect=void 0;function N(d,g){const p=[];for(let c of g)if(!(d.start>=c.range.end)){if(d.endg.concat(p),[]))}class C{constructor(){this.groups=[],this._size=0}splice(g,p,c=[]){const o=c.length-p,s=N({start:0,end:g},this.groups),a=N({start:g+p,end:Number.POSITIVE_INFINITY},this.groups).map(r=>({range:M(r.range,o),size:r.size})),u=c.map((r,i)=>({range:{start:g+i,end:g+i+1},size:r.size}));this.groups=S(s,u,a),this._size=this.groups.reduce((r,i)=>r+i.size*(i.range.end-i.range.start),0)}get count(){const g=this.groups.length;return g?this.groups[g-1].range.end:0}get size(){return this._size}indexAt(g){if(g<0)return-1;let p=0,c=0;for(let o of this.groups){const s=o.range.end-o.range.start,a=c+s*o.size;if(gN.Disposable.None;function u(A){return(B,F=null,D)=>{let R=!1,W;return W=A(x=>{if(!R)return W?W.dispose():R=!0,B.call(F,x)},null,D),R&&W.dispose(),W}}a.once=u;function r(A,B){return m((F,D=null,R)=>A(W=>F.call(D,B(W)),null,R))}a.map=r;function i(A,B){return m((F,D=null,R)=>A(W=>{B(W),F.call(D,W)},null,R))}a.forEach=i;function n(A,B){return m((F,D=null,R)=>A(W=>B(W)&&F.call(D,W),null,R))}a.filter=n;function t(A){return A}a.signal=t;function l(...A){return(B,F=null,D)=>N.combinedDisposable(...A.map(R=>R(W=>B.call(F,W),null,D)))}a.any=l;function h(A,B,F){let D=F;return r(A,R=>(D=B(D,R),D))}a.reduce=h;function m(A){let B;const F=new p({onFirstListenerAdd(){B=A(F.fire,F)},onLastListenerRemove(){B.dispose()}});return F.event}a.snapshot=m;function _(A,B,F=100,D=!1,R){let W,x,K,Y=0;const ee=new p({leakWarningThreshold:R,onFirstListenerAdd(){W=A(se=>{Y++,x=B(x,se),D&&!K&&(ee.fire(x),x=void 0),clearTimeout(K),K=setTimeout(()=>{const ne=x;x=void 0,K=void 0,(!D||Y>1)&&ee.fire(ne),Y=0},F)})},onLastListenerRemove(){W.dispose()}});return ee.event}a.debounce=_;function f(A){const B=new Date().getTime();return r(u(A),F=>new Date().getTime()-B)}a.stopwatch=f;function v(A){let B=!0,F;return n(A,D=>{const R=B||D!==F;return B=!1,F=D,R})}a.latch=v;function y(A,B=!1,F=[]){let D=F.slice(),R=A(K=>{D?D.push(K):x.fire(K)});const W=()=>{D&&D.forEach(K=>x.fire(K)),D=null},x=new p({onFirstListenerAdd(){R||(R=A(K=>x.fire(K)))},onFirstListenerDidAdd(){D&&(B?setTimeout(W):W())},onLastListenerRemove(){R&&R.dispose(),R=null}});return x.event}a.buffer=y;class L{constructor(B){this.event=B}map(B){return new L(r(this.event,B))}forEach(B){return new L(i(this.event,B))}filter(B){return new L(n(this.event,B))}reduce(B,F){return new L(h(this.event,B,F))}latch(){return new L(v(this.event))}debounce(B,F=100,D=!1,R){return new L(_(this.event,B,F,D,R))}on(B,F,D){return this.event(B,F,D)}once(B,F,D){return u(this.event)(B,F,D)}}function I(A){return new L(A)}a.chain=I;function k(A,B,F=D=>D){const D=(...K)=>x.fire(F(...K)),R=()=>A.on(B,D),W=()=>A.removeListener(B,D),x=new p({onFirstListenerAdd:R,onLastListenerRemove:W});return x.event}a.fromNodeEventEmitter=k;function E(A,B,F=D=>D){const D=(...K)=>x.fire(F(...K)),R=()=>A.addEventListener(B,D),W=()=>A.removeEventListener(B,D),x=new p({onFirstListenerAdd:R,onLastListenerRemove:W});return x.event}a.fromDOMEventEmitter=E;function T(A){const B=new p;let F=!1;return A.then(void 0,()=>null).then(()=>{F?B.fire(void 0):setTimeout(()=>B.fire(void 0),0)}),F=!0,B.event}a.fromPromise=T;function O(A){return new Promise(B=>u(A)(B))}a.toPromise=O})(S=e.Event||(e.Event={}));class C{constructor(u){this._listenerCount=0,this._invocationCount=0,this._elapsedOverall=0,this._name=`${u}_${C._idPool++}`}start(u){this._stopWatch=new w.StopWatch(!0),this._listenerCount=u}stop(){if(this._stopWatch){const u=this._stopWatch.elapsed();this._elapsedOverall+=u,this._invocationCount+=1,console.info(`did FIRE ${this._name}: elapsed_ms: ${u.toFixed(5)}, listener: ${this._listenerCount} (elapsed_overall: ${this._elapsedOverall.toFixed(2)}, invocations: ${this._invocationCount})`),this._stopWatch=void 0}}}C._idPool=0;let d=-1;class g{constructor(u,r=Math.random().toString(18).slice(2,5)){this.customThreshold=u,this.name=r,this._warnCountdown=0}dispose(){this._stacks&&this._stacks.clear()}check(u){let r=d;if(typeof this.customThreshold=="number"&&(r=this.customThreshold),!(r<=0||u{const t=this._stacks.get(i)||0;this._stacks.set(i,t-1)}}}}class p{constructor(u){var r;this._disposed=!1,this._options=u,this._leakageMon=d>0?new g(this._options&&this._options.leakWarningThreshold):void 0,this._perfMon=((r=this._options)===null||r===void 0?void 0:r._profName)?new C(this._options._profName):void 0}get event(){return this._event||(this._event=(u,r,i)=>{var n;this._listeners||(this._listeners=new M.LinkedList);const t=this._listeners.isEmpty();t&&this._options&&this._options.onFirstListenerAdd&&this._options.onFirstListenerAdd(this);const l=this._listeners.push(r?[u,r]:u);t&&this._options&&this._options.onFirstListenerDidAdd&&this._options.onFirstListenerDidAdd(this),this._options&&this._options.onListenerDidAdd&&this._options.onListenerDidAdd(this,u,r);const h=(n=this._leakageMon)===null||n===void 0?void 0:n.check(this._listeners.size);let m;return m={dispose:()=>{h&&h(),m.dispose=p._noop,this._disposed||(l(),this._options&&this._options.onLastListenerRemove&&(this._listeners&&!this._listeners.isEmpty()||this._options.onLastListenerRemove(this)))}},i instanceof N.DisposableStore?i.add(m):Array.isArray(i)&&i.push(m),m}),this._event}fire(u){var r,i;if(this._listeners){this._deliveryQueue||(this._deliveryQueue=new M.LinkedList);for(let n of this._listeners)this._deliveryQueue.push([n,u]);for((r=this._perfMon)===null||r===void 0||r.start(this._deliveryQueue.size);this._deliveryQueue.size>0;){const[n,t]=this._deliveryQueue.shift();try{typeof n=="function"?n.call(void 0,t):n[0].call(n[1],t)}catch(l){b.onUnexpectedError(l)}}(i=this._perfMon)===null||i===void 0||i.stop()}}dispose(){var u,r,i;(u=this._listeners)===null||u===void 0||u.clear(),(r=this._deliveryQueue)===null||r===void 0||r.clear(),(i=this._leakageMon)===null||i===void 0||i.dispose(),this._disposed=!0}}e.Emitter=p,p._noop=function(){};class c extends p{constructor(u){super(u);this._isPaused=0,this._eventQueue=new M.LinkedList,this._mergeFn=u==null?void 0:u.merge}pause(){this._isPaused++}resume(){if(this._isPaused!==0&&--this._isPaused==0)if(this._mergeFn){const u=Array.from(this._eventQueue);this._eventQueue.clear(),super.fire(this._mergeFn(u))}else for(;!this._isPaused&&this._eventQueue.size!==0;)super.fire(this._eventQueue.shift())}fire(u){this._listeners&&(this._isPaused!==0?this._eventQueue.push(u):super.fire(u))}}e.PauseableEmitter=c;class o{constructor(){this.buffers=[]}wrapEvent(u){return(r,i,n)=>u(t=>{const l=this.buffers[this.buffers.length-1];l?l.push(()=>r.call(i,t)):r.call(i,t)},void 0,n)}bufferEvents(u){const r=[];this.buffers.push(r);const i=u();return this.buffers.pop(),r.forEach(n=>n()),i}}e.EventBufferer=o;class s{constructor(){this.listening=!1,this.inputEvent=S.None,this.inputEventListener=N.Disposable.None,this.emitter=new p({onFirstListenerDidAdd:()=>{this.listening=!0,this.inputEventListener=this.inputEvent(this.emitter.fire,this.emitter)},onLastListenerRemove:()=>{this.listening=!1,this.inputEventListener.dispose()}}),this.event=this.emitter.event}set input(u){this.inputEvent=u,this.listening&&(this.inputEventListener.dispose(),this.inputEventListener=u(this.emitter.fire,this.emitter))}dispose(){this.inputEventListener.dispose(),this.emitter.dispose()}}e.Relay=s}),define(Q[35],J([0,1,6]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.isStandalone=e.isAndroid=e.isElectron=e.isEdgeLegacyWebView=e.isIPad=e.isWebkitWebView=e.isSafari=e.isChrome=e.isWebKit=e.isFirefox=e.getPixelRatio=e.getZoomFactor=e.onDidChangeZoomLevel=e.getTimeSinceLastZoomLevelChanged=e.getZoomLevel=void 0;class N{constructor(){this._zoomLevel=0,this._lastZoomLevelChangeTime=0,this._onDidChangeZoomLevel=new b.Emitter,this.onDidChangeZoomLevel=this._onDidChangeZoomLevel.event,this._zoomFactor=1}getZoomLevel(){return this._zoomLevel}getTimeSinceLastZoomLevelChanged(){return Date.now()-this._lastZoomLevelChangeTime}getZoomFactor(){return this._zoomFactor}getPixelRatio(){let c=document.createElement("canvas").getContext("2d"),o=window.devicePixelRatio||1,s=c.webkitBackingStorePixelRatio||c.mozBackingStorePixelRatio||c.msBackingStorePixelRatio||c.oBackingStorePixelRatio||c.backingStorePixelRatio||1;return o/s}}N.INSTANCE=new N;function M(){return N.INSTANCE.getZoomLevel()}e.getZoomLevel=M;function w(){return N.INSTANCE.getTimeSinceLastZoomLevelChanged()}e.getTimeSinceLastZoomLevelChanged=w;function S(p){return N.INSTANCE.onDidChangeZoomLevel(p)}e.onDidChangeZoomLevel=S;function C(){return N.INSTANCE.getZoomFactor()}e.getZoomFactor=C;function d(){return N.INSTANCE.getPixelRatio()}e.getPixelRatio=d;const g=navigator.userAgent;e.isFirefox=g.indexOf("Firefox")>=0,e.isWebKit=g.indexOf("AppleWebKit")>=0,e.isChrome=g.indexOf("Chrome")>=0,e.isSafari=!e.isChrome&&g.indexOf("Safari")>=0,e.isWebkitWebView=!e.isChrome&&!e.isSafari&&e.isWebKit,e.isIPad=g.indexOf("iPad")>=0||e.isSafari&&navigator.maxTouchPoints>0,e.isEdgeLegacyWebView=g.indexOf("Edge/")>=0&&g.indexOf("WebView/")>=0,e.isElectron=g.indexOf("Electron/")>=0,e.isAndroid=g.indexOf("Android")>=0,e.isStandalone=window.matchMedia&&window.matchMedia("(display-mode: standalone)").matches}),define(Q[151],J([0,1,35,17]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.BrowserFeatures=void 0,e.BrowserFeatures={clipboard:{writeText:N.isNative||document.queryCommandSupported&&document.queryCommandSupported("copy")||!!(navigator&&navigator.clipboard&&navigator.clipboard.writeText),readText:N.isNative||!!(navigator&&navigator.clipboard&&navigator.clipboard.readText)},keyboard:(()=>N.isNative||b.isStandalone?0:navigator.keyboard||b.isSafari?1:2)(),touch:"ontouchstart"in window||navigator.maxTouchPoints>0||window.navigator.msMaxTouchPoints>0,pointerEvents:window.PointerEvent&&("ontouchstart"in window||window.navigator.maxTouchPoints>0||navigator.maxTouchPoints>0||window.navigator.msMaxTouchPoints>0)}}),define(Q[55],J([0,1,6]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.stop=e.stopEvent=e.domEvent=void 0;const N=(S,C,d)=>{const g=c=>p.fire(c),p=new b.Emitter({onFirstListenerAdd:()=>{S.addEventListener(C,g,d)},onLastListenerRemove:()=>{S.removeEventListener(C,g,d)}});return p.event};e.domEvent=N;function M(S){return S.preventDefault(),S.stopPropagation(),S}e.stopEvent=M;function w(S){return b.Event.map(S,M)}e.stop=w}),define(Q[56],J([0,1,35,39,17]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.StandardKeyboardEvent=void 0;let w=new Array(230),S=new Array(112);(function(){for(let a=0;a{l.token.onCancellationRequested(()=>{f(N.canceled())}),Promise.resolve(h).then(v=>{l.dispose(),_(v)},v=>{l.dispose(),f(v)})});return new class{cancel(){l.cancel()}then(_,f){return m.then(_,f)}catch(_){return this.then(void 0,_)}finally(_){return m.finally(_)}}}e.createCancelablePromise=S;function C(t,l,h){return Promise.race([t,new Promise(m=>l.onCancellationRequested(()=>m(h)))])}e.raceCancellation=C;class d{constructor(){this.activePromise=null,this.queuedPromise=null,this.queuedPromiseFactory=null}queue(l){if(this.activePromise){if(this.queuedPromiseFactory=l,!this.queuedPromise){const h=()=>{this.queuedPromise=null;const m=this.queue(this.queuedPromiseFactory);return this.queuedPromiseFactory=null,m};this.queuedPromise=new Promise(m=>{this.activePromise.then(h,h).then(m)})}return new Promise((h,m)=>{this.queuedPromise.then(h,m)})}return this.activePromise=l(),new Promise((h,m)=>{this.activePromise.then(_=>{this.activePromise=null,h(_)},_=>{this.activePromise=null,m(_)})})}}e.Throttler=d;class g{constructor(l){this.defaultDelay=l,this.timeout=null,this.completionPromise=null,this.doResolve=null,this.doReject=null,this.task=null}trigger(l,h=this.defaultDelay){return this.task=l,this.cancelTimeout(),this.completionPromise||(this.completionPromise=new Promise((m,_)=>{this.doResolve=m,this.doReject=_}).then(()=>{if(this.completionPromise=null,this.doResolve=null,this.task){const m=this.task;return this.task=null,m()}})),this.timeout=setTimeout(()=>{this.timeout=null,this.doResolve&&this.doResolve(null)},h),this.completionPromise}isTriggered(){return this.timeout!==null}cancel(){this.cancelTimeout(),this.completionPromise&&(this.doReject&&this.doReject(N.canceled()),this.completionPromise=null)}cancelTimeout(){this.timeout!==null&&(clearTimeout(this.timeout),this.timeout=null)}dispose(){this.cancelTimeout()}}e.Delayer=g;class p{constructor(l){this.delayer=new g(l),this.throttler=new d}trigger(l,h){return this.delayer.trigger(()=>this.throttler.queue(l),h)}cancel(){this.delayer.cancel()}dispose(){this.delayer.dispose()}}e.ThrottledDelayer=p;function c(t,l){return l?new Promise((h,m)=>{const _=setTimeout(h,t);l.onCancellationRequested(()=>{clearTimeout(_),m(N.canceled())})}):S(h=>c(t,h))}e.timeout=c;function o(t,l=0){const h=setTimeout(t,l);return M.toDisposable(()=>clearTimeout(h))}e.disposableTimeout=o;function s(t,l=m=>!!m,h=null){let m=0;const _=t.length,f=()=>{if(m>=_)return Promise.resolve(h);const v=t[m++];return Promise.resolve(v()).then(L=>l(L)?Promise.resolve(L):f())};return f()}e.first=s;class a{constructor(l,h){this._token=-1,typeof l=="function"&&typeof h=="number"&&this.setIfNotSet(l,h)}dispose(){this.cancel()}cancel(){this._token!==-1&&(clearTimeout(this._token),this._token=-1)}cancelAndSet(l,h){this.cancel(),this._token=setTimeout(()=>{this._token=-1,l()},h)}setIfNotSet(l,h){this._token===-1&&(this._token=setTimeout(()=>{this._token=-1,l()},h))}}e.TimeoutTimer=a;class u{constructor(){this._token=-1}dispose(){this.cancel()}cancel(){this._token!==-1&&(clearInterval(this._token),this._token=-1)}cancelAndSet(l,h){this.cancel(),this._token=setInterval(()=>{l()},h)}}e.IntervalTimer=u;class r{constructor(l,h){this.timeoutToken=-1,this.runner=l,this.timeout=h,this.timeoutHandler=this.onTimeout.bind(this)}dispose(){this.cancel(),this.runner=null}cancel(){this.isScheduled()&&(clearTimeout(this.timeoutToken),this.timeoutToken=-1)}schedule(l=this.timeout){this.cancel(),this.timeoutToken=setTimeout(this.timeoutHandler,l)}get delay(){return this.timeout}set delay(l){this.timeout=l}isScheduled(){return this.timeoutToken!==-1}onTimeout(){this.timeoutToken=-1,this.runner&&this.doRun()}doRun(){this.runner&&this.runner()}}e.RunOnceScheduler=r,function(){if(typeof requestIdleCallback!="function"||typeof cancelIdleCallback!="function"){const t=Object.freeze({didTimeout:!0,timeRemaining(){return 15}});e.runWhenIdle=l=>{const h=setTimeout(()=>l(t));let m=!1;return{dispose(){m||(m=!0,clearTimeout(h))}}}}else e.runWhenIdle=(t,l)=>{const h=requestIdleCallback(t,typeof l=="number"?{timeout:l}:void 0);let m=!1;return{dispose(){m||(m=!0,cancelIdleCallback(h))}}}}();class i{constructor(l){this._didRun=!1,this._executor=()=>{try{this._value=l()}catch(h){this._error=h}finally{this._didRun=!0}},this._handle=e.runWhenIdle(()=>this._executor())}dispose(){this._handle.dispose()}get value(){if(this._didRun||(this._handle.dispose(),this._executor()),this._error)throw this._error;return this._value}}e.IdleValue=i;var n;(function(t){function l(f){return Ie(this,void 0,void 0,function*(){return typeof Promise.allSettled=="function"?h(f):m(f)})}t.allSettled=l;function h(f){return Ie(this,void 0,void 0,function*(){return Promise.allSettled(f)})}function m(f){return Ie(this,void 0,void 0,function*(){return Promise.all(f.map(v=>v.then(y=>({status:"fulfilled",value:y}),y=>({status:"rejected",reason:y}))))})}function _(f){return Ie(this,void 0,void 0,function*(){let v;const y=yield Promise.all(f.map(L=>L.then(I=>I,I=>{v||(v=I)})));if(typeof v!="undefined")throw v;return y})}t.settled=_})(n=e.Promises||(e.Promises={}))}),define(Q[282],J([0,1,15,2]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ScrollbarVisibilityController=void 0;class M extends N.Disposable{constructor(S,C,d){super();this._visibility=S,this._visibleClassName=C,this._invisibleClassName=d,this._domNode=null,this._isVisible=!1,this._isNeeded=!1,this._shouldBeVisible=!1,this._revealTimer=this._register(new b.TimeoutTimer)}applyVisibilitySetting(S){return this._visibility===2?!1:this._visibility===3?!0:S}setShouldBeVisible(S){const C=this.applyVisibilitySetting(S);this._shouldBeVisible!==C&&(this._shouldBeVisible=C,this.ensureVisibility())}setIsNeeded(S){this._isNeeded!==S&&(this._isNeeded=S,this.ensureVisibility())}setDomNode(S){this._domNode=S,this._domNode.setClassName(this._invisibleClassName),this.setShouldBeVisible(!1)}ensureVisibility(){if(!this._isNeeded){this._hide(!1);return}this._shouldBeVisible?this._reveal():this._hide(!0)}_reveal(){this._isVisible||(this._isVisible=!0,this._revealTimer.setIfNotSet(()=>{this._domNode&&this._domNode.setClassName(this._visibleClassName)},0))}_hide(S){this._revealTimer.cancel(),!!this._isVisible&&(this._isVisible=!1,this._domNode&&this._domNode.setClassName(this._invisibleClassName+(S?" fade":"")))}}e.ScrollbarVisibilityController=M}),define(Q[27],J([0,1,6]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CSSIcon=e.Codicon=e.registerCodicon=e.iconRegistry=void 0;class N{constructor(){this._icons=new Map,this._onDidRegister=new b.Emitter}add(g){const p=this._icons.get(g.id);p?g.description?p.description=g.description:console.error(`Duplicate registration of codicon ${g.id}`):(this._icons.set(g.id,g),this._onDidRegister.fire(g))}get(g){return this._icons.get(g)}get all(){return this._icons.values()}get onDidRegister(){return this._onDidRegister.event}}const M=new N;e.iconRegistry=M;function w(d,g){return new S(d,g)}e.registerCodicon=w;class S{constructor(g,p,c){this.id=g,this.definition=p,this.description=c,M.add(this)}get classNames(){return"codicon codicon-"+this.id}get classNamesArray(){return["codicon","codicon-"+this.id]}get cssSelector(){return".codicon.codicon-"+this.id}}e.Codicon=S;var C;(function(d){d.iconNameSegment="[A-Za-z0-9]+",d.iconNameExpression="[A-Za-z0-9\\-]+",d.iconModifierExpression="~[A-Za-z]+";const g=new RegExp(`^(${d.iconNameExpression})(${d.iconModifierExpression})?$`);function p(s){if(s instanceof S)return["codicon","codicon-"+s.id];const a=g.exec(s.id);if(!a)return p(S.error);let[,u,r]=a;const i=["codicon","codicon-"+u];return r&&i.push("codicon-modifier-"+r.substr(1)),i}d.asClassNameArray=p;function c(s){return p(s).join(" ")}d.asClassName=c;function o(s){return"."+p(s).join(".")}d.asCSSSelector=o})(C=e.CSSIcon||(e.CSSIcon={})),function(d){d.add=new d("add",{fontCharacter:"\\ea60"}),d.plus=new d("plus",{fontCharacter:"\\ea60"}),d.gistNew=new d("gist-new",{fontCharacter:"\\ea60"}),d.repoCreate=new d("repo-create",{fontCharacter:"\\ea60"}),d.lightbulb=new d("lightbulb",{fontCharacter:"\\ea61"}),d.lightBulb=new d("light-bulb",{fontCharacter:"\\ea61"}),d.repo=new d("repo",{fontCharacter:"\\ea62"}),d.repoDelete=new d("repo-delete",{fontCharacter:"\\ea62"}),d.gistFork=new d("gist-fork",{fontCharacter:"\\ea63"}),d.repoForked=new d("repo-forked",{fontCharacter:"\\ea63"}),d.gitPullRequest=new d("git-pull-request",{fontCharacter:"\\ea64"}),d.gitPullRequestAbandoned=new d("git-pull-request-abandoned",{fontCharacter:"\\ea64"}),d.recordKeys=new d("record-keys",{fontCharacter:"\\ea65"}),d.keyboard=new d("keyboard",{fontCharacter:"\\ea65"}),d.tag=new d("tag",{fontCharacter:"\\ea66"}),d.tagAdd=new d("tag-add",{fontCharacter:"\\ea66"}),d.tagRemove=new d("tag-remove",{fontCharacter:"\\ea66"}),d.person=new d("person",{fontCharacter:"\\ea67"}),d.personAdd=new d("person-add",{fontCharacter:"\\ea67"}),d.personFollow=new d("person-follow",{fontCharacter:"\\ea67"}),d.personOutline=new d("person-outline",{fontCharacter:"\\ea67"}),d.personFilled=new d("person-filled",{fontCharacter:"\\ea67"}),d.gitBranch=new d("git-branch",{fontCharacter:"\\ea68"}),d.gitBranchCreate=new d("git-branch-create",{fontCharacter:"\\ea68"}),d.gitBranchDelete=new d("git-branch-delete",{fontCharacter:"\\ea68"}),d.sourceControl=new d("source-control",{fontCharacter:"\\ea68"}),d.mirror=new d("mirror",{fontCharacter:"\\ea69"}),d.mirrorPublic=new d("mirror-public",{fontCharacter:"\\ea69"}),d.star=new d("star",{fontCharacter:"\\ea6a"}),d.starAdd=new d("star-add",{fontCharacter:"\\ea6a"}),d.starDelete=new d("star-delete",{fontCharacter:"\\ea6a"}),d.starEmpty=new d("star-empty",{fontCharacter:"\\ea6a"}),d.comment=new d("comment",{fontCharacter:"\\ea6b"}),d.commentAdd=new d("comment-add",{fontCharacter:"\\ea6b"}),d.alert=new d("alert",{fontCharacter:"\\ea6c"}),d.warning=new d("warning",{fontCharacter:"\\ea6c"}),d.search=new d("search",{fontCharacter:"\\ea6d"}),d.searchSave=new d("search-save",{fontCharacter:"\\ea6d"}),d.logOut=new d("log-out",{fontCharacter:"\\ea6e"}),d.signOut=new d("sign-out",{fontCharacter:"\\ea6e"}),d.logIn=new d("log-in",{fontCharacter:"\\ea6f"}),d.signIn=new d("sign-in",{fontCharacter:"\\ea6f"}),d.eye=new d("eye",{fontCharacter:"\\ea70"}),d.eyeUnwatch=new d("eye-unwatch",{fontCharacter:"\\ea70"}),d.eyeWatch=new d("eye-watch",{fontCharacter:"\\ea70"}),d.circleFilled=new d("circle-filled",{fontCharacter:"\\ea71"}),d.primitiveDot=new d("primitive-dot",{fontCharacter:"\\ea71"}),d.closeDirty=new d("close-dirty",{fontCharacter:"\\ea71"}),d.debugBreakpoint=new d("debug-breakpoint",{fontCharacter:"\\ea71"}),d.debugBreakpointDisabled=new d("debug-breakpoint-disabled",{fontCharacter:"\\ea71"}),d.debugHint=new d("debug-hint",{fontCharacter:"\\ea71"}),d.primitiveSquare=new d("primitive-square",{fontCharacter:"\\ea72"}),d.edit=new d("edit",{fontCharacter:"\\ea73"}),d.pencil=new d("pencil",{fontCharacter:"\\ea73"}),d.info=new d("info",{fontCharacter:"\\ea74"}),d.issueOpened=new d("issue-opened",{fontCharacter:"\\ea74"}),d.gistPrivate=new d("gist-private",{fontCharacter:"\\ea75"}),d.gitForkPrivate=new d("git-fork-private",{fontCharacter:"\\ea75"}),d.lock=new d("lock",{fontCharacter:"\\ea75"}),d.mirrorPrivate=new d("mirror-private",{fontCharacter:"\\ea75"}),d.close=new d("close",{fontCharacter:"\\ea76"}),d.removeClose=new d("remove-close",{fontCharacter:"\\ea76"}),d.x=new d("x",{fontCharacter:"\\ea76"}),d.repoSync=new d("repo-sync",{fontCharacter:"\\ea77"}),d.sync=new d("sync",{fontCharacter:"\\ea77"}),d.clone=new d("clone",{fontCharacter:"\\ea78"}),d.desktopDownload=new d("desktop-download",{fontCharacter:"\\ea78"}),d.beaker=new d("beaker",{fontCharacter:"\\ea79"}),d.microscope=new d("microscope",{fontCharacter:"\\ea79"}),d.vm=new d("vm",{fontCharacter:"\\ea7a"}),d.deviceDesktop=new d("device-desktop",{fontCharacter:"\\ea7a"}),d.file=new d("file",{fontCharacter:"\\ea7b"}),d.fileText=new d("file-text",{fontCharacter:"\\ea7b"}),d.more=new d("more",{fontCharacter:"\\ea7c"}),d.ellipsis=new d("ellipsis",{fontCharacter:"\\ea7c"}),d.kebabHorizontal=new d("kebab-horizontal",{fontCharacter:"\\ea7c"}),d.mailReply=new d("mail-reply",{fontCharacter:"\\ea7d"}),d.reply=new d("reply",{fontCharacter:"\\ea7d"}),d.organization=new d("organization",{fontCharacter:"\\ea7e"}),d.organizationFilled=new d("organization-filled",{fontCharacter:"\\ea7e"}),d.organizationOutline=new d("organization-outline",{fontCharacter:"\\ea7e"}),d.newFile=new d("new-file",{fontCharacter:"\\ea7f"}),d.fileAdd=new d("file-add",{fontCharacter:"\\ea7f"}),d.newFolder=new d("new-folder",{fontCharacter:"\\ea80"}),d.fileDirectoryCreate=new d("file-directory-create",{fontCharacter:"\\ea80"}),d.trash=new d("trash",{fontCharacter:"\\ea81"}),d.trashcan=new d("trashcan",{fontCharacter:"\\ea81"}),d.history=new d("history",{fontCharacter:"\\ea82"}),d.clock=new d("clock",{fontCharacter:"\\ea82"}),d.folder=new d("folder",{fontCharacter:"\\ea83"}),d.fileDirectory=new d("file-directory",{fontCharacter:"\\ea83"}),d.symbolFolder=new d("symbol-folder",{fontCharacter:"\\ea83"}),d.logoGithub=new d("logo-github",{fontCharacter:"\\ea84"}),d.markGithub=new d("mark-github",{fontCharacter:"\\ea84"}),d.github=new d("github",{fontCharacter:"\\ea84"}),d.terminal=new d("terminal",{fontCharacter:"\\ea85"}),d.console=new d("console",{fontCharacter:"\\ea85"}),d.repl=new d("repl",{fontCharacter:"\\ea85"}),d.zap=new d("zap",{fontCharacter:"\\ea86"}),d.symbolEvent=new d("symbol-event",{fontCharacter:"\\ea86"}),d.error=new d("error",{fontCharacter:"\\ea87"}),d.stop=new d("stop",{fontCharacter:"\\ea87"}),d.variable=new d("variable",{fontCharacter:"\\ea88"}),d.symbolVariable=new d("symbol-variable",{fontCharacter:"\\ea88"}),d.array=new d("array",{fontCharacter:"\\ea8a"}),d.symbolArray=new d("symbol-array",{fontCharacter:"\\ea8a"}),d.symbolModule=new d("symbol-module",{fontCharacter:"\\ea8b"}),d.symbolPackage=new d("symbol-package",{fontCharacter:"\\ea8b"}),d.symbolNamespace=new d("symbol-namespace",{fontCharacter:"\\ea8b"}),d.symbolObject=new d("symbol-object",{fontCharacter:"\\ea8b"}),d.symbolMethod=new d("symbol-method",{fontCharacter:"\\ea8c"}),d.symbolFunction=new d("symbol-function",{fontCharacter:"\\ea8c"}),d.symbolConstructor=new d("symbol-constructor",{fontCharacter:"\\ea8c"}),d.symbolBoolean=new d("symbol-boolean",{fontCharacter:"\\ea8f"}),d.symbolNull=new d("symbol-null",{fontCharacter:"\\ea8f"}),d.symbolNumeric=new d("symbol-numeric",{fontCharacter:"\\ea90"}),d.symbolNumber=new d("symbol-number",{fontCharacter:"\\ea90"}),d.symbolStructure=new d("symbol-structure",{fontCharacter:"\\ea91"}),d.symbolStruct=new d("symbol-struct",{fontCharacter:"\\ea91"}),d.symbolParameter=new d("symbol-parameter",{fontCharacter:"\\ea92"}),d.symbolTypeParameter=new d("symbol-type-parameter",{fontCharacter:"\\ea92"}),d.symbolKey=new d("symbol-key",{fontCharacter:"\\ea93"}),d.symbolText=new d("symbol-text",{fontCharacter:"\\ea93"}),d.symbolReference=new d("symbol-reference",{fontCharacter:"\\ea94"}),d.goToFile=new d("go-to-file",{fontCharacter:"\\ea94"}),d.symbolEnum=new d("symbol-enum",{fontCharacter:"\\ea95"}),d.symbolValue=new d("symbol-value",{fontCharacter:"\\ea95"}),d.symbolRuler=new d("symbol-ruler",{fontCharacter:"\\ea96"}),d.symbolUnit=new d("symbol-unit",{fontCharacter:"\\ea96"}),d.activateBreakpoints=new d("activate-breakpoints",{fontCharacter:"\\ea97"}),d.archive=new d("archive",{fontCharacter:"\\ea98"}),d.arrowBoth=new d("arrow-both",{fontCharacter:"\\ea99"}),d.arrowDown=new d("arrow-down",{fontCharacter:"\\ea9a"}),d.arrowLeft=new d("arrow-left",{fontCharacter:"\\ea9b"}),d.arrowRight=new d("arrow-right",{fontCharacter:"\\ea9c"}),d.arrowSmallDown=new d("arrow-small-down",{fontCharacter:"\\ea9d"}),d.arrowSmallLeft=new d("arrow-small-left",{fontCharacter:"\\ea9e"}),d.arrowSmallRight=new d("arrow-small-right",{fontCharacter:"\\ea9f"}),d.arrowSmallUp=new d("arrow-small-up",{fontCharacter:"\\eaa0"}),d.arrowUp=new d("arrow-up",{fontCharacter:"\\eaa1"}),d.bell=new d("bell",{fontCharacter:"\\eaa2"}),d.bold=new d("bold",{fontCharacter:"\\eaa3"}),d.book=new d("book",{fontCharacter:"\\eaa4"}),d.bookmark=new d("bookmark",{fontCharacter:"\\eaa5"}),d.debugBreakpointConditionalUnverified=new d("debug-breakpoint-conditional-unverified",{fontCharacter:"\\eaa6"}),d.debugBreakpointConditional=new d("debug-breakpoint-conditional",{fontCharacter:"\\eaa7"}),d.debugBreakpointConditionalDisabled=new d("debug-breakpoint-conditional-disabled",{fontCharacter:"\\eaa7"}),d.debugBreakpointDataUnverified=new d("debug-breakpoint-data-unverified",{fontCharacter:"\\eaa8"}),d.debugBreakpointData=new d("debug-breakpoint-data",{fontCharacter:"\\eaa9"}),d.debugBreakpointDataDisabled=new d("debug-breakpoint-data-disabled",{fontCharacter:"\\eaa9"}),d.debugBreakpointLogUnverified=new d("debug-breakpoint-log-unverified",{fontCharacter:"\\eaaa"}),d.debugBreakpointLog=new d("debug-breakpoint-log",{fontCharacter:"\\eaab"}),d.debugBreakpointLogDisabled=new d("debug-breakpoint-log-disabled",{fontCharacter:"\\eaab"}),d.briefcase=new d("briefcase",{fontCharacter:"\\eaac"}),d.broadcast=new d("broadcast",{fontCharacter:"\\eaad"}),d.browser=new d("browser",{fontCharacter:"\\eaae"}),d.bug=new d("bug",{fontCharacter:"\\eaaf"}),d.calendar=new d("calendar",{fontCharacter:"\\eab0"}),d.caseSensitive=new d("case-sensitive",{fontCharacter:"\\eab1"}),d.check=new d("check",{fontCharacter:"\\eab2"}),d.checklist=new d("checklist",{fontCharacter:"\\eab3"}),d.chevronDown=new d("chevron-down",{fontCharacter:"\\eab4"}),d.chevronLeft=new d("chevron-left",{fontCharacter:"\\eab5"}),d.chevronRight=new d("chevron-right",{fontCharacter:"\\eab6"}),d.chevronUp=new d("chevron-up",{fontCharacter:"\\eab7"}),d.chromeClose=new d("chrome-close",{fontCharacter:"\\eab8"}),d.chromeMaximize=new d("chrome-maximize",{fontCharacter:"\\eab9"}),d.chromeMinimize=new d("chrome-minimize",{fontCharacter:"\\eaba"}),d.chromeRestore=new d("chrome-restore",{fontCharacter:"\\eabb"}),d.circleOutline=new d("circle-outline",{fontCharacter:"\\eabc"}),d.debugBreakpointUnverified=new d("debug-breakpoint-unverified",{fontCharacter:"\\eabc"}),d.circleSlash=new d("circle-slash",{fontCharacter:"\\eabd"}),d.circuitBoard=new d("circuit-board",{fontCharacter:"\\eabe"}),d.clearAll=new d("clear-all",{fontCharacter:"\\eabf"}),d.clippy=new d("clippy",{fontCharacter:"\\eac0"}),d.closeAll=new d("close-all",{fontCharacter:"\\eac1"}),d.cloudDownload=new d("cloud-download",{fontCharacter:"\\eac2"}),d.cloudUpload=new d("cloud-upload",{fontCharacter:"\\eac3"}),d.code=new d("code",{fontCharacter:"\\eac4"}),d.collapseAll=new d("collapse-all",{fontCharacter:"\\eac5"}),d.colorMode=new d("color-mode",{fontCharacter:"\\eac6"}),d.commentDiscussion=new d("comment-discussion",{fontCharacter:"\\eac7"}),d.compareChanges=new d("compare-changes",{fontCharacter:"\\eafd"}),d.creditCard=new d("credit-card",{fontCharacter:"\\eac9"}),d.dash=new d("dash",{fontCharacter:"\\eacc"}),d.dashboard=new d("dashboard",{fontCharacter:"\\eacd"}),d.database=new d("database",{fontCharacter:"\\eace"}),d.debugContinue=new d("debug-continue",{fontCharacter:"\\eacf"}),d.debugDisconnect=new d("debug-disconnect",{fontCharacter:"\\ead0"}),d.debugPause=new d("debug-pause",{fontCharacter:"\\ead1"}),d.debugRestart=new d("debug-restart",{fontCharacter:"\\ead2"}),d.debugStart=new d("debug-start",{fontCharacter:"\\ead3"}),d.debugStepInto=new d("debug-step-into",{fontCharacter:"\\ead4"}),d.debugStepOut=new d("debug-step-out",{fontCharacter:"\\ead5"}),d.debugStepOver=new d("debug-step-over",{fontCharacter:"\\ead6"}),d.debugStop=new d("debug-stop",{fontCharacter:"\\ead7"}),d.debug=new d("debug",{fontCharacter:"\\ead8"}),d.deviceCameraVideo=new d("device-camera-video",{fontCharacter:"\\ead9"}),d.deviceCamera=new d("device-camera",{fontCharacter:"\\eada"}),d.deviceMobile=new d("device-mobile",{fontCharacter:"\\eadb"}),d.diffAdded=new d("diff-added",{fontCharacter:"\\eadc"}),d.diffIgnored=new d("diff-ignored",{fontCharacter:"\\eadd"}),d.diffModified=new d("diff-modified",{fontCharacter:"\\eade"}),d.diffRemoved=new d("diff-removed",{fontCharacter:"\\eadf"}),d.diffRenamed=new d("diff-renamed",{fontCharacter:"\\eae0"}),d.diff=new d("diff",{fontCharacter:"\\eae1"}),d.discard=new d("discard",{fontCharacter:"\\eae2"}),d.editorLayout=new d("editor-layout",{fontCharacter:"\\eae3"}),d.emptyWindow=new d("empty-window",{fontCharacter:"\\eae4"}),d.exclude=new d("exclude",{fontCharacter:"\\eae5"}),d.extensions=new d("extensions",{fontCharacter:"\\eae6"}),d.eyeClosed=new d("eye-closed",{fontCharacter:"\\eae7"}),d.fileBinary=new d("file-binary",{fontCharacter:"\\eae8"}),d.fileCode=new d("file-code",{fontCharacter:"\\eae9"}),d.fileMedia=new d("file-media",{fontCharacter:"\\eaea"}),d.filePdf=new d("file-pdf",{fontCharacter:"\\eaeb"}),d.fileSubmodule=new d("file-submodule",{fontCharacter:"\\eaec"}),d.fileSymlinkDirectory=new d("file-symlink-directory",{fontCharacter:"\\eaed"}),d.fileSymlinkFile=new d("file-symlink-file",{fontCharacter:"\\eaee"}),d.fileZip=new d("file-zip",{fontCharacter:"\\eaef"}),d.files=new d("files",{fontCharacter:"\\eaf0"}),d.filter=new d("filter",{fontCharacter:"\\eaf1"}),d.flame=new d("flame",{fontCharacter:"\\eaf2"}),d.foldDown=new d("fold-down",{fontCharacter:"\\eaf3"}),d.foldUp=new d("fold-up",{fontCharacter:"\\eaf4"}),d.fold=new d("fold",{fontCharacter:"\\eaf5"}),d.folderActive=new d("folder-active",{fontCharacter:"\\eaf6"}),d.folderOpened=new d("folder-opened",{fontCharacter:"\\eaf7"}),d.gear=new d("gear",{fontCharacter:"\\eaf8"}),d.gift=new d("gift",{fontCharacter:"\\eaf9"}),d.gistSecret=new d("gist-secret",{fontCharacter:"\\eafa"}),d.gist=new d("gist",{fontCharacter:"\\eafb"}),d.gitCommit=new d("git-commit",{fontCharacter:"\\eafc"}),d.gitCompare=new d("git-compare",{fontCharacter:"\\eafd"}),d.gitMerge=new d("git-merge",{fontCharacter:"\\eafe"}),d.githubAction=new d("github-action",{fontCharacter:"\\eaff"}),d.githubAlt=new d("github-alt",{fontCharacter:"\\eb00"}),d.globe=new d("globe",{fontCharacter:"\\eb01"}),d.grabber=new d("grabber",{fontCharacter:"\\eb02"}),d.graph=new d("graph",{fontCharacter:"\\eb03"}),d.gripper=new d("gripper",{fontCharacter:"\\eb04"}),d.heart=new d("heart",{fontCharacter:"\\eb05"}),d.home=new d("home",{fontCharacter:"\\eb06"}),d.horizontalRule=new d("horizontal-rule",{fontCharacter:"\\eb07"}),d.hubot=new d("hubot",{fontCharacter:"\\eb08"}),d.inbox=new d("inbox",{fontCharacter:"\\eb09"}),d.issueClosed=new d("issue-closed",{fontCharacter:"\\eb0a"}),d.issueReopened=new d("issue-reopened",{fontCharacter:"\\eb0b"}),d.issues=new d("issues",{fontCharacter:"\\eb0c"}),d.italic=new d("italic",{fontCharacter:"\\eb0d"}),d.jersey=new d("jersey",{fontCharacter:"\\eb0e"}),d.json=new d("json",{fontCharacter:"\\eb0f"}),d.kebabVertical=new d("kebab-vertical",{fontCharacter:"\\eb10"}),d.key=new d("key",{fontCharacter:"\\eb11"}),d.law=new d("law",{fontCharacter:"\\eb12"}),d.lightbulbAutofix=new d("lightbulb-autofix",{fontCharacter:"\\eb13"}),d.linkExternal=new d("link-external",{fontCharacter:"\\eb14"}),d.link=new d("link",{fontCharacter:"\\eb15"}),d.listOrdered=new d("list-ordered",{fontCharacter:"\\eb16"}),d.listUnordered=new d("list-unordered",{fontCharacter:"\\eb17"}),d.liveShare=new d("live-share",{fontCharacter:"\\eb18"}),d.loading=new d("loading",{fontCharacter:"\\eb19"}),d.location=new d("location",{fontCharacter:"\\eb1a"}),d.mailRead=new d("mail-read",{fontCharacter:"\\eb1b"}),d.mail=new d("mail",{fontCharacter:"\\eb1c"}),d.markdown=new d("markdown",{fontCharacter:"\\eb1d"}),d.megaphone=new d("megaphone",{fontCharacter:"\\eb1e"}),d.mention=new d("mention",{fontCharacter:"\\eb1f"}),d.milestone=new d("milestone",{fontCharacter:"\\eb20"}),d.mortarBoard=new d("mortar-board",{fontCharacter:"\\eb21"}),d.move=new d("move",{fontCharacter:"\\eb22"}),d.multipleWindows=new d("multiple-windows",{fontCharacter:"\\eb23"}),d.mute=new d("mute",{fontCharacter:"\\eb24"}),d.noNewline=new d("no-newline",{fontCharacter:"\\eb25"}),d.note=new d("note",{fontCharacter:"\\eb26"}),d.octoface=new d("octoface",{fontCharacter:"\\eb27"}),d.openPreview=new d("open-preview",{fontCharacter:"\\eb28"}),d.package_=new d("package",{fontCharacter:"\\eb29"}),d.paintcan=new d("paintcan",{fontCharacter:"\\eb2a"}),d.pin=new d("pin",{fontCharacter:"\\eb2b"}),d.play=new d("play",{fontCharacter:"\\eb2c"}),d.run=new d("run",{fontCharacter:"\\eb2c"}),d.plug=new d("plug",{fontCharacter:"\\eb2d"}),d.preserveCase=new d("preserve-case",{fontCharacter:"\\eb2e"}),d.preview=new d("preview",{fontCharacter:"\\eb2f"}),d.project=new d("project",{fontCharacter:"\\eb30"}),d.pulse=new d("pulse",{fontCharacter:"\\eb31"}),d.question=new d("question",{fontCharacter:"\\eb32"}),d.quote=new d("quote",{fontCharacter:"\\eb33"}),d.radioTower=new d("radio-tower",{fontCharacter:"\\eb34"}),d.reactions=new d("reactions",{fontCharacter:"\\eb35"}),d.references=new d("references",{fontCharacter:"\\eb36"}),d.refresh=new d("refresh",{fontCharacter:"\\eb37"}),d.regex=new d("regex",{fontCharacter:"\\eb38"}),d.remoteExplorer=new d("remote-explorer",{fontCharacter:"\\eb39"}),d.remote=new d("remote",{fontCharacter:"\\eb3a"}),d.remove=new d("remove",{fontCharacter:"\\eb3b"}),d.replaceAll=new d("replace-all",{fontCharacter:"\\eb3c"}),d.replace=new d("replace",{fontCharacter:"\\eb3d"}),d.repoClone=new d("repo-clone",{fontCharacter:"\\eb3e"}),d.repoForcePush=new d("repo-force-push",{fontCharacter:"\\eb3f"}),d.repoPull=new d("repo-pull",{fontCharacter:"\\eb40"}),d.repoPush=new d("repo-push",{fontCharacter:"\\eb41"}),d.report=new d("report",{fontCharacter:"\\eb42"}),d.requestChanges=new d("request-changes",{fontCharacter:"\\eb43"}),d.rocket=new d("rocket",{fontCharacter:"\\eb44"}),d.rootFolderOpened=new d("root-folder-opened",{fontCharacter:"\\eb45"}),d.rootFolder=new d("root-folder",{fontCharacter:"\\eb46"}),d.rss=new d("rss",{fontCharacter:"\\eb47"}),d.ruby=new d("ruby",{fontCharacter:"\\eb48"}),d.saveAll=new d("save-all",{fontCharacter:"\\eb49"}),d.saveAs=new d("save-as",{fontCharacter:"\\eb4a"}),d.save=new d("save",{fontCharacter:"\\eb4b"}),d.screenFull=new d("screen-full",{fontCharacter:"\\eb4c"}),d.screenNormal=new d("screen-normal",{fontCharacter:"\\eb4d"}),d.searchStop=new d("search-stop",{fontCharacter:"\\eb4e"}),d.server=new d("server",{fontCharacter:"\\eb50"}),d.settingsGear=new d("settings-gear",{fontCharacter:"\\eb51"}),d.settings=new d("settings",{fontCharacter:"\\eb52"}),d.shield=new d("shield",{fontCharacter:"\\eb53"}),d.smiley=new d("smiley",{fontCharacter:"\\eb54"}),d.sortPrecedence=new d("sort-precedence",{fontCharacter:"\\eb55"}),d.splitHorizontal=new d("split-horizontal",{fontCharacter:"\\eb56"}),d.splitVertical=new d("split-vertical",{fontCharacter:"\\eb57"}),d.squirrel=new d("squirrel",{fontCharacter:"\\eb58"}),d.starFull=new d("star-full",{fontCharacter:"\\eb59"}),d.starHalf=new d("star-half",{fontCharacter:"\\eb5a"}),d.symbolClass=new d("symbol-class",{fontCharacter:"\\eb5b"}),d.symbolColor=new d("symbol-color",{fontCharacter:"\\eb5c"}),d.symbolConstant=new d("symbol-constant",{fontCharacter:"\\eb5d"}),d.symbolEnumMember=new d("symbol-enum-member",{fontCharacter:"\\eb5e"}),d.symbolField=new d("symbol-field",{fontCharacter:"\\eb5f"}),d.symbolFile=new d("symbol-file",{fontCharacter:"\\eb60"}),d.symbolInterface=new d("symbol-interface",{fontCharacter:"\\eb61"}),d.symbolKeyword=new d("symbol-keyword",{fontCharacter:"\\eb62"}),d.symbolMisc=new d("symbol-misc",{fontCharacter:"\\eb63"}),d.symbolOperator=new d("symbol-operator",{fontCharacter:"\\eb64"}),d.symbolProperty=new d("symbol-property",{fontCharacter:"\\eb65"}),d.wrench=new d("wrench",{fontCharacter:"\\eb65"}),d.wrenchSubaction=new d("wrench-subaction",{fontCharacter:"\\eb65"}),d.symbolSnippet=new d("symbol-snippet",{fontCharacter:"\\eb66"}),d.tasklist=new d("tasklist",{fontCharacter:"\\eb67"}),d.telescope=new d("telescope",{fontCharacter:"\\eb68"}),d.textSize=new d("text-size",{fontCharacter:"\\eb69"}),d.threeBars=new d("three-bars",{fontCharacter:"\\eb6a"}),d.thumbsdown=new d("thumbsdown",{fontCharacter:"\\eb6b"}),d.thumbsup=new d("thumbsup",{fontCharacter:"\\eb6c"}),d.tools=new d("tools",{fontCharacter:"\\eb6d"}),d.triangleDown=new d("triangle-down",{fontCharacter:"\\eb6e"}),d.triangleLeft=new d("triangle-left",{fontCharacter:"\\eb6f"}),d.triangleRight=new d("triangle-right",{fontCharacter:"\\eb70"}),d.triangleUp=new d("triangle-up",{fontCharacter:"\\eb71"}),d.twitter=new d("twitter",{fontCharacter:"\\eb72"}),d.unfold=new d("unfold",{fontCharacter:"\\eb73"}),d.unlock=new d("unlock",{fontCharacter:"\\eb74"}),d.unmute=new d("unmute",{fontCharacter:"\\eb75"}),d.unverified=new d("unverified",{fontCharacter:"\\eb76"}),d.verified=new d("verified",{fontCharacter:"\\eb77"}),d.versions=new d("versions",{fontCharacter:"\\eb78"}),d.vmActive=new d("vm-active",{fontCharacter:"\\eb79"}),d.vmOutline=new d("vm-outline",{fontCharacter:"\\eb7a"}),d.vmRunning=new d("vm-running",{fontCharacter:"\\eb7b"}),d.watch=new d("watch",{fontCharacter:"\\eb7c"}),d.whitespace=new d("whitespace",{fontCharacter:"\\eb7d"}),d.wholeWord=new d("whole-word",{fontCharacter:"\\eb7e"}),d.window=new d("window",{fontCharacter:"\\eb7f"}),d.wordWrap=new d("word-wrap",{fontCharacter:"\\eb80"}),d.zoomIn=new d("zoom-in",{fontCharacter:"\\eb81"}),d.zoomOut=new d("zoom-out",{fontCharacter:"\\eb82"}),d.listFilter=new d("list-filter",{fontCharacter:"\\eb83"}),d.listFlat=new d("list-flat",{fontCharacter:"\\eb84"}),d.listSelection=new d("list-selection",{fontCharacter:"\\eb85"}),d.selection=new d("selection",{fontCharacter:"\\eb85"}),d.listTree=new d("list-tree",{fontCharacter:"\\eb86"}),d.debugBreakpointFunctionUnverified=new d("debug-breakpoint-function-unverified",{fontCharacter:"\\eb87"}),d.debugBreakpointFunction=new d("debug-breakpoint-function",{fontCharacter:"\\eb88"}),d.debugBreakpointFunctionDisabled=new d("debug-breakpoint-function-disabled",{fontCharacter:"\\eb88"}),d.debugStackframeActive=new d("debug-stackframe-active",{fontCharacter:"\\eb89"}),d.debugStackframeDot=new d("debug-stackframe-dot",{fontCharacter:"\\eb8a"}),d.debugStackframe=new d("debug-stackframe",{fontCharacter:"\\eb8b"}),d.debugStackframeFocused=new d("debug-stackframe-focused",{fontCharacter:"\\eb8b"}),d.debugBreakpointUnsupported=new d("debug-breakpoint-unsupported",{fontCharacter:"\\eb8c"}),d.symbolString=new d("symbol-string",{fontCharacter:"\\eb8d"}),d.debugReverseContinue=new d("debug-reverse-continue",{fontCharacter:"\\eb8e"}),d.debugStepBack=new d("debug-step-back",{fontCharacter:"\\eb8f"}),d.debugRestartFrame=new d("debug-restart-frame",{fontCharacter:"\\eb90"}),d.callIncoming=new d("call-incoming",{fontCharacter:"\\eb92"}),d.callOutgoing=new d("call-outgoing",{fontCharacter:"\\eb93"}),d.menu=new d("menu",{fontCharacter:"\\eb94"}),d.expandAll=new d("expand-all",{fontCharacter:"\\eb95"}),d.feedback=new d("feedback",{fontCharacter:"\\eb96"}),d.groupByRefType=new d("group-by-ref-type",{fontCharacter:"\\eb97"}),d.ungroupByRefType=new d("ungroup-by-ref-type",{fontCharacter:"\\eb98"}),d.account=new d("account",{fontCharacter:"\\eb99"}),d.bellDot=new d("bell-dot",{fontCharacter:"\\eb9a"}),d.debugConsole=new d("debug-console",{fontCharacter:"\\eb9b"}),d.library=new d("library",{fontCharacter:"\\eb9c"}),d.output=new d("output",{fontCharacter:"\\eb9d"}),d.runAll=new d("run-all",{fontCharacter:"\\eb9e"}),d.syncIgnored=new d("sync-ignored",{fontCharacter:"\\eb9f"}),d.pinned=new d("pinned",{fontCharacter:"\\eba0"}),d.githubInverted=new d("github-inverted",{fontCharacter:"\\eba1"}),d.debugAlt=new d("debug-alt",{fontCharacter:"\\eb91"}),d.serverProcess=new d("server-process",{fontCharacter:"\\eba2"}),d.serverEnvironment=new d("server-environment",{fontCharacter:"\\eba3"}),d.pass=new d("pass",{fontCharacter:"\\eba4"}),d.stopCircle=new d("stop-circle",{fontCharacter:"\\eba5"}),d.playCircle=new d("play-circle",{fontCharacter:"\\eba6"}),d.record=new d("record",{fontCharacter:"\\eba7"}),d.debugAltSmall=new d("debug-alt-small",{fontCharacter:"\\eba8"}),d.vmConnect=new d("vm-connect",{fontCharacter:"\\eba9"}),d.cloud=new d("cloud",{fontCharacter:"\\ebaa"}),d.merge=new d("merge",{fontCharacter:"\\ebab"}),d.exportIcon=new d("export",{fontCharacter:"\\ebac"}),d.graphLeft=new d("graph-left",{fontCharacter:"\\ebad"}),d.magnet=new d("magnet",{fontCharacter:"\\ebae"}),d.notebook=new d("notebook",{fontCharacter:"\\ebaf"}),d.redo=new d("redo",{fontCharacter:"\\ebb0"}),d.checkAll=new d("check-all",{fontCharacter:"\\ebb1"}),d.pinnedDirty=new d("pinned-dirty",{fontCharacter:"\\ebb2"}),d.passFilled=new d("pass-filled",{fontCharacter:"\\ebb3"}),d.circleLargeFilled=new d("circle-large-filled",{fontCharacter:"\\ebb4"}),d.circleLargeOutline=new d("circle-large-outline",{fontCharacter:"\\ebb5"}),d.combine=new d("combine",{fontCharacter:"\\ebb6"}),d.gather=new d("gather",{fontCharacter:"\\ebb6"}),d.table=new d("table",{fontCharacter:"\\ebb7"}),d.variableGroup=new d("variable-group",{fontCharacter:"\\ebb8"}),d.typeHierarchy=new d("type-hierarchy",{fontCharacter:"\\ebb9"}),d.typeHierarchySub=new d("type-hierarchy-sub",{fontCharacter:"\\ebba"}),d.typeHierarchySuper=new d("type-hierarchy-super",{fontCharacter:"\\ebbb"}),d.gitPullRequestCreate=new d("git-pull-request-create",{fontCharacter:"\\ebbc"}),d.runAbove=new d("run-above",{fontCharacter:"\\ebbd"}),d.runBelow=new d("run-below",{fontCharacter:"\\ebbe"}),d.notebookTemplate=new d("notebook-template",{fontCharacter:"\\ebbf"}),d.debugRerun=new d("debug-rerun",{fontCharacter:"\\ebc0"}),d.dropDownButton=new d("drop-down-button",d.chevronDown.definition)}(S=e.Codicon||(e.Codicon={}))}),define(Q[197],J([0,1,27]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.treeItemLoadingIcon=e.treeFilterClearIcon=e.treeFilterOnTypeOffIcon=e.treeFilterOnTypeOnIcon=e.treeItemExpandedIcon=void 0,e.treeItemExpandedIcon=b.registerCodicon("tree-item-expanded",b.Codicon.chevronDown),e.treeFilterOnTypeOnIcon=b.registerCodicon("tree-filter-on-type-on",b.Codicon.listFilter),e.treeFilterOnTypeOffIcon=b.registerCodicon("tree-filter-on-type-off",b.Codicon.listSelection),e.treeFilterClearIcon=b.registerCodicon("tree-filter-clear",b.Codicon.close),e.treeItemLoadingIcon=b.registerCodicon("tree-item-loading",b.Codicon.loading)}),define(Q[283],J([0,1,15]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.compareByPrefix=e.compareAnything=e.compareFileNames=void 0;const N=new b.IdleValue(()=>{const C=new Intl.Collator(void 0,{numeric:!0,sensitivity:"base"});return{collator:C,collatorIsNumeric:C.resolvedOptions().numeric}});function M(C,d,g=!1){const p=C||"",c=d||"",o=N.value.collator.compare(p,c);return N.value.collatorIsNumeric&&o===0&&p!==c?pc.length)return 1}return 0}e.compareByPrefix=S}),define(Q[121],J([0,1,6,2]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SmoothScrollingOperation=e.SmoothScrollingUpdate=e.Scrollable=e.ScrollState=void 0;class M{constructor(s,a,u,r,i,n){s=s|0,a=a|0,u=u|0,r=r|0,i=i|0,n=n|0,this.rawScrollLeft=u,this.rawScrollTop=n,s<0&&(s=0),u+s>a&&(u=a-s),u<0&&(u=0),r<0&&(r=0),n+r>i&&(n=i-r),n<0&&(n=0),this.width=s,this.scrollWidth=a,this.scrollLeft=u,this.height=r,this.scrollHeight=i,this.scrollTop=n}equals(s){return this.rawScrollLeft===s.rawScrollLeft&&this.rawScrollTop===s.rawScrollTop&&this.width===s.width&&this.scrollWidth===s.scrollWidth&&this.scrollLeft===s.scrollLeft&&this.height===s.height&&this.scrollHeight===s.scrollHeight&&this.scrollTop===s.scrollTop}withScrollDimensions(s,a){return new M(typeof s.width!="undefined"?s.width:this.width,typeof s.scrollWidth!="undefined"?s.scrollWidth:this.scrollWidth,a?this.rawScrollLeft:this.scrollLeft,typeof s.height!="undefined"?s.height:this.height,typeof s.scrollHeight!="undefined"?s.scrollHeight:this.scrollHeight,a?this.rawScrollTop:this.scrollTop)}withScrollPosition(s){return new M(this.width,this.scrollWidth,typeof s.scrollLeft!="undefined"?s.scrollLeft:this.rawScrollLeft,this.height,this.scrollHeight,typeof s.scrollTop!="undefined"?s.scrollTop:this.rawScrollTop)}createScrollEvent(s,a){const u=this.width!==s.width,r=this.scrollWidth!==s.scrollWidth,i=this.scrollLeft!==s.scrollLeft,n=this.height!==s.height,t=this.scrollHeight!==s.scrollHeight,l=this.scrollTop!==s.scrollTop;return{inSmoothScrolling:a,oldWidth:s.width,oldScrollWidth:s.scrollWidth,oldScrollLeft:s.scrollLeft,width:this.width,scrollWidth:this.scrollWidth,scrollLeft:this.scrollLeft,oldHeight:s.height,oldScrollHeight:s.scrollHeight,oldScrollTop:s.scrollTop,height:this.height,scrollHeight:this.scrollHeight,scrollTop:this.scrollTop,widthChanged:u,scrollWidthChanged:r,scrollLeftChanged:i,heightChanged:n,scrollHeightChanged:t,scrollTopChanged:l}}}e.ScrollState=M;class w extends N.Disposable{constructor(s,a){super();this._onScroll=this._register(new b.Emitter),this.onScroll=this._onScroll.event,this._smoothScrollDuration=s,this._scheduleAtNextAnimationFrame=a,this._state=new M(0,0,0,0,0,0),this._smoothScrolling=null}dispose(){this._smoothScrolling&&(this._smoothScrolling.dispose(),this._smoothScrolling=null),super.dispose()}setSmoothScrollDuration(s){this._smoothScrollDuration=s}validateScrollPosition(s){return this._state.withScrollPosition(s)}getScrollDimensions(){return this._state}setScrollDimensions(s,a){const u=this._state.withScrollDimensions(s,a);this._setState(u,Boolean(this._smoothScrolling)),this._smoothScrolling&&this._smoothScrolling.acceptScrollDimensions(this._state)}getFutureScrollPosition(){return this._smoothScrolling?this._smoothScrolling.to:this._state}getCurrentScrollPosition(){return this._state}setScrollPositionNow(s){const a=this._state.withScrollPosition(s);this._smoothScrolling&&(this._smoothScrolling.dispose(),this._smoothScrolling=null),this._setState(a,!1)}setScrollPositionSmooth(s,a){if(this._smoothScrollDuration===0)return this.setScrollPositionNow(s);if(this._smoothScrolling){s={scrollLeft:typeof s.scrollLeft=="undefined"?this._smoothScrolling.to.scrollLeft:s.scrollLeft,scrollTop:typeof s.scrollTop=="undefined"?this._smoothScrolling.to.scrollTop:s.scrollTop};const u=this._state.withScrollPosition(s);if(this._smoothScrolling.to.scrollLeft===u.scrollLeft&&this._smoothScrolling.to.scrollTop===u.scrollTop)return;let r;a?r=new g(this._smoothScrolling.from,u,this._smoothScrolling.startTime,this._smoothScrolling.duration):r=this._smoothScrolling.combine(this._state,u,this._smoothScrollDuration),this._smoothScrolling.dispose(),this._smoothScrolling=r}else{const u=this._state.withScrollPosition(s);this._smoothScrolling=g.start(this._state,u,this._smoothScrollDuration)}this._smoothScrolling.animationFrameDisposable=this._scheduleAtNextAnimationFrame(()=>{!this._smoothScrolling||(this._smoothScrolling.animationFrameDisposable=null,this._performSmoothScrolling())})}_performSmoothScrolling(){if(!!this._smoothScrolling){const s=this._smoothScrolling.tick(),a=this._state.withScrollPosition(s);if(this._setState(a,!0),!!this._smoothScrolling){if(s.isDone){this._smoothScrolling.dispose(),this._smoothScrolling=null;return}this._smoothScrolling.animationFrameDisposable=this._scheduleAtNextAnimationFrame(()=>{!this._smoothScrolling||(this._smoothScrolling.animationFrameDisposable=null,this._performSmoothScrolling())})}}}_setState(s,a){const u=this._state;u.equals(s)||(this._state=s,this._onScroll.fire(this._state.createScrollEvent(u,a)))}}e.Scrollable=w;class S{constructor(s,a,u){this.scrollLeft=s,this.scrollTop=a,this.isDone=u}}e.SmoothScrollingUpdate=S;function C(o,s){const a=s-o;return function(u){return o+a*c(u)}}function d(o,s,a){return function(u){return u2.5*u){let i,n;return s=j.length?te:j[ue]})}e.format=M;function w(G){return G.replace(/[<>&]/g,function(j){switch(j){case"<":return"<";case">":return">";case"&":return"&";default:return j}})}e.escape=w;function S(G){return G.replace(/[\\\{\}\*\+\?\|\^\$\.\[\]\(\)]/g,"\\$&")}e.escapeRegExpCharacters=S;function C(G,j=" "){const te=d(G,j);return g(te,j)}e.trim=C;function d(G,j){if(!G||!j)return G;const te=j.length;if(te===0||G.length===0)return G;let Z=0;for(;G.indexOf(j,Z)===Z;)Z=Z+te;return G.substring(Z)}e.ltrim=d;function g(G,j){if(!G||!j)return G;const te=j.length,Z=G.length;if(te===0||Z===0)return G;let ue=Z,he=-1;for(;he=G.lastIndexOf(j,ue-1),!(he===-1||he+te!==ue);){if(he===0)return"";ue=he}return G.substring(0,ue)}e.rtrim=g;function p(G){return G.replace(/[\-\\\{\}\+\?\|\^\$\.\,\[\]\(\)\#\s]/g,"\\$&").replace(/[\*]/g,".*")}e.convertSimple2RegExpPattern=p;function c(G){return G.replace(/\*/g,"")}e.stripWildcards=c;function o(G,j,te={}){if(!G)throw new Error("Cannot create regex from empty string");j||(G=S(G)),te.wholeWord&&(/\B/.test(G.charAt(0))||(G="\\b"+G),/\B/.test(G.charAt(G.length-1))||(G=G+"\\b"));let Z="";return te.global&&(Z+="g"),te.matchCase||(Z+="i"),te.multiline&&(Z+="m"),te.unicode&&(Z+="u"),new RegExp(G,Z)}e.createRegExp=o;function s(G){return G.source==="^"||G.source==="^$"||G.source==="$"||G.source==="^\\s*$"?!1:!!(G.exec("")&&G.lastIndex===0)}e.regExpLeadsToEndlessLoop=s;function a(G){return(G.global?"g":"")+(G.ignoreCase?"i":"")+(G.multiline?"m":"")+(G.unicode?"u":"")}e.regExpFlags=a;function u(G){return G.split(/\r\n|\r|\n/)}e.splitLines=u;function r(G){for(let j=0,te=G.length;j=0;te--){const Z=G.charCodeAt(te);if(Z!==32&&Z!==9)return te}return-1}e.lastNonWhitespaceIndex=n;function t(G,j){return Gj?1:0}e.compare=t;function l(G,j,te=0,Z=G.length,ue=0,he=j.length){for(;teCe)return 1}const re=Z-te,ce=he-ue;return rece?1:0}e.compareSubstring=l;function h(G,j){return m(G,j,0,G.length,0,j.length)}e.compareIgnoreCase=h;function m(G,j,te=0,Z=G.length,ue=0,he=j.length){for(;tece?1:0}e.compareSubstringIgnoreCase=m;function _(G){return G>=97&&G<=122}e.isLowerAsciiLetter=_;function f(G){return G>=65&&G<=90}e.isUpperAsciiLetter=f;function v(G){return _(G)||f(G)}function y(G,j){return G.length===j.length&&L(G,j)}e.equalsIgnoreCase=y;function L(G,j,te=G.length){for(let Z=0;ZG.length?!1:L(G,j,te)}e.startsWithIgnoreCase=I;function k(G,j){let te,Z=Math.min(G.length,j.length);for(te=0;te1){const Z=G.charCodeAt(j-2);if(T(Z))return A(Z,te)}return te}function D(G,j){const te=oe.getInstance(),Z=j,ue=G.length,he=B(G,ue,j);j+=he>=65536?2:1;let re=te.getGraphemeBreakType(he);for(;j=65536?2:1,re=me}return j-Z}e.nextCharLength=D;function R(G,j){const te=oe.getInstance(),Z=j,ue=F(G,j);j-=ue>=65536?2:1;let he=te.getGraphemeBreakType(ue);for(;j>0;){const re=F(G,j),ce=te.getGraphemeBreakType(re);if(ie(ce,he))break;j-=re>=65536?2:1,he=ce}return Z-j}e.prevCharLength=R;function W(G){const j=G.byteLength,te=[];let Z=0;for(;Z=240&&Z+3>>0|(G[Z++]&63)<<12>>>0|(G[Z++]&63)<<6>>>0|(G[Z++]&63)<<0>>>0:ue>=224&&Z+2>>0|(G[Z++]&63)<<6>>>0|(G[Z++]&63)<<0>>>0:ue>=192&&Z+1>>0|(G[Z++]&63)<<0>>>0:he=G[Z++],he>=0&&he<=55295||he>=57344&&he<=65535)te.push(String.fromCharCode(he));else if(he>=65536&&he<=1114111){const re=he-65536,ce=55296+((re&1047552)>>>10),me=56320+((re&1023)>>>0);te.push(String.fromCharCode(ce)),te.push(String.fromCharCode(me))}else te.push(String.fromCharCode(65533))}return te.join("")}e.decodeUTF8=W;const x=/(?:[\u05BE\u05C0\u05C3\u05C6\u05D0-\u05F4\u0608\u060B\u060D\u061B-\u064A\u066D-\u066F\u0671-\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u0710\u0712-\u072F\u074D-\u07A5\u07B1-\u07EA\u07F4\u07F5\u07FA-\u0815\u081A\u0824\u0828\u0830-\u0858\u085E-\u08BD\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFD3D\uFD50-\uFDFC\uFE70-\uFEFC]|\uD802[\uDC00-\uDD1B\uDD20-\uDE00\uDE10-\uDE33\uDE40-\uDEE4\uDEEB-\uDF35\uDF40-\uDFFF]|\uD803[\uDC00-\uDCFF]|\uD83A[\uDC00-\uDCCF\uDD00-\uDD43\uDD50-\uDFFF]|\uD83B[\uDC00-\uDEBB])/;function K(G){return x.test(G)}e.containsRTL=K;const Y=/(?:[\u231A\u231B\u23F0\u23F3\u2600-\u27BF\u2B50\u2B55]|\uD83C[\uDDE6-\uDDFF\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F\uDE80-\uDEFC\uDFE0-\uDFEB]|\uD83E[\uDD00-\uDDFF\uDE70-\uDED6])/;function ee(G){return Y.test(G)}e.containsEmoji=ee;const se=/^[\t\n\r\x20-\x7E]*$/;function ne(G){return se.test(G)}e.isBasicASCII=ne,e.UNUSUAL_LINE_TERMINATORS=/[\u2028\u2029]/;function le(G){return e.UNUSUAL_LINE_TERMINATORS.test(G)}e.containsUnusualLineTerminators=le;function X(G){for(let j=0,te=G.length;j=11904&&G<=55215||G>=63744&&G<=64255||G>=65281&&G<=65374}e.isFullWidthCharacter=z;function P(G){return G>=127462&&G<=127487||G===8986||G===8987||G===9200||G===9203||G>=9728&&G<=10175||G===11088||G===11093||G>=127744&&G<=128591||G>=128640&&G<=128764||G>=128992&&G<=129003||G>=129280&&G<=129535||G>=129648&&G<=129750}e.isEmojiImprecise=P,e.UTF8_BOM_CHARACTER=String.fromCharCode(65279);function V(G){return!!(G&&G.length>0&&G.charCodeAt(0)===65279)}e.startsWithUTF8BOM=V;function U(G,j=!1){return G?(j&&(G=G.replace(/\\./g,"")),G.toLowerCase()!==G):!1}e.containsUppercaseCharacter=U;function H(G){const j=90-65+1;return G=G%(2*j),Gte[3*ue+1])ue=2*ue+1;else return te[3*ue+2];return 0}}oe._INSTANCE=null;function ae(){return JSON.parse("[0,0,0,51592,51592,11,44424,44424,11,72251,72254,5,7150,7150,7,48008,48008,11,55176,55176,11,128420,128420,14,3276,3277,5,9979,9980,14,46216,46216,11,49800,49800,11,53384,53384,11,70726,70726,5,122915,122916,5,129320,129327,14,2558,2558,5,5906,5908,5,9762,9763,14,43360,43388,8,45320,45320,11,47112,47112,11,48904,48904,11,50696,50696,11,52488,52488,11,54280,54280,11,70082,70083,1,71350,71350,7,73111,73111,5,127892,127893,14,128726,128727,14,129473,129474,14,2027,2035,5,2901,2902,5,3784,3789,5,6754,6754,5,8418,8420,5,9877,9877,14,11088,11088,14,44008,44008,5,44872,44872,11,45768,45768,11,46664,46664,11,47560,47560,11,48456,48456,11,49352,49352,11,50248,50248,11,51144,51144,11,52040,52040,11,52936,52936,11,53832,53832,11,54728,54728,11,69811,69814,5,70459,70460,5,71096,71099,7,71998,71998,5,72874,72880,5,119149,119149,7,127374,127374,14,128335,128335,14,128482,128482,14,128765,128767,14,129399,129400,14,129680,129685,14,1476,1477,5,2377,2380,7,2759,2760,5,3137,3140,7,3458,3459,7,4153,4154,5,6432,6434,5,6978,6978,5,7675,7679,5,9723,9726,14,9823,9823,14,9919,9923,14,10035,10036,14,42736,42737,5,43596,43596,5,44200,44200,11,44648,44648,11,45096,45096,11,45544,45544,11,45992,45992,11,46440,46440,11,46888,46888,11,47336,47336,11,47784,47784,11,48232,48232,11,48680,48680,11,49128,49128,11,49576,49576,11,50024,50024,11,50472,50472,11,50920,50920,11,51368,51368,11,51816,51816,11,52264,52264,11,52712,52712,11,53160,53160,11,53608,53608,11,54056,54056,11,54504,54504,11,54952,54952,11,68108,68111,5,69933,69940,5,70197,70197,7,70498,70499,7,70845,70845,5,71229,71229,5,71727,71735,5,72154,72155,5,72344,72345,5,73023,73029,5,94095,94098,5,121403,121452,5,126981,127182,14,127538,127546,14,127990,127990,14,128391,128391,14,128445,128449,14,128500,128505,14,128752,128752,14,129160,129167,14,129356,129356,14,129432,129442,14,129648,129651,14,129751,131069,14,173,173,4,1757,1757,1,2274,2274,1,2494,2494,5,2641,2641,5,2876,2876,5,3014,3016,7,3262,3262,7,3393,3396,5,3570,3571,7,3968,3972,5,4228,4228,7,6086,6086,5,6679,6680,5,6912,6915,5,7080,7081,5,7380,7392,5,8252,8252,14,9096,9096,14,9748,9749,14,9784,9786,14,9833,9850,14,9890,9894,14,9938,9938,14,9999,9999,14,10085,10087,14,12349,12349,14,43136,43137,7,43454,43456,7,43755,43755,7,44088,44088,11,44312,44312,11,44536,44536,11,44760,44760,11,44984,44984,11,45208,45208,11,45432,45432,11,45656,45656,11,45880,45880,11,46104,46104,11,46328,46328,11,46552,46552,11,46776,46776,11,47000,47000,11,47224,47224,11,47448,47448,11,47672,47672,11,47896,47896,11,48120,48120,11,48344,48344,11,48568,48568,11,48792,48792,11,49016,49016,11,49240,49240,11,49464,49464,11,49688,49688,11,49912,49912,11,50136,50136,11,50360,50360,11,50584,50584,11,50808,50808,11,51032,51032,11,51256,51256,11,51480,51480,11,51704,51704,11,51928,51928,11,52152,52152,11,52376,52376,11,52600,52600,11,52824,52824,11,53048,53048,11,53272,53272,11,53496,53496,11,53720,53720,11,53944,53944,11,54168,54168,11,54392,54392,11,54616,54616,11,54840,54840,11,55064,55064,11,65438,65439,5,69633,69633,5,69837,69837,1,70018,70018,7,70188,70190,7,70368,70370,7,70465,70468,7,70712,70719,5,70835,70840,5,70850,70851,5,71132,71133,5,71340,71340,7,71458,71461,5,71985,71989,7,72002,72002,7,72193,72202,5,72281,72283,5,72766,72766,7,72885,72886,5,73104,73105,5,92912,92916,5,113824,113827,4,119173,119179,5,121505,121519,5,125136,125142,5,127279,127279,14,127489,127490,14,127570,127743,14,127900,127901,14,128254,128254,14,128369,128370,14,128400,128400,14,128425,128432,14,128468,128475,14,128489,128494,14,128715,128720,14,128745,128745,14,128759,128760,14,129004,129023,14,129296,129304,14,129340,129342,14,129388,129392,14,129404,129407,14,129454,129455,14,129485,129487,14,129659,129663,14,129719,129727,14,917536,917631,5,13,13,2,1160,1161,5,1564,1564,4,1807,1807,1,2085,2087,5,2363,2363,7,2402,2403,5,2507,2508,7,2622,2624,7,2691,2691,7,2786,2787,5,2881,2884,5,3006,3006,5,3072,3072,5,3170,3171,5,3267,3268,7,3330,3331,7,3406,3406,1,3538,3540,5,3655,3662,5,3897,3897,5,4038,4038,5,4184,4185,5,4352,4447,8,6068,6069,5,6155,6157,5,6448,6449,7,6742,6742,5,6783,6783,5,6966,6970,5,7042,7042,7,7143,7143,7,7212,7219,5,7412,7412,5,8206,8207,4,8294,8303,4,8596,8601,14,9410,9410,14,9742,9742,14,9757,9757,14,9770,9770,14,9794,9794,14,9828,9828,14,9855,9855,14,9882,9882,14,9900,9903,14,9929,9933,14,9963,9967,14,9987,9988,14,10006,10006,14,10062,10062,14,10175,10175,14,11744,11775,5,42607,42607,5,43043,43044,7,43263,43263,5,43444,43445,7,43569,43570,5,43698,43700,5,43766,43766,5,44032,44032,11,44144,44144,11,44256,44256,11,44368,44368,11,44480,44480,11,44592,44592,11,44704,44704,11,44816,44816,11,44928,44928,11,45040,45040,11,45152,45152,11,45264,45264,11,45376,45376,11,45488,45488,11,45600,45600,11,45712,45712,11,45824,45824,11,45936,45936,11,46048,46048,11,46160,46160,11,46272,46272,11,46384,46384,11,46496,46496,11,46608,46608,11,46720,46720,11,46832,46832,11,46944,46944,11,47056,47056,11,47168,47168,11,47280,47280,11,47392,47392,11,47504,47504,11,47616,47616,11,47728,47728,11,47840,47840,11,47952,47952,11,48064,48064,11,48176,48176,11,48288,48288,11,48400,48400,11,48512,48512,11,48624,48624,11,48736,48736,11,48848,48848,11,48960,48960,11,49072,49072,11,49184,49184,11,49296,49296,11,49408,49408,11,49520,49520,11,49632,49632,11,49744,49744,11,49856,49856,11,49968,49968,11,50080,50080,11,50192,50192,11,50304,50304,11,50416,50416,11,50528,50528,11,50640,50640,11,50752,50752,11,50864,50864,11,50976,50976,11,51088,51088,11,51200,51200,11,51312,51312,11,51424,51424,11,51536,51536,11,51648,51648,11,51760,51760,11,51872,51872,11,51984,51984,11,52096,52096,11,52208,52208,11,52320,52320,11,52432,52432,11,52544,52544,11,52656,52656,11,52768,52768,11,52880,52880,11,52992,52992,11,53104,53104,11,53216,53216,11,53328,53328,11,53440,53440,11,53552,53552,11,53664,53664,11,53776,53776,11,53888,53888,11,54000,54000,11,54112,54112,11,54224,54224,11,54336,54336,11,54448,54448,11,54560,54560,11,54672,54672,11,54784,54784,11,54896,54896,11,55008,55008,11,55120,55120,11,64286,64286,5,66272,66272,5,68900,68903,5,69762,69762,7,69817,69818,5,69927,69931,5,70003,70003,5,70070,70078,5,70094,70094,7,70194,70195,7,70206,70206,5,70400,70401,5,70463,70463,7,70475,70477,7,70512,70516,5,70722,70724,5,70832,70832,5,70842,70842,5,70847,70848,5,71088,71089,7,71102,71102,7,71219,71226,5,71231,71232,5,71342,71343,7,71453,71455,5,71463,71467,5,71737,71738,5,71995,71996,5,72000,72000,7,72145,72147,7,72160,72160,5,72249,72249,7,72273,72278,5,72330,72342,5,72752,72758,5,72850,72871,5,72882,72883,5,73018,73018,5,73031,73031,5,73109,73109,5,73461,73462,7,94031,94031,5,94192,94193,7,119142,119142,7,119155,119162,4,119362,119364,5,121476,121476,5,122888,122904,5,123184,123190,5,126976,126979,14,127184,127231,14,127344,127345,14,127405,127461,14,127514,127514,14,127561,127567,14,127778,127779,14,127896,127896,14,127985,127986,14,127995,127999,5,128326,128328,14,128360,128366,14,128378,128378,14,128394,128397,14,128405,128406,14,128422,128423,14,128435,128443,14,128453,128464,14,128479,128480,14,128484,128487,14,128496,128498,14,128640,128709,14,128723,128724,14,128736,128741,14,128747,128748,14,128755,128755,14,128762,128762,14,128981,128991,14,129096,129103,14,129292,129292,14,129311,129311,14,129329,129330,14,129344,129349,14,129360,129374,14,129394,129394,14,129402,129402,14,129413,129425,14,129445,129450,14,129466,129471,14,129483,129483,14,129511,129535,14,129653,129655,14,129667,129670,14,129705,129711,14,129731,129743,14,917505,917505,4,917760,917999,5,10,10,3,127,159,4,768,879,5,1471,1471,5,1536,1541,1,1648,1648,5,1767,1768,5,1840,1866,5,2070,2073,5,2137,2139,5,2307,2307,7,2366,2368,7,2382,2383,7,2434,2435,7,2497,2500,5,2519,2519,5,2563,2563,7,2631,2632,5,2677,2677,5,2750,2752,7,2763,2764,7,2817,2817,5,2879,2879,5,2891,2892,7,2914,2915,5,3008,3008,5,3021,3021,5,3076,3076,5,3146,3149,5,3202,3203,7,3264,3265,7,3271,3272,7,3298,3299,5,3390,3390,5,3402,3404,7,3426,3427,5,3535,3535,5,3544,3550,7,3635,3635,7,3763,3763,7,3893,3893,5,3953,3966,5,3981,3991,5,4145,4145,7,4157,4158,5,4209,4212,5,4237,4237,5,4520,4607,10,5970,5971,5,6071,6077,5,6089,6099,5,6277,6278,5,6439,6440,5,6451,6456,7,6683,6683,5,6744,6750,5,6765,6770,7,6846,6846,5,6964,6964,5,6972,6972,5,7019,7027,5,7074,7077,5,7083,7085,5,7146,7148,7,7154,7155,7,7222,7223,5,7394,7400,5,7416,7417,5,8204,8204,5,8233,8233,4,8288,8292,4,8413,8416,5,8482,8482,14,8986,8987,14,9193,9203,14,9654,9654,14,9733,9733,14,9745,9745,14,9752,9752,14,9760,9760,14,9766,9766,14,9774,9775,14,9792,9792,14,9800,9811,14,9825,9826,14,9831,9831,14,9852,9853,14,9872,9873,14,9880,9880,14,9885,9887,14,9896,9897,14,9906,9916,14,9926,9927,14,9936,9936,14,9941,9960,14,9974,9974,14,9982,9985,14,9992,9997,14,10002,10002,14,10017,10017,14,10055,10055,14,10071,10071,14,10145,10145,14,11013,11015,14,11503,11505,5,12334,12335,5,12951,12951,14,42612,42621,5,43014,43014,5,43047,43047,7,43204,43205,5,43335,43345,5,43395,43395,7,43450,43451,7,43561,43566,5,43573,43574,5,43644,43644,5,43710,43711,5,43758,43759,7,44005,44005,5,44012,44012,7,44060,44060,11,44116,44116,11,44172,44172,11,44228,44228,11,44284,44284,11,44340,44340,11,44396,44396,11,44452,44452,11,44508,44508,11,44564,44564,11,44620,44620,11,44676,44676,11,44732,44732,11,44788,44788,11,44844,44844,11,44900,44900,11,44956,44956,11,45012,45012,11,45068,45068,11,45124,45124,11,45180,45180,11,45236,45236,11,45292,45292,11,45348,45348,11,45404,45404,11,45460,45460,11,45516,45516,11,45572,45572,11,45628,45628,11,45684,45684,11,45740,45740,11,45796,45796,11,45852,45852,11,45908,45908,11,45964,45964,11,46020,46020,11,46076,46076,11,46132,46132,11,46188,46188,11,46244,46244,11,46300,46300,11,46356,46356,11,46412,46412,11,46468,46468,11,46524,46524,11,46580,46580,11,46636,46636,11,46692,46692,11,46748,46748,11,46804,46804,11,46860,46860,11,46916,46916,11,46972,46972,11,47028,47028,11,47084,47084,11,47140,47140,11,47196,47196,11,47252,47252,11,47308,47308,11,47364,47364,11,47420,47420,11,47476,47476,11,47532,47532,11,47588,47588,11,47644,47644,11,47700,47700,11,47756,47756,11,47812,47812,11,47868,47868,11,47924,47924,11,47980,47980,11,48036,48036,11,48092,48092,11,48148,48148,11,48204,48204,11,48260,48260,11,48316,48316,11,48372,48372,11,48428,48428,11,48484,48484,11,48540,48540,11,48596,48596,11,48652,48652,11,48708,48708,11,48764,48764,11,48820,48820,11,48876,48876,11,48932,48932,11,48988,48988,11,49044,49044,11,49100,49100,11,49156,49156,11,49212,49212,11,49268,49268,11,49324,49324,11,49380,49380,11,49436,49436,11,49492,49492,11,49548,49548,11,49604,49604,11,49660,49660,11,49716,49716,11,49772,49772,11,49828,49828,11,49884,49884,11,49940,49940,11,49996,49996,11,50052,50052,11,50108,50108,11,50164,50164,11,50220,50220,11,50276,50276,11,50332,50332,11,50388,50388,11,50444,50444,11,50500,50500,11,50556,50556,11,50612,50612,11,50668,50668,11,50724,50724,11,50780,50780,11,50836,50836,11,50892,50892,11,50948,50948,11,51004,51004,11,51060,51060,11,51116,51116,11,51172,51172,11,51228,51228,11,51284,51284,11,51340,51340,11,51396,51396,11,51452,51452,11,51508,51508,11,51564,51564,11,51620,51620,11,51676,51676,11,51732,51732,11,51788,51788,11,51844,51844,11,51900,51900,11,51956,51956,11,52012,52012,11,52068,52068,11,52124,52124,11,52180,52180,11,52236,52236,11,52292,52292,11,52348,52348,11,52404,52404,11,52460,52460,11,52516,52516,11,52572,52572,11,52628,52628,11,52684,52684,11,52740,52740,11,52796,52796,11,52852,52852,11,52908,52908,11,52964,52964,11,53020,53020,11,53076,53076,11,53132,53132,11,53188,53188,11,53244,53244,11,53300,53300,11,53356,53356,11,53412,53412,11,53468,53468,11,53524,53524,11,53580,53580,11,53636,53636,11,53692,53692,11,53748,53748,11,53804,53804,11,53860,53860,11,53916,53916,11,53972,53972,11,54028,54028,11,54084,54084,11,54140,54140,11,54196,54196,11,54252,54252,11,54308,54308,11,54364,54364,11,54420,54420,11,54476,54476,11,54532,54532,11,54588,54588,11,54644,54644,11,54700,54700,11,54756,54756,11,54812,54812,11,54868,54868,11,54924,54924,11,54980,54980,11,55036,55036,11,55092,55092,11,55148,55148,11,55216,55238,9,65056,65071,5,65529,65531,4,68097,68099,5,68159,68159,5,69446,69456,5,69688,69702,5,69808,69810,7,69815,69816,7,69821,69821,1,69888,69890,5,69932,69932,7,69957,69958,7,70016,70017,5,70067,70069,7,70079,70080,7,70089,70092,5,70095,70095,5,70191,70193,5,70196,70196,5,70198,70199,5,70367,70367,5,70371,70378,5,70402,70403,7,70462,70462,5,70464,70464,5,70471,70472,7,70487,70487,5,70502,70508,5,70709,70711,7,70720,70721,7,70725,70725,7,70750,70750,5,70833,70834,7,70841,70841,7,70843,70844,7,70846,70846,7,70849,70849,7,71087,71087,5,71090,71093,5,71100,71101,5,71103,71104,5,71216,71218,7,71227,71228,7,71230,71230,7,71339,71339,5,71341,71341,5,71344,71349,5,71351,71351,5,71456,71457,7,71462,71462,7,71724,71726,7,71736,71736,7,71984,71984,5,71991,71992,7,71997,71997,7,71999,71999,1,72001,72001,1,72003,72003,5,72148,72151,5,72156,72159,7,72164,72164,7,72243,72248,5,72250,72250,1,72263,72263,5,72279,72280,7,72324,72329,1,72343,72343,7,72751,72751,7,72760,72765,5,72767,72767,5,72873,72873,7,72881,72881,7,72884,72884,7,73009,73014,5,73020,73021,5,73030,73030,1,73098,73102,7,73107,73108,7,73110,73110,7,73459,73460,5,78896,78904,4,92976,92982,5,94033,94087,7,94180,94180,5,113821,113822,5,119141,119141,5,119143,119145,5,119150,119154,5,119163,119170,5,119210,119213,5,121344,121398,5,121461,121461,5,121499,121503,5,122880,122886,5,122907,122913,5,122918,122922,5,123628,123631,5,125252,125258,5,126980,126980,14,127183,127183,14,127245,127247,14,127340,127343,14,127358,127359,14,127377,127386,14,127462,127487,6,127491,127503,14,127535,127535,14,127548,127551,14,127568,127569,14,127744,127777,14,127780,127891,14,127894,127895,14,127897,127899,14,127902,127984,14,127987,127989,14,127991,127994,14,128000,128253,14,128255,128317,14,128329,128334,14,128336,128359,14,128367,128368,14,128371,128377,14,128379,128390,14,128392,128393,14,128398,128399,14,128401,128404,14,128407,128419,14,128421,128421,14,128424,128424,14,128433,128434,14,128444,128444,14,128450,128452,14,128465,128467,14,128476,128478,14,128481,128481,14,128483,128483,14,128488,128488,14,128495,128495,14,128499,128499,14,128506,128591,14,128710,128714,14,128721,128722,14,128725,128725,14,128728,128735,14,128742,128744,14,128746,128746,14,128749,128751,14,128753,128754,14,128756,128758,14,128761,128761,14,128763,128764,14,128884,128895,14,128992,129003,14,129036,129039,14,129114,129119,14,129198,129279,14,129293,129295,14,129305,129310,14,129312,129319,14,129328,129328,14,129331,129338,14,129343,129343,14,129351,129355,14,129357,129359,14,129375,129387,14,129393,129393,14,129395,129398,14,129401,129401,14,129403,129403,14,129408,129412,14,129426,129431,14,129443,129444,14,129451,129453,14,129456,129465,14,129472,129472,14,129475,129482,14,129484,129484,14,129488,129510,14,129536,129647,14,129652,129652,14,129656,129658,14,129664,129666,14,129671,129679,14,129686,129704,14,129712,129718,14,129728,129730,14,129744,129750,14,917504,917504,4,917506,917535,4,917632,917759,4,918000,921599,4,0,9,4,11,12,4,14,31,4,169,169,14,174,174,14,1155,1159,5,1425,1469,5,1473,1474,5,1479,1479,5,1552,1562,5,1611,1631,5,1750,1756,5,1759,1764,5,1770,1773,5,1809,1809,5,1958,1968,5,2045,2045,5,2075,2083,5,2089,2093,5,2259,2273,5,2275,2306,5,2362,2362,5,2364,2364,5,2369,2376,5,2381,2381,5,2385,2391,5,2433,2433,5,2492,2492,5,2495,2496,7,2503,2504,7,2509,2509,5,2530,2531,5,2561,2562,5,2620,2620,5,2625,2626,5,2635,2637,5,2672,2673,5,2689,2690,5,2748,2748,5,2753,2757,5,2761,2761,7,2765,2765,5,2810,2815,5,2818,2819,7,2878,2878,5,2880,2880,7,2887,2888,7,2893,2893,5,2903,2903,5,2946,2946,5,3007,3007,7,3009,3010,7,3018,3020,7,3031,3031,5,3073,3075,7,3134,3136,5,3142,3144,5,3157,3158,5,3201,3201,5,3260,3260,5,3263,3263,5,3266,3266,5,3270,3270,5,3274,3275,7,3285,3286,5,3328,3329,5,3387,3388,5,3391,3392,7,3398,3400,7,3405,3405,5,3415,3415,5,3457,3457,5,3530,3530,5,3536,3537,7,3542,3542,5,3551,3551,5,3633,3633,5,3636,3642,5,3761,3761,5,3764,3772,5,3864,3865,5,3895,3895,5,3902,3903,7,3967,3967,7,3974,3975,5,3993,4028,5,4141,4144,5,4146,4151,5,4155,4156,7,4182,4183,7,4190,4192,5,4226,4226,5,4229,4230,5,4253,4253,5,4448,4519,9,4957,4959,5,5938,5940,5,6002,6003,5,6070,6070,7,6078,6085,7,6087,6088,7,6109,6109,5,6158,6158,4,6313,6313,5,6435,6438,7,6441,6443,7,6450,6450,5,6457,6459,5,6681,6682,7,6741,6741,7,6743,6743,7,6752,6752,5,6757,6764,5,6771,6780,5,6832,6845,5,6847,6848,5,6916,6916,7,6965,6965,5,6971,6971,7,6973,6977,7,6979,6980,7,7040,7041,5,7073,7073,7,7078,7079,7,7082,7082,7,7142,7142,5,7144,7145,5,7149,7149,5,7151,7153,5,7204,7211,7,7220,7221,7,7376,7378,5,7393,7393,7,7405,7405,5,7415,7415,7,7616,7673,5,8203,8203,4,8205,8205,13,8232,8232,4,8234,8238,4,8265,8265,14,8293,8293,4,8400,8412,5,8417,8417,5,8421,8432,5,8505,8505,14,8617,8618,14,9000,9000,14,9167,9167,14,9208,9210,14,9642,9643,14,9664,9664,14,9728,9732,14,9735,9741,14,9743,9744,14,9746,9746,14,9750,9751,14,9753,9756,14,9758,9759,14,9761,9761,14,9764,9765,14,9767,9769,14,9771,9773,14,9776,9783,14,9787,9791,14,9793,9793,14,9795,9799,14,9812,9822,14,9824,9824,14,9827,9827,14,9829,9830,14,9832,9832,14,9851,9851,14,9854,9854,14,9856,9861,14,9874,9876,14,9878,9879,14,9881,9881,14,9883,9884,14,9888,9889,14,9895,9895,14,9898,9899,14,9904,9905,14,9917,9918,14,9924,9925,14,9928,9928,14,9934,9935,14,9937,9937,14,9939,9940,14,9961,9962,14,9968,9973,14,9975,9978,14,9981,9981,14,9986,9986,14,9989,9989,14,9998,9998,14,10000,10001,14,10004,10004,14,10013,10013,14,10024,10024,14,10052,10052,14,10060,10060,14,10067,10069,14,10083,10084,14,10133,10135,14,10160,10160,14,10548,10549,14,11035,11036,14,11093,11093,14,11647,11647,5,12330,12333,5,12336,12336,14,12441,12442,5,12953,12953,14,42608,42610,5,42654,42655,5,43010,43010,5,43019,43019,5,43045,43046,5,43052,43052,5,43188,43203,7,43232,43249,5,43302,43309,5,43346,43347,7,43392,43394,5,43443,43443,5,43446,43449,5,43452,43453,5,43493,43493,5,43567,43568,7,43571,43572,7,43587,43587,5,43597,43597,7,43696,43696,5,43703,43704,5,43713,43713,5,43756,43757,5,43765,43765,7,44003,44004,7,44006,44007,7,44009,44010,7,44013,44013,5,44033,44059,12,44061,44087,12,44089,44115,12,44117,44143,12,44145,44171,12,44173,44199,12,44201,44227,12,44229,44255,12,44257,44283,12,44285,44311,12,44313,44339,12,44341,44367,12,44369,44395,12,44397,44423,12,44425,44451,12,44453,44479,12,44481,44507,12,44509,44535,12,44537,44563,12,44565,44591,12,44593,44619,12,44621,44647,12,44649,44675,12,44677,44703,12,44705,44731,12,44733,44759,12,44761,44787,12,44789,44815,12,44817,44843,12,44845,44871,12,44873,44899,12,44901,44927,12,44929,44955,12,44957,44983,12,44985,45011,12,45013,45039,12,45041,45067,12,45069,45095,12,45097,45123,12,45125,45151,12,45153,45179,12,45181,45207,12,45209,45235,12,45237,45263,12,45265,45291,12,45293,45319,12,45321,45347,12,45349,45375,12,45377,45403,12,45405,45431,12,45433,45459,12,45461,45487,12,45489,45515,12,45517,45543,12,45545,45571,12,45573,45599,12,45601,45627,12,45629,45655,12,45657,45683,12,45685,45711,12,45713,45739,12,45741,45767,12,45769,45795,12,45797,45823,12,45825,45851,12,45853,45879,12,45881,45907,12,45909,45935,12,45937,45963,12,45965,45991,12,45993,46019,12,46021,46047,12,46049,46075,12,46077,46103,12,46105,46131,12,46133,46159,12,46161,46187,12,46189,46215,12,46217,46243,12,46245,46271,12,46273,46299,12,46301,46327,12,46329,46355,12,46357,46383,12,46385,46411,12,46413,46439,12,46441,46467,12,46469,46495,12,46497,46523,12,46525,46551,12,46553,46579,12,46581,46607,12,46609,46635,12,46637,46663,12,46665,46691,12,46693,46719,12,46721,46747,12,46749,46775,12,46777,46803,12,46805,46831,12,46833,46859,12,46861,46887,12,46889,46915,12,46917,46943,12,46945,46971,12,46973,46999,12,47001,47027,12,47029,47055,12,47057,47083,12,47085,47111,12,47113,47139,12,47141,47167,12,47169,47195,12,47197,47223,12,47225,47251,12,47253,47279,12,47281,47307,12,47309,47335,12,47337,47363,12,47365,47391,12,47393,47419,12,47421,47447,12,47449,47475,12,47477,47503,12,47505,47531,12,47533,47559,12,47561,47587,12,47589,47615,12,47617,47643,12,47645,47671,12,47673,47699,12,47701,47727,12,47729,47755,12,47757,47783,12,47785,47811,12,47813,47839,12,47841,47867,12,47869,47895,12,47897,47923,12,47925,47951,12,47953,47979,12,47981,48007,12,48009,48035,12,48037,48063,12,48065,48091,12,48093,48119,12,48121,48147,12,48149,48175,12,48177,48203,12,48205,48231,12,48233,48259,12,48261,48287,12,48289,48315,12,48317,48343,12,48345,48371,12,48373,48399,12,48401,48427,12,48429,48455,12,48457,48483,12,48485,48511,12,48513,48539,12,48541,48567,12,48569,48595,12,48597,48623,12,48625,48651,12,48653,48679,12,48681,48707,12,48709,48735,12,48737,48763,12,48765,48791,12,48793,48819,12,48821,48847,12,48849,48875,12,48877,48903,12,48905,48931,12,48933,48959,12,48961,48987,12,48989,49015,12,49017,49043,12,49045,49071,12,49073,49099,12,49101,49127,12,49129,49155,12,49157,49183,12,49185,49211,12,49213,49239,12,49241,49267,12,49269,49295,12,49297,49323,12,49325,49351,12,49353,49379,12,49381,49407,12,49409,49435,12,49437,49463,12,49465,49491,12,49493,49519,12,49521,49547,12,49549,49575,12,49577,49603,12,49605,49631,12,49633,49659,12,49661,49687,12,49689,49715,12,49717,49743,12,49745,49771,12,49773,49799,12,49801,49827,12,49829,49855,12,49857,49883,12,49885,49911,12,49913,49939,12,49941,49967,12,49969,49995,12,49997,50023,12,50025,50051,12,50053,50079,12,50081,50107,12,50109,50135,12,50137,50163,12,50165,50191,12,50193,50219,12,50221,50247,12,50249,50275,12,50277,50303,12,50305,50331,12,50333,50359,12,50361,50387,12,50389,50415,12,50417,50443,12,50445,50471,12,50473,50499,12,50501,50527,12,50529,50555,12,50557,50583,12,50585,50611,12,50613,50639,12,50641,50667,12,50669,50695,12,50697,50723,12,50725,50751,12,50753,50779,12,50781,50807,12,50809,50835,12,50837,50863,12,50865,50891,12,50893,50919,12,50921,50947,12,50949,50975,12,50977,51003,12,51005,51031,12,51033,51059,12,51061,51087,12,51089,51115,12,51117,51143,12,51145,51171,12,51173,51199,12,51201,51227,12,51229,51255,12,51257,51283,12,51285,51311,12,51313,51339,12,51341,51367,12,51369,51395,12,51397,51423,12,51425,51451,12,51453,51479,12,51481,51507,12,51509,51535,12,51537,51563,12,51565,51591,12,51593,51619,12,51621,51647,12,51649,51675,12,51677,51703,12,51705,51731,12,51733,51759,12,51761,51787,12,51789,51815,12,51817,51843,12,51845,51871,12,51873,51899,12,51901,51927,12,51929,51955,12,51957,51983,12,51985,52011,12,52013,52039,12,52041,52067,12,52069,52095,12,52097,52123,12,52125,52151,12,52153,52179,12,52181,52207,12,52209,52235,12,52237,52263,12,52265,52291,12,52293,52319,12,52321,52347,12,52349,52375,12,52377,52403,12,52405,52431,12,52433,52459,12,52461,52487,12,52489,52515,12,52517,52543,12,52545,52571,12,52573,52599,12,52601,52627,12,52629,52655,12,52657,52683,12,52685,52711,12,52713,52739,12,52741,52767,12,52769,52795,12,52797,52823,12,52825,52851,12,52853,52879,12,52881,52907,12,52909,52935,12,52937,52963,12,52965,52991,12,52993,53019,12,53021,53047,12,53049,53075,12,53077,53103,12,53105,53131,12,53133,53159,12,53161,53187,12,53189,53215,12,53217,53243,12,53245,53271,12,53273,53299,12,53301,53327,12,53329,53355,12,53357,53383,12,53385,53411,12,53413,53439,12,53441,53467,12,53469,53495,12,53497,53523,12,53525,53551,12,53553,53579,12,53581,53607,12,53609,53635,12,53637,53663,12,53665,53691,12,53693,53719,12,53721,53747,12,53749,53775,12,53777,53803,12,53805,53831,12,53833,53859,12,53861,53887,12,53889,53915,12,53917,53943,12,53945,53971,12,53973,53999,12,54001,54027,12,54029,54055,12,54057,54083,12,54085,54111,12,54113,54139,12,54141,54167,12,54169,54195,12,54197,54223,12,54225,54251,12,54253,54279,12,54281,54307,12,54309,54335,12,54337,54363,12,54365,54391,12,54393,54419,12,54421,54447,12,54449,54475,12,54477,54503,12,54505,54531,12,54533,54559,12,54561,54587,12,54589,54615,12,54617,54643,12,54645,54671,12,54673,54699,12,54701,54727,12,54729,54755,12,54757,54783,12,54785,54811,12,54813,54839,12,54841,54867,12,54869,54895,12,54897,54923,12,54925,54951,12,54953,54979,12,54981,55007,12,55009,55035,12,55037,55063,12,55065,55091,12,55093,55119,12,55121,55147,12,55149,55175,12,55177,55203,12,55243,55291,10,65024,65039,5,65279,65279,4,65520,65528,4,66045,66045,5,66422,66426,5,68101,68102,5,68152,68154,5,68325,68326,5,69291,69292,5,69632,69632,7,69634,69634,7,69759,69761,5]")}}),define(Q[101],J([0,1,8]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.writeUInt8=e.readUInt8=e.writeUInt32BE=e.readUInt32BE=e.writeUInt16LE=e.readUInt16LE=e.VSBuffer=void 0;const N=typeof Buffer!="undefined",M=typeof TextDecoder!="undefined";let w;class S{constructor(a){this.buffer=a,this.byteLength=this.buffer.byteLength}static wrap(a){return N&&!Buffer.isBuffer(a)&&(a=Buffer.from(a.buffer,a.byteOffset,a.byteLength)),new S(a)}toString(){return N?this.buffer.toString():M?(w||(w=new TextDecoder),w.decode(this.buffer)):b.decodeUTF8(this.buffer)}}e.VSBuffer=S;function C(s,a){return s[a+0]<<0>>>0|s[a+1]<<8>>>0}e.readUInt16LE=C;function d(s,a,u){s[u+0]=a&255,a=a>>>8,s[u+1]=a&255}e.writeUInt16LE=d;function g(s,a){return s[a]*Math.pow(2,24)+s[a+1]*Math.pow(2,16)+s[a+2]*Math.pow(2,8)+s[a+3]}e.readUInt32BE=g;function p(s,a,u){s[u+3]=a,a=a>>>8,s[u+2]=a,a=a>>>8,s[u+1]=a,a=a>>>8,s[u]=a}e.writeUInt32BE=p;function c(s,a){return s[a]}e.readUInt8=c;function o(s,a,u){s[u]=a}e.writeUInt8=o}),define(Q[152],J([0,1,17,8,72]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.hasDriveLetter=e.isRootOrDriveLetter=e.isWindowsDriveLetter=e.isEqualOrParent=e.toSlashes=void 0;function w(p){return p.replace(/[\\/]/g,M.posix.sep)}e.toSlashes=w;function S(p,c,o,s=M.sep){if(p===c)return!0;if(!p||!c||c.length>p.length)return!1;if(o){if(!N.startsWithIgnoreCase(p,c))return!1;if(c.length===p.length)return!0;let u=c.length;return c.charAt(c.length-1)===s&&u--,p.charAt(u)===s}return c.charAt(c.length-1)!==s&&(c+=s),p.indexOf(c)===0}e.isEqualOrParent=S;function C(p){return p>=65&&p<=90||p>=97&&p<=122}e.isWindowsDriveLetter=C;function d(p){const c=M.normalize(p);return b.isWindows?p.length>3?!1:g(c)&&(p.length===2||c.charCodeAt(2)===92):c===M.posix.sep}e.isRootOrDriveLetter=d;function g(p){return b.isWindows?C(p.charCodeAt(0))&&p.charCodeAt(1)===58:!1}e.hasDriveLetter=g}),define(Q[89],J([0,1,8]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.StringSHA1=e.toHexString=e.stringHash=e.doHash=e.hash=void 0;function N(u){return M(u,0)}e.hash=N;function M(u,r){switch(typeof u){case"object":return u===null?w(349,r):Array.isArray(u)?d(u,r):g(u,r);case"string":return C(u,r);case"boolean":return S(u,r);case"number":return w(u,r);case"undefined":return w(937,r);default:return w(617,r)}}e.doHash=M;function w(u,r){return(r<<5)-r+u|0}function S(u,r){return w(u?433:863,r)}function C(u,r){r=w(149417,r);for(let i=0,n=u.length;iM(n,i),r)}function g(u,r){return r=w(181387,r),Object.keys(u).sort().reduce((i,n)=>(i=C(n,i),M(u[n],i)),r)}function p(u,r,i=32){const n=i-r,t=~((1<>>n)>>>0}function c(u,r=0,i=u.byteLength,n=0){for(let t=0;ti.toString(16).padStart(2,"0")).join(""):o((u>>>0).toString(16),r/4)}e.toHexString=s;class a{constructor(){this._h0=1732584193,this._h1=4023233417,this._h2=2562383102,this._h3=271733878,this._h4=3285377520,this._buff=new Uint8Array(64+3),this._buffDV=new DataView(this._buff.buffer),this._buffLen=0,this._totalLen=0,this._leftoverHighSurrogate=0,this._finished=!1}update(r){const i=r.length;if(i!==0){const n=this._buff;let t=this._buffLen,l=this._leftoverHighSurrogate,h,m;for(l!==0?(h=l,m=-1,l=0):(h=r.charCodeAt(0),m=0);;){let _=h;if(b.isHighSurrogate(h))if(m+1>>6,r[i++]=128|(n&63)>>>0):n<65536?(r[i++]=224|(n&61440)>>>12,r[i++]=128|(n&4032)>>>6,r[i++]=128|(n&63)>>>0):(r[i++]=240|(n&1835008)>>>18,r[i++]=128|(n&258048)>>>12,r[i++]=128|(n&4032)>>>6,r[i++]=128|(n&63)>>>0),i>=64&&(this._step(),i-=64,this._totalLen+=64,r[0]=r[64+0],r[1]=r[64+1],r[2]=r[64+2]),i}digest(){return this._finished||(this._finished=!0,this._leftoverHighSurrogate&&(this._leftoverHighSurrogate=0,this._buffLen=this._push(this._buff,this._buffLen,65533)),this._totalLen+=this._buffLen,this._wrapUp()),s(this._h0)+s(this._h1)+s(this._h2)+s(this._h3)+s(this._h4)}_wrapUp(){this._buff[this._buffLen++]=128,c(this._buff,this._buffLen),this._buffLen>56&&(this._step(),c(this._buff));const r=8*this._totalLen;this._buffDV.setUint32(56,Math.floor(r/4294967296),!1),this._buffDV.setUint32(60,r%4294967296,!1),this._step()}_step(){const r=a._bigBlock32,i=this._buffDV;for(let y=0;y<64;y+=4)r.setUint32(y,i.getUint32(y,!1),!1);for(let y=64;y<320;y+=4)r.setUint32(y,p(r.getUint32(y-12,!1)^r.getUint32(y-32,!1)^r.getUint32(y-56,!1)^r.getUint32(y-64,!1),1),!1);let n=this._h0,t=this._h1,l=this._h2,h=this._h3,m=this._h4,_,f,v;for(let y=0;y<80;y++)y<20?(_=t&l|~t&h,f=1518500249):y<40?(_=t^l^h,f=1859775393):y<60?(_=t&l|t&h|l&h,f=2400959708):(_=t^l^h,f=3395469782),v=p(n,5)+_+m+f+r.getUint32(y*4,!1)&4294967295,m=h,h=l,l=p(t,30),t=n,n=v;this._h0=this._h0+n&4294967295,this._h1=this._h1+t&4294967295,this._h2=this._h2+l&4294967295,this._h3=this._h3+h&4294967295,this._h4=this._h4+m&4294967295}}e.StringSHA1=a,a._bigBlock32=new DataView(new ArrayBuffer(320))}),define(Q[153],J([0,1,277,89]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LcsDiff=e.MyArray=e.Debug=e.stringDiff=e.StringDiffSequence=void 0;class M{constructor(c){this.source=c}getElements(){const c=this.source,o=new Int32Array(c.length);for(let s=0,a=c.length;s0||this.m_modifiedCount>0)&&this.m_changes.push(new b.DiffChange(this.m_originalStart,this.m_originalCount,this.m_modifiedStart,this.m_modifiedCount)),this.m_originalCount=0,this.m_modifiedCount=0,this.m_originalStart=1073741824,this.m_modifiedStart=1073741824}AddOriginalElement(c,o){this.m_originalStart=Math.min(this.m_originalStart,c),this.m_modifiedStart=Math.min(this.m_modifiedStart,o),this.m_originalCount++}AddModifiedElement(c,o){this.m_originalStart=Math.min(this.m_originalStart,c),this.m_modifiedStart=Math.min(this.m_modifiedStart,o),this.m_modifiedCount++}getChanges(){return(this.m_originalCount>0||this.m_modifiedCount>0)&&this.MarkNextChange(),this.m_changes}getReverseChanges(){return(this.m_originalCount>0||this.m_modifiedCount>0)&&this.MarkNextChange(),this.m_changes.reverse(),this.m_changes}}class g{constructor(c,o,s=null){this.ContinueProcessingPredicate=s;const[a,u,r]=g._getElements(c),[i,n,t]=g._getElements(o);this._hasStrings=r&&t,this._originalStringElements=a,this._originalElementsOrHash=u,this._modifiedStringElements=i,this._modifiedElementsOrHash=n,this.m_forwardHistory=[],this.m_reverseHistory=[]}static _isStringArray(c){return c.length>0&&typeof c[0]=="string"}static _getElements(c){const o=c.getElements();if(g._isStringArray(o)){const s=new Int32Array(o.length);for(let a=0,u=o.length;a=c&&a>=s&&this.ElementsAreEqual(o,a);)o--,a--;if(c>o||s>a){let h;return s<=a?(S.Assert(c===o+1,"originalStart should only be one more than originalEnd"),h=[new b.DiffChange(c,0,s,a-s+1)]):c<=o?(S.Assert(s===a+1,"modifiedStart should only be one more than modifiedEnd"),h=[new b.DiffChange(c,o-c+1,s,0)]):(S.Assert(c===o+1,"originalStart should only be one more than originalEnd"),S.Assert(s===a+1,"modifiedStart should only be one more than modifiedEnd"),h=[]),h}const r=[0],i=[0],n=this.ComputeRecursionPoint(c,o,s,a,r,i,u),t=r[0],l=i[0];if(n!==null)return n;if(!u[0]){const h=this.ComputeDiffRecursive(c,t,s,l,u);let m=[];return u[0]?m=[new b.DiffChange(t+1,o-(t+1)+1,l+1,a-(l+1)+1)]:m=this.ComputeDiffRecursive(t+1,o,l+1,a,u),this.ConcatenateChanges(h,m)}return[new b.DiffChange(c,o-c+1,s,a-s+1)]}WALKTRACE(c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y,L,I){let k=null,E=null,T=new d,O=o,A=s,B=_[0]-y[0]-a,F=-1073741824,D=this.m_forwardHistory.length-1;do{const R=B+c;R===O||R=0&&(t=this.m_forwardHistory[D],c=t[0],O=1,A=t.length-1)}while(--D>=-1);if(k=T.getReverseChanges(),I[0]){let R=_[0]+1,W=y[0]+1;if(k!==null&&k.length>0){const x=k[k.length-1];R=Math.max(R,x.getOriginalEnd()),W=Math.max(W,x.getModifiedEnd())}E=[new b.DiffChange(R,m-R+1,W,v-W+1)]}else{T=new d,O=r,A=i,B=_[0]-y[0]-n,F=1073741824,D=L?this.m_reverseHistory.length-1:this.m_reverseHistory.length-2;do{const R=B+u;R===O||R=l[R+1]?(h=l[R+1]-1,f=h-B-n,h>F&&T.MarkNextChange(),F=h+1,T.AddOriginalElement(h+1,f+1),B=R+1-u):(h=l[R-1],f=h-B-n,h>F&&T.MarkNextChange(),F=h,T.AddModifiedElement(h+1,f+1),B=R-1-u),D>=0&&(l=this.m_reverseHistory[D],u=l[0],O=1,A=l.length-1)}while(--D>=-1);E=T.getChanges()}return this.ConcatenateChanges(k,E)}ComputeRecursionPoint(c,o,s,a,u,r,i){let n=0,t=0,l=0,h=0,m=0,_=0;c--,s--,u[0]=0,r[0]=0,this.m_forwardHistory=[],this.m_reverseHistory=[];const f=o-c+(a-s),v=f+1,y=new Int32Array(v),L=new Int32Array(v),I=a-s,k=o-c,E=c-s,T=o-a,A=(k-I)%2==0;y[I]=c,L[k]=o,i[0]=!1;for(let B=1;B<=f/2+1;B++){let F=0,D=0;l=this.ClipDiagonalBound(I-B,B,I,v),h=this.ClipDiagonalBound(I+B,B,I,v);for(let W=l;W<=h;W+=2){W===l||WF+D&&(F=n,D=t),!A&&Math.abs(W-k)<=B-1&&n>=L[W])return u[0]=n,r[0]=t,x<=L[W]&&1447>0&&B<=1447+1?this.WALKTRACE(I,l,h,E,k,m,_,T,y,L,n,o,u,t,a,r,A,i):null}const R=(F-c+(D-s)-B)/2;if(this.ContinueProcessingPredicate!==null&&!this.ContinueProcessingPredicate(F,R))return i[0]=!0,u[0]=F,r[0]=D,R>0&&1447>0&&B<=1447+1?this.WALKTRACE(I,l,h,E,k,m,_,T,y,L,n,o,u,t,a,r,A,i):(c++,s++,[new b.DiffChange(c,o-c+1,s,a-s+1)]);m=this.ClipDiagonalBound(k-B,B,k,v),_=this.ClipDiagonalBound(k+B,B,k,v);for(let W=m;W<=_;W+=2){W===m||W<_&&L[W-1]>=L[W+1]?n=L[W+1]-1:n=L[W-1],t=n-(W-k)-T;const x=n;for(;n>c&&t>s&&this.ElementsAreEqual(n,t);)n--,t--;if(L[W]=n,A&&Math.abs(W-I)<=B&&n<=y[W])return u[0]=n,r[0]=t,x>=y[W]&&1447>0&&B<=1447+1?this.WALKTRACE(I,l,h,E,k,m,_,T,y,L,n,o,u,t,a,r,A,i):null}if(B<=1447){let W=new Int32Array(h-l+2);W[0]=I-l+1,C.Copy2(y,l,W,1,h-l+1),this.m_forwardHistory.push(W),W=new Int32Array(_-m+2),W[0]=k-m+1,C.Copy2(L,m,W,1,_-m+1),this.m_reverseHistory.push(W)}}return this.WALKTRACE(I,l,h,E,k,m,_,T,y,L,n,o,u,t,a,r,A,i)}PrettifyChanges(c){for(let o=0;o0,i=s.modifiedLength>0;for(;s.originalStart+s.originalLength=0;o--){const s=c[o];let a=0,u=0;if(o>0){const l=c[o-1];l.originalLength>0&&(a=l.originalStart+l.originalLength),l.modifiedLength>0&&(u=l.modifiedStart+l.modifiedLength)}const r=s.originalLength>0,i=s.modifiedLength>0;let n=0,t=this._boundaryScore(s.originalStart,s.originalLength,s.modifiedStart,s.modifiedLength);for(let l=1;;l++){const h=s.originalStart-l,m=s.modifiedStart-l;if(ht&&(t=_,n=l)}s.originalStart-=n,s.modifiedStart-=n}if(this._hasStrings)for(let o=1,s=c.length;o0&&_>n&&(n=_,t=h,l=m)}return n>0?[t,l]:null}_contiguousSequenceScore(c,o,s){let a=0;for(let u=0;u=this._originalElementsOrHash.length-1?!0:this._hasStrings&&/^\s*$/.test(this._originalStringElements[c])}_OriginalRegionIsBoundary(c,o){if(this._OriginalIsBoundary(c)||this._OriginalIsBoundary(c-1))return!0;if(o>0){const s=c+o;if(this._OriginalIsBoundary(s-1)||this._OriginalIsBoundary(s))return!0}return!1}_ModifiedIsBoundary(c){return c<=0||c>=this._modifiedElementsOrHash.length-1?!0:this._hasStrings&&/^\s*$/.test(this._modifiedStringElements[c])}_ModifiedRegionIsBoundary(c,o){if(this._ModifiedIsBoundary(c)||this._ModifiedIsBoundary(c-1))return!0;if(o>0){const s=c+o;if(this._ModifiedIsBoundary(s-1)||this._ModifiedIsBoundary(s))return!0}return!1}_boundaryScore(c,o,s,a){const u=this._OriginalRegionIsBoundary(c,o)?1:0,r=this._ModifiedRegionIsBoundary(s,a)?1:0;return u+r}ConcatenateChanges(c,o){let s=[];if(c.length===0||o.length===0)return o.length>0?o:c;if(this.ChangesOverlap(c[c.length-1],o[0],s)){const a=new Array(c.length+o.length-1);return C.Copy(c,0,a,0,c.length-1),a[c.length-1]=s[0],C.Copy(o,1,a,c.length,o.length-1),a}else{const a=new Array(c.length+o.length);return C.Copy(c,0,a,0,c.length),C.Copy(o,0,a,c.length,o.length),a}}ChangesOverlap(c,o,s){if(S.Assert(c.originalStart<=o.originalStart,"Left change is not less than or equal to right change"),S.Assert(c.modifiedStart<=o.modifiedStart,"Left change is not less than or equal to right change"),c.originalStart+c.originalLength>=o.originalStart||c.modifiedStart+c.modifiedLength>=o.modifiedStart){const a=c.originalStart;let u=c.originalLength;const r=c.modifiedStart;let i=c.modifiedLength;return c.originalStart+c.originalLength>=o.originalStart&&(u=o.originalStart+o.originalLength-c.originalStart),c.modifiedStart+c.modifiedLength>=o.modifiedStart&&(i=o.modifiedStart+o.modifiedLength-c.modifiedStart),s[0]=new b.DiffChange(a,u,r,i),!0}else return s[0]=null,!1}ClipDiagonalBound(c,o,s,a){if(c>=0&&ct.children.map(L=>o.getId(L.element).toString())},{getElements:()=>[...t.children.slice(0,h),...l,...t.children.slice(h+a)].map(L=>o.getId(L.element).toString())}).ComputeDiff(!1);if(m.quitEarly)return this.spliceSimple(s,a,l,r);const _=s.slice(0,-1),f=(L,I,k)=>{if(i>0)for(let E=0;Ek.originalStart-I.originalStart))f(v,y,v-(L.originalStart+L.originalLength)),v=L.originalStart,y=L.modifiedStart-h,this.spliceSimple([..._,v],L.originalLength,S.Iterable.slice(l,y,y+L.modifiedLength),r);f(v,y,v)}spliceSimple(o,s,a=S.Iterable.empty(),{onDidCreateNode:u,onDidDeleteNode:r}){const{parentNode:i,listIndex:n,revealed:t,visible:l}=this.getParentNodeWithListIndex(o),h=[],m=S.Iterable.map(a,A=>this.createTreeNode(A,i,i.visible?1:0,t,h,u)),_=o[o.length-1],f=i.children.length>0;let v=0;for(let A=_;A>=0&&AB+(F.visible?F.renderNodeCount:0),0);this._updateAncestorsRenderNodeCount(i,I-A),this.list.splice(n,A,h)}if(k.length>0&&r){const A=B=>{r(B),B.children.forEach(A)};k.forEach(A)}const T=i.children.length>0;f!==T&&this.setCollapsible(o.slice(0,-1),T),this._onDidSplice.fire({insertedNodes:y,deletedNodes:k});let O=i;for(;O;){if(O.visibility===2){this.refilter();break}O=O.parent}}rerender(o){if(o.length===0)throw new b.TreeError(this.user,"Invalid tree location");const{node:s,listIndex:a,revealed:u}=this.getTreeNodeWithListIndex(o);s.visible&&u&&this.list.splice(a,1,[s])}has(o){return this.hasTreeNode(o)}getListIndex(o){const{listIndex:s,visible:a,revealed:u}=this.getTreeNodeWithListIndex(o);return a&&u?s:-1}getListRenderCount(o){return this.getTreeNode(o).renderNodeCount}isCollapsible(o){return this.getTreeNode(o).collapsible}setCollapsible(o,s){const a=this.getTreeNode(o);typeof s=="undefined"&&(s=!a.collapsible);const u={collapsible:s};return this.eventBufferer.bufferEvents(()=>this._setCollapseState(o,u))}isCollapsed(o){return this.getTreeNode(o).collapsed}setCollapsed(o,s,a){const u=this.getTreeNode(o);typeof s=="undefined"&&(s=!u.collapsed);const r={collapsed:s,recursive:a||!1};return this.eventBufferer.bufferEvents(()=>this._setCollapseState(o,r))}_setCollapseState(o,s){const{node:a,listIndex:u,revealed:r}=this.getTreeNodeWithListIndex(o),i=this._setListNodeCollapseState(a,u,r,s);if(a!==this.root&&this.autoExpandSingleChildren&&i&&!g(s)&&a.collapsible&&!a.collapsed&&!s.recursive){let n=-1;for(let t=0;t-1){n=-1;break}else n=t;n>-1&&this._setCollapseState([...o,n],s)}return i}_setListNodeCollapseState(o,s,a,u){const r=this._setNodeCollapseState(o,u,!1);if(!a||!o.visible||!r)return r;const i=o.renderNodeCount,n=this.updateNodeAfterCollapseChange(o),t=i-(s===-1?0:1);return this.list.splice(s+1,t,n.slice(1)),r}_setNodeCollapseState(o,s,a){let u;if(o===this.root?u=!1:(g(s)?(u=o.collapsible!==s.collapsible,o.collapsible=s.collapsible):o.collapsible?(u=o.collapsed!==s.collapsed,o.collapsed=s.collapsed):u=!1,u&&this._onDidChangeCollapseState.fire({node:o,deep:a})),!g(s)&&s.recursive)for(const r of o.children)u=this._setNodeCollapseState(r,s,!0)||u;return u}expandTo(o){this.eventBufferer.bufferEvents(()=>{let s=this.getTreeNode(o);for(;s.parent;)s=s.parent,o=o.slice(0,o.length-1),s.collapsed&&this._setCollapseState(o,{collapsed:!1,recursive:!1})})}refilter(){const o=this.root.renderNodeCount,s=this.updateNodeAfterFilterChange(this.root);this.list.splice(0,o,s)}createTreeNode(o,s,a,u,r,i){const n={parent:s,element:o.element,children:[],depth:s.depth+1,visibleChildrenCount:0,visibleChildIndex:-1,collapsible:typeof o.collapsible=="boolean"?o.collapsible:typeof o.collapsed!="undefined",collapsed:typeof o.collapsed=="undefined"?this.collapseByDefault:o.collapsed,renderNodeCount:1,visibility:1,visible:!0,filterData:void 0},t=this._filterNode(n,a);n.visibility=t,u&&r.push(n);const l=o.children||S.Iterable.empty(),h=u&&t!==0&&!n.collapsed,m=S.Iterable.map(l,v=>this.createTreeNode(v,n,t,h,r,i));let _=0,f=1;for(const v of m)n.children.push(v),f+=v.renderNodeCount,v.visible&&(v.visibleChildIndex=_++);return n.collapsible=n.collapsible||n.children.length>0,n.visibleChildrenCount=_,n.visible=t===2?_>0:t===1,n.visible?n.collapsed||(n.renderNodeCount=f):(n.renderNodeCount=0,u&&r.pop()),i&&i(n),n}updateNodeAfterCollapseChange(o){const s=o.renderNodeCount,a=[];return this._updateNodeAfterCollapseChange(o,a),this._updateAncestorsRenderNodeCount(o.parent,a.length-s),a}_updateNodeAfterCollapseChange(o,s){if(o.visible===!1)return 0;if(s.push(o),o.renderNodeCount=1,!o.collapsed)for(const a of o.children)o.renderNodeCount+=this._updateNodeAfterCollapseChange(a,s);return this._onDidChangeRenderNodeCount.fire(o),o.renderNodeCount}updateNodeAfterFilterChange(o){const s=o.renderNodeCount,a=[];return this._updateNodeAfterFilterChange(o,o.visible?1:0,a),this._updateAncestorsRenderNodeCount(o.parent,a.length-s),a}_updateNodeAfterFilterChange(o,s,a,u=!0){let r;if(o!==this.root){if(r=this._filterNode(o,s),r===0)return o.visible=!1,o.renderNodeCount=0,!1;u&&a.push(o)}const i=a.length;o.renderNodeCount=o===this.root?0:1;let n=!1;if(!o.collapsed||r!==0){let t=0;for(const l of o.children)n=this._updateNodeAfterFilterChange(l,r,a,u&&!o.collapsed)||n,l.visible&&(l.visibleChildIndex=t++);o.visibleChildrenCount=t}else o.visibleChildrenCount=0;return o!==this.root&&(o.visible=r===2?n:r===1),o.visible?o.collapsed||(o.renderNodeCount+=a.length-i):(o.renderNodeCount=0,u&&a.pop()),this._onDidChangeRenderNodeCount.fire(o),o.visible}_updateAncestorsRenderNodeCount(o,s){if(s!==0)for(;o;)o.renderNodeCount+=s,this._onDidChangeRenderNodeCount.fire(o),o=o.parent}_filterNode(o,s){const a=this.filter?this.filter.filter(o.element,s):1;return typeof a=="boolean"?(o.filterData=void 0,a?1:0):C(a)?(o.filterData=a.data,d(a.visibility)):(o.filterData=void 0,d(a))}hasTreeNode(o,s=this.root){if(!o||o.length===0)return!0;const[a,...u]=o;return a<0||a>s.children.length?!1:this.hasTreeNode(u,s.children[a])}getTreeNode(o,s=this.root){if(!o||o.length===0)return s;const[a,...u]=o;if(a<0||a>s.children.length)throw new b.TreeError(this.user,"Invalid tree location");return this.getTreeNode(u,s.children[a])}getTreeNodeWithListIndex(o){if(o.length===0)return{node:this.root,listIndex:-1,revealed:!0,visible:!1};const{parentNode:s,listIndex:a,revealed:u,visible:r}=this.getParentNodeWithListIndex(o),i=o[o.length-1];if(i<0||i>s.children.length)throw new b.TreeError(this.user,"Invalid tree location");const n=s.children[i];return{node:n,listIndex:a,revealed:u,visible:r&&n.visible}}getParentNodeWithListIndex(o,s=this.root,a=0,u=!0,r=!0){const[i,...n]=o;if(i<0||i>s.children.length)throw new b.TreeError(this.user,"Invalid tree location");for(let t=0;t{var r;if(u.element!==null){const i=u;if(c.add(i.element),this.nodes.set(i.element,i),this.identityProvider){const n=this.identityProvider.getId(i.element).toString();o.add(n),this.nodesByIdentity.set(n,i)}(r=p.onDidCreateNode)===null||r===void 0||r.call(p,i)}},a=u=>{var r;if(u.element!==null){const i=u;if(c.has(i.element)||this.nodes.delete(i.element),this.identityProvider){const n=this.identityProvider.getId(i.element).toString();o.has(n)||this.nodesByIdentity.delete(n)}(r=p.onDidDeleteNode)===null||r===void 0||r.call(p,i)}};this.model.splice([...d,0],Number.MAX_VALUE,g,Object.assign(Object.assign({},p),{onDidCreateNode:s,onDidDeleteNode:a}))}preserveCollapseState(d=b.Iterable.empty()){return this.sorter&&(d=w.mergeSort([...d],this.sorter.compare.bind(this.sorter))),b.Iterable.map(d,g=>{let p=this.nodes.get(g.element);if(!p&&this.identityProvider){const s=this.identityProvider.getId(g.element).toString();p=this.nodesByIdentity.get(s)}if(!p)return Object.assign(Object.assign({},g),{children:this.preserveCollapseState(g.children)});const c=typeof g.collapsible=="boolean"?g.collapsible:p.collapsible,o=typeof g.collapsed!="undefined"?g.collapsed:p.collapsed;return Object.assign(Object.assign({},g),{collapsible:c,collapsed:o,children:this.preserveCollapseState(g.children)})})}rerender(d){const g=this.getElementLocation(d);this.model.rerender(g)}has(d){return this.nodes.has(d)}getListIndex(d){const g=this.getElementLocation(d);return this.model.getListIndex(g)}getListRenderCount(d){const g=this.getElementLocation(d);return this.model.getListRenderCount(g)}isCollapsible(d){const g=this.getElementLocation(d);return this.model.isCollapsible(g)}setCollapsible(d,g){const p=this.getElementLocation(d);return this.model.setCollapsible(p,g)}isCollapsed(d){const g=this.getElementLocation(d);return this.model.isCollapsed(g)}setCollapsed(d,g,p){const c=this.getElementLocation(d);return this.model.setCollapsed(c,g,p)}expandTo(d){const g=this.getElementLocation(d);this.model.expandTo(g)}refilter(){this.model.refilter()}getNode(d=null){if(d===null)return this.model.getNode(this.model.rootRef);const g=this.nodes.get(d);if(!g)throw new M.TreeError(this.user,`Tree element not found: ${d}`);return g}getNodeLocation(d){return d.element}getParentNodeLocation(d){if(d===null)throw new M.TreeError(this.user,"Invalid getParentNodeLocation call");const g=this.nodes.get(d);if(!g)throw new M.TreeError(this.user,`Tree element not found: ${d}`);const p=this.model.getNodeLocation(g),c=this.model.getParentNodeLocation(p);return this.model.getNode(c).element}getElementLocation(d){if(d===null)return[];const g=this.nodes.get(d);if(!g)throw new M.TreeError(this.user,`Tree element not found: ${d}`);return this.model.getNodeLocation(g)}}e.ObjectTreeModel=S}),define(Q[284],J([0,1,54,6,98,155]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CompressibleObjectTreeModel=e.DefaultElementMapper=e.CompressedObjectTreeModel=e.decompress=e.compress=void 0;function S(n){const t=[n.element],l=n.incompressible||!1;return{element:{elements:t,incompressible:l},children:b.Iterable.map(b.Iterable.from(n.children),S),collapsible:n.collapsible,collapsed:n.collapsed}}function C(n){const t=[n.element],l=n.incompressible||!1;let h,m;for(;[m,h]=b.Iterable.consume(b.Iterable.from(n.children),2),!(m.length!==1||m[0].incompressible);)n=m[0],t.push(n.element);return{element:{elements:t,incompressible:l},children:b.Iterable.map(b.Iterable.concat(m,h),C),collapsible:n.collapsible,collapsed:n.collapsed}}e.compress=C;function d(n,t=0){let l;return td(h,0)),t===0&&n.element.incompressible?{element:n.element.elements[t],children:l,incompressible:!0,collapsible:n.collapsible,collapsed:n.collapsed}:{element:n.element.elements[t],children:l,collapsible:n.collapsible,collapsed:n.collapsed}}function g(n){return d(n,0)}e.decompress=g;function p(n,t,l){return n.element===t?Object.assign(Object.assign({},n),{children:l}):Object.assign(Object.assign({},n),{children:b.Iterable.map(b.Iterable.from(n.children),h=>p(h,t,l))})}const c=n=>({getId(t){return t.elements.map(l=>n.getId(l).toString()).join("\0")}});class o{constructor(t,l,h={}){this.user=t,this.rootRef=null,this.nodes=new Map,this.model=new w.ObjectTreeModel(t,l,h),this.enabled=typeof h.compressionEnabled=="undefined"?!0:h.compressionEnabled,this.identityProvider=h.identityProvider}get onDidSplice(){return this.model.onDidSplice}get onDidChangeCollapseState(){return this.model.onDidChangeCollapseState}get onDidChangeRenderNodeCount(){return this.model.onDidChangeRenderNodeCount}setChildren(t,l=b.Iterable.empty(),h){const m=h.diffIdentityProvider&&c(h.diffIdentityProvider);if(t===null){const T=b.Iterable.map(l,this.enabled?C:S);this._setChildren(null,T,{diffIdentityProvider:m,diffDepth:Infinity});return}const _=this.nodes.get(t);if(!_)throw new Error("Unknown compressed tree node");const f=this.model.getNode(_),v=this.model.getParentNodeLocation(_),y=this.model.getNode(v),L=g(f),I=p(L,t,l),k=(this.enabled?C:S)(I),E=y.children.map(T=>T===f?k:T);this._setChildren(y.element,E,{diffIdentityProvider:m,diffDepth:f.depth-y.depth})}setCompressionEnabled(t){if(t!==this.enabled){this.enabled=t;const h=this.model.getNode().children,m=b.Iterable.map(h,g),_=b.Iterable.map(m,t?C:S);this._setChildren(null,_,{diffIdentityProvider:this.identityProvider,diffDepth:Infinity})}}_setChildren(t,l,h){const m=new Set,_=v=>{for(const y of v.element.elements)m.add(y),this.nodes.set(y,v.element)},f=v=>{for(const y of v.element.elements)m.has(y)||this.nodes.delete(y)};this.model.setChildren(t,l,Object.assign(Object.assign({},h),{onDidCreateNode:_,onDidDeleteNode:f}))}has(t){return this.nodes.has(t)}getListIndex(t){const l=this.getCompressedNode(t);return this.model.getListIndex(l)}getListRenderCount(t){const l=this.getCompressedNode(t);return this.model.getListRenderCount(l)}getNode(t){if(typeof t=="undefined")return this.model.getNode();const l=this.getCompressedNode(t);return this.model.getNode(l)}getNodeLocation(t){const l=this.model.getNodeLocation(t);return l===null?null:l.elements[l.elements.length-1]}getParentNodeLocation(t){const l=this.getCompressedNode(t),h=this.model.getParentNodeLocation(l);return h===null?null:h.elements[h.elements.length-1]}isCollapsible(t){const l=this.getCompressedNode(t);return this.model.isCollapsible(l)}setCollapsible(t,l){const h=this.getCompressedNode(t);return this.model.setCollapsible(h,l)}isCollapsed(t){const l=this.getCompressedNode(t);return this.model.isCollapsed(l)}setCollapsed(t,l,h){const m=this.getCompressedNode(t);return this.model.setCollapsed(m,l,h)}expandTo(t){const l=this.getCompressedNode(t);this.model.expandTo(l)}rerender(t){const l=this.getCompressedNode(t);this.model.rerender(l)}refilter(){this.model.refilter()}getCompressedNode(t){if(t===null)return null;const l=this.nodes.get(t);if(!l)throw new M.TreeError(this.user,`Tree element not found: ${t}`);return l}}e.CompressedObjectTreeModel=o;const s=n=>n[n.length-1];e.DefaultElementMapper=s;class a{constructor(t,l){this.unwrapper=t,this.node=l}get element(){return this.node.element===null?null:this.unwrapper(this.node.element)}get children(){return this.node.children.map(t=>new a(this.unwrapper,t))}get depth(){return this.node.depth}get visibleChildrenCount(){return this.node.visibleChildrenCount}get visibleChildIndex(){return this.node.visibleChildIndex}get collapsible(){return this.node.collapsible}get collapsed(){return this.node.collapsed}get visible(){return this.node.visible}get filterData(){return this.node.filterData}}function u(n,t){return{splice(l,h,m){t.splice(l,h,m.map(_=>n.map(_)))},updateElementHeight(l,h){t.updateElementHeight(l,h)}}}function r(n,t){return Object.assign(Object.assign({},t),{identityProvider:t.identityProvider&&{getId(l){return t.identityProvider.getId(n(l))}},sorter:t.sorter&&{compare(l,h){return t.sorter.compare(l.elements[0],h.elements[0])}},filter:t.filter&&{filter(l,h){return t.filter.filter(n(l),h)}}})}class i{constructor(t,l,h={}){this.rootRef=null,this.elementMapper=h.elementMapper||e.DefaultElementMapper;const m=_=>this.elementMapper(_.elements);this.nodeMapper=new M.WeakMapper(_=>new a(m,_)),this.model=new o(t,u(this.nodeMapper,l),r(m,h))}get onDidSplice(){return N.Event.map(this.model.onDidSplice,({insertedNodes:t,deletedNodes:l})=>({insertedNodes:t.map(h=>this.nodeMapper.map(h)),deletedNodes:l.map(h=>this.nodeMapper.map(h))}))}get onDidChangeCollapseState(){return N.Event.map(this.model.onDidChangeCollapseState,({node:t,deep:l})=>({node:this.nodeMapper.map(t),deep:l}))}get onDidChangeRenderNodeCount(){return N.Event.map(this.model.onDidChangeRenderNodeCount,t=>this.nodeMapper.map(t))}setChildren(t,l=b.Iterable.empty(),h={}){this.model.setChildren(t,l,h)}setCompressionEnabled(t){this.model.setCompressionEnabled(t)}has(t){return this.model.has(t)}getListIndex(t){return this.model.getListIndex(t)}getListRenderCount(t){return this.model.getListRenderCount(t)}getNode(t){return this.nodeMapper.map(this.model.getNode(t))}getNodeLocation(t){return t.element}getParentNodeLocation(t){return this.model.getParentNodeLocation(t)}isCollapsible(t){return this.model.isCollapsible(t)}setCollapsible(t,l){return this.model.setCollapsible(t,l)}isCollapsed(t){return this.model.isCollapsed(t)}setCollapsed(t,l,h){return this.model.setCollapsed(t,l,h)}expandTo(t){return this.model.expandTo(t)}rerender(t){return this.model.rerender(t)}refilter(){return this.model.refilter()}getCompressedTreeNode(t=null){return this.model.getNode(t)}}e.CompressibleObjectTreeModel=i}),define(Q[285],J([0,1,8]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.buildReplaceStringWithCasePreserved=void 0;function N(S,C){if(S&&S[0]!==""){const d=M(S,C,"-"),g=M(S,C,"_");return d&&!g?w(S,C,"-"):!d&&g?w(S,C,"_"):S[0].toUpperCase()===S[0]?C.toUpperCase():S[0].toLowerCase()===S[0]?C.toLowerCase():b.containsUppercaseCharacter(S[0][0])&&C.length>0?C[0].toUpperCase()+C.substr(1):C}else return C}e.buildReplaceStringWithCasePreserved=N;function M(S,C,d){return S[0].indexOf(d)!==-1&&C.indexOf(d)!==-1&&S[0].split(d).length===C.split(d).length}function w(S,C,d){const g=C.split(d),p=S[0].split(d);let c="";return g.forEach((o,s)=>{c+=N([p[s]],o)+d}),c.slice(0,-1)}}),define(Q[82],J([0,1,8]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var N;(function(M){M[M.Ignore=0]="Ignore",M[M.Info=1]="Info",M[M.Warning=2]="Warning",M[M.Error=3]="Error"})(N||(N={})),function(M){const w="error",S="warning",C="warn",d="info";function g(p){return p?b.equalsIgnoreCase(w,p)?M.Error:b.equalsIgnoreCase(S,p)||b.equalsIgnoreCase(C,p)?M.Warning:b.equalsIgnoreCase(d,p)?M.Info:M.Ignore:M.Ignore}M.fromValue=g}(N||(N={})),e.default=N}),define(Q[20],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.withNullAsUndefined=e.createProxyObject=e.getAllMethodNames=e.getAllPropertyNames=e.validateConstraint=e.validateConstraints=e.isFunction=e.assertIsDefined=e.assertType=e.isUndefinedOrNull=e.isUndefined=e.isBoolean=e.isNumber=e.isObject=e.isString=e.isArray=void 0;function b(n){return Array.isArray(n)}e.isArray=b;function N(n){return typeof n=="string"}e.isString=N;function M(n){return typeof n=="object"&&n!==null&&!Array.isArray(n)&&!(n instanceof RegExp)&&!(n instanceof Date)}e.isObject=M;function w(n){return typeof n=="number"&&!isNaN(n)}e.isNumber=w;function S(n){return n===!0||n===!1}e.isBoolean=S;function C(n){return typeof n=="undefined"}e.isUndefined=C;function d(n){return C(n)||n===null}e.isUndefinedOrNull=d;function g(n,t){if(!n)throw new Error(t?`Unexpected type, expected '${t}'`:"Unexpected type")}e.assertType=g;function p(n){if(d(n))throw new Error("Assertion Failed: argument is undefined or null");return n}e.assertIsDefined=p;function c(n){return typeof n=="function"}e.isFunction=c;function o(n,t){const l=Math.min(n.length,t.length);for(let h=0;hfunction(){const _=Array.prototype.slice.call(arguments,0);return t(m,_)};let h={};for(const m of n)h[m]=l(m);return h}e.createProxyObject=r;function i(n){return n===null?void 0:n}e.withNullAsUndefined=i}),define(Q[40],J([0,1,20]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getOrDefault=e.equals=e.mixin=e.cloneAndChange=e.deepFreeze=e.deepClone=void 0;function N(c){if(!c||typeof c!="object"||c instanceof RegExp)return c;const o=Array.isArray(c)?[]:{};return Object.keys(c).forEach(s=>{c[s]&&typeof c[s]=="object"?o[s]=N(c[s]):o[s]=c[s]}),o}e.deepClone=N;function M(c){if(!c||typeof c!="object")return c;const o=[c];for(;o.length>0;){const s=o.shift();Object.freeze(s);for(const a in s)if(w.call(s,a)){const u=s[a];typeof u=="object"&&!Object.isFrozen(u)&&o.push(u)}}return c}e.deepFreeze=M;const w=Object.prototype.hasOwnProperty;function S(c,o){return C(c,o,new Set)}e.cloneAndChange=S;function C(c,o,s){if(b.isUndefinedOrNull(c))return c;const a=o(c);if(typeof a!="undefined")return a;if(b.isArray(c)){const u=[];for(const r of c)u.push(C(r,o,s));return u}if(b.isObject(c)){if(s.has(c))throw new Error("Cannot clone recursive data-structure");s.add(c);const u={};for(let r in c)w.call(c,r)&&(u[r]=C(c[r],o,s));return s.delete(c),u}return c}function d(c,o,s=!0){return b.isObject(c)?(b.isObject(o)&&Object.keys(o).forEach(a=>{a in c?s&&(b.isObject(c[a])&&b.isObject(o[a])?d(c[a],o[a],s):c[a]=o[a]):c[a]=o[a]}),c):o}e.mixin=d;function g(c,o){if(c===o)return!0;if(c==null||o===null||o===void 0||typeof c!=typeof o||typeof c!="object"||Array.isArray(c)!==Array.isArray(o))return!1;let s,a;if(Array.isArray(c)){if(c.length!==o.length)return!1;for(s=0;s255?255:M|0}e.toUint8=b;function N(M){return M<0?0:M>4294967295?4294967295:M|0}e.toUint32=N}),define(Q[24],J([0,1,17,72]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.uriToFsPath=e.URI=void 0;const M=/^\w[\w\d+.-]*$/,w=/^\//,S=/^\/\//;function C(f,v){if(!f.scheme&&v)throw new Error(`[UriError]: Scheme is missing: {scheme: "", authority: "${f.authority}", path: "${f.path}", query: "${f.query}", fragment: "${f.fragment}"}`);if(f.scheme&&!M.test(f.scheme))throw new Error("[UriError]: Scheme contains illegal characters.");if(f.path){if(f.authority){if(!w.test(f.path))throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character')}else if(S.test(f.path))throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")')}}function d(f,v){return!f&&!v?"file":f}function g(f,v){switch(f){case"https":case"http":case"file":v?v[0]!==c&&(v=c+v):v=c;break}return v}const p="",c="/",o=/^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/;class s{constructor(v,y,L,I,k,E=!1){typeof v=="object"?(this.scheme=v.scheme||p,this.authority=v.authority||p,this.path=v.path||p,this.query=v.query||p,this.fragment=v.fragment||p):(this.scheme=d(v,E),this.authority=y||p,this.path=g(this.scheme,L||p),this.query=I||p,this.fragment=k||p,C(this,E))}static isUri(v){return v instanceof s?!0:v?typeof v.authority=="string"&&typeof v.fragment=="string"&&typeof v.path=="string"&&typeof v.query=="string"&&typeof v.scheme=="string"&&typeof v.fsPath=="string"&&typeof v.with=="function"&&typeof v.toString=="function":!1}get fsPath(){return t(this,!1)}with(v){if(!v)return this;let{scheme:y,authority:L,path:I,query:k,fragment:E}=v;return y===void 0?y=this.scheme:y===null&&(y=p),L===void 0?L=this.authority:L===null&&(L=p),I===void 0?I=this.path:I===null&&(I=p),k===void 0?k=this.query:k===null&&(k=p),E===void 0?E=this.fragment:E===null&&(E=p),y===this.scheme&&L===this.authority&&I===this.path&&k===this.query&&E===this.fragment?this:new u(y,L,I,k,E)}static parse(v,y=!1){const L=o.exec(v);return L?new u(L[2]||p,_(L[4]||p),_(L[5]||p),_(L[7]||p),_(L[9]||p),y):new u(p,p,p,p,p)}static file(v){let y=p;if(b.isWindows&&(v=v.replace(/\\/g,c)),v[0]===c&&v[1]===c){const L=v.indexOf(c,2);L===-1?(y=v.substring(2),v=c):(y=v.substring(2,L),v=v.substring(L)||c)}return new u("file",y,v,p,p)}static from(v){return new u(v.scheme,v.authority,v.path,v.query,v.fragment)}static joinPath(v,...y){if(!v.path)throw new Error("[UriError]: cannot call joinPath on URI without path");let L;return b.isWindows&&v.scheme==="file"?L=s.file(N.win32.join(t(v,!0),...y)).path:L=N.posix.join(v.path,...y),v.with({path:L})}toString(v=!1){return l(this,v)}toJSON(){return this}static revive(v){if(v){if(v instanceof s)return v;{const y=new u(v);return y._formatted=v.external,y._fsPath=v._sep===a?v.fsPath:null,y}}else return v}}e.URI=s;const a=b.isWindows?1:void 0;class u extends s{constructor(){super(...arguments);this._formatted=null,this._fsPath=null}get fsPath(){return this._fsPath||(this._fsPath=t(this,!1)),this._fsPath}toString(v=!1){return v?l(this,!0):(this._formatted||(this._formatted=l(this,!1)),this._formatted)}toJSON(){const v={$mid:1};return this._fsPath&&(v.fsPath=this._fsPath,v._sep=a),this._formatted&&(v.external=this._formatted),this.path&&(v.path=this.path),this.scheme&&(v.scheme=this.scheme),this.authority&&(v.authority=this.authority),this.query&&(v.query=this.query),this.fragment&&(v.fragment=this.fragment),v}}const r={[58]:"%3A",[47]:"%2F",[63]:"%3F",[35]:"%23",[91]:"%5B",[93]:"%5D",[64]:"%40",[33]:"%21",[36]:"%24",[38]:"%26",[39]:"%27",[40]:"%28",[41]:"%29",[42]:"%2A",[43]:"%2B",[44]:"%2C",[59]:"%3B",[61]:"%3D",[32]:"%20"};function i(f,v){let y,L=-1;for(let I=0;I=97&&k<=122||k>=65&&k<=90||k>=48&&k<=57||k===45||k===46||k===95||k===126||v&&k===47)L!==-1&&(y+=encodeURIComponent(f.substring(L,I)),L=-1),y!==void 0&&(y+=f.charAt(I));else{y===void 0&&(y=f.substr(0,I));const E=r[k];E!==void 0?(L!==-1&&(y+=encodeURIComponent(f.substring(L,I)),L=-1),y+=E):L===-1&&(L=I)}}return L!==-1&&(y+=encodeURIComponent(f.substring(L))),y!==void 0?y:f}function n(f){let v;for(let y=0;y1&&f.scheme==="file"?y=`//${f.authority}${f.path}`:f.path.charCodeAt(0)===47&&(f.path.charCodeAt(1)>=65&&f.path.charCodeAt(1)<=90||f.path.charCodeAt(1)>=97&&f.path.charCodeAt(1)<=122)&&f.path.charCodeAt(2)===58?v?y=f.path.substr(1):y=f.path[1].toLowerCase()+f.path.substr(2):y=f.path,b.isWindows&&(y=y.replace(/\//g,"\\")),y}e.uriToFsPath=t;function l(f,v){const y=v?n:i;let L="",{scheme:I,authority:k,path:E,query:T,fragment:O}=f;if(I&&(L+=I,L+=":"),(k||I==="file")&&(L+=c,L+=c),k){let A=k.indexOf("@");if(A!==-1){const B=k.substr(0,A);k=k.substr(A+1),A=B.indexOf(":"),A===-1?L+=y(B,!1):(L+=y(B.substr(0,A),!1),L+=":",L+=y(B.substr(A+1),!1)),L+="@"}k=k.toLowerCase(),A=k.indexOf(":"),A===-1?L+=y(k,!1):(L+=y(k.substr(0,A),!1),L+=k.substr(A))}if(E){if(E.length>=3&&E.charCodeAt(0)===47&&E.charCodeAt(2)===58){const A=E.charCodeAt(1);A>=65&&A<=90&&(E=`/${String.fromCharCode(A+32)}:${E.substr(3)}`)}else if(E.length>=2&&E.charCodeAt(1)===58){const A=E.charCodeAt(0);A>=65&&A<=90&&(E=`${String.fromCharCode(A+32)}:${E.substr(2)}`)}L+=y(E,!0)}return T&&(L+="?",L+=y(T,!1)),O&&(L+="#",L+=v?O:i(O,!1)),L}function h(f){try{return decodeURIComponent(f)}catch(v){return f.length>3?f.substr(0,3)+h(f.substr(3)):f}}const m=/(%[0-9A-Za-z][0-9A-Za-z])+/g;function _(f){return f.match(m)?f.replace(m,v=>h(v)):f}}),define(Q[51],J([0,1,24,8]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LRUCache=e.LinkedMap=e.ResourceMap=e.TernarySearchTree=e.UriIterator=e.PathIterator=e.ConfigKeysIterator=e.StringIterator=void 0;class M{constructor(){this._value="",this._pos=0}reset(a){return this._value=a,this._pos=0,this}next(){return this._pos+=1,this}hasNext(){return this._pos!1){return new g(new C(a))}static forStrings(){return new g(new M)}static forConfigKeys(){return new g(new w)}clear(){this._root=void 0}set(a,u){const r=this._iter.reset(a);let i;for(this._root||(this._root=new d,this._root.segment=r.value()),i=this._root;;){const t=r.cmp(i.segment);if(t>0)i.left||(i.left=new d,i.left.segment=r.value()),i=i.left;else if(t<0)i.right||(i.right=new d,i.right.segment=r.value()),i=i.right;else if(r.hasNext())r.next(),i.mid||(i.mid=new d,i.mid.segment=r.value()),i=i.mid;else break}const n=i.value;return i.value=u,i.key=a,n}get(a){var u;return(u=this._getNode(a))===null||u===void 0?void 0:u.value}_getNode(a){const u=this._iter.reset(a);let r=this._root;for(;r;){const i=u.cmp(r.segment);if(i>0)r=r.left;else if(i<0)r=r.right;else if(u.hasNext())u.next(),r=r.mid;else break}return r}has(a){const u=this._getNode(a);return!((u==null?void 0:u.value)===void 0&&(u==null?void 0:u.mid)===void 0)}delete(a){return this._delete(a,!1)}deleteSuperstr(a){return this._delete(a,!0)}_delete(a,u){const r=this._iter.reset(a),i=[];let n=this._root;for(;n;){const t=r.cmp(n.segment);if(t>0)i.push([1,n]),n=n.left;else if(t<0)i.push([-1,n]),n=n.right;else if(r.hasNext())r.next(),i.push([0,n]),n=n.mid;else{for(u?(n.left=void 0,n.mid=void 0,n.right=void 0):n.value=void 0;i.length>0&&n.isEmpty();){let[l,h]=i.pop();switch(l){case 1:h.left=void 0;break;case 0:h.mid=void 0;break;case-1:h.right=void 0;break}n=h}break}}}findSubstr(a){const u=this._iter.reset(a);let r=this._root,i;for(;r;){const n=u.cmp(r.segment);if(n>0)r=r.left;else if(n<0)r=r.right;else if(u.hasNext())u.next(),i=r.value||i,r=r.mid;else break}return r&&r.value||i}findSuperstr(a){const u=this._iter.reset(a);let r=this._root;for(;r;){const i=u.cmp(r.segment);if(i>0)r=r.left;else if(i<0)r=r.right;else if(u.hasNext())u.next(),r=r.mid;else return r.mid?this._entries(r.mid):void 0}}forEach(a){for(const[u,r]of this)a(r,u)}*[Symbol.iterator](){yield*this._entries(this._root)}*_entries(a){a&&(yield*this._entries(a.left),a.value&&(yield[a.key,a.value]),yield*this._entries(a.mid),yield*this._entries(a.right))}}e.TernarySearchTree=g;class p{constructor(a,u){this[Symbol.toStringTag]="ResourceMap",a instanceof p?(this.map=new Map(a.map),this.toKey=u!=null?u:p.defaultToKey):(this.map=new Map,this.toKey=a!=null?a:p.defaultToKey)}set(a,u){return this.map.set(this.toKey(a),u),this}get(a){return this.map.get(this.toKey(a))}has(a){return this.map.has(this.toKey(a))}get size(){return this.map.size}clear(){this.map.clear()}delete(a){return this.map.delete(this.toKey(a))}forEach(a,u){typeof u!="undefined"&&(a=a.bind(u));for(let[r,i]of this.map)a(i,b.URI.parse(r),this)}values(){return this.map.values()}*keys(){for(let a of this.map.keys())yield b.URI.parse(a)}*entries(){for(let a of this.map.entries())yield[b.URI.parse(a[0]),a[1]]}*[Symbol.iterator](){for(let a of this.map)yield[b.URI.parse(a[0]),a[1]]}}e.ResourceMap=p,p.defaultToKey=s=>s.toString();class c{constructor(){this[Symbol.toStringTag]="LinkedMap",this._map=new Map,this._head=void 0,this._tail=void 0,this._size=0,this._state=0}clear(){this._map.clear(),this._head=void 0,this._tail=void 0,this._size=0,this._state++}isEmpty(){return!this._head&&!this._tail}get size(){return this._size}get first(){var a;return(a=this._head)===null||a===void 0?void 0:a.value}get last(){var a;return(a=this._tail)===null||a===void 0?void 0:a.value}has(a){return this._map.has(a)}get(a,u=0){const r=this._map.get(a);if(!!r)return u!==0&&this.touch(r,u),r.value}set(a,u,r=0){let i=this._map.get(a);if(i)i.value=u,r!==0&&this.touch(i,r);else{switch(i={key:a,value:u,next:void 0,previous:void 0},r){case 0:this.addItemLast(i);break;case 1:this.addItemFirst(i);break;case 2:this.addItemLast(i);break;default:this.addItemLast(i);break}this._map.set(a,i),this._size++}return this}delete(a){return!!this.remove(a)}remove(a){const u=this._map.get(a);if(!!u)return this._map.delete(a),this.removeItem(u),this._size--,u.value}shift(){if(!(!this._head&&!this._tail)){if(!this._head||!this._tail)throw new Error("Invalid list");const a=this._head;return this._map.delete(a.key),this.removeItem(a),this._size--,a.value}}forEach(a,u){const r=this._state;let i=this._head;for(;i;){if(u?a.bind(u)(i.value,i.key,this):a(i.value,i.key,this),this._state!==r)throw new Error("LinkedMap got modified during iteration.");i=i.next}}keys(){const a=this,u=this._state;let r=this._head;const i={[Symbol.iterator](){return i},next(){if(a._state!==u)throw new Error("LinkedMap got modified during iteration.");if(r){const n={value:r.key,done:!1};return r=r.next,n}else return{value:void 0,done:!0}}};return i}values(){const a=this,u=this._state;let r=this._head;const i={[Symbol.iterator](){return i},next(){if(a._state!==u)throw new Error("LinkedMap got modified during iteration.");if(r){const n={value:r.value,done:!1};return r=r.next,n}else return{value:void 0,done:!0}}};return i}entries(){const a=this,u=this._state;let r=this._head;const i={[Symbol.iterator](){return i},next(){if(a._state!==u)throw new Error("LinkedMap got modified during iteration.");if(r){const n={value:[r.key,r.value],done:!1};return r=r.next,n}else return{value:void 0,done:!0}}};return i}[Symbol.iterator](){return this.entries()}trimOld(a){if(!(a>=this.size)){if(a===0){this.clear();return}let u=this._head,r=this.size;for(;u&&r>a;)this._map.delete(u.key),u=u.next,r--;this._head=u,this._size=r,u&&(u.previous=void 0),this._state++}}addItemFirst(a){if(!this._head&&!this._tail)this._tail=a;else if(this._head)a.next=this._head,this._head.previous=a;else throw new Error("Invalid list");this._head=a,this._state++}addItemLast(a){if(!this._head&&!this._tail)this._head=a;else if(this._tail)a.previous=this._tail,this._tail.next=a;else throw new Error("Invalid list");this._tail=a,this._state++}removeItem(a){if(a===this._head&&a===this._tail)this._head=void 0,this._tail=void 0;else if(a===this._head){if(!a.next)throw new Error("Invalid list");a.next.previous=void 0,this._head=a.next}else if(a===this._tail){if(!a.previous)throw new Error("Invalid list");a.previous.next=void 0,this._tail=a.previous}else{const u=a.next,r=a.previous;if(!u||!r)throw new Error("Invalid list");u.previous=r,r.next=u}a.next=void 0,a.previous=void 0,this._state++}touch(a,u){if(!this._head||!this._tail)throw new Error("Invalid list");if(!(u!==1&&u!==2)){if(u===1){if(a===this._head)return;const r=a.next,i=a.previous;a===this._tail?(i.next=void 0,this._tail=i):(r.previous=i,i.next=r),a.previous=void 0,a.next=this._head,this._head.previous=a,this._head=a,this._state++}else if(u===2){if(a===this._tail)return;const r=a.next,i=a.previous;a===this._head?(r.previous=void 0,this._head=r):(r.previous=i,i.next=r),a.next=void 0,a.previous=this._tail,this._tail.next=a,this._tail=a,this._state++}}}toJSON(){const a=[];return this.forEach((u,r)=>{a.push([r,u])}),a}fromJSON(a){this.clear();for(const[u,r]of a)this.set(u,r)}}e.LinkedMap=c;class o extends c{constructor(a,u=1){super();this._limit=a,this._ratio=Math.min(Math.max(0,u),1)}get limit(){return this._limit}set limit(a){this._limit=a,this.checkTrim()}get(a,u=2){return super.get(a,u)}peek(a){return super.get(a,0)}set(a,u){return super.set(a,u,2),this.checkTrim(),this}checkTrim(){this.size>this._limit&&this.trimOld(Math.round(this._limit*this._ratio))}}e.LRUCache=o}),define(Q[66],J([0,1,51,8]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.fuzzyScoreGracefulAggressive=e.fuzzyScore=e.FuzzyScore=e.isPatternInWord=e.createMatches=e.anyScore=e.matchesFuzzy=e.matchesWords=e.matchesCamelCase=e.isUpper=e.matchesSubString=e.matchesContiguousSubString=e.matchesPrefix=e.or=void 0;function M(...G){return function(j,te){for(let Z=0,ue=G.length;Z0?[{start:0,end:j.length}]:[]:null}function S(G,j){const te=j.toLowerCase().indexOf(G.toLowerCase());return te===-1?null:[{start:te,end:te+G.length}]}e.matchesContiguousSubString=S;function C(G,j){return d(G.toLowerCase(),j.toLowerCase(),0,0)}e.matchesSubString=C;function d(G,j,te,Z){if(te===G.length)return[];if(Z===j.length)return null;if(G[te]===j[Z]){let ue=null;return(ue=d(G,j,te+1,Z+1))?i({start:Z,end:Z+1},ue):null}return d(G,j,te,Z+1)}function g(G){return 97<=G&&G<=122}function p(G){return 65<=G&&G<=90}e.isUpper=p;function c(G){return 48<=G&&G<=57}function o(G){return G===32||G===9||G===10||G===13}const s=new Set;"`~!@#$%^&*()-=+[{]}\\|;:'\",.<>/?".split("").forEach(G=>s.add(G.charCodeAt(0)));function a(G){return o(G)||s.has(G)}function u(G,j){return G===j||a(G)&&a(j)}function r(G){return g(G)||p(G)||c(G)}function i(G,j){return j.length===0?j=[G]:G.end===j[0].start?j[0].start=G.start:j.unshift(G),j}function n(G,j){for(let te=j;te0&&!r(G.charCodeAt(te-1)))return te}return G.length}function t(G,j,te,Z){if(te===G.length)return[];if(Z===j.length)return null;if(G[te]!==j[Z].toLowerCase())return null;{let ue=null,he=Z+1;for(ue=t(G,j,te+1,Z+1);!ue&&(he=n(j,he)).6}function m(G){const{upperPercent:j,lowerPercent:te,alphaPercent:Z,numericPercent:ue}=G;return te>.2&&j<.8&&Z>.6&&ue<.2}function _(G){let j=0,te=0,Z=0,ue=0;for(let he=0;he60)return null;const te=l(j);if(!m(te)){if(!h(te))return null;j=j.toLowerCase()}let Z=null,ue=0;for(G=G.toLowerCase();ue0&&a(G.charCodeAt(te-1)))return te;return G.length}const I=M(e.matchesPrefix,f,S),k=M(e.matchesPrefix,f,C),E=new b.LRUCache(1e4);function T(G,j,te=!1){if(typeof G!="string"||typeof j!="string")return null;let Z=E.get(G);Z||(Z=new RegExp(N.convertSimple2RegExpPattern(G),"i"),E.set(G,Z));const ue=Z.exec(j);return ue?[{start:ue.index,end:ue.index+ue[0].length}]:te?k(G,j):I(G,j)}e.matchesFuzzy=T;function O(G,j,te,Z,ue,he){const re=U(G,j,0,Z,ue,0,!0);if(re)return re;let ce=[],me=0,Ce=he;for(let be=0;be=0)me+=1,ce.unshift(Le),Ce=Le+1;else if(ce.length>0)break}return[me,he,...ce]}e.anyScore=O;function A(G){if(typeof G=="undefined")return[];const j=[],te=G[1];for(let Z=G.length-1;Z>1;Z--){const ue=G[Z]+te,he=j[j.length-1];he&&he.end===ue?he.end=ue+1:j.push({start:ue,end:ue+1})}return j}e.createMatches=A;const B=128;function F(){const G=[],j=[];for(let te=0;te<=B;te++)j[te]=0;for(let te=0;te<=B;te++)G.push(j.slice(0));return G}function D(G){const j=[];for(let te=0;te<=G;te++)j[te]=0;return j}const R=D(2*B),W=D(2*B),x=F(),K=F(),Y=F(),ee=!1;function se(G,j,te,Z,ue){function he(ce,me,Ce=" "){for(;ce.lengthhe(ce,3)).join("|")} +`;for(let ce=0;ce<=te;ce++)ce===0?re+=" |":re+=`${j[ce-1]}|`,re+=G[ce].slice(0,ue+1).map(me=>he(me.toString(),3)).join("|")+` +`;return re}function ne(G,j,te,Z){G=G.substr(j),te=te.substr(Z),console.log(se(K,G,G.length,te,te.length)),console.log(se(Y,G,G.length,te,te.length)),console.log(se(x,G,G.length,te,te.length))}function le(G,j){if(j<0||j>=G.length)return!1;const te=G.codePointAt(j);switch(te){case 95:case 45:case 46:case 32:case 47:case 92:case 39:case 34:case 58:case 36:case 60:case 40:case 91:return!0;case void 0:return!1;default:return!!N.isEmojiImprecise(te)}}function X(G,j){if(j<0||j>=G.length)return!1;switch(G.charCodeAt(j)){case 32:case 9:return!0;default:return!1}}function z(G,j,te){return j[G]!==te[G]}function P(G,j,te,Z,ue,he,re=!1){for(;jB?B:G.length,me=Z.length>B?B:Z.length;if(!(te>=ce||he>=me||ce-te>me-he)&&!!P(j,te,ce,ue,he,me,!0)){H(ce,me,te,he,j,ue);let Ce=1,be=1,Le=te,De=he;const Re=[!1];for(Ce=1,Le=te;Leye,Ne=ke?K[Ce][be-1]+(x[Ce][be-1]>0?-5:0):0,Te=De>ye+1&&x[Ce][be-1]>0,Oe=Te?K[Ce][be-2]+(x[Ce][be-2]>0?-5:0):0;if(Te&&(!ke||Oe>=Ne)&&(!pe||Oe>=ve))K[Ce][be]=Oe,Y[Ce][be]=3,x[Ce][be]=0;else if(ke&&(!pe||Ne>=ve))K[Ce][be]=Ne,Y[Ce][be]=2,x[Ce][be]=0;else if(pe)K[Ce][be]=ve,Y[Ce][be]=1,x[Ce][be]=x[Ce-1][be-1]+1;else throw new Error("not possible")}}if(ee&&ne(G,te,Z,he),!(!Re[0]&&!re)){Ce--,be--;const Ee=[K[Ce][be],he];let Ae=0,Se=0;for(;Ce>=1;){let ye=be;do{const fe=Y[Ce][ye];if(fe===3)ye=ye-2;else if(fe===2)ye=ye-1;else break}while(ye>=1);Ae>1&&j[te+Ce-1]===ue[he+be-1]&&!z(ye+he-1,Z,ue)&&Ae+1>x[Ce][ye]&&(ye=be),ye===be?Ae++:Ae=1,Se||(Se=ye),Ce--,be=ye-1,Ee.push(be)}me===ce&&(Ee[0]+=2);const we=Se-ce;return Ee[0]-=we,Ee}}}e.fuzzyScore=U;function H(G,j,te,Z,ue,he){let re=G-1,ce=j-1;for(;re>=te&&ce>=Z;)ue[re]===he[ce]&&(W[re]=ce,re--),ce--}function $(G,j,te,Z,ue,he,re,ce,me,Ce,be){if(j[te]!==he[re])return Number.MIN_SAFE_INTEGER;let Le=1,De=!1;return re===te-Z?Le=G[te]===ue[re]?7:5:z(re,ue,he)&&(re===0||!z(re-1,ue,he))?(Le=G[te]===ue[re]?7:5,De=!0):le(he,re)&&(re===0||!le(he,re-1))?Le=5:(le(he,re-1)||X(he,re-1))&&(Le=5,De=!0),Le>1&&te===Z&&(be[0]=!0),De||(De=z(re,ue,he)||le(he,re-1)||X(he,re-1)),te===Z?re>me&&(Le-=De?3:5):Ce?Le+=De?2:0:Le+=De?0:1,re+1===ce&&(Le-=De?3:5),Le}function ie(G,j,te,Z,ue,he,re){return oe(G,j,te,Z,ue,he,!0,re)}e.fuzzyScoreGracefulAggressive=ie;function oe(G,j,te,Z,ue,he,re,ce){let me=U(G,j,te,Z,ue,he,ce);if(me&&!re)return me;if(G.length>=3){const Ce=Math.min(7,G.length-1);for(let be=te+1;beme[0])&&(me=De))}}}return me}function ae(G,j){if(!(j+1>=G.length)){const te=G[j],Z=G[j+1];if(te!==Z)return G.slice(0,j)+Z+te+G.slice(j+2)}}}),define(Q[286],J([0,1,66,72,17,8]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.pieceToQuery=e.prepareQuery=e.scoreFuzzy2=void 0;const S=[void 0,[]];function C(r,i,n=0,t=0){const l=i;return l.values&&l.values.length>1?d(r,l.values,n,t):g(r,i,n,t)}e.scoreFuzzy2=C;function d(r,i,n,t){let l=0;const h=[];for(const m of i){const[_,f]=g(r,m,n,t);if(typeof _!="number")return S;l+=_,h.push(...f)}return[l,p(h)]}function g(r,i,n,t){const l=b.fuzzyScore(i.original,i.originalLowercase,n,r,r.toLowerCase(),t,!0);return l?[l[0],b.createMatches(l)]:S}function p(r){const i=r.sort((l,h)=>l.start-h.start),n=[];let t;for(const l of i)!t||!c(t,l)?(t=l,n.push(l)):(t.start=Math.min(t.start,l.start),t.end=Math.max(t.end,l.end));return n}function c(r,i){return!(r.end=0;let m;const _=r.split(o);if(_.length>1)for(const f of _){const{pathNormalized:v,normalized:y,normalizedLowercase:L}=a(f);y&&(m||(m=[]),m.push({original:f,originalLowercase:f.toLowerCase(),pathNormalized:v,normalized:y,normalizedLowercase:L}))}return{original:r,originalLowercase:i,pathNormalized:n,normalized:t,normalizedLowercase:l,values:m,containsPathSeparator:h}}e.prepareQuery=s;function a(r){let i;M.isWindows?i=r.replace(/\//g,N.sep):i=r.replace(/\\/g,N.sep);const n=w.stripWildcards(i).replace(/\s/g,"");return{pathNormalized:i,normalized:n,normalizedLowercase:n.toLowerCase()}}function u(r){return Array.isArray(r)?s(r.map(i=>i.original).join(o)):s(r.original)}e.pieceToQuery=u}),define(Q[198],J([0,1,8,152,72,51,15]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.isRelativePattern=e.parse=e.match=e.splitGlobAware=void 0;const C="**",d="/",g="[/\\\\]",p="[^/\\\\]",c=/\//g;function o(R){switch(R){case 0:return"";case 1:return`${p}*?`;default:return`(?:${g}|${p}+${g}|${g}${p}+)*?`}}function s(R,W){if(!R)return[];const x=[];let K=!1,Y=!1,ee="";for(const se of R){switch(se){case W:if(!K&&!Y){x.push(ee),ee="";continue}break;case"{":K=!0;break;case"}":K=!1;break;case"[":Y=!0;break;case"]":Y=!1;break}ee+=se}return ee&&x.push(ee),x}e.splitGlobAware=s;function a(R){if(!R)return"";let W="";const x=s(R,d);if(x.every(K=>K===C))W=".*";else{let K=!1;x.forEach((Y,ee)=>{if(Y===C){K||(W+=o(2),K=!0);return}let se=!1,ne="",le=!1,X="";for(const z of Y){if(z!=="}"&&se){ne+=z;continue}if(le&&(z!=="]"||!X)){let P;z==="-"?P=z:(z==="^"||z==="!")&&!X?P="^":z===d?P="":P=b.escapeRegExpCharacters(z),X+=P;continue}switch(z){case"{":se=!0;continue;case"[":le=!0;continue;case"}":W+=`(?:${s(ne,",").map(U=>a(U)).join("|")})`,se=!1,ne="";break;case"]":W+="["+X+"]",le=!1,X="";break;case"?":W+=p;continue;case"*":W+=o(1);continue;default:W+=b.escapeRegExpCharacters(z)}}eef(ne,W)).filter(ne=>ne!==_),R),K=x.length;if(!K)return _;if(K===1)return x[0];const Y=function(ne,le){for(let X=0,z=x.length;X!!ne.allBasenames);ee&&(Y.allBasenames=ee.allBasenames);const se=x.reduce((ne,le)=>le.allPaths?ne.concat(le.allPaths):ne,[]);return se.length&&(Y.allPaths=se),Y}function k(R,W,x){const K=M.sep===M.posix.sep,Y=K?R:R.replace(c,M.sep),ee=M.sep+Y,se=M.posix.sep+R,ne=x?function(le,X){return typeof le=="string"&&(le===Y||le.endsWith(ee)||!K&&(le===R||le.endsWith(se)))?W:null}:function(le,X){return typeof le=="string"&&(le===Y||!K&&le===R)?W:null};return ne.allPaths=[(x?"*/":"./")+R],ne}function E(R){try{const W=new RegExp(`^${a(R)}$`);return function(x){return W.lastIndex=0,typeof x=="string"&&W.test(x)?R:null}}catch(W){return _}}function T(R,W,x){return!R||typeof W!="string"?!1:O(R)(W,void 0,x)}e.match=T;function O(R,W={}){if(!R)return m;if(typeof R=="string"||A(R)){const x=f(R,W);if(x===_)return m;const K=function(Y,ee){return!!x(Y,ee)};return x.allBasenames&&(K.allBasenames=x.allBasenames),x.allPaths&&(K.allPaths=x.allPaths),K}return B(R,W)}e.parse=O;function A(R){const W=R;return W&&typeof W.base=="string"&&typeof W.pattern=="string"}e.isRelativePattern=A;function B(R,W){const x=D(Object.getOwnPropertyNames(R).map(ne=>F(ne,R[ne],W)).filter(ne=>ne!==_)),K=x.length;if(!K)return _;if(!x.some(ne=>!!ne.requiresSiblings)){if(K===1)return x[0];const ne=function(z,P){for(let V=0,U=x.length;V!!z.allBasenames);le&&(ne.allBasenames=le.allBasenames);const X=x.reduce((z,P)=>P.allPaths?z.concat(P.allPaths):z,[]);return X.length&&(ne.allPaths=X),ne}const Y=function(ne,le,X){let z;for(let P=0,V=x.length;P!!ne.allBasenames);ee&&(Y.allBasenames=ee.allBasenames);const se=x.reduce((ne,le)=>le.allPaths?ne.concat(le.allPaths):ne,[]);return se.length&&(Y.allPaths=se),Y}function F(R,W,x){if(W===!1)return _;const K=f(R,x);if(K===_)return _;if(typeof W=="boolean")return K;if(W){const Y=W.when;if(typeof Y=="string"){const ee=(se,ne,le,X)=>{if(!X||!K(se,ne))return null;const z=Y.replace("$(basename)",le),P=X(z);return S.isThenable(P)?P.then(V=>V?R:null):P?R:null};return ee.requiresSiblings=!0,ee}}return K}function D(R,W){const x=R.filter(ne=>!!ne.basenames);if(x.length<2)return R;const K=x.reduce((ne,le)=>{const X=le.basenames;return X?ne.concat(X):ne},[]);let Y;if(W){Y=[];for(let ne=0,le=K.length;ne{const X=le.patterns;return X?ne.concat(X):ne},[]);const ee=function(ne,le){if(typeof ne!="string")return null;if(!le){let z;for(z=ne.length;z>0;z--){const P=ne.charCodeAt(z-1);if(P===47||P===92)break}le=ne.substr(z)}const X=K.indexOf(le);return X!==-1?Y[X]:null};ee.basenames=K,ee.patterns=Y,ee.allBasenames=K;const se=R.filter(ne=>!ne.basenames);return se.push(ee),se}}),define(Q[102],J([0,1,27,66,8]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.matchesFuzzyIconAware=e.parseLabelWithIcons=e.stripIcons=e.markdownEscapeEscapedIcons=e.escapeIcons=e.iconStartMarker=void 0,e.iconStartMarker="$(";const w=new RegExp(`\\$\\(${b.CSSIcon.iconNameExpression}(?:${b.CSSIcon.iconModifierExpression})?\\)`,"g"),S=new RegExp(`(\\\\)?${w.source}`,"g");function C(u){return u.replace(S,(r,i)=>i?r:`\\${r}`)}e.escapeIcons=C;const d=new RegExp(`\\\\${w.source}`,"g");function g(u){return u.replace(d,r=>`\\${r}`)}e.markdownEscapeEscapedIcons=g;const p=new RegExp(`(\\s)?(\\\\)?${w.source}(\\s)?`,"g");function c(u){return u.indexOf(e.iconStartMarker)===-1?u:u.replace(p,(r,i,n,t)=>n?r:i||t||"")}e.stripIcons=c;function o(u){const r=u.indexOf(e.iconStartMarker);return r===-1?{text:u}:s(u,r)}e.parseLabelWithIcons=o;function s(u,r){const i=[];let n="";function t(L){if(L){n+=L;for(const I of L)i.push(m)}}let l=-1,h="",m=0,_,f,v=r;const y=u.length;for(t(u.substr(0,r));v" ".repeat(i.length)).replace(/^>/gm,"\\>").replace(/\n/g,u===1?`\\ +`:` + +`),this}appendMarkdown(a){return this.value+=a,this}appendCodeblock(a,u){return this.value+="\n```",this.value+=a,this.value+=` +`,this.value+=u,this.value+="\n```\n",this}}e.MarkdownString=w;function S(s){return C(s)?!s.value:Array.isArray(s)?s.every(S):!0}e.isEmptyMarkdownString=S;function C(s){return s instanceof w?!0:s&&typeof s=="object"?typeof s.value=="string"&&(typeof s.isTrusted=="boolean"||s.isTrusted===void 0)&&(typeof s.supportThemeIcons=="boolean"||s.supportThemeIcons===void 0):!1}e.isMarkdownString=C;function d(s,a){return!s&&!a?!0:!s||!a?!1:Array.isArray(s)&&Array.isArray(a)?b.equals(s,a,g):C(s)&&C(a)?g(s,a):!1}e.markedStringsEquals=d;function g(s,a){return s===a?!0:!s||!a?!1:s.value===a.value&&s.isTrusted===a.isTrusted&&s.supportThemeIcons===a.supportThemeIcons}function p(s){return s.replace(/[\\`*_{}[\]()#+\-.!]/g,"\\$&")}e.escapeMarkdownSyntaxTokens=p;function c(s){return s&&s.replace(/\\([\\`*_{}[\]()#+\-.!])/g,"$1")}e.removeMarkdownEscapes=c;function o(s){const a=[],u=s.split("|").map(i=>i.trim());s=u[0];const r=u[1];if(r){const i=/height=(\d+)/.exec(r),n=/width=(\d+)/.exec(r),t=i?i[1]:"",l=n?n[1]:"",h=isFinite(parseInt(l)),m=isFinite(parseInt(t));h&&a.push(`width="${l}"`),m&&a.push(`height="${t}"`)}return{href:s,dimensions:a}}e.parseHrefAndDimensions=o}),define(Q[199],J([0,1,101,24]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.revive=e.parse=void 0;function M(S){let C=JSON.parse(S);return C=w(C),C}e.parse=M;function w(S,C=0){if(!S||C>200)return S;if(typeof S=="object"){switch(S.$mid){case 1:return N.URI.revive(S);case 2:return new RegExp(S.source,S.flags)}if(S instanceof b.VSBuffer||S instanceof Uint8Array)return S;if(Array.isArray(S))for(let d=0;d{let ve=pe.relatedTarget;for(;ve&&ve!==de;)ve=ve.parentNode;ve!==de&&ge(pe)})}e.addDisposableNonBubblingMouseOutListener=_;function f(de,ge){return r(de,"pointerout",pe=>{let ve=pe.relatedTarget;for(;ve&&ve!==de;)ve=ve.parentNode;ve!==de&&ge(pe)})}e.addDisposableNonBubblingPointerOutListener=f;let v=null;function y(de){if(!v){const ge=pe=>setTimeout(()=>pe(new Date().getTime()),0);v=self.requestAnimationFrame||self.msRequestAnimationFrame||self.webkitRequestAnimationFrame||self.mozRequestAnimationFrame||self.oRequestAnimationFrame||ge}return v.call(self,de)}class L{constructor(ge,pe=0){this._runner=ge,this.priority=pe,this._canceled=!1}dispose(){this._canceled=!0}execute(){if(!this._canceled)try{this._runner()}catch(ge){C.onUnexpectedError(ge)}}static sort(ge,pe){return pe.priority-ge.priority}}(function(){let de=[],ge=null,pe=!1,ve=!1,ke=()=>{for(pe=!1,ge=de,de=[],ve=!0;ge.length>0;)ge.sort(L.sort),ge.shift().execute();ve=!1};e.scheduleAtNextAnimationFrame=(Ne,Te=0)=>{let Oe=new L(Ne,Te);return de.push(Oe),pe||(pe=!0,y(ke)),Oe},e.runAtThisOrScheduleAtNextAnimationFrame=(Ne,Te)=>{if(ve){let Oe=new L(Ne,Te);return ge.push(Oe),Oe}else return e.scheduleAtNextAnimationFrame(Ne,Te)}})();const I=8,k=function(de,ge){return ge};class E extends g.Disposable{constructor(ge,pe,ve,ke=k,Ne=I){super();let Te=null,Oe=0,Fe=this._register(new S.TimeoutTimer),Pe=()=>{Oe=new Date().getTime(),ve(Te),Te=null};this._register(r(ge,pe,xe=>{Te=ke(Te,xe);let We=new Date().getTime()-Oe;We>=Ne?(Fe.cancel(),Pe()):Fe.setIfNotSet(Pe,Ne-We)}))}}function T(de,ge,pe,ve,ke){return new E(de,ge,pe,ve,ke)}e.addDisposableThrottledListener=T;function O(de){return document.defaultView.getComputedStyle(de,null)}e.getComputedStyle=O;function A(de){if(de!==document.body)return new F(de.clientWidth,de.clientHeight);if(p.isIOS&&window.visualViewport){const ge=window.visualViewport.width,pe=window.visualViewport.height-(b.isStandalone?20+4:0);return new F(ge,pe)}if(window.innerWidth&&window.innerHeight)return new F(window.innerWidth,window.innerHeight);if(document.body&&document.body.clientWidth&&document.body.clientHeight)return new F(document.body.clientWidth,document.body.clientHeight);if(document.documentElement&&document.documentElement.clientWidth&&document.documentElement.clientHeight)return new F(document.documentElement.clientWidth,document.documentElement.clientHeight);throw new Error("Unable to figure out browser width and height")}e.getClientArea=A;class B{static convertToPixels(ge,pe){return parseFloat(pe)||0}static getDimension(ge,pe,ve){let ke=O(ge),Ne="0";return ke&&(ke.getPropertyValue?Ne=ke.getPropertyValue(pe):Ne=ke.getAttribute(ve)),B.convertToPixels(ge,Ne)}static getBorderLeftWidth(ge){return B.getDimension(ge,"border-left-width","borderLeftWidth")}static getBorderRightWidth(ge){return B.getDimension(ge,"border-right-width","borderRightWidth")}static getBorderTopWidth(ge){return B.getDimension(ge,"border-top-width","borderTopWidth")}static getBorderBottomWidth(ge){return B.getDimension(ge,"border-bottom-width","borderBottomWidth")}static getPaddingLeft(ge){return B.getDimension(ge,"padding-left","paddingLeft")}static getPaddingRight(ge){return B.getDimension(ge,"padding-right","paddingRight")}static getPaddingTop(ge){return B.getDimension(ge,"padding-top","paddingTop")}static getPaddingBottom(ge){return B.getDimension(ge,"padding-bottom","paddingBottom")}static getMarginLeft(ge){return B.getDimension(ge,"margin-left","marginLeft")}static getMarginTop(ge){return B.getDimension(ge,"margin-top","marginTop")}static getMarginRight(ge){return B.getDimension(ge,"margin-right","marginRight")}static getMarginBottom(ge){return B.getDimension(ge,"margin-bottom","marginBottom")}}class F{constructor(ge,pe){this.width=ge,this.height=pe}with(ge=this.width,pe=this.height){return ge!==this.width||pe!==this.height?new F(ge,pe):this}static is(ge){return typeof ge=="object"&&typeof ge.height=="number"&&typeof ge.width=="number"}static lift(ge){return ge instanceof F?ge:new F(ge.width,ge.height)}static equals(ge,pe){return ge===pe?!0:!ge||!pe?!1:ge.width===pe.width&&ge.height===pe.height}}e.Dimension=F;function D(de){let ge=de.offsetParent,pe=de.offsetTop,ve=de.offsetLeft;for(;(de=de.parentNode)!==null&&de!==document.body&&de!==document.documentElement;){pe-=de.scrollTop;const ke=X(de)?null:O(de);ke&&(ve-=ke.direction!=="rtl"?de.scrollLeft:-de.scrollLeft),de===ge&&(ve+=B.getBorderLeftWidth(de),pe+=B.getBorderTopWidth(de),pe+=de.offsetTop,ve+=de.offsetLeft,ge=de.offsetParent)}return{left:ve,top:pe}}e.getTopLeftOffset=D;function R(de,ge,pe){typeof ge=="number"&&(de.style.width=`${ge}px`),typeof pe=="number"&&(de.style.height=`${pe}px`)}e.size=R;function W(de){let ge=de.getBoundingClientRect();return{left:ge.left+e.StandardWindow.scrollX,top:ge.top+e.StandardWindow.scrollY,width:ge.width,height:ge.height}}e.getDomNodePagePosition=W,e.StandardWindow=new class{get scrollX(){return typeof window.scrollX=="number"?window.scrollX:document.body.scrollLeft+document.documentElement.scrollLeft}get scrollY(){return typeof window.scrollY=="number"?window.scrollY:document.body.scrollTop+document.documentElement.scrollTop}};function x(de){let ge=B.getMarginLeft(de)+B.getMarginRight(de);return de.offsetWidth+ge}e.getTotalWidth=x;function K(de){let ge=B.getBorderLeftWidth(de)+B.getBorderRightWidth(de),pe=B.getPaddingLeft(de)+B.getPaddingRight(de);return de.offsetWidth-ge-pe}e.getContentWidth=K;function Y(de){let ge=B.getBorderTopWidth(de)+B.getBorderBottomWidth(de),pe=B.getPaddingTop(de)+B.getPaddingBottom(de);return de.offsetHeight-ge-pe}e.getContentHeight=Y;function ee(de){let ge=B.getMarginTop(de)+B.getMarginBottom(de);return de.offsetHeight+ge}e.getTotalHeight=ee;function se(de,ge){for(;de;){if(de===ge)return!0;de=de.parentNode}return!1}e.isAncestor=se;function ne(de,ge,pe){for(;de&&de.nodeType===de.ELEMENT_NODE;){if(de.classList.contains(ge))return de;if(pe){if(typeof pe=="string"){if(de.classList.contains(pe))return null}else if(de===pe)return null}de=de.parentNode}return null}e.findParentWithClass=ne;function le(de,ge,pe){return!!ne(de,ge,pe)}e.hasParentWithClass=le;function X(de){return de&&!!de.host&&!!de.mode}e.isShadowRoot=X;function z(de){return!!P(de)}e.isInShadowDOM=z;function P(de){for(;de.parentNode;){if(de===document.body)return null;de=de.parentNode}return X(de)?de:null}e.getShadowRoot=P;function V(){let de=document.activeElement;for(;de==null?void 0:de.shadowRoot;)de=de.shadowRoot.activeElement;return de}e.getActiveElement=V;function U(de=document.getElementsByTagName("head")[0]){let ge=document.createElement("style");return ge.type="text/css",ge.media="screen",de.appendChild(ge),ge}e.createStyleSheet=U;let H=null;function $(){return H||(H=U()),H}function ie(de){var ge,pe;return((ge=de==null?void 0:de.sheet)===null||ge===void 0?void 0:ge.rules)?de.sheet.rules:((pe=de==null?void 0:de.sheet)===null||pe===void 0?void 0:pe.cssRules)?de.sheet.cssRules:[]}function oe(de,ge,pe=$()){!pe||!ge||pe.sheet.insertRule(de+"{"+ge+"}",0)}e.createCSSRule=oe;function ae(de,ge=$()){if(!!ge){let pe=ie(ge),ve=[];for(let ke=0;ke=0;ke--)ge.sheet.deleteRule(ve[ke])}}e.removeCSSRulesContainingSelector=ae;function G(de){return typeof HTMLElement=="object"?de instanceof HTMLElement:de&&typeof de=="object"&&de.nodeType===1&&typeof de.nodeName=="string"}e.isHTMLElement=G,e.EventType={CLICK:"click",AUXCLICK:"auxclick",DBLCLICK:"dblclick",MOUSE_UP:"mouseup",MOUSE_DOWN:"mousedown",MOUSE_OVER:"mouseover",MOUSE_MOVE:"mousemove",MOUSE_OUT:"mouseout",MOUSE_ENTER:"mouseenter",MOUSE_LEAVE:"mouseleave",MOUSE_WHEEL:"wheel",POINTER_UP:"pointerup",POINTER_DOWN:"pointerdown",POINTER_MOVE:"pointermove",CONTEXT_MENU:"contextmenu",WHEEL:"wheel",KEY_DOWN:"keydown",KEY_PRESS:"keypress",KEY_UP:"keyup",LOAD:"load",BEFORE_UNLOAD:"beforeunload",UNLOAD:"unload",ABORT:"abort",ERROR:"error",RESIZE:"resize",SCROLL:"scroll",FULLSCREEN_CHANGE:"fullscreenchange",WK_FULLSCREEN_CHANGE:"webkitfullscreenchange",SELECT:"select",CHANGE:"change",SUBMIT:"submit",RESET:"reset",FOCUS:"focus",FOCUS_IN:"focusin",FOCUS_OUT:"focusout",BLUR:"blur",INPUT:"input",STORAGE:"storage",DRAG_START:"dragstart",DRAG:"drag",DRAG_ENTER:"dragenter",DRAG_LEAVE:"dragleave",DRAG_OVER:"dragover",DROP:"drop",DRAG_END:"dragend",ANIMATION_START:b.isWebKit?"webkitAnimationStart":"animationstart",ANIMATION_END:b.isWebKit?"webkitAnimationEnd":"animationend",ANIMATION_ITERATION:b.isWebKit?"webkitAnimationIteration":"animationiteration"},e.EventHelper={stop:function(de,ge){de.preventDefault?de.preventDefault():de.returnValue=!1,ge&&(de.stopPropagation?de.stopPropagation():de.cancelBubble=!0)}};function j(de){let ge=[];for(let pe=0;de&&de.nodeType===de.ELEMENT_NODE;pe++)ge[pe]=de.scrollTop,de=de.parentNode;return ge}e.saveParentsScrollTop=j;function te(de,ge){for(let pe=0;de&&de.nodeType===de.ELEMENT_NODE;pe++)de.scrollTop!==ge[pe]&&(de.scrollTop=ge[pe]),de=de.parentNode}e.restoreParentsScrollTop=te;class Z extends g.Disposable{constructor(ge){super();this._onDidFocus=this._register(new d.Emitter),this.onDidFocus=this._onDidFocus.event,this._onDidBlur=this._register(new d.Emitter),this.onDidBlur=this._onDidBlur.event;let pe=se(document.activeElement,ge),ve=!1;const ke=()=>{ve=!1,pe||(pe=!0,this._onDidFocus.fire())},Ne=()=>{pe&&(ve=!0,window.setTimeout(()=>{ve&&(ve=!1,pe=!1,this._onDidBlur.fire())},0))};this._refreshStateHandler=()=>{se(document.activeElement,ge)!==pe&&(pe?Ne():ke())},this._register(N.domEvent(ge,e.EventType.FOCUS,!0)(ke)),this._register(N.domEvent(ge,e.EventType.BLUR,!0)(Ne))}}function ue(de){return new Z(de)}e.trackFocus=ue;function he(de,...ge){if(de.append(...ge),ge.length===1&&typeof ge[0]!="string")return ge[0]}e.append=he;function re(de,...ge){de.innerText="",he(de,...ge)}e.reset=re;const ce=/([\w\-]+)?(#([\w\-]+))?((\.([\w\-]+))*)/;var me;(function(de){de.HTML="http://www.w3.org/1999/xhtml",de.SVG="http://www.w3.org/2000/svg"})(me=e.Namespace||(e.Namespace={}));function Ce(de,ge,pe,...ve){let ke=ce.exec(ge);if(!ke)throw new Error("Bad use of emmet");pe=Object.assign({},pe||{});let Ne=ke[1]||"div",Te;return de!==me.HTML?Te=document.createElementNS(de,Ne):Te=document.createElement(Ne),ke[3]&&(Te.id=ke[3]),ke[4]&&(Te.className=ke[4].replace(/\./g," ").trim()),Object.keys(pe).forEach(Oe=>{const Fe=pe[Oe];typeof Fe!="undefined"&&(/^on\w+$/.test(Oe)?Te[Oe]=Fe:Oe==="selected"?Fe&&Te.setAttribute(Oe,"true"):Te.setAttribute(Oe,Fe))}),Te.append(...ve),Te}function be(de,ge,...pe){return Ce(me.HTML,de,ge,...pe)}e.$=be,be.SVG=function(de,ge,...pe){return Ce(me.SVG,de,ge,...pe)};function Le(...de){for(let ge of de)ge.style.display="",ge.removeAttribute("aria-hidden")}e.show=Le;function De(...de){for(let ge of de)ge.style.display="none",ge.setAttribute("aria-hidden","true")}e.hide=De;function Re(de){return Array.prototype.slice.call(document.getElementsByTagName(de),0)}e.getElementsByTagName=Re;function Ee(de){const ge=window.devicePixelRatio*de;return Math.max(1,Math.floor(ge))/window.devicePixelRatio}e.computeScreenAwareSize=Ee;function Ae(de){if(b.isElectron||b.isEdgeLegacyWebView)window.open(de);else{let ge=window.open();ge&&(ge.opener=null,ge.location.href=de)}}e.windowOpenNoOpener=Ae;function Se(de){const ge=()=>{de(),pe=e.scheduleAtNextAnimationFrame(ge)};let pe=e.scheduleAtNextAnimationFrame(ge);return g.toDisposable(()=>pe.dispose())}e.animate=Se,c.RemoteAuthorities.setPreferredWebSchema(/^https:/.test(window.location.href)?"https":"http");function we(de){return de?`url('${c.FileAccess.asBrowserUri(de).toString(!0).replace(/'/g,"%27")}')`:"url('')"}e.asCSSUrl=we;function ye(de){return`'${de.replace(/'/g,"%27")}'`}e.asCSSPropertyValue=ye;class fe extends d.Emitter{constructor(){super();this._subscriptions=new g.DisposableStore,this._keyStatus={altKey:!1,shiftKey:!1,ctrlKey:!1,metaKey:!1},this._subscriptions.add(N.domEvent(window,"keydown",!0)(ge=>{const pe=new M.StandardKeyboardEvent(ge);if(!(pe.keyCode===6&&ge.repeat)){if(ge.altKey&&!this._keyStatus.altKey)this._keyStatus.lastKeyPressed="alt";else if(ge.ctrlKey&&!this._keyStatus.ctrlKey)this._keyStatus.lastKeyPressed="ctrl";else if(ge.metaKey&&!this._keyStatus.metaKey)this._keyStatus.lastKeyPressed="meta";else if(ge.shiftKey&&!this._keyStatus.shiftKey)this._keyStatus.lastKeyPressed="shift";else if(pe.keyCode!==6)this._keyStatus.lastKeyPressed=void 0;else return;this._keyStatus.altKey=ge.altKey,this._keyStatus.ctrlKey=ge.ctrlKey,this._keyStatus.metaKey=ge.metaKey,this._keyStatus.shiftKey=ge.shiftKey,this._keyStatus.lastKeyPressed&&(this._keyStatus.event=ge,this.fire(this._keyStatus))}})),this._subscriptions.add(N.domEvent(window,"keyup",!0)(ge=>{!ge.altKey&&this._keyStatus.altKey?this._keyStatus.lastKeyReleased="alt":!ge.ctrlKey&&this._keyStatus.ctrlKey?this._keyStatus.lastKeyReleased="ctrl":!ge.metaKey&&this._keyStatus.metaKey?this._keyStatus.lastKeyReleased="meta":!ge.shiftKey&&this._keyStatus.shiftKey?this._keyStatus.lastKeyReleased="shift":this._keyStatus.lastKeyReleased=void 0,this._keyStatus.lastKeyPressed!==this._keyStatus.lastKeyReleased&&(this._keyStatus.lastKeyPressed=void 0),this._keyStatus.altKey=ge.altKey,this._keyStatus.ctrlKey=ge.ctrlKey,this._keyStatus.metaKey=ge.metaKey,this._keyStatus.shiftKey=ge.shiftKey,this._keyStatus.lastKeyReleased&&(this._keyStatus.event=ge,this.fire(this._keyStatus))})),this._subscriptions.add(N.domEvent(document.body,"mousedown",!0)(ge=>{this._keyStatus.lastKeyPressed=void 0})),this._subscriptions.add(N.domEvent(document.body,"mouseup",!0)(ge=>{this._keyStatus.lastKeyPressed=void 0})),this._subscriptions.add(N.domEvent(document.body,"mousemove",!0)(ge=>{ge.buttons&&(this._keyStatus.lastKeyPressed=void 0)})),this._subscriptions.add(N.domEvent(window,"blur")(ge=>{this.resetKeyStatus()}))}get keyStatus(){return this._keyStatus}resetKeyStatus(){this.doResetKeyStatus(),this.fire(this._keyStatus)}doResetKeyStatus(){this._keyStatus={altKey:!1,shiftKey:!1,ctrlKey:!1,metaKey:!1}}static getInstance(){return fe.instance||(fe.instance=new fe),fe.instance}dispose(){super.dispose(),this._subscriptions.dispose()}}e.ModifierKeyEmitter=fe}),define(Q[156],J([0,1,7]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createElement=e.renderFormattedText=e.renderText=void 0;function N(c,o={}){const s=w(o);return s.textContent=c,s}e.renderText=N;function M(c,o={}){const s=w(o);return C(s,d(c),o.actionHandler),s}e.renderFormattedText=M;function w(c){const o=c.inline?"span":"div",s=document.createElement(o);return c.className&&(s.className=c.className),s}e.createElement=w;class S{constructor(o){this.source=o,this.index=0}eos(){return this.index>=this.source.length}next(){const o=this.peek();return this.advance(),o}peek(){return this.source[this.index]}advance(){this.index++}}function C(c,o,s){let a;if(o.type===2)a=document.createTextNode(o.content||"");else if(o.type===3)a=document.createElement("b");else if(o.type===4)a=document.createElement("i");else if(o.type===5&&s){const u=document.createElement("a");u.href="#",s.disposeables.add(b.addStandardDisposableListener(u,"click",r=>{s.callback(String(o.index),r)})),a=u}else o.type===7?a=document.createElement("br"):o.type===1&&(a=c);a&&c!==a&&c.appendChild(a),a&&Array.isArray(o.children)&&o.children.forEach(u=>{C(a,u,s)})}function d(c){const o={type:1,children:[]};let s=0,a=o;const u=[],r=new S(c);for(;!r.eos();){let i=r.next();const n=i==="\\"&&p(r.peek())!==0;if(n&&(i=r.next()),!n&&g(i)&&i===r.peek()){r.advance(),a.type===2&&(a=u.pop());const t=p(i);if(a.type===t||a.type===5&&t===6)a=u.pop();else{const l={type:t,children:[]};t===5&&(l.index=s,s++),a.children.push(l),u.push(a),a=l}}else if(i===` +`)a.type===2&&(a=u.pop()),a.children.push({type:7});else if(a.type!==2){const t={type:2,content:i};a.children.push(t),u.push(a),a=t}else a.content+=i}return a.type===2&&(a=u.pop()),u.length,o}function g(c){return p(c)!==0}function p(c){switch(c){case"*":return 3;case"_":return 4;case"[":return 5;case"]":return 6;default:return 0}}}),define(Q[90],J([0,1,7,193,50,2]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.GlobalMouseMoveMonitor=e.standardMouseMoveMerger=void 0;function S(d,g){let p=new M.StandardMouseEvent(g);return p.preventDefault(),{leftButton:p.leftButton,buttons:p.buttons,posx:p.posx,posy:p.posy}}e.standardMouseMoveMerger=S;class C{constructor(){this._hooks=new w.DisposableStore,this._mouseMoveEventMerger=null,this._mouseMoveCallback=null,this._onStopCallback=null}dispose(){this.stopMonitoring(!1),this._hooks.dispose()}stopMonitoring(g,p){if(!!this.isMonitoring()){this._hooks.clear(),this._mouseMoveEventMerger=null,this._mouseMoveCallback=null;const c=this._onStopCallback;this._onStopCallback=null,g&&c&&c(p)}}isMonitoring(){return!!this._mouseMoveEventMerger}startMonitoring(g,p,c,o,s){if(!this.isMonitoring()){this._mouseMoveEventMerger=c,this._mouseMoveCallback=o,this._onStopCallback=s;const a=N.IframeUtils.getSameOriginWindowChain(),u="mousemove",r="mouseup",i=a.map(t=>t.window.document),n=b.getShadowRoot(g);n&&i.unshift(n);for(const t of i)this._hooks.add(b.addDisposableThrottledListener(t,u,l=>{if(l.buttons!==p){this.stopMonitoring(!0);return}this._mouseMoveCallback(l)},(l,h)=>this._mouseMoveEventMerger(l,h))),this._hooks.add(b.addDisposableListener(t,r,l=>this.stopMonitoring(!0)));if(N.IframeUtils.hasDifferentOriginAncestor()){let t=a[a.length-1];this._hooks.add(b.addDisposableListener(t.window.document,"mouseout",l=>{new M.StandardMouseEvent(l).target.tagName.toLowerCase()==="html"&&this.stopMonitoring(!0)})),this._hooks.add(b.addDisposableListener(t.window.document,"mouseover",l=>{new M.StandardMouseEvent(l).target.tagName.toLowerCase()==="html"&&this.stopMonitoring(!0)})),this._hooks.add(b.addDisposableListener(t.window.document.body,"mouseleave",l=>{this.stopMonitoring(!0)}))}}}}e.GlobalMouseMoveMonitor=C});var Me=this&&this.__decorate||function(q,e,b,N){var M=arguments.length,w=M<3?e:N===null?N=Object.getOwnPropertyDescriptor(e,b):N,S;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")w=Reflect.decorate(q,e,b,N);else for(var C=q.length-1;C>=0;C--)(S=q[C])&&(w=(M<3?S(w):M>3?S(e,b,w):S(e,b))||w);return M>3&&w&&Object.defineProperty(e,b,w),w};define(Q[60],J([0,1,19,2,7,99]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Gesture=e.EventType=void 0;var S;(function(d){d.Tap="-monaco-gesturetap",d.Change="-monaco-gesturechange",d.Start="-monaco-gesturestart",d.End="-monaco-gesturesend",d.Contextmenu="-monaco-gesturecontextmenu"})(S=e.EventType||(e.EventType={}));class C extends N.Disposable{constructor(){super();this.dispatched=!1,this.activeTouches={},this.handle=null,this.targets=[],this.ignoreTargets=[],this._lastSetTapCountTime=0,this._register(M.addDisposableListener(document,"touchstart",g=>this.onTouchStart(g),{passive:!1})),this._register(M.addDisposableListener(document,"touchend",g=>this.onTouchEnd(g))),this._register(M.addDisposableListener(document,"touchmove",g=>this.onTouchMove(g),{passive:!1}))}static addTarget(g){return C.isTouchDevice()?(C.INSTANCE||(C.INSTANCE=new C),C.INSTANCE.targets.push(g),{dispose:()=>{C.INSTANCE.targets=C.INSTANCE.targets.filter(p=>p!==g)}}):N.Disposable.None}static ignoreTarget(g){return C.isTouchDevice()?(C.INSTANCE||(C.INSTANCE=new C),C.INSTANCE.ignoreTargets.push(g),{dispose:()=>{C.INSTANCE.ignoreTargets=C.INSTANCE.ignoreTargets.filter(p=>p!==g)}}):N.Disposable.None}static isTouchDevice(){return"ontouchstart"in window||navigator.maxTouchPoints>0||window.navigator.msMaxTouchPoints>0}dispose(){this.handle&&(this.handle.dispose(),this.handle=null),super.dispose()}onTouchStart(g){let p=Date.now();this.handle&&(this.handle.dispose(),this.handle=null);for(let c=0,o=g.targetTouches.length;c=C.HOLD_DELAY&&Math.abs(u.initialPageX-b.tail(u.rollingPageX))<30&&Math.abs(u.initialPageY-b.tail(u.rollingPageY))<30){let i=this.newGestureEvent(S.Contextmenu,u.initialTarget);i.pageX=b.tail(u.rollingPageX),i.pageY=b.tail(u.rollingPageY),this.dispatchEvent(i)}else if(c===1){let i=b.tail(u.rollingPageX),n=b.tail(u.rollingPageY),t=b.tail(u.rollingTimestamps)-u.rollingTimestamps[0],l=i-u.rollingPageX[0],h=n-u.rollingPageY[0];const m=this.targets.filter(_=>u.initialTarget instanceof Node&&_.contains(u.initialTarget));this.inertia(m,p,Math.abs(l)/t,l>0?1:-1,i,Math.abs(h)/t,h>0?1:-1,n)}this.dispatchEvent(this.newGestureEvent(S.End,u.initialTarget)),delete this.activeTouches[a.identifier]}this.dispatched&&(g.preventDefault(),g.stopPropagation(),this.dispatched=!1)}newGestureEvent(g,p){let c=document.createEvent("CustomEvent");return c.initEvent(g,!1,!0),c.initialTarget=p,c.tapCount=0,c}dispatchEvent(g){if(g.type===S.Tap){const p=new Date().getTime();let c=0;p-this._lastSetTapCountTime>C.CLEAR_TAP_COUNT_TIME?c=1:c=2,this._lastSetTapCountTime=p,g.tapCount=c}else(g.type===S.Change||g.type===S.Contextmenu)&&(this._lastSetTapCountTime=0);for(let p=0;p{g.initialTarget instanceof Node&&p.contains(g.initialTarget)&&(p.dispatchEvent(g),this.dispatched=!0)})}inertia(g,p,c,o,s,a,u,r){this.handle=M.scheduleAtNextAnimationFrame(()=>{let i=Date.now(),n=i-p,t=0,l=0,h=!0;c+=C.SCROLL_FRICTION*n,a+=C.SCROLL_FRICTION*n,c>0&&(h=!1,t=o*c*n),a>0&&(h=!1,l=u*a*n);let m=this.newGestureEvent(S.Change);m.translationX=t,m.translationY=l,g.forEach(_=>_.dispatchEvent(m)),h||this.inertia(g,i,c,o,s+t,a,u,r+l)})}onTouchMove(g){let p=Date.now();for(let c=0,o=g.changedTouches.length;c3&&(a.rollingPageX.shift(),a.rollingPageY.shift(),a.rollingTimestamps.shift()),a.rollingPageX.push(s.pageX),a.rollingPageY.push(s.pageY),a.rollingTimestamps.push(p)}this.dispatched&&(g.preventDefault(),g.stopPropagation(),this.dispatched=!1)}}C.SCROLL_FRICTION=-.005,C.HOLD_DELAY=700,C.CLEAR_TAP_COUNT_TIME=400,Me([w.memoize],C,"isTouchDevice",null),e.Gesture=C}),define(Q[103],J([0,1,7,27]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.renderIcon=e.renderLabelWithIcons=void 0;const M=new RegExp(`(\\\\)?\\$\\((${N.CSSIcon.iconNameExpression}(?:${N.CSSIcon.iconModifierExpression})?)\\)`,"g");function w(C){const d=new Array;let g,p=0,c=0;for(;(g=M.exec(C))!==null;){c=g.index||0,d.push(C.substring(p,c)),p=(g.index||0)+g[0].length;const[,o,s]=g;d.push(o?`$(${s})`:S({id:s}))}return p{p=c===`\r +`?-1:0,o+=g;for(const s of d)s.end<=o||(s.start>=o&&(s.start+=p),s.end>=o&&(s.end+=p));return g+=p,"\u23CE"})}}e.HighlightedLabel=w}),define(Q[287],J([0,1,7]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RowCache=void 0;function N(w){try{w.parentElement&&w.parentElement.removeChild(w)}catch(S){}}class M{constructor(S){this.renderers=S,this.cache=new Map}alloc(S){let C=this.getTemplateCache(S).pop();if(!C){const d=b.$(".monaco-list-row"),p=this.getRenderer(S).renderTemplate(d);C={domNode:d,templateId:S,templateData:p}}return C}release(S){!S||this.releaseRow(S)}releaseRow(S){const{domNode:C,templateId:d}=S;C&&(C.classList.remove("scrolling"),N(C)),this.getTemplateCache(d).push(S)}getTemplateCache(S){let C=this.cache.get(S);return C||(C=[],this.cache.set(S,C)),C}dispose(){this.cache.forEach((S,C)=>{for(const d of S)this.getRenderer(C).disposeTemplate(d.templateData),d.templateData=null}),this.cache.clear()}getRenderer(S){const C=this.renderers.get(S);if(!C)throw new Error(`No renderer found for ${S}`);return C}}e.RowCache=M}),define(Q[52],J([0,1,7,56,50,2,60]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Widget=void 0;class C extends w.Disposable{onclick(g,p){this._register(b.addDisposableListener(g,b.EventType.CLICK,c=>p(new M.StandardMouseEvent(c))))}onmousedown(g,p){this._register(b.addDisposableListener(g,b.EventType.MOUSE_DOWN,c=>p(new M.StandardMouseEvent(c))))}onmouseover(g,p){this._register(b.addDisposableListener(g,b.EventType.MOUSE_OVER,c=>p(new M.StandardMouseEvent(c))))}onnonbubblingmouseout(g,p){this._register(b.addDisposableNonBubblingMouseOutListener(g,c=>p(new M.StandardMouseEvent(c))))}onkeydown(g,p){this._register(b.addDisposableListener(g,b.EventType.KEY_DOWN,c=>p(new N.StandardKeyboardEvent(c))))}onkeyup(g,p){this._register(b.addDisposableListener(g,b.EventType.KEY_UP,c=>p(new N.StandardKeyboardEvent(c))))}oninput(g,p){this._register(b.addDisposableListener(g,b.EventType.INPUT,p))}onblur(g,p){this._register(b.addDisposableListener(g,b.EventType.BLUR,p))}onfocus(g,p){this._register(b.addDisposableListener(g,b.EventType.FOCUS,p))}ignoreGesture(g){S.Gesture.ignoreTarget(g)}}e.Widget=C}),define(Q[158],J([0,1,90,52,15]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ScrollbarArrow=e.ARROW_IMG_SIZE=void 0,e.ARROW_IMG_SIZE=11;class w extends N.Widget{constructor(C){super();this._onActivate=C.onActivate,this.bgDomNode=document.createElement("div"),this.bgDomNode.className="arrow-background",this.bgDomNode.style.position="absolute",this.bgDomNode.style.width=C.bgWidth+"px",this.bgDomNode.style.height=C.bgHeight+"px",typeof C.top!="undefined"&&(this.bgDomNode.style.top="0px"),typeof C.left!="undefined"&&(this.bgDomNode.style.left="0px"),typeof C.bottom!="undefined"&&(this.bgDomNode.style.bottom="0px"),typeof C.right!="undefined"&&(this.bgDomNode.style.right="0px"),this.domNode=document.createElement("div"),this.domNode.className=C.className,this.domNode.classList.add(...C.icon.classNamesArray),this.domNode.style.position="absolute",this.domNode.style.width=e.ARROW_IMG_SIZE+"px",this.domNode.style.height=e.ARROW_IMG_SIZE+"px",typeof C.top!="undefined"&&(this.domNode.style.top=C.top+"px"),typeof C.left!="undefined"&&(this.domNode.style.left=C.left+"px"),typeof C.bottom!="undefined"&&(this.domNode.style.bottom=C.bottom+"px"),typeof C.right!="undefined"&&(this.domNode.style.right=C.right+"px"),this._mouseMoveMonitor=this._register(new b.GlobalMouseMoveMonitor),this.onmousedown(this.bgDomNode,d=>this._arrowMouseDown(d)),this.onmousedown(this.domNode,d=>this._arrowMouseDown(d)),this._mousedownRepeatTimer=this._register(new M.IntervalTimer),this._mousedownScheduleRepeatTimer=this._register(new M.TimeoutTimer)}_arrowMouseDown(C){const d=()=>{this._mousedownRepeatTimer.cancelAndSet(()=>this._onActivate(),1e3/24)};this._onActivate(),this._mousedownRepeatTimer.cancel(),this._mousedownScheduleRepeatTimer.cancelAndSet(d,200),this._mouseMoveMonitor.startMonitoring(C.target,C.buttons,b.standardMouseMoveMerger,g=>{},()=>{this._mousedownRepeatTimer.cancel(),this._mousedownScheduleRepeatTimer.cancel()}),C.preventDefault()}}e.ScrollbarArrow=w}),define(Q[200],J([0,1,7,30,90,158,282,52,17]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractScrollbar=void 0;const g=140;class p extends C.Widget{constructor(o){super();this._lazyRender=o.lazyRender,this._host=o.host,this._scrollable=o.scrollable,this._scrollByPage=o.scrollByPage,this._scrollbarState=o.scrollbarState,this._visibilityController=this._register(new S.ScrollbarVisibilityController(o.visibility,"visible scrollbar "+o.extraScrollbarClassName,"invisible scrollbar "+o.extraScrollbarClassName)),this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._mouseMoveMonitor=this._register(new M.GlobalMouseMoveMonitor),this._shouldRender=!0,this.domNode=N.createFastDomNode(document.createElement("div")),this.domNode.setAttribute("role","presentation"),this.domNode.setAttribute("aria-hidden","true"),this._visibilityController.setDomNode(this.domNode),this.domNode.setPosition("absolute"),this.onmousedown(this.domNode.domNode,s=>this._domNodeMouseDown(s))}_createArrow(o){const s=this._register(new w.ScrollbarArrow(o));this.domNode.domNode.appendChild(s.bgDomNode),this.domNode.domNode.appendChild(s.domNode)}_createSlider(o,s,a,u){this.slider=N.createFastDomNode(document.createElement("div")),this.slider.setClassName("slider"),this.slider.setPosition("absolute"),this.slider.setTop(o),this.slider.setLeft(s),typeof a=="number"&&this.slider.setWidth(a),typeof u=="number"&&this.slider.setHeight(u),this.slider.setLayerHinting(!0),this.slider.setContain("strict"),this.domNode.domNode.appendChild(this.slider.domNode),this.onmousedown(this.slider.domNode,r=>{r.leftButton&&(r.preventDefault(),this._sliderMouseDown(r,()=>{}))}),this.onclick(this.slider.domNode,r=>{r.leftButton&&r.stopPropagation()})}_onElementSize(o){return this._scrollbarState.setVisibleSize(o)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}_onElementScrollSize(o){return this._scrollbarState.setScrollSize(o)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}_onElementScrollPosition(o){return this._scrollbarState.setScrollPosition(o)&&(this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded()),this._shouldRender=!0,this._lazyRender||this.render()),this._shouldRender}beginReveal(){this._visibilityController.setShouldBeVisible(!0)}beginHide(){this._visibilityController.setShouldBeVisible(!1)}render(){!this._shouldRender||(this._shouldRender=!1,this._renderDomNode(this._scrollbarState.getRectangleLargeSize(),this._scrollbarState.getRectangleSmallSize()),this._updateSlider(this._scrollbarState.getSliderSize(),this._scrollbarState.getArrowSize()+this._scrollbarState.getSliderPosition()))}_domNodeMouseDown(o){o.target===this.domNode.domNode&&this._onMouseDown(o)}delegateMouseDown(o){const s=this.domNode.domNode.getClientRects()[0].top,a=s+this._scrollbarState.getSliderPosition(),u=s+this._scrollbarState.getSliderPosition()+this._scrollbarState.getSliderSize(),r=this._sliderMousePosition(o);a<=r&&r<=u?o.leftButton&&(o.preventDefault(),this._sliderMouseDown(o,()=>{})):this._onMouseDown(o)}_onMouseDown(o){let s,a;if(o.target===this.domNode.domNode&&typeof o.browserEvent.offsetX=="number"&&typeof o.browserEvent.offsetY=="number")s=o.browserEvent.offsetX,a=o.browserEvent.offsetY;else{const r=b.getDomNodePagePosition(this.domNode.domNode);s=o.posx-r.left,a=o.posy-r.top}const u=this._mouseDownRelativePosition(s,a);this._setDesiredScrollPositionNow(this._scrollByPage?this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(u):this._scrollbarState.getDesiredScrollPositionFromOffset(u)),o.leftButton&&(o.preventDefault(),this._sliderMouseDown(o,()=>{}))}_sliderMouseDown(o,s){const a=this._sliderMousePosition(o),u=this._sliderOrthogonalMousePosition(o),r=this._scrollbarState.clone();this.slider.toggleClassName("active",!0),this._mouseMoveMonitor.startMonitoring(o.target,o.buttons,M.standardMouseMoveMerger,i=>{const n=this._sliderOrthogonalMousePosition(i),t=Math.abs(n-u);if(d.isWindows&&t>g){this._setDesiredScrollPositionNow(r.getScrollPosition());return}const h=this._sliderMousePosition(i)-a;this._setDesiredScrollPositionNow(r.getDesiredScrollPositionFromDelta(h))},()=>{this.slider.toggleClassName("active",!1),this._host.onDragEnd(),s()}),this._host.onDragStart()}_setDesiredScrollPositionNow(o){const s={};this.writeScrollPosition(s,o),this._scrollable.setScrollPositionNow(s)}updateScrollbarSize(o){this._updateScrollbarSize(o),this._scrollbarState.setScrollbarSize(o),this._shouldRender=!0,this._lazyRender||this.render()}isNeeded(){return this._scrollbarState.isNeeded()}}e.AbstractScrollbar=p}),define(Q[288],J([0,1,50,200,158,194,27]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.HorizontalScrollbar=void 0;const C=S.registerCodicon("scrollbar-button-left",S.Codicon.triangleLeft),d=S.registerCodicon("scrollbar-button-right",S.Codicon.triangleRight);class g extends N.AbstractScrollbar{constructor(c,o,s){const a=c.getScrollDimensions(),u=c.getCurrentScrollPosition();super({lazyRender:o.lazyRender,host:s,scrollbarState:new w.ScrollbarState(o.horizontalHasArrows?o.arrowSize:0,o.horizontal===2?0:o.horizontalScrollbarSize,o.vertical===2?0:o.verticalScrollbarSize,a.width,a.scrollWidth,u.scrollLeft),visibility:o.horizontal,extraScrollbarClassName:"horizontal",scrollable:c,scrollByPage:o.scrollByPage});if(o.horizontalHasArrows){const r=(o.arrowSize-M.ARROW_IMG_SIZE)/2,i=(o.horizontalScrollbarSize-M.ARROW_IMG_SIZE)/2;this._createArrow({className:"scra",icon:C,top:i,left:r,bottom:void 0,right:void 0,bgWidth:o.arrowSize,bgHeight:o.horizontalScrollbarSize,onActivate:()=>this._host.onMouseWheel(new b.StandardWheelEvent(null,1,0))}),this._createArrow({className:"scra",icon:d,top:i,left:void 0,bottom:void 0,right:r,bgWidth:o.arrowSize,bgHeight:o.horizontalScrollbarSize,onActivate:()=>this._host.onMouseWheel(new b.StandardWheelEvent(null,-1,0))})}this._createSlider(Math.floor((o.horizontalScrollbarSize-o.horizontalSliderSize)/2),0,void 0,o.horizontalSliderSize)}_updateSlider(c,o){this.slider.setWidth(c),this.slider.setLeft(o)}_renderDomNode(c,o){this.domNode.setWidth(c),this.domNode.setHeight(o),this.domNode.setLeft(0),this.domNode.setBottom(0)}onDidScroll(c){return this._shouldRender=this._onElementScrollSize(c.scrollWidth)||this._shouldRender,this._shouldRender=this._onElementScrollPosition(c.scrollLeft)||this._shouldRender,this._shouldRender=this._onElementSize(c.width)||this._shouldRender,this._shouldRender}_mouseDownRelativePosition(c,o){return c}_sliderMousePosition(c){return c.posx}_sliderOrthogonalMousePosition(c){return c.posy}_updateScrollbarSize(c){this.slider.setHeight(c)}writeScrollPosition(c,o){c.scrollLeft=o}}e.HorizontalScrollbar=g}),define(Q[289],J([0,1,50,200,158,194,27]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.VerticalScrollbar=void 0;const C=S.registerCodicon("scrollbar-button-up",S.Codicon.triangleUp),d=S.registerCodicon("scrollbar-button-down",S.Codicon.triangleDown);class g extends N.AbstractScrollbar{constructor(c,o,s){const a=c.getScrollDimensions(),u=c.getCurrentScrollPosition();super({lazyRender:o.lazyRender,host:s,scrollbarState:new w.ScrollbarState(o.verticalHasArrows?o.arrowSize:0,o.vertical===2?0:o.verticalScrollbarSize,0,a.height,a.scrollHeight,u.scrollTop),visibility:o.vertical,extraScrollbarClassName:"vertical",scrollable:c,scrollByPage:o.scrollByPage});if(o.verticalHasArrows){const r=(o.arrowSize-M.ARROW_IMG_SIZE)/2,i=(o.verticalScrollbarSize-M.ARROW_IMG_SIZE)/2;this._createArrow({className:"scra",icon:C,top:r,left:i,bottom:void 0,right:void 0,bgWidth:o.verticalScrollbarSize,bgHeight:o.arrowSize,onActivate:()=>this._host.onMouseWheel(new b.StandardWheelEvent(null,0,1))}),this._createArrow({className:"scra",icon:d,top:void 0,left:i,bottom:r,right:void 0,bgWidth:o.verticalScrollbarSize,bgHeight:o.arrowSize,onActivate:()=>this._host.onMouseWheel(new b.StandardWheelEvent(null,0,-1))})}this._createSlider(0,Math.floor((o.verticalScrollbarSize-o.verticalSliderSize)/2),o.verticalSliderSize,void 0)}_updateSlider(c,o){this.slider.setHeight(c),this.slider.setTop(o)}_renderDomNode(c,o){this.domNode.setWidth(o),this.domNode.setHeight(c),this.domNode.setRight(0),this.domNode.setTop(0)}onDidScroll(c){return this._shouldRender=this._onElementScrollSize(c.scrollHeight)||this._shouldRender,this._shouldRender=this._onElementScrollPosition(c.scrollTop)||this._shouldRender,this._shouldRender=this._onElementSize(c.height)||this._shouldRender,this._shouldRender}_mouseDownRelativePosition(c,o){return o}_sliderMousePosition(c){return c.posy}_sliderOrthogonalMousePosition(c){return c.posx}_updateScrollbarSize(c){this.slider.setWidth(c)}writeScrollPosition(c,o){c.scrollTop=o}}e.VerticalScrollbar=g}),define(Q[44],J([0,1,152,72,24,8,43]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DataUri=e.resolvePath=e.normalizePath=e.joinPath=e.dirname=e.basename=e.basenameOrAuthority=e.isEqual=e.extUri=e.ExtUri=e.originalFSPath=void 0;function C(p){return M.uriToFsPath(p,!0)}e.originalFSPath=C;class d{constructor(c){this._ignorePathCasing=c}compare(c,o,s=!1){return c===o?0:w.compare(this.getComparisonKey(c,s),this.getComparisonKey(o,s))}isEqual(c,o,s=!1){return c===o?!0:!c||!o?!1:this.getComparisonKey(c,s)===this.getComparisonKey(o,s)}getComparisonKey(c,o=!1){return c.with({path:this._ignorePathCasing(c)?c.path.toLowerCase():void 0,fragment:o?null:void 0}).toString()}joinPath(c,...o){return M.URI.joinPath(c,...o)}basenameOrAuthority(c){return e.basename(c)||c.authority}basename(c){return N.posix.basename(c.path)}dirname(c){if(c.path.length===0)return c;let o;return c.scheme===S.Schemas.file?o=M.URI.file(N.dirname(C(c))).path:(o=N.posix.dirname(c.path),c.authority&&o.length&&o.charCodeAt(0)!==47&&(console.error(`dirname("${c.toString})) resulted in a relative path`),o="/")),c.with({path:o})}normalizePath(c){if(!c.path.length)return c;let o;return c.scheme===S.Schemas.file?o=M.URI.file(N.normalize(C(c))).path:o=N.posix.normalize(c.path),c.with({path:o})}resolvePath(c,o){if(c.scheme===S.Schemas.file){const s=M.URI.file(N.resolve(C(c),o));return c.with({authority:s.authority,path:s.path})}return o.indexOf("/")===-1&&(o=b.toSlashes(o),/^[a-zA-Z]:(\/|$)/.test(o)&&(o="/"+o)),c.with({path:N.posix.resolve(c.path,o)})}}e.ExtUri=d,e.extUri=new d(()=>!1),e.isEqual=e.extUri.isEqual.bind(e.extUri),e.basenameOrAuthority=e.extUri.basenameOrAuthority.bind(e.extUri),e.basename=e.extUri.basename.bind(e.extUri),e.dirname=e.extUri.dirname.bind(e.extUri),e.joinPath=e.extUri.joinPath.bind(e.extUri),e.normalizePath=e.extUri.normalizePath.bind(e.extUri),e.resolvePath=e.extUri.resolvePath.bind(e.extUri);var g;(function(p){p.META_DATA_LABEL="label",p.META_DATA_DESCRIPTION="description",p.META_DATA_SIZE="size",p.META_DATA_MIME="mime";function c(o){const s=new Map;o.path.substring(o.path.indexOf(";")+1,o.path.lastIndexOf(";")).split(";").forEach(r=>{const[i,n]=r.split(":");i&&n&&s.set(i,n)});const u=o.path.substring(0,o.path.indexOf(";"));return u&&s.set(p.META_DATA_MIME,u),s}p.parseMetaData=c})(g=e.DataUri||(e.DataUri={}))}),define(Q[290],J([0,1,7,156,12,73,119,684,685,199,40,8,24,43,102,44,50,103,6,55]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t){"use strict";var l;Object.defineProperty(e,"__esModule",{value:!0}),e.renderMarkdown=void 0;const h=(l=window.trustedTypes)===null||l===void 0?void 0:l.createPolicy("insane",{createHTML(v,y){return d.insane(v,y)}});function m(v,y={},L={}){var I;const k=N.createElement(y),E=function(R){let W;try{W=g.parse(decodeURIComponent(R))}catch(x){}return W?(W=p.cloneAndChange(W,x=>{if(v.uris&&v.uris[x])return o.URI.revive(v.uris[x])}),encodeURIComponent(JSON.stringify(W))):R},T=function(R,W){const x=v.uris&&v.uris[R];if(!x)return R;let K=o.URI.revive(x);return o.URI.parse(R).toString()===K.toString()?R:W?s.FileAccess.asBrowserUri(K).toString(!0):(K.query&&(K=K.with({query:E(K.query)})),K.toString())};let O;const A=new Promise(R=>O=R),B=new C.Renderer;B.image=(R,W,x)=>{let K=[],Y=[];if(R){({href:R,dimensions:K}=w.parseHrefAndDimensions(R)),R=T(R,!0);try{const ee=o.URI.parse(R);y.baseUrl&&ee.scheme===s.Schemas.file&&(R=u.resolvePath(y.baseUrl,R).toString())}catch(ee){}Y.push(`src="${R}"`)}return x&&Y.push(`alt="${x}"`),W&&Y.push(`title="${W}"`),K.length&&(Y=Y.concat(K)),""},B.link=(R,W,x)=>(R===x&&(x=w.removeMarkdownEscapes(x)),R=T(R,!1),y.baseUrl&&(/^\w[\w\d+.-]*:/.test(R)||(R=u.resolvePath(y.baseUrl,R).toString())),W=w.removeMarkdownEscapes(W),R=w.removeMarkdownEscapes(R),!R||R.match(/^data:|javascript:/i)||R.match(/^command:/i)&&!v.isTrusted||R.match(/^command:(\/\/\/)?_workbench\.downloadResource/i)?x:(R=R.replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'"),`
    ${x}`)),B.paragraph=R=>(v.supportThemeIcons&&(R=i.renderLabelWithIcons(R).map(x=>typeof x=="string"?x:x.outerHTML).join("")),`

    ${R}

    `),y.codeBlockRenderer&&(B.code=(R,W)=>{const x=y.codeBlockRenderer(W,R),K=S.defaultGenerator.nextId(),Y=Promise.all([x,A]).then(ee=>{const se=k.querySelector(`div[data-code="${K}"]`);se&&b.reset(se,ee[0])}).catch(ee=>{});return y.asyncRenderCallback&&Y.then(y.asyncRenderCallback),`
    ${c.escape(R)}
    `}),y.actionHandler&&y.actionHandler.disposeables.add(n.Event.any(t.domEvent(k,"click"),t.domEvent(k,"auxclick"))(R=>{const W=new r.StandardMouseEvent(R);if(!(!W.leftButton&&!W.middleButton)){let x=W.target;if(!(x.tagName!=="A"&&(x=x.parentElement,!x||x.tagName!=="A")))try{const K=x.dataset.href;K&&y.actionHandler.callback(K,W)}catch(K){M.onUnexpectedError(K)}finally{W.preventDefault()}}})),L.sanitizer=R=>(v.isTrusted?R.match(/^(]+>)|(<\/\s*span>)$/):void 0)?R:"",L.sanitize=!0,L.silent=!0,L.renderer=B;let F=(I=v.value)!==null&&I!==void 0?I:"";F.length>1e5&&(F=`${F.substr(0,1e5)}\u2026`),v.supportThemeIcons&&(F=a.markdownEscapeEscapedIcons(F));const D=C.parse(F,L);if(k.innerHTML=_(v,D),O(),y.asyncRenderCallback)for(const R of k.getElementsByTagName("img")){const W=b.addDisposableListener(R,"load",()=>{W.dispose(),y.asyncRenderCallback()})}return k}e.renderMarkdown=m;function _(v,y){var L;const I=f(v);return(L=h==null?void 0:h.createHTML(y,I))!==null&&L!==void 0?L:d.insane(y,I)}function f(v){const y=[s.Schemas.http,s.Schemas.https,s.Schemas.mailto,s.Schemas.data,s.Schemas.file,s.Schemas.vscodeRemote,s.Schemas.vscodeRemoteResource];return v.isTrusted&&y.push(s.Schemas.command),{allowedSchemes:y,allowedTags:["ul","li","p","code","blockquote","ol","h1","h2","h3","h4","h5","h6","hr","em","pre","table","thead","tbody","tr","th","td","div","del","a","strong","br","img","span"],allowedAttributes:{a:["href","name","target","data-href"],img:["src","title","alt","width","height"],div:["class","data-code"],span:["class","style"],th:["align"],td:["align"]},filter(L){return L.tag==="span"&&v.isTrusted?L.attrs.style&&Object.keys(L.attrs).length===1?!!L.attrs.style.match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/):L.attrs.class?!!L.attrs.class.match(/^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/):!1:!0}}}}),define(Q[159],J([0,1,24,43,17,44,152]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.normalizeDriveLetter=e.getBaseLabel=void 0;function C(g){if(!!g){typeof g=="string"&&(g=b.URI.file(g));const p=w.basename(g)||(g.scheme===N.Schemas.file?g.fsPath:g.path);return M.isWindows&&S.isRootOrDriveLetter(p)?d(p):p}}e.getBaseLabel=C;function d(g){return S.hasDriveLetter(g)?g.charAt(0).toUpperCase()+g.slice(1):g}e.normalizeDriveLetter=d}),define(Q[291],J([0,1,72,8,198,43,44]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.guessMimeTypes=e.registerTextMime=e.MIME_UNKNOWN=e.MIME_TEXT=void 0,e.MIME_TEXT="text/plain",e.MIME_UNKNOWN="application/unknown";let C=[],d=[],g=[];function p(u,r=!1){const i=c(u);C.push(i),i.userConfigured?g.push(i):d.push(i),r&&!i.userConfigured&&C.forEach(n=>{n.mime===i.mime||n.userConfigured||(i.extension&&n.extension===i.extension&&console.warn(`Overwriting extension <<${i.extension}>> to now point to mime <<${i.mime}>>`),i.filename&&n.filename===i.filename&&console.warn(`Overwriting filename <<${i.filename}>> to now point to mime <<${i.mime}>>`),i.filepattern&&n.filepattern===i.filepattern&&console.warn(`Overwriting filepattern <<${i.filepattern}>> to now point to mime <<${i.mime}>>`),i.firstline&&n.firstline===i.firstline&&console.warn(`Overwriting firstline <<${i.firstline}>> to now point to mime <<${i.mime}>>`))})}e.registerTextMime=p;function c(u){return{id:u.id,mime:u.mime,filename:u.filename,extension:u.extension,filepattern:u.filepattern,firstline:u.firstline,userConfigured:u.userConfigured,filenameLowercase:u.filename?u.filename.toLowerCase():void 0,extensionLowercase:u.extension?u.extension.toLowerCase():void 0,filepatternLowercase:u.filepattern?u.filepattern.toLowerCase():void 0,filepatternOnPath:u.filepattern?u.filepattern.indexOf(b.posix.sep)>=0:!1}}function o(u,r){let i;if(u)switch(u.scheme){case w.Schemas.file:i=u.fsPath;break;case w.Schemas.data:i=S.DataUri.parseMetaData(u).get(S.DataUri.META_DATA_LABEL);break;default:i=u.path}if(!i)return[e.MIME_UNKNOWN];i=i.toLowerCase();const n=b.basename(i),t=s(i,n,g);if(t)return[t,e.MIME_TEXT];const l=s(i,n,d);if(l)return[l,e.MIME_TEXT];if(r){const h=a(r);if(h)return[h,e.MIME_TEXT]}return[e.MIME_UNKNOWN]}e.guessMimeTypes=o;function s(u,r,i){let n=null,t=null,l=null;for(let h=i.length-1;h>=0;h--){const m=i[h];if(r===m.filenameLowercase){n=m;break}if(m.filepattern&&(!t||m.filepattern.length>t.filepattern.length)){const _=m.filepatternOnPath?u:r;M.match(m.filepatternLowercase,_)&&(t=m)}m.extension&&(!l||m.extension.length>l.extension.length)&&r.endsWith(m.extensionLowercase)&&(l=m)}return n?n.mime:t?t.mime:l?l.mime:null}function a(u){if(N.startsWithUTF8BOM(u)&&(u=u.substr(1)),u.length>0)for(let r=C.length-1;r>=0;r--){const i=C[r];if(!!i.firstline){const n=u.match(i.firstline);if(n&&n.length>0)return i.mime}}return null}}),define(Q[292],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.generateUuid=void 0;const b=new Uint8Array(16),N=[];for(let S=0;S<256;S++)N.push(S.toString(16).padStart(2,"0"));let M;typeof crypto=="object"&&typeof crypto.getRandomValues=="function"?M=crypto.getRandomValues.bind(crypto):M=function(S){for(let C=0;C{this._pendingReplies[r]={resolve:i,reject:n},this._send({vsWorker:this._workerId,req:r,method:a,args:u})})}handleMessage(a){!a||!a.vsWorker||this._workerId!==-1&&a.vsWorker!==this._workerId||this._handleMessage(a)}_handleMessage(a){if(a.seq){let n=a;if(!this._pendingReplies[n.seq]){console.warn("Got reply to unknown seq");return}let t=this._pendingReplies[n.seq];if(delete this._pendingReplies[n.seq],n.err){let l=n.err;n.err.$isError&&(l=new Error,l.name=n.err.name,l.message=n.err.message,l.stack=n.err.stack),t.reject(l);return}t.resolve(n.res);return}let u=a,r=u.req;this._handler.handleMessage(u.method,u.args).then(n=>{this._send({vsWorker:this._workerId,seq:r,res:n,err:void 0})},n=>{n.detail instanceof Error&&(n.detail=b.transformErrorForSerialization(n.detail)),this._send({vsWorker:this._workerId,seq:r,res:void 0,err:b.transformErrorForSerialization(n)})})}_send(a){let u=[];if(a.req){const r=a;for(let i=0;i{this._protocol.handleMessage(h)},h=>{i&&i(h)})),this._protocol=new g({sendMessage:(h,m)=>{this._worker.postMessage(h,m)},handleMessage:(h,m)=>{if(typeof r[h]!="function")return Promise.reject(new Error("Missing method "+h+" on main thread host."));try{return Promise.resolve(r[h].apply(r,m))}catch(_){return Promise.reject(_)}}}),this._protocol.setWorkerId(this._worker.getId());let n=null;typeof self.require!="undefined"&&typeof self.require.getConfig=="function"?n=self.require.getConfig():typeof self.requirejs!="undefined"&&(n=self.requirejs.s.contexts._.config);const t=w.getAllMethodNames(r);this._onModuleLoaded=this._protocol.sendMessage(S,[this._worker.getId(),JSON.parse(JSON.stringify(n)),u,t]);const l=(h,m)=>this._request(h,m);this._lazyProxy=new Promise((h,m)=>{i=m,this._onModuleLoaded.then(_=>{h(w.createProxyObject(_,l))},_=>{m(_),this._onError("Worker failed to load "+u,_)})})}getProxyObject(){return this._lazyProxy}_request(a,u){return new Promise((r,i)=>{this._onModuleLoaded.then(()=>{this._protocol.sendMessage(a,u).then(r,i)},i)})}_onError(a,u){console.error(a),console.info(u)}}e.SimpleWorkerClient=p;class c{constructor(a,u){this._requestHandlerFactory=u,this._requestHandler=null,this._protocol=new g({sendMessage:(r,i)=>{a(r,i)},handleMessage:(r,i)=>this._handleMessage(r,i)})}onmessage(a){this._protocol.handleMessage(a)}_handleMessage(a,u){if(a===S)return this.initialize(u[0],u[1],u[2],u[3]);if(!this._requestHandler||typeof this._requestHandler[a]!="function")return Promise.reject(new Error("Missing requestHandler or method: "+a));try{return Promise.resolve(this._requestHandler[a].apply(this._requestHandler,u))}catch(r){return Promise.reject(r)}}initialize(a,u,r,i){this._protocol.setWorkerId(a);const n=(l,h)=>this._protocol.sendMessage(l,h),t=w.createProxyObject(i,n);return this._requestHandlerFactory?(this._requestHandler=this._requestHandlerFactory(t),Promise.resolve(w.getAllMethodNames(this._requestHandler))):(u&&(typeof u.baseUrl!="undefined"&&delete u.baseUrl,typeof u.paths!="undefined"&&typeof u.paths.vs!="undefined"&&delete u.paths.vs,typeof u.trustedTypesPolicy!==void 0&&delete u.trustedTypesPolicy,u.catchError=!0,self.require.config(u)),new Promise((l,h)=>{self.require([r],m=>{if(this._requestHandler=m.create(t),!this._requestHandler){h(new Error("No RequestHandler!"));return}l(w.getAllMethodNames(this._requestHandler))},h)}))}}e.SimpleWorkerServer=c;function o(s){return new c(s,null)}e.create=o}),define(Q[202],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ItemActivation=e.NO_KEY_MODS=void 0,e.NO_KEY_MODS={ctrlCmd:!1,alt:!1};var b;(function(N){N[N.NONE=0]="NONE",N[N.FIRST=1]="FIRST",N[N.SECOND=2]="SECOND",N[N.LAST=3]="LAST"})(b=e.ItemActivation||(e.ItemActivation={}))}),define(Q[293],J([0,1,2,6,15,20]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InMemoryStorageDatabase=e.Storage=void 0;var S;(function(g){g[g.None=0]="None",g[g.Initialized=1]="Initialized",g[g.Closed=2]="Closed"})(S||(S={}));class C extends b.Disposable{constructor(p,c=Object.create(null)){super();this.database=p,this.options=c,this._onDidChangeStorage=this._register(new N.Emitter),this.onDidChangeStorage=this._onDidChangeStorage.event,this.state=S.None,this.cache=new Map,this.flushDelayer=new M.ThrottledDelayer(C.DEFAULT_FLUSH_DELAY),this.pendingDeletes=new Set,this.pendingInserts=new Map,this.whenFlushedCallbacks=[],this.registerListeners()}registerListeners(){this._register(this.database.onDidChangeItemsExternal(p=>this.onDidChangeItemsExternal(p)))}onDidChangeItemsExternal(p){var c,o;(c=p.changed)===null||c===void 0||c.forEach((s,a)=>this.accept(a,s)),(o=p.deleted)===null||o===void 0||o.forEach(s=>this.accept(s,void 0))}accept(p,c){if(this.state!==S.Closed){let o=!1;w.isUndefinedOrNull(c)?o=this.cache.delete(p):this.cache.get(p)!==c&&(this.cache.set(p,c),o=!0),o&&this._onDidChangeStorage.fire(p)}}get(p,c){const o=this.cache.get(p);return w.isUndefinedOrNull(o)?c:o}getBoolean(p,c){const o=this.get(p);return w.isUndefinedOrNull(o)?c:o==="true"}getNumber(p,c){const o=this.get(p);return w.isUndefinedOrNull(o)?c:parseInt(o,10)}set(p,c){return Ie(this,void 0,void 0,function*(){if(this.state!==S.Closed){if(w.isUndefinedOrNull(c))return this.delete(p);const o=String(c);if(this.cache.get(p)!==o)return this.cache.set(p,o),this.pendingInserts.set(p,o),this.pendingDeletes.delete(p),this._onDidChangeStorage.fire(p),this.flushDelayer.trigger(()=>this.flushPending())}})}delete(p){return Ie(this,void 0,void 0,function*(){if(this.state!==S.Closed&&!!this.cache.delete(p))return this.pendingDeletes.has(p)||this.pendingDeletes.add(p),this.pendingInserts.delete(p),this._onDidChangeStorage.fire(p),this.flushDelayer.trigger(()=>this.flushPending())})}get hasPending(){return this.pendingInserts.size>0||this.pendingDeletes.size>0}flushPending(){return Ie(this,void 0,void 0,function*(){if(!!this.hasPending){const p={insert:this.pendingInserts,delete:this.pendingDeletes};return this.pendingDeletes=new Set,this.pendingInserts=new Map,this.database.updateItems(p).finally(()=>{var c;if(!this.hasPending)for(;this.whenFlushedCallbacks.length;)(c=this.whenFlushedCallbacks.pop())===null||c===void 0||c()})}})}dispose(){this.flushDelayer.cancel(),this.flushDelayer.dispose(),super.dispose()}}e.Storage=C,C.DEFAULT_FLUSH_DELAY=100;class d{constructor(){this.onDidChangeItemsExternal=N.Event.None,this.items=new Map}updateItems(p){return Ie(this,void 0,void 0,function*(){p.insert&&p.insert.forEach((c,o)=>this.items.set(o,c)),p.delete&&p.delete.forEach(c=>this.items.delete(c))})}}e.InMemoryStorageDatabase=d}),define(Q[294],J([0,1,17,201]),function(q,e,b,N){"use strict";var M;Object.defineProperty(e,"__esModule",{value:!0}),e.DefaultWorkerFactory=e.getWorkerBootstrapUrl=void 0;const w=(M=window.trustedTypes)===null||M===void 0?void 0:M.createPolicy("defaultWorkerFactory",{createScriptURL:c=>c});function S(c,o){if(b.globals.MonacoEnvironment){if(typeof b.globals.MonacoEnvironment.getWorker=="function")return b.globals.MonacoEnvironment.getWorker(c,o);if(typeof b.globals.MonacoEnvironment.getWorkerUrl=="function"){const s=b.globals.MonacoEnvironment.getWorkerUrl(c,o);return new Worker(w?w.createScriptURL(s):s,{name:o})}}if(typeof q=="function"){const s=q.toUrl("./"+c),a=C(s,o);return new Worker(w?w.createScriptURL(a):a,{name:o})}throw new Error("You must define a function MonacoEnvironment.getWorkerUrl or MonacoEnvironment.getWorker")}function C(c,o,s=!1){if(s||/^((http:)|(https:)|(file:))/.test(c)){const a=String(window.location),u=a.substr(0,a.length-window.location.hash.length-window.location.search.length-window.location.pathname.length);if(s||c.substring(0,u.length)!==u){const r="vs/base/worker/defaultWorkerFactory.js",i=q.toUrl(r).slice(0,-r.length),n=`/*${o}*/self.MonacoEnvironment={baseUrl: '${i}'};importScripts('${c}');/*${o}*/`;if(s)return`data:text/javascript;charset=utf-8,${encodeURIComponent(n)}`;const t=new Blob([n],{type:"application/javascript"});return URL.createObjectURL(t)}}return c+"#"+o}e.getWorkerBootstrapUrl=C;function d(c){return typeof c.then=="function"}class g{constructor(o,s,a,u,r){this.id=s;const i=S("workerMain.js",a);d(i)?this.worker=i:this.worker=Promise.resolve(i),this.postMessage(o,[]),this.worker.then(n=>{n.onmessage=function(t){u(t.data)},n.onmessageerror=r,typeof n.addEventListener=="function"&&n.addEventListener("error",r)})}getId(){return this.id}postMessage(o,s){this.worker&&this.worker.then(a=>a.postMessage(o,s))}dispose(){this.worker&&this.worker.then(o=>o.terminate()),this.worker=null}}class p{constructor(o){this._label=o,this._webWorkerFailedBeforeError=!1}create(o,s,a){let u=++p.LAST_WORKER_ID;if(this._webWorkerFailedBeforeError)throw this._webWorkerFailedBeforeError;return new g(o,u,this._label||"anonymous"+u,s,r=>{N.logOnceWebWorkerWarning(r),this._webWorkerFailedBeforeError=r,a(r)})}}e.DefaultWorkerFactory=p,p.LAST_WORKER_ID=0}),define(Q[203],J([10]),{}),define(Q[295],J([10]),{}),define(Q[47],J([0,1,17,7,295]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.status=e.alert=e.setARIAContainer=void 0;const M=2e4;let w,S,C,d,g;function p(a){w=document.createElement("div"),w.className="monaco-aria-container";const u=()=>{const i=document.createElement("div");return i.className="monaco-alert",i.setAttribute("role","alert"),i.setAttribute("aria-atomic","true"),w.appendChild(i),i};S=u(),C=u();const r=()=>{const i=document.createElement("div");return i.className="monaco-status",i.setAttribute("role","complementary"),i.setAttribute("aria-live","polite"),i.setAttribute("aria-atomic","true"),w.appendChild(i),i};d=r(),g=r(),a.appendChild(w)}e.setARIAContainer=p;function c(a){!w||(S.textContent!==a?(N.clearNode(C),s(S,a)):(N.clearNode(S),s(C,a)))}e.alert=c;function o(a){!w||(b.isMacintosh?c(a):d.textContent!==a?(N.clearNode(g),s(d,a)):(N.clearNode(d),s(g,a)))}e.status=o;function s(a,u){N.clearNode(a),u.length>M&&(u=u.substr(0,M)),a.textContent=u,a.style.visibility="hidden",a.style.visibility="visible"}}),define(Q[296],J([10]),{}),define(Q[297],J([0,1,56,29,40,6,2,60,103,7,296]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Button=void 0;const p={buttonBackground:N.Color.fromHex("#0E639C"),buttonHoverBackground:N.Color.fromHex("#006BB3"),buttonForeground:N.Color.white};class c extends S.Disposable{constructor(s,a){super();this._onDidClick=this._register(new w.Emitter),this.options=a||Object.create(null),M.mixin(this.options,p,!1),this.buttonForeground=this.options.buttonForeground,this.buttonBackground=this.options.buttonBackground,this.buttonHoverBackground=this.options.buttonHoverBackground,this.buttonSecondaryForeground=this.options.buttonSecondaryForeground,this.buttonSecondaryBackground=this.options.buttonSecondaryBackground,this.buttonSecondaryHoverBackground=this.options.buttonSecondaryHoverBackground,this.buttonBorder=this.options.buttonBorder,this._element=document.createElement("a"),this._element.classList.add("monaco-button"),this._element.tabIndex=0,this._element.setAttribute("role","button"),s.appendChild(this._element),this._register(C.Gesture.addTarget(this._element)),[g.EventType.CLICK,C.EventType.Tap].forEach(u=>{this._register(g.addDisposableListener(this._element,u,r=>{if(!this.enabled){g.EventHelper.stop(r);return}this._onDidClick.fire(r)}))}),this._register(g.addDisposableListener(this._element,g.EventType.KEY_DOWN,u=>{const r=new b.StandardKeyboardEvent(u);let i=!1;this.enabled&&(r.equals(3)||r.equals(10))?(this._onDidClick.fire(u),i=!0):r.equals(9)&&(this._element.blur(),i=!0),i&&g.EventHelper.stop(r,!0)})),this._register(g.addDisposableListener(this._element,g.EventType.MOUSE_OVER,u=>{this._element.classList.contains("disabled")||this.setHoverBackground()})),this._register(g.addDisposableListener(this._element,g.EventType.MOUSE_OUT,u=>{this.applyStyles()})),this.focusTracker=this._register(g.trackFocus(this._element)),this._register(this.focusTracker.onDidFocus(()=>this.setHoverBackground())),this._register(this.focusTracker.onDidBlur(()=>this.applyStyles())),this.applyStyles()}get onDidClick(){return this._onDidClick.event}setHoverBackground(){let s;this.options.secondary?s=this.buttonSecondaryHoverBackground?this.buttonSecondaryHoverBackground.toString():null:s=this.buttonHoverBackground?this.buttonHoverBackground.toString():null,s&&(this._element.style.backgroundColor=s)}style(s){this.buttonForeground=s.buttonForeground,this.buttonBackground=s.buttonBackground,this.buttonHoverBackground=s.buttonHoverBackground,this.buttonSecondaryForeground=s.buttonSecondaryForeground,this.buttonSecondaryBackground=s.buttonSecondaryBackground,this.buttonSecondaryHoverBackground=s.buttonSecondaryHoverBackground,this.buttonBorder=s.buttonBorder,this.applyStyles()}applyStyles(){if(this._element){let s,a;this.options.secondary?(a=this.buttonSecondaryForeground?this.buttonSecondaryForeground.toString():"",s=this.buttonSecondaryBackground?this.buttonSecondaryBackground.toString():""):(a=this.buttonForeground?this.buttonForeground.toString():"",s=this.buttonBackground?this.buttonBackground.toString():"");const u=this.buttonBorder?this.buttonBorder.toString():"";this._element.style.color=a,this._element.style.backgroundColor=s,this._element.style.borderWidth=u?"1px":"",this._element.style.borderStyle=u?"solid":"",this._element.style.borderColor=u}}get element(){return this._element}set label(s){this._element.classList.add("monaco-text-button"),this.options.supportIcons?g.reset(this._element,...d.renderLabelWithIcons(s)):this._element.textContent=s,typeof this.options.title=="string"?this._element.title=this.options.title:this.options.title&&(this._element.title=s)}set enabled(s){s?(this._element.classList.remove("disabled"),this._element.setAttribute("aria-disabled",String(!1)),this._element.tabIndex=0):(this._element.classList.add("disabled"),this._element.setAttribute("aria-disabled",String(!0)))}get enabled(){return!this._element.classList.contains("disabled")}}e.Button=c}),define(Q[298],J([10]),{}),define(Q[160],J([0,1,52,29,6,27,298]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Checkbox=void 0;const S={inputActiveOptionBorder:N.Color.fromHex("#007ACC00"),inputActiveOptionForeground:N.Color.fromHex("#FFFFFF"),inputActiveOptionBackground:N.Color.fromHex("#0E639C50")};class C extends b.Widget{constructor(g){super();this._onChange=this._register(new M.Emitter),this.onChange=this._onChange.event,this._onKeyDown=this._register(new M.Emitter),this.onKeyDown=this._onKeyDown.event,this._opts=Object.assign(Object.assign({},S),g),this._checked=this._opts.isChecked;const p=["monaco-custom-checkbox"];this._opts.icon&&p.push(...w.CSSIcon.asClassNameArray(this._opts.icon)),this._opts.actionClassName&&p.push(...this._opts.actionClassName.split(" ")),this._checked&&p.push("checked"),this.domNode=document.createElement("div"),this.domNode.title=this._opts.title,this.domNode.classList.add(...p),this._opts.notFocusable||(this.domNode.tabIndex=0),this.domNode.setAttribute("role","checkbox"),this.domNode.setAttribute("aria-checked",String(this._checked)),this.domNode.setAttribute("aria-label",this._opts.title),this.applyStyles(),this.onclick(this.domNode,c=>{this.checked=!this._checked,this._onChange.fire(!1),c.preventDefault()}),this.ignoreGesture(this.domNode),this.onkeydown(this.domNode,c=>{if(c.keyCode===10||c.keyCode===3){this.checked=!this._checked,this._onChange.fire(!0),c.preventDefault();return}this._onKeyDown.fire(c)})}get enabled(){return this.domNode.getAttribute("aria-disabled")!=="true"}focus(){this.domNode.focus()}get checked(){return this._checked}set checked(g){this._checked=g,this.domNode.setAttribute("aria-checked",String(this._checked)),this.domNode.classList.toggle("checked",this._checked),this.applyStyles()}width(){return 2+2+2+16}style(g){g.inputActiveOptionBorder&&(this._opts.inputActiveOptionBorder=g.inputActiveOptionBorder),g.inputActiveOptionForeground&&(this._opts.inputActiveOptionForeground=g.inputActiveOptionForeground),g.inputActiveOptionBackground&&(this._opts.inputActiveOptionBackground=g.inputActiveOptionBackground),this.applyStyles()}applyStyles(){this.domNode&&(this.domNode.style.borderColor=this._checked&&this._opts.inputActiveOptionBorder?this._opts.inputActiveOptionBorder.toString():"transparent",this.domNode.style.color=this._checked&&this._opts.inputActiveOptionForeground?this._opts.inputActiveOptionForeground.toString():"inherit",this.domNode.style.backgroundColor=this._checked&&this._opts.inputActiveOptionBackground?this._opts.inputActiveOptionBackground.toString():"transparent")}enable(){this.domNode.setAttribute("aria-disabled",String(!1))}disable(){this.domNode.setAttribute("aria-disabled",String(!0))}}e.Checkbox=C}),define(Q[299],J([10]),{}),define(Q[300],J([10]),{}),define(Q[123],J([0,1,27,299,300]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.formatRule=void 0;function N(M){let w=M.definition;for(;w instanceof b.Codicon;)w=w.definition;return`.codicon-${M.id}:before { content: '${w.fontCharacter}'; }`}e.formatRule=N}),define(Q[301],J([10]),{}),define(Q[204],J([0,1,7,17,2,120,151,301]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ContextView=e.layout=e.LayoutAnchorMode=void 0;var C;(function(c){c[c.AVOID=0]="AVOID",c[c.ALIGN=1]="ALIGN"})(C=e.LayoutAnchorMode||(e.LayoutAnchorMode={}));function d(c,o,s){const a=s.mode===C.ALIGN?s.offset:s.offset+s.size,u=s.mode===C.ALIGN?s.offset+s.size:s.offset;return s.position===0?o<=c-a?a:o<=u?u-o:Math.max(c-o,0):o<=u?u-o:o<=c-a?a:0}e.layout=d;class g extends M.Disposable{constructor(o,s){super();this.container=null,this.delegate=null,this.toDisposeOnClean=M.Disposable.None,this.toDisposeOnSetContainer=M.Disposable.None,this.shadowRoot=null,this.shadowRootHostElement=null,this.view=b.$(".context-view"),this.useFixedPosition=!1,this.useShadowDOM=!1,b.hide(this.view),this.setContainer(o,s),this._register(M.toDisposable(()=>this.setContainer(null,1)))}setContainer(o,s){var a;if(this.container&&(this.toDisposeOnSetContainer.dispose(),this.shadowRoot?(this.shadowRoot.removeChild(this.view),this.shadowRoot=null,(a=this.shadowRootHostElement)===null||a===void 0||a.remove(),this.shadowRootHostElement=null):this.container.removeChild(this.view),this.container=null),o){if(this.container=o,this.useFixedPosition=s!==1,this.useShadowDOM=s===3,this.useShadowDOM){this.shadowRootHostElement=b.$(".shadow-root-host"),this.container.appendChild(this.shadowRootHostElement),this.shadowRoot=this.shadowRootHostElement.attachShadow({mode:"open"});const r=document.createElement("style");r.textContent=p,this.shadowRoot.appendChild(r),this.shadowRoot.appendChild(this.view),this.shadowRoot.appendChild(b.$("slot"))}else this.container.appendChild(this.view);const u=new M.DisposableStore;g.BUBBLE_UP_EVENTS.forEach(r=>{u.add(b.addStandardDisposableListener(this.container,r,i=>{this.onDOMEvent(i,!1)}))}),g.BUBBLE_DOWN_EVENTS.forEach(r=>{u.add(b.addStandardDisposableListener(this.container,r,i=>{this.onDOMEvent(i,!0)},!0))}),this.toDisposeOnSetContainer=u}}show(o){this.isVisible()&&this.hide(),b.clearNode(this.view),this.view.className="context-view",this.view.style.top="0px",this.view.style.left="0px",this.view.style.zIndex="2500",this.view.style.position=this.useFixedPosition?"fixed":"absolute",b.show(this.view),this.toDisposeOnClean=o.render(this.view)||M.Disposable.None,this.delegate=o,this.doLayout(),this.delegate.focus&&this.delegate.focus()}getViewElement(){return this.view}layout(){if(!!this.isVisible()){if(this.delegate.canRelayout===!1&&!(N.isIOS&&S.BrowserFeatures.pointerEvents)){this.hide();return}this.delegate.layout&&this.delegate.layout(),this.doLayout()}}doLayout(){if(!!this.isVisible()){let o=this.delegate.getAnchor(),s;if(b.isHTMLElement(o)){let m=b.getDomNodePagePosition(o);s={top:m.top,left:m.left,width:m.width,height:m.height}}else s={top:o.y,left:o.x,width:o.width||1,height:o.height||2};const a=b.getTotalWidth(this.view),u=b.getTotalHeight(this.view),r=this.delegate.anchorPosition||0,i=this.delegate.anchorAlignment||0,n=this.delegate.anchorAxisAlignment||0;let t,l;if(n===0){const m={offset:s.top-window.pageYOffset,size:s.height,position:r===0?0:1},_={offset:s.left,size:s.width,position:i===0?0:1,mode:C.ALIGN};t=d(window.innerHeight,u,m)+window.pageYOffset,w.Range.intersects({start:t,end:t+u},{start:m.offset,end:m.offset+m.size})&&(_.mode=C.AVOID),l=d(window.innerWidth,a,_)}else{const m={offset:s.left,size:s.width,position:i===0?0:1},_={offset:s.top,size:s.height,position:r===0?0:1,mode:C.ALIGN};l=d(window.innerWidth,a,m),w.Range.intersects({start:l,end:l+a},{start:m.offset,end:m.offset+m.size})&&(_.mode=C.AVOID),t=d(window.innerHeight,u,_)+window.pageYOffset}this.view.classList.remove("top","bottom","left","right"),this.view.classList.add(r===0?"bottom":"top"),this.view.classList.add(i===0?"left":"right"),this.view.classList.toggle("fixed",this.useFixedPosition);const h=b.getDomNodePagePosition(this.container);this.view.style.top=`${t-(this.useFixedPosition?b.getDomNodePagePosition(this.view).top:h.top)}px`,this.view.style.left=`${l-(this.useFixedPosition?b.getDomNodePagePosition(this.view).left:h.left)}px`,this.view.style.width="initial"}}hide(o){const s=this.delegate;this.delegate=null,(s==null?void 0:s.onHide)&&s.onHide(o),this.toDisposeOnClean.dispose(),b.hide(this.view)}isVisible(){return!!this.delegate}onDOMEvent(o,s){this.delegate&&(this.delegate.onDOMEvent?this.delegate.onDOMEvent(o,document.activeElement):s&&!b.isAncestor(o.target,this.container)&&this.hide())}dispose(){this.hide(),super.dispose()}}e.ContextView=g,g.BUBBLE_UP_EVENTS=["click","keydown","focus","blur"],g.BUBBLE_DOWN_EVENTS=["click"];let p=` + :host { + all: initial; /* 1st rule so subsequent properties are reset. */ + } + + @font-face { + font-family: "codicon"; + src: url("./codicon.ttf?5d4d76ab2ce5108968ad644d591a16a6") format("truetype"); + } + + .codicon[class*='codicon-'] { + font: normal normal normal 16px/1 codicon; + display: inline-block; + text-decoration: none; + text-rendering: auto; + text-align: center; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + } + + :host { + font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", system-ui, "Ubuntu", "Droid Sans", sans-serif; + } + + :host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; } + :host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; } + :host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; } + :host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; } + :host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Nanum Gothic", "Apple SD Gothic Neo", "AppleGothic", sans-serif; } + + :host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; } + :host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; } + :host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; } + :host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; } + :host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; } + + :host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; } + :host-context(.linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; } + :host-context(.linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; } + :host-context(.linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; } + :host-context(.linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; } +`}),define(Q[302],J([10]),{}),define(Q[205],J([0,1,7,8,29,40,302]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CountBadge=void 0;const S={badgeBackground:M.Color.fromHex("#4D4D4D"),badgeForeground:M.Color.fromHex("#FFFFFF")};class C{constructor(g,p){this.count=0,this.options=p||Object.create(null),w.mixin(this.options,S,!1),this.badgeBackground=this.options.badgeBackground,this.badgeForeground=this.options.badgeForeground,this.badgeBorder=this.options.badgeBorder,this.element=b.append(g,b.$(".monaco-count-badge")),this.countFormat=this.options.countFormat||"{0}",this.titleFormat=this.options.titleFormat||"",this.setCount(this.options.count||0)}setCount(g){this.count=g,this.render()}setTitleFormat(g){this.titleFormat=g,this.render()}render(){this.element.textContent=N.format(this.countFormat,this.count),this.element.title=N.format(this.titleFormat,this.count),this.applyStyles()}style(g){this.badgeBackground=g.badgeBackground,this.badgeForeground=g.badgeForeground,this.badgeBorder=g.badgeBorder,this.applyStyles()}applyStyles(){if(this.element){const g=this.badgeBackground?this.badgeBackground.toString():"",p=this.badgeForeground?this.badgeForeground.toString():"",c=this.badgeBorder?this.badgeBorder.toString():"";this.element.style.backgroundColor=g,this.element.style.color=p,this.element.style.borderWidth=c?"1px":"",this.element.style.borderStyle=c?"solid":"",this.element.style.borderColor=c}}}e.CountBadge=C}),define(Q[206],J([10]),{}),define(Q[207],J([10]),{}),define(Q[303],J([10]),{}),define(Q[304],J([10]),{}),define(Q[305],J([10]),{}),define(Q[306],J([10]),{}),define(Q[208],J([10]),{}),define(Q[307],J([10]),{}),define(Q[124],J([0,1,307]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MOUSE_CURSOR_TEXT_CSS_CLASS_NAME=void 0,e.MOUSE_CURSOR_TEXT_CSS_CLASS_NAME="monaco-mouse-cursor-text"}),define(Q[308],J([10]),{}),define(Q[309],J([0,1,2,29,40,7,15,308]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ProgressBar=void 0;const C="done",d="active",g="infinite",p="discrete",c={progressBarBackground:N.Color.fromHex("#0E70C0")};class o extends b.Disposable{constructor(a,u){super();this.options=u||Object.create(null),M.mixin(this.options,c,!1),this.workedVal=0,this.progressBarBackground=this.options.progressBarBackground,this._register(this.showDelayedScheduler=new S.RunOnceScheduler(()=>w.show(this.element),0)),this.create(a)}create(a){this.element=document.createElement("div"),this.element.classList.add("monaco-progress-container"),this.element.setAttribute("role","progressbar"),this.element.setAttribute("aria-valuemin","0"),a.appendChild(this.element),this.bit=document.createElement("div"),this.bit.classList.add("progress-bit"),this.element.appendChild(this.bit),this.applyStyles()}off(){this.bit.style.width="inherit",this.bit.style.opacity="1",this.element.classList.remove(d,g,p),this.workedVal=0,this.totalWork=void 0}stop(){return this.doDone(!1)}doDone(a){return this.element.classList.add(C),this.element.classList.contains(g)?(this.bit.style.opacity="0",a?setTimeout(()=>this.off(),200):this.off()):(this.bit.style.width="inherit",a?setTimeout(()=>this.off(),200):this.off()),this}infinite(){return this.bit.style.width="2%",this.bit.style.opacity="1",this.element.classList.remove(p,C),this.element.classList.add(d,g),this}getContainer(){return this.element}style(a){this.progressBarBackground=a.progressBarBackground,this.applyStyles()}applyStyles(){if(this.bit){const a=this.progressBarBackground?this.progressBarBackground.toString():"";this.bit.style.backgroundColor=a}}}e.ProgressBar=o}),define(Q[310],J([10]),{}),define(Q[104],J([0,1,2,17,20,60,50,6,7,55,15,310]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Sash=e.OrthogonalEdge=void 0;let c=!1;var o;(function(r){r.North="north",r.South="south",r.East="east",r.West="west"})(o=e.OrthogonalEdge||(e.OrthogonalEdge={}));let s=4;const a=new C.Emitter;class u extends b.Disposable{constructor(i,n,t){super();this.hoverDelayer=this._register(new p.Delayer(300)),this._state=3,this._onDidEnablementChange=this._register(new C.Emitter),this.onDidEnablementChange=this._onDidEnablementChange.event,this._onDidStart=this._register(new C.Emitter),this.onDidStart=this._onDidStart.event,this._onDidChange=this._register(new C.Emitter),this.onDidChange=this._onDidChange.event,this._onDidReset=this._register(new C.Emitter),this.onDidReset=this._onDidReset.event,this._onDidEnd=this._register(new C.Emitter),this.onDidEnd=this._onDidEnd.event,this.linkedSash=void 0,this.orthogonalStartSashDisposables=this._register(new b.DisposableStore),this.orthogonalStartDragHandleDisposables=this._register(new b.DisposableStore),this.orthogonalEndSashDisposables=this._register(new b.DisposableStore),this.orthogonalEndDragHandleDisposables=this._register(new b.DisposableStore),this.el=d.append(i,d.$(".monaco-sash")),t.orthogonalEdge&&this.el.classList.add(`orthogonal-edge-${t.orthogonalEdge}`),N.isMacintosh&&this.el.classList.add("mac"),this._register(g.domEvent(this.el,"mousedown")(this.onMouseDown,this)),this._register(g.domEvent(this.el,"dblclick")(this.onMouseDoubleClick,this)),this._register(g.domEvent(this.el,"mouseenter")(()=>u.onMouseEnter(this))),this._register(g.domEvent(this.el,"mouseleave")(()=>u.onMouseLeave(this))),this._register(w.Gesture.addTarget(this.el)),this._register(g.domEvent(this.el,w.EventType.Start)(this.onTouchStart,this)),typeof t.size=="number"?(this.size=t.size,t.orientation===0?this.el.style.width=`${this.size}px`:this.el.style.height=`${this.size}px`):(this.size=s,this._register(a.event(l=>{this.size=l,this.layout()}))),this.hidden=!1,this.layoutProvider=n,this.orthogonalStartSash=t.orthogonalStartSash,this.orthogonalEndSash=t.orthogonalEndSash,this.orientation=t.orientation||0,this.orientation===1?(this.el.classList.add("horizontal"),this.el.classList.remove("vertical")):(this.el.classList.remove("horizontal"),this.el.classList.add("vertical")),this.el.classList.toggle("debug",c),this.layout()}get state(){return this._state}set state(i){this._state!==i&&(this.el.classList.toggle("disabled",i===0),this.el.classList.toggle("minimum",i===1),this.el.classList.toggle("maximum",i===2),this._state=i,this._onDidEnablementChange.fire(i))}get orthogonalStartSash(){return this._orthogonalStartSash}set orthogonalStartSash(i){if(this.orthogonalStartDragHandleDisposables.clear(),this.orthogonalStartSashDisposables.clear(),i){const n=t=>{this.orthogonalStartDragHandleDisposables.clear(),t!==0&&(this._orthogonalStartDragHandle=d.append(this.el,d.$(".orthogonal-drag-handle.start")),this.orthogonalStartDragHandleDisposables.add(b.toDisposable(()=>this._orthogonalStartDragHandle.remove())),g.domEvent(this._orthogonalStartDragHandle,"mouseenter")(()=>u.onMouseEnter(i),void 0,this.orthogonalStartDragHandleDisposables),g.domEvent(this._orthogonalStartDragHandle,"mouseleave")(()=>u.onMouseLeave(i),void 0,this.orthogonalStartDragHandleDisposables))};this.orthogonalStartSashDisposables.add(i.onDidEnablementChange(n,this)),n(i.state)}this._orthogonalStartSash=i}get orthogonalEndSash(){return this._orthogonalEndSash}set orthogonalEndSash(i){if(this.orthogonalEndDragHandleDisposables.clear(),this.orthogonalEndSashDisposables.clear(),i){const n=t=>{this.orthogonalEndDragHandleDisposables.clear(),t!==0&&(this._orthogonalEndDragHandle=d.append(this.el,d.$(".orthogonal-drag-handle.end")),this.orthogonalEndDragHandleDisposables.add(b.toDisposable(()=>this._orthogonalEndDragHandle.remove())),g.domEvent(this._orthogonalEndDragHandle,"mouseenter")(()=>u.onMouseEnter(i),void 0,this.orthogonalEndDragHandleDisposables),g.domEvent(this._orthogonalEndDragHandle,"mouseleave")(()=>u.onMouseLeave(i),void 0,this.orthogonalEndDragHandleDisposables))};this.orthogonalEndSashDisposables.add(i.onDidEnablementChange(n,this)),n(i.state)}this._orthogonalEndSash=i}onMouseDown(i){d.EventHelper.stop(i,!1);let n=!1;if(!i.__orthogonalSashEvent){const E=this.getOrthogonalSash(i);E&&(n=!0,i.__orthogonalSashEvent=!0,E.onMouseDown(i))}if(this.linkedSash&&!i.__linkedSashEvent&&(i.__linkedSashEvent=!0,this.linkedSash.onMouseDown(i)),!!this.state){const t=[...d.getElementsByTagName("iframe"),...d.getElementsByTagName("webview")];for(const E of t)E.style.pointerEvents="none";const l=new S.StandardMouseEvent(i),h=l.posx,m=l.posy,_=l.altKey,f={startX:h,currentX:h,startY:m,currentY:m,altKey:_};this.el.classList.add("active"),this._onDidStart.fire(f);const v=d.createStyleSheet(this.el),y=()=>{let E="";n?E="all-scroll":this.orientation===1?this.state===1?E="s-resize":this.state===2?E="n-resize":E=N.isMacintosh?"row-resize":"ns-resize":this.state===1?E="e-resize":this.state===2?E="w-resize":E=N.isMacintosh?"col-resize":"ew-resize",v.textContent=`* { cursor: ${E} !important; }`},L=new b.DisposableStore;y(),n||this.onDidEnablementChange(y,null,L);const I=E=>{d.EventHelper.stop(E,!1);const T=new S.StandardMouseEvent(E),O={startX:h,currentX:T.posx,startY:m,currentY:T.posy,altKey:_};this._onDidChange.fire(O)},k=E=>{d.EventHelper.stop(E,!1),this.el.removeChild(v),this.el.classList.remove("active"),this._onDidEnd.fire(),L.dispose();for(const T of t)T.style.pointerEvents="auto"};g.domEvent(window,"mousemove")(I,null,L),g.domEvent(window,"mouseup")(k,null,L)}}onMouseDoubleClick(i){const n=this.getOrthogonalSash(i);n&&n._onDidReset.fire(),this.linkedSash&&this.linkedSash._onDidReset.fire(),this._onDidReset.fire()}onTouchStart(i){d.EventHelper.stop(i);const n=[],t=i.pageX,l=i.pageY,h=i.altKey;this._onDidStart.fire({startX:t,currentX:t,startY:l,currentY:l,altKey:h}),n.push(d.addDisposableListener(this.el,w.EventType.Change,m=>{M.isNumber(m.pageX)&&M.isNumber(m.pageY)&&this._onDidChange.fire({startX:t,currentX:m.pageX,startY:l,currentY:m.pageY,altKey:h})})),n.push(d.addDisposableListener(this.el,w.EventType.End,()=>{this._onDidEnd.fire(),b.dispose(n)}))}static onMouseEnter(i,n=!1){i.el.classList.contains("active")?(i.hoverDelayer.cancel(),i.el.classList.add("hover")):i.hoverDelayer.trigger(()=>i.el.classList.add("hover")),!n&&i.linkedSash&&u.onMouseEnter(i.linkedSash,!0)}static onMouseLeave(i,n=!1){i.hoverDelayer.cancel(),i.el.classList.remove("hover"),!n&&i.linkedSash&&u.onMouseLeave(i.linkedSash,!0)}layout(){if(this.orientation===0){const i=this.layoutProvider;this.el.style.left=i.getVerticalSashLeft(this)-this.size/2+"px",i.getVerticalSashTop&&(this.el.style.top=i.getVerticalSashTop(this)+"px"),i.getVerticalSashHeight&&(this.el.style.height=i.getVerticalSashHeight(this)+"px")}else{const i=this.layoutProvider;this.el.style.top=i.getHorizontalSashTop(this)-this.size/2+"px",i.getHorizontalSashLeft&&(this.el.style.left=i.getHorizontalSashLeft(this)+"px"),i.getHorizontalSashWidth&&(this.el.style.width=i.getHorizontalSashWidth(this)+"px")}}hide(){this.hidden=!0,this.el.style.display="none",this.el.setAttribute("aria-hidden","true")}getOrthogonalSash(i){if(!(!i.target||!(i.target instanceof HTMLElement))&&i.target.classList.contains("orthogonal-drag-handle"))return i.target.classList.contains("start")?this.orthogonalStartSash:this.orthogonalEndSash}dispose(){super.dispose(),this.el.remove()}}e.Sash=u}),define(Q[311],J([10]),{}),define(Q[61],J([0,1,7,30,50,288,289,52,15,6,2,17,121,35,311]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DomScrollableElement=e.SmoothScrollableElement=e.ScrollableElement=e.AbstractScrollableElement=e.MouseWheelClassifier=void 0;const a=500,u=50,r=!0;class i{constructor(v,y,L){this.timestamp=v,this.deltaX=y,this.deltaY=L,this.score=0}}class n{constructor(){this._capacity=5,this._memory=[],this._front=-1,this._rear=-1}isPhysicalMouseWheel(){if(this._front===-1&&this._rear===-1)return!1;let v=1,y=0,L=1,I=this._rear;do{const k=I===this._front?v:Math.pow(2,-L);if(v-=k,y+=this._memory[I].score*k,I===this._front)break;I=(this._capacity+I-1)%this._capacity,L++}while(!0);return y<=.5}accept(v,y,L){const I=new i(v,y,L);I.score=this._computeScore(I),this._front===-1&&this._rear===-1?(this._memory[0]=I,this._front=0,this._rear=0):(this._rear=(this._rear+1)%this._capacity,this._rear===this._front&&(this._front=(this._front+1)%this._capacity),this._memory[this._rear]=I)}_computeScore(v){if(Math.abs(v.deltaX)>0&&Math.abs(v.deltaY)>0)return 1;let y=.5;const L=this._front===-1&&this._rear===-1?null:this._memory[this._rear];return(!this._isAlmostInt(v.deltaX)||!this._isAlmostInt(v.deltaY))&&(y+=.25),Math.min(Math.max(y,0),1)}_isAlmostInt(v){return Math.abs(Math.round(v)-v)<.01}}e.MouseWheelClassifier=n,n.INSTANCE=new n;class t extends C.Widget{constructor(v,y,L){super();this._onScroll=this._register(new g.Emitter),this.onScroll=this._onScroll.event,this._onWillScroll=this._register(new g.Emitter),v.style.overflow="hidden",this._options=_(y),this._scrollable=L,this._register(this._scrollable.onScroll(k=>{this._onWillScroll.fire(k),this._onDidScroll(k),this._onScroll.fire(k)}));const I={onMouseWheel:k=>this._onMouseWheel(k),onDragStart:()=>this._onDragStart(),onDragEnd:()=>this._onDragEnd()};this._verticalScrollbar=this._register(new S.VerticalScrollbar(this._scrollable,this._options,I)),this._horizontalScrollbar=this._register(new w.HorizontalScrollbar(this._scrollable,this._options,I)),this._domNode=document.createElement("div"),this._domNode.className="monaco-scrollable-element "+this._options.className,this._domNode.setAttribute("role","presentation"),this._domNode.style.position="relative",this._domNode.style.overflow="hidden",this._domNode.appendChild(v),this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode),this._domNode.appendChild(this._verticalScrollbar.domNode.domNode),this._options.useShadows?(this._leftShadowDomNode=N.createFastDomNode(document.createElement("div")),this._leftShadowDomNode.setClassName("shadow"),this._domNode.appendChild(this._leftShadowDomNode.domNode),this._topShadowDomNode=N.createFastDomNode(document.createElement("div")),this._topShadowDomNode.setClassName("shadow"),this._domNode.appendChild(this._topShadowDomNode.domNode),this._topLeftShadowDomNode=N.createFastDomNode(document.createElement("div")),this._topLeftShadowDomNode.setClassName("shadow top-left-corner"),this._domNode.appendChild(this._topLeftShadowDomNode.domNode)):(this._leftShadowDomNode=null,this._topShadowDomNode=null,this._topLeftShadowDomNode=null),this._listenOnDomNode=this._options.listenOnDomNode||this._domNode,this._mouseWheelToDispose=[],this._setListeningToMouseWheel(this._options.handleMouseWheel),this.onmouseover(this._listenOnDomNode,k=>this._onMouseOver(k)),this.onnonbubblingmouseout(this._listenOnDomNode,k=>this._onMouseOut(k)),this._hideTimeout=this._register(new d.TimeoutTimer),this._isDragging=!1,this._mouseIsOver=!1,this._shouldRender=!0,this._revealOnScroll=!0}dispose(){this._mouseWheelToDispose=p.dispose(this._mouseWheelToDispose),super.dispose()}getDomNode(){return this._domNode}getOverviewRulerLayoutInfo(){return{parent:this._domNode,insertBefore:this._verticalScrollbar.domNode.domNode}}delegateVerticalScrollbarMouseDown(v){this._verticalScrollbar.delegateMouseDown(v)}getScrollDimensions(){return this._scrollable.getScrollDimensions()}setScrollDimensions(v){this._scrollable.setScrollDimensions(v,!1)}updateClassName(v){this._options.className=v,c.isMacintosh&&(this._options.className+=" mac"),this._domNode.className="monaco-scrollable-element "+this._options.className}updateOptions(v){typeof v.handleMouseWheel!="undefined"&&(this._options.handleMouseWheel=v.handleMouseWheel,this._setListeningToMouseWheel(this._options.handleMouseWheel)),typeof v.mouseWheelScrollSensitivity!="undefined"&&(this._options.mouseWheelScrollSensitivity=v.mouseWheelScrollSensitivity),typeof v.fastScrollSensitivity!="undefined"&&(this._options.fastScrollSensitivity=v.fastScrollSensitivity),typeof v.scrollPredominantAxis!="undefined"&&(this._options.scrollPredominantAxis=v.scrollPredominantAxis),typeof v.horizontalScrollbarSize!="undefined"&&this._horizontalScrollbar.updateScrollbarSize(v.horizontalScrollbarSize),this._options.lazyRender||this._render()}_setListeningToMouseWheel(v){if(this._mouseWheelToDispose.length>0!==v&&(this._mouseWheelToDispose=p.dispose(this._mouseWheelToDispose),v)){const L=I=>{this._onMouseWheel(new M.StandardWheelEvent(I))};this._mouseWheelToDispose.push(b.addDisposableListener(this._listenOnDomNode,b.EventType.MOUSE_WHEEL,L,{passive:!1}))}}_onMouseWheel(v){const y=n.INSTANCE;if(r){const k=window.devicePixelRatio/s.getZoomFactor();c.isWindows||c.isLinux?y.accept(Date.now(),v.deltaX/k,v.deltaY/k):y.accept(Date.now(),v.deltaX,v.deltaY)}let L=!1;if(v.deltaY||v.deltaX){let k=v.deltaY*this._options.mouseWheelScrollSensitivity,E=v.deltaX*this._options.mouseWheelScrollSensitivity;this._options.scrollPredominantAxis&&(Math.abs(k)>=Math.abs(E)?E=0:k=0),this._options.flipAxes&&([k,E]=[E,k]);const T=!c.isMacintosh&&v.browserEvent&&v.browserEvent.shiftKey;(this._options.scrollYToX||T)&&!E&&(E=k,k=0),v.browserEvent&&v.browserEvent.altKey&&(E=E*this._options.fastScrollSensitivity,k=k*this._options.fastScrollSensitivity);const O=this._scrollable.getFutureScrollPosition();let A={};if(k){const B=O.scrollTop-u*k;this._verticalScrollbar.writeScrollPosition(A,B)}if(E){const B=O.scrollLeft-u*E;this._horizontalScrollbar.writeScrollPosition(A,B)}A=this._scrollable.validateScrollPosition(A),(O.scrollLeft!==A.scrollLeft||O.scrollTop!==A.scrollTop)&&(r&&this._options.mouseWheelSmoothScroll&&y.isPhysicalMouseWheel()?this._scrollable.setScrollPositionSmooth(A):this._scrollable.setScrollPositionNow(A),L=!0)}let I=L;!I&&this._options.alwaysConsumeMouseWheel&&(I=!0),!I&&this._options.consumeMouseWheelIfScrollbarIsNeeded&&(this._verticalScrollbar.isNeeded()||this._horizontalScrollbar.isNeeded())&&(I=!0),I&&(v.preventDefault(),v.stopPropagation())}_onDidScroll(v){this._shouldRender=this._horizontalScrollbar.onDidScroll(v)||this._shouldRender,this._shouldRender=this._verticalScrollbar.onDidScroll(v)||this._shouldRender,this._options.useShadows&&(this._shouldRender=!0),this._revealOnScroll&&this._reveal(),this._options.lazyRender||this._render()}renderNow(){if(!this._options.lazyRender)throw new Error("Please use `lazyRender` together with `renderNow`!");this._render()}_render(){if(!!this._shouldRender&&(this._shouldRender=!1,this._horizontalScrollbar.render(),this._verticalScrollbar.render(),this._options.useShadows)){const v=this._scrollable.getCurrentScrollPosition(),y=v.scrollTop>0,L=v.scrollLeft>0;this._leftShadowDomNode.setClassName("shadow"+(L?" left":"")),this._topShadowDomNode.setClassName("shadow"+(y?" top":"")),this._topLeftShadowDomNode.setClassName("shadow top-left-corner"+(y?" top":"")+(L?" left":""))}}_onDragStart(){this._isDragging=!0,this._reveal()}_onDragEnd(){this._isDragging=!1,this._hide()}_onMouseOut(v){this._mouseIsOver=!1,this._hide()}_onMouseOver(v){this._mouseIsOver=!0,this._reveal()}_reveal(){this._verticalScrollbar.beginReveal(),this._horizontalScrollbar.beginReveal(),this._scheduleHide()}_hide(){!this._mouseIsOver&&!this._isDragging&&(this._verticalScrollbar.beginHide(),this._horizontalScrollbar.beginHide())}_scheduleHide(){!this._mouseIsOver&&!this._isDragging&&this._hideTimeout.cancelAndSet(()=>this._hide(),a)}}e.AbstractScrollableElement=t;class l extends t{constructor(v,y){y=y||{},y.mouseWheelSmoothScroll=!1;const L=new o.Scrollable(0,I=>b.scheduleAtNextAnimationFrame(I));super(v,y,L);this._register(L)}setScrollPosition(v){this._scrollable.setScrollPositionNow(v)}}e.ScrollableElement=l;class h extends t{constructor(v,y,L){super(v,y,L)}setScrollPosition(v){v.reuseAnimation?this._scrollable.setScrollPositionSmooth(v,v.reuseAnimation):this._scrollable.setScrollPositionNow(v)}getScrollPosition(){return this._scrollable.getCurrentScrollPosition()}}e.SmoothScrollableElement=h;class m extends l{constructor(v,y){super(v,y);this._element=v,this.onScroll(L=>{L.scrollTopChanged&&(this._element.scrollTop=L.scrollTop),L.scrollLeftChanged&&(this._element.scrollLeft=L.scrollLeft)}),this.scanDomNode()}scanDomNode(){this.setScrollDimensions({width:this._element.clientWidth,scrollWidth:this._element.scrollWidth,height:this._element.clientHeight,scrollHeight:this._element.scrollHeight}),this.setScrollPosition({scrollLeft:this._element.scrollLeft,scrollTop:this._element.scrollTop})}}e.DomScrollableElement=m;function _(f){const v={lazyRender:typeof f.lazyRender!="undefined"?f.lazyRender:!1,className:typeof f.className!="undefined"?f.className:"",useShadows:typeof f.useShadows!="undefined"?f.useShadows:!0,handleMouseWheel:typeof f.handleMouseWheel!="undefined"?f.handleMouseWheel:!0,flipAxes:typeof f.flipAxes!="undefined"?f.flipAxes:!1,consumeMouseWheelIfScrollbarIsNeeded:typeof f.consumeMouseWheelIfScrollbarIsNeeded!="undefined"?f.consumeMouseWheelIfScrollbarIsNeeded:!1,alwaysConsumeMouseWheel:typeof f.alwaysConsumeMouseWheel!="undefined"?f.alwaysConsumeMouseWheel:!1,scrollYToX:typeof f.scrollYToX!="undefined"?f.scrollYToX:!1,mouseWheelScrollSensitivity:typeof f.mouseWheelScrollSensitivity!="undefined"?f.mouseWheelScrollSensitivity:1,fastScrollSensitivity:typeof f.fastScrollSensitivity!="undefined"?f.fastScrollSensitivity:5,scrollPredominantAxis:typeof f.scrollPredominantAxis!="undefined"?f.scrollPredominantAxis:!0,mouseWheelSmoothScroll:typeof f.mouseWheelSmoothScroll!="undefined"?f.mouseWheelSmoothScroll:!0,arrowSize:typeof f.arrowSize!="undefined"?f.arrowSize:11,listenOnDomNode:typeof f.listenOnDomNode!="undefined"?f.listenOnDomNode:null,horizontal:typeof f.horizontal!="undefined"?f.horizontal:1,horizontalScrollbarSize:typeof f.horizontalScrollbarSize!="undefined"?f.horizontalScrollbarSize:10,horizontalSliderSize:typeof f.horizontalSliderSize!="undefined"?f.horizontalSliderSize:0,horizontalHasArrows:typeof f.horizontalHasArrows!="undefined"?f.horizontalHasArrows:!1,vertical:typeof f.vertical!="undefined"?f.vertical:1,verticalScrollbarSize:typeof f.verticalScrollbarSize!="undefined"?f.verticalScrollbarSize:10,verticalHasArrows:typeof f.verticalHasArrows!="undefined"?f.verticalHasArrows:!1,verticalSliderSize:typeof f.verticalSliderSize!="undefined"?f.verticalSliderSize:0,scrollByPage:typeof f.scrollByPage!="undefined"?f.scrollByPage:!1};return v.horizontalSliderSize=typeof f.horizontalSliderSize!="undefined"?f.horizontalSliderSize:v.horizontalScrollbarSize,v.verticalSliderSize=typeof f.verticalSliderSize!="undefined"?f.verticalSliderSize:v.verticalScrollbarSize,c.isMacintosh&&(v.className+=" mac"),v}}),define(Q[209],J([0,1,7,2,61,303]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.renderHoverAction=e.HoverWidget=void 0;const w=b.$;class S extends N.Disposable{constructor(){super();this.containerDomNode=document.createElement("div"),this.containerDomNode.className="monaco-hover",this.containerDomNode.tabIndex=0,this.containerDomNode.setAttribute("role","tooltip"),this.contentsDomNode=document.createElement("div"),this.contentsDomNode.className="monaco-hover-content",this._scrollbar=this._register(new M.DomScrollableElement(this.contentsDomNode,{consumeMouseWheelIfScrollbarIsNeeded:!0})),this.containerDomNode.appendChild(this._scrollbar.getDomNode())}onContentsChanged(){this._scrollbar.scanDomNode()}}e.HoverWidget=S;function C(d,g,p){const c=b.append(d,w("div.action-container")),o=b.append(c,w("a.action"));o.setAttribute("href","#"),o.setAttribute("role","button"),g.iconClass&&b.append(o,w(`span.icon.${g.iconClass}`));const s=b.append(o,w("span"));return s.textContent=p?`${g.label} (${p})`:g.label,b.addDisposableListener(c,b.EventType.CLICK,a=>{a.stopPropagation(),a.preventDefault(),g.run(c)})}e.renderHoverAction=C}),define(Q[161],J([0,1,40,2,60,6,55,61,121,281,287,99,120,19,149,15,35,7]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ListView=e.NativeDragAndDropData=e.ExternalElementsDragAndDropData=e.ElementsDragAndDropData=void 0;const n={useShadows:!0,verticalScrollMode:1,setRowLineHeight:!0,setRowHeight:!0,supportDynamicHeights:!1,dnd:{getDragElements(v){return[v]},getDragURI(){return null},onDragStart(){},onDragOver(){return!1},drop(){}},horizontalScrolling:!1,transformOptimization:!0,alwaysConsumeMouseWheel:!0};class t{constructor(y){this.elements=y}update(){}getData(){return this.elements}}e.ElementsDragAndDropData=t;class l{constructor(y){this.elements=y}update(){}getData(){return this.elements}}e.ExternalElementsDragAndDropData=l;class h{constructor(){this.types=[],this.files=[]}update(y){if(y.types&&this.types.splice(0,this.types.length,...y.types),y.files){this.files.splice(0,this.files.length);for(let L=0;Lk,(y==null?void 0:y.getPosInSet)?this.getPosInSet=y.getPosInSet.bind(y):this.getPosInSet=(L,I)=>I+1,(y==null?void 0:y.getRole)?this.getRole=y.getRole.bind(y):this.getRole=L=>"listitem",(y==null?void 0:y.isChecked)?this.isChecked=y.isChecked.bind(y):this.isChecked=L=>{}}}class f{constructor(y,L,I,k=n){if(this.virtualDelegate=L,this.domId=`list_id_${++f.InstanceCount}`,this.renderers=new Map,this.renderWidth=0,this._scrollHeight=0,this.scrollableElementUpdateDisposable=null,this.scrollableElementWidthDelayer=new u.Delayer(50),this.splicing=!1,this.dragOverAnimationStopDisposable=N.Disposable.None,this.dragOverMouseY=0,this.canDrop=!1,this.currentDragFeedbackDisposable=N.Disposable.None,this.onDragLeaveTimeout=N.Disposable.None,this.disposables=new N.DisposableStore,this._onDidChangeContentHeight=new w.Emitter,this._horizontalScrolling=!1,k.horizontalScrolling&&k.supportDynamicHeights)throw new Error("Horizontal scrolling and dynamic heights not supported simultaneously");this.items=[],this.itemId=0,this.rangeMap=new g.RangeMap;for(const T of I)this.renderers.set(T.templateId,T);this.cache=this.disposables.add(new p.RowCache(this.renderers)),this.lastRenderTop=0,this.lastRenderHeight=0,this.domNode=document.createElement("div"),this.domNode.className="monaco-list",this.domNode.classList.add(this.domId),this.domNode.tabIndex=0,this.domNode.classList.toggle("mouse-support",typeof k.mouseSupport=="boolean"?k.mouseSupport:!0),this._horizontalScrolling=b.getOrDefault(k,T=>T.horizontalScrolling,n.horizontalScrolling),this.domNode.classList.toggle("horizontal-scrolling",this._horizontalScrolling),this.additionalScrollHeight=typeof k.additionalScrollHeight=="undefined"?0:k.additionalScrollHeight,this.accessibilityProvider=new _(k.accessibilityProvider),this.rowsContainer=document.createElement("div"),this.rowsContainer.className="monaco-list-rows",b.getOrDefault(k,T=>T.transformOptimization,n.transformOptimization)&&(this.rowsContainer.style.transform="translate3d(0px, 0px, 0px)"),this.disposables.add(M.Gesture.addTarget(this.rowsContainer)),this.scrollable=new d.Scrollable(b.getOrDefault(k,T=>T.smoothScrolling,!1)?125:0,T=>i.scheduleAtNextAnimationFrame(T)),this.scrollableElement=this.disposables.add(new C.SmoothScrollableElement(this.rowsContainer,{alwaysConsumeMouseWheel:b.getOrDefault(k,T=>T.alwaysConsumeMouseWheel,n.alwaysConsumeMouseWheel),horizontal:1,vertical:b.getOrDefault(k,T=>T.verticalScrollMode,n.verticalScrollMode),useShadows:b.getOrDefault(k,T=>T.useShadows,n.useShadows)},this.scrollable)),this.domNode.appendChild(this.scrollableElement.getDomNode()),y.appendChild(this.domNode),this.scrollableElement.onScroll(this.onScroll,this,this.disposables),S.domEvent(this.rowsContainer,M.EventType.Change)(this.onTouchChange,this,this.disposables),S.domEvent(this.scrollableElement.getDomNode(),"scroll")(T=>T.target.scrollTop=0,null,this.disposables),w.Event.map(S.domEvent(this.domNode,"dragover"),T=>this.toDragEvent(T))(this.onDragOver,this,this.disposables),w.Event.map(S.domEvent(this.domNode,"drop"),T=>this.toDragEvent(T))(this.onDrop,this,this.disposables),S.domEvent(this.domNode,"dragleave")(this.onDragLeave,this,this.disposables),S.domEvent(window,"dragend")(this.onDragEnd,this,this.disposables),this.setRowLineHeight=b.getOrDefault(k,T=>T.setRowLineHeight,n.setRowLineHeight),this.setRowHeight=b.getOrDefault(k,T=>T.setRowHeight,n.setRowHeight),this.supportDynamicHeights=b.getOrDefault(k,T=>T.supportDynamicHeights,n.supportDynamicHeights),this.dnd=b.getOrDefault(k,T=>T.dnd,n.dnd),this.layout()}get contentHeight(){return this.rangeMap.size}get horizontalScrolling(){return this._horizontalScrolling}set horizontalScrolling(y){if(y!==this._horizontalScrolling){if(y&&this.supportDynamicHeights)throw new Error("Horizontal scrolling and dynamic heights not supported simultaneously");if(this._horizontalScrolling=y,this.domNode.classList.toggle("horizontal-scrolling",this._horizontalScrolling),this._horizontalScrolling){for(const L of this.items)this.measureItemWidth(L);this.updateScrollWidth(),this.scrollableElement.setScrollDimensions({width:i.getContentWidth(this.domNode)}),this.rowsContainer.style.width=`${Math.max(this.scrollWidth||0,this.renderWidth)}px`}else this.scrollableElementWidthDelayer.cancel(),this.scrollableElement.setScrollDimensions({width:this.renderWidth,scrollWidth:this.renderWidth}),this.rowsContainer.style.width=""}}updateOptions(y){y.additionalScrollHeight!==void 0&&(this.additionalScrollHeight=y.additionalScrollHeight),y.smoothScrolling!==void 0&&this.scrollable.setSmoothScrollDuration(y.smoothScrolling?125:0),y.horizontalScrolling!==void 0&&(this.horizontalScrolling=y.horizontalScrolling)}splice(y,L,I=[]){if(this.splicing)throw new Error("Can't run recursive splices.");this.splicing=!0;try{return this._splice(y,L,I)}finally{this.splicing=!1,this._onDidChangeContentHeight.fire(this.contentHeight)}}_splice(y,L,I=[]){const k=this.getRenderRange(this.lastRenderTop,this.lastRenderHeight),E={start:y,end:y+L},T=o.Range.intersect(k,E),O=new Map;for(let z=T.start;z({id:String(this.itemId++),element:z,templateId:this.virtualDelegate.getTemplateId(z),size:this.virtualDelegate.getHeight(z),width:void 0,hasDynamicHeight:!!this.virtualDelegate.hasDynamicHeight&&this.virtualDelegate.hasDynamicHeight(z),lastDynamicHeightWidth:void 0,row:null,uri:void 0,dropTarget:!1,dragStartDisposable:N.Disposable.None}));let R;y===0&&L>=this.items.length?(this.rangeMap=new g.RangeMap,this.rangeMap.splice(0,0,D),R=this.items,this.items=D):(this.rangeMap.splice(y,L,D),R=this.items.splice(y,L,...D));const W=I.length-L,x=this.getRenderRange(this.lastRenderTop,this.lastRenderHeight),K=g.shift(B,W),Y=o.Range.intersect(x,K);for(let z=Y.start;zg.shift(z,W)),le=[{start:y,end:y+I.length},...se].map(z=>o.Range.intersect(x,z)),X=this.getNextToLastElement(le);for(const z of le)for(let P=z.start;Pz.element)}eventuallyUpdateScrollDimensions(){this._scrollHeight=this.contentHeight,this.rowsContainer.style.height=`${this._scrollHeight}px`,this.scrollableElementUpdateDisposable||(this.scrollableElementUpdateDisposable=i.scheduleAtNextAnimationFrame(()=>{this.scrollableElement.setScrollDimensions({scrollHeight:this.scrollHeight}),this.updateScrollWidth(),this.scrollableElementUpdateDisposable=null}))}eventuallyUpdateScrollWidth(){if(!this.horizontalScrolling){this.scrollableElementWidthDelayer.cancel();return}this.scrollableElementWidthDelayer.trigger(()=>this.updateScrollWidth())}updateScrollWidth(){if(!!this.horizontalScrolling){let y=0;for(const L of this.items)typeof L.width!="undefined"&&(y=Math.max(y,L.width));this.scrollWidth=y,this.scrollableElement.setScrollDimensions({scrollWidth:y===0?0:y+10})}}rerender(){if(!!this.supportDynamicHeights){for(const y of this.items)y.lastDynamicHeightWidth=void 0;this._rerender(this.lastRenderTop,this.lastRenderHeight)}}get length(){return this.items.length}get renderHeight(){return this.scrollableElement.getScrollDimensions().height}element(y){return this.items[y].element}domElement(y){const L=this.items[y].row;return L&&L.domNode}elementHeight(y){return this.items[y].size}elementTop(y){return this.rangeMap.positionAt(y)}indexAt(y){return this.rangeMap.indexAt(y)}indexAfter(y){return this.rangeMap.indexAfter(y)}layout(y,L){let I={height:typeof y=="number"?y:i.getContentHeight(this.domNode)};this.scrollableElementUpdateDisposable&&(this.scrollableElementUpdateDisposable.dispose(),this.scrollableElementUpdateDisposable=null,I.scrollHeight=this.scrollHeight),this.scrollableElement.setScrollDimensions(I),typeof L!="undefined"&&(this.renderWidth=L,this.supportDynamicHeights&&this._rerender(this.scrollTop,this.renderHeight),this.horizontalScrolling&&this.scrollableElement.setScrollDimensions({width:typeof L=="number"?L:i.getContentWidth(this.domNode)}))}render(y,L,I,k,E,T=!1){const O=this.getRenderRange(L,I),A=o.Range.relativeComplement(O,y),B=o.Range.relativeComplement(y,O),F=this.getNextToLastElement(A);if(T){const D=o.Range.intersect(y,O);for(let R=D.start;Rthis.onDragStart(k.element,A,F))}this.horizontalScrolling&&(this.measureItemWidth(k),this.eventuallyUpdateScrollWidth())}measureItemWidth(y){if(!(!y.row||!y.row.domNode)){y.row.domNode.style.width=r.isFirefox?"-moz-fit-content":"fit-content",y.width=i.getContentWidth(y.row.domNode);const L=window.getComputedStyle(y.row.domNode);L.paddingLeft&&(y.width+=parseFloat(L.paddingLeft)),L.paddingRight&&(y.width+=parseFloat(L.paddingRight)),y.row.domNode.style.width=""}}updateItemInDOM(y,L){y.row.domNode.style.top=`${this.elementTop(L)}px`,this.setRowHeight&&(y.row.domNode.style.height=`${y.size}px`),this.setRowLineHeight&&(y.row.domNode.style.lineHeight=`${y.size}px`),y.row.domNode.setAttribute("data-index",`${L}`),y.row.domNode.setAttribute("data-last-element",L===this.length-1?"true":"false"),y.row.domNode.setAttribute("aria-setsize",String(this.accessibilityProvider.getSetSize(y.element,L,this.length))),y.row.domNode.setAttribute("aria-posinset",String(this.accessibilityProvider.getPosInSet(y.element,L))),y.row.domNode.setAttribute("id",this.getElementDomId(L)),y.row.domNode.classList.toggle("drop-target",y.dropTarget)}removeItemFromDOM(y){const L=this.items[y];if(L.dragStartDisposable.dispose(),L.row){const I=this.renderers.get(L.templateId);I&&I.disposeElement&&I.disposeElement(L.element,y,L.row.templateData,L.size),this.cache.release(L.row),L.row=null}this.horizontalScrolling&&this.eventuallyUpdateScrollWidth()}getScrollTop(){return this.scrollableElement.getScrollPosition().scrollTop}setScrollTop(y,L){this.scrollableElementUpdateDisposable&&(this.scrollableElementUpdateDisposable.dispose(),this.scrollableElementUpdateDisposable=null,this.scrollableElement.setScrollDimensions({scrollHeight:this.scrollHeight})),this.scrollableElement.setScrollPosition({scrollTop:y,reuseAnimation:L})}get scrollTop(){return this.getScrollTop()}set scrollTop(y){this.setScrollTop(y)}get scrollHeight(){return this._scrollHeight+(this.horizontalScrolling?10:0)+this.additionalScrollHeight}get onMouseClick(){return w.Event.map(S.domEvent(this.domNode,"click"),y=>this.toMouseEvent(y))}get onMouseDblClick(){return w.Event.map(S.domEvent(this.domNode,"dblclick"),y=>this.toMouseEvent(y))}get onMouseMiddleClick(){return w.Event.filter(w.Event.map(S.domEvent(this.domNode,"auxclick"),y=>this.toMouseEvent(y)),y=>y.browserEvent.button===1)}get onMouseDown(){return w.Event.map(S.domEvent(this.domNode,"mousedown"),y=>this.toMouseEvent(y))}get onContextMenu(){return w.Event.map(S.domEvent(this.domNode,"contextmenu"),y=>this.toMouseEvent(y))}get onTouchStart(){return w.Event.map(S.domEvent(this.domNode,"touchstart"),y=>this.toTouchEvent(y))}get onTap(){return w.Event.map(S.domEvent(this.rowsContainer,M.EventType.Tap),y=>this.toGestureEvent(y))}toMouseEvent(y){const L=this.getItemIndexFromEventTarget(y.target||null),I=typeof L=="undefined"?void 0:this.items[L],k=I&&I.element;return{browserEvent:y,index:L,element:k}}toTouchEvent(y){const L=this.getItemIndexFromEventTarget(y.target||null),I=typeof L=="undefined"?void 0:this.items[L],k=I&&I.element;return{browserEvent:y,index:L,element:k}}toGestureEvent(y){const L=this.getItemIndexFromEventTarget(y.initialTarget||null),I=typeof L=="undefined"?void 0:this.items[L],k=I&&I.element;return{browserEvent:y,index:L,element:k}}toDragEvent(y){const L=this.getItemIndexFromEventTarget(y.target||null),I=typeof L=="undefined"?void 0:this.items[L],k=I&&I.element;return{browserEvent:y,index:L,element:k}}onScroll(y){try{const L=this.getRenderRange(this.lastRenderTop,this.lastRenderHeight);this.render(L,y.scrollTop,y.height,y.scrollLeft,y.scrollWidth),this.supportDynamicHeights&&this._rerender(y.scrollTop,y.height,y.inSmoothScrolling)}catch(L){throw console.error("Got bad scroll event:",y),L}}onTouchChange(y){y.preventDefault(),y.stopPropagation(),this.scrollTop-=y.translationY}onDragStart(y,L,I){if(!!I.dataTransfer){const k=this.dnd.getDragElements(y);if(I.dataTransfer.effectAllowed="copyMove",I.dataTransfer.setData(a.DataTransfers.RESOURCES,JSON.stringify([L])),I.dataTransfer.setDragImage){let E;this.dnd.getDragLabel&&(E=this.dnd.getDragLabel(k,I)),typeof E=="undefined"&&(E=String(k.length));const T=i.$(".monaco-drag-image");T.textContent=E,document.body.appendChild(T),I.dataTransfer.setDragImage(T,-10,-10),setTimeout(()=>document.body.removeChild(T),0)}this.currentDragData=new t(k),a.StaticDND.CurrentDragAndDropData=new l(k),this.dnd.onDragStart&&this.dnd.onDragStart(this.currentDragData,I)}}onDragOver(y){if(y.browserEvent.preventDefault(),this.onDragLeaveTimeout.dispose(),a.StaticDND.CurrentDragAndDropData&&a.StaticDND.CurrentDragAndDropData.getData()==="vscode-ui"||(this.setupDragAndDropScrollTopAnimation(y.browserEvent),!y.browserEvent.dataTransfer))return!1;if(!this.currentDragData)if(a.StaticDND.CurrentDragAndDropData)this.currentDragData=a.StaticDND.CurrentDragAndDropData;else{if(!y.browserEvent.dataTransfer.types)return!1;this.currentDragData=new h}const L=this.dnd.onDragOver(this.currentDragData,y.element,y.index,y.browserEvent);if(this.canDrop=typeof L=="boolean"?L:L.accept,!this.canDrop)return this.currentDragFeedback=void 0,this.currentDragFeedbackDisposable.dispose(),!1;y.browserEvent.dataTransfer.dropEffect=typeof L!="boolean"&&L.effect===0?"copy":"move";let I;if(typeof L!="boolean"&&L.feedback?I=L.feedback:typeof y.index=="undefined"?I=[-1]:I=[y.index],I=s.distinct(I).filter(k=>k>=-1&&kk-E),I=I[0]===-1?[-1]:I,m(this.currentDragFeedback,I))return!0;if(this.currentDragFeedback=I,this.currentDragFeedbackDisposable.dispose(),I[0]===-1)this.domNode.classList.add("drop-target"),this.rowsContainer.classList.add("drop-target"),this.currentDragFeedbackDisposable=N.toDisposable(()=>{this.domNode.classList.remove("drop-target"),this.rowsContainer.classList.remove("drop-target")});else{for(const k of I){const E=this.items[k];E.dropTarget=!0,E.row&&E.row.domNode.classList.add("drop-target")}this.currentDragFeedbackDisposable=N.toDisposable(()=>{for(const k of I){const E=this.items[k];E.dropTarget=!1,E.row&&E.row.domNode.classList.remove("drop-target")}})}return!0}onDragLeave(){this.onDragLeaveTimeout.dispose(),this.onDragLeaveTimeout=u.disposableTimeout(()=>this.clearDragOverFeedback(),100)}onDrop(y){if(!!this.canDrop){const L=this.currentDragData;this.teardownDragAndDropScrollTopAnimation(),this.clearDragOverFeedback(),this.currentDragData=void 0,a.StaticDND.CurrentDragAndDropData=void 0,!(!L||!y.browserEvent.dataTransfer)&&(y.browserEvent.preventDefault(),L.update(y.browserEvent.dataTransfer),this.dnd.drop(L,y.element,y.index,y.browserEvent))}}onDragEnd(y){this.canDrop=!1,this.teardownDragAndDropScrollTopAnimation(),this.clearDragOverFeedback(),this.currentDragData=void 0,a.StaticDND.CurrentDragAndDropData=void 0,this.dnd.onDragEnd&&this.dnd.onDragEnd(y)}clearDragOverFeedback(){this.currentDragFeedback=void 0,this.currentDragFeedbackDisposable.dispose(),this.currentDragFeedbackDisposable=N.Disposable.None}setupDragAndDropScrollTopAnimation(y){if(!this.dragOverAnimationDisposable){const L=i.getTopLeftOffset(this.domNode).top;this.dragOverAnimationDisposable=i.animate(this.animateDragAndDropScrollTop.bind(this,L))}this.dragOverAnimationStopDisposable.dispose(),this.dragOverAnimationStopDisposable=u.disposableTimeout(()=>{this.dragOverAnimationDisposable&&(this.dragOverAnimationDisposable.dispose(),this.dragOverAnimationDisposable=void 0)},1e3),this.dragOverMouseY=y.pageY}animateDragAndDropScrollTop(y){if(this.dragOverMouseY!==void 0){const L=this.dragOverMouseY-y,I=this.renderHeight-35;L<35?this.scrollTop+=Math.max(-14,Math.floor(.3*(L-35))):L>I&&(this.scrollTop+=Math.min(14,Math.floor(.3*(L-I))))}}teardownDragAndDropScrollTopAnimation(){this.dragOverAnimationStopDisposable.dispose(),this.dragOverAnimationDisposable&&(this.dragOverAnimationDisposable.dispose(),this.dragOverAnimationDisposable=void 0)}getItemIndexFromEventTarget(y){const L=this.scrollableElement.getDomNode();let I=y;for(;I instanceof HTMLElement&&I!==this.rowsContainer&&L.contains(I);){const k=I.getAttribute("data-index");if(k){const E=Number(k);if(!isNaN(E))return E}I=I.parentElement}}getRenderRange(y,L){return{start:this.rangeMap.indexAt(y),end:this.rangeMap.indexAfter(y+L-1)}}_rerender(y,L,I){const k=this.getRenderRange(y,L);let E,T;y===this.elementTop(k.start)?(E=k.start,T=0):k.end-k.start>1&&(E=k.start+1,T=this.elementTop(E)-y);let O=0;for(;;){const A=this.getRenderRange(y,L);let B=!1;for(let F=A.start;FH.templateData===V);if(U>=0){const H=this.renderedElements[U];this.trait.unrender(V),H.index=P}else{const H={index:P,templateData:V};this.renderedElements.push(H)}this.trait.renderIndex(P,V)}splice(z,P,V){const U=[];for(const H of this.renderedElements)H.index=z+P&&U.push({index:H.index+V-P,templateData:H.templateData});this.renderedElements=U}renderIndexes(z){for(const{index:P,templateData:V}of this.renderedElements)z.indexOf(P)>-1&&this.trait.renderIndex(P,V)}disposeTemplate(z){const P=this.renderedElements.findIndex(V=>V.templateData===z);P<0||this.renderedElements.splice(P,1)}}class h{constructor(z){this._trait=z,this.indexes=[],this.sortedIndexes=[],this._onChange=new g.Emitter,this.onChange=this._onChange.event}get trait(){return this._trait}get renderer(){return new l(this)}splice(z,P,V){const U=V.length-P,H=z+P,$=[...this.sortedIndexes.filter(ie=>ieie?oe+z:-1).filter(ie=>ie!==-1),...this.sortedIndexes.filter(ie=>ie>=H).map(ie=>ie+U)];this.renderer.splice(z,P,V.length),this._set($,$)}renderIndex(z,P){P.classList.toggle(this._trait,this.contains(z))}unrender(z){z.classList.remove(this._trait)}set(z,P){return this._set(z,[...z].sort(Y),P)}_set(z,P,V){const U=this.indexes,H=this.sortedIndexes;this.indexes=z,this.sortedIndexes=P;const $=x(H,z);return this.renderer.renderIndexes($),this._onChange.fire({indexes:z,browserEvent:V}),U}get(){return this.indexes}contains(z){return M.binarySearch(this.sortedIndexes,z,Y)>=0}dispose(){b.dispose(this._onChange)}}Me([w.memoize],h.prototype,"renderer",null);class m extends h{constructor(z){super("selected");this.setAriaSelected=z}renderIndex(z,P){super.renderIndex(z,P),this.setAriaSelected&&(this.contains(z)?P.setAttribute("aria-selected","true"):P.setAttribute("aria-selected","false"))}}class _{constructor(z,P,V){this.trait=z,this.view=P,this.identityProvider=V}splice(z,P,V){if(!this.identityProvider)return this.trait.splice(z,P,V.map(()=>!1));const U=this.trait.get().map($=>this.identityProvider.getId(this.view.element($)).toString()),H=V.map($=>U.indexOf(this.identityProvider.getId($).toString())>-1);this.trait.splice(z,P,H)}}function f(X){return X.tagName==="INPUT"||X.tagName==="TEXTAREA"}e.isInputElement=f;function v(X){return X.classList.contains("monaco-editor")?!0:X.classList.contains("monaco-list")||!X.parentElement?!1:v(X.parentElement)}e.isMonacoEditor=v;class y{constructor(z,P,V){this.list=z,this.view=P,this.disposables=new b.DisposableStore;const U=V.multipleSelectionSupport!==!1,H=g.Event.chain(p.domEvent(P.domNode,"keydown")).filter($=>!f($.target)).map($=>new d.StandardKeyboardEvent($));H.filter($=>$.keyCode===3).on(this.onEnter,this,this.disposables),H.filter($=>$.keyCode===16).on(this.onUpArrow,this,this.disposables),H.filter($=>$.keyCode===18).on(this.onDownArrow,this,this.disposables),H.filter($=>$.keyCode===11).on(this.onPageUpArrow,this,this.disposables),H.filter($=>$.keyCode===12).on(this.onPageDownArrow,this,this.disposables),H.filter($=>$.keyCode===9).on(this.onEscape,this,this.disposables),U&&H.filter($=>(S.isMacintosh?$.metaKey:$.ctrlKey)&&$.keyCode===31).on(this.onCtrlA,this,this.disposables)}onEnter(z){z.preventDefault(),z.stopPropagation(),this.list.setSelection(this.list.getFocus(),z.browserEvent)}onUpArrow(z){z.preventDefault(),z.stopPropagation(),this.list.focusPrevious(1,!1,z.browserEvent),this.list.reveal(this.list.getFocus()[0]),this.view.domNode.focus()}onDownArrow(z){z.preventDefault(),z.stopPropagation(),this.list.focusNext(1,!1,z.browserEvent),this.list.reveal(this.list.getFocus()[0]),this.view.domNode.focus()}onPageUpArrow(z){z.preventDefault(),z.stopPropagation(),this.list.focusPreviousPage(z.browserEvent),this.list.reveal(this.list.getFocus()[0]),this.view.domNode.focus()}onPageDownArrow(z){z.preventDefault(),z.stopPropagation(),this.list.focusNextPage(z.browserEvent),this.list.reveal(this.list.getFocus()[0]),this.view.domNode.focus()}onCtrlA(z){z.preventDefault(),z.stopPropagation(),this.list.setSelection(M.range(this.list.length),z.browserEvent),this.view.domNode.focus()}onEscape(z){this.list.getSelection().length&&(z.preventDefault(),z.stopPropagation(),this.list.setSelection([],z.browserEvent),this.view.domNode.focus())}dispose(){this.disposables.dispose()}}var L;(function(X){X[X.Idle=0]="Idle",X[X.Typing=1]="Typing"})(L||(L={})),e.DefaultKeyboardNavigationDelegate=new class{mightProducePrintableCharacter(X){return X.ctrlKey||X.metaKey||X.altKey?!1:X.keyCode>=31&&X.keyCode<=56||X.keyCode>=21&&X.keyCode<=30||X.keyCode>=93&&X.keyCode<=102||X.keyCode>=80&&X.keyCode<=90}};class I{constructor(z,P,V,U){this.list=z,this.view=P,this.keyboardNavigationLabelProvider=V,this.delegate=U,this.enabled=!1,this.state=L.Idle,this.automaticKeyboardNavigation=!0,this.triggered=!1,this.previouslyFocused=-1,this.enabledDisposables=new b.DisposableStore,this.disposables=new b.DisposableStore,this.updateOptions(z.options)}updateOptions(z){(typeof z.enableKeyboardNavigation=="undefined"?!0:!!z.enableKeyboardNavigation)?this.enable():this.disable(),typeof z.automaticKeyboardNavigation!="undefined"&&(this.automaticKeyboardNavigation=z.automaticKeyboardNavigation)}enable(){if(!this.enabled){const z=g.Event.chain(p.domEvent(this.view.domNode,"keydown")).filter(U=>!f(U.target)).filter(()=>this.automaticKeyboardNavigation||this.triggered).map(U=>new d.StandardKeyboardEvent(U)).filter(U=>this.delegate.mightProducePrintableCharacter(U)).forEach(U=>{U.stopPropagation(),U.preventDefault()}).map(U=>U.browserEvent.key).event,P=g.Event.debounce(z,()=>null,800);g.Event.reduce(g.Event.any(z,P),(U,H)=>H===null?null:(U||"")+H)(this.onInput,this,this.enabledDisposables),P(this.onClear,this,this.enabledDisposables),this.enabled=!0,this.triggered=!1}}disable(){!this.enabled||(this.enabledDisposables.clear(),this.enabled=!1,this.triggered=!1)}onClear(){var z;const P=this.list.getFocus();if(P.length>0&&P[0]===this.previouslyFocused){const V=(z=this.list.options.accessibilityProvider)===null||z===void 0?void 0:z.getAriaLabel(this.list.element(P[0]));V&&n.alert(V)}this.previouslyFocused=-1}onInput(z){if(!z){this.state=L.Idle,this.triggered=!1;return}const P=this.list.getFocus(),V=P.length>0?P[0]:0,U=this.state===L.Idle?1:0;this.state=L.Typing;for(let H=0;H!f(U.target)).map(U=>new d.StandardKeyboardEvent(U)).filter(U=>U.keyCode===2&&!U.ctrlKey&&!U.metaKey&&!U.shiftKey&&!U.altKey).on(this.onTab,this,this.disposables)}onTab(z){if(z.target===this.view.domNode){const P=this.list.getFocus();if(P.length!==0){const V=this.view.domElement(P[0]);if(!!V){const U=V.querySelector("[tabIndex]");if(!(!U||!(U instanceof HTMLElement)||U.tabIndex===-1)){const H=window.getComputedStyle(U);H.visibility==="hidden"||H.display==="none"||(z.preventDefault(),z.stopPropagation(),U.focus())}}}}}dispose(){this.disposables.dispose()}}function E(X){return S.isMacintosh?X.browserEvent.metaKey:X.browserEvent.ctrlKey}e.isSelectionSingleChangeEvent=E;function T(X){return X.browserEvent.shiftKey}e.isSelectionRangeChangeEvent=T;function O(X){return X instanceof MouseEvent&&X.button===2}const A={isSelectionSingleChangeEvent:E,isSelectionRangeChangeEvent:T};class B{constructor(z){this.list=z,this.disposables=new b.DisposableStore,this._onPointer=new g.Emitter,this.onPointer=this._onPointer.event,this.multipleSelectionSupport=z.options.multipleSelectionSupport!==!1,this.multipleSelectionSupport&&(this.multipleSelectionController=z.options.multipleSelectionController||A),this.mouseSupport=typeof z.options.mouseSupport=="undefined"||!!z.options.mouseSupport,this.mouseSupport&&(z.onMouseDown(this.onMouseDown,this,this.disposables),z.onContextMenu(this.onContextMenu,this,this.disposables),z.onMouseDblClick(this.onDoubleClick,this,this.disposables),z.onTouchStart(this.onMouseDown,this,this.disposables),this.disposables.add(C.Gesture.addTarget(z.getHTMLElement()))),g.Event.any(z.onMouseClick,z.onMouseMiddleClick,z.onTap)(this.onViewPointer,this,this.disposables)}isSelectionSingleChangeEvent(z){return this.multipleSelectionController?this.multipleSelectionController.isSelectionSingleChangeEvent(z):S.isMacintosh?z.browserEvent.metaKey:z.browserEvent.ctrlKey}isSelectionRangeChangeEvent(z){return this.multipleSelectionController?this.multipleSelectionController.isSelectionRangeChangeEvent(z):z.browserEvent.shiftKey}isSelectionChangeEvent(z){return this.isSelectionSingleChangeEvent(z)||this.isSelectionRangeChangeEvent(z)}onMouseDown(z){v(z.browserEvent.target)||document.activeElement!==z.browserEvent.target&&this.list.domFocus()}onContextMenu(z){if(!v(z.browserEvent.target)){const P=typeof z.index=="undefined"?[]:[z.index];this.list.setFocus(P,z.browserEvent)}}onViewPointer(z){if(!!this.mouseSupport&&!(f(z.browserEvent.target)||v(z.browserEvent.target))){let P=this.list.getFocus()[0];const V=this.list.getSelection();P=P===void 0?V[0]:P;const U=z.index;if(typeof U=="undefined"){this.list.setFocus([],z.browserEvent),this.list.setSelection([],z.browserEvent);return}if(this.multipleSelectionSupport&&this.isSelectionRangeChangeEvent(z))return this.changeSelection(z,P);if(this.multipleSelectionSupport&&this.isSelectionChangeEvent(z))return this.changeSelection(z,P);this.list.setFocus([U],z.browserEvent),O(z.browserEvent)||this.list.setSelection([U],z.browserEvent),this._onPointer.fire(z)}}onDoubleClick(z){if(!(f(z.browserEvent.target)||v(z.browserEvent.target))&&!(this.multipleSelectionSupport&&this.isSelectionChangeEvent(z))){const P=this.list.getFocus();this.list.setSelection(P,z.browserEvent)}}changeSelection(z,P){const V=z.index;if(this.isSelectionRangeChangeEvent(z)&&P!==void 0){const U=Math.min(P,V),H=Math.max(P,V),$=M.range(U,H+1),ie=this.list.getSelection(),oe=W(x(ie,[P]),P);if(oe.length===0)return;const ae=x($,K(ie,oe));this.list.setSelection(ae,z.browserEvent)}else if(this.isSelectionSingleChangeEvent(z)){const U=this.list.getSelection(),H=U.filter($=>$!==V);this.list.setFocus([V]),U.length===H.length?this.list.setSelection([...H,V],z.browserEvent):this.list.setSelection(H,z.browserEvent)}}dispose(){this.disposables.dispose()}}e.MouseController=B;class F{constructor(z,P){this.styleElement=z,this.selectorSuffix=P}style(z){const P=this.selectorSuffix&&`.${this.selectorSuffix}`,V=[];z.listBackground&&(z.listBackground.isOpaque()?V.push(`.monaco-list${P} .monaco-list-rows { background: ${z.listBackground}; }`):S.isMacintosh||console.warn(`List with id '${this.selectorSuffix}' was styled with a non-opaque background color. This will break sub-pixel antialiasing.`)),z.listFocusBackground&&(V.push(`.monaco-list${P}:focus .monaco-list-row.focused { background-color: ${z.listFocusBackground}; }`),V.push(`.monaco-list${P}:focus .monaco-list-row.focused:hover { background-color: ${z.listFocusBackground}; }`)),z.listFocusForeground&&V.push(`.monaco-list${P}:focus .monaco-list-row.focused { color: ${z.listFocusForeground}; }`),z.listActiveSelectionBackground&&(V.push(`.monaco-list${P}:focus .monaco-list-row.selected { background-color: ${z.listActiveSelectionBackground}; }`),V.push(`.monaco-list${P}:focus .monaco-list-row.selected:hover { background-color: ${z.listActiveSelectionBackground}; }`)),z.listActiveSelectionForeground&&V.push(`.monaco-list${P}:focus .monaco-list-row.selected { color: ${z.listActiveSelectionForeground}; }`),z.listFocusAndSelectionBackground&&V.push(` + .monaco-drag-image, + .monaco-list${P}:focus .monaco-list-row.selected.focused { background-color: ${z.listFocusAndSelectionBackground}; } + `),z.listFocusAndSelectionForeground&&V.push(` + .monaco-drag-image, + .monaco-list${P}:focus .monaco-list-row.selected.focused { color: ${z.listFocusAndSelectionForeground}; } + `),z.listInactiveFocusForeground&&(V.push(`.monaco-list${P} .monaco-list-row.focused { color: ${z.listInactiveFocusForeground}; }`),V.push(`.monaco-list${P} .monaco-list-row.focused:hover { color: ${z.listInactiveFocusForeground}; }`)),z.listInactiveFocusBackground&&(V.push(`.monaco-list${P} .monaco-list-row.focused { background-color: ${z.listInactiveFocusBackground}; }`),V.push(`.monaco-list${P} .monaco-list-row.focused:hover { background-color: ${z.listInactiveFocusBackground}; }`)),z.listInactiveSelectionBackground&&(V.push(`.monaco-list${P} .monaco-list-row.selected { background-color: ${z.listInactiveSelectionBackground}; }`),V.push(`.monaco-list${P} .monaco-list-row.selected:hover { background-color: ${z.listInactiveSelectionBackground}; }`)),z.listInactiveSelectionForeground&&V.push(`.monaco-list${P} .monaco-list-row.selected { color: ${z.listInactiveSelectionForeground}; }`),z.listHoverBackground&&V.push(`.monaco-list${P}:not(.drop-target) .monaco-list-row:hover:not(.selected):not(.focused) { background-color: ${z.listHoverBackground}; }`),z.listHoverForeground&&V.push(`.monaco-list${P} .monaco-list-row:hover:not(.selected):not(.focused) { color: ${z.listHoverForeground}; }`),z.listSelectionOutline&&V.push(`.monaco-list${P} .monaco-list-row.selected { outline: 1px dotted ${z.listSelectionOutline}; outline-offset: -1px; }`),z.listFocusOutline&&V.push(` + .monaco-drag-image, + .monaco-list${P}:focus .monaco-list-row.focused { outline: 1px solid ${z.listFocusOutline}; outline-offset: -1px; } + `),z.listInactiveFocusOutline&&V.push(`.monaco-list${P} .monaco-list-row.focused { outline: 1px dotted ${z.listInactiveFocusOutline}; outline-offset: -1px; }`),z.listHoverOutline&&V.push(`.monaco-list${P} .monaco-list-row:hover { outline: 1px dashed ${z.listHoverOutline}; outline-offset: -1px; }`),z.listDropBackground&&V.push(` + .monaco-list${P}.drop-target, + .monaco-list${P} .monaco-list-rows.drop-target, + .monaco-list${P} .monaco-list-row.drop-target { background-color: ${z.listDropBackground} !important; color: inherit !important; } + `),z.listFilterWidgetBackground&&V.push(`.monaco-list-type-filter { background-color: ${z.listFilterWidgetBackground} }`),z.listFilterWidgetOutline&&V.push(`.monaco-list-type-filter { border: 1px solid ${z.listFilterWidgetOutline}; }`),z.listFilterWidgetNoMatchesOutline&&V.push(`.monaco-list-type-filter.no-matches { border: 1px solid ${z.listFilterWidgetNoMatchesOutline}; }`),z.listMatchesShadow&&V.push(`.monaco-list-type-filter { box-shadow: 1px 1px 1px ${z.listMatchesShadow}; }`),z.tableColumnsBorder&&V.push(` + .monaco-table:hover > .monaco-split-view2, + .monaco-table:hover > .monaco-split-view2 .monaco-sash.vertical::before { + border-color: ${z.tableColumnsBorder}; + }`),this.styleElement.textContent=V.join(` +`)}}e.DefaultStyleController=F;const D={listFocusBackground:s.Color.fromHex("#7FB0D0"),listActiveSelectionBackground:s.Color.fromHex("#0E639C"),listActiveSelectionForeground:s.Color.fromHex("#FFFFFF"),listFocusAndSelectionBackground:s.Color.fromHex("#094771"),listFocusAndSelectionForeground:s.Color.fromHex("#FFFFFF"),listInactiveSelectionBackground:s.Color.fromHex("#3F3F46"),listHoverBackground:s.Color.fromHex("#2A2D2E"),listDropBackground:s.Color.fromHex("#383B3D"),treeIndentGuidesStroke:s.Color.fromHex("#a9a9a9"),tableColumnsBorder:s.Color.fromHex("#cccccc").transparent(.2)},R={keyboardSupport:!0,mouseSupport:!0,multipleSelectionSupport:!0,dnd:{getDragURI(){return null},onDragStart(){},onDragOver(){return!1},drop(){}}};function W(X,z){const P=X.indexOf(z);if(P===-1)return[];const V=[];let U=P-1;for(;U>=0&&X[U]===z-(P-U);)V.push(X[U--]);for(V.reverse(),U=P;U=X.length)P.push(z[U++]);else if(U>=z.length)P.push(X[V++]);else if(X[V]===z[U]){P.push(X[V]),V++,U++;continue}else X[V]=X.length)P.push(z[U++]);else if(U>=z.length)P.push(X[V++]);else if(X[V]===z[U]){V++,U++;continue}else X[V]X-z;class ee{constructor(z,P){this._templateId=z,this.renderers=P}get templateId(){return this._templateId}renderTemplate(z){return this.renderers.map(P=>P.renderTemplate(z))}renderElement(z,P,V,U){let H=0;for(const $ of this.renderers)$.renderElement(z,P,V[H++],U)}disposeElement(z,P,V,U){let H=0;for(const $ of this.renderers)$.disposeElement&&$.disposeElement(z,P,V[H],U),H+=1}disposeTemplate(z){let P=0;for(const V of this.renderers)V.disposeTemplate(z[P++])}}class se{constructor(z){this.accessibilityProvider=z,this.templateId="a18n"}renderTemplate(z){return z}renderElement(z,P,V){const U=this.accessibilityProvider.getAriaLabel(z);U?V.setAttribute("aria-label",U):V.removeAttribute("aria-label");const H=this.accessibilityProvider.getAriaLevel&&this.accessibilityProvider.getAriaLevel(z);typeof H=="number"?V.setAttribute("aria-level",`${H}`):V.removeAttribute("aria-level")}disposeTemplate(z){}}class ne{constructor(z,P){this.list=z,this.dnd=P}getDragElements(z){const P=this.list.getSelectedElements();return P.indexOf(z)>-1?P:[z]}getDragURI(z){return this.dnd.getDragURI(z)}getDragLabel(z,P){if(this.dnd.getDragLabel)return this.dnd.getDragLabel(z,P)}onDragStart(z,P){this.dnd.onDragStart&&this.dnd.onDragStart(z,P)}onDragOver(z,P,V,U){return this.dnd.onDragOver(z,P,V,U)}onDragEnd(z){this.dnd.onDragEnd&&this.dnd.onDragEnd(z)}drop(z,P,V,U){this.dnd.drop(z,P,V,U)}}class le{constructor(z,P,V,U,H=R){var $;this.user=z,this._options=H,this.eventBufferer=new g.EventBufferer,this._ariaLabel="",this.disposables=new b.DisposableStore,this._onDidDispose=new g.Emitter,this.onDidDispose=this._onDidDispose.event;const ie=this._options.accessibilityProvider&&this._options.accessibilityProvider.getWidgetRole?($=this._options.accessibilityProvider)===null||$===void 0?void 0:$.getWidgetRole():"list";this.selection=new m(ie!=="listbox"),this.focus=new h("focused"),a.mixin(H,D,!1);const oe=[this.focus.renderer,this.selection.renderer];this.accessibilityProvider=H.accessibilityProvider,this.accessibilityProvider&&(oe.push(new se(this.accessibilityProvider)),this.accessibilityProvider.onDidChangeActiveDescendant&&this.accessibilityProvider.onDidChangeActiveDescendant(this.onDidChangeActiveDescendant,this,this.disposables)),U=U.map(G=>new ee(G.templateId,[...oe,G]));const ae=Object.assign(Object.assign({},H),{dnd:H.dnd&&new ne(this,H.dnd)});if(this.view=new o.ListView(P,V,U,ae),this.view.domNode.setAttribute("role",ie),H.styleController)this.styleController=H.styleController(this.view.domId);else{const G=t.createStyleSheet(this.view.domNode);this.styleController=new F(G,this.view.domId)}if(this.spliceable=new u.CombinedSpliceable([new _(this.focus,this.view,H.identityProvider),new _(this.selection,this.view,H.identityProvider),this.view]),this.disposables.add(this.focus),this.disposables.add(this.selection),this.disposables.add(this.view),this.disposables.add(this._onDidDispose),this.onDidFocus=g.Event.map(p.domEvent(this.view.domNode,"focus",!0),()=>null),this.onDidBlur=g.Event.map(p.domEvent(this.view.domNode,"blur",!0),()=>null),this.disposables.add(new k(this,this.view)),typeof H.keyboardSupport!="boolean"||H.keyboardSupport){const G=new y(this,this.view,H);this.disposables.add(G)}if(H.keyboardNavigationLabelProvider){const G=H.keyboardNavigationDelegate||e.DefaultKeyboardNavigationDelegate;this.typeLabelController=new I(this,this.view,H.keyboardNavigationLabelProvider,G),this.disposables.add(this.typeLabelController)}this.mouseController=this.createMouseController(H),this.disposables.add(this.mouseController),this.onDidChangeFocus(this._onFocusChange,this,this.disposables),this.onDidChangeSelection(this._onSelectionChange,this,this.disposables),this.accessibilityProvider&&(this.ariaLabel=this.accessibilityProvider.getWidgetAriaLabel()),H.multipleSelectionSupport&&this.view.domNode.setAttribute("aria-multiselectable","true")}get onDidChangeFocus(){return g.Event.map(this.eventBufferer.wrapEvent(this.focus.onChange),z=>this.toListEvent(z))}get onDidChangeSelection(){return g.Event.map(this.eventBufferer.wrapEvent(this.selection.onChange),z=>this.toListEvent(z))}get domId(){return this.view.domId}get onMouseClick(){return this.view.onMouseClick}get onMouseDblClick(){return this.view.onMouseDblClick}get onMouseMiddleClick(){return this.view.onMouseMiddleClick}get onPointer(){return this.mouseController.onPointer}get onMouseDown(){return this.view.onMouseDown}get onTouchStart(){return this.view.onTouchStart}get onTap(){return this.view.onTap}get onContextMenu(){let z=!1;const P=g.Event.chain(p.domEvent(this.view.domNode,"keydown")).map(H=>new d.StandardKeyboardEvent(H)).filter(H=>z=H.keyCode===58||H.shiftKey&&H.keyCode===68).map(p.stopEvent).filter(()=>!1).event,V=g.Event.chain(p.domEvent(this.view.domNode,"keyup")).forEach(()=>z=!1).map(H=>new d.StandardKeyboardEvent(H)).filter(H=>H.keyCode===58||H.shiftKey&&H.keyCode===68).map(p.stopEvent).map(({browserEvent:H})=>{const $=this.getFocus(),ie=$.length?$[0]:void 0,oe=typeof ie!="undefined"?this.view.element(ie):void 0,ae=typeof ie!="undefined"?this.view.domElement(ie):this.view.domNode;return{index:ie,element:oe,anchor:ae,browserEvent:H}}).event,U=g.Event.chain(this.view.onContextMenu).filter(H=>!z).map(({element:H,index:$,browserEvent:ie})=>({element:H,index:$,anchor:{x:ie.clientX+1,y:ie.clientY},browserEvent:ie})).event;return g.Event.any(P,V,U)}get onKeyDown(){return p.domEvent(this.view.domNode,"keydown")}createMouseController(z){return new B(this)}updateOptions(z={}){this._options=Object.assign(Object.assign({},this._options),z),this.typeLabelController&&this.typeLabelController.updateOptions(this._options),this.view.updateOptions(z)}get options(){return this._options}splice(z,P,V=[]){if(z<0||z>this.view.length)throw new c.ListError(this.user,`Invalid start index: ${z}`);if(P<0)throw new c.ListError(this.user,`Invalid delete count: ${P}`);P===0&&V.length===0||this.eventBufferer.bufferEvents(()=>this.spliceable.splice(z,P,V))}rerender(){this.view.rerender()}element(z){return this.view.element(z)}get length(){return this.view.length}get contentHeight(){return this.view.contentHeight}get scrollTop(){return this.view.getScrollTop()}set scrollTop(z){this.view.setScrollTop(z)}get ariaLabel(){return this._ariaLabel}set ariaLabel(z){this._ariaLabel=z,this.view.domNode.setAttribute("aria-label",z)}domFocus(){this.view.domNode.focus({preventScroll:!0})}layout(z,P){this.view.layout(z,P)}setSelection(z,P){for(const V of z)if(V<0||V>=this.length)throw new c.ListError(this.user,`Invalid index ${V}`);this.selection.set(z,P)}getSelection(){return this.selection.get()}getSelectedElements(){return this.getSelection().map(z=>this.view.element(z))}setFocus(z,P){for(const V of z)if(V<0||V>=this.length)throw new c.ListError(this.user,`Invalid index ${V}`);this.focus.set(z,P)}focusNext(z=1,P=!1,V,U){if(this.length!==0){const H=this.focus.get(),$=this.findNextIndex(H.length>0?H[0]+z:0,P,U);$>-1&&this.setFocus([$],V)}}focusPrevious(z=1,P=!1,V,U){if(this.length!==0){const H=this.focus.get(),$=this.findPreviousIndex(H.length>0?H[0]-z:0,P,U);$>-1&&this.setFocus([$],V)}}focusNextPage(z,P){let V=this.view.indexAt(this.view.getScrollTop()+this.view.renderHeight);V=V===0?0:V-1;const U=this.view.element(V),H=this.getFocusedElements()[0];if(H!==U){const $=this.findPreviousIndex(V,!1,P);$>-1&&H!==this.view.element($)?this.setFocus([$],z):this.setFocus([V],z)}else{const $=this.view.getScrollTop();this.view.setScrollTop($+this.view.renderHeight-this.view.elementHeight(V)),this.view.getScrollTop()!==$&&(this.setFocus([]),setTimeout(()=>this.focusNextPage(z,P),0))}}focusPreviousPage(z,P){let V;const U=this.view.getScrollTop();U===0?V=this.view.indexAt(U):V=this.view.indexAfter(U-1);const H=this.view.element(V),$=this.getFocusedElements()[0];if($!==H){const ie=this.findNextIndex(V,!1,P);ie>-1&&$!==this.view.element(ie)?this.setFocus([ie],z):this.setFocus([V],z)}else{const ie=U;this.view.setScrollTop(U-this.view.renderHeight),this.view.getScrollTop()!==ie&&(this.setFocus([]),setTimeout(()=>this.focusPreviousPage(z,P),0))}}focusLast(z,P){if(this.length!==0){const V=this.findPreviousIndex(this.length-1,!1,P);V>-1&&this.setFocus([V],z)}}focusFirst(z,P){this.focusNth(0,z,P)}focusNth(z,P,V){if(this.length!==0){const U=this.findNextIndex(z,!1,V);U>-1&&this.setFocus([U],P)}}findNextIndex(z,P=!1,V){for(let U=0;U=this.length&&!P)return-1;if(z=z%this.length,!V||V(this.element(z)))return z;z++}return-1}findPreviousIndex(z,P=!1,V){for(let U=0;Uthis.view.element(z))}reveal(z,P){if(z<0||z>=this.length)throw new c.ListError(this.user,`Invalid index ${z}`);const V=this.view.getScrollTop(),U=this.view.elementTop(z),H=this.view.elementHeight(z);if(N.isNumber(P)){const $=H-this.view.renderHeight;this.view.setScrollTop($*r.clamp(P,0,1)+U)}else{const $=U+H,ie=V+this.view.renderHeight;U=ie||(U=ie&&this.view.setScrollTop($-this.view.renderHeight))}}getRelativeTop(z){if(z<0||z>=this.length)throw new c.ListError(this.user,`Invalid index ${z}`);const P=this.view.getScrollTop(),V=this.view.elementTop(z),U=this.view.elementHeight(z);if(VP+this.view.renderHeight)return null;const H=U-this.view.renderHeight;return Math.abs((P-V)/H)}getHTMLElement(){return this.view.domNode}style(z){this.styleController.style(z)}toListEvent({indexes:z,browserEvent:P}){return{indexes:z,elements:z.map(V=>this.view.element(V)),browserEvent:P}}_onFocusChange(){const z=this.focus.get();this.view.domNode.classList.toggle("element-focused",z.length>0),this.onDidChangeActiveDescendant()}onDidChangeActiveDescendant(){var z;const P=this.focus.get();if(P.length>0){let V;((z=this.accessibilityProvider)===null||z===void 0?void 0:z.getActiveDescendantId)&&(V=this.accessibilityProvider.getActiveDescendantId(this.view.element(P[0]))),this.view.domNode.setAttribute("aria-activedescendant",V||this.view.getElementDomId(P[0]))}else this.view.domNode.removeAttribute("aria-activedescendant")}_onSelectionChange(){const z=this.selection.get();this.view.domNode.classList.toggle("selection-none",z.length===0),this.view.domNode.classList.toggle("selection-single",z.length===1),this.view.domNode.classList.toggle("selection-multiple",z.length>1)}dispose(){this._onDidDispose.fire(),this.disposables.dispose(),this._onDidDispose.dispose()}}Me([w.memoize],le.prototype,"onDidChangeFocus",null),Me([w.memoize],le.prototype,"onDidChangeSelection",null),Me([w.memoize],le.prototype,"onContextMenu",null),e.List=le}),define(Q[312],J([0,1,2,19,105,6,23,208]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PagedList=void 0;class C{constructor(o,s){this.renderer=o,this.modelProvider=s}get templateId(){return this.renderer.templateId}renderTemplate(o){return{data:this.renderer.renderTemplate(o),disposable:b.Disposable.None}}renderElement(o,s,a,u){if(a.disposable&&a.disposable.dispose(),!!a.data){const r=this.modelProvider();if(r.isResolved(o))return this.renderer.renderElement(r.get(o),o,a.data,u);const i=new S.CancellationTokenSource,n=r.resolve(o,i.token);a.disposable={dispose:()=>i.cancel()},this.renderer.renderPlaceholder(o,a.data),n.then(t=>this.renderer.renderElement(t,o,a.data,u))}}disposeTemplate(o){o.disposable&&(o.disposable.dispose(),o.disposable=void 0),o.data&&(this.renderer.disposeTemplate(o.data),o.data=void 0)}}class d{constructor(o,s){this.modelProvider=o,this.accessibilityProvider=s}getWidgetAriaLabel(){return this.accessibilityProvider.getWidgetAriaLabel()}getAriaLabel(o){const s=this.modelProvider();return s.isResolved(o)?this.accessibilityProvider.getAriaLabel(s.get(o)):null}}function g(c,o){return Object.assign(Object.assign({},o),{accessibilityProvider:o.accessibilityProvider&&new d(c,o.accessibilityProvider)})}class p{constructor(o,s,a,u,r={}){const i=()=>this.model,n=u.map(t=>new C(t,i));this.list=new M.List(o,s,a,n,g(i,r))}updateOptions(o){this.list.updateOptions(o)}getHTMLElement(){return this.list.getHTMLElement()}get onDidFocus(){return this.list.onDidFocus}get onDidDispose(){return this.list.onDidDispose}get onMouseDblClick(){return w.Event.map(this.list.onMouseDblClick,({element:o,index:s,browserEvent:a})=>({element:o===void 0?void 0:this._model.get(o),index:s,browserEvent:a}))}get onPointer(){return w.Event.map(this.list.onPointer,({element:o,index:s,browserEvent:a})=>({element:o===void 0?void 0:this._model.get(o),index:s,browserEvent:a}))}get onDidChangeFocus(){return w.Event.map(this.list.onDidChangeFocus,({elements:o,indexes:s,browserEvent:a})=>({elements:o.map(u=>this._model.get(u)),indexes:s,browserEvent:a}))}get onDidChangeSelection(){return w.Event.map(this.list.onDidChangeSelection,({elements:o,indexes:s,browserEvent:a})=>({elements:o.map(u=>this._model.get(u)),indexes:s,browserEvent:a}))}get model(){return this._model}set model(o){this._model=o,this.list.splice(0,this.list.length,N.range(o.length))}getFocus(){return this.list.getFocus()}setSelection(o,s){this.list.setSelection(o,s)}getSelection(){return this.list.getSelection()}getSelectedElements(){return this.getSelection().map(o=>this.model.get(o))}style(o){this.list.style(o)}dispose(){this.list.dispose()}}e.PagedList=p}),define(Q[313],J([10]),{}),define(Q[210],J([0,1,2,6,20,100,19,104,29,55,7,61,121,313]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SplitView=e.Sizing=void 0;const s={separatorBorder:d.Color.transparent};class a{constructor(h,m,_,f){this.container=h,this.view=m,this.disposable=f,this._cachedVisibleSize=void 0,typeof _=="number"?(this._size=_,this._cachedVisibleSize=void 0,h.classList.add("visible")):(this._size=0,this._cachedVisibleSize=_.cachedVisibleSize)}set size(h){this._size=h}get size(){return this._size}get visible(){return typeof this._cachedVisibleSize=="undefined"}setVisible(h,m){h!==this.visible&&(h?(this.size=w.clamp(this._cachedVisibleSize,this.viewMinimumSize,this.viewMaximumSize),this._cachedVisibleSize=void 0):(this._cachedVisibleSize=typeof m=="number"?m:this.size,this.size=0),this.container.classList.toggle("visible",h),this.view.setVisible&&this.view.setVisible(h))}get minimumSize(){return this.visible?this.view.minimumSize:0}get viewMinimumSize(){return this.view.minimumSize}get maximumSize(){return this.visible?this.view.maximumSize:0}get viewMaximumSize(){return this.view.maximumSize}get priority(){return this.view.priority}get snap(){return!!this.view.snap}set enabled(h){this.container.style.pointerEvents=h?"":"none"}layout(h,m){this.layoutContainer(h),this.view.layout(this.size,h,m)}dispose(){return this.disposable.dispose(),this.view}}class u extends a{layoutContainer(h){this.container.style.top=`${h}px`,this.container.style.height=`${this.size}px`}}class r extends a{layoutContainer(h){this.container.style.left=`${h}px`,this.container.style.width=`${this.size}px`}}var i;(function(l){l[l.Idle=0]="Idle",l[l.Busy=1]="Busy"})(i||(i={}));var n;(function(l){l.Distribute={type:"distribute"};function h(_){return{type:"split",index:_}}l.Split=h;function m(_){return{type:"invisible",cachedVisibleSize:_}}l.Invisible=m})(n=e.Sizing||(e.Sizing={}));class t extends b.Disposable{constructor(h,m={}){var _,f;super();this.size=0,this.contentSize=0,this.proportions=void 0,this.viewItems=[],this.sashItems=[],this.state=i.Idle,this._onDidSashChange=this._register(new N.Emitter),this.onDidSashChange=this._onDidSashChange.event,this._onDidSashReset=this._register(new N.Emitter),this._startSnappingEnabled=!0,this._endSnappingEnabled=!0,this.orientation=M.isUndefined(m.orientation)?0:m.orientation,this.inverseAltBehavior=!!m.inverseAltBehavior,this.proportionalLayout=M.isUndefined(m.proportionalLayout)?!0:!!m.proportionalLayout,this.getSashOrthogonalSize=m.getSashOrthogonalSize,this.el=document.createElement("div"),this.el.classList.add("monaco-split-view2"),this.el.classList.add(this.orientation===0?"vertical":"horizontal"),h.appendChild(this.el),this.sashContainer=p.append(this.el,p.$(".sash-container")),this.viewContainer=p.$(".split-view-container"),this.scrollable=new o.Scrollable(125,p.scheduleAtNextAnimationFrame),this.scrollableElement=this._register(new c.SmoothScrollableElement(this.viewContainer,{vertical:this.orientation===0?(_=m.scrollbarVisibility)!==null&&_!==void 0?_:1:2,horizontal:this.orientation===1?(f=m.scrollbarVisibility)!==null&&f!==void 0?f:1:2},this.scrollable)),this._register(this.scrollableElement.onScroll(v=>{this.viewContainer.scrollTop=v.scrollTop,this.viewContainer.scrollLeft=v.scrollLeft})),p.append(this.el,this.scrollableElement.getDomNode()),this.style(m.styles||s),m.descriptor&&(this.size=m.descriptor.size,m.descriptor.views.forEach((v,y)=>{const L=M.isUndefined(v.visible)||v.visible?v.size:{type:"invisible",cachedVisibleSize:v.size},I=v.view;this.doAddView(I,L,y,!0)}),this.contentSize=this.viewItems.reduce((v,y)=>v+y.size,0),this.saveProportions())}get orthogonalStartSash(){return this._orthogonalStartSash}set orthogonalStartSash(h){for(const m of this.sashItems)m.sash.orthogonalStartSash=h;this._orthogonalStartSash=h}get orthogonalEndSash(){return this._orthogonalEndSash}set orthogonalEndSash(h){for(const m of this.sashItems)m.sash.orthogonalEndSash=h;this._orthogonalEndSash=h}get startSnappingEnabled(){return this._startSnappingEnabled}set startSnappingEnabled(h){this._startSnappingEnabled!==h&&(this._startSnappingEnabled=h,this.updateSashEnablement())}get endSnappingEnabled(){return this._endSnappingEnabled}set endSnappingEnabled(h){this._endSnappingEnabled!==h&&(this._endSnappingEnabled=h,this.updateSashEnablement())}style(h){h.separatorBorder.isTransparent()?(this.el.classList.remove("separator-border"),this.el.style.removeProperty("--separator-border")):(this.el.classList.add("separator-border"),this.el.style.setProperty("--separator-border",h.separatorBorder.toString()))}addView(h,m,_=this.viewItems.length,f){this.doAddView(h,m,_,f)}layout(h,m){const _=Math.max(this.size,this.contentSize);if(this.size=h,this.layoutContext=m,this.proportions)for(let f=0;fthis.viewItems[L].priority===1),y=f.filter(L=>this.viewItems[L].priority===2);this.resize(this.viewItems.length-1,h-_,void 0,v,y)}this.distributeEmptySpace(),this.layoutViews()}saveProportions(){this.proportionalLayout&&this.contentSize>0&&(this.proportions=this.viewItems.map(h=>h.size/this.contentSize))}onSashStart({sash:h,start:m,alt:_}){for(const L of this.viewItems)L.enabled=!1;const f=this.sashItems.findIndex(L=>L.sash===h),v=b.combinedDisposable(g.domEvent(document.body,"keydown")(L=>y(this.sashDragState.current,L.altKey)),g.domEvent(document.body,"keyup")(()=>y(this.sashDragState.current,!1))),y=(L,I)=>{const k=this.viewItems.map(B=>B.size);let E=Number.NEGATIVE_INFINITY,T=Number.POSITIVE_INFINITY;if(this.inverseAltBehavior&&(I=!I),I)if(f===this.sashItems.length-1){const F=this.viewItems[f];E=(F.minimumSize-F.size)/2,T=(F.maximumSize-F.size)/2}else{const F=this.viewItems[f+1];E=(F.size-F.maximumSize)/2,T=(F.size-F.minimumSize)/2}let O,A;if(!I){const B=S.range(f,-1),F=S.range(f+1,this.viewItems.length),D=B.reduce((ne,le)=>ne+(this.viewItems[le].minimumSize-k[le]),0),R=B.reduce((ne,le)=>ne+(this.viewItems[le].viewMaximumSize-k[le]),0),W=F.length===0?Number.POSITIVE_INFINITY:F.reduce((ne,le)=>ne+(k[le]-this.viewItems[le].minimumSize),0),x=F.length===0?Number.NEGATIVE_INFINITY:F.reduce((ne,le)=>ne+(k[le]-this.viewItems[le].viewMaximumSize),0),K=Math.max(D,x),Y=Math.min(W,R),ee=this.findFirstSnapIndex(B),se=this.findFirstSnapIndex(F);if(typeof ee=="number"){const ne=this.viewItems[ee],le=Math.floor(ne.viewMinimumSize/2);O={index:ee,limitDelta:ne.visible?K-le:K+le,size:ne.size}}if(typeof se=="number"){const ne=this.viewItems[se],le=Math.floor(ne.viewMinimumSize/2);A={index:se,limitDelta:ne.visible?Y+le:Y-le,size:ne.size}}}this.sashDragState={start:L,current:L,index:f,sizes:k,minDelta:E,maxDelta:T,alt:I,snapBefore:O,snapAfter:A,disposable:v}};y(m,_)}onSashChange({current:h}){const{index:m,start:_,sizes:f,alt:v,minDelta:y,maxDelta:L,snapBefore:I,snapAfter:k}=this.sashDragState;this.sashDragState.current=h;const E=h-_,T=this.resize(m,E,f,void 0,void 0,y,L,I,k);if(v){const O=m===this.sashItems.length-1,A=this.viewItems.map(x=>x.size),B=O?m:m+1,F=this.viewItems[B],D=F.size-F.maximumSize,R=F.size-F.minimumSize,W=O?m-1:m+1;this.resize(W,-T,A,void 0,void 0,D,R)}this.distributeEmptySpace(),this.layoutViews()}onSashEnd(h){this._onDidSashChange.fire(h),this.sashDragState.disposable.dispose(),this.saveProportions();for(const m of this.viewItems)m.enabled=!0}onViewChange(h,m){const _=this.viewItems.indexOf(h);_<0||_>=this.viewItems.length||(m=typeof m=="number"?m:h.size,m=w.clamp(m,h.minimumSize,h.maximumSize),this.inverseAltBehavior&&_>0?(this.resize(_-1,Math.floor((h.size-m)/2)),this.distributeEmptySpace(),this.layoutViews()):(h.size=m,this.relayout([_],void 0)))}resizeView(h,m){if(this.state!==i.Idle)throw new Error("Cant modify splitview");if(this.state=i.Busy,!(h<0||h>=this.viewItems.length)){const _=S.range(this.viewItems.length).filter(L=>L!==h),f=[..._.filter(L=>this.viewItems[L].priority===1),h],v=_.filter(L=>this.viewItems[L].priority===2),y=this.viewItems[h];m=Math.round(m),m=w.clamp(m,y.minimumSize,Math.min(y.maximumSize,this.size)),y.size=m,this.relayout(f,v),this.state=i.Idle}}distributeViewSizes(){const h=[];let m=0;for(const L of this.viewItems)L.maximumSize-L.minimumSize>0&&(h.push(L),m+=L.size);const _=Math.floor(m/h.length);for(const L of h)L.size=w.clamp(_,L.minimumSize,L.maximumSize);const f=S.range(this.viewItems.length),v=f.filter(L=>this.viewItems[L].priority===1),y=f.filter(L=>this.viewItems[L].priority===2);this.relayout(v,y)}getViewSize(h){return h<0||h>=this.viewItems.length?-1:this.viewItems[h].size}doAddView(h,m,_=this.viewItems.length,f){if(this.state!==i.Idle)throw new Error("Cant modify splitview");this.state=i.Busy;const v=p.$(".split-view-view");_===this.viewItems.length?this.viewContainer.appendChild(v):this.viewContainer.insertBefore(v,this.viewContainer.children.item(_));const y=h.onDidChange(O=>this.onViewChange(E,O)),L=b.toDisposable(()=>this.viewContainer.removeChild(v)),I=b.combinedDisposable(y,L);let k;typeof m=="number"?k=m:m.type==="split"?k=this.getViewSize(m.index)/2:m.type==="invisible"?k={cachedVisibleSize:m.cachedVisibleSize}:k=h.minimumSize;const E=this.orientation===0?new u(v,h,k,I):new r(v,h,k,I);if(this.viewItems.splice(_,0,E),this.viewItems.length>1){let O={orthogonalStartSash:this.orthogonalStartSash,orthogonalEndSash:this.orthogonalEndSash};const A=this.orientation===0?new C.Sash(this.sashContainer,{getHorizontalSashTop:ne=>this.getSashPosition(ne),getHorizontalSashWidth:this.getSashOrthogonalSize},Object.assign(Object.assign({},O),{orientation:1})):new C.Sash(this.sashContainer,{getVerticalSashLeft:ne=>this.getSashPosition(ne),getVerticalSashHeight:this.getSashOrthogonalSize},Object.assign(Object.assign({},O),{orientation:0})),B=this.orientation===0?ne=>({sash:A,start:ne.startY,current:ne.currentY,alt:ne.altKey}):ne=>({sash:A,start:ne.startX,current:ne.currentX,alt:ne.altKey}),D=N.Event.map(A.onDidStart,B)(this.onSashStart,this),W=N.Event.map(A.onDidChange,B)(this.onSashChange,this),K=N.Event.map(A.onDidEnd,()=>this.sashItems.findIndex(ne=>ne.sash===A))(this.onSashEnd,this),Y=A.onDidReset(()=>{const ne=this.sashItems.findIndex(V=>V.sash===A),le=S.range(ne,-1),X=S.range(ne+1,this.viewItems.length),z=this.findFirstSnapIndex(le),P=this.findFirstSnapIndex(X);typeof z=="number"&&!this.viewItems[z].visible||typeof P=="number"&&!this.viewItems[P].visible||this._onDidSashReset.fire(ne)}),ee=b.combinedDisposable(D,W,K,Y,A),se={sash:A,disposable:ee};this.sashItems.splice(_-1,0,se)}v.appendChild(h.element);let T;typeof m!="number"&&m.type==="split"&&(T=[m.index]),f||this.relayout([_],T),this.state=i.Idle,!f&&typeof m!="number"&&m.type==="distribute"&&this.distributeViewSizes()}relayout(h,m){const _=this.viewItems.reduce((f,v)=>f+v.size,0);this.resize(this.viewItems.length-1,this.size-_,void 0,h,m),this.distributeEmptySpace(),this.layoutViews(),this.saveProportions()}resize(h,m,_=this.viewItems.map(E=>E.size),f,v,y=Number.NEGATIVE_INFINITY,L=Number.POSITIVE_INFINITY,I,k){if(h<0||h>=this.viewItems.length)return 0;const E=S.range(h,-1),T=S.range(h+1,this.viewItems.length);if(v)for(const se of v)S.pushToStart(E,se),S.pushToStart(T,se);if(f)for(const se of f)S.pushToEnd(E,se),S.pushToEnd(T,se);const O=E.map(se=>this.viewItems[se]),A=E.map(se=>_[se]),B=T.map(se=>this.viewItems[se]),F=T.map(se=>_[se]),D=E.reduce((se,ne)=>se+(this.viewItems[ne].minimumSize-_[ne]),0),R=E.reduce((se,ne)=>se+(this.viewItems[ne].maximumSize-_[ne]),0),W=T.length===0?Number.POSITIVE_INFINITY:T.reduce((se,ne)=>se+(_[ne]-this.viewItems[ne].minimumSize),0),x=T.length===0?Number.NEGATIVE_INFINITY:T.reduce((se,ne)=>se+(_[ne]-this.viewItems[ne].maximumSize),0),K=Math.max(D,x,y),Y=Math.min(W,R,L);let ee=!1;if(I){const se=this.viewItems[I.index],ne=m>=I.limitDelta;ee=ne!==se.visible,se.setVisible(ne,I.size)}if(!ee&&k){const se=this.viewItems[k.index],ne=mL+I.size,0);let _=this.size-m;const f=S.range(this.viewItems.length-1,-1),v=f.filter(L=>this.viewItems[L].priority===1),y=f.filter(L=>this.viewItems[L].priority===2);for(const L of y)S.pushToStart(f,L);for(const L of v)S.pushToEnd(f,L);typeof h=="number"&&S.pushToEnd(f,h);for(let L=0;_!==0&&Lm+_.size,0);let h=0;for(const m of this.viewItems)m.layout(h,this.layoutContext),h+=m.size;this.sashItems.forEach(m=>m.sash.layout()),this.updateSashEnablement(),this.updateScrollableElement()}updateScrollableElement(){this.orientation===0?this.scrollableElement.setScrollDimensions({height:this.size,scrollHeight:this.contentSize}):this.scrollableElement.setScrollDimensions({width:this.size,scrollWidth:this.contentSize})}updateSashEnablement(){let h=!1;const m=this.viewItems.map(I=>h=I.size-I.minimumSize>0||h);h=!1;const _=this.viewItems.map(I=>h=I.maximumSize-I.size>0||h),f=[...this.viewItems].reverse();h=!1;const v=f.map(I=>h=I.size-I.minimumSize>0||h).reverse();h=!1;const y=f.map(I=>h=I.maximumSize-I.size>0||h).reverse();let L=0;for(let I=0;I0||this.startSnappingEnabled)?k.state=1:W&&m[I]&&(L0)return;if(!_.visible&&_.snap)return m}}dispose(){super.dispose(),this.viewItems.forEach(h=>h.dispose()),this.viewItems=[],this.sashItems.forEach(h=>h.disposable.dispose()),this.sashItems=[]}}e.SplitView=t}),define(Q[314],J([10]),{}),define(Q[315],J([0,1,105,7,210,6,314]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Table=void 0;class S{constructor(c,o,s){this.columns=c,this.getColumnSize=s,this.templateId=S.TemplateId,this.renderedTemplates=new Set;const a=new Map(o.map(u=>[u.templateId,u]));this.renderers=[];for(const u of c){const r=a.get(u.templateId);if(!r)throw new Error(`Table cell renderer for template id ${u.templateId} not found.`);this.renderers.push(r)}}renderTemplate(c){const o=N.append(c,N.$(".monaco-table-tr")),s=[],a=[];for(let r=0;rnew d(l,h)),n={size:i.reduce((l,h)=>l+h.column.weight,0),views:i.map(l=>({size:l.column.weight,view:l}))};this.splitview=new M.SplitView(this.domNode,{orientation:1,scrollbarVisibility:2,getSashOrthogonalSize:()=>this.cachedHeight,descriptor:n}),this.splitview.el.style.height=`${s.headerRowHeight}px`,this.splitview.el.style.lineHeight=`${s.headerRowHeight}px`;const t=new S(a,u,l=>this.splitview.getViewSize(l));this.list=new b.List(c,this.domNode,C(s),[t],r),this.columnLayoutDisposable=w.Event.any(...i.map(l=>l.onDidLayout))(([l,h])=>t.layoutColumn(l,h)),this.styleElement=N.createStyleSheet(this.domNode),this.style({})}get onDidChangeFocus(){return this.list.onDidChangeFocus}get onDidChangeSelection(){return this.list.onDidChangeSelection}get onMouseDblClick(){return this.list.onMouseDblClick}get onPointer(){return this.list.onPointer}get onDidFocus(){return this.list.onDidFocus}get onDidDispose(){return this.list.onDidDispose}updateOptions(c){this.list.updateOptions(c)}splice(c,o,s=[]){this.list.splice(c,o,s)}getHTMLElement(){return this.domNode}style(c){const o=[];o.push(`.monaco-table.${this.domId} > .monaco-split-view2 .monaco-sash.vertical::before { + top: ${this.virtualDelegate.headerRowHeight+1}px; + height: calc(100% - ${this.virtualDelegate.headerRowHeight}px); + }`),this.styleElement.textContent=o.join(` +`),this.list.style(c)}getSelectedElements(){return this.list.getSelectedElements()}setSelection(c,o){this.list.setSelection(c,o)}getSelection(){return this.list.getSelection()}getFocus(){return this.list.getFocus()}dispose(){this.splitview.dispose(),this.list.dispose(),this.columnLayoutDisposable.dispose()}}e.Table=g,g.InstanceCount=0}),define(Q[316],J([10]),{}),define(Q[125],J([10]),{}),define(Q[211],J([0,1,7,119,125]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getIconClass=void 0;const M={},w=new N.IdGenerator("quick-input-button-icon-");function S(C){if(!!C){let d;const g=C.dark.toString();return M[g]?d=M[g]:(d=w.nextId(),b.createCSSRule(`.${d}`,`background-image: ${b.asCSSUrl(C.light||C.dark)}`),b.createCSSRule(`.vs-dark .${d}, .hc-black .${d}`,`background-image: ${b.asCSSUrl(C.dark)}`),M[g]=d),d}}e.getIconClass=S}),define(Q[317],J([10]),{}),define(Q[318],J([10]),{}),define(Q[319],J([10]),{}),define(Q[320],J([10]),{}),define(Q[321],J([10]),{}),define(Q[322],J([10]),{}),define(Q[323],J([10]),{}),define(Q[324],J([10]),{}),define(Q[325],J([10]),{}),define(Q[326],J([10]),{}),define(Q[327],J([10]),{}),define(Q[328],J([10]),{}),define(Q[329],J([10]),{}),define(Q[330],J([10]),{}),define(Q[331],J([10]),{}),define(Q[332],J([10]),{}),define(Q[333],J([10]),{}),define(Q[334],J([10]),{}),define(Q[335],J([10]),{}),define(Q[336],J([10]),{}),define(Q[337],J([10]),{}),define(Q[338],J([10]),{}),define(Q[339],J([10]),{}),define(Q[340],J([10]),{}),define(Q[341],J([10]),{}),define(Q[342],J([10]),{}),define(Q[343],J([10]),{}),define(Q[344],J([10]),{}),define(Q[345],J([10]),{}),define(Q[346],J([10]),{}),define(Q[347],J([10]),{}),define(Q[348],J([10]),{}),define(Q[349],J([10]),{}),define(Q[350],J([10]),{}),define(Q[351],J([10]),{}),define(Q[352],J([10]),{}),define(Q[353],J([10]),{}),define(Q[354],J([10]),{}),define(Q[355],J([10]),{}),define(Q[356],J([10]),{}),define(Q[357],J([10]),{}),define(Q[358],J([10]),{}),define(Q[359],J([10]),{}),define(Q[360],J([10]),{}),define(Q[361],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.readCharWidths=e.CharWidthRequest=void 0;class b{constructor(S,C){this.chr=S,this.type=C,this.width=0}fulfill(S){this.width=S}}e.CharWidthRequest=b;class N{constructor(S,C){this._bareFontInfo=S,this._requests=C,this._container=null,this._testElements=null}read(){this._createDomElements(),document.body.appendChild(this._container),this._readFromDomElements(),document.body.removeChild(this._container),this._container=null,this._testElements=null}_createDomElements(){const S=document.createElement("div");S.style.position="absolute",S.style.top="-50000px",S.style.width="50000px";const C=document.createElement("div");C.style.fontFamily=this._bareFontInfo.getMassagedFontFamily(),C.style.fontWeight=this._bareFontInfo.fontWeight,C.style.fontSize=this._bareFontInfo.fontSize+"px",C.style.fontFeatureSettings=this._bareFontInfo.fontFeatureSettings,C.style.lineHeight=this._bareFontInfo.lineHeight+"px",C.style.letterSpacing=this._bareFontInfo.letterSpacing+"px",S.appendChild(C);const d=document.createElement("div");d.style.fontFamily=this._bareFontInfo.getMassagedFontFamily(),d.style.fontWeight="bold",d.style.fontSize=this._bareFontInfo.fontSize+"px",d.style.fontFeatureSettings=this._bareFontInfo.fontFeatureSettings,d.style.lineHeight=this._bareFontInfo.lineHeight+"px",d.style.letterSpacing=this._bareFontInfo.letterSpacing+"px",S.appendChild(d);const g=document.createElement("div");g.style.fontFamily=this._bareFontInfo.getMassagedFontFamily(),g.style.fontWeight=this._bareFontInfo.fontWeight,g.style.fontSize=this._bareFontInfo.fontSize+"px",g.style.fontFeatureSettings=this._bareFontInfo.fontFeatureSettings,g.style.lineHeight=this._bareFontInfo.lineHeight+"px",g.style.letterSpacing=this._bareFontInfo.letterSpacing+"px",g.style.fontStyle="italic",S.appendChild(g);const p=[];for(const c of this._requests){let o;c.type===0&&(o=C),c.type===2&&(o=d),c.type===1&&(o=g),o.appendChild(document.createElement("br"));const s=document.createElement("span");N._render(s,c),o.appendChild(s),p.push(s)}this._container=S,this._testElements=p}static _render(S,C){if(C.chr===" "){let d="\xA0";for(let g=0;g<8;g++)d+=d;S.innerText=d}else{let d=C.chr;for(let g=0;g<8;g++)d+=d;S.textContent=d}}_readFromDomElements(){for(let S=0,C=this._requests.length;S{w&&w[0]&&w[0].contentRect?this.observe({width:w[0].contentRect.width,height:w[0].contentRect.height}):this.observe()}),this.resizeObserver.observe(this.referenceDomElement)):this.measureReferenceDomElementToken===-1&&(this.measureReferenceDomElementToken=setInterval(()=>this.observe(),100))}stopObserving(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.measureReferenceDomElementToken!==-1&&(clearInterval(this.measureReferenceDomElementToken),this.measureReferenceDomElementToken=-1)}observe(w){this.measureReferenceDomElement(!0,w)}measureReferenceDomElement(w,S){let C=0,d=0;S?(C=S.width,d=S.height):this.referenceDomElement&&(C=this.referenceDomElement.clientWidth,d=this.referenceDomElement.clientHeight),C=Math.max(5,C),d=Math.max(5,d),(this.width!==C||this.height!==d)&&(this.width=C,this.height=d,w&&this.changeCallback())}}e.ElementSizeObserver=N}),define(Q[162],J([0,1,7,90,50,2]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.GlobalEditorMouseMoveMonitor=e.EditorPointerEventFactory=e.EditorMouseEventFactory=e.EditorMouseEvent=e.createEditorPagePosition=e.EditorPagePosition=e.ClientCoordinates=e.PageCoordinates=void 0;class S{constructor(u,r){this.x=u,this.y=r}toClientCoordinates(){return new C(this.x-b.StandardWindow.scrollX,this.y-b.StandardWindow.scrollY)}}e.PageCoordinates=S;class C{constructor(u,r){this.clientX=u,this.clientY=r}toPageCoordinates(){return new S(this.clientX+b.StandardWindow.scrollX,this.clientY+b.StandardWindow.scrollY)}}e.ClientCoordinates=C;class d{constructor(u,r,i,n){this.x=u,this.y=r,this.width=i,this.height=n}}e.EditorPagePosition=d;function g(a){const u=b.getDomNodePagePosition(a);return new d(u.left,u.top,u.width,u.height)}e.createEditorPagePosition=g;class p extends M.StandardMouseEvent{constructor(u,r){super(u);this.pos=new S(this.posx,this.posy),this.editorPos=g(r)}}e.EditorMouseEvent=p;class c{constructor(u){this._editorViewDomNode=u}_create(u){return new p(u,this._editorViewDomNode)}onContextMenu(u,r){return b.addDisposableListener(u,"contextmenu",i=>{r(this._create(i))})}onMouseUp(u,r){return b.addDisposableListener(u,"mouseup",i=>{r(this._create(i))})}onMouseDown(u,r){return b.addDisposableListener(u,"mousedown",i=>{r(this._create(i))})}onMouseLeave(u,r){return b.addDisposableNonBubblingMouseOutListener(u,i=>{r(this._create(i))})}onMouseMoveThrottled(u,r,i,n){const t=(l,h)=>i(l,this._create(h));return b.addDisposableThrottledListener(u,"mousemove",r,t,n)}}e.EditorMouseEventFactory=c;class o{constructor(u){this._editorViewDomNode=u}_create(u){return new p(u,this._editorViewDomNode)}onPointerUp(u,r){return b.addDisposableListener(u,"pointerup",i=>{r(this._create(i))})}onPointerDown(u,r){return b.addDisposableListener(u,"pointerdown",i=>{r(this._create(i))})}onPointerLeave(u,r){return b.addDisposableNonBubblingPointerOutListener(u,i=>{r(this._create(i))})}onPointerMoveThrottled(u,r,i,n){const t=(l,h)=>i(l,this._create(h));return b.addDisposableThrottledListener(u,"pointermove",r,t,n)}}e.EditorPointerEventFactory=o;class s extends w.Disposable{constructor(u){super();this._editorViewDomNode=u,this._globalMouseMoveMonitor=this._register(new N.GlobalMouseMoveMonitor),this._keydownListener=null}startMonitoring(u,r,i,n,t){this._keydownListener=b.addStandardDisposableListener(document,"keydown",h=>{h.toKeybinding().isModifierKey()||this._globalMouseMoveMonitor.stopMonitoring(!0,h.browserEvent)},!0);const l=(h,m)=>i(h,new p(m,this._editorViewDomNode));this._globalMouseMoveMonitor.startMonitoring(u,r,l,n,h=>{this._keydownListener.dispose(),t(h)})}stopMonitoring(){this._globalMouseMoveMonitor.stopMonitoring(!0)}}e.GlobalEditorMouseMoveMonitor=s}),define(Q[362],J([0,1,6,2]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractCodeEditorService=void 0;class M extends N.Disposable{constructor(){super();this._onCodeEditorAdd=this._register(new b.Emitter),this.onCodeEditorAdd=this._onCodeEditorAdd.event,this._onCodeEditorRemove=this._register(new b.Emitter),this.onCodeEditorRemove=this._onCodeEditorRemove.event,this._onDiffEditorAdd=this._register(new b.Emitter),this._onDiffEditorRemove=this._register(new b.Emitter),this._onDecorationTypeRegistered=this._register(new b.Emitter),this._modelProperties=new Map,this._codeEditors=Object.create(null),this._diffEditors=Object.create(null)}addCodeEditor(S){this._codeEditors[S.getId()]=S,this._onCodeEditorAdd.fire(S)}removeCodeEditor(S){delete this._codeEditors[S.getId()]&&this._onCodeEditorRemove.fire(S)}listCodeEditors(){return Object.keys(this._codeEditors).map(S=>this._codeEditors[S])}addDiffEditor(S){this._diffEditors[S.getId()]=S,this._onDiffEditorAdd.fire(S)}removeDiffEditor(S){delete this._diffEditors[S.getId()]&&this._onDiffEditorRemove.fire(S)}listDiffEditors(){return Object.keys(this._diffEditors).map(S=>this._diffEditors[S])}getFocusedCodeEditor(){let S=null;const C=this.listCodeEditors();for(const d of C){if(d.hasTextFocus())return d;d.hasWidgetFocus()&&(S=d)}return S}setModelProperty(S,C,d){const g=S.toString();let p;this._modelProperties.has(g)?p=this._modelProperties.get(g):(p=new Map,this._modelProperties.set(g,p)),p.set(C,d)}getModelProperty(S,C){const d=S.toString();if(this._modelProperties.has(d))return this._modelProperties.get(d).get(C)}}e.AbstractCodeEditorService=M}),define(Q[213],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getCharIndex=e.allCharCodes=void 0,e.allCharCodes=(()=>{const N=[];for(let M=32;M<=126;M++)N.push(M);return N.push(65533),N})();const b=(N,M)=>(N-=32,N<0||N>96?M<=2?(N+96)%96:96-1:N);e.getCharIndex=b}),define(Q[363],J([0,1,213,122]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MinimapCharRenderer=void 0;class M{constructor(S,C){this.scale=C,this.charDataNormal=M.soften(S,12/15),this.charDataLight=M.soften(S,50/60)}static soften(S,C){let d=new Uint8ClampedArray(S.length);for(let g=0,p=S.length;gS.width||d+i>S.height){console.warn("bad render request outside image data");return}const n=s?this.charDataLight:this.charDataNormal,t=b.getCharIndex(g,o),l=S.width*4,h=c.r,m=c.g,_=c.b,f=p.r-h,v=p.g-m,y=p.b-_,L=S.data;let I=t*u*r,k=d*l+C*4;for(let E=0;ES.width||d+u>S.height){console.warn("bad render request outside image data");return}const r=S.width*4,i=.5,n=p.r,t=p.g,l=p.b,h=g.r-n,m=g.g-t,_=g.b-l,f=n+h*i,v=t+m*i,y=l+_*i,L=S.data;let I=d*r+C*4;for(let k=0;k{const S=new Uint8ClampedArray(w.length/2);for(let C=0;C>1]=N[w[C]]<<4|N[w[C+1]]&15;return S};e.prebakedMiniMaps={1:b.once(()=>M("0000511D6300CF609C709645A78432005642574171487021003C451900274D35D762755E8B629C5BA856AF57BA649530C167D1512A272A3F6038604460398526BCA2A968DB6F8957C768BE5FBE2FB467CF5D8D5B795DC7625B5DFF50DE64C466DB2FC47CD860A65E9A2EB96CB54CE06DA763AB2EA26860524D3763536601005116008177A8705E53AB738E6A982F88BAA35B5F5B626D9C636B449B737E5B7B678598869A662F6B5B8542706C704C80736A607578685B70594A49715A4522E792")),2:b.once(()=>M("000000000000000055394F383D2800008B8B1F210002000081B1CBCBCC820000847AAF6B9AAF2119BE08B8881AD60000A44FD07DCCF107015338130C00000000385972265F390B406E2437634B4B48031B12B8A0847000001E15B29A402F0000000000004B33460B00007A752C2A0000000000004D3900000084394B82013400ABA5CFC7AD9C0302A45A3E5A98AB000089A43382D97900008BA54AA087A70A0248A6A7AE6DBE0000BF6F94987EA40A01A06DCFA7A7A9030496C32F77891D0000A99FB1A0AFA80603B29AB9CA75930D010C0948354D3900000C0948354F37460D0028BE673D8400000000AF9D7B6E00002B007AA8933400007AA642675C2700007984CFB9C3985B768772A8A6B7B20000CAAECAAFC4B700009F94A6009F840009D09F9BA4CA9C0000CC8FC76DC87F0000C991C472A2000000A894A48CA7B501079BA2C9C69BA20000B19A5D3FA89000005CA6009DA2960901B0A7F0669FB200009D009E00B7890000DAD0F5D092820000D294D4C48BD10000B5A7A4A3B1A50402CAB6CBA6A2000000B5A7A4A3B1A8044FCDADD19D9CB00000B7778F7B8AAE0803C9AB5D3F5D3F00009EA09EA0BAB006039EA0989A8C7900009B9EF4D6B7C00000A9A7816CACA80000ABAC84705D3F000096DA635CDC8C00006F486F266F263D4784006124097B00374F6D2D6D2D6D4A3A95872322000000030000000000008D8939130000000000002E22A5C9CBC70600AB25C0B5C9B400061A2DB04CA67001082AA6BEBEBFC606002321DACBC19E03087AA08B6768380000282FBAC0B8CA7A88AD25BBA5A29900004C396C5894A6000040485A6E356E9442A32CD17EADA70000B4237923628600003E2DE9C1D7B500002F25BBA5A2990000231DB6AFB4A804023025C0B5CAB588062B2CBDBEC0C706882435A75CA20000002326BD6A82A908048B4B9A5A668000002423A09CB4BB060025259C9D8A7900001C1FCAB2C7C700002A2A9387ABA200002626A4A47D6E9D14333163A0C87500004B6F9C2D643A257049364936493647358A34438355497F1A0000A24C1D590000D38DFFBDD4CD3126"))}}),define(Q[365],J([0,1,363,213,364,122]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MinimapCharRendererFactory=void 0;class S{static create(d,g){if(this.lastCreated&&d===this.lastCreated.scale&&g===this.lastFontFamily)return this.lastCreated;let p;return M.prebakedMiniMaps[d]?p=new b.MinimapCharRenderer(M.prebakedMiniMaps[d](),d):p=S.createFromSampleData(S.createSampleData(g).data,d),this.lastFontFamily=g,this.lastCreated=p,p}static createSampleData(d){const g=document.createElement("canvas"),p=g.getContext("2d");g.style.height=`${16}px`,g.height=16,g.width=96*10,g.style.width=96*10+"px",p.fillStyle="#ffffff",p.font=`bold ${16}px ${d}`,p.textBaseline="middle";let c=0;for(const o of N.allCharCodes)p.fillText(String.fromCharCode(o),c,16/2),c+=10;return p.getImageData(0,0,96*10,16)}static createFromSampleData(d,g){const p=16*10*4*96;if(d.length!==p)throw new Error("Unexpected source in MinimapCharRenderer");let c=S._downsample(d,g);return new b.MinimapCharRenderer(c,g)}static _downsampleChar(d,g,p,c,o){const s=1*o,a=2*o;let u=c,r=0;for(let i=0;i0){const r=255/u;for(let i=0;i=0&&S<256?this._asciiMap[S]=d:this._map.set(S,d)}get(S){return S>=0&&S<256?this._asciiMap[S]:this._map.get(S)||this._defaultValue}}e.CharacterClassifier=N;class M{constructor(){this._actual=new N(0)}add(S){this._actual.set(S,1)}has(S){return this._actual.get(S)===1}}e.CharacterSet=M}),define(Q[106],J([0,1,91]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getMapForWordSeparators=e.WordCharacterClassifier=void 0;class N extends b.CharacterClassifier{constructor(S){super(0);for(let C=0,d=S.length;C(S.hasOwnProperty(C)||(S[C]=w(C)),S[C])}e.getMapForWordSeparators=M(w=>new N(w))}),define(Q[14],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Position=void 0;class b{constructor(M,w){this.lineNumber=M,this.column=w}with(M=this.lineNumber,w=this.column){return M===this.lineNumber&&w===this.column?this:new b(M,w)}delta(M=0,w=0){return this.with(this.lineNumber+M,this.column+w)}equals(M){return b.equals(this,M)}static equals(M,w){return!M&&!w?!0:!!M&&!!w&&M.lineNumber===w.lineNumber&&M.column===w.column}isBefore(M){return b.isBefore(this,M)}static isBefore(M,w){return M.lineNumberC||w===C&&S>d?(this.startLineNumber=C,this.startColumn=d,this.endLineNumber=w,this.endColumn=S):(this.startLineNumber=w,this.startColumn=S,this.endLineNumber=C,this.endColumn=d)}isEmpty(){return N.isEmpty(this)}static isEmpty(w){return w.startLineNumber===w.endLineNumber&&w.startColumn===w.endColumn}containsPosition(w){return N.containsPosition(this,w)}static containsPosition(w,S){return!(S.lineNumberw.endLineNumber||S.lineNumber===w.startLineNumber&&S.columnw.endColumn)}containsRange(w){return N.containsRange(this,w)}static containsRange(w,S){return!(S.startLineNumberw.endLineNumber||S.endLineNumber>w.endLineNumber||S.startLineNumber===w.startLineNumber&&S.startColumnw.endColumn)}strictContainsRange(w){return N.strictContainsRange(this,w)}static strictContainsRange(w,S){return!(S.startLineNumberw.endLineNumber||S.endLineNumber>w.endLineNumber||S.startLineNumber===w.startLineNumber&&S.startColumn<=w.startColumn||S.endLineNumber===w.endLineNumber&&S.endColumn>=w.endColumn)}plusRange(w){return N.plusRange(this,w)}static plusRange(w,S){let C,d,g,p;return S.startLineNumberw.endLineNumber?(g=S.endLineNumber,p=S.endColumn):S.endLineNumber===w.endLineNumber?(g=S.endLineNumber,p=Math.max(S.endColumn,w.endColumn)):(g=w.endLineNumber,p=w.endColumn),new N(C,d,g,p)}intersectRanges(w){return N.intersectRanges(this,w)}static intersectRanges(w,S){let C=w.startLineNumber,d=w.startColumn,g=w.endLineNumber,p=w.endColumn,c=S.startLineNumber,o=S.startColumn,s=S.endLineNumber,a=S.endColumn;return Cs?(g=s,p=a):g===s&&(p=Math.min(p,a)),C>g||C===g&&d>p?null:new N(C,d,g,p)}equalsRange(w){return N.equalsRange(this,w)}static equalsRange(w,S){return!!w&&!!S&&w.startLineNumber===S.startLineNumber&&w.startColumn===S.startColumn&&w.endLineNumber===S.endLineNumber&&w.endColumn===S.endColumn}getEndPosition(){return N.getEndPosition(this)}static getEndPosition(w){return new b.Position(w.endLineNumber,w.endColumn)}getStartPosition(){return N.getStartPosition(this)}static getStartPosition(w){return new b.Position(w.startLineNumber,w.startColumn)}toString(){return"["+this.startLineNumber+","+this.startColumn+" -> "+this.endLineNumber+","+this.endColumn+"]"}setEndPosition(w,S){return new N(this.startLineNumber,this.startColumn,w,S)}setStartPosition(w,S){return new N(w,S,this.endLineNumber,this.endColumn)}collapseToStart(){return N.collapseToStart(this)}static collapseToStart(w){return new N(w.startLineNumber,w.startColumn,w.startLineNumber,w.startColumn)}static fromPositions(w,S=w){return new N(w.lineNumber,w.column,S.lineNumber,S.column)}static lift(w){return w?new N(w.startLineNumber,w.startColumn,w.endLineNumber,w.endColumn):null}static isIRange(w){return w&&typeof w.startLineNumber=="number"&&typeof w.startColumn=="number"&&typeof w.endLineNumber=="number"&&typeof w.endColumn=="number"}static areIntersectingOrTouching(w,S){return!(w.endLineNumberw.startLineNumber}}e.Range=N}),define(Q[214],J([0,1,8,14,3]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PagedScreenReaderStrategy=e.TextAreaState=e._debugComposition=void 0,e._debugComposition=!1;class w{constructor(d,g,p,c,o){this.value=d,this.selectionStart=g,this.selectionEnd=p,this.selectionStartPosition=c,this.selectionEndPosition=o}toString(){return"[ <"+this.value+">, selectionStart: "+this.selectionStart+", selectionEnd: "+this.selectionEnd+"]"}static readFromTextArea(d){return new w(d.getValue(),d.getSelectionStart(),d.getSelectionEnd(),null,null)}collapseSelection(){return new w(this.value,this.value.length,this.value.length,null,null)}writeToTextArea(d,g,p){e._debugComposition&&console.log("writeToTextArea "+d+": "+this.toString()),g.setValue(d,this.value),p&&g.setSelectionRange(d,this.selectionStart,this.selectionEnd)}deduceEditorPosition(d){if(d<=this.selectionStart){const c=this.value.substring(d,this.selectionStart);return this._finishDeduceEditorPosition(this.selectionStartPosition,c,-1)}if(d>=this.selectionEnd){const c=this.value.substring(this.selectionEnd,d);return this._finishDeduceEditorPosition(this.selectionEndPosition,c,1)}const g=this.value.substring(this.selectionStart,d);if(g.indexOf(String.fromCharCode(8230))===-1)return this._finishDeduceEditorPosition(this.selectionStartPosition,g,1);const p=this.value.substring(d,this.selectionEnd);return this._finishDeduceEditorPosition(this.selectionEndPosition,p,-1)}_finishDeduceEditorPosition(d,g,p){let c=0,o=-1;for(;(o=g.indexOf(` +`,o+1))!==-1;)c++;return[d,p*g.length,c]}static selectedText(d){return new w(d,0,d.length,null,null)}static deduceInput(d,g,p){if(!d)return{text:"",replacePrevCharCnt:0,replaceNextCharCnt:0,positionDelta:0};e._debugComposition&&(console.log("------------------------deduceInput"),console.log("PREVIOUS STATE: "+d.toString()),console.log("CURRENT STATE: "+g.toString()));let c=d.value,o=d.selectionStart,s=d.selectionEnd,a=g.value,u=g.selectionStart,r=g.selectionEnd;const i=c.substring(s),n=a.substring(r),t=b.commonSuffixLength(i,n);a=a.substring(0,a.length-t),c=c.substring(0,c.length-t);const l=c.substring(0,o),h=a.substring(0,u),m=b.commonPrefixLength(l,h);if(a=a.substring(m),c=c.substring(m),u-=m,o-=m,r-=m,s-=m,e._debugComposition&&(console.log("AFTER DIFFING PREVIOUS STATE: <"+c+">, selectionStart: "+o+", selectionEnd: "+s),console.log("AFTER DIFFING CURRENT STATE: <"+a+">, selectionStart: "+u+", selectionEnd: "+r)),p&&u===r&&c.length>0){let f=null;if(u===a.length?a.startsWith(c)&&(f=a.substring(c.length)):a.endsWith(c)&&(f=a.substring(0,a.length-c.length)),f!==null&&f.length>0&&(/\uFE0F/.test(f)||b.containsEmoji(f)))return{text:f,replacePrevCharCnt:0,replaceNextCharCnt:0,positionDelta:0}}if(u===r){if(c===a&&o===0&&s===c.length&&u===a.length&&a.indexOf(` +`)===-1&&b.containsFullWidthCharacter(a))return{text:"",replacePrevCharCnt:0,replaceNextCharCnt:0,positionDelta:0};const f=l.length-m;return e._debugComposition&&console.log("REMOVE PREVIOUS: "+(l.length-m)+" chars"),{text:a,replacePrevCharCnt:f,replaceNextCharCnt:0,positionDelta:0}}const _=s-o;return{text:a,replacePrevCharCnt:_,replaceNextCharCnt:0,positionDelta:0}}static deduceAndroidCompositionInput(d,g){if(!d)return{text:"",replacePrevCharCnt:0,replaceNextCharCnt:0,positionDelta:0};if(e._debugComposition&&(console.log("------------------------deduceAndroidCompositionInput"),console.log("PREVIOUS STATE: "+d.toString()),console.log("CURRENT STATE: "+g.toString())),d.value===g.value)return{text:"",replacePrevCharCnt:0,replaceNextCharCnt:0,positionDelta:g.selectionEnd-d.selectionEnd};const p=Math.min(b.commonPrefixLength(d.value,g.value),d.selectionEnd),c=Math.min(b.commonSuffixLength(d.value,g.value),d.value.length-d.selectionEnd),o=d.value.substring(p,d.value.length-c),s=g.value.substring(p,g.value.length-c),a=d.selectionStart-p,u=d.selectionEnd-p,r=g.selectionStart-p,i=g.selectionEnd-p;return e._debugComposition&&(console.log("AFTER DIFFING PREVIOUS STATE: <"+o+">, selectionStart: "+a+", selectionEnd: "+u),console.log("AFTER DIFFING CURRENT STATE: <"+s+">, selectionStart: "+r+", selectionEnd: "+i)),{text:s,replacePrevCharCnt:u,replaceNextCharCnt:o.length-u,positionDelta:i-s.length}}}e.TextAreaState=w,w.EMPTY=new w("",0,0,null,null);class S{static _getPageOfLine(d,g){return Math.floor((d-1)/g)}static _getRangeForPage(d,g){const p=d*g,c=p+1,o=p+g;return new M.Range(c,1,o+1,1)}static fromEditorSelection(d,g,p,c,o){const s=S._getPageOfLine(p.startLineNumber,c),a=S._getRangeForPage(s,c),u=S._getPageOfLine(p.endLineNumber,c),r=S._getRangeForPage(u,c),i=a.intersectRanges(new M.Range(1,1,p.startLineNumber,p.startColumn));let n=g.getValueInRange(i,1);const t=g.getLineCount(),l=g.getLineMaxColumn(t),h=r.intersectRanges(new M.Range(p.endLineNumber,p.endColumn,t,l));let m=g.getValueInRange(h,1),_;if(s===u||s+1===u)_=g.getValueInRange(p,1);else{const f=a.intersectRanges(p),v=r.intersectRanges(p);_=g.getValueInRange(f,1)+String.fromCharCode(8230)+g.getValueInRange(v,1)}if(o){const f=500;n.length>f&&(n=n.substring(n.length-f,n.length)),m.length>f&&(m=m.substring(0,f)),_.length>2*f&&(_=_.substring(0,f)+String.fromCharCode(8230)+_.substring(_.length-f,_.length))}return new w(n+_+m,n.length,n.length+_.length,new N.Position(p.startLineNumber,p.startColumn),new N.Position(p.endLineNumber,p.endColumn))}}e.PagedScreenReaderStrategy=S}),define(Q[215],J([0,1,195,6,2,40,3]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DiffNavigator=void 0;const C={followsCaret:!0,ignoreCharChanges:!0,alwaysRevealFirst:!0};class d extends M.Disposable{constructor(p,c={}){super();this._onDidUpdate=this._register(new N.Emitter),this._editor=p,this._options=w.mixin(c,C,!1),this.disposed=!1,this.nextIdx=-1,this.ranges=[],this.ignoreSelectionChange=!1,this.revealFirst=Boolean(this._options.alwaysRevealFirst),this._register(this._editor.onDidDispose(()=>this.dispose())),this._register(this._editor.onDidUpdateDiff(()=>this._onDiffUpdated())),this._options.followsCaret&&this._register(this._editor.getModifiedEditor().onDidChangeCursorPosition(o=>{this.ignoreSelectionChange||(this.nextIdx=-1)})),this._options.alwaysRevealFirst&&this._register(this._editor.getModifiedEditor().onDidChangeModel(o=>{this.revealFirst=!0})),this._init()}_init(){!!this._editor.getLineChanges()}_onDiffUpdated(){this._init(),this._compute(this._editor.getLineChanges()),this.revealFirst&&this._editor.getLineChanges()!==null&&(this.revealFirst=!1,this.nextIdx=-1,this.next(1))}_compute(p){this.ranges=[],p&&p.forEach(c=>{!this._options.ignoreCharChanges&&c.charChanges?c.charChanges.forEach(o=>{this.ranges.push({rhs:!0,range:new S.Range(o.modifiedStartLineNumber,o.modifiedStartColumn,o.modifiedEndLineNumber,o.modifiedEndColumn)})}):this.ranges.push({rhs:!0,range:new S.Range(c.modifiedStartLineNumber,1,c.modifiedStartLineNumber,1)})}),this.ranges.sort((c,o)=>c.range.getStartPosition().isBeforeOrEqual(o.range.getStartPosition())?-1:o.range.getStartPosition().isBeforeOrEqual(c.range.getStartPosition())?1:0),this._onDidUpdate.fire(this)}_initIdx(p){let c=!1,o=this._editor.getPosition();if(!o){this.nextIdx=0;return}for(let s=0,a=this.ranges.length;s=this.ranges.length&&(this.nextIdx=0)):(this.nextIdx-=1,this.nextIdx<0&&(this.nextIdx=this.ranges.length-1));let o=this.ranges[this.nextIdx];this.ignoreSelectionChange=!0;try{let s=o.range.getStartPosition();this._editor.setPosition(s),this._editor.revealPositionInCenter(s,c)}finally{this.ignoreSelectionChange=!1}}}canNavigate(){return this.ranges&&this.ranges.length>0}next(p=0){this._move(!0,p)}previous(p=0){this._move(!1,p)}dispose(){super.dispose(),this.ranges=[],this.disposed=!0}}e.DiffNavigator=d}),define(Q[62],J([0,1,3]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EditOperation=void 0;class N{static insert(w,S){return{range:new b.Range(w.lineNumber,w.column,w.lineNumber,w.column),text:S,forceMoveMarkers:!0}}static delete(w){return{range:w,text:null}}static replace(w,S){return{range:w,text:S}}static replaceMove(w,S){return{range:w,text:S,forceMoveMarkers:!0}}}e.EditOperation=N}),define(Q[366],J([0,1,8,62,3]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.trimTrailingWhitespace=e.TrimTrailingWhitespaceCommand=void 0;class w{constructor(d,g){this._selection=d,this._cursors=g,this._selectionId=null}getEditOperations(d,g){let p=S(d,this._cursors);for(let c=0,o=p.length;cs.lineNumber===a.lineNumber?s.column-a.column:s.lineNumber-a.lineNumber);for(let s=d.length-2;s>=0;s--)d[s].lineNumber===d[s+1].lineNumber&&d.splice(s,1);let g=[],p=0,c=0,o=d.length;for(let s=1,a=C.getLineCount();s<=a;s++){let u=C.getLineContent(s),r=u.length+1,i=0;if(!(c255?255:M|0}}e.RGBA8=b,b.Empty=new b(0,0,0,0)}),define(Q[21],J([0,1,14,3]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Selection=void 0;class M extends N.Range{constructor(S,C,d,g){super(S,C,d,g);this.selectionStartLineNumber=S,this.selectionStartColumn=C,this.positionLineNumber=d,this.positionColumn=g}toString(){return"["+this.selectionStartLineNumber+","+this.selectionStartColumn+" -> "+this.positionLineNumber+","+this.positionColumn+"]"}equalsSelection(S){return M.selectionsEqual(this,S)}static selectionsEqual(S,C){return S.selectionStartLineNumber===C.selectionStartLineNumber&&S.selectionStartColumn===C.selectionStartColumn&&S.positionLineNumber===C.positionLineNumber&&S.positionColumn===C.positionColumn}getDirection(){return this.selectionStartLineNumber===this.startLineNumber&&this.selectionStartColumn===this.startColumn?0:1}setEndPosition(S,C){return this.getDirection()===0?new M(this.startLineNumber,this.startColumn,S,C):new M(S,C,this.startLineNumber,this.startColumn)}getPosition(){return new b.Position(this.positionLineNumber,this.positionColumn)}setStartPosition(S,C){return this.getDirection()===0?new M(S,C,this.endLineNumber,this.endColumn):new M(this.endLineNumber,this.endColumn,S,C)}static fromPositions(S,C=S){return new M(S.lineNumber,S.column,C.lineNumber,C.column)}static liftSelection(S){return new M(S.selectionStartLineNumber,S.selectionStartColumn,S.positionLineNumber,S.positionColumn)}static selectionsArrEqual(S,C){if(S&&!C||!S&&C)return!1;if(!S&&!C)return!0;if(S.length!==C.length)return!1;for(let d=0,g=S.length;dthis._onCut.fire(),0)),this._asyncFocusGainWriteScreenReaderContent=this._register(new M.RunOnceScheduler(()=>this.writeScreenReaderContent("asyncFocusGain"),0)),this._textAreaState=g.TextAreaState.EMPTY,this._selectionChangeListener=null,this.writeScreenReaderContent("ctor"),this._hasFocus=!1,this._isDoingComposition=!1,this._nextCommand=0;let l=null;this._register(N.addStandardDisposableListener(t.domNode,"keydown",f=>{(f.keyCode===109||this._isDoingComposition&&f.keyCode===1)&&f.stopPropagation(),f.equals(9)&&f.preventDefault(),l=f,this._onKeyDown.fire(f)})),this._register(N.addStandardDisposableListener(t.domNode,"keyup",f=>{this._onKeyUp.fire(f)})),this._register(N.addDisposableListener(t.domNode,"compositionstart",f=>{if(g._debugComposition&&console.log("[compositionstart]",f),!this._isDoingComposition){if(this._isDoingComposition=!0,C.isMacintosh&&this._textAreaState.selectionStart===this._textAreaState.selectionEnd&&this._textAreaState.selectionStart>0&&this._textAreaState.value.substr(this._textAreaState.selectionStart-1,1)===f.data&&(l&&l.equals(109)&&(l.code==="ArrowRight"||l.code==="ArrowLeft")||b.isFirefox)){g._debugComposition&&console.log("[compositionstart] Handling long press case on macOS + arrow key or Firefox",f),this._textAreaState=new g.TextAreaState(this._textAreaState.value,this._textAreaState.selectionStart-1,this._textAreaState.selectionEnd,this._textAreaState.selectionStartPosition?new p.Position(this._textAreaState.selectionStartPosition.lineNumber,this._textAreaState.selectionStartPosition.column-1):null,this._textAreaState.selectionEndPosition),this._onCompositionStart.fire({revealDeltaColumns:-1});return}if(b.isAndroid){this._onCompositionStart.fire({revealDeltaColumns:-this._textAreaState.selectionStart});return}this._setAndWriteTextAreaState("compositionstart",g.TextAreaState.EMPTY),this._onCompositionStart.fire({revealDeltaColumns:0})}}));const h=f=>{const v=this._textAreaState,y=g.TextAreaState.readFromTextArea(this._textArea);return[y,g.TextAreaState.deduceInput(v,y,f)]},m=()=>{const f=this._textAreaState,v=g.TextAreaState.readFromTextArea(this._textArea);return[v,g.TextAreaState.deduceAndroidCompositionInput(f,v)]},_=f=>{const v=this._textAreaState,y=g.TextAreaState.selectedText(f),L={text:y.value,replacePrevCharCnt:v.selectionEnd-v.selectionStart,replaceNextCharCnt:0,positionDelta:0};return[y,L]};this._register(N.addDisposableListener(t.domNode,"compositionupdate",f=>{if(g._debugComposition&&console.log("[compositionupdate]",f),b.isAndroid){const[L,I]=m();this._textAreaState=L,this._onType.fire(I),this._onCompositionUpdate.fire(f);return}const[v,y]=_(f.data||"");this._textAreaState=v,this._onType.fire(y),this._onCompositionUpdate.fire(f)})),this._register(N.addDisposableListener(t.domNode,"compositionend",f=>{if(g._debugComposition&&console.log("[compositionend]",f),!!this._isDoingComposition){if(this._isDoingComposition=!1,b.isAndroid){const[L,I]=m();this._textAreaState=L,this._onType.fire(I),this._onCompositionEnd.fire();return}const[v,y]=_(f.data||"");this._textAreaState=v,this._onType.fire(y),(b.isChrome||b.isFirefox)&&(this._textAreaState=g.TextAreaState.readFromTextArea(this._textArea)),this._onCompositionEnd.fire()}})),this._register(N.addDisposableListener(t.domNode,"input",()=>{if(this._textArea.setIgnoreSelectionChangeTime("received input event"),!this._isDoingComposition){const[f,v]=h(C.isMacintosh);v.replacePrevCharCnt===0&&v.text.length===1&&d.isHighSurrogate(v.text.charCodeAt(0))||(this._textAreaState=f,this._nextCommand===0?(v.text!==""||v.replacePrevCharCnt!==0)&&this._onType.fire(v):((v.text!==""||v.replacePrevCharCnt!==0)&&this._firePaste(v.text,null),this._nextCommand=0))}})),this._register(N.addDisposableListener(t.domNode,"cut",f=>{this._textArea.setIgnoreSelectionChangeTime("received cut event"),this._ensureClipboardGetsEditorSelection(f),this._asyncTriggerCut.schedule()})),this._register(N.addDisposableListener(t.domNode,"copy",f=>{this._ensureClipboardGetsEditorSelection(f)})),this._register(N.addDisposableListener(t.domNode,"paste",f=>{if(this._textArea.setIgnoreSelectionChangeTime("received paste event"),u.canUseTextData(f)){const[v,y]=u.getTextData(f);v!==""&&this._firePaste(v,y)}else this._textArea.getSelectionStart()!==this._textArea.getSelectionEnd()&&this._setAndWriteTextAreaState("paste",g.TextAreaState.EMPTY),this._nextCommand=1})),this._register(N.addDisposableListener(t.domNode,"focus",()=>{const f=this._hasFocus;this._setHasFocus(!0),b.isSafari&&!f&&this._hasFocus&&this._asyncFocusGainWriteScreenReaderContent.schedule()})),this._register(N.addDisposableListener(t.domNode,"blur",()=>{this._isDoingComposition&&(this._isDoingComposition=!1,this.writeScreenReaderContent("blurWithoutCompositionEnd"),this._onCompositionEnd.fire()),this._setHasFocus(!1)})),this._register(N.addDisposableListener(t.domNode,o.Tap,()=>{b.isAndroid&&this._isDoingComposition&&(this._isDoingComposition=!1,this.writeScreenReaderContent("tapWithoutCompositionEnd"),this._onCompositionEnd.fire())}))}_installSelectionChangeListener(){let n=0;return N.addDisposableListener(document,"selectionchange",t=>{if(!!this._hasFocus&&!this._isDoingComposition&&!!b.isChrome){const l=Date.now(),h=l-n;if(n=l,!(h<5)){const m=l-this._textArea.getIgnoreSelectionChangeTime();if(this._textArea.resetSelectionChangeTime(),!(m<100)&&!(!this._textAreaState.selectionStartPosition||!this._textAreaState.selectionEndPosition)){const _=this._textArea.getValue();if(this._textAreaState.value===_){const f=this._textArea.getSelectionStart(),v=this._textArea.getSelectionEnd();if(!(this._textAreaState.selectionStart===f&&this._textAreaState.selectionEnd===v)){const y=this._textAreaState.deduceEditorPosition(f),L=this._host.deduceModelPosition(y[0],y[1],y[2]),I=this._textAreaState.deduceEditorPosition(v),k=this._host.deduceModelPosition(I[0],I[1],I[2]),E=new c.Selection(L.lineNumber,L.column,k.lineNumber,k.column);this._onSelectionChangeRequest.fire(E)}}}}}})}dispose(){super.dispose(),this._selectionChangeListener&&(this._selectionChangeListener.dispose(),this._selectionChangeListener=null)}focusTextArea(){this._setHasFocus(!0),this.refreshFocusState()}isFocused(){return this._hasFocus}refreshFocusState(){const n=N.getShadowRoot(this.textArea.domNode);n?this._setHasFocus(n.activeElement===this.textArea.domNode):N.isInDOM(this.textArea.domNode)?this._setHasFocus(document.activeElement===this.textArea.domNode):this._setHasFocus(!1)}_setHasFocus(n){this._hasFocus!==n&&(this._hasFocus=n,this._selectionChangeListener&&(this._selectionChangeListener.dispose(),this._selectionChangeListener=null),this._hasFocus&&(this._selectionChangeListener=this._installSelectionChangeListener()),this._hasFocus&&this.writeScreenReaderContent("focusgain"),this._hasFocus?this._onFocus.fire():this._onBlur.fire())}_setAndWriteTextAreaState(n,t){this._hasFocus||(t=t.collapseSelection()),t.writeToTextArea(n,this._textArea,this._hasFocus),this._textAreaState=t}writeScreenReaderContent(n){this._isDoingComposition||this._setAndWriteTextAreaState(n,this._host.getScreenReaderContent(this._textAreaState))}_ensureClipboardGetsEditorSelection(n){const t=this._host.getDataToCopy(u.canUseTextData(n)),l={version:1,isFromEmptySelection:t.isFromEmptySelection,multicursorText:t.multicursorText,mode:t.mode};if(s.INSTANCE.set(b.isFirefox?t.text.replace(/\r\n/g,` +`):t.text,l),!u.canUseTextData(n)){this._setAndWriteTextAreaState("copy or cut",g.TextAreaState.selectedText(t.text));return}u.setTextData(n,t.text,t.html,l)}_firePaste(n,t){t||(t=s.INSTANCE.get(n)),this._onPaste.fire({text:n,metadata:t})}}e.TextAreaInput=a;class u{static canUseTextData(n){return!!(n.clipboardData||window.clipboardData)}static getTextData(n){if(n.clipboardData){n.preventDefault();const t=n.clipboardData.getData("text/plain");let l=null;const h=n.clipboardData.getData("vscode-editor-data");if(typeof h=="string")try{l=JSON.parse(h),l.version!==1&&(l=null)}catch(m){}return[t,l]}if(window.clipboardData)return n.preventDefault(),[window.clipboardData.getData("Text"),null];throw new Error("ClipboardEventUtils.getTextData: Cannot use text data!")}static setTextData(n,t,l,h){if(n.clipboardData){n.clipboardData.setData("text/plain",t),typeof l=="string"&&n.clipboardData.setData("text/html",l),n.clipboardData.setData("vscode-editor-data",JSON.stringify(h)),n.preventDefault();return}if(window.clipboardData){window.clipboardData.setData("Text",t),n.preventDefault();return}throw new Error("ClipboardEventUtils.setTextData: Cannot use text data!")}}class r extends S.Disposable{constructor(n){super();this._actual=n,this._ignoreSelectionChangeTime=0}setIgnoreSelectionChangeTime(n){this._ignoreSelectionChangeTime=Date.now()}getIgnoreSelectionChangeTime(){return this._ignoreSelectionChangeTime}resetSelectionChangeTime(){this._ignoreSelectionChangeTime=0}getValue(){return this._actual.domNode.value}setValue(n,t){const l=this._actual.domNode;l.value!==t&&(this.setIgnoreSelectionChangeTime("setValue"),l.value=t)}getSelectionStart(){return this._actual.domNode.selectionDirection==="backward"?this._actual.domNode.selectionEnd:this._actual.domNode.selectionStart}getSelectionEnd(){return this._actual.domNode.selectionDirection==="backward"?this._actual.domNode.selectionStart:this._actual.domNode.selectionEnd}setSelectionRange(n,t,l){const h=this._actual.domNode;let m=null;const _=N.getShadowRoot(h);_?m=_.activeElement:m=document.activeElement;const f=m===h,v=h.selectionStart,y=h.selectionEnd;if(f&&v===t&&y===l){b.isFirefox&&window.parent!==window&&h.focus();return}if(f){this.setIgnoreSelectionChangeTime("setSelectionRange"),h.setSelectionRange(t,l),b.isFirefox&&window.parent!==window&&h.focus();return}try{const L=N.saveParentsScrollTop(h);this.setIgnoreSelectionChangeTime("setSelectionRange"),h.focus(),h.setSelectionRange(t,l),N.restoreParentsScrollTop(h,L)}catch(L){}}}}),define(Q[92],J([0,1,21]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ReplaceCommandThatPreservesSelection=e.ReplaceCommandWithOffsetCursorState=e.ReplaceCommandWithoutChangingPosition=e.ReplaceCommandThatSelectsText=e.ReplaceCommand=void 0;class N{constructor(g,p,c=!1){this._range=g,this._text=p,this.insertsAutoWhitespace=c}getEditOperations(g,p){p.addTrackedEditOperation(this._range,this._text)}computeCursorState(g,p){let o=p.getInverseEditOperations()[0].range;return new b.Selection(o.endLineNumber,o.endColumn,o.endLineNumber,o.endColumn)}}e.ReplaceCommand=N;class M{constructor(g,p){this._range=g,this._text=p}getEditOperations(g,p){p.addTrackedEditOperation(this._range,this._text)}computeCursorState(g,p){const o=p.getInverseEditOperations()[0].range;return new b.Selection(o.startLineNumber,o.startColumn,o.endLineNumber,o.endColumn)}}e.ReplaceCommandThatSelectsText=M;class w{constructor(g,p,c=!1){this._range=g,this._text=p,this.insertsAutoWhitespace=c}getEditOperations(g,p){p.addTrackedEditOperation(this._range,this._text)}computeCursorState(g,p){let o=p.getInverseEditOperations()[0].range;return new b.Selection(o.startLineNumber,o.startColumn,o.startLineNumber,o.startColumn)}}e.ReplaceCommandWithoutChangingPosition=w;class S{constructor(g,p,c,o,s=!1){this._range=g,this._text=p,this._columnDeltaOffset=o,this._lineNumberDeltaOffset=c,this.insertsAutoWhitespace=s}getEditOperations(g,p){p.addTrackedEditOperation(this._range,this._text)}computeCursorState(g,p){let o=p.getInverseEditOperations()[0].range;return new b.Selection(o.endLineNumber+this._lineNumberDeltaOffset,o.endColumn+this._columnDeltaOffset,o.endLineNumber+this._lineNumberDeltaOffset,o.endColumn+this._columnDeltaOffset)}}e.ReplaceCommandWithOffsetCursorState=S;class C{constructor(g,p,c,o=!1){this._range=g,this._text=p,this._initialSelection=c,this._forceMoveMarkers=o,this._selectionId=null}getEditOperations(g,p){p.addTrackedEditOperation(this._range,this._text,this._forceMoveMarkers),this._selectionId=p.trackSelection(this._initialSelection)}computeCursorState(g,p){return p.getTrackedSelection(this._selectionId)}}e.ReplaceCommandThatPreservesSelection=C}),define(Q[367],J([0,1,3,21]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SurroundSelectionCommand=void 0;class M{constructor(S,C,d){this._range=S,this._charBeforeSelection=C,this._charAfterSelection=d}getEditOperations(S,C){C.addTrackedEditOperation(new b.Range(this._range.startLineNumber,this._range.startColumn,this._range.startLineNumber,this._range.startColumn),this._charBeforeSelection),C.addTrackedEditOperation(new b.Range(this._range.endLineNumber,this._range.endColumn,this._range.endLineNumber,this._range.endColumn),this._charAfterSelection)}computeCursorState(S,C){let d=C.getInverseEditOperations(),g=d[0].range,p=d[1].range;return new N.Selection(g.endLineNumber,g.endColumn,p.endLineNumber,p.endColumn-this._charAfterSelection.length)}}e.SurroundSelectionCommand=M}),define(Q[93],J([0,1,8,17,101]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.decodeUTF16LE=e.createStringBuilder=e.hasTextDecoder=e.getPlatformTextDecoder=void 0;let w;function S(){return w||(w=new TextDecoder(N.isLittleEndian()?"UTF-16LE":"UTF-16BE")),w}e.getPlatformTextDecoder=S,e.hasTextDecoder=typeof TextDecoder!="undefined",e.hasTextDecoder?(e.createStringBuilder=c=>new g(c),e.decodeUTF16LE=C):(e.createStringBuilder=c=>new p,e.decodeUTF16LE=d);function C(c,o,s){const a=new Uint16Array(c.buffer,o,s);return S().decode(a)}function d(c,o,s){let a=[],u=0;for(let r=0;r=this._capacity){this._flushBuffer(),this._completedStrings[this._completedStrings.length]=o;return}for(let a=0;a=this._lines.length)throw new Error("Illegal value for lineNumber");return this._lines[p]}onLinesDeleted(g,p){if(this.getCount()===0)return null;const c=this.getStartLineNumber(),o=this.getEndLineNumber();if(po)return null;let s=0,a=0;for(let r=c;r<=o;r++){const i=r-this._rendLineNumberStart;g<=r&&r<=p&&(a===0?(s=i,a=1):a++)}if(g=c&&a<=o&&(this._lines[a-this._rendLineNumberStart].onContentChanged(),s=!0);return s}onLinesInserted(g,p){if(this.getCount()===0)return null;const c=p-g+1,o=this.getStartLineNumber(),s=this.getEndLineNumber();if(g<=o)return this._rendLineNumberStart+=c,null;if(g>s)return null;if(c+g>s)return this._lines.splice(g-this._rendLineNumberStart,s-g+1);const a=[];for(let t=0;tc)){const r=Math.max(p,u.fromLineNumber),i=Math.min(c,u.toLineNumber);for(let n=r;n<=i;n++){const t=n-this._rendLineNumberStart;this._lines[t].onTokensChanged(),o=!0}}}return o}}e.RenderedLinesCollection=w;class S{constructor(g){this._host=g,this.domNode=this._createDomNode(),this._linesCollection=new w(()=>this._host.createVisibleLine())}_createDomNode(){const g=b.createFastDomNode(document.createElement("div"));return g.setClassName("view-layer"),g.setPosition("absolute"),g.domNode.setAttribute("role","presentation"),g.domNode.setAttribute("aria-hidden","true"),g}onConfigurationChanged(g){return!!g.hasChanged(124)}onFlushed(g){return this._linesCollection.flush(),!0}onLinesChanged(g){return this._linesCollection.onLinesChanged(g.fromLineNumber,g.toLineNumber)}onLinesDeleted(g){const p=this._linesCollection.onLinesDeleted(g.fromLineNumber,g.toLineNumber);if(p)for(let c=0,o=p.length;cp){const a=p,u=Math.min(c,s.rendLineNumberStart-1);a<=u&&(this._insertLinesBefore(s,a,u,o,p),s.linesLength+=u-a+1)}else if(s.rendLineNumberStart0&&(this._removeLinesBefore(s,a),s.linesLength-=a)}if(s.rendLineNumberStart=p,s.rendLineNumberStart+s.linesLength-1c){const a=Math.max(0,c-s.rendLineNumberStart+1),r=s.linesLength-1-a+1;r>0&&(this._removeLinesAfter(s,r),s.linesLength-=r)}return this._finishRendering(s,!1,o),s}_renderUntouchedLines(g,p,c,o,s){const a=g.rendLineNumberStart,u=g.lines;for(let r=p;r<=c;r++){const i=a+r;u[r].layoutLine(i,o[i-s])}}_insertLinesBefore(g,p,c,o,s){const a=[];let u=0;for(let r=p;r<=c;r++)a[u++]=this.host.createVisibleLine();g.lines=a.concat(g.lines)}_removeLinesBefore(g,p){for(let c=0;c=0;u--){const r=g.lines[u];o[u]&&(r.setDomNode(a),a=a.previousSibling)}}_finishRenderingInvalidLines(g,p,c){const o=document.createElement("div");C._ttPolicy&&(p=C._ttPolicy.createHTML(p)),o.innerHTML=p;for(let s=0;sd}),C._sb=N.createStringBuilder(1e5)}),define(Q[127],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenizationResult2=e.TokenizationResult=e.Token=void 0;class b{constructor(S,C,d){this.offset=S|0,this.type=C,this.language=d}toString(){return"("+this.offset+", "+this.type+")"}}e.Token=b;class N{constructor(S,C){this.tokens=S,this.endState=C}}e.TokenizationResult=N;class M{constructor(S,C){this.tokens=S,this.endState=C}}e.TokenizationResult2=M}),define(Q[368],J([0,1,153,8]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DiffComputer=void 0;const M=3;function w(u,r,i,n){return new b.LcsDiff(u,r,i).ComputeDiff(n)}class S{constructor(r){const i=[],n=[];for(let t=0,l=r.length;t0&&i.originalLength<20&&i.modifiedLength>0&&i.modifiedLength<20&&l()){const I=n.createCharSequence(r,i.originalStart,i.originalStart+i.originalLength-1),k=t.createCharSequence(r,i.modifiedStart,i.modifiedStart+i.modifiedLength-1);let E=w(I,k,l,!0).changes;m&&(E=g(E)),L=[];for(let T=0,O=E.length;T1&&E>1;){const T=L.charCodeAt(k-2),O=I.charCodeAt(E-2);if(T!==O)break;k--,E--}(k>1||E>1)&&this._pushTrimWhitespaceCharChange(t,l+1,1,k,h+1,1,E)}{let k=s(L,1),E=s(I,1);const T=L.length+1,O=I.length+1;for(;k!0;const r=Date.now();return()=>Date.now()-r0&&o>0)&&!(s>0&&a>0)){let u=Math.abs(o-a),r=Math.abs(c-s);if(u===0){g.spacesDiff=r,r>0&&0<=s-1&&s-10?g++:v>1&&p++,N(c,o,h,f,r),!(r.looksLikeAlignment&&!(C&&S===r.spacesDiff)))){let L=r.spacesDiff;L<=a&&u[L]++,c=h,o=f}}let i=C;g!==p&&(i=g{let h=u[l];h>t&&(t=h,n=l)}),n===4&&u[4]>0&&u[2]>0&&u[2]>=u[4]/2&&(n=2)}return{insertSpaces:i,tabSize:n}}e.guessIndentation=M}),define(Q[370],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.intervalCompare=e.recomputeMaxEnd=e.nodeAcceptEdit=e.IntervalTree=e.SENTINEL=e.IntervalNode=e.getNodeIsInOverviewRuler=e.getNodeColor=void 0;function b(D){return(D.metadata&1)>>>0}e.getNodeColor=b;function N(D,R){D.metadata=D.metadata&254|R<<0}function M(D){return(D.metadata&2)>>>1==1}function w(D,R){D.metadata=D.metadata&253|(R?1:0)<<1}function S(D){return(D.metadata&4)>>>2==1}function C(D,R){D.metadata=D.metadata&251|(R?1:0)<<2}function d(D){return(D.metadata&8)>>>3==1}e.getNodeIsInOverviewRuler=d;function g(D,R){D.metadata=D.metadata&247|(R?1:0)<<3}function p(D){return(D.metadata&48)>>>4}function c(D,R){D.metadata=D.metadata&207|R<<4}function o(D){return(D.metadata&64)>>>6==1}function s(D,R){D.metadata=D.metadata&191|(R?1:0)<<6}class a{constructor(R,W,x){this.metadata=0,this.parent=this,this.left=this,this.right=this,N(this,1),this.start=W,this.end=x,this.delta=0,this.maxEnd=x,this.id=R,this.ownerId=0,this.options=null,C(this,!1),c(this,1),g(this,!1),s(this,!1),this.cachedVersionId=0,this.cachedAbsoluteStart=W,this.cachedAbsoluteEnd=x,this.range=null,w(this,!1)}reset(R,W,x,K){this.start=W,this.end=x,this.maxEnd=x,this.cachedVersionId=R,this.cachedAbsoluteStart=W,this.cachedAbsoluteEnd=x,this.range=K}setOptions(R){this.options=R;let W=this.options.className;C(this,W==="squiggly-error"||W==="squiggly-warning"||W==="squiggly-info"),c(this,this.options.stickiness),g(this,!!(this.options.overviewRuler&&this.options.overviewRuler.color)),s(this,this.options.collapseOnReplaceEdit)}setCachedOffsets(R,W,x){this.cachedVersionId!==x&&(this.range=null),this.cachedVersionId=x,this.cachedAbsoluteStart=R,this.cachedAbsoluteEnd=W}detach(){this.parent=null,this.left=null,this.right=null}}e.IntervalNode=a,e.SENTINEL=new a(null,0,0),e.SENTINEL.parent=e.SENTINEL,e.SENTINEL.left=e.SENTINEL,e.SENTINEL.right=e.SENTINEL,N(e.SENTINEL,0);class u{constructor(){this.root=e.SENTINEL,this.requestNormalizeDelta=!1}intervalSearch(R,W,x,K,Y){return this.root===e.SENTINEL?[]:f(this,R,W,x,K,Y)}search(R,W,x){return this.root===e.SENTINEL?[]:_(this,R,W,x)}collectNodesFromOwner(R){return h(this,R)}collectNodesPostOrder(){return m(this)}insert(R){v(this,R),this._normalizeDeltaIfNecessary()}delete(R){L(this,R),this._normalizeDeltaIfNecessary()}resolveNode(R,W){const x=R;let K=0;for(;R!==this.root;)R===R.parent.right&&(K+=R.parent.delta),R=R.parent;const Y=x.start+K,ee=x.end+K;x.setCachedOffsets(Y,ee,W)}acceptReplace(R,W,x,K){const Y=t(this,R,R+W);for(let ee=0,se=Y.length;eeW||x===1?!1:x===2?!0:R}function n(D,R,W,x,K){const Y=p(D),ee=Y===0||Y===2,se=Y===1||Y===2,ne=W-R,le=x,X=Math.min(ne,le),z=D.start;let P=!1;const V=D.end;let U=!1;R<=z&&V<=W&&o(D)&&(D.start=R,P=!0,D.end=R,U=!0);{const $=K?1:ne>0?2:0;!P&&i(z,ee,R,$)&&(P=!0),!U&&i(V,se,R,$)&&(U=!0)}if(X>0&&!K){const $=ne>le?2:0;!P&&i(z,ee,R+X,$)&&(P=!0),!U&&i(V,se,R+X,$)&&(U=!0)}{const $=K?1:0;!P&&i(z,ee,W,$)&&(D.start=R+le,P=!0),!U&&i(V,se,W,$)&&(D.end=R+le,U=!0)}const H=le-ne;P||(D.start=Math.max(0,z+H)),U||(D.end=Math.max(0,V+H)),D.start>D.end&&(D.end=D.start)}e.nodeAcceptEdit=n;function t(D,R,W){let x=D.root,K=0,Y=0,ee=0,se=0,ne=[],le=0;for(;x!==e.SENTINEL;){if(M(x)){w(x.left,!1),w(x.right,!1),x===x.parent.right&&(K-=x.parent.delta),x=x.parent;continue}if(!M(x.left)){if(Y=K+x.maxEnd,YW){w(x,!0);continue}if(se=K+x.end,se>=R&&(x.setCachedOffsets(ee,se,0),ne[le++]=x),w(x,!0),x.right!==e.SENTINEL&&!M(x.right)){K+=x.delta,x=x.right;continue}}return w(D.root,!1),ne}function l(D,R,W,x){let K=D.root,Y=0,ee=0,se=0;const ne=x-(W-R);for(;K!==e.SENTINEL;){if(M(K)){w(K.left,!1),w(K.right,!1),K===K.parent.right&&(Y-=K.parent.delta),A(K),K=K.parent;continue}if(!M(K.left)){if(ee=Y+K.maxEnd,eeW){K.start+=ne,K.end+=ne,K.delta+=ne,(K.delta<-1073741824||K.delta>1073741824)&&(D.requestNormalizeDelta=!0),w(K,!0);continue}if(w(K,!0),K.right!==e.SENTINEL&&!M(K.right)){Y+=K.delta,K=K.right;continue}}w(D.root,!1)}function h(D,R){let W=D.root,x=[],K=0;for(;W!==e.SENTINEL;){if(M(W)){w(W.left,!1),w(W.right,!1),W=W.parent;continue}if(W.left!==e.SENTINEL&&!M(W.left)){W=W.left;continue}if(W.ownerId===R&&(x[K++]=W),w(W,!0),W.right!==e.SENTINEL&&!M(W.right)){W=W.right;continue}}return w(D.root,!1),x}function m(D){let R=D.root,W=[],x=0;for(;R!==e.SENTINEL;){if(M(R)){w(R.left,!1),w(R.right,!1),R=R.parent;continue}if(R.left!==e.SENTINEL&&!M(R.left)){R=R.left;continue}if(R.right!==e.SENTINEL&&!M(R.right)){R=R.right;continue}W[x++]=R,w(R,!0)}return w(D.root,!1),W}function _(D,R,W,x){let K=D.root,Y=0,ee=0,se=0,ne=[],le=0;for(;K!==e.SENTINEL;){if(M(K)){w(K.left,!1),w(K.right,!1),K===K.parent.right&&(Y-=K.parent.delta),K=K.parent;continue}if(K.left!==e.SENTINEL&&!M(K.left)){K=K.left;continue}ee=Y+K.start,se=Y+K.end,K.setCachedOffsets(ee,se,x);let X=!0;if(R&&K.ownerId&&K.ownerId!==R&&(X=!1),W&&S(K)&&(X=!1),X&&(ne[le++]=K),w(K,!0),K.right!==e.SENTINEL&&!M(K.right)){Y+=K.delta,K=K.right;continue}}return w(D.root,!1),ne}function f(D,R,W,x,K,Y){let ee=D.root,se=0,ne=0,le=0,X=0,z=[],P=0;for(;ee!==e.SENTINEL;){if(M(ee)){w(ee.left,!1),w(ee.right,!1),ee===ee.parent.right&&(se-=ee.parent.delta),ee=ee.parent;continue}if(!M(ee.left)){if(ne=se+ee.maxEnd,neW){w(ee,!0);continue}if(X=se+ee.end,X>=R){ee.setCachedOffsets(le,X,Y);let V=!0;x&&ee.ownerId&&ee.ownerId!==x&&(V=!1),K&&S(ee)&&(V=!1),V&&(z[P++]=ee)}if(w(ee,!0),ee.right!==e.SENTINEL&&!M(ee.right)){se+=ee.delta,ee=ee.right;continue}}return w(D.root,!1),z}function v(D,R){if(D.root===e.SENTINEL)return R.parent=e.SENTINEL,R.left=e.SENTINEL,R.right=e.SENTINEL,N(R,0),D.root=R,D.root;y(D,R),B(R.parent);let W=R;for(;W!==D.root&&b(W.parent)===1;)if(W.parent===W.parent.parent.left){const x=W.parent.parent.right;b(x)===1?(N(W.parent,0),N(x,0),N(W.parent.parent,1),W=W.parent.parent):(W===W.parent.right&&(W=W.parent,E(D,W)),N(W.parent,0),N(W.parent.parent,1),T(D,W.parent.parent))}else{const x=W.parent.parent.left;b(x)===1?(N(W.parent,0),N(x,0),N(W.parent.parent,1),W=W.parent.parent):(W===W.parent.left&&(W=W.parent,T(D,W)),N(W.parent,0),N(W.parent.parent,1),E(D,W.parent.parent))}return N(D.root,0),R}function y(D,R){let W=0,x=D.root;const K=R.start,Y=R.end;for(;;)if(F(K,Y,x.start+W,x.end+W)<0)if(x.left===e.SENTINEL){R.start-=W,R.end-=W,R.maxEnd-=W,x.left=R;break}else x=x.left;else if(x.right===e.SENTINEL){R.start-=W+x.delta,R.end-=W+x.delta,R.maxEnd-=W+x.delta,x.right=R;break}else W+=x.delta,x=x.right;R.parent=x,R.left=e.SENTINEL,R.right=e.SENTINEL,N(R,1)}function L(D,R){let W,x;if(R.left===e.SENTINEL?(W=R.right,x=R,W.delta+=R.delta,(W.delta<-1073741824||W.delta>1073741824)&&(D.requestNormalizeDelta=!0),W.start+=R.delta,W.end+=R.delta):R.right===e.SENTINEL?(W=R.left,x=R):(x=I(R.right),W=x.right,W.start+=x.delta,W.end+=x.delta,W.delta+=x.delta,(W.delta<-1073741824||W.delta>1073741824)&&(D.requestNormalizeDelta=!0),x.start+=R.delta,x.end+=R.delta,x.delta=R.delta,(x.delta<-1073741824||x.delta>1073741824)&&(D.requestNormalizeDelta=!0)),x===D.root){D.root=W,N(W,0),R.detach(),k(),A(W),D.root.parent=e.SENTINEL;return}let K=b(x)===1;if(x===x.parent.left?x.parent.left=W:x.parent.right=W,x===R?W.parent=x.parent:(x.parent===R?W.parent=x:W.parent=x.parent,x.left=R.left,x.right=R.right,x.parent=R.parent,N(x,b(R)),R===D.root?D.root=x:R===R.parent.left?R.parent.left=x:R.parent.right=x,x.left!==e.SENTINEL&&(x.left.parent=x),x.right!==e.SENTINEL&&(x.right.parent=x)),R.detach(),K){B(W.parent),x!==R&&(B(x),B(x.parent)),k();return}B(W),B(W.parent),x!==R&&(B(x),B(x.parent));let Y;for(;W!==D.root&&b(W)===0;)W===W.parent.left?(Y=W.parent.right,b(Y)===1&&(N(Y,0),N(W.parent,1),E(D,W.parent),Y=W.parent.right),b(Y.left)===0&&b(Y.right)===0?(N(Y,1),W=W.parent):(b(Y.right)===0&&(N(Y.left,0),N(Y,1),T(D,Y),Y=W.parent.right),N(Y,b(W.parent)),N(W.parent,0),N(Y.right,0),E(D,W.parent),W=D.root)):(Y=W.parent.left,b(Y)===1&&(N(Y,0),N(W.parent,1),T(D,W.parent),Y=W.parent.left),b(Y.left)===0&&b(Y.right)===0?(N(Y,1),W=W.parent):(b(Y.left)===0&&(N(Y.right,0),N(Y,1),E(D,Y),Y=W.parent.left),N(Y,b(W.parent)),N(W.parent,0),N(Y.left,0),T(D,W.parent),W=D.root));N(W,0),k()}function I(D){for(;D.left!==e.SENTINEL;)D=D.left;return D}function k(){e.SENTINEL.parent=e.SENTINEL,e.SENTINEL.delta=0,e.SENTINEL.start=0,e.SENTINEL.end=0}function E(D,R){const W=R.right;W.delta+=R.delta,(W.delta<-1073741824||W.delta>1073741824)&&(D.requestNormalizeDelta=!0),W.start+=R.delta,W.end+=R.delta,R.right=W.left,W.left!==e.SENTINEL&&(W.left.parent=R),W.parent=R.parent,R.parent===e.SENTINEL?D.root=W:R===R.parent.left?R.parent.left=W:R.parent.right=W,W.left=R,R.parent=W,A(R),A(W)}function T(D,R){const W=R.left;R.delta-=W.delta,(R.delta<-1073741824||R.delta>1073741824)&&(D.requestNormalizeDelta=!0),R.start-=W.delta,R.end-=W.delta,R.left=W.right,W.right!==e.SENTINEL&&(W.right.parent=R),W.parent=R.parent,R.parent===e.SENTINEL?D.root=W:R===R.parent.right?R.parent.right=W:R.parent.left=W,W.right=R,R.parent=W,A(R),A(W)}function O(D){let R=D.end;if(D.left!==e.SENTINEL){const W=D.left.maxEnd;W>R&&(R=W)}if(D.right!==e.SENTINEL){const W=D.right.maxEnd+D.delta;W>R&&(R=W)}return R}function A(D){D.maxEnd=O(D)}e.recomputeMaxEnd=A;function B(D){for(;D!==e.SENTINEL;){const R=O(D);if(D.maxEnd===R)return;D.maxEnd=R,D=D.parent}}function F(D,R,W,x){return D===W?R-x:D-W}e.intervalCompare=F}),define(Q[371],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.recomputeTreeMetadata=e.updateTreeMetadata=e.fixInsert=e.rbDelete=e.rightRotate=e.leftRotate=e.resetSentinel=e.calculateLF=e.calculateSize=e.righttest=e.leftest=e.SENTINEL=e.TreeNode=void 0;class b{constructor(u,r){this.piece=u,this.color=r,this.size_left=0,this.lf_left=0,this.parent=this,this.left=this,this.right=this}next(){if(this.right!==e.SENTINEL)return N(this.right);let u=this;for(;u.parent!==e.SENTINEL&&u.parent.left!==u;)u=u.parent;return u.parent===e.SENTINEL?e.SENTINEL:u.parent}prev(){if(this.left!==e.SENTINEL)return M(this.left);let u=this;for(;u.parent!==e.SENTINEL&&u.parent.right!==u;)u=u.parent;return u.parent===e.SENTINEL?e.SENTINEL:u.parent}detach(){this.parent=null,this.left=null,this.right=null}}e.TreeNode=b,e.SENTINEL=new b(null,0),e.SENTINEL.parent=e.SENTINEL,e.SENTINEL.left=e.SENTINEL,e.SENTINEL.right=e.SENTINEL,e.SENTINEL.color=0;function N(a){for(;a.left!==e.SENTINEL;)a=a.left;return a}e.leftest=N;function M(a){for(;a.right!==e.SENTINEL;)a=a.right;return a}e.righttest=M;function w(a){return a===e.SENTINEL?0:a.size_left+a.piece.length+w(a.right)}e.calculateSize=w;function S(a){return a===e.SENTINEL?0:a.lf_left+a.piece.lineFeedCnt+S(a.right)}e.calculateLF=S;function C(){e.SENTINEL.parent=e.SENTINEL}e.resetSentinel=C;function d(a,u){let r=u.right;r.size_left+=u.size_left+(u.piece?u.piece.length:0),r.lf_left+=u.lf_left+(u.piece?u.piece.lineFeedCnt:0),u.right=r.left,r.left!==e.SENTINEL&&(r.left.parent=u),r.parent=u.parent,u.parent===e.SENTINEL?a.root=r:u.parent.left===u?u.parent.left=r:u.parent.right=r,r.left=u,u.parent=r}e.leftRotate=d;function g(a,u){let r=u.left;u.left=r.right,r.right!==e.SENTINEL&&(r.right.parent=u),r.parent=u.parent,u.size_left-=r.size_left+(r.piece?r.piece.length:0),u.lf_left-=r.lf_left+(r.piece?r.piece.lineFeedCnt:0),u.parent===e.SENTINEL?a.root=r:u===u.parent.right?u.parent.right=r:u.parent.left=r,r.right=u,u.parent=r}e.rightRotate=g;function p(a,u){let r,i;if(u.left===e.SENTINEL?(i=u,r=i.right):u.right===e.SENTINEL?(i=u,r=i.left):(i=N(u.right),r=i.right),i===a.root){a.root=r,r.color=0,u.detach(),C(),a.root.parent=e.SENTINEL;return}let n=i.color===1;if(i===i.parent.left?i.parent.left=r:i.parent.right=r,i===u?(r.parent=i.parent,s(a,r)):(i.parent===u?r.parent=i:r.parent=i.parent,s(a,r),i.left=u.left,i.right=u.right,i.parent=u.parent,i.color=u.color,u===a.root?a.root=i:u===u.parent.left?u.parent.left=i:u.parent.right=i,i.left!==e.SENTINEL&&(i.left.parent=i),i.right!==e.SENTINEL&&(i.right.parent=i),i.size_left=u.size_left,i.lf_left=u.lf_left,s(a,i)),u.detach(),r.parent.left===r){let l=w(r),h=S(r);if(l!==r.parent.size_left||h!==r.parent.lf_left){let m=l-r.parent.size_left,_=h-r.parent.lf_left;r.parent.size_left=l,r.parent.lf_left=h,o(a,r.parent,m,_)}}if(s(a,r.parent),n){C();return}let t;for(;r!==a.root&&r.color===0;)r===r.parent.left?(t=r.parent.right,t.color===1&&(t.color=0,r.parent.color=1,d(a,r.parent),t=r.parent.right),t.left.color===0&&t.right.color===0?(t.color=1,r=r.parent):(t.right.color===0&&(t.left.color=0,t.color=1,g(a,t),t=r.parent.right),t.color=r.parent.color,r.parent.color=0,t.right.color=0,d(a,r.parent),r=a.root)):(t=r.parent.left,t.color===1&&(t.color=0,r.parent.color=1,g(a,r.parent),t=r.parent.left),t.left.color===0&&t.right.color===0?(t.color=1,r=r.parent):(t.left.color===0&&(t.right.color=0,t.color=1,d(a,t),t=r.parent.left),t.color=r.parent.color,r.parent.color=0,t.left.color=0,g(a,r.parent),r=a.root));r.color=0,C()}e.rbDelete=p;function c(a,u){for(s(a,u);u!==a.root&&u.parent.color===1;)if(u.parent===u.parent.parent.left){const r=u.parent.parent.right;r.color===1?(u.parent.color=0,r.color=0,u.parent.parent.color=1,u=u.parent.parent):(u===u.parent.right&&(u=u.parent,d(a,u)),u.parent.color=0,u.parent.parent.color=1,g(a,u.parent.parent))}else{const r=u.parent.parent.left;r.color===1?(u.parent.color=0,r.color=0,u.parent.parent.color=1,u=u.parent.parent):(u===u.parent.left&&(u=u.parent,g(a,u)),u.parent.color=0,u.parent.parent.color=1,d(a,u.parent.parent))}a.root.color=0}e.fixInsert=c;function o(a,u,r,i){for(;u!==a.root&&u!==e.SENTINEL;)u.parent.left===u&&(u.parent.size_left+=r,u.parent.lf_left+=i),u=u.parent}e.updateTreeMetadata=o;function s(a,u){let r=0,i=0;if(u!==a.root){if(r===0){for(;u!==a.root&&u===u.parent.right;)u=u.parent;if(u===a.root)return;u=u.parent,r=w(u.left)-u.size_left,i=S(u.left)-u.lf_left,u.size_left+=r,u.lf_left+=i}for(;u!==a.root&&(r!==0||i!==0);)u.parent.left===u&&(u.parent.size_left+=r,u.parent.lf_left+=i),u=u.parent}}e.recomputeTreeMetadata=s}),define(Q[218],J([0,1,101,93]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.compressConsecutiveTextChanges=e.TextChange=void 0;function M(d){return d.replace(/\n/g,"\\n").replace(/\r/g,"\\r")}class w{constructor(g,p,c,o){this.oldPosition=g,this.oldText=p,this.newPosition=c,this.newText=o}get oldLength(){return this.oldText.length}get oldEnd(){return this.oldPosition+this.oldText.length}get newLength(){return this.newText.length}get newEnd(){return this.newPosition+this.newText.length}toString(){return this.oldText.length===0?`(insert@${this.oldPosition} "${M(this.newText)}")`:this.newText.length===0?`(delete@${this.oldPosition} "${M(this.oldText)}")`:`(replace@${this.oldPosition} "${M(this.oldText)}" with "${M(this.newText)}")`}static _writeStringSize(g){return 4+2*g.length}static _writeString(g,p,c){const o=p.length;b.writeUInt32BE(g,o,c),c+=4;for(let s=0;s=0;let l=null;try{l=b.createRegExp(this.searchString,this.isRegex,{matchCase:this.matchCase,wholeWord:!1,multiline:t,global:!0,unicode:!0})}catch(m){return null}if(!l)return null;let h=!this.isRegex&&!t;return h&&this.searchString.toLowerCase()!==this.searchString.toUpperCase()&&(h=this.matchCase),new p(l,this.wordSeparators?N.getMapForWordSeparators(this.wordSeparators):null,h?this.searchString:null)}}e.SearchParams=d;function g(n){if(!n||n.length===0)return!1;for(let t=0,l=n.length;t=l)break;const m=n.charCodeAt(t);if(m===110||m===114||m===87||m===119)return!0}return!1}e.isMultilineRegexSource=g;class p{constructor(t,l,h){this.regex=t,this.wordSeparators=l,this.simpleSearch=h}}e.SearchData=p;function c(n,t,l){if(!l)return new S.FindMatch(n,null);let h=[];for(let m=0,_=t.length;m<_;m++)h[m]=t[m];return new S.FindMatch(n,h)}e.createFindMatch=c;class o{constructor(t){let l=[],h=0;for(let m=0,_=t.length;m<_;m++)t.charCodeAt(m)===10&&(l[h++]=m);this._lineFeedsOffsets=l}findLineFeedCountBeforeOffset(t){const l=this._lineFeedsOffsets;let h=0,m=l.length-1;if(m===-1||t<=l[0])return 0;for(;h>0);l[_]>=t?m=_-1:l[_+1]>=t?(h=_,m=_):h=_+1}return h+1}}class s{static findMatches(t,l,h,m,_){const f=l.parseSearchRequest();return f?f.regex.multiline?this._doFindMatchesMultiline(t,h,new i(f.wordSeparators,f.regex),m,_):this._doFindMatchesLineByLine(t,h,f,m,_):[]}static _getMultilineMatchRange(t,l,h,m,_,f){let v,y=0;m?(y=m.findLineFeedCountBeforeOffset(_),v=l+_+y):v=l+_;let L;if(m){let T=m.findLineFeedCountBeforeOffset(_+f.length)-y;L=v+f.length+T}else L=v+f.length;const I=t.getPositionAt(v),k=t.getPositionAt(L);return new w.Range(I.lineNumber,I.column,k.lineNumber,k.column)}static _doFindMatchesMultiline(t,l,h,m,_){const f=t.getOffsetAt(l.getStartPosition()),v=t.getValueInRange(l,1),y=t.getEOL()===`\r +`?new o(v):null,L=[];let I=0,k;for(h.reset(0);k=h.next(v);)if(L[I++]=c(this._getMultilineMatchRange(t,f,v,y,k.index,k[0]),k,m),I>=_)return L;return L}static _doFindMatchesLineByLine(t,l,h,m,_){const f=[];let v=0;if(l.startLineNumber===l.endLineNumber){const L=t.getLineContent(l.startLineNumber).substring(l.startColumn-1,l.endColumn-1);return v=this._findMatchesInLine(h,L,l.startLineNumber,l.startColumn-1,v,f,m,_),f}const y=t.getLineContent(l.startLineNumber).substring(l.startColumn-1);v=this._findMatchesInLine(h,y,l.startLineNumber,l.startColumn-1,v,f,m,_);for(let L=l.startLineNumber+1;L=y))return _;return _}const I=new i(t.wordSeparators,t.regex);let k;I.reset(0);do if(k=I.next(l),k&&(f[_++]=c(new w.Range(h,k.index+1+m,h,k.index+1+k[0].length+m),k,v),_>=y))return _;while(k);return _}static findNextMatch(t,l,h,m){const _=l.parseSearchRequest();if(!_)return null;const f=new i(_.wordSeparators,_.regex);return _.regex.multiline?this._doFindNextMatchMultiline(t,h,f,m):this._doFindNextMatchLineByLine(t,h,f,m)}static _doFindNextMatchMultiline(t,l,h,m){const _=new M.Position(l.lineNumber,1),f=t.getOffsetAt(_),v=t.getLineCount(),y=t.getValueInRange(new w.Range(_.lineNumber,_.column,v,t.getLineMaxColumn(v)),1),L=t.getEOL()===`\r +`?new o(y):null;h.reset(l.column-1);let I=h.next(y);return I?c(this._getMultilineMatchRange(t,f,y,L,I.index,I[0]),I,m):l.lineNumber!==1||l.column!==1?this._doFindNextMatchMultiline(t,new M.Position(1,1),h,m):null}static _doFindNextMatchLineByLine(t,l,h,m){const _=t.getLineCount(),f=l.lineNumber,v=t.getLineContent(f),y=this._findFirstMatchInLine(h,v,f,l.column,m);if(y)return y;for(let L=1;L<=_;L++){const I=(f+L-1)%_,k=t.getLineContent(I+1),E=this._findFirstMatchInLine(h,k,I+1,1,m);if(E)return E}return null}static _findFirstMatchInLine(t,l,h,m,_){t.reset(m-1);const f=t.next(l);return f?c(new w.Range(h,f.index+1,h,f.index+1+f[0].length),f,_):null}static findPreviousMatch(t,l,h,m){const _=l.parseSearchRequest();if(!_)return null;const f=new i(_.wordSeparators,_.regex);return _.regex.multiline?this._doFindPreviousMatchMultiline(t,h,f,m):this._doFindPreviousMatchLineByLine(t,h,f,m)}static _doFindPreviousMatchMultiline(t,l,h,m){const _=this._doFindMatchesMultiline(t,new w.Range(1,1,l.lineNumber,l.column),h,m,10*C);if(_.length>0)return _[_.length-1];const f=t.getLineCount();return l.lineNumber!==f||l.column!==t.getLineMaxColumn(f)?this._doFindPreviousMatchMultiline(t,new M.Position(f,t.getLineMaxColumn(f)),h,m):null}static _doFindPreviousMatchLineByLine(t,l,h,m){const _=t.getLineCount(),f=l.lineNumber,v=t.getLineContent(f).substring(0,l.column-1),y=this._findLastMatchInLine(h,v,f,m);if(y)return y;for(let L=1;L<=_;L++){const I=(_+f-L-1)%_,k=t.getLineContent(I+1),E=this._findLastMatchInLine(h,k,I+1,m);if(E)return E}return null}static _findLastMatchInLine(t,l,h,m){let _=null,f;for(t.reset(0);f=t.next(l);)_=c(new w.Range(h,f.index+1,h,f.index+1+f[0].length),f,m);return _}}e.TextModelSearch=s;function a(n,t,l,h,m){if(h===0)return!0;const _=t.charCodeAt(h-1);if(n.get(_)!==0||_===13||_===10)return!0;if(m>0){const f=t.charCodeAt(h);if(n.get(f)!==0)return!0}return!1}function u(n,t,l,h,m){if(h+m===l)return!0;const _=t.charCodeAt(h+m);if(n.get(_)!==0||_===13||_===10)return!0;if(m>0){const f=t.charCodeAt(h+m-1);if(n.get(f)!==0)return!0}return!1}function r(n,t,l,h,m){return a(n,t,l,h,m)&&u(n,t,l,h,m)}e.isValidMatch=r;class i{constructor(t,l){this._wordSeparators=t,this._searchRegex=l,this._prevMatchStartIndex=-1,this._prevMatchLength=0}reset(t){this._searchRegex.lastIndex=t,this._prevMatchStartIndex=-1,this._prevMatchLength=0}next(t){const l=t.length;let h;do{if(this._prevMatchStartIndex+this._prevMatchLength===l||(h=this._searchRegex.exec(t),!h))return null;const m=h.index,_=h[0].length;if(m===this._prevMatchStartIndex&&_===this._prevMatchLength){if(_===0){b.getNextCodePoint(t,l,this._searchRegex.lastIndex)>65535?this._searchRegex.lastIndex+=2:this._searchRegex.lastIndex+=1;continue}return null}if(this._prevMatchStartIndex=m,this._prevMatchLength=_,!this._wordSeparators||r(this._wordSeparators,t,l,m,_))return h}while(h);return null}}e.Searcher=i}),define(Q[219],J([0,1,14,3,53,371,166]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PieceTreeBase=e.StringBuffer=e.Piece=e.createLineStarts=e.createLineStartsFast=e.LineStarts=e.createUintArray=e.AverageBufferSize=void 0,e.AverageBufferSize=65535;function C(r){let i;return r[r.length-1]<65536?i=new Uint16Array(r.length):i=new Uint32Array(r.length),i.set(r,0),i}e.createUintArray=C;class d{constructor(i,n,t,l,h){this.lineStarts=i,this.cr=n,this.lf=t,this.crlf=l,this.isBasicASCII=h}}e.LineStarts=d;function g(r,i=!0){let n=[0],t=1;for(let l=0,h=r.length;l126)&&(m=!1)}const _=new d(C(r),t,l,h,m);return r.length=0,_}e.createLineStarts=p;class c{constructor(i,n,t,l,h){this.bufferIndex=i,this.start=n,this.end=t,this.lineFeedCnt=l,this.length=h}}e.Piece=c;class o{constructor(i,n){this.buffer=i,this.lineStarts=n}}e.StringBuffer=o;class s{constructor(i,n){this._pieces=[],this._tree=i,this._BOM=n,this._index=0,i.root!==w.SENTINEL&&i.iterate(i.root,t=>(t!==w.SENTINEL&&this._pieces.push(t.piece),!0))}read(){return this._pieces.length===0?this._index===0?(this._index++,this._BOM):null:this._index>this._pieces.length-1?null:this._index===0?this._BOM+this._tree.getPieceContent(this._pieces[this._index++]):this._tree.getPieceContent(this._pieces[this._index++])}}class a{constructor(i){this._limit=i,this._cache=[]}get(i){for(let n=this._cache.length-1;n>=0;n--){let t=this._cache[n];if(t.nodeStartOffset<=i&&t.nodeStartOffset+t.node.piece.length>=i)return t}return null}get2(i){for(let n=this._cache.length-1;n>=0;n--){let t=this._cache[n];if(t.nodeStartLineNumber&&t.nodeStartLineNumber=i)return t}return null}set(i){this._cache.length>=this._limit&&this._cache.shift(),this._cache.push(i)}validate(i){let n=!1,t=this._cache;for(let l=0;l=i){t[l]=null,n=!0;continue}}if(n){let l=[];for(const h of t)h!==null&&l.push(h);this._cache=l}}}class u{constructor(i,n,t){this.create(i,n,t)}create(i,n,t){this._buffers=[new o("",[0])],this._lastChangeBufferPos={line:0,column:0},this.root=w.SENTINEL,this._lineCnt=1,this._length=0,this._EOL=n,this._EOLLength=n.length,this._EOLNormalized=t;let l=null;for(let h=0,m=i.length;h0){i[h].lineStarts||(i[h].lineStarts=g(i[h].buffer));let _=new c(h+1,{line:0,column:0},{line:i[h].lineStarts.length-1,column:i[h].buffer.length-i[h].lineStarts[i[h].lineStarts.length-1]},i[h].lineStarts.length-1,i[h].buffer.length);this._buffers.push(i[h]),l=this.rbInsertRight(l,_)}this._searchCache=new a(1),this._lastVisitedLine={lineNumber:0,value:""},this.computeBufferMetadata()}normalizeEOL(i){let n=e.AverageBufferSize,t=n-Math.floor(n/3),l=t*2,h="",m=0,_=[];if(this.iterate(this.root,f=>{let v=this.getNodeContent(f),y=v.length;if(m<=t||m+y0){let f=h.replace(/\r\n|\r|\n/g,i);_.push(new o(f,g(f)))}this.create(_,i,!0)}getEOL(){return this._EOL}setEOL(i){this._EOL=i,this._EOLLength=this._EOL.length,this.normalizeEOL(i)}createSnapshot(i){return new s(this,i)}getOffsetAt(i,n){let t=0,l=this.root;for(;l!==w.SENTINEL;)if(l.left!==w.SENTINEL&&l.lf_left+1>=i)l=l.left;else{if(l.lf_left+l.piece.lineFeedCnt+1>=i)return t+=l.size_left,t+=this.getAccumulatedValue(l,i-l.lf_left-2)+n-1;i-=l.lf_left+l.piece.lineFeedCnt,t+=l.size_left+l.piece.length,l=l.right}return t}getPositionAt(i){i=Math.floor(i),i=Math.max(0,i);let n=this.root,t=0,l=i;for(;n!==w.SENTINEL;)if(n.size_left!==0&&n.size_left>=i)n=n.left;else if(n.size_left+n.piece.length>=i){let h=this.getIndexOf(n,i-n.size_left);if(t+=n.lf_left+h.index,h.index===0){let m=this.getOffsetAt(t+1,1),_=l-m;return new b.Position(t+1,_+1)}return new b.Position(t+1,h.remainder+1)}else if(i-=n.size_left+n.piece.length,t+=n.lf_left+n.piece.lineFeedCnt,n.right===w.SENTINEL){let h=this.getOffsetAt(t+1,1),m=l-i-h;return new b.Position(t+1,m+1)}else n=n.right;return new b.Position(1,1)}getValueInRange(i,n){if(i.startLineNumber===i.endLineNumber&&i.startColumn===i.endColumn)return"";let t=this.nodeAt2(i.startLineNumber,i.startColumn),l=this.nodeAt2(i.endLineNumber,i.endColumn),h=this.getValueInRange2(t,l);return n?n!==this._EOL||!this._EOLNormalized?h.replace(/\r\n|\r|\n/g,n):n===this.getEOL()&&this._EOLNormalized?h:h.replace(/\r\n|\r|\n/g,n):h}getValueInRange2(i,n){if(i.node===n.node){let _=i.node,f=this._buffers[_.piece.bufferIndex].buffer,v=this.offsetInBuffer(_.piece.bufferIndex,_.piece.start);return f.substring(v+i.remainder,v+n.remainder)}let t=i.node,l=this._buffers[t.piece.bufferIndex].buffer,h=this.offsetInBuffer(t.piece.bufferIndex,t.piece.start),m=l.substring(h+i.remainder,h+t.piece.length);for(t=t.next();t!==w.SENTINEL;){let _=this._buffers[t.piece.bufferIndex].buffer,f=this.offsetInBuffer(t.piece.bufferIndex,t.piece.start);if(t===n.node){m+=_.substring(f,f+n.remainder);break}else m+=_.substr(f,t.piece.length);t=t.next()}return m}getLinesContent(){let i=[],n=0,t="",l=!1;return this.iterate(this.root,h=>{if(h===w.SENTINEL)return!0;const m=h.piece;let _=m.length;if(_===0)return!0;const f=this._buffers[m.bufferIndex].buffer,v=this._buffers[m.bufferIndex].lineStarts,y=m.start.line,L=m.end.line;let I=v[y]+m.start.column;if(l&&(f.charCodeAt(I)===10&&(I++,_--),i[n++]=t,t="",l=!1,_===0))return!0;if(y===L)return!this._EOLNormalized&&f.charCodeAt(I+_-1)===13?(l=!0,t+=f.substr(I,_-1)):t+=f.substr(I,_),!0;t+=this._EOLNormalized?f.substring(I,Math.max(I,v[y+1]-this._EOLLength)):f.substring(I,v[y+1]).replace(/(\r\n|\r|\n)$/,""),i[n++]=t;for(let k=y+1;kD+E,n.reset(0)):(B=I.buffer,F=D=>D,n.reset(E));do if(O=n.next(B),O){if(F(O.index)>=T)return y;this.positionInBuffer(i,F(O.index)-k,A);let D=this.getLineFeedCnt(i.piece.bufferIndex,h,A),R=A.line===h.line?A.column-h.column+l:A.column+1,W=R+O[0].length;if(L[y++]=S.createFindMatch(new N.Range(t+D,R,t+D,W),O,f),F(O.index)+O[0].length>=T||y>=v)return y}while(O);return y}findMatchesLineByLine(i,n,t,l){const h=[];let m=0;const _=new S.Searcher(n.wordSeparators,n.regex);let f=this.nodeAt2(i.startLineNumber,i.startColumn);if(f===null)return[];let v=this.nodeAt2(i.endLineNumber,i.endColumn);if(v===null)return[];let y=this.positionInBuffer(f.node,f.remainder),L=this.positionInBuffer(v.node,v.remainder);if(f.node===v.node)return this.findMatchesInNode(f.node,_,i.startLineNumber,i.startColumn,y,L,n,t,l,m,h),h;let I=i.startLineNumber,k=f.node;for(;k!==v.node;){let T=this.getLineFeedCnt(k.piece.bufferIndex,y,k.piece.end);if(T>=1){let A=this._buffers[k.piece.bufferIndex].lineStarts,B=this.offsetInBuffer(k.piece.bufferIndex,k.piece.start),F=A[y.line+T],D=I===i.startLineNumber?i.startColumn:1;if(m=this.findMatchesInNode(k,_,I,D,y,this.positionInBuffer(k,F-B),n,t,l,m,h),m>=l)return h;I+=T}let O=I===i.startLineNumber?i.startColumn-1:0;if(I===i.endLineNumber){const A=this.getLineContent(I).substring(O,i.endColumn-1);return m=this._findMatchesInLine(n,_,A,i.endLineNumber,O,m,h,t,l),h}if(m=this._findMatchesInLine(n,_,this.getLineContent(I).substr(O),I,O,m,h,t,l),m>=l)return h;I++,f=this.nodeAt2(I,1),k=f.node,y=this.positionInBuffer(f.node,f.remainder)}if(I===i.endLineNumber){let T=I===i.startLineNumber?i.startColumn-1:0;const O=this.getLineContent(I).substring(T,i.endColumn-1);return m=this._findMatchesInLine(n,_,O,i.endLineNumber,T,m,h,t,l),h}let E=I===i.startLineNumber?i.startColumn:1;return m=this.findMatchesInNode(v.node,_,I,E,y,L,n,t,l,m,h),h}_findMatchesInLine(i,n,t,l,h,m,_,f,v){const y=i.wordSeparators;if(!f&&i.simpleSearch){const I=i.simpleSearch,k=I.length,E=t.length;let T=-k;for(;(T=t.indexOf(I,T+k))!==-1;)if((!y||S.isValidMatch(y,t,E,T,k))&&(_[m++]=new M.FindMatch(new N.Range(l,T+1+h,l,T+1+k+h),null),m>=v))return m;return m}let L;n.reset(0);do if(L=n.next(t),L&&(_[m++]=S.createFindMatch(new N.Range(l,L.index+1+h,l,L.index+1+L[0].length+h),L,f),m>=v))return m;while(L);return m}insert(i,n,t=!1){if(this._EOLNormalized=this._EOLNormalized&&t,this._lastVisitedLine.lineNumber=0,this._lastVisitedLine.value="",this.root!==w.SENTINEL){let{node:l,remainder:h,nodeStartOffset:m}=this.nodeAt(i),_=l.piece,f=_.bufferIndex,v=this.positionInBuffer(l,h);if(l.piece.bufferIndex===0&&_.end.line===this._lastChangeBufferPos.line&&_.end.column===this._lastChangeBufferPos.column&&m+_.length===i&&n.lengthi){let y=[],L=new c(_.bufferIndex,v,_.end,this.getLineFeedCnt(_.bufferIndex,v,_.end),this.offsetInBuffer(f,_.end)-this.offsetInBuffer(f,v));if(this.shouldCheckCRLF()&&this.endWithCR(n)&&this.nodeCharCodeAt(l,h)===10){let T={line:L.start.line+1,column:0};L=new c(L.bufferIndex,T,L.end,this.getLineFeedCnt(L.bufferIndex,T,L.end),L.length-1),n+=` +`}if(this.shouldCheckCRLF()&&this.startWithLF(n))if(this.nodeCharCodeAt(l,h-1)===13){let T=this.positionInBuffer(l,h-1);this.deleteNodeTail(l,T),n="\r"+n,l.piece.length===0&&y.push(l)}else this.deleteNodeTail(l,v);else this.deleteNodeTail(l,v);let I=this.createNewPieces(n);L.length>0&&this.rbInsertRight(l,L);let k=l;for(let E=0;E=0;m--)h=this.rbInsertLeft(h,l[m]);this.validateCRLFWithPrevNode(h),this.deleteNodes(t)}insertContentToNodeRight(i,n){this.adjustCarriageReturnFromNext(i,n)&&(i+=` +`);let t=this.createNewPieces(i),l=this.rbInsertRight(n,t[0]),h=l;for(let m=1;m=I)v=L+1;else break;return t?(t.line=L,t.column=f-k,null):{line:L,column:f-k}}getLineFeedCnt(i,n,t){if(t.column===0)return t.line-n.line;let l=this._buffers[i].lineStarts;if(t.line===l.length-1)return t.line-n.line;let h=l[t.line+1],m=l[t.line]+t.column;if(h>m+1)return t.line-n.line;let _=m-1;return this._buffers[i].buffer.charCodeAt(_)===13?t.line-n.line+1:t.line-n.line}offsetInBuffer(i,n){return this._buffers[i].lineStarts[n.line]+n.column}deleteNodes(i){for(let n=0;ne.AverageBufferSize){let y=[];for(;i.length>e.AverageBufferSize;){const I=i.charCodeAt(e.AverageBufferSize-1);let k;I===13||I>=55296&&I<=56319?(k=i.substring(0,e.AverageBufferSize-1),i=i.substring(e.AverageBufferSize-1)):(k=i.substring(0,e.AverageBufferSize),i=i.substring(e.AverageBufferSize));let E=g(k);y.push(new c(this._buffers.length,{line:0,column:0},{line:E.length-1,column:k.length-E[E.length-1]},E.length-1,k.length)),this._buffers.push(new o(k,E))}let L=g(i);return y.push(new c(this._buffers.length,{line:0,column:0},{line:L.length-1,column:i.length-L[L.length-1]},L.length-1,i.length)),this._buffers.push(new o(i,L)),y}let n=this._buffers[0].buffer.length;const t=g(i,!1);let l=this._lastChangeBufferPos;if(this._buffers[0].lineStarts[this._buffers[0].lineStarts.length-1]===n&&n!==0&&this.startWithLF(i)&&this.endWithCR(this._buffers[0].buffer)){this._lastChangeBufferPos={line:this._lastChangeBufferPos.line,column:this._lastChangeBufferPos.column+1},l=this._lastChangeBufferPos;for(let y=0;y=i-1)t=t.left;else if(t.lf_left+t.piece.lineFeedCnt>i-1){let f=this.getAccumulatedValue(t,i-t.lf_left-2),v=this.getAccumulatedValue(t,i-t.lf_left-1),y=this._buffers[t.piece.bufferIndex].buffer,L=this.offsetInBuffer(t.piece.bufferIndex,t.piece.start);return m+=t.size_left,this._searchCache.set({node:t,nodeStartOffset:m,nodeStartLineNumber:_-(i-1-t.lf_left)}),y.substring(L+f,L+v-n)}else if(t.lf_left+t.piece.lineFeedCnt===i-1){let f=this.getAccumulatedValue(t,i-t.lf_left-2),v=this._buffers[t.piece.bufferIndex].buffer,y=this.offsetInBuffer(t.piece.bufferIndex,t.piece.start);l=v.substring(y+f,y+t.piece.length);break}else i-=t.lf_left+t.piece.lineFeedCnt,m+=t.size_left+t.piece.length,t=t.right}for(t=t.next();t!==w.SENTINEL;){let m=this._buffers[t.piece.bufferIndex].buffer;if(t.piece.lineFeedCnt>0){let _=this.getAccumulatedValue(t,0),f=this.offsetInBuffer(t.piece.bufferIndex,t.piece.start);return l+=m.substring(f,f+_-n),l}else{let _=this.offsetInBuffer(t.piece.bufferIndex,t.piece.start);l+=m.substr(_,t.piece.length)}t=t.next()}return l}computeBufferMetadata(){let i=this.root,n=1,t=0;for(;i!==w.SENTINEL;)n+=i.lf_left+i.piece.lineFeedCnt,t+=i.size_left+i.piece.length,i=i.right;this._lineCnt=n,this._length=t,this._searchCache.validate(this._length)}getIndexOf(i,n){let t=i.piece,l=this.positionInBuffer(i,n),h=l.line-t.start.line;if(this.offsetInBuffer(t.bufferIndex,t.end)-this.offsetInBuffer(t.bufferIndex,t.start)===n){let m=this.getLineFeedCnt(i.piece.bufferIndex,t.start,l);if(m!==h)return{index:m,remainder:0}}return{index:h,remainder:l.column}}getAccumulatedValue(i,n){if(n<0)return 0;let t=i.piece,l=this._buffers[t.bufferIndex].lineStarts,h=t.start.line+n+1;return h>t.end.line?l[t.end.line]+t.end.column-l[t.start.line]-t.start.column:l[h]-l[t.start.line]-t.start.column}deleteNodeTail(i,n){const t=i.piece,l=t.lineFeedCnt,h=this.offsetInBuffer(t.bufferIndex,t.end),m=n,_=this.offsetInBuffer(t.bufferIndex,m),f=this.getLineFeedCnt(t.bufferIndex,t.start,m),v=f-l,y=_-h,L=t.length+y;i.piece=new c(t.bufferIndex,t.start,m,f,L),w.updateTreeMetadata(this,i,y,v)}deleteNodeHead(i,n){const t=i.piece,l=t.lineFeedCnt,h=this.offsetInBuffer(t.bufferIndex,t.start),m=n,_=this.getLineFeedCnt(t.bufferIndex,m,t.end),f=this.offsetInBuffer(t.bufferIndex,m),v=_-l,y=h-f,L=t.length+y;i.piece=new c(t.bufferIndex,m,t.end,_,L),w.updateTreeMetadata(this,i,y,v)}shrinkNode(i,n,t){const l=i.piece,h=l.start,m=l.end,_=l.length,f=l.lineFeedCnt,v=n,y=this.getLineFeedCnt(l.bufferIndex,l.start,v),L=this.offsetInBuffer(l.bufferIndex,n)-this.offsetInBuffer(l.bufferIndex,h);i.piece=new c(l.bufferIndex,l.start,v,y,L),w.updateTreeMetadata(this,i,L-_,y-f);let I=new c(l.bufferIndex,t,m,this.getLineFeedCnt(l.bufferIndex,t,m),this.offsetInBuffer(l.bufferIndex,m)-this.offsetInBuffer(l.bufferIndex,t)),k=this.rbInsertRight(i,I);this.validateCRLFWithPrevNode(k)}appendToNode(i,n){this.adjustCarriageReturnFromNext(n,i)&&(n+=` +`);const t=this.shouldCheckCRLF()&&this.startWithLF(n)&&this.endWithCR(i),l=this._buffers[0].buffer.length;this._buffers[0].buffer+=n;const h=g(n,!1);for(let k=0;ki)n=n.left;else if(n.size_left+n.piece.length>=i){l+=n.size_left;let h={node:n,remainder:i-n.size_left,nodeStartOffset:l};return this._searchCache.set(h),h}else i-=n.size_left+n.piece.length,l+=n.size_left+n.piece.length,n=n.right;return null}nodeAt2(i,n){let t=this.root,l=0;for(;t!==w.SENTINEL;)if(t.left!==w.SENTINEL&&t.lf_left>=i-1)t=t.left;else if(t.lf_left+t.piece.lineFeedCnt>i-1){let h=this.getAccumulatedValue(t,i-t.lf_left-2),m=this.getAccumulatedValue(t,i-t.lf_left-1);return l+=t.size_left,{node:t,remainder:Math.min(h+n-1,m),nodeStartOffset:l}}else if(t.lf_left+t.piece.lineFeedCnt===i-1){let h=this.getAccumulatedValue(t,i-t.lf_left-2);if(h+n-1<=t.piece.length)return{node:t,remainder:h+n-1,nodeStartOffset:l};n-=t.piece.length-h;break}else i-=t.lf_left+t.piece.lineFeedCnt,l+=t.size_left+t.piece.length,t=t.right;for(t=t.next();t!==w.SENTINEL;){if(t.piece.lineFeedCnt>0){let h=this.getAccumulatedValue(t,0),m=this.offsetOfNode(t);return{node:t,remainder:Math.min(n-1,h),nodeStartOffset:m}}else if(t.piece.length>=n-1){let h=this.offsetOfNode(t);return{node:t,remainder:n-1,nodeStartOffset:h}}else n-=t.piece.length;t=t.next()}return null}nodeCharCodeAt(i,n){if(i.piece.lineFeedCnt<1)return-1;let t=this._buffers[i.piece.bufferIndex],l=this.offsetInBuffer(i.piece.bufferIndex,i.piece.start)+n;return t.buffer.charCodeAt(l)}offsetOfNode(i){if(!i)return 0;let n=i.size_left;for(;i!==this.root;)i.parent.right===i&&(n+=i.parent.size_left+i.parent.piece.length),i=i.parent;return n}shouldCheckCRLF(){return!(this._EOLNormalized&&this._EOL===` +`)}startWithLF(i){if(typeof i=="string")return i.charCodeAt(0)===10;if(i===w.SENTINEL||i.piece.lineFeedCnt===0)return!1;let n=i.piece,t=this._buffers[n.bufferIndex].lineStarts,l=n.start.line,h=t[l]+n.start.column;return l===t.length-1||t[l+1]>h+1?!1:this._buffers[n.bufferIndex].buffer.charCodeAt(h)===10}endWithCR(i){return typeof i=="string"?i.charCodeAt(i.length-1)===13:i===w.SENTINEL||i.piece.lineFeedCnt===0?!1:this.nodeCharCodeAt(i,i.piece.length-1)===13}validateCRLFWithPrevNode(i){if(this.shouldCheckCRLF()&&this.startWithLF(i)){let n=i.prev();this.endWithCR(n)&&this.fixCRLF(n,i)}}validateCRLFWithNextNode(i){if(this.shouldCheckCRLF()&&this.endWithCR(i)){let n=i.next();this.startWithLF(n)&&this.fixCRLF(i,n)}}fixCRLF(i,n){let t=[],l=this._buffers[i.piece.bufferIndex].lineStarts,h;i.piece.end.column===0?h={line:i.piece.end.line-1,column:l[i.piece.end.line]-l[i.piece.end.line-1]-1}:h={line:i.piece.end.line,column:i.piece.end.column-1};const m=i.piece.length-1,_=i.piece.lineFeedCnt-1;i.piece=new c(i.piece.bufferIndex,i.piece.start,h,_,m),w.updateTreeMetadata(this,i,-1,-1),i.piece.length===0&&t.push(i);let f={line:n.piece.start.line+1,column:0};const v=n.piece.length-1,y=this.getLineFeedCnt(n.piece.bufferIndex,f,n.piece.end);n.piece=new c(n.piece.bufferIndex,f,n.piece.end,y,v),w.updateTreeMetadata(this,n,-1,-1),n.piece.length===0&&t.push(n);let L=this.createNewPieces(`\r +`);this.rbInsertRight(i,L[0]);for(let I=0;I/?";function b(C=""){let d="(-?\\d*\\.\\d\\w*)|([^";for(const g of e.USUAL_WORD_SEPARATORS)C.indexOf(g)>=0||(d+="\\"+g);return d+="\\s]+)",new RegExp(d,"g")}e.DEFAULT_WORD_REGEXP=b();function N(C){let d=e.DEFAULT_WORD_REGEXP;if(C&&C instanceof RegExp)if(C.global)d=C;else{let g="g";C.ignoreCase&&(g+="i"),C.multiline&&(g+="m"),C.unicode&&(g+="u"),d=new RegExp(C.source,g)}return d.lastIndex=0,d}e.ensureValidWordDefinition=N;const M={maxLen:1e3,windowSize:15,timeBudget:150};function w(C,d,g,p,c=M){if(g.length>c.maxLen){let r=C-c.maxLen/2;return r<0?r=0:p+=r,g=g.substring(r,C+c.maxLen/2),w(C,d,g,p,c)}const o=Date.now(),s=C-1-p;let a=-1,u=null;for(let r=1;!(Date.now()-o>=c.timeBudget);r++){const i=s-c.windowSize*r;d.lastIndex=Math.max(0,i);const n=S(d,g,s,a);if(!n&&u||(u=n,i<=0))break;a=i}if(u){let r={word:u[0],startColumn:p+1+u.index,endColumn:p+1+u.index+u[0].length};return d.lastIndex=0,r}return null}e.getWordAtText=w;function S(C,d,g,p){let c;for(;c=C.exec(d);){const o=c.index||0;if(o<=g&&C.lastIndex>=g)return c;if(p>0&&o>p)return null}return null}}),define(Q[373],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FrankensteinMode=void 0;class b{constructor(M){this._languageIdentifier=M}getId(){return this._languageIdentifier.language}}e.FrankensteinMode=b}),define(Q[109],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AutoClosingPairs=e.StandardAutoClosingPairConditional=e.IndentAction=void 0;var b;(function(S){S[S.None=0]="None",S[S.Indent=1]="Indent",S[S.IndentOutdent=2]="IndentOutdent",S[S.Outdent=3]="Outdent"})(b=e.IndentAction||(e.IndentAction={}));class N{constructor(C){if(this.open=C.open,this.close=C.close,this._standardTokenMask=0,Array.isArray(C.notIn))for(let d=0,g=C.notIn.length;dg&&(g=c)}return g}else{if(typeof w=="string")return d?w==="*"?5:w===C?10:0:0;if(w){const{language:g,pattern:p,scheme:c,hasAccessToAllModels:o}=w;if(!d&&!o)return 0;let s=0;if(c)if(c===S.scheme)s=10;else if(c==="*")s=5;else return 0;if(g)if(g===C)s=10;else if(g==="*")s=Math.max(s,5);else return 0;if(p){let a;if(typeof p=="string"?a=p:a=Object.assign(Object.assign({},p),{base:N.normalize(p.base)}),a===S.fsPath||b.match(a,S.fsPath))s=10;else return 0}return s}else return 0}}e.score=M}),define(Q[375],J([0,1,91]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.computeLinks=e.LinkComputer=e.StateMachine=e.Uint8Matrix=void 0;class N{constructor(o,s,a){const u=new Uint8Array(o*s);for(let r=0,i=o*s;rs&&(s=t),n>a&&(a=n),l>a&&(a=l)}s++,a++;let u=new N(a,s,0);for(let r=0,i=o.length;r=this._maxCharCode?0:this._states.get(o,s)}}e.StateMachine=M;let w=null;function S(){return w===null&&(w=new M([[1,104,2],[1,72,2],[1,102,6],[1,70,6],[2,116,3],[2,84,3],[3,116,4],[3,84,4],[4,112,5],[4,80,5],[5,115,9],[5,83,9],[5,58,10],[6,105,7],[6,73,7],[7,108,8],[7,76,8],[8,101,9],[8,69,9],[9,58,10],[10,47,11],[11,47,12]])),w}let C=null;function d(){if(C===null){C=new b.CharacterClassifier(0);const c=` <>'"\u3001\u3002\uFF61\uFF64\uFF0C\uFF0E\uFF1A\uFF1B\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\uFF08\uFF3B\uFF5B\uFF62\uFF63\uFF5D\uFF3D\uFF09\u3015\u3011\u300F\u300D\u300B\u3009\u201D\u2019\uFF40\uFF5E\u2026`;for(let s=0;su);if(u>0){const n=s.charCodeAt(u-1),t=s.charCodeAt(i);(n===40&&t===41||n===91&&t===93||n===123&&t===125)&&i--}return{range:{startLineNumber:a,startColumn:u+1,endLineNumber:a,endColumn:i+2},url:s.substring(u,i+1)}}static computeLinks(o,s=S()){const a=d();let u=[];for(let r=1,i=o.getLineCount();r<=i;r++){const n=o.getLineContent(r),t=n.length;let l=0,h=0,m=0,_=1,f=!1,v=!1,y=!1,L=!1;for(;l0&&w.getLanguageId(c-1)===g;)c--;return new N(w,g,c,p+1,w.getStartOffset(c),w.getEndOffset(p))}e.createScopedLineTokens=b;class N{constructor(S,C,d,g,p,c){this._actual=S,this.languageId=C,this._firstTokenIndex=d,this._lastTokenIndex=g,this.firstCharOffset=p,this._lastCharOffset=c}getLineContent(){return this._actual.getLineContent().substring(this.firstCharOffset,this._lastCharOffset)}getActualLineContentBefore(S){return this._actual.getLineContent().substring(0,this.firstCharOffset+S)}getTokenCount(){return this._lastTokenIndex-this._firstTokenIndex}findTokenIndexAtOffset(S){return this._actual.findTokenIndexAtOffset(S+this.firstCharOffset)-this._firstTokenIndex}getStandardTokenType(S){return this._actual.getStandardTokenType(S+this._firstTokenIndex)}}e.ScopedLineTokens=N;function M(w){return(w&7)!=0}e.ignoreBracketsInToken=M}),define(Q[376],J([0,1,109]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CharacterPairSupport=void 0;class N{constructor(w){if(w.autoClosingPairs?this._autoClosingPairs=w.autoClosingPairs.map(S=>new b.StandardAutoClosingPairConditional(S)):w.brackets?this._autoClosingPairs=w.brackets.map(S=>new b.StandardAutoClosingPairConditional({open:S[0],close:S[1]})):this._autoClosingPairs=[],w.__electricCharacterSupport&&w.__electricCharacterSupport.docComment){const S=w.__electricCharacterSupport.docComment;this._autoClosingPairs.push(new b.StandardAutoClosingPairConditional({open:S.open,close:S.close||""}))}this._autoCloseBefore=typeof w.autoCloseBefore=="string"?w.autoCloseBefore:N.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED,this._surroundingPairs=w.surroundingPairs||this._autoClosingPairs}getAutoClosingPairs(){return this._autoClosingPairs}getAutoCloseBeforeSet(){return this._autoCloseBefore}static shouldAutoClosePair(w,S,C){if(S.getTokenCount()===0)return!0;const d=S.findTokenIndexAtOffset(C-2),g=S.getStandardTokenType(d);return w.isOK(g)}getSurroundingPairs(){return this._surroundingPairs}}e.CharacterPairSupport=N,N.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED=`;:.,=}])> + `}),define(Q[377],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IndentRulesSupport=void 0;function b(M){return M.global&&(M.lastIndex=0),!0}class N{constructor(w){this._indentationRules=w}shouldIncrease(w){return!!(this._indentationRules&&this._indentationRules.increaseIndentPattern&&b(this._indentationRules.increaseIndentPattern)&&this._indentationRules.increaseIndentPattern.test(w))}shouldDecrease(w){return!!(this._indentationRules&&this._indentationRules.decreaseIndentPattern&&b(this._indentationRules.decreaseIndentPattern)&&this._indentationRules.decreaseIndentPattern.test(w))}shouldIndentNextLine(w){return!!(this._indentationRules&&this._indentationRules.indentNextLinePattern&&b(this._indentationRules.indentNextLinePattern)&&this._indentationRules.indentNextLinePattern.test(w))}shouldIgnore(w){return!!(this._indentationRules&&this._indentationRules.unIndentedLinePattern&&b(this._indentationRules.unIndentedLinePattern)&&this._indentationRules.unIndentedLinePattern.test(w))}getIndentMetadata(w){let S=0;return this.shouldIncrease(w)&&(S+=1),this.shouldDecrease(w)&&(S+=2),this.shouldIndentNextLine(w)&&(S+=4),this.shouldIgnore(w)&&(S+=8),S}}e.IndentRulesSupport=N}),define(Q[378],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.BasicInplaceReplace=void 0;class b{constructor(){this._defaultValueSet=[["true","false"],["True","False"],["Private","Public","Friend","ReadOnly","Partial","Protected","WriteOnly"],["public","protected","private"]]}navigateValueSet(M,w,S,C,d){if(M&&w){let g=this.doNavigateValueSet(w,d);if(g)return{range:M,value:g}}if(S&&C){let g=this.doNavigateValueSet(C,d);if(g)return{range:S,value:g}}return null}doNavigateValueSet(M,w){let S=this.numberReplace(M,w);return S!==null?S:this.textReplace(M,w)}numberReplace(M,w){let S=Math.pow(10,M.length-(M.lastIndexOf(".")+1)),C=Number(M),d=parseFloat(M);return!isNaN(C)&&!isNaN(d)&&C===d?C===0&&!w?null:(C=Math.floor(C*S),C+=w?S:-S,String(C/S)):null}textReplace(M,w){return this.valueSetsReplace(this._defaultValueSet,M,w)}valueSetsReplace(M,w,S){let C=null;for(let d=0,g=M.length;C===null&&d=0?(C+=S?1:-1,C<0?C=M.length-1:C%=M.length,M[C]):null}}e.BasicInplaceReplace=b,b.INSTANCE=new b}),define(Q[379],J([0,1,12,8,109]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.OnEnterSupport=void 0;class w{constructor(C){C=C||{},C.brackets=C.brackets||[["(",")"],["{","}"],["[","]"]],this._brackets=[],C.brackets.forEach(d=>{const g=w._createOpenBracketRegExp(d[0]),p=w._createCloseBracketRegExp(d[1]);g&&p&&this._brackets.push({open:d[0],openRegExp:g,close:d[1],closeRegExp:p})}),this._regExpRules=C.onEnterRules||[]}onEnter(C,d,g,p){if(C>=3)for(let c=0,o=this._regExpRules.length;cu.reg?u.reg.test(u.text):!0))return s.action}if(C>=2&&g.length>0&&p.length>0)for(let c=0,o=this._brackets.length;c=2&&g.length>0){for(let c=0,o=this._brackets.length;c[v[0].toLowerCase(),v[1].toLowerCase()]);const h=[];for(let v=0;v{const[L,I]=v,[k,E]=y;return L===k||L===E||I===k||I===E},_=(v,y)=>{const L=Math.min(v,y),I=Math.max(v,y);for(let k=0;k0&&f.push({open:y,close:L})}return f}class C{constructor(l,h){const m=S(h);this.brackets=m.map((_,f)=>new w(l,f,_.open,_.close,c(_.open,_.close,m,f),o(_.open,_.close,m,f))),this.forwardRegex=s(this.brackets),this.reversedRegex=a(this.brackets),this.textIsBracket={},this.textIsOpenBracket={},this.maxBracketLength=0;for(const _ of this.brackets){for(const f of _.open)this.textIsBracket[f]=_,this.textIsOpenBracket[f]=!0,this.maxBracketLength=Math.max(this.maxBracketLength,f.length);for(const f of _.close)this.textIsBracket[f]=_,this.textIsOpenBracket[f]=!1,this.maxBracketLength=Math.max(this.maxBracketLength,f.length)}}}e.RichEditBrackets=C;function d(t,l,h,m){for(let _=0,f=l.length;_=0&&m.push(y);for(const y of v.close)y.indexOf(t)>=0&&m.push(y)}}function g(t,l){return t.length-l.length}function p(t){if(t.length<=1)return t;const l=[],h=new Set;for(const m of t)h.has(m)||(l.push(m),h.add(m));return l}function c(t,l,h,m){let _=[];_=_.concat(t),_=_.concat(l);for(let f=0,v=_.length;f=0;v--)_[f++]=m.charCodeAt(v);return N.getPlatformTextDecoder().decode(_)}else{let _=[],f=0;for(let v=m.length-1;v>=0;v--)_[f++]=m.charAt(v);return _.join("")}}let l=null,h=null;return function(_){return l!==_&&(l=_,h=t(l)),h}}();class n{static _findPrevBracketInText(l,h,m,_){let f=m.match(l);if(!f)return null;let v=m.length-(f.index||0),y=f[0].length,L=_+v;return new M.Range(h,L-y+1,h,L+1)}static findPrevBracketInRange(l,h,m,_,f){const y=i(m).substring(m.length-f,m.length-_);return this._findPrevBracketInText(l,h,y,_)}static findNextBracketInText(l,h,m,_){let f=m.match(l);if(!f)return null;let v=f.index||0,y=f[0].length;if(y===0)return null;let L=_+v;return new M.Range(h,L+1,h,L+1+y)}static findNextBracketInRange(l,h,m,_,f){const v=m.substring(_,f);return this.findNextBracketInText(l,h,v,_)}}e.BracketsUtils=n}),define(Q[380],J([0,1,167,168]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.BracketElectricCharacterSupport=void 0;class M{constructor(S){this._richEditBrackets=S}getElectricCharacters(){let S=[];if(this._richEditBrackets)for(const C of this._richEditBrackets.brackets)for(const d of C.close){const g=d.charAt(d.length-1);S.push(g)}return S=S.filter((C,d,g)=>g.indexOf(C)===d),S}onElectricCharacter(S,C,d){if(!this._richEditBrackets||this._richEditBrackets.brackets.length===0)return null;const g=C.findTokenIndexAtOffset(d-1);if(b.ignoreBracketsInToken(C.getStandardTokenType(g)))return null;const p=this._richEditBrackets.reversedRegex,c=C.getLineContent().substring(0,d-1)+S,o=N.BracketsUtils.findPrevBracketInRange(p,1,c,0,c.length);if(!o)return null;const s=c.substring(o.startColumn-1,o.endColumn-1).toLowerCase();if(this._richEditBrackets.textIsOpenBracket[s])return null;const u=C.getActualLineContentBefore(o.startColumn-1);return/^\s*$/.test(u)?{matchOpenBracket:s}:null}}e.BracketElectricCharacterSupport=M}),define(Q[41],J([0,1,6,2,8,128,109,167,376,380,377,379,168]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LanguageConfigurationRegistry=e.LanguageConfigurationRegistryImpl=e.LanguageConfigurationChangeEvent=e.RichEditSupport=void 0;class s{constructor(t,l){this._languageIdentifier=t,this._brackets=null,this._electricCharacter=null,this._conf=l,this._onEnterSupport=this._conf.brackets||this._conf.indentationRules||this._conf.onEnterRules?new c.OnEnterSupport(this._conf):null,this.comments=s._handleComments(this._conf),this.characterPair=new d.CharacterPairSupport(this._conf),this.wordDefinition=this._conf.wordPattern||w.DEFAULT_WORD_REGEXP,this.indentationRules=this._conf.indentationRules,this._conf.indentationRules?this.indentRulesSupport=new p.IndentRulesSupport(this._conf.indentationRules):this.indentRulesSupport=null,this.foldingRules=this._conf.folding||{}}get brackets(){return!this._brackets&&this._conf.brackets&&(this._brackets=new o.RichEditBrackets(this._languageIdentifier,this._conf.brackets)),this._brackets}get electricCharacter(){return this._electricCharacter||(this._electricCharacter=new g.BracketElectricCharacterSupport(this.brackets)),this._electricCharacter}onEnter(t,l,h,m){return this._onEnterSupport?this._onEnterSupport.onEnter(t,l,h,m):null}static _handleComments(t){let l=t.comments;if(!l)return null;let h={};if(l.lineComment&&(h.lineCommentToken=l.lineComment),l.blockComment){let[m,_]=l.blockComment;h.blockCommentStartToken=m,h.blockCommentEndToken=_}return h}}e.RichEditSupport=s;class a{constructor(t){this.languageIdentifier=t}}e.LanguageConfigurationChangeEvent=a;class u{constructor(t,l,h){this.configuration=t,this.priority=l,this.order=h}static cmp(t,l){return t.priority===l.priority?t.order-l.order:t.priority-l.priority}}class r{constructor(t){this.languageIdentifier=t,this._resolved=null,this._entries=[],this._order=0,this._resolved=null}register(t,l){const h=new u(t,l,++this._order);return this._entries.push(h),this._resolved=null,N.toDisposable(()=>{for(let m=0;m{_.dispose(),this._onDidChange.fire(new a(t))})}_getRichEditSupport(t){const l=this._entries2.get(t);return l?l.getRichEditSupport():null}getIndentationRules(t){const l=this._getRichEditSupport(t);return l&&l.indentationRules||null}_getElectricCharacterSupport(t){let l=this._getRichEditSupport(t);return l&&l.electricCharacter||null}getElectricCharacters(t){let l=this._getElectricCharacterSupport(t);return l?l.getElectricCharacters():[]}onElectricCharacter(t,l,h){let m=C.createScopedLineTokens(l,h-1),_=this._getElectricCharacterSupport(m.languageId);return _?_.onElectricCharacter(t,m,h-m.firstCharOffset):null}getComments(t){let l=this._getRichEditSupport(t);return l&&l.comments||null}_getCharacterPairSupport(t){let l=this._getRichEditSupport(t);return l&&l.characterPair||null}getAutoClosingPairs(t){const l=this._getCharacterPairSupport(t);return new S.AutoClosingPairs(l?l.getAutoClosingPairs():[])}getAutoCloseBeforeSet(t){let l=this._getCharacterPairSupport(t);return l?l.getAutoCloseBeforeSet():d.CharacterPairSupport.DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED}getSurroundingPairs(t){let l=this._getCharacterPairSupport(t);return l?l.getSurroundingPairs():[]}shouldAutoClosePair(t,l,h){const m=C.createScopedLineTokens(l,h-1);return d.CharacterPairSupport.shouldAutoClosePair(t,m,h-m.firstCharOffset)}getWordDefinition(t){let l=this._getRichEditSupport(t);return l?w.ensureValidWordDefinition(l.wordDefinition||null):w.ensureValidWordDefinition(null)}getFoldingRules(t){let l=this._getRichEditSupport(t);return l?l.foldingRules:{}}getIndentRulesSupport(t){let l=this._getRichEditSupport(t);return l&&l.indentRulesSupport||null}getPrecedingValidLine(t,l,h){let m=t.getLanguageIdAtPosition(l,0);if(l>1){let _,f=-1;for(_=l-1;_>=1;_--){if(t.getLanguageIdAtPosition(_,0)!==m)return f;let v=t.getLineContent(_);if(h.shouldIgnore(v)||/^\s+$/.test(v)||v===""){f=_;continue}return _}}return-1}getInheritIndentForLine(t,l,h,m=!0){if(t<4)return null;const _=this.getIndentRulesSupport(l.getLanguageIdentifier().id);if(!_)return null;if(h<=1)return{indentation:"",action:null};const f=this.getPrecedingValidLine(l,h,_);if(f<0)return null;if(f<1)return{indentation:"",action:null};const v=l.getLineContent(f);if(_.shouldIncrease(v)||_.shouldIndentNextLine(v))return{indentation:M.getLeadingWhitespace(v),action:S.IndentAction.Indent,line:f};if(_.shouldDecrease(v))return{indentation:M.getLeadingWhitespace(v),action:null,line:f};{if(f===1)return{indentation:M.getLeadingWhitespace(l.getLineContent(f)),action:null,line:f};const y=f-1,L=_.getIndentMetadata(l.getLineContent(y));if(!(L&(1|2))&&L&4){let I=0;for(let k=y-1;k>0;k--)if(!_.shouldIndentNextLine(l.getLineContent(k))){I=k;break}return{indentation:M.getLeadingWhitespace(l.getLineContent(I+1)),action:null,line:I+1}}if(m)return{indentation:M.getLeadingWhitespace(l.getLineContent(f)),action:null,line:f};for(let I=f;I>0;I--){const k=l.getLineContent(I);if(_.shouldIncrease(k))return{indentation:M.getLeadingWhitespace(k),action:S.IndentAction.Indent,line:I};if(_.shouldIndentNextLine(k)){let E=0;for(let T=I-1;T>0;T--)if(!_.shouldIndentNextLine(l.getLineContent(I))){E=T;break}return{indentation:M.getLeadingWhitespace(l.getLineContent(E+1)),action:null,line:E+1}}else if(_.shouldDecrease(k))return{indentation:M.getLeadingWhitespace(k),action:null,line:I}}return{indentation:M.getLeadingWhitespace(l.getLineContent(1)),action:null,line:1}}}getGoodIndentForLine(t,l,h,m,_){if(t<4)return null;const f=this._getRichEditSupport(h);if(!f)return null;const v=this.getIndentRulesSupport(h);if(!v)return null;const y=this.getInheritIndentForLine(t,l,m),L=l.getLineContent(m);if(y){const I=y.line;if(I!==void 0){const k=f.onEnter(t,"",l.getLineContent(I),"");if(k){let E=M.getLeadingWhitespace(l.getLineContent(I));return k.removeText&&(E=E.substring(0,E.length-k.removeText)),k.indentAction===S.IndentAction.Indent||k.indentAction===S.IndentAction.IndentOutdent?E=_.shiftIndent(E):k.indentAction===S.IndentAction.Outdent&&(E=_.unshiftIndent(E)),v.shouldDecrease(L)&&(E=_.unshiftIndent(E)),k.appendText&&(E+=k.appendText),M.getLeadingWhitespace(E)}}return v.shouldDecrease(L)?y.action===S.IndentAction.Indent?y.indentation:_.unshiftIndent(y.indentation):y.action===S.IndentAction.Indent?_.shiftIndent(y.indentation):y.indentation}return null}getIndentForEnter(t,l,h,m){if(t<4)return null;l.forceTokenization(h.startLineNumber);const _=l.getLineTokens(h.startLineNumber),f=C.createScopedLineTokens(_,h.startColumn-1),v=f.getLineContent();let y=!1,L;f.firstCharOffset>0&&_.getLanguageId(0)!==f.languageId?(y=!0,L=v.substr(0,h.startColumn-1-f.firstCharOffset)):L=_.getLineContent().substring(0,h.startColumn-1);let I;h.isEmpty()?I=v.substr(h.startColumn-1-f.firstCharOffset):I=this.getScopedLineTokens(l,h.endLineNumber,h.endColumn).getLineContent().substr(h.endColumn-1-f.firstCharOffset);const k=this.getIndentRulesSupport(f.languageId);if(!k)return null;const E=L,T=M.getLeadingWhitespace(L),O={getLineTokens:D=>l.getLineTokens(D),getLanguageIdentifier:()=>l.getLanguageIdentifier(),getLanguageIdAtPosition:(D,R)=>l.getLanguageIdAtPosition(D,R),getLineContent:D=>D===h.startLineNumber?E:l.getLineContent(D)},A=M.getLeadingWhitespace(_.getLineContent()),B=this.getInheritIndentForLine(t,O,h.startLineNumber+1);if(!B){const D=y?A:T;return{beforeEnter:D,afterEnter:D}}let F=y?A:B.indentation;return B.action===S.IndentAction.Indent&&(F=m.shiftIndent(F)),k.shouldDecrease(I)&&(F=m.unshiftIndent(F)),{beforeEnter:y?A:T,afterEnter:F}}getIndentActionForType(t,l,h,m,_){if(t<4)return null;const f=this.getScopedLineTokens(l,h.startLineNumber,h.startColumn);if(f.firstCharOffset)return null;const v=this.getIndentRulesSupport(f.languageId);if(!v)return null;const y=f.getLineContent(),L=y.substr(0,h.startColumn-1-f.firstCharOffset);let I;if(h.isEmpty()?I=y.substr(h.startColumn-1-f.firstCharOffset):I=this.getScopedLineTokens(l,h.endLineNumber,h.endColumn).getLineContent().substr(h.endColumn-1-f.firstCharOffset),!v.shouldDecrease(L+I)&&v.shouldDecrease(L+m+I)){const k=this.getInheritIndentForLine(t,l,h.startLineNumber,!1);if(!k)return null;let E=k.indentation;return k.action!==S.IndentAction.Indent&&(E=_.unshiftIndent(E)),E}return null}getIndentMetadata(t,l){const h=this.getIndentRulesSupport(t.getLanguageIdentifier().id);return!h||l<1||l>t.getLineCount()?null:h.getIndentMetadata(t.getLineContent(l))}getEnterAction(t,l,h){const m=this.getScopedLineTokens(l,h.startLineNumber,h.startColumn),_=this._getRichEditSupport(m.languageId);if(!_)return null;const f=m.getLineContent(),v=f.substr(0,h.startColumn-1-m.firstCharOffset);let y;h.isEmpty()?y=f.substr(h.startColumn-1-m.firstCharOffset):y=this.getScopedLineTokens(l,h.endLineNumber,h.endColumn).getLineContent().substr(h.endColumn-1-m.firstCharOffset);let L="";if(h.startLineNumber>1&&m.firstCharOffset===0){const A=this.getScopedLineTokens(l,h.startLineNumber-1);A.languageId===m.languageId&&(L=A.getLineContent())}const I=_.onEnter(t,L,v,y);if(!I)return null;const k=I.indentAction;let E=I.appendText;const T=I.removeText||0;E?k===S.IndentAction.Indent&&(E=" "+E):k===S.IndentAction.Indent||k===S.IndentAction.IndentOutdent?E=" ":E="";let O=this.getIndentationAtPosition(l,h.startLineNumber,h.startColumn);return T&&(O=O.substring(0,O.length-T)),{indentAction:k,appendText:E,removeText:T,indentation:O}}getIndentationAtPosition(t,l,h){const m=t.getLineContent(l);let _=M.getLeadingWhitespace(m);return _.length>h-1&&(_=_.substring(0,h-1)),_}getScopedLineTokens(t,l,h){t.forceTokenization(l);const m=t.getLineTokens(l),_=typeof h=="undefined"?t.getLineMaxColumn(l)-1:h-1;return C.createScopedLineTokens(m,_)}getBracketsSupport(t){const l=this._getRichEditSupport(t);return l&&l.brackets||null}}e.LanguageConfigurationRegistryImpl=i,e.LanguageConfigurationRegistry=new i}),define(Q[381],J([0,1,29]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.generateTokensCSSForColorMap=e.ThemeTrieElement=e.ThemeTrieElementRule=e.strcmp=e.toStandardTokenType=e.TokenTheme=e.ColorMap=e.parseTokenTheme=e.ParsedTokenThemeRule=void 0;class N{constructor(r,i,n,t,l){this.token=r,this.index=i,this.fontStyle=n,this.foreground=t,this.background=l}}e.ParsedTokenThemeRule=N;function M(u){if(!u||!Array.isArray(u))return[];let r=[],i=0;for(let n=0,t=u.length;n{let L=c(v.token,y.token);return L!==0?L:v.index-y.index});let i=0,n="000000",t="ffffff";for(;u.length>=1&&u[0].token==="";){let v=u.shift();v.fontStyle!==-1&&(i=v.fontStyle),v.foreground!==null&&(n=v.foreground),v.background!==null&&(t=v.background)}let l=new C;for(let v of r)l.getId(v);let h=l.getId(n),m=l.getId(t),_=new o(i,h,m),f=new s(_);for(let v=0,y=u.length;v>>0,this._cache.set(i,n)}return(n|r<<0)>>>0}}e.TokenTheme=d;const g=/\b(comment|string|regex|regexp)\b/;function p(u){let r=u.match(g);if(!r)return 0;switch(r[1]){case"comment":return 1;case"string":return 2;case"regex":return 4;case"regexp":return 4}throw new Error("Unexpected match for standard token type!")}e.toStandardTokenType=p;function c(u,r){return ur?1:0}e.strcmp=c;class o{constructor(r,i,n){this._fontStyle=r,this._foreground=i,this._background=n,this.metadata=(this._fontStyle<<11|this._foreground<<14|this._background<<23)>>>0}clone(){return new o(this._fontStyle,this._foreground,this._background)}acceptOverwrite(r,i,n){r!==-1&&(this._fontStyle=r),i!==0&&(this._foreground=i),n!==0&&(this._background=n),this.metadata=(this._fontStyle<<11|this._foreground<<14|this._background<<23)>>>0}}e.ThemeTrieElementRule=o;class s{constructor(r){this._mainRule=r,this._children=new Map}match(r){if(r==="")return this._mainRule;let i=r.indexOf("."),n,t;i===-1?(n=r,t=""):(n=r.substring(0,i),t=r.substring(i+1));let l=this._children.get(n);return typeof l!="undefined"?l.match(t):this._mainRule}insert(r,i,n,t){if(r===""){this._mainRule.acceptOverwrite(i,n,t);return}let l=r.indexOf("."),h,m;l===-1?(h=r,m=""):(h=r.substring(0,l),m=r.substring(l+1));let _=this._children.get(h);typeof _=="undefined"&&(_=new s(this._mainRule.clone()),this._children.set(h,_)),_.insert(m,i,n,t)}}e.ThemeTrieElement=s;function a(u){let r=[];for(let i=1,n=u.length;i{this._map.get(S)===C&&(this._map.delete(S),this.fire([S]))})}registerPromise(S,C){let d=null,g=!1;return this._promises.set(S,C.then(p=>{this._promises.delete(S),!(g||!p)&&(d=this.register(S,p))})),N.toDisposable(()=>{g=!0,d&&d.dispose()})}getPromise(S){const C=this.get(S);if(C)return Promise.resolve(C);const d=this._promises.get(S);return d?d.then(g=>this.get(S)):null}get(S){return this._map.get(S)||null}setColorMap(S){this._colorMap=S,this._onDidChange.fire({changedLanguages:Array.from(this._map.keys()),changedColorMap:!0})}getColorMap(){return this._colorMap}getDefaultBackground(){return this._colorMap&&this._colorMap.length>2?this._colorMap[2]:null}}e.TokenizationRegistryImpl=M}),define(Q[383],J([0,1,101,17]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.encodeSemanticTokensDto=void 0;function M(d){for(let g=0,p=d.length;gd&&(n=d-t);const l=u.color;let h=this._color2Id[l];h||(h=++this._lastAssignedId,this._color2Id[l]=h,this._id2Color[h]=l);const m=new b(n-t,n+t,h);u.setColorZone(m),o.push(m)}return this._colorZonesInvalid=!1,o.sort(b.compare),o}}e.OverviewZoneManager=M}),define(Q[110],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.VisibleRanges=e.HorizontalPosition=e.HorizontalRange=e.LineVisibleRanges=e.RenderingContext=e.RestrictedRenderingContext=void 0;class b{constructor(g,p){this._viewLayout=g,this.viewportData=p,this.scrollWidth=this._viewLayout.getScrollWidth(),this.scrollHeight=this._viewLayout.getScrollHeight(),this.visibleRange=this.viewportData.visibleRange,this.bigNumbersDelta=this.viewportData.bigNumbersDelta;const c=this._viewLayout.getCurrentViewport();this.scrollTop=c.top,this.scrollLeft=c.left,this.viewportWidth=c.width,this.viewportHeight=c.height}getScrolledTopFromAbsoluteTop(g){return g-this.scrollTop}getVerticalOffsetForLineNumber(g){return this._viewLayout.getVerticalOffsetForLineNumber(g)}getDecorationsInViewport(){return this.viewportData.getDecorationsInViewport()}}e.RestrictedRenderingContext=b;class N extends b{constructor(g,p,c){super(g,p);this._viewLines=c}linesVisibleRangesForRange(g,p){return this._viewLines.linesVisibleRangesForRange(g,p)}visibleRangeForPosition(g){return this._viewLines.visibleRangeForPosition(g)}}e.RenderingContext=N;class M{constructor(g,p,c){this.outsideRenderedLine=g,this.lineNumber=p,this.ranges=c}}e.LineVisibleRanges=M;class w{constructor(g,p){this.left=Math.round(g),this.width=Math.round(p)}toString(){return`[${this.left},${this.width}]`}}e.HorizontalRange=w;class S{constructor(g,p){this.outsideRenderedLine=g,this.left=Math.round(p)}}e.HorizontalPosition=S;class C{constructor(g,p){this.outsideRenderedLine=g,this.ranges=p}}e.VisibleRanges=C}),define(Q[384],J([0,1,110]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RangeUtil=void 0;class N{constructor(S,C){this.left=S,this.width=C}toString(){return`[${this.left},${this.width}]`}static compare(S,C){return S.left-C.left}}class M{static _createRange(){return this._handyReadyRange||(this._handyReadyRange=document.createRange()),this._handyReadyRange}static _detachRange(S,C){S.selectNodeContents(C)}static _readClientRects(S,C,d,g,p){const c=this._createRange();try{return c.setStart(S,C),c.setEnd(d,g),c.getClientRects()}catch(o){return null}finally{this._detachRange(c,p)}}static _mergeAdjacentRanges(S){if(S.length===1)return[new b.HorizontalRange(S[0].left,S[0].width)];S.sort(N.compare);let C=[],d=0,g=S[0].left,p=S[0].width;for(let c=1,o=S.length;c=a?p=Math.max(p,a+u-g):(C[d++]=new b.HorizontalRange(g,p),g=a,p=u)}return C[d++]=new b.HorizontalRange(g,p),C}static _createHorizontalRangesFromClientRects(S,C){if(!S||S.length===0)return null;const d=[];for(let g=0,p=S.length;ga)return null;if(C=Math.min(a,Math.max(s,C)),g=Math.min(a,Math.max(s,g)),C===g&&d===p&&d===0){const n=S.children[C].getClientRects();return this._createHorizontalRangesFromClientRects(n,c)}C!==g&&g>0&&p===0&&(g--,p=1073741824);let u=S.children[C].firstChild,r=S.children[g].firstChild;if((!u||!r)&&(!u&&d===0&&C>0&&(u=S.children[C-1].firstChild,d=1073741824),!r&&p===0&&g>0&&(r=S.children[g-1].firstChild,p=1073741824)),!u||!r)return null;d=Math.min(u.textContent.length,Math.max(0,d)),p=Math.min(r.textContent.length,Math.max(0,p));const i=this._readClientRects(u,d,r,p,o);return this._createHorizontalRangesFromClientRects(i,c)}}e.RangeUtil=M}),define(Q[385],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewContext=e.EditorTheme=void 0;class b{constructor(w){this._theme=w}get type(){return this._theme.type}update(w){this._theme=w}getColor(w){return this._theme.getColor(w)}}e.EditorTheme=b;class N{constructor(w,S,C){this.configuration=w,this.theme=new b(S),this.model=C,this.viewLayout=C.viewLayout}addEventHandler(w){this.model.addViewEventHandler(w)}removeEventHandler(w){this.model.removeViewEventHandler(w)}}e.ViewContext=N}),define(Q[170],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewZonesChangedEvent=e.ViewTokensColorsChangedEvent=e.ViewTokensChangedEvent=e.ViewThemeChangedEvent=e.ViewScrollChangedEvent=e.ViewRevealRangeRequestEvent=e.ViewLinesInsertedEvent=e.ViewLinesDeletedEvent=e.ViewLinesChangedEvent=e.ViewLineMappingChangedEvent=e.ViewLanguageConfigurationEvent=e.ViewFocusChangedEvent=e.ViewFlushedEvent=e.ViewDecorationsChangedEvent=e.ViewCursorStateChangedEvent=e.ViewConfigurationChangedEvent=e.ViewCompositionEndEvent=e.ViewCompositionStartEvent=void 0;class b{constructor(){this.type=0}}e.ViewCompositionStartEvent=b;class N{constructor(){this.type=1}}e.ViewCompositionEndEvent=N;class M{constructor(h){this.type=2,this._source=h}hasChanged(h){return this._source.hasChanged(h)}}e.ViewConfigurationChangedEvent=M;class w{constructor(h,m){this.type=3,this.selections=h,this.modelSelections=m}}e.ViewCursorStateChangedEvent=w;class S{constructor(h){this.type=4,h?(this.affectsMinimap=h.affectsMinimap,this.affectsOverviewRuler=h.affectsOverviewRuler):(this.affectsMinimap=!0,this.affectsOverviewRuler=!0)}}e.ViewDecorationsChangedEvent=S;class C{constructor(){this.type=5}}e.ViewFlushedEvent=C;class d{constructor(h){this.type=6,this.isFocused=h}}e.ViewFocusChangedEvent=d;class g{constructor(){this.type=7}}e.ViewLanguageConfigurationEvent=g;class p{constructor(){this.type=8}}e.ViewLineMappingChangedEvent=p;class c{constructor(h,m){this.type=9,this.fromLineNumber=h,this.toLineNumber=m}}e.ViewLinesChangedEvent=c;class o{constructor(h,m){this.type=10,this.fromLineNumber=h,this.toLineNumber=m}}e.ViewLinesDeletedEvent=o;class s{constructor(h,m){this.type=11,this.fromLineNumber=h,this.toLineNumber=m}}e.ViewLinesInsertedEvent=s;class a{constructor(h,m,_,f,v,y){this.type=12,this.source=h,this.range=m,this.selections=_,this.verticalType=f,this.revealHorizontal=v,this.scrollType=y}}e.ViewRevealRangeRequestEvent=a;class u{constructor(h){this.type=13,this.scrollWidth=h.scrollWidth,this.scrollLeft=h.scrollLeft,this.scrollHeight=h.scrollHeight,this.scrollTop=h.scrollTop,this.scrollWidthChanged=h.scrollWidthChanged,this.scrollLeftChanged=h.scrollLeftChanged,this.scrollHeightChanged=h.scrollHeightChanged,this.scrollTopChanged=h.scrollTopChanged}}e.ViewScrollChangedEvent=u;class r{constructor(){this.type=14}}e.ViewThemeChangedEvent=r;class i{constructor(h){this.type=15,this.ranges=h}}e.ViewTokensChangedEvent=i;class n{constructor(){this.type=16}}e.ViewTokensColorsChangedEvent=n;class t{constructor(){this.type=17}}e.ViewZonesChangedEvent=t}),define(Q[171],J([0,1,8]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LineDecorationsNormalizer=e.DecorationSegment=e.LineDecoration=void 0;class N{constructor(d,g,p,c){this.startColumn=d,this.endColumn=g,this.className=p,this.type=c}static _equals(d,g){return d.startColumn===g.startColumn&&d.endColumn===g.endColumn&&d.className===g.className&&d.type===g.type}static equalsArr(d,g){const p=d.length,c=g.length;if(p!==c)return!1;for(let o=0;o=o||(a[u++]=new N(Math.max(1,r.startColumn-c+1),Math.min(s+1,r.endColumn-c+1),r.className,r.type));return a}static filter(d,g,p,c){if(d.length===0)return[];let o=[],s=0;for(let a=0,u=d.length;ag)&&!(i.isEmpty()&&(r.type===0||r.type===3))){const n=i.startLineNumber===g?i.startColumn:p,t=i.endLineNumber===g?i.endColumn:c;o[s++]=new N(n,t,r.inlineClassName,r.type)}}return o}static _typeCompare(d,g){const p=[2,0,1,3];return p[d]-p[g]}static compare(d,g){if(d.startColumn===g.startColumn){if(d.endColumn===g.endColumn){const p=N._typeCompare(d.type,g.type);return p===0?d.classNameg.className?1:0:p}return d.endColumn-g.endColumn}return d.startColumn-g.startColumn}}e.LineDecoration=N;class M{constructor(d,g,p,c){this.startOffset=d,this.endOffset=g,this.className=p,this.metadata=c}}e.DecorationSegment=M;class w{constructor(){this.stopOffsets=[],this.classNames=[],this.metadata=[],this.count=0}static _metadata(d){let g=0;for(let p=0,c=d.length;p0&&this.stopOffsets[0]0&&g=d){this.stopOffsets.splice(c,0,d),this.classNames.splice(c,0,g),this.metadata.splice(c,0,p);break}this.count++}}class S{static normalize(d,g){if(g.length===0)return[];let p=[];const c=new w;let o=0;for(let s=0,a=g.length;s1){const m=d.charCodeAt(r-2);b.isHighSurrogate(m)&&r--}if(i>1){const m=d.charCodeAt(i-2);b.isHighSurrogate(m)&&i--}const l=r-1,h=i-2;o=c.consumeLowerThan(l,o,p),c.count===0&&(o=l),c.insert(h,n,t)}return c.consumeLowerThan(1073741824,o,p),p}}e.LineDecorationsNormalizer=S}),define(Q[386],J([0,1,8]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LinesLayout=e.EditorWhitespace=void 0;class N{constructor(){this._hasPending=!1,this._inserts=[],this._changes=[],this._removes=[]}insert(C){this._hasPending=!0,this._inserts.push(C)}change(C){this._hasPending=!0,this._changes.push(C)}remove(C){this._hasPending=!0,this._removes.push(C)}mustCommit(){return this._hasPending}commit(C){if(!!this._hasPending){const d=this._inserts,g=this._changes,p=this._removes;this._hasPending=!1,this._inserts=[],this._changes=[],this._removes=[],C._commitPendingChanges(d,g,p)}}}class M{constructor(C,d,g,p,c){this.id=C,this.afterLineNumber=d,this.ordinal=g,this.height=p,this.minWidth=c,this.prefixSum=0}}e.EditorWhitespace=M;class w{constructor(C,d,g,p){this._instanceId=b.singleLetterHash(++w.INSTANCE_COUNT),this._pendingChanges=new N,this._lastWhitespaceId=0,this._arr=[],this._prefixSumValidIndex=-1,this._minWidth=-1,this._lineCount=C,this._lineHeight=d,this._paddingTop=g,this._paddingBottom=p}static findInsertionIndex(C,d,g){let p=0,c=C.length;for(;p>>1;d===C[o].afterLineNumber?g{d=!0,p=p|0,c=c|0,o=o|0,s=s|0;const a=this._instanceId+ ++this._lastWhitespaceId;return this._pendingChanges.insert(new M(a,p,c,o,s)),a},changeOneWhitespace:(p,c,o)=>{d=!0,c=c|0,o=o|0,this._pendingChanges.change({id:p,newAfterLineNumber:c,newHeight:o})},removeWhitespace:p=>{d=!0,this._pendingChanges.remove({id:p})}})}finally{this._pendingChanges.commit(this)}return d}_commitPendingChanges(C,d,g){if((C.length>0||g.length>0)&&(this._minWidth=-1),C.length+d.length+g.length<=1){for(const a of C)this._insertWhitespace(a);for(const a of d)this._changeOneWhitespace(a.id,a.newAfterLineNumber,a.newHeight);for(const a of g){const u=this._findWhitespaceIndex(a.id);u!==-1&&this._removeWhitespace(u)}return}const p=new Set;for(const a of g)p.add(a.id);const c=new Map;for(const a of d)c.set(a.id,a);const o=a=>{let u=[];for(const r of a)if(!p.has(r.id)){if(c.has(r.id)){const i=c.get(r.id);r.afterLineNumber=i.newAfterLineNumber,r.height=i.newHeight}u.push(r)}return u},s=o(this._arr).concat(o(C));s.sort((a,u)=>a.afterLineNumber===u.afterLineNumber?a.ordinal-u.ordinal:a.afterLineNumber-u.afterLineNumber),this._arr=s,this._prefixSumValidIndex=-1}_checkPendingChanges(){this._pendingChanges.mustCommit()&&this._pendingChanges.commit(this)}_insertWhitespace(C){const d=w.findInsertionIndex(this._arr,C.afterLineNumber,C.ordinal);this._arr.splice(d,0,C),this._prefixSumValidIndex=Math.min(this._prefixSumValidIndex,d-1)}_findWhitespaceIndex(C){const d=this._arr;for(let g=0,p=d.length;gd&&(this._arr[g].afterLineNumber-=d-C+1)}}onLinesInserted(C,d){this._checkPendingChanges(),C=C|0,d=d|0,this._lineCount+=d-C+1;for(let g=0,p=this._arr.length;g=d.length||d[s+1].afterLineNumber>=C)return s;g=s+1|0}else p=s-1|0}return-1}_findFirstWhitespaceAfterLineNumber(C){C=C|0;const g=this._findLastWhitespaceBeforeLineNumber(C)+1;return g1?d=this._lineHeight*(C-1):d=0;const g=this.getWhitespaceAccumulatedHeightBeforeLineNumber(C);return d+g+this._paddingTop}getWhitespaceMinWidth(){if(this._checkPendingChanges(),this._minWidth===-1){let C=0;for(let d=0,g=this._arr.length;dd}isInTopPadding(C){return this._paddingTop===0?!1:(this._checkPendingChanges(),C=d-this._paddingBottom}getLineNumberAtOrAfterVerticalOffset(C){if(this._checkPendingChanges(),C=C|0,C<0)return 1;const d=this._lineCount|0,g=this._lineHeight;let p=1,c=d;for(;p=s+g)p=o+1;else{if(C>=s)return o;c=o}}return p>d?d:p}getLinesViewportData(C,d){this._checkPendingChanges(),C=C|0,d=d|0;const g=this._lineHeight,p=this.getLineNumberAtOrAfterVerticalOffset(C)|0,c=this.getVerticalOffsetForLineNumber(p)|0;let o=this._lineCount|0,s=this.getFirstWhitespaceIndexAfterLineNumber(p)|0;const a=this.getWhitespacesCount()|0;let u,r;s===-1?(s=a,r=o+1,u=0):(r=this.getAfterLineNumberForWhitespaceIndex(s)|0,u=this.getHeightForWhitespaceIndex(s)|0);let i=c,n=i;const t=5e5;let l=0;c>=t&&(l=Math.floor(c/t)*t,l=Math.floor(l/g)*g,n-=l);const h=[],m=C+(d-C)/2;let _=-1;for(let L=p;L<=o;L++){if(_===-1){const I=i,k=i+g;(I<=m&&mm)&&(_=L)}for(i+=g,h[L-p]=n,n+=g;r===L;)n+=u,i+=u,s++,s>=a?r=o+1:(r=this.getAfterLineNumberForWhitespaceIndex(s)|0,u=this.getHeightForWhitespaceIndex(s)|0);if(i>=d){o=L;break}}_===-1&&(_=o);const f=this.getVerticalOffsetForLineNumber(o)|0;let v=p,y=o;return vd&&y--,{bigNumbersDelta:l,startLineNumber:p,endLineNumber:o,relativeVerticalOffset:h,centeredLineNumber:_,completelyVisibleStartLineNumber:v,completelyVisibleEndLineNumber:y}}getVerticalOffsetForWhitespaceIndex(C){this._checkPendingChanges(),C=C|0;const d=this.getAfterLineNumberForWhitespaceIndex(C);let g;d>=1?g=this._lineHeight*d:g=0;let p;return C>0?p=this.getWhitespacesAccumulatedHeight(C-1):p=0,g+p+this._paddingTop}getWhitespaceIndexAtOrAfterVerticallOffset(C){this._checkPendingChanges(),C=C|0;let d=0,g=this.getWhitespacesCount()-1;if(g<0)return-1;const p=this.getVerticalOffsetForWhitespaceIndex(g),c=this.getHeightForWhitespaceIndex(g);if(C>=p+c)return-1;for(;d=s+a)d=o+1;else{if(C>=s)return o;g=o}}return d}getWhitespaceAtVerticalOffset(C){this._checkPendingChanges(),C=C|0;const d=this.getWhitespaceIndexAtOrAfterVerticallOffset(C);if(d<0||d>=this.getWhitespacesCount())return null;const g=this.getVerticalOffsetForWhitespaceIndex(d);if(g>C)return null;const p=this.getHeightForWhitespaceIndex(d),c=this.getIdForWhitespaceIndex(d),o=this.getAfterLineNumberForWhitespaceIndex(d);return{id:c,afterLineNumber:o,verticalOffset:g,height:p}}getWhitespaceViewportData(C,d){this._checkPendingChanges(),C=C|0,d=d|0;const g=this.getWhitespaceIndexAtOrAfterVerticallOffset(C),p=this.getWhitespacesCount()-1;if(g<0)return[];let c=[];for(let o=g;o<=p;o++){const s=this.getVerticalOffsetForWhitespaceIndex(o),a=this.getHeightForWhitespaceIndex(o);if(s>=d)break;c.push({id:this.getIdForWhitespaceIndex(o),afterLineNumber:this.getAfterLineNumberForWhitespaceIndex(o),verticalOffset:s,height:a})}return c}getWhitespaces(){return this._checkPendingChanges(),this._arr.slice(0)}getWhitespacesCount(){return this._checkPendingChanges(),this._arr.length}getIdForWhitespaceIndex(C){return this._checkPendingChanges(),C=C|0,this._arr[C].id}getAfterLineNumberForWhitespaceIndex(C){return this._checkPendingChanges(),C=C|0,this._arr[C].afterLineNumber}getHeightForWhitespaceIndex(C){return this._checkPendingChanges(),C=C|0,this._arr[C].height}}e.LinesLayout=w,w.INSTANCE_COUNT=0}),define(Q[129],J([0,1,8,93,171]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.renderViewLine2=e.RenderLineOutput2=e.renderViewLine=e.RenderLineOutput=e.CharacterMapping=e.RenderLineInput=e.LineRange=void 0;class w{constructor(h,m,_){this.endIndex=h,this.type=m,this.metadata=_}isWhitespace(){return!!(this.metadata&1)}}class S{constructor(h,m){this.startOffset=h,this.endOffset=m}equals(h){return this.startOffset===h.startOffset&&this.endOffset===h.endOffset}}e.LineRange=S;class C{constructor(h,m,_,f,v,y,L,I,k,E,T,O,A,B,F,D,R,W,x){this.useMonospaceOptimizations=h,this.canUseHalfwidthRightwardsArrow=m,this.lineContent=_,this.continuesWithWrappedLine=f,this.isBasicASCII=v,this.containsRTL=y,this.fauxIndentLength=L,this.lineTokens=I,this.lineDecorations=k.sort(M.LineDecoration.compare),this.tabSize=E,this.startVisibleColumn=T,this.spaceWidth=O,this.stopRenderingLineAfter=F,this.renderWhitespace=D==="all"?4:D==="boundary"?1:D==="selection"?2:D==="trailing"?3:0,this.renderControlCharacters=R,this.fontLigatures=W,this.selectionsOnLine=x&&x.sort((ee,se)=>ee.startOffset>>16}static getCharIndex(h){return(h&65535)>>>0}setPartData(h,m,_,f){let v=(m<<16|_<<0)>>>0;this._data[h]=v,this._absoluteOffsets[h]=f+_}getAbsoluteOffsets(){return this._absoluteOffsets}charOffsetToPartData(h){return this.length===0?0:h<0?this._data[0]:h>=this.length?this._data[this.length-1]:this._data[h]}partDataToCharOffset(h,m,_){if(this.length===0)return 0;let f=(h<<16|_<<0)>>>0,v=0,y=this.length-1;for(;v+1>>1,D=this._data[F];if(D===f)return F;D>f?y=F:v=F}if(v===y)return v;let L=this._data[v],I=this._data[y];if(L===f)return v;if(I===f)return y;let k=d.getPartIndex(L),E=d.getCharIndex(L),T=d.getPartIndex(I),O;k!==T?O=m:O=d.getCharIndex(I);let A=_-E,B=O-_;return A<=B?v:y}}e.CharacterMapping=d;class g{constructor(h,m,_){this.characterMapping=h,this.containsRTL=m,this.containsForeignElements=_}}e.RenderLineOutput=g;function p(l,h){if(l.lineContent.length===0){let m=0,_="";if(l.lineDecorations.length>0){const f=[],v=[];for(let y=0,L=l.lineDecorations.length;y0?``:"",L=v.length>0?``:"";_=`${y}${L}`}}return h.appendASCIIString(_),new g(new d(0,0),!1,m)}return t(a(l),h)}e.renderViewLine=p;class c{constructor(h,m,_,f){this.characterMapping=h,this.html=m,this.containsRTL=_,this.containsForeignElements=f}}e.RenderLineOutput2=c;function o(l){let h=N.createStringBuilder(1e4),m=p(l,h);return new c(m.characterMapping,h.build(),m.containsRTL,m.containsForeignElements)}e.renderViewLine2=o;class s{constructor(h,m,_,f,v,y,L,I,k,E,T,O,A,B,F){this.fontIsMonospace=h,this.canUseHalfwidthRightwardsArrow=m,this.lineContent=_,this.len=f,this.isOverflowing=v,this.parts=y,this.containsForeignElements=L,this.fauxIndentLength=I,this.tabSize=k,this.startVisibleColumn=E,this.containsRTL=T,this.spaceWidth=O,this.renderSpaceCharCode=A,this.renderWhitespace=B,this.renderControlCharacters=F}}function a(l){const h=l.lineContent;let m,_;l.stopRenderingLineAfter!==-1&&l.stopRenderingLineAfter0){for(let y=0,L=l.lineDecorations.length;y0&&(_[f++]=new w(h,"",0));for(let v=0,y=l.getCount();v=m){_[f++]=new w(m,I,0);break}_[f++]=new w(L,I,0)}}return _}function r(l,h,m){let _=0,f=[],v=0;if(m)for(let y=0,L=h.length;y=50&&(f[v++]=new w(O+1,E,T),A=O+1,O=-1);A!==k&&(f[v++]=new w(k,E,T))}else f[v++]=I;_=k}else for(let y=0,L=h.length;y50){const T=I.type,O=I.metadata,A=Math.ceil(E/50);for(let B=1;B=ne.endOffset&&(se++,ne=k&&k[se]);let V;if(zY)V=!0;else if(P===9)V=!0;else if(P===32)if(E)if(ee)V=!0;else{const U=z+1z),V&&T&&(V=x||z>Y),ee){if(!V||!I&&le>=y){if(O){const U=B>0?A[B-1].endIndex:v;for(let H=U+1;H<=z;H++)A[B++]=new w(H,"mtkw",1)}else A[B++]=new w(z,"mtkw",1);le=le%y}}else(z===R||V&&z>v)&&(A[B++]=new w(z,D,0),le=le%y);for(P===9?le=y:b.isFullWidthCharacter(P)?le+=2:le++,ee=V;z===R;)F++,F0?h.charCodeAt(m-1):0,P=m>1?h.charCodeAt(m-2):0;z===32&&P!==32&&P!==9||(X=!0)}else X=!0;if(X)if(O){const z=B>0?A[B-1].endIndex:v;for(let P=z+1;P<=m;P++)A[B++]=new w(P,"mtkw",1)}else A[B++]=new w(m,"mtkw",1);else A[B++]=new w(m,D,0);return A}function n(l,h,m,_){_.sort(M.LineDecoration.compare);const f=M.LineDecorationsNormalizer.normalize(l,_),v=f.length;let y=0,L=[],I=0,k=0;for(let T=0,O=m.length;Tk&&(k=R.startOffset,L[I++]=new w(k,F,D)),R.endOffset+1<=B)k=R.endOffset+1,L[I++]=new w(k,F+" "+R.className,D|R.metadata),y++;else{k=B,L[I++]=new w(k,F+" "+R.className,D|R.metadata);break}}B>k&&(k=B,L[I++]=new w(k,F,D))}const E=m[m.length-1].endIndex;if(y'):h.appendASCIIString("");for(let ne=0,le=I.length;ne=k&&(oe+=G)}}for(U&&(h.appendASCIIString(' style="width:'),h.appendASCIIString(String(A*$)),h.appendASCIIString('px"')),h.appendASCII(62);W1?h.write1(8594):h.write1(65515);for(let ae=2;ae<=oe;ae++)h.write1(160)}else oe=1,h.write1(B);K+=oe,W>=k&&(x+=oe)}ee=$}else{let $=0;for(h.appendASCII(62);W=k&&(x+=ae)}ee=$}H?Y++:Y=0,h.appendASCIIString("")}return R.setPartData(y,I.length-1,K,se),L&&h.appendASCIIString(""),h.appendASCIIString(""),new g(R,O,f)}}),define(Q[387],J([0,1,3]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewportData=void 0;class N{constructor(w,S,C,d){this.selections=w,this.startLineNumber=S.startLineNumber|0,this.endLineNumber=S.endLineNumber|0,this.relativeVerticalOffset=S.relativeVerticalOffset,this.bigNumbersDelta=S.bigNumbersDelta|0,this.whitespaceViewportData=C,this._model=d,this.visibleRange=new b.Range(S.startLineNumber,this._model.getLineMinColumn(S.startLineNumber),S.endLineNumber,this._model.getLineMaxColumn(S.endLineNumber))}getViewLineRenderingData(w){return this._model.getViewLineRenderingData(this.visibleRange,w)}getDecorationsInViewport(){return this._model.getDecorationsInViewport(this.visibleRange)}}e.ViewportData=N}),define(Q[222],J([0,1,122]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PrefixSumComputer=e.PrefixSumIndexOfResult=void 0;class N{constructor(S,C){this.index=S,this.remainder=C}}e.PrefixSumIndexOfResult=N;class M{constructor(S){this.values=S,this.prefixSum=new Uint32Array(S.length),this.prefixSumValidIndex=new Int32Array(1),this.prefixSumValidIndex[0]=-1}insertValues(S,C){S=b.toUint32(S);const d=this.values,g=this.prefixSum,p=C.length;return p===0?!1:(this.values=new Uint32Array(d.length+p),this.values.set(d.subarray(0,S),0),this.values.set(d.subarray(S),S+p),this.values.set(C,S),S-1=0&&this.prefixSum.set(g.subarray(0,this.prefixSumValidIndex[0]+1)),!0)}changeValue(S,C){return S=b.toUint32(S),C=b.toUint32(C),this.values[S]===C?!1:(this.values[S]=C,S-1=d.length)return!1;let p=d.length-S;return C>=p&&(C=p),C===0?!1:(this.values=new Uint32Array(d.length-C),this.values.set(d.subarray(0,S),0),this.values.set(d.subarray(S+C),S),this.prefixSum=new Uint32Array(this.values.length),S-1=0&&this.prefixSum.set(g.subarray(0,this.prefixSumValidIndex[0]+1)),!0)}getTotalValue(){return this.values.length===0?0:this._getAccumulatedValue(this.values.length-1)}getAccumulatedValue(S){return S<0?0:(S=b.toUint32(S),this._getAccumulatedValue(S))}_getAccumulatedValue(S){if(S<=this.prefixSumValidIndex[0])return this.prefixSum[S];let C=this.prefixSumValidIndex[0]+1;C===0&&(this.prefixSum[0]=this.values[0],C++),S>=this.values.length&&(S=this.values.length-1);for(let d=C;d<=S;d++)this.prefixSum[d]=this.prefixSum[d-1]+this.values[d];return this.prefixSumValidIndex[0]=Math.max(this.prefixSumValidIndex[0],S),this.prefixSum[S]}getIndexOf(S){S=Math.floor(S),this.getTotalValue();let C=0,d=this.values.length-1,g=0,p=0,c=0;for(;C<=d;)if(g=C+(d-C)/2|0,p=this.prefixSum[g],c=p-this.values[g],S=p)C=g+1;else break;return new N(g,S-c)}}e.PrefixSumComputer=M}),define(Q[388],J([0,1,8,14,222]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MirrorTextModel=void 0;class w{constructor(C,d,g,p){this._uri=C,this._lines=d,this._eol=g,this._versionId=p,this._lineStarts=null,this._cachedTextValue=null}dispose(){this._lines.length=0}getText(){return this._cachedTextValue===null&&(this._cachedTextValue=this._lines.join(this._eol)),this._cachedTextValue}onEvents(C){C.eol&&C.eol!==this._eol&&(this._eol=C.eol,this._lineStarts=null);const d=C.changes;for(const g of d)this._acceptDeleteRange(g.range),this._acceptInsertText(new N.Position(g.range.startLineNumber,g.range.startColumn),g.text);this._versionId=C.versionId,this._cachedTextValue=null}_ensureLineStarts(){if(!this._lineStarts){const C=this._eol.length,d=this._lines.length,g=new Uint32Array(d);for(let p=0;pthis._lines.length)h=this._lines.length,m=this._lines[h-1].length+1,_=!0;else{let f=this._lines[h-1].length+1;m<1?(m=1,_=!0):m>f&&(m=f,_=!0)}return _?{lineNumber:h,column:m}:l}}class i{constructor(l,h){this._host=l,this._models=Object.create(null),this._foreignModuleFactory=h,this._foreignModule=null}dispose(){this._models=Object.create(null)}_getModel(l){return this._models[l]}_getModels(){let l=[];return Object.keys(this._models).forEach(h=>l.push(this._models[h])),l}acceptNewModel(l){this._models[l.url]=new r(w.URI.parse(l.url),l.lines,l.EOL,l.versionId)}acceptModelChanged(l,h){!this._models[l]||this._models[l].onEvents(h)}acceptRemovedModel(l){!this._models[l]||delete this._models[l]}computeDiff(l,h,m,_){return Ie(this,void 0,void 0,function*(){const f=this._getModel(l),v=this._getModel(h);if(!f||!v)return null;const y=f.getLinesContent(),L=v.getLinesContent(),k=new d.DiffComputer(y,L,{shouldComputeCharChanges:!0,shouldPostProcessCharChanges:!0,shouldIgnoreTrimWhitespace:m,shouldMakePrettyDiff:!0,maxComputationTime:_}).computeDiff(),E=k.changes.length>0?!1:this._modelsAreIdentical(f,v);return{quitEarly:k.quitEarly,identical:E,changes:k.changes}})}_modelsAreIdentical(l,h){const m=l.getLineCount(),_=h.getLineCount();if(m!==_)return!1;for(let f=1;f<=m;f++){const v=l.getLineContent(f),y=h.getLineContent(f);if(v!==y)return!1}return!0}computeMoreMinimalEdits(l,h){return Ie(this,void 0,void 0,function*(){const m=this._getModel(l);if(!m)return h;const _=[];let f;h=b.mergeSort(h,(v,y)=>{if(v.range&&y.range)return C.Range.compareRangesUsingStarts(v.range,y.range);let L=v.range?0:1,I=y.range?0:1;return L-I});for(let{range:v,text:y,eol:L}of h)if(typeof L=="number"&&(f=L),!(C.Range.isEmpty(v)&&!y)){const I=m.getValueInRange(v);if(y=y.replace(/\r\n|\n|\r/g,m.eol),I!==y){if(Math.max(y.length,I.length)>i._diffLimit){_.push({range:v,text:y});continue}const k=N.stringDiff(I,y,!1),E=m.offsetAt(C.Range.lift(v).getStartPosition());for(const T of k){const O=m.positionAt(E+T.originalStart),A=m.positionAt(E+T.originalStart+T.originalLength),B={text:y.substr(T.modifiedStart,T.modifiedLength),range:{startLineNumber:O.lineNumber,startColumn:O.column,endLineNumber:A.lineNumber,endColumn:A.column}};m.getValueInRange(B.range)!==B.text&&_.push(B)}}}return typeof f=="number"&&_.push({eol:f,text:"",range:{startLineNumber:0,startColumn:0,endLineNumber:0,endColumn:0}}),_})}computeLinks(l){return Ie(this,void 0,void 0,function*(){let h=this._getModel(l);return h?c.computeLinks(h):null})}textualSuggest(l,h,m,_){return Ie(this,void 0,void 0,function*(){const f=new u.StopWatch(!0),v=new RegExp(m,_),y=new Set;e:for(let L of l){const I=this._getModel(L);if(!!I){for(let k of I.words(v))if(!(k===h||!isNaN(Number(k)))&&(y.add(k),y.size>i._suggestionsLimit))break e}}return{words:Array.from(y),duration:f.elapsed()}})}computeWordRanges(l,h,m,_){return Ie(this,void 0,void 0,function*(){let f=this._getModel(l);if(!f)return Object.create(null);const v=new RegExp(m,_),y=Object.create(null);for(let L=h.startLineNumber;Lthis._host.fhr(y,L);let v={host:a.createProxyObject(m,_),getMirrorModels:()=>this._getModels()};return this._foreignModuleFactory?(this._foreignModule=this._foreignModuleFactory(v,h),Promise.resolve(a.getAllMethodNames(this._foreignModule))):new Promise((y,L)=>{q([l],I=>{this._foreignModule=I.create(v,h),y(a.getAllMethodNames(this._foreignModule))},L)})}fmr(l,h){if(!this._foreignModule||typeof this._foreignModule[l]!="function")return Promise.reject(new Error("Missing requestHandler or method: "+l));try{return Promise.resolve(this._foreignModule[l].apply(this._foreignModule,h))}catch(m){return Promise.reject(m)}}}e.EditorSimpleWorker=i,i._diffLimit=1e5,i._suggestionsLimit=1e4;function n(t){return new i(t,null)}e.create=n,typeof importScripts=="function"&&(M.globals.monaco=s.createMonacoBaseAPI())}),define(Q[111],J([0,1,2]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewEventHandler=void 0;class N extends b.Disposable{constructor(){super();this._shouldRender=!0}shouldRender(){return this._shouldRender}forceShouldRender(){this._shouldRender=!0}setShouldRender(){this._shouldRender=!0}onDidRender(){this._shouldRender=!1}onCompositionStart(w){return!1}onCompositionEnd(w){return!1}onConfigurationChanged(w){return!1}onCursorStateChanged(w){return!1}onDecorationsChanged(w){return!1}onFlushed(w){return!1}onFocusChanged(w){return!1}onLanguageConfigurationChanged(w){return!1}onLineMappingChanged(w){return!1}onLinesChanged(w){return!1}onLinesDeleted(w){return!1}onLinesInserted(w){return!1}onRevealRangeRequest(w){return!1}onScrollChanged(w){return!1}onThemeChanged(w){return!1}onTokensChanged(w){return!1}onTokensColorsChanged(w){return!1}onZonesChanged(w){return!1}handleEvents(w){let S=!1;for(let C=0,d=w.length;C=s,h=i,m=n>=s;let _=p.left,f=c.left;return _+o>a.scrollLeft+a.viewportWidth&&(_=a.scrollLeft+a.viewportWidth-o),f+o>a.scrollLeft+a.viewportWidth&&(f=a.scrollLeft+a.viewportWidth-o),_u){const i=r-(u-s);r-=i,o-=i}if(r=v,I=t+s<=l.height-y;return this._fixedOverflowWidgets?{fitsAbove:L,aboveTop:Math.max(n,v),aboveLeft:m,fitsBelow:I,belowTop:t,belowLeft:f}:{fitsAbove:L,aboveTop:u,aboveLeft:h,fitsBelow:I,belowTop:r,belowLeft:_}}_prepareRenderWidgetAtExactPositionOverflowing(p){return new w(p.top,p.left+this._contentLeft)}_getTopAndBottomLeft(p){if(!this._viewRange)return[null,null];const c=p.linesVisibleRangesForRange(this._viewRange,!1);if(!c||c.length===0)return[null,null];let o=c[0],s=c[0];for(const l of c)l.lineNumbers.lineNumber&&(s=l);let a=1073741824;for(const l of o.ranges)l.leftp.endLineNumber||this.domNode.setMaxWidth(this._maxWidth)}prepareRender(p){this._renderData=this._prepareRenderWidget(p)}render(p){if(!this._renderData){this._isVisible&&(this.domNode.removeAttribute("monaco-visible-content-widget"),this._isVisible=!1,this.domNode.setVisibility("hidden")),typeof this._actual.afterRender=="function"&&d(this._actual.afterRender,this._actual,null);return}this.allowEditorOverflow?(this.domNode.setTop(this._renderData.coordinate.top),this.domNode.setLeft(this._renderData.coordinate.left)):(this.domNode.setTop(this._renderData.coordinate.top+p.scrollTop-p.bigNumbersDelta),this.domNode.setLeft(this._renderData.coordinate.left)),this._isVisible||(this.domNode.setVisibility("inherit"),this.domNode.setAttribute("monaco-visible-content-widget","true"),this._isVisible=!0),typeof this._actual.afterRender=="function"&&d(this._actual.afterRender,this._actual,this._renderData.position)}}function d(g,p,...c){try{return g.call(p,...c)}catch(o){return null}}}),define(Q[391],J([0,1,94,3,110,319]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DecorationsOverlay=void 0;class w extends b.DynamicViewOverlay{constructor(C){super();this._context=C;const d=this._context.configuration.options;this._lineHeight=d.get(53),this._typicalHalfwidthCharacterWidth=d.get(38).typicalHalfwidthCharacterWidth,this._renderResult=null,this._context.addEventHandler(this)}dispose(){this._context.removeEventHandler(this),this._renderResult=null,super.dispose()}onConfigurationChanged(C){const d=this._context.configuration.options;return this._lineHeight=d.get(53),this._typicalHalfwidthCharacterWidth=d.get(38).typicalHalfwidthCharacterWidth,!0}onDecorationsChanged(C){return!0}onFlushed(C){return!0}onLinesChanged(C){return!0}onLinesDeleted(C){return!0}onLinesInserted(C){return!0}onScrollChanged(C){return C.scrollTopChanged||C.scrollWidthChanged}onZonesChanged(C){return!0}prepareRender(C){const d=C.getDecorationsInViewport();let g=[],p=0;for(let a=0,u=d.length;a{if(a.options.zIndexu.options.zIndex)return 1;const r=a.options.className,i=u.options.className;return ri?1:N.Range.compareRangesUsingStarts(a.range,u.range)});const c=C.visibleRange.startLineNumber,o=C.visibleRange.endLineNumber,s=[];for(let a=c;a<=o;a++){const u=a-c;s[u]=""}this._renderWholeLineDecorations(C,g,s),this._renderNormalDecorations(C,g,s),this._renderResult=s}_renderWholeLineDecorations(C,d,g){const p=String(this._lineHeight),c=C.visibleRange.startLineNumber,o=C.visibleRange.endLineNumber;for(let s=0,a=d.length;s',i=Math.max(u.range.startLineNumber,c),n=Math.min(u.range.endLineNumber,o);for(let t=i;t<=n;t++){const l=t-c;g[l]+=r}}}}_renderNormalDecorations(C,d,g){const p=String(this._lineHeight),c=C.visibleRange.startLineNumber;let o=null,s=!1,a=null;for(let u=0,r=d.length;u';s[n]+=m}}}}render(C,d){if(!this._renderResult)return"";const g=d-C;return g<0||g>=this._renderResult.length?"":this._renderResult[g]}}e.DecorationsOverlay=w}),define(Q[172],J([0,1,94,320]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.GlyphMarginOverlay=e.DedupOverlay=e.DecorationToRender=void 0;class N{constructor(C,d,g){this.startLineNumber=+C,this.endLineNumber=+d,this.className=String(g)}}e.DecorationToRender=N;class M extends b.DynamicViewOverlay{_render(C,d,g){const p=[];for(let s=C;s<=d;s++){const a=s-C;p[a]=[]}if(g.length===0)return p;g.sort((s,a)=>s.className===a.className?s.startLineNumber===a.startLineNumber?s.endLineNumber-a.endLineNumber:s.startLineNumber-a.startLineNumber:s.className',u=[];for(let r=d;r<=g;r++){const i=r-d,n=p[i];n.length===0?u[i]="":u[i]='
    =this._renderResult.length?"":this._renderResult[g]}}e.GlyphMarginOverlay=w}),define(Q[392],J([0,1,172,324]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LinesDecorationsOverlay=void 0;class N extends b.DedupOverlay{constructor(w){super();this._context=w;const C=this._context.configuration.options.get(124);this._decorationsLeft=C.decorationsLeft,this._decorationsWidth=C.decorationsWidth,this._renderResult=null,this._context.addEventHandler(this)}dispose(){this._context.removeEventHandler(this),this._renderResult=null,super.dispose()}onConfigurationChanged(w){const C=this._context.configuration.options.get(124);return this._decorationsLeft=C.decorationsLeft,this._decorationsWidth=C.decorationsWidth,!0}onDecorationsChanged(w){return!0}onFlushed(w){return!0}onLinesChanged(w){return!0}onLinesDeleted(w){return!0}onLinesInserted(w){return!0}onScrollChanged(w){return w.scrollTopChanged}onZonesChanged(w){return!0}_getDecorations(w){const S=w.getDecorationsInViewport();let C=[],d=0;for(let g=0,p=S.length;g
    ',o=[];for(let s=S;s<=C;s++){const a=s-S,u=d[a];let r="";for(let i=0,n=u.length;i';g[c]=s}this._renderResult=g}render(w,S){return this._renderResult?this._renderResult[S-w]:""}}e.MarginViewLineDecorationsOverlay=N}),define(Q[394],J([0,1,30,45,327]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewOverlayWidgets=void 0;class M extends N.ViewPart{constructor(S){super(S);const d=this._context.configuration.options.get(124);this._widgets={},this._verticalScrollbarWidth=d.verticalScrollbarWidth,this._minimapWidth=d.minimap.minimapWidth,this._horizontalScrollbarHeight=d.horizontalScrollbarHeight,this._editorHeight=d.height,this._editorWidth=d.width,this._domNode=b.createFastDomNode(document.createElement("div")),N.PartFingerprints.write(this._domNode,4),this._domNode.setClassName("overlayWidgets")}dispose(){super.dispose(),this._widgets={}}getDomNode(){return this._domNode}onConfigurationChanged(S){const d=this._context.configuration.options.get(124);return this._verticalScrollbarWidth=d.verticalScrollbarWidth,this._minimapWidth=d.minimap.minimapWidth,this._horizontalScrollbarHeight=d.horizontalScrollbarHeight,this._editorHeight=d.height,this._editorWidth=d.width,!0}addWidget(S){const C=b.createFastDomNode(S.getDomNode());this._widgets[S.getId()]={widget:S,preference:null,domNode:C},C.setPosition("absolute"),C.setAttribute("widgetId",S.getId()),this._domNode.appendChild(C),this.setShouldRender()}setWidgetPosition(S,C){const d=this._widgets[S.getId()];return d.preference===C?!1:(d.preference=C,this.setShouldRender(),!0)}removeWidget(S){const C=S.getId();if(this._widgets.hasOwnProperty(C)){const g=this._widgets[C].domNode.domNode;delete this._widgets[C],g.parentNode.removeChild(g),this.setShouldRender()}}_renderWidget(S){const C=S.domNode;if(S.preference===null){C.unsetTop();return}if(S.preference===0)C.setTop(0),C.setRight(2*this._verticalScrollbarWidth+this._minimapWidth);else if(S.preference===1){const d=C.domNode.clientHeight;C.setTop(this._editorHeight-d-2*this._horizontalScrollbarHeight),C.setRight(2*this._verticalScrollbarWidth+this._minimapWidth)}else S.preference===2&&(C.setTop(0),C.domNode.style.right="50%")}prepareRender(S){}render(S){this._domNode.setWidth(this._editorWidth);const C=Object.keys(this._widgets);for(let d=0,g=C.length;dthis._context.viewLayout.getVerticalOffsetForLineNumber(p)),this._zoneManager.setDOMWidth(0),this._zoneManager.setDOMHeight(0),this._zoneManager.setOuterHeight(this._context.viewLayout.getScrollHeight()),this._zoneManager.setLineHeight(g.get(53)),this._zoneManager.setPixelRatio(g.get(122)),this._context.addEventHandler(this)}dispose(){this._context.removeEventHandler(this),super.dispose()}onConfigurationChanged(C){const d=this._context.configuration.options;return C.hasChanged(53)&&(this._zoneManager.setLineHeight(d.get(53)),this._render()),C.hasChanged(122)&&(this._zoneManager.setPixelRatio(d.get(122)),this._domNode.setWidth(this._zoneManager.getDOMWidth()),this._domNode.setHeight(this._zoneManager.getDOMHeight()),this._domNode.domNode.width=this._zoneManager.getCanvasWidth(),this._domNode.domNode.height=this._zoneManager.getCanvasHeight(),this._render()),!0}onFlushed(C){return this._render(),!0}onScrollChanged(C){return C.scrollHeightChanged&&(this._zoneManager.setOuterHeight(C.scrollHeight),this._render()),!0}onZonesChanged(C){return this._render(),!0}getDomNode(){return this._domNode.domNode}setLayout(C){this._domNode.setTop(C.top),this._domNode.setRight(C.right);let d=!1;d=this._zoneManager.setDOMWidth(C.width)||d,d=this._zoneManager.setDOMHeight(C.height)||d,d&&(this._domNode.setWidth(this._zoneManager.getDOMWidth()),this._domNode.setHeight(this._zoneManager.getDOMHeight()),this._domNode.domNode.width=this._zoneManager.getCanvasWidth(),this._domNode.domNode.height=this._zoneManager.getCanvasHeight(),this._render())}setZones(C){this._zoneManager.setZones(C),this._render()}_render(){if(this._zoneManager.getOuterHeight()===0)return!1;const C=this._zoneManager.getCanvasWidth(),d=this._zoneManager.getCanvasHeight(),g=this._zoneManager.resolveColorZones(),p=this._zoneManager.getId2Color(),c=this._domNode.domNode.getContext("2d");return c.clearRect(0,0,C,d),g.length>0&&this._renderOneLane(c,g,p,C),!0}_renderOneLane(C,d,g,p){let c=0,o=0,s=0;for(const a of d){const u=a.colorId,r=a.from,i=a.to;u!==c?(C.fillRect(0,o,p,s-o),c=u,C.fillStyle=g[c],o=r,s=i):s>=r?s=Math.max(s,i):(C.fillRect(0,o,p,s-o),o=r,s=i)}C.fillRect(0,o,p,s-o)}}e.OverviewRuler=w}),define(Q[396],J([0,1,30,12,45,14]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewZones=void 0;const S=()=>{throw new Error("Invalid change accessor")};class C extends M.ViewPart{constructor(p){super(p);const c=this._context.configuration.options,o=c.get(124);this._lineHeight=c.get(53),this._contentWidth=o.contentWidth,this._contentLeft=o.contentLeft,this.domNode=b.createFastDomNode(document.createElement("div")),this.domNode.setClassName("view-zones"),this.domNode.setPosition("absolute"),this.domNode.setAttribute("role","presentation"),this.domNode.setAttribute("aria-hidden","true"),this.marginDomNode=b.createFastDomNode(document.createElement("div")),this.marginDomNode.setClassName("margin-view-zones"),this.marginDomNode.setPosition("absolute"),this.marginDomNode.setAttribute("role","presentation"),this.marginDomNode.setAttribute("aria-hidden","true"),this._zones={}}dispose(){super.dispose(),this._zones={}}_recomputeWhitespacesProps(){const p=this._context.viewLayout.getWhitespaces(),c=new Map;for(const s of p)c.set(s.id,s);let o=!1;return this._context.model.changeWhitespace(s=>{const a=Object.keys(this._zones);for(let u=0,r=a.length;u{const s={addZone:a=>(c=!0,this._addZone(o,a)),removeZone:a=>{!a||(c=this._removeZone(o,a)||c)},layoutZone:a=>{!a||(c=this._layoutZone(o,a)||c)}};d(p,s),s.addZone=S,s.removeZone=S,s.layoutZone=S}),c}_addZone(p,c){const o=this._computeWhitespaceProps(c),a={whitespaceId:p.insertWhitespace(o.afterViewLineNumber,this._getZoneOrdinal(c),o.heightInPx,o.minWidthInPx),delegate:c,isVisible:!1,domNode:b.createFastDomNode(c.domNode),marginDomNode:c.marginDomNode?b.createFastDomNode(c.marginDomNode):null};return this._safeCallOnComputedHeight(a.delegate,o.heightInPx),a.domNode.setPosition("absolute"),a.domNode.domNode.style.width="100%",a.domNode.setDisplay("none"),a.domNode.setAttribute("monaco-view-zone",a.whitespaceId),this.domNode.appendChild(a.domNode),a.marginDomNode&&(a.marginDomNode.setPosition("absolute"),a.marginDomNode.domNode.style.width="100%",a.marginDomNode.setDisplay("none"),a.marginDomNode.setAttribute("monaco-view-zone",a.whitespaceId),this.marginDomNode.appendChild(a.marginDomNode)),this._zones[a.whitespaceId]=a,this.setShouldRender(),a.whitespaceId}_removeZone(p,c){if(this._zones.hasOwnProperty(c)){const o=this._zones[c];return delete this._zones[c],p.removeWhitespace(o.whitespaceId),o.domNode.removeAttribute("monaco-visible-view-zone"),o.domNode.removeAttribute("monaco-view-zone"),o.domNode.domNode.parentNode.removeChild(o.domNode.domNode),o.marginDomNode&&(o.marginDomNode.removeAttribute("monaco-visible-view-zone"),o.marginDomNode.removeAttribute("monaco-view-zone"),o.marginDomNode.domNode.parentNode.removeChild(o.marginDomNode.domNode)),this.setShouldRender(),!0}return!1}_layoutZone(p,c){if(this._zones.hasOwnProperty(c)){const o=this._zones[c],s=this._computeWhitespaceProps(o.delegate);return p.changeOneWhitespace(o.whitespaceId,s.afterViewLineNumber,s.heightInPx),this._safeCallOnComputedHeight(o.delegate,s.heightInPx),this.setShouldRender(),!0}return!1}shouldSuppressMouseDownOnViewZone(p){if(this._zones.hasOwnProperty(p)){const c=this._zones[p];return Boolean(c.delegate.suppressMouseDown)}return!1}_heightInPixels(p){return typeof p.heightInPx=="number"?p.heightInPx:typeof p.heightInLines=="number"?this._lineHeight*p.heightInLines:this._lineHeight}_minWidthInPixels(p){return typeof p.minWidthInPx=="number"?p.minWidthInPx:0}_safeCallOnComputedHeight(p,c){if(typeof p.onComputedHeight=="function")try{p.onComputedHeight(c)}catch(o){N.onUnexpectedError(o)}}_safeCallOnDomNodeTop(p,c){if(typeof p.onDomNodeTop=="function")try{p.onDomNodeTop(c)}catch(o){N.onUnexpectedError(o)}}prepareRender(p){}render(p){const c=p.viewportData.whitespaceViewportData,o={};let s=!1;for(let u=0,r=c.length;u0?o[r-1]:0,s=n)a=r+1;else break}return new M(r,s-i)}}e.LineBreakData=w;class S{constructor(o,s){this.tabSize=o,this.data=s}}e.MinimapLinesRenderingData=S;class C{constructor(o,s,a,u,r,i){this.content=o,this.continuesWithWrappedLine=s,this.minColumn=a,this.maxColumn=u,this.startVisibleColumn=r,this.tokens=i}}e.ViewLineData=C;class d{constructor(o,s,a,u,r,i,n,t,l,h){this.minColumn=o,this.maxColumn=s,this.content=a,this.continuesWithWrappedLine=u,this.isBasicASCII=d.isBasicASCII(a,i),this.containsRTL=d.containsRTL(a,this.isBasicASCII,r),this.tokens=n,this.inlineDecorations=t,this.tabSize=l,this.startVisibleColumn=h}static isBasicASCII(o,s){return s?b.isBasicASCII(o):!0}static containsRTL(o,s,a){return!s&&a?b.containsRTL(o):!1}}e.ViewLineRenderingData=d;class g{constructor(o,s,a){this.range=o,this.inlineClassName=s,this.type=a}}e.InlineDecoration=g;class p{constructor(o,s){this.range=o,this.options=s}}e.ViewModelDecoration=p}),define(Q[397],J([0,1,8,91,63]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MonospaceLineBreaksComputerFactory=void 0;class w extends N.CharacterClassifier{constructor(r,i){super(0);for(let n=0;n=0&&r<256?this._asciiMap[r]:r>=12352&&r<=12543||r>=13312&&r<=19903||r>=19968&&r<=40959?3:this._map.get(r)||this._defaultValue}}let S=[],C=[];class d{constructor(r,i){this.classifier=new w(r,i)}static create(r){return new d(r.get(113),r.get(112))}createLineBreaksComputer(r,i,n,t){i=i|0,n=+n;let l=[],h=[];return{addRequest:(m,_)=>{l.push(m),h.push(_)},finalize:()=>{const m=r.typicalFullwidthCharacterWidth/r.typicalHalfwidthCharacterWidth;let _=[];for(let f=0,v=l.length;f=0){let F=Math.abs(f[B]-O);for(;B+1=F)break;F=D,B++}}for(;BF&&(F=E,D=T);let R=0,W=0,x=0,K=0;if(D<=O){let ee=D,se=F===0?0:i.charCodeAt(F-1),ne=F===0?0:u.get(se),le=!0;for(let X=F;XE&&s(se,ne,P,V)&&(R=z,W=ee),ee+=U,ee>O){z>E?(x=z,K=ee-U):(x=X+1,K=ee),ee-W>y&&(R=0),le=!1;break}se=P,ne=V}if(le){k>0&&(L[k]=_[_.length-1],I[k]=f[_.length-1],k++);break}}if(R===0){let ee=D,se=i.charCodeAt(F),ne=u.get(se),le=!1;for(let X=F-1;X>=E;X--){const z=X+1,P=i.charCodeAt(X);if(P===9){le=!0;break}let V,U;if(b.isLowSurrogate(P)?(X--,V=0,U=2):(V=u.get(P),U=b.isFullWidthCharacter(P)?l:1),ee<=O){if(x===0&&(x=z,K=ee),ee<=O-y)break;if(s(P,V,se,ne)){R=z,W=ee;break}}ee-=U,se=P,ne=V}if(R!==0){const X=y-(K-W);if(X<=n){const z=i.charCodeAt(x);let P;b.isHighSurrogate(z)?P=2:P=c(z,K,n,l),X-P<0&&(R=0)}}if(le){B--;continue}}if(R===0&&(R=x,W=K),R<=E){const ee=i.charCodeAt(E);b.isHighSurrogate(ee)?(R=E+2,W=T+2):(R=E+1,W=T+c(ee,T,n,l))}for(E=R,L[k]=R,T=W,I[k]=W,k++,O=W+y;B<0||B=Y)break;Y=ee,B++}}return k===0?null:(L.length=k,I.length=k,S=r.breakOffsets,C=r.breakOffsetsVisibleColumn,r.breakOffsets=L,r.breakOffsetsVisibleColumn=I,r.wrappedTextIndentLength=v,r)}function p(u,r,i,n,t,l){if(n===-1)return null;const h=r.length;if(h<=1)return null;const m=a(r,i,n,t,l),_=n-m;let f=[],v=[],y=0,L=0,I=0,k=n,E=r.charCodeAt(0),T=u.get(E),O=c(E,0,i,t),A=1;b.isHighSurrogate(E)&&(O+=1,E=r.charCodeAt(1),T=u.get(E),A++);for(let B=A;Bk&&((L===0||O-I>_)&&(L=F,I=O-W),f[y]=L,v[y]=I,y++,k=I+_,L=0),E=D,T=R}return y===0?null:(f[y]=h,v[y]=O,new M.LineBreakData(f,v,m))}function c(u,r,i,n){return u===9?i-r%i:b.isFullWidthCharacter(u)||u<32?n:1}function o(u,r){return r-u%r}function s(u,r,i,n){return i!==32&&(r===2||r===3&&n!==2||n===1||n===3&&r!==1)}function a(u,r,i,n,t){let l=0;if(t!==0){const h=b.firstNonWhitespaceIndex(u);if(h!==-1){for(let _=0;_i&&(l=0)}}return l}}),define(Q[173],J([0,1,6,2]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ReadOnlyEditAttemptEvent=e.CursorStateChangedEvent=e.ViewZonesChangedEvent=e.ScrollChangedEvent=e.FocusChangedEvent=e.ContentSizeChangedEvent=e.ViewModelEventsCollector=e.ViewModelEventDispatcher=void 0;class M extends N.Disposable{constructor(){super();this._onEvent=this._register(new b.Emitter),this.onEvent=this._onEvent.event,this._eventHandlers=[],this._viewEventQueue=null,this._isConsumingViewEventQueue=!1,this._collector=null,this._collectorCnt=0,this._outgoingEvents=[]}emitOutgoingEvent(s){this._addOutgoingEvent(s),this._emitOugoingEvents()}_addOutgoingEvent(s){for(let a=0,u=this._outgoingEvents.length;a0;){if(this._collector||this._isConsumingViewEventQueue)return;const s=this._outgoingEvents.shift();s.isNoOp()||this._onEvent.fire(s)}}addViewEventHandler(s){for(let a=0,u=this._eventHandlers.length;a0&&this._emitMany(a)}this._emitOugoingEvents()}emitSingleViewEvent(s){try{this.beginEmitViewEvents().emitViewEvent(s)}finally{this.endEmitViewEvents()}}_emitMany(s){this._viewEventQueue?this._viewEventQueue=this._viewEventQueue.concat(s):this._viewEventQueue=s,this._isConsumingViewEventQueue||this._consumeViewEventQueue()}_consumeViewEventQueue(){try{this._isConsumingViewEventQueue=!0,this._doConsumeQueue()}finally{this._isConsumingViewEventQueue=!1}}_doConsumeQueue(){for(;this._viewEventQueue;){const s=this._viewEventQueue;this._viewEventQueue=null;const a=this._eventHandlers.slice(0);for(const u of a)u.handleEvents(s)}}}e.ViewModelEventDispatcher=M;class w{constructor(){this.viewEvents=[],this.outgoingEvents=[]}emitViewEvent(s){this.viewEvents.push(s)}emitOutgoingEvent(s){this.outgoingEvents.push(s)}}e.ViewModelEventsCollector=w;class S{constructor(s,a,u,r){this.kind=0,this._oldContentWidth=s,this._oldContentHeight=a,this.contentWidth=u,this.contentHeight=r,this.contentWidthChanged=this._oldContentWidth!==this.contentWidth,this.contentHeightChanged=this._oldContentHeight!==this.contentHeight}isNoOp(){return!this.contentWidthChanged&&!this.contentHeightChanged}merge(s){return s.kind!==0?this:new S(this._oldContentWidth,this._oldContentHeight,s.contentWidth,s.contentHeight)}}e.ContentSizeChangedEvent=S;class C{constructor(s,a){this.kind=1,this.oldHasFocus=s,this.hasFocus=a}isNoOp(){return this.oldHasFocus===this.hasFocus}merge(s){return s.kind!==1?this:new C(this.oldHasFocus,s.hasFocus)}}e.FocusChangedEvent=C;class d{constructor(s,a,u,r,i,n,t,l){this.kind=2,this._oldScrollWidth=s,this._oldScrollLeft=a,this._oldScrollHeight=u,this._oldScrollTop=r,this.scrollWidth=i,this.scrollLeft=n,this.scrollHeight=t,this.scrollTop=l,this.scrollWidthChanged=this._oldScrollWidth!==this.scrollWidth,this.scrollLeftChanged=this._oldScrollLeft!==this.scrollLeft,this.scrollHeightChanged=this._oldScrollHeight!==this.scrollHeight,this.scrollTopChanged=this._oldScrollTop!==this.scrollTop}isNoOp(){return!this.scrollWidthChanged&&!this.scrollLeftChanged&&!this.scrollHeightChanged&&!this.scrollTopChanged}merge(s){return s.kind!==2?this:new d(this._oldScrollWidth,this._oldScrollLeft,this._oldScrollHeight,this._oldScrollTop,s.scrollWidth,s.scrollLeft,s.scrollHeight,s.scrollTop)}}e.ScrollChangedEvent=d;class g{constructor(){this.kind=3}isNoOp(){return!1}merge(s){return this}}e.ViewZonesChangedEvent=g;class p{constructor(s,a,u,r,i,n,t){this.kind=5,this.oldSelections=s,this.selections=a,this.oldModelVersionId=u,this.modelVersionId=r,this.source=i,this.reason=n,this.reachedMaxCursorCount=t}static _selectionsAreEqual(s,a){if(!s&&!a)return!0;if(!s||!a)return!1;const u=s.length,r=a.length;if(u!==r)return!1;for(let i=0;i=a?0:r.horizontalScrollbarSize}_getContentHeight(s,a,u){const r=this._configuration.options;let i=this._linesLayout.getLinesTotalHeight();return r.get(89)?i+=Math.max(0,a-r.get(53)-r.get(69).bottom):i+=this._getHorizontalScrollbarHeight(s,u),i}_updateHeight(){const s=this._scrollable.getScrollDimensions(),a=s.width,u=s.height,r=s.contentWidth;this._scrollable.setScrollDimensions(new g(a,s.contentWidth,u,this._getContentHeight(a,u,r)))}getCurrentViewport(){const s=this._scrollable.getScrollDimensions(),a=this._scrollable.getCurrentScrollPosition();return new S.Viewport(a.scrollTop,a.scrollLeft,s.width,s.height)}getFutureViewport(){const s=this._scrollable.getScrollDimensions(),a=this._scrollable.getFutureScrollPosition();return new S.Viewport(a.scrollTop,a.scrollLeft,s.width,s.height)}_computeContentWidth(s){const a=this._configuration.options,u=a.get(125),r=a.get(38);if(u.isViewportWrapping){const i=a.get(124),n=a.get(59);return s>i.contentWidth+r.typicalHalfwidthCharacterWidth&&n.enabled&&n.side==="right"?s+i.verticalScrollbarWidth:s}else{const i=a.get(88)*r.typicalHalfwidthCharacterWidth,n=this._linesLayout.getWhitespaceMinWidth();return Math.max(s+i,n)}}setMaxLineWidth(s){const a=this._scrollable.getScrollDimensions();this._scrollable.setScrollDimensions(new g(a.width,this._computeContentWidth(s),a.height,a.contentHeight)),this._updateHeight()}saveState(){const s=this._scrollable.getFutureScrollPosition();let a=s.scrollTop,u=this._linesLayout.getLineNumberAtOrAfterVerticalOffset(a),r=this._linesLayout.getWhitespaceAccumulatedHeightBeforeLineNumber(u);return{scrollTop:a,scrollTopWithoutViewZones:a-r,scrollLeft:s.scrollLeft}}changeWhitespace(s){const a=this._linesLayout.changeWhitespace(s);return a&&this.onHeightMaybeChanged(),a}getVerticalOffsetForLineNumber(s){return this._linesLayout.getVerticalOffsetForLineNumber(s)}isAfterLines(s){return this._linesLayout.isAfterLines(s)}isInTopPadding(s){return this._linesLayout.isInTopPadding(s)}isInBottomPadding(s){return this._linesLayout.isInBottomPadding(s)}getLineNumberAtVerticalOffset(s){return this._linesLayout.getLineNumberAtOrAfterVerticalOffset(s)}getWhitespaceAtVerticalOffset(s){return this._linesLayout.getWhitespaceAtVerticalOffset(s)}getLinesViewportData(){const s=this.getCurrentViewport();return this._linesLayout.getLinesViewportData(s.top,s.top+s.height)}getLinesViewportDataAtScrollTop(s){const a=this._scrollable.getScrollDimensions();return s+a.height>a.scrollHeight&&(s=a.scrollHeight-a.height),s<0&&(s=0),this._linesLayout.getLinesViewportData(s,s+a.height)}getWhitespaceViewportData(){const s=this.getCurrentViewport();return this._linesLayout.getWhitespaceViewportData(s.top,s.top+s.height)}getWhitespaces(){return this._linesLayout.getWhitespaces()}getContentWidth(){return this._scrollable.getScrollDimensions().contentWidth}getScrollWidth(){return this._scrollable.getScrollDimensions().scrollWidth}getContentHeight(){return this._scrollable.getScrollDimensions().contentHeight}getScrollHeight(){return this._scrollable.getScrollDimensions().scrollHeight}getCurrentScrollLeft(){return this._scrollable.getCurrentScrollPosition().scrollLeft}getCurrentScrollTop(){return this._scrollable.getCurrentScrollPosition().scrollTop}validateScrollPosition(s){return this._scrollable.validateScrollPosition(s)}setScrollPosition(s,a){a===1?this._scrollable.setScrollPositionNow(s):this._scrollable.setScrollPositionSmooth(s)}deltaScrollNow(s,a){const u=this._scrollable.getCurrentScrollPosition();this._scrollable.setScrollPositionNow({scrollLeft:u.scrollLeft+s,scrollTop:u.scrollTop+a})}}e.ViewLayout=c}),define(Q[399],J([0,1,3,21]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MoveCaretCommand=void 0;class M{constructor(S,C){this._selection=S,this._isMovingLeft=C}getEditOperations(S,C){if(!(this._selection.startLineNumber!==this._selection.endLineNumber||this._selection.isEmpty())){const d=this._selection.startLineNumber,g=this._selection.startColumn,p=this._selection.endColumn;if(!(this._isMovingLeft&&g===1)&&!(!this._isMovingLeft&&p===S.getLineMaxColumn(d)))if(this._isMovingLeft){const c=new b.Range(d,g-1,d,g),o=S.getValueInRange(c);C.addEditOperation(c,null),C.addEditOperation(new b.Range(d,p,d,p),o)}else{const c=new b.Range(d,p,d,p+1),o=S.getValueInRange(c);C.addEditOperation(c,null),C.addEditOperation(new b.Range(d,g,d,g),o)}}}computeCursorState(S,C){return this._isMovingLeft?new N.Selection(this._selection.startLineNumber,this._selection.startColumn-1,this._selection.endLineNumber,this._selection.endColumn-1):new N.Selection(this._selection.startLineNumber,this._selection.startColumn+1,this._selection.endLineNumber,this._selection.endColumn+1)}}e.MoveCaretCommand=M}),define(Q[130],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CodeActionCommandArgs=e.filtersAction=e.mayIncludeActionsOfKind=e.CodeActionKind=void 0;class b{constructor(d){this.value=d}equals(d){return this.value===d.value}contains(d){return this.equals(d)||this.value===""||d.value.startsWith(this.value+b.sep)}intersects(d){return this.contains(d)||d.contains(this)}append(d){return new b(this.value+b.sep+d)}}e.CodeActionKind=b,b.sep=".",b.None=new b("@@none@@"),b.Empty=new b(""),b.QuickFix=new b("quickfix"),b.Refactor=new b("refactor"),b.Source=new b("source"),b.SourceOrganizeImports=b.Source.append("organizeImports"),b.SourceFixAll=b.Source.append("fixAll");function N(C,d){return!(C.include&&!C.include.intersects(d)||C.excludes&&C.excludes.some(g=>w(d,g,C.include))||!C.includeSourceActions&&b.Source.contains(d))}e.mayIncludeActionsOfKind=N;function M(C,d){const g=d.kind?new b(d.kind):void 0;return!(C.include&&(!g||!C.include.contains(g))||C.excludes&&g&&C.excludes.some(p=>w(g,p,C.include))||!C.includeSourceActions&&g&&b.Source.contains(g)||C.onlyIncludePreferredActions&&!d.isPreferred)}e.filtersAction=M;function w(C,d,g){return!(!d.contains(C)||g&&d.contains(g))}class S{constructor(d,g,p){this.kind=d,this.apply=g,this.preferred=p}static fromUser(d,g){return!d||typeof d!="object"?new S(g.kind,g.apply,!1):new S(S.getKindFromUser(d,g.kind),S.getApplyFromUser(d,g.apply),S.getPreferredUser(d))}static getApplyFromUser(d,g){switch(typeof d.apply=="string"?d.apply.toLowerCase():""){case"first":return"first";case"never":return"never";case"ifsingle":return"ifSingle";default:return g}}static getKindFromUser(d,g){return typeof d.kind=="string"?new b(d.kind):g}static getPreferredUser(d){return typeof d.preferred=="boolean"?d.preferred:!1}}e.CodeActionCommandArgs=S}),define(Q[400],J([0,1,6]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ColorPickerModel=void 0;class N{constructor(w,S,C){this.presentationIndex=C,this._onColorFlushed=new b.Emitter,this.onColorFlushed=this._onColorFlushed.event,this._onDidChangeColor=new b.Emitter,this.onDidChangeColor=this._onDidChangeColor.event,this._onDidChangePresentation=new b.Emitter,this.onDidChangePresentation=this._onDidChangePresentation.event,this.originalColor=w,this._color=w,this._colorPresentations=S}get color(){return this._color}set color(w){this._color.equals(w)||(this._color=w,this._onDidChangeColor.fire(w))}get presentation(){return this.colorPresentations[this.presentationIndex]}get colorPresentations(){return this._colorPresentations}set colorPresentations(w){this._colorPresentations=w,this.presentationIndex>w.length-1&&(this.presentationIndex=0),this._onDidChangePresentation.fire(this.presentation)}selectNextColorPresentation(){this.presentationIndex=(this.presentationIndex+1)%this.colorPresentations.length,this.flushColor(),this._onDidChangePresentation.fire(this.presentation)}guessColorPresentation(w,S){for(let C=0;Cs)return!1;for(let a=0;a=65&&u<=90&&u+32===r)&&!(r>=65&&r<=90&&r+32===u))return!1}return!0}_createOperationsForBlockComment(g,p,c,o,s,a){const u=g.startLineNumber,r=g.startColumn,i=g.endLineNumber,n=g.endColumn,t=s.getLineContent(u),l=s.getLineContent(i);let h=t.lastIndexOf(p,r-1+p.length),m=l.indexOf(c,n-1-c.length);if(h!==-1&&m!==-1)if(u===i)t.substring(h+p.length,m).indexOf(c)>=0&&(h=-1,m=-1);else{const f=t.substring(h+p.length),v=l.substring(0,m);(f.indexOf(c)>=0||v.indexOf(c)>=0)&&(h=-1,m=-1)}let _;h!==-1&&m!==-1?(o&&h+p.length0&&l.charCodeAt(m-1)===32&&(c=" "+c,m-=1),_=C._createRemoveBlockCommentOperations(new M.Range(u,h+p.length+1,i,m+1),p,c)):(_=C._createAddBlockCommentOperations(g,p,c,this._insertSpace),this._usedEndToken=_.length===1?c:null);for(const f of _)a.addTrackedEditOperation(f.range,f.text)}static _createRemoveBlockCommentOperations(g,p,c){let o=[];return M.Range.isEmpty(g)?o.push(b.EditOperation.delete(new M.Range(g.startLineNumber,g.startColumn-p.length,g.endLineNumber,g.endColumn+c.length))):(o.push(b.EditOperation.delete(new M.Range(g.startLineNumber,g.startColumn-p.length,g.startLineNumber,g.startColumn))),o.push(b.EditOperation.delete(new M.Range(g.endLineNumber,g.endColumn,g.endLineNumber,g.endColumn+c.length)))),o}static _createAddBlockCommentOperations(g,p,c,o){let s=[];return M.Range.isEmpty(g)?s.push(b.EditOperation.replace(new M.Range(g.startLineNumber,g.startColumn,g.endLineNumber,g.endColumn),p+" "+c)):(s.push(b.EditOperation.insert(new N.Position(g.startLineNumber,g.startColumn),p+(o?" ":""))),s.push(b.EditOperation.insert(new N.Position(g.endLineNumber,g.endColumn),(o?" ":"")+c))),s}getEditOperations(g,p){const c=this._selection.startLineNumber,o=this._selection.startColumn;g.tokenizeIfCheap(c);const s=g.getLanguageIdAtPosition(c,o),a=S.LanguageConfigurationRegistry.getComments(s);!a||!a.blockCommentStartToken||!a.blockCommentEndToken||this._createOperationsForBlockComment(this._selection,a.blockCommentStartToken,a.blockCommentEndToken,this._insertSpace,g,p)}computeCursorState(g,p){const c=p.getInverseEditOperations();if(c.length===2){const o=c[0],s=c[1];return new w.Selection(o.range.endLineNumber,o.range.endColumn,s.range.startLineNumber,s.range.startColumn)}else{const o=c[0].range,s=this._usedEndToken?-this._usedEndToken.length-1:0;return new w.Selection(o.endLineNumber,o.endColumn+s,o.endLineNumber,o.endColumn+s)}}}e.BlockCommentCommand=C}),define(Q[401],J([0,1,8,62,14,3,21,41,224]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LineCommentCommand=void 0;class g{constructor(c,o,s,a,u,r){this._selection=c,this._tabSize=o,this._type=s,this._insertSpace=a,this._selectionId=null,this._deltaColumn=0,this._moveEndPositionDown=!1,this._ignoreEmptyLines=u,this._ignoreFirstLine=r||!1}static _gatherPreflightCommentStrings(c,o,s){c.tokenizeIfCheap(o);const a=c.getLanguageIdAtPosition(o,1),u=C.LanguageConfigurationRegistry.getComments(a),r=u?u.lineCommentToken:null;if(!r)return null;let i=[];for(let n=0,t=s-o+1;nu?o[n].commentStrOffset=r-1:o[n].commentStrOffset=r}}}e.LineCommentCommand=g}),define(Q[402],J([0,1,21,3]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DragAndDropCommand=void 0;class M{constructor(S,C,d){this.selection=S,this.targetPosition=C,this.copy=d,this.targetSelection=null}getEditOperations(S,C){let d=S.getValueInRange(this.selection);if(this.copy||C.addEditOperation(this.selection,null),C.addEditOperation(new N.Range(this.targetPosition.lineNumber,this.targetPosition.column,this.targetPosition.lineNumber,this.targetPosition.column),d),this.selection.containsPosition(this.targetPosition)&&!(this.copy&&(this.selection.getEndPosition().equals(this.targetPosition)||this.selection.getStartPosition().equals(this.targetPosition)))){this.targetSelection=this.selection;return}if(this.copy){this.targetSelection=new b.Selection(this.targetPosition.lineNumber,this.targetPosition.column,this.selection.endLineNumber-this.selection.startLineNumber+this.targetPosition.lineNumber,this.selection.startLineNumber===this.selection.endLineNumber?this.targetPosition.column+this.selection.endColumn-this.selection.startColumn:this.selection.endColumn);return}if(this.targetPosition.lineNumber>this.selection.endLineNumber){this.targetSelection=new b.Selection(this.targetPosition.lineNumber-this.selection.endLineNumber+this.selection.startLineNumber,this.targetPosition.column,this.targetPosition.lineNumber,this.selection.startLineNumber===this.selection.endLineNumber?this.targetPosition.column+this.selection.endColumn-this.selection.startColumn:this.selection.endColumn);return}if(this.targetPosition.lineNumber0){let C=[];for(let p=0;pb.Range.compareRangesUsingStarts(p.range,c.range));let d=[],g=C[0];for(let p=1;p0){let i=[],n=u.caseOps.length,t=0;for(let l=0,h=r.length;l=n){i.push(r.slice(l));break}switch(u.caseOps[t]){case"U":i.push(r[l].toUpperCase());break;case"u":i.push(r[l].toUpperCase()),t++;break;case"L":i.push(r[l].toLowerCase());break;case"l":i.push(r[l].toLowerCase()),t++;break;default:i.push(r[l])}}r=i.join("")}o+=r}return o}static _substitute(p,c){if(c===null)return"";if(p===0)return c[0];let o="";for(;p>0;){if(p=s)break;let u=g.charCodeAt(o);switch(u){case 92:c.emitUnchanged(o-1),c.emitStatic("\\",o+1);break;case 110:c.emitUnchanged(o-1),c.emitStatic(` +`,o+1);break;case 116:c.emitUnchanged(o-1),c.emitStatic(" ",o+1);break;case 117:case 85:case 108:case 76:c.emitUnchanged(o-1),c.emitStatic("",o+1),p.push(String.fromCharCode(u));break}continue}if(a===36){if(o++,o>=s)break;let u=g.charCodeAt(o);if(u===36){c.emitUnchanged(o-1),c.emitStatic("$",o+1);continue}if(u===48||u===38){c.emitUnchanged(o-1),c.emitMatchIndex(0,o+1,p),p.length=0;continue}if(49<=u&&u<=57){let r=u-48;if(o+1e.MAX_FOLDING_REGIONS)throw new Error("invalid startIndexes or endIndexes size");this._startIndexes=S,this._endIndexes=C,this._collapseStates=new Uint32Array(Math.ceil(S.length/32)),this._types=d,this._parentsComputed=!1}ensureParentIndices(){if(!this._parentsComputed){this._parentsComputed=!0;let S=[],C=(d,g)=>{let p=S[S.length-1];return this.getStartLineNumber(p)<=d&&this.getEndLineNumber(p)>=g};for(let d=0,g=this._startIndexes.length;de.MAX_LINE_NUMBER||c>e.MAX_LINE_NUMBER)throw new Error("startLineNumber or endLineNumber must not exceed "+e.MAX_LINE_NUMBER);for(;S.length>0&&!C(p,c);)S.pop();let o=S.length>0?S[S.length-1]:-1;S.push(d),this._startIndexes[d]=p+((o&255)<<24),this._endIndexes[d]=c+((o&65280)<<16)}}}get length(){return this._startIndexes.length}getStartLineNumber(S){return this._startIndexes[S]&e.MAX_LINE_NUMBER}getEndLineNumber(S){return this._endIndexes[S]&e.MAX_LINE_NUMBER}getType(S){return this._types?this._types[S]:void 0}hasTypes(){return!!this._types}isCollapsed(S){let C=S/32|0,d=S%32;return(this._collapseStates[C]&1<>>24)+((this._endIndexes[S]&b)>>>16);return C===e.MAX_FOLDING_REGIONS?-1:C}contains(S,C){return this.getStartLineNumber(S)<=C&&this.getEndLineNumber(S)>=C}findIndex(S){let C=0,d=this._startIndexes.length;if(d===0)return-1;for(;C=0){if(this.getEndLineNumber(C)>=S)return C;for(C=this.getParentIndex(C);C!==-1;){if(this.contains(C,S))return C;C=this.getParentIndex(C)}}return-1}toString(){let S=[];for(let C=0;C=this.endLineNumber}containsLine(S){return this.startLineNumber<=S&&S<=this.endLineNumber}}e.FoldingRegion=M}),define(Q[405],J([0,1,6,174]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.setCollapseStateForType=e.setCollapseStateForMatchingLines=e.setCollapseStateAtLevel=e.setCollapseStateUp=e.setCollapseStateLevelsUp=e.setCollapseStateLevelsDown=e.toggleCollapseState=e.FoldingModel=void 0;class M{constructor(s,a){this._updateEventEmitter=new b.Emitter,this.onDidChange=this._updateEventEmitter.event,this._textModel=s,this._decorationProvider=a,this._regions=new N.FoldingRegions(new Uint32Array(0),new Uint32Array(0)),this._editorDecorationIds=[],this._isInitialized=!1}get regions(){return this._regions}get textModel(){return this._textModel}get isInitialized(){return this._isInitialized}toggleCollapseState(s){if(!!s.length){s=s.sort((u,r)=>u.regionIndex-r.regionIndex);const a={};this._decorationProvider.changeDecorations(u=>{let r=0,i=-1,n=-1;const t=l=>{for(;rn&&(n=h),r++}};for(let l of s){let h=l.regionIndex,m=this._editorDecorationIds[h];if(m&&!a[m]){a[m]=!0,t(h);let _=!this._regions.isCollapsed(h);this._regions.setCollapsed(h,_),i=Math.max(i,this._regions.getEndLineNumber(h))}}t(this._regions.length)}),this._updateEventEmitter.fire({model:this,collapseStateChanged:s})}}update(s,a=[]){let u=[],r=(_,f)=>{for(let v of a)if(_{const v=s.getStartLineNumber(_),y=s.getEndLineNumber(_);f&&r(v,y)&&(f=!1),s.setCollapsed(_,f);const L=this._textModel.getLineMaxColumn(v),I={startLineNumber:v,startColumn:Math.max(L-1,1),endLineNumber:v,endColumn:L};u.push({range:I,options:this._decorationProvider.getDecorationOption(f,y<=i)}),f&&y>i&&(i=y)},t=0,l=()=>{for(;t=v)n(h,f===v),h++;else break}}m=l()}for(;h0)return s}applyMemento(s){if(!!Array.isArray(s)){let a=[];for(let u of s){let r=this.getRegionAtLine(u.startLineNumber);r&&!r.isCollapsed&&a.push(r)}this.toggleCollapseState(a)}}dispose(){this._decorationProvider.deltaDecorations(this._editorDecorationIds,[])}getAllRegionsAtLine(s,a){let u=[];if(this._regions){let r=this._regions.findRange(s),i=1;for(;r>=0;){let n=this._regions.toRegion(r);(!a||a(n,i))&&u.push(n),i++,r=n.parentIndex}}return u}getRegionAtLine(s){if(this._regions){let a=this._regions.findRange(s);if(a>=0)return this._regions.toRegion(a)}return null}getRegionsInside(s,a){let u=[],r=s?s.regionIndex+1:0,i=s?s.endLineNumber:Number.MAX_VALUE;if(a&&a.length===2){const n=[];for(let t=r,l=this._regions.length;t0&&!h.containedBy(n[n.length-1]);)n.pop();n.push(h),a(h,n.length)&&u.push(h)}else break}}else for(let n=r,t=this._regions.length;n1){let t=o.getRegionsInside(i,(l,h)=>l.isCollapsed!==n&&h0)for(let i of u){let n=o.getRegionAtLine(i);if(n&&(n.isCollapsed!==s&&r.push(n),a>1)){let t=o.getRegionsInside(n,(l,h)=>l.isCollapsed!==s&&hn.isCollapsed!==s&&tt.isCollapsed!==s&&l<=a);r.push(...n)}o.toggleCollapseState(r)}e.setCollapseStateLevelsUp=C;function d(o,s,a){let u=[];for(let r of a){let i=o.getAllRegionsAtLine(r,n=>n.isCollapsed!==s);i.length>0&&u.push(i[0])}o.toggleCollapseState(u)}e.setCollapseStateUp=d;function g(o,s,a,u){let r=(n,t)=>t===s&&n.isCollapsed!==a&&!u.some(l=>n.containsLine(l)),i=o.getRegionsInside(null,r);o.toggleCollapseState(i)}e.setCollapseStateAtLevel=g;function p(o,s,a){let u=o.textModel,r=o.regions,i=[];for(let n=r.length-1;n>=0;n--)if(a!==r.isCollapsed(n)){let t=r.getStartLineNumber(n);s.test(u.getLineContent(t))&&i.push(r.toRegion(n))}o.toggleCollapseState(i)}e.setCollapseStateForMatchingLines=p;function c(o,s,a){let u=o.regions,r=[];for(let i=u.length-1;i>=0;i--)a!==u.isCollapsed(i)&&s===u.getType(i)&&r.push(u.toRegion(i));o.toggleCollapseState(r)}e.setCollapseStateForType=c}),define(Q[406],J([0,1,6,3,19]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.HiddenRangeModel=void 0;class w{constructor(g){this._updateEventEmitter=new b.Emitter,this._foldingModel=g,this._foldingModelListener=g.onDidChange(p=>this.updateHiddenRanges()),this._hiddenRanges=[],g.regions.length&&this.updateHiddenRanges()}get onDidChange(){return this._updateEventEmitter.event}get hiddenRanges(){return this._hiddenRanges}updateHiddenRanges(){let g=!1,p=[],c=0,o=0,s=Number.MAX_VALUE,a=-1,u=this._foldingModel.regions;for(;c({startLineNumber:g.startLineNumber-1,endLineNumber:g.endLineNumber}))}applyHiddenRanges(g){this._hiddenRanges=g,this._updateEventEmitter.fire(g)}hasRanges(){return this._hiddenRanges.length>0}isHidden(g){return C(this._hiddenRanges,g)!==null}adjustSelections(g){let p=!1,c=this._foldingModel.textModel,o=null,s=a=>((!o||!S(a,o))&&(o=C(this._hiddenRanges,a)),o?o.startLineNumber-1:null);for(let a=0,u=g.length;a0&&(this._hiddenRanges=[],this._updateEventEmitter.fire(this._hiddenRanges)),this._foldingModelListener&&(this._foldingModelListener.dispose(),this._foldingModelListener=null)}}e.HiddenRangeModel=w;function S(d,g){return d>=g.startLineNumber&&d<=g.endLineNumber}function C(d,g){let p=M.findFirstInSorted(d,c=>g=0&&d[p].endLineNumber>=g?d[p]:null}}),define(Q[225],J([0,1,12,174,2]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.sanitizeRanges=e.RangesCollector=e.SyntaxRangeProvider=e.ID_SYNTAX_PROVIDER=void 0;const w=5e3,S={};e.ID_SYNTAX_PROVIDER="syntax";class C{constructor(o,s,a,u=w){this.editorModel=o,this.providers=s,this.limit=u,this.id=e.ID_SYNTAX_PROVIDER;for(const r of s)typeof r.onDidChange=="function"&&(this.disposables||(this.disposables=new M.DisposableStore),this.disposables.add(r.onDidChange(a)))}compute(o){return d(this.providers,this.editorModel,o).then(s=>s?p(s,this.limit):null)}dispose(){var o;(o=this.disposables)===null||o===void 0||o.dispose()}}e.SyntaxRangeProvider=C;function d(c,o,s){let a=null,u=c.map((r,i)=>Promise.resolve(r.provideFoldingRanges(o,S,s)).then(n=>{if(!s.isCancellationRequested&&Array.isArray(n)){Array.isArray(a)||(a=[]);let t=o.getLineCount();for(let l of n)l.start>0&&l.end>l.start&&l.end<=t&&a.push({start:l.start,end:l.end,rank:i,kind:l.kind})}},b.onUnexpectedExternalError));return Promise.all(u).then(r=>a)}class g{constructor(o){this._startIndexes=[],this._endIndexes=[],this._nestingLevels=[],this._nestingLevelCounts=[],this._types=[],this._length=0,this._foldingRangesLimit=o}add(o,s,a,u){if(!(o>N.MAX_LINE_NUMBER||s>N.MAX_LINE_NUMBER)){let r=this._length;this._startIndexes[r]=o,this._endIndexes[r]=s,this._nestingLevels[r]=u,this._types[r]=a,this._length++,u<30&&(this._nestingLevelCounts[u]=(this._nestingLevelCounts[u]||0)+1)}}toIndentRanges(){if(this._length<=this._foldingRangesLimit){let o=new Uint32Array(this._length),s=new Uint32Array(this._length);for(let a=0;athis._foldingRangesLimit){s=i;break}o+=n}}let a=new Uint32Array(this._foldingRangesLimit),u=new Uint32Array(this._foldingRangesLimit),r=[];for(let i=0,n=0;i{let t=i.start-n.start;return t===0&&(t=i.rank-n.rank),t}),a=new g(o),u,r=[];for(let i of s)if(!u)u=i,a.add(i.start,i.end,i.kind&&i.kind.value,r.length);else if(i.start>u.start)if(i.end<=u.end)r.push(u),u=i,a.add(i.start,i.end,i.kind&&i.kind.value,r.length);else{if(i.start>u.end){do u=r.pop();while(u&&i.start>u.end);u&&r.push(u),u=i}a.add(i.start,i.end,i.kind&&i.kind.value,r.length)}return a.toIndentRanges()}e.sanitizeRanges=p}),define(Q[407],J([0,1,225]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InitializingRangeProvider=e.ID_INIT_PROVIDER=void 0,e.ID_INIT_PROVIDER="init";class N{constructor(w,S,C,d){if(this.editorModel=w,this.id=e.ID_INIT_PROVIDER,S.length){let g=p=>({range:{startLineNumber:p.startLineNumber,startColumn:0,endLineNumber:p.endLineNumber,endColumn:w.getLineLength(p.endLineNumber)},options:{stickiness:1}});this.decorationIds=w.deltaDecorations([],S.map(g)),this.timeout=setTimeout(C,d)}}dispose(){this.decorationIds&&(this.editorModel.deltaDecorations(this.decorationIds,[]),this.decorationIds=void 0),typeof this.timeout=="number"&&(clearTimeout(this.timeout),this.timeout=void 0)}compute(w){let S=[];if(this.decorationIds)for(let C of this.decorationIds){let d=this.editorModel.getDecorationRange(C);d&&S.push({start:d.startLineNumber,end:d.endLineNumber,rank:1})}return Promise.resolve(b.sanitizeRanges(S,Number.MAX_VALUE))}}e.InitializingRangeProvider=N}),define(Q[226],J([0,1,62,3]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FormattingEdit=void 0;class M{static _handleEolEdits(S,C){let d,g=[];for(let p of C)typeof p.eol=="number"&&(d=p.eol),p.range&&typeof p.text=="string"&&g.push(p);return typeof d=="number"&&S.hasModel()&&S.getModel().pushEOL(d),g}static _isFullModelReplaceEdit(S,C){if(!S.hasModel())return!1;const d=S.getModel(),g=d.validateRange(C.range);return d.getFullModelRange().equalsRange(g)}static execute(S,C,d){d&&S.pushUndoStop();const g=M._handleEolEdits(S,C);g.length===1&&M._isFullModelReplaceEdit(S,g[0])?S.executeEdits("formatEditsCommand",g.map(p=>b.EditOperation.replace(N.Range.lift(p.range),p.text))):S.executeEdits("formatEditsCommand",g.map(p=>b.EditOperation.replaceMove(N.Range.lift(p.range),p.text))),d&&S.pushUndoStop()}}e.FormattingEdit=M}),define(Q[227],J([0,1,2,6,17]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ClickLinkGesture=e.ClickLinkOptions=e.ClickLinkKeyboardEvent=e.ClickLinkMouseEvent=void 0;function w(c,o){return!!c[o]}class S{constructor(o,s){this.target=o.target,this.hasTriggerModifier=w(o.event,s.triggerModifier),this.hasSideBySideModifier=w(o.event,s.triggerSideBySideModifier),this.isNoneOrSingleMouseDown=o.event.detail<=1}}e.ClickLinkMouseEvent=S;class C{constructor(o,s){this.keyCodeIsTriggerKey=o.keyCode===s.triggerKey,this.keyCodeIsSideBySideKey=o.keyCode===s.triggerSideBySideKey,this.hasTriggerModifier=w(o,s.triggerModifier)}}e.ClickLinkKeyboardEvent=C;class d{constructor(o,s,a,u){this.triggerKey=o,this.triggerModifier=s,this.triggerSideBySideKey=a,this.triggerSideBySideModifier=u}equals(o){return this.triggerKey===o.triggerKey&&this.triggerModifier===o.triggerModifier&&this.triggerSideBySideKey===o.triggerSideBySideKey&&this.triggerSideBySideModifier===o.triggerSideBySideModifier}}e.ClickLinkOptions=d;function g(c){return c==="altKey"?M.isMacintosh?new d(57,"metaKey",6,"altKey"):new d(5,"ctrlKey",6,"altKey"):M.isMacintosh?new d(6,"altKey",57,"metaKey"):new d(6,"altKey",5,"ctrlKey")}class p extends b.Disposable{constructor(o){super();this._onMouseMoveOrRelevantKeyDown=this._register(new N.Emitter),this.onMouseMoveOrRelevantKeyDown=this._onMouseMoveOrRelevantKeyDown.event,this._onExecute=this._register(new N.Emitter),this.onExecute=this._onExecute.event,this._onCancel=this._register(new N.Emitter),this.onCancel=this._onCancel.event,this._editor=o,this._opts=g(this._editor.getOption(64)),this._lastMouseMoveEvent=null,this._hasTriggerKeyOnMouseDown=!1,this._lineNumberOnMouseDown=0,this._register(this._editor.onDidChangeConfiguration(s=>{if(s.hasChanged(64)){const a=g(this._editor.getOption(64));if(this._opts.equals(a))return;this._opts=a,this._lastMouseMoveEvent=null,this._hasTriggerKeyOnMouseDown=!1,this._lineNumberOnMouseDown=0,this._onCancel.fire()}})),this._register(this._editor.onMouseMove(s=>this._onEditorMouseMove(new S(s,this._opts)))),this._register(this._editor.onMouseDown(s=>this._onEditorMouseDown(new S(s,this._opts)))),this._register(this._editor.onMouseUp(s=>this._onEditorMouseUp(new S(s,this._opts)))),this._register(this._editor.onKeyDown(s=>this._onEditorKeyDown(new C(s,this._opts)))),this._register(this._editor.onKeyUp(s=>this._onEditorKeyUp(new C(s,this._opts)))),this._register(this._editor.onMouseDrag(()=>this._resetHandler())),this._register(this._editor.onDidChangeCursorSelection(s=>this._onDidChangeCursorSelection(s))),this._register(this._editor.onDidChangeModel(s=>this._resetHandler())),this._register(this._editor.onDidChangeModelContent(()=>this._resetHandler())),this._register(this._editor.onDidScrollChange(s=>{(s.scrollTopChanged||s.scrollLeftChanged)&&this._resetHandler()}))}_onDidChangeCursorSelection(o){o.selection&&o.selection.startColumn!==o.selection.endColumn&&this._resetHandler()}_onEditorMouseMove(o){this._lastMouseMoveEvent=o,this._onMouseMoveOrRelevantKeyDown.fire([o,null])}_onEditorMouseDown(o){this._hasTriggerKeyOnMouseDown=o.hasTriggerModifier,this._lineNumberOnMouseDown=o.target.position?o.target.position.lineNumber:0}_onEditorMouseUp(o){const s=o.target.position?o.target.position.lineNumber:0;this._hasTriggerKeyOnMouseDown&&this._lineNumberOnMouseDown&&this._lineNumberOnMouseDown===s&&this._onExecute.fire(o)}_onEditorKeyDown(o){this._lastMouseMoveEvent&&(o.keyCodeIsTriggerKey||o.keyCodeIsSideBySideKey&&o.hasTriggerModifier)?this._onMouseMoveOrRelevantKeyDown.fire([this._lastMouseMoveEvent,o]):o.hasTriggerModifier&&this._onCancel.fire()}_onEditorKeyUp(o){o.keyCodeIsTriggerKey&&this._onCancel.fire()}_resetHandler(){this._lastMouseMoveEvent=null,this._hasTriggerKeyOnMouseDown=!1,this._onCancel.fire()}}e.ClickLinkGesture=p}),define(Q[228],J([0,1,15,12]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.HoverOperation=void 0;class M{constructor(S,C,d,g,p){this._computer=S,this._state=0,this._hoverTime=p,this._firstWaitScheduler=new b.RunOnceScheduler(()=>this._triggerAsyncComputation(),0),this._secondWaitScheduler=new b.RunOnceScheduler(()=>this._triggerSyncComputation(),0),this._loadingMessageScheduler=new b.RunOnceScheduler(()=>this._showLoadingMessage(),0),this._asyncComputationPromise=null,this._asyncComputationPromiseDone=!1,this._completeCallback=C,this._errorCallback=d,this._progressCallback=g}setHoverTime(S){this._hoverTime=S}_firstWaitTime(){return this._hoverTime/2}_secondWaitTime(){return this._hoverTime/2}_loadingMessageTime(){return 3*this._hoverTime}_triggerAsyncComputation(){this._state=2,this._secondWaitScheduler.schedule(this._secondWaitTime()),this._computer.computeAsync?(this._asyncComputationPromiseDone=!1,this._asyncComputationPromise=b.createCancelablePromise(S=>this._computer.computeAsync(S)),this._asyncComputationPromise.then(S=>{this._asyncComputationPromiseDone=!0,this._withAsyncResult(S)},S=>this._onError(S))):this._asyncComputationPromiseDone=!0}_triggerSyncComputation(){this._computer.computeSync&&this._computer.onResult(this._computer.computeSync(),!0),this._asyncComputationPromiseDone?(this._state=0,this._onComplete(this._computer.getResult())):(this._state=3,this._onProgress(this._computer.getResult()))}_showLoadingMessage(){this._state===3&&this._onProgress(this._computer.getResultWithLoadingMessage())}_withAsyncResult(S){S&&this._computer.onResult(S,!1),this._state===3&&(this._state=0,this._onComplete(this._computer.getResult()))}_onComplete(S){this._completeCallback(S)}_onError(S){this._errorCallback?this._errorCallback(S):N.onUnexpectedError(S)}_onProgress(S){this._progressCallback(S)}start(S){if(S===0)this._state===0&&(this._state=1,this._firstWaitScheduler.schedule(this._firstWaitTime()),this._loadingMessageScheduler.schedule(this._loadingMessageTime()));else switch(this._state){case 0:this._triggerAsyncComputation(),this._secondWaitScheduler.cancel(),this._triggerSyncComputation();break;case 2:this._secondWaitScheduler.cancel(),this._triggerSyncComputation();break}}cancel(){this._loadingMessageScheduler.cancel(),this._state===1&&this._firstWaitScheduler.cancel(),this._state===2&&(this._secondWaitScheduler.cancel(),this._asyncComputationPromise&&(this._asyncComputationPromise.cancel(),this._asyncComputationPromise=null)),this._state===3&&this._asyncComputationPromise&&(this._asyncComputationPromise.cancel(),this._asyncComputationPromise=null),this._state=0}}e.HoverOperation=M}),define(Q[408],J([0,1,52]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.GlyphHoverWidget=void 0;class N extends b.Widget{constructor(w,S){super();this._id=w,this._editor=S,this._isVisible=!1,this._domNode=document.createElement("div"),this._domNode.className="monaco-hover hidden",this._domNode.setAttribute("aria-hidden","true"),this._domNode.setAttribute("role","tooltip"),this._showAtLineNumber=-1,this._register(this._editor.onDidChangeConfiguration(C=>{C.hasChanged(38)&&this.updateFont()})),this._editor.addOverlayWidget(this)}get isVisible(){return this._isVisible}set isVisible(w){this._isVisible=w,this._domNode.classList.toggle("hidden",!this._isVisible)}getId(){return this._id}getDomNode(){return this._domNode}showAt(w){this._showAtLineNumber=w,this.isVisible||(this.isVisible=!0);const S=this._editor.getLayoutInfo(),C=this._editor.getTopForLineNumber(this._showAtLineNumber),d=this._editor.getScrollTop(),g=this._editor.getOption(53),p=this._domNode.clientHeight,c=C-d-(p-g)/2;this._domNode.style.left=`${S.glyphMarginLeft+S.glyphMarginWidth}px`,this._domNode.style.top=`${Math.max(Math.round(c),0)}px`}hide(){!this.isVisible||(this.isVisible=!1)}getPosition(){return null}dispose(){this._editor.removeOverlayWidget(this),super.dispose()}updateFont(){const w=Array.prototype.slice.call(this._domNode.getElementsByTagName("code")),S=Array.prototype.slice.call(this._domNode.getElementsByClassName("code"));[...w,...S].forEach(C=>this._editor.applyFontInfo(C))}updateContents(w){this._domNode.textContent="",this._domNode.appendChild(w),this.updateFont()}}e.GlyphHoverWidget=N}),define(Q[409],J([0,1,21]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InPlaceReplaceCommand=void 0;class N{constructor(w,S,C){this._editRange=w,this._originalSelection=S,this._text=C}getEditOperations(w,S){S.addTrackedEditOperation(this._editRange,this._text)}computeCursorState(w,S){const d=S.getInverseEditOperations()[0].range;return this._originalSelection.isEmpty()?new b.Selection(d.endLineNumber,Math.min(this._originalSelection.positionColumn,d.endColumn),d.endLineNumber,Math.min(this._originalSelection.positionColumn,d.endColumn)):new b.Selection(d.endLineNumber,d.endColumn-this._text.length,d.endLineNumber,d.endColumn)}}e.InPlaceReplaceCommand=N}),define(Q[229],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.generateIndent=e.getSpaceCnt=void 0;function b(M,w){let S=0;for(let C=0;C=c)return null;let o=[];for(let a=p;a<=c;a++)o.push(C.getLineContent(a));let s=o.slice(0);return s.sort(M.getCollator().compare),g===!0&&(s=s.reverse()),{startLineNumber:p,endLineNumber:c,before:o,after:s}}function S(C,d,g){let p=w(C,d,g);return p?b.EditOperation.replace(new N.Range(p.startLineNumber,1,p.endLineNumber,C.getLineMaxColumn(p.endLineNumber)),p.after.join(` +`)):null}}),define(Q[230],J([0,1,14,3,71]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.BracketSelectionRangeProvider=void 0;class w{provideSelectionRanges(C,d){return Ie(this,void 0,void 0,function*(){const g=[];for(const p of d){const c=[];g.push(c);const o=new Map;yield new Promise(s=>w._bracketsRightYield(s,0,C,p,o)),yield new Promise(s=>w._bracketsLeftYield(s,0,C,p,o,c))}return g})}static _bracketsRightYield(C,d,g,p,c){const o=new Map,s=Date.now();for(;;){if(d>=w._maxRounds){C();break}if(!p){C();break}let a=g.findNextBracket(p);if(!a){C();break}if(Date.now()-s>w._maxDuration){setTimeout(()=>w._bracketsRightYield(C,d+1,g,p,c));break}const r=a.close[0];if(a.isOpen){let i=o.has(r)?o.get(r):0;o.set(r,i+1)}else{let i=o.has(r)?o.get(r):0;if(i-=1,o.set(r,Math.max(0,i)),i<0){let n=c.get(r);n||(n=new M.LinkedList,c.set(r,n)),n.push(a.range)}}p=a.range.getEndPosition()}}static _bracketsLeftYield(C,d,g,p,c,o){const s=new Map,a=Date.now();for(;;){if(d>=w._maxRounds&&c.size===0){C();break}if(!p){C();break}let u=g.findPrevBracket(p);if(!u){C();break}if(Date.now()-a>w._maxDuration){setTimeout(()=>w._bracketsLeftYield(C,d+1,g,p,c,o));break}const i=u.close[0];if(u.isOpen){let n=s.has(i)?s.get(i):0;if(n-=1,s.set(i,Math.max(0,n)),n<0){let t=c.get(i);if(t){let l=t.shift();t.size===0&&c.delete(i);const h=N.Range.fromPositions(u.range.getEndPosition(),l.getStartPosition()),m=N.Range.fromPositions(u.range.getStartPosition(),l.getEndPosition());o.push({range:h}),o.push({range:m}),w._addBracketLeading(g,m,o)}}}else{let n=s.has(i)?s.get(i):0;s.set(i,n+1)}p=u.range.getStartPosition()}}static _addBracketLeading(C,d,g){if(d.startLineNumber!==d.endLineNumber){const p=d.startLineNumber,c=C.getLineFirstNonWhitespaceColumn(p);c!==0&&c!==d.startColumn&&(g.push({range:N.Range.fromPositions(new b.Position(p,c),d.getEndPosition())}),g.push({range:N.Range.fromPositions(new b.Position(p,1),d.getEndPosition())}));const o=p-1;if(o>0){const s=C.getLineFirstNonWhitespaceColumn(o);s===d.startColumn&&s!==C.getLineLastNonWhitespaceColumn(o)&&(g.push({range:N.Range.fromPositions(new b.Position(o,s),d.getEndPosition())}),g.push({range:N.Range.fromPositions(new b.Position(o,1),d.getEndPosition())}))}}}}e.BracketSelectionRangeProvider=w,w._maxDuration=30,w._maxRounds=2}),define(Q[412],J([0,1,3,8]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WordSelectionRangeProvider=void 0;class M{provideSelectionRanges(S,C){const d=[];for(const g of C){const p=[];d.push(p),this._addInWordRanges(p,S,g),this._addWordRanges(p,S,g),this._addWhitespaceLine(p,S,g),p.push({range:S.getFullModelRange()})}return d}_addInWordRanges(S,C,d){const g=C.getWordAtPosition(d);if(!!g){let{word:p,startColumn:c}=g,o=d.column-c,s=o,a=o,u=0;for(;s>=0;s--){let r=p.charCodeAt(s);if(s!==o&&(r===95||r===45))break;if(N.isLowerAsciiLetter(r)&&N.isUpperAsciiLetter(u))break;u=r}for(s+=1;a0&&C.getLineFirstNonWhitespaceColumn(d.lineNumber)===0&&C.getLineLastNonWhitespaceColumn(d.lineNumber)===0&&S.push({range:new b.Range(d.lineNumber,1,d.lineNumber,C.getLineMaxColumn(d.lineNumber))})}}e.WordSelectionRangeProvider=M}),define(Q[131],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SnippetParser=e.TextmateSnippet=e.Variable=e.FormatString=e.Transform=e.Choice=e.Placeholder=e.TransformableMarker=e.Text=e.Marker=e.Scanner=void 0;class b{constructor(){this.value="",this.pos=0}static isDigitCharacter(u){return u>=48&&u<=57}static isVariableCharacter(u){return u===95||u>=97&&u<=122||u>=65&&u<=90}text(u){this.value=u,this.pos=0}tokenText(u){return this.value.substr(u.pos,u.len)}next(){if(this.pos>=this.value.length)return{type:14,pos:this.pos,len:0};let u=this.pos,r=0,i=this.value.charCodeAt(u),n;if(n=b._table[i],typeof n=="number")return this.pos+=1,{type:n,pos:u,len:1};if(b.isDigitCharacter(i)){n=8;do r+=1,i=this.value.charCodeAt(u+r);while(b.isDigitCharacter(i));return this.pos+=r,{type:n,pos:u,len:r}}if(b.isVariableCharacter(i)){n=9;do i=this.value.charCodeAt(u+ ++r);while(b.isVariableCharacter(i)||b.isDigitCharacter(i));return this.pos+=r,{type:n,pos:u,len:r}}n=10;do r+=1,i=this.value.charCodeAt(u+r);while(!isNaN(i)&&typeof b._table[i]=="undefined"&&!b.isDigitCharacter(i)&&!b.isVariableCharacter(i));return this.pos+=r,{type:n,pos:u,len:r}}}e.Scanner=b,b._table={[36]:0,[58]:1,[44]:2,[123]:3,[125]:4,[92]:5,[47]:6,[124]:7,[43]:11,[45]:12,[63]:13};class N{constructor(){this._children=[]}appendChild(u){return u instanceof M&&this._children[this._children.length-1]instanceof M?this._children[this._children.length-1].value+=u.value:(u.parent=this,this._children.push(u)),this}replace(u,r){const{parent:i}=u,n=i.children.indexOf(u),t=i.children.slice(0);t.splice(n,1,...r),i._children=t,function l(h,m){for(const _ of h)_.parent=m,l(_.children,_)}(r,i)}get children(){return this._children}get snippet(){let u=this;for(;;){if(!u)return;if(u instanceof o)return u;u=u.parent}}toString(){return this.children.reduce((u,r)=>u+r.toString(),"")}len(){return 0}}e.Marker=N;class M extends N{constructor(u){super();this.value=u}toString(){return this.value}len(){return this.value.length}clone(){return new M(this.value)}}e.Text=M;class w extends N{}e.TransformableMarker=w;class S extends w{constructor(u){super();this.index=u}static compareByIndex(u,r){return u.index===r.index?0:u.isFinalTabstop?1:r.isFinalTabstop||u.indexr.index?1:0}get isFinalTabstop(){return this.index===0}get choice(){return this._children.length===1&&this._children[0]instanceof C?this._children[0]:void 0}clone(){let u=new S(this.index);return this.transform&&(u.transform=this.transform.clone()),u._children=this.children.map(r=>r.clone()),u}}e.Placeholder=S;class C extends N{constructor(){super(...arguments);this.options=[]}appendChild(u){return u instanceof M&&(u.parent=this,this.options.push(u)),this}toString(){return this.options[0].value}len(){return this.options[0].len()}clone(){let u=new C;return this.options.forEach(u.appendChild,u),u}}e.Choice=C;class d extends N{constructor(){super(...arguments);this.regexp=new RegExp("")}resolve(u){const r=this;let i=!1,n=u.replace(this.regexp,function(){return i=!0,r._replace(Array.prototype.slice.call(arguments,0,-2))});return!i&&this._children.some(t=>t instanceof g&&Boolean(t.elseValue))&&(n=this._replace([])),n}_replace(u){let r="";for(const i of this._children)if(i instanceof g){let n=u[i.index]||"";n=i.resolve(n),r+=n}else r+=i.toString();return r}toString(){return""}clone(){let u=new d;return u.regexp=new RegExp(this.regexp.source,""+(this.regexp.ignoreCase?"i":"")+(this.regexp.global?"g":"")),u._children=this.children.map(r=>r.clone()),u}}e.Transform=d;class g extends N{constructor(u,r,i,n){super();this.index=u,this.shorthandName=r,this.ifValue=i,this.elseValue=n}resolve(u){return this.shorthandName==="upcase"?u?u.toLocaleUpperCase():"":this.shorthandName==="downcase"?u?u.toLocaleLowerCase():"":this.shorthandName==="capitalize"?u?u[0].toLocaleUpperCase()+u.substr(1):"":this.shorthandName==="pascalcase"?u?this._toPascalCase(u):"":Boolean(u)&&typeof this.ifValue=="string"?this.ifValue:!Boolean(u)&&typeof this.elseValue=="string"?this.elseValue:u||""}_toPascalCase(u){const r=u.match(/[a-z]+/gi);return r?r.map(function(i){return i.charAt(0).toUpperCase()+i.substr(1).toLowerCase()}).join(""):u}clone(){return new g(this.index,this.shorthandName,this.ifValue,this.elseValue)}}e.FormatString=g;class p extends w{constructor(u){super();this.name=u}resolve(u){let r=u.resolve(this);return this.transform&&(r=this.transform.resolve(r||"")),r!==void 0?(this._children=[new M(r)],!0):!1}clone(){const u=new p(this.name);return this.transform&&(u.transform=this.transform.clone()),u._children=this.children.map(r=>r.clone()),u}}e.Variable=p;function c(a,u){const r=[...a];for(;r.length>0;){const i=r.shift();if(!u(i))break;r.unshift(...i.children)}}class o extends N{get placeholderInfo(){if(!this._placeholders){let u=[],r;this.walk(function(i){return i instanceof S&&(u.push(i),r=!r||r.indexn===u?(i=!0,!1):(r+=n.len(),!0)),i?r:-1}fullLen(u){let r=0;return c([u],i=>(r+=i.len(),!0)),r}enclosingPlaceholders(u){let r=[],{parent:i}=u;for(;i;)i instanceof S&&r.push(i),i=i.parent;return r}resolveVariables(u){return this.walk(r=>(r instanceof p&&r.resolve(u)&&(this._placeholders=void 0),!0)),this}appendChild(u){return this._placeholders=void 0,super.appendChild(u)}replace(u,r){return this._placeholders=void 0,super.replace(u,r)}clone(){let u=new o;return this._children=this.children.map(r=>r.clone()),u}walk(u){c(this.children,u)}}e.TextmateSnippet=o;class s{constructor(){this._scanner=new b,this._token={type:14,pos:0,len:0}}static escape(u){return u.replace(/\$|}|\\/g,"\\$&")}static guessNeedsClipboard(u){return/\${?CLIPBOARD/.test(u)}parse(u,r,i){this._scanner.text(u),this._token=this._scanner.next();const n=new o;for(;this._parse(n););const t=new Map,l=[];let h=0;n.walk(m=>(m instanceof S&&(h+=1,m.isFinalTabstop?t.set(0,void 0):!t.has(m.index)&&m.children.length>0?t.set(m.index,m.children):l.push(m)),!0));for(const m of l){const _=t.get(m.index);if(_){const f=new S(m.index);f.transform=m.transform;for(const v of _)f.appendChild(v.clone());n.replace(m,[f])}}return i||(i=h>0&&r),!t.has(0)&&i&&n.appendChild(new S(0)),n}_accept(u,r){if(u===void 0||this._token.type===u){let i=r?this._scanner.tokenText(this._token):!0;return this._token=this._scanner.next(),i}return!1}_backTo(u){return this._scanner.pos=u.pos+u.len,this._token=u,!1}_until(u){const r=this._token;for(;this._token.type!==u;){if(this._token.type===14)return!1;if(this._token.type===5){const n=this._scanner.next();if(n.type!==0&&n.type!==4&&n.type!==5)return!1}this._token=this._scanner.next()}const i=this._scanner.value.substring(r.pos,this._token.pos).replace(/\\(\$|}|\\)/g,"$1");return this._token=this._scanner.next(),i}_parse(u){return this._parseEscaped(u)||this._parseTabstopOrVariableName(u)||this._parseComplexPlaceholder(u)||this._parseComplexVariable(u)||this._parseAnything(u)}_parseEscaped(u){let r;return(r=this._accept(5,!0))?(r=this._accept(0,!0)||this._accept(4,!0)||this._accept(5,!0)||r,u.appendChild(new M(r)),!0):!1}_parseTabstopOrVariableName(u){let r;const i=this._token;return this._accept(0)&&(r=this._accept(9,!0)||this._accept(8,!0))?(u.appendChild(/^\d+$/.test(r)?new S(Number(r)):new p(r)),!0):this._backTo(i)}_parseComplexPlaceholder(u){let r;const i=this._token;if(!(this._accept(0)&&this._accept(3)&&(r=this._accept(8,!0))))return this._backTo(i);const t=new S(Number(r));if(this._accept(1))for(;;){if(this._accept(4))return u.appendChild(t),!0;if(!this._parse(t))return u.appendChild(new M("${"+r+":")),t.children.forEach(u.appendChild,u),!0}else if(t.index>0&&this._accept(7)){const l=new C;for(;;){if(this._parseChoiceElement(l)){if(this._accept(2))continue;if(this._accept(7)&&(t.appendChild(l),this._accept(4)))return u.appendChild(t),!0}return this._backTo(i),!1}}else return this._accept(6)?this._parseTransform(t)?(u.appendChild(t),!0):(this._backTo(i),!1):this._accept(4)?(u.appendChild(t),!0):this._backTo(i)}_parseChoiceElement(u){const r=this._token,i=[];for(;!(this._token.type===2||this._token.type===7);){let n;if((n=this._accept(5,!0))?n=this._accept(2,!0)||this._accept(7,!0)||this._accept(5,!0)||n:n=this._accept(void 0,!0),!n)return this._backTo(r),!1;i.push(n)}return i.length===0?(this._backTo(r),!1):(u.appendChild(new M(i.join(""))),!0)}_parseComplexVariable(u){let r;const i=this._token;if(!(this._accept(0)&&this._accept(3)&&(r=this._accept(9,!0))))return this._backTo(i);const t=new p(r);if(this._accept(1))for(;;){if(this._accept(4))return u.appendChild(t),!0;if(!this._parse(t))return u.appendChild(new M("${"+r+":")),t.children.forEach(u.appendChild,u),!0}else return this._accept(6)?this._parseTransform(t)?(u.appendChild(t),!0):(this._backTo(i),!1):this._accept(4)?(u.appendChild(t),!0):this._backTo(i)}_parseTransform(u){let r=new d,i="",n="";for(;!this._accept(6);){let t;if(t=this._accept(5,!0)){t=this._accept(6,!0)||t,i+=t;continue}if(this._token.type!==14){i+=this._accept(void 0,!0);continue}return!1}for(;!this._accept(6);){let t;if(t=this._accept(5,!0)){t=this._accept(5,!0)||this._accept(6,!0)||t,r.appendChild(new M(t));continue}if(!(this._parseFormatString(r)||this._parseAnything(r)))return!1}for(;!this._accept(4);){if(this._token.type!==14){n+=this._accept(void 0,!0);continue}return!1}try{r.regexp=new RegExp(i,n)}catch(t){return!1}return u.transform=r,!0}_parseFormatString(u){const r=this._token;if(!this._accept(0))return!1;let i=!1;this._accept(3)&&(i=!0);let n=this._accept(8,!0);if(n)if(i){if(this._accept(4))return u.appendChild(new g(Number(n))),!0;if(!this._accept(1))return this._backTo(r),!1}else return u.appendChild(new g(Number(n))),!0;else return this._backTo(r),!1;if(this._accept(6)){let t=this._accept(9,!0);return!t||!this._accept(4)?(this._backTo(r),!1):(u.appendChild(new g(Number(n),t)),!0)}else if(this._accept(11)){let t=this._until(4);if(t)return u.appendChild(new g(Number(n),void 0,t,void 0)),!0}else if(this._accept(12)){let t=this._until(4);if(t)return u.appendChild(new g(Number(n),void 0,void 0,t)),!0}else if(this._accept(13)){let t=this._until(1);if(t){let l=this._until(4);if(l)return u.appendChild(new g(Number(n),void 0,t,l)),!0}}else{let t=this._until(4);if(t)return u.appendChild(new g(Number(n),void 0,void 0,t)),!0}return this._backTo(r),!1}_parseAnything(u){return this._token.type!==14?(u.appendChild(new M(this._scanner.tokenText(this._token))),this._accept(void 0),!0):!1}}e.SnippetParser=s}),define(Q[413],J([0,1,66,8,19]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CompletionModel=e.LineContext=void 0;class w{constructor(d,g){this.leadingLineContent=d,this.characterCountDelta=g}}e.LineContext=w;class S{constructor(d,g,p,c,o,s,a){this.clipboardText=a,this._snippetCompareFn=S._compareCompletionItems,this._items=d,this._column=g,this._wordDistance=c,this._options=o,this._refilterKind=1,this._lineContext=p,s==="top"?this._snippetCompareFn=S._compareCompletionItemsSnippetsUp:s==="bottom"&&(this._snippetCompareFn=S._compareCompletionItemsSnippetsDown)}get lineContext(){return this._lineContext}set lineContext(d){(this._lineContext.leadingLineContent!==d.leadingLineContent||this._lineContext.characterCountDelta!==d.characterCountDelta)&&(this._refilterKind=this._lineContext.characterCountDelta2e3?b.fuzzyScore:b.fuzzyScoreGracefulAggressive;for(let r=0;r=t)i.score=b.FuzzyScore.Default;else if(typeof i.completion.filterText=="string"){let m=u(c,o,h,i.completion.filterText,i.filterTextLow,0,!1);if(!m)continue;N.compareIgnoreCase(i.completion.filterText,l)===0?i.score=m:(i.score=b.anyScore(c,o,h,l,i.labelLow,0),i.score[0]=m[0])}else{let m=u(c,o,h,l,i.labelLow,0,!1);if(!m)continue;i.score=m}}i.idx=r,i.distance=this._wordDistance.distance(i.position,i.completion),a.push(i),d.push(l.length)}}this._filteredItems=a.sort(this._snippetCompareFn),this._refilterKind=0,this._stats={pLabelLen:d.length?M.quickSelect(d.length-.85,d,(r,i)=>r-i):0}}static _compareCompletionItems(d,g){return d.score[0]>g.score[0]?-1:d.score[0]g.distance?1:d.idxg.idx?1:0}static _compareCompletionItemsSnippetsDown(d,g){if(d.completion.kind!==g.completion.kind){if(d.completion.kind===27)return 1;if(g.completion.kind===27)return-1}return S._compareCompletionItems(d,g)}static _compareCompletionItemsSnippetsUp(d,g){if(d.completion.kind!==g.completion.kind){if(d.completion.kind===27)return-1;if(g.completion.kind===27)return 1}return S._compareCompletionItems(d,g)}}e.CompletionModel=S}),define(Q[231],J([0,1,6,2,7,104]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ResizableHTMLElement=void 0;class S{constructor(){this._onDidWillResize=new b.Emitter,this.onDidWillResize=this._onDidWillResize.event,this._onDidResize=new b.Emitter,this.onDidResize=this._onDidResize.event,this._sashListener=new N.DisposableStore,this._size=new M.Dimension(0,0),this._minSize=new M.Dimension(0,0),this._maxSize=new M.Dimension(Number.MAX_SAFE_INTEGER,Number.MAX_SAFE_INTEGER),this.domNode=document.createElement("div"),this._eastSash=new w.Sash(this.domNode,{getVerticalSashLeft:()=>this._size.width},{orientation:0}),this._westSash=new w.Sash(this.domNode,{getVerticalSashLeft:()=>0},{orientation:0}),this._northSash=new w.Sash(this.domNode,{getHorizontalSashTop:()=>0},{orientation:1,orthogonalEdge:w.OrthogonalEdge.North}),this._southSash=new w.Sash(this.domNode,{getHorizontalSashTop:()=>this._size.height},{orientation:1,orthogonalEdge:w.OrthogonalEdge.South}),this._northSash.orthogonalStartSash=this._westSash,this._northSash.orthogonalEndSash=this._eastSash,this._southSash.orthogonalStartSash=this._westSash,this._southSash.orthogonalEndSash=this._eastSash;let d,g=0,p=0;this._sashListener.add(b.Event.any(this._northSash.onDidStart,this._eastSash.onDidStart,this._southSash.onDidStart,this._westSash.onDidStart)(()=>{d===void 0&&(this._onDidWillResize.fire(),d=this._size,g=0,p=0)})),this._sashListener.add(b.Event.any(this._northSash.onDidEnd,this._eastSash.onDidEnd,this._southSash.onDidEnd,this._westSash.onDidEnd)(()=>{d!==void 0&&(d=void 0,g=0,p=0,this._onDidResize.fire({dimension:this._size,done:!0}))})),this._sashListener.add(this._eastSash.onDidChange(c=>{d&&(p=c.currentX-c.startX,this.layout(d.height+g,d.width+p),this._onDidResize.fire({dimension:this._size,done:!1,east:!0}))})),this._sashListener.add(this._westSash.onDidChange(c=>{d&&(p=-(c.currentX-c.startX),this.layout(d.height+g,d.width+p),this._onDidResize.fire({dimension:this._size,done:!1,west:!0}))})),this._sashListener.add(this._northSash.onDidChange(c=>{d&&(g=-(c.currentY-c.startY),this.layout(d.height+g,d.width+p),this._onDidResize.fire({dimension:this._size,done:!1,north:!0}))})),this._sashListener.add(this._southSash.onDidChange(c=>{d&&(g=c.currentY-c.startY,this.layout(d.height+g,d.width+p),this._onDidResize.fire({dimension:this._size,done:!1,south:!0}))})),this._sashListener.add(b.Event.any(this._eastSash.onDidReset,this._westSash.onDidReset)(c=>{this._preferredSize&&(this.layout(this._size.height,this._preferredSize.width),this._onDidResize.fire({dimension:this._size,done:!0}))})),this._sashListener.add(b.Event.any(this._northSash.onDidReset,this._southSash.onDidReset)(c=>{this._preferredSize&&(this.layout(this._preferredSize.height,this._size.width),this._onDidResize.fire({dimension:this._size,done:!0}))}))}dispose(){this._northSash.dispose(),this._southSash.dispose(),this._eastSash.dispose(),this._westSash.dispose(),this._sashListener.dispose(),this.domNode.remove()}enableSashes(d,g,p,c){this._northSash.state=d?3:0,this._eastSash.state=g?3:0,this._southSash.state=p?3:0,this._westSash.state=c?3:0}layout(d=this.size.height,g=this.size.width){const{height:p,width:c}=this._minSize,{height:o,width:s}=this._maxSize;d=Math.max(p,Math.min(o,d)),g=Math.max(c,Math.min(s,g));const a=new M.Dimension(g,d);M.Dimension.equals(a,this._size)||(this.domNode.style.height=d+"px",this.domNode.style.width=g+"px",this._size=a,this._northSash.layout(),this._eastSash.layout(),this._southSash.layout(),this._westSash.layout())}get size(){return this._size}set maxSize(d){this._maxSize=d}get maxSize(){return this._maxSize}set minSize(d){this._minSize=d}get minSize(){return this._minSize}set preferredSize(d){this._preferredSize=d}get preferredSize(){return this._preferredSize}}e.ResizableHTMLElement=S}),define(Q[414],J([0,1,19,2,91]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CommitCharacterController=void 0;class w{constructor(C,d,g){this._disposables=new N.DisposableStore,this._disposables.add(d.onDidShow(()=>this._onItem(d.getFocusedItem()))),this._disposables.add(d.onDidFocus(this._onItem,this)),this._disposables.add(d.onDidHide(this.reset,this)),this._disposables.add(C.onWillType(p=>{if(this._active&&!d.isFrozen()){const c=p.charCodeAt(p.length-1);this._active.acceptCharacters.has(c)&&C.getOption(0)&&g(this._active.item)}}))}_onItem(C){if(!C||!b.isNonEmptyArray(C.item.completion.commitCharacters)){this.reset();return}if(!(this._active&&this._active.item.item===C.item)){const d=new M.CharacterSet;for(const g of C.item.completion.commitCharacters)g.length>0&&d.add(g.charCodeAt(0));this._active={acceptCharacters:d,item:C}}}reset(){this._active=void 0}dispose(){this._disposables.dispose()}}e.CommitCharacterController=w}),define(Q[415],J([0,1,2]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.OvertypingCapturer=void 0;class N{constructor(w,S){this._disposables=new b.DisposableStore,this._lastOvertyped=[],this._empty=!0,this._disposables.add(w.onWillType(()=>{if(!!this._empty&&!!w.hasModel()){const C=w.getSelections(),d=C.length;let g=!1;for(let c=0;cN._maxSelectionLength)return;this._lastOvertyped[c]={value:p.getValueInRange(o),multiline:o.startLineNumber!==o.endLineNumber}}this._empty=!1}}})),this._disposables.add(S.onDidCancel(C=>{!this._empty&&!C.retrigger&&(this._empty=!0)}))}getLastOvertypedInfo(w){if(!this._empty&&w>=0&&w=0?i[n]:i[Math.max(0,~n-1)],l=c.length;for(const h of c){if(!N.Range.containsRange(h.range,t))break;l-=1}return l}}})}}e.WordDistance=w,w.None=new class extends w{distance(){return 0}}}),define(Q[232],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.stateExists=e.findRules=e.substituteMatches=e.createError=e.log=e.sanitize=e.fixCase=e.empty=e.isIAction=e.isString=e.isFuzzyAction=e.isFuzzyActionArr=void 0;function b(a){return Array.isArray(a)}e.isFuzzyActionArr=b;function N(a){return!b(a)}e.isFuzzyAction=N;function M(a){return typeof a=="string"}e.isString=M;function w(a){return!M(a)}e.isIAction=w;function S(a){return!a}e.empty=S;function C(a,u){return a.ignoreCase&&u?u.toLowerCase():u}e.fixCase=C;function d(a){return a.replace(/[&<>'"_]/g,"-")}e.sanitize=d;function g(a,u){console.log(`${a.languageId}: ${u}`)}e.log=g;function p(a,u){return new Error(`${a.languageId}: ${u}`)}e.createError=p;function c(a,u,r,i,n){const t=/\$((\$)|(#)|(\d\d?)|[sS](\d\d?)|@(\w+))/g;let l=null;return u.replace(t,function(h,m,_,f,v,y,L,I,k){return S(_)?S(f)?!S(v)&&v0;){const i=a.tokenizer[r];if(i)return i;const n=r.lastIndexOf(".");n<0?r=null:r=r.substr(0,n)}return null}e.findRules=o;function s(a,u){let r=u;for(;r&&r.length>0;){if(a.stateNames[r])return!0;const n=r.lastIndexOf(".");n<0?r=null:r=r.substr(0,n)}return!1}e.stateExists=s}),define(Q[417],J([0,1,232]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.compile=void 0;function N(a,u){if(!u||!Array.isArray(u))return!1;for(const r of u)if(!a(r))return!1;return!0}function M(a,u){return typeof a=="boolean"?a:u}function w(a,u){return typeof a=="string"?a:u}function S(a){const u={};for(const r of a)u[r]=!0;return u}function C(a,u=!1){u&&(a=a.map(function(i){return i.toLowerCase()}));const r=S(a);return u?function(i){return r[i.toLowerCase()]!==void 0&&r.hasOwnProperty(i.toLowerCase())}:function(i){return r[i]!==void 0&&r.hasOwnProperty(i)}}function d(a,u){let r=0,i;do i=!1,u=u.replace(/(.|^)@(\w+)/g,function(t,l,h){if(l==="@")return t;i=!0;let m="";if(typeof a[h]=="string")m=a[h];else if(a[h]&&a[h]instanceof RegExp)m=a[h].source;else throw a[h]===void 0?b.createError(a,"language definition does not contain attribute '"+h+"', used at: "+u):b.createError(a,"attribute reference '"+h+"' must be a string, used at: "+u);return l+(b.empty(m)?"":"(?:"+m+")")}),r++;while(i&&r<5);u=u.replace(/@@/g,"@");let n=(a.ignoreCase?"i":"")+(a.unicode?"u":"");return new RegExp(u,n)}function g(a,u,r,i){if(i<0)return a;if(i=100){i=i-100;let n=r.split(".");if(n.unshift(r),i=0&&(i.tokenSubst=!0),typeof r.bracket=="string")if(r.bracket==="@open")i.bracket=1;else if(r.bracket==="@close")i.bracket=-1;else throw b.createError(a,"a 'bracket' attribute must be either '@open' or '@close', in rule: "+u);if(r.next){if(typeof r.next!="string")throw b.createError(a,"the next state must be a string value in rule: "+u);{let n=r.next;if(!/^(@pop|@push|@popall)$/.test(n)&&(n[0]==="@"&&(n=n.substr(1)),n.indexOf("$")<0&&!b.stateExists(a,b.substituteMatches(a,n,"",[],""))))throw b.createError(a,"the next state '"+r.next+"' is not defined in rule: "+u);i.next=n}}return typeof r.goBack=="number"&&(i.goBack=r.goBack),typeof r.switchTo=="string"&&(i.switchTo=r.switchTo),typeof r.log=="string"&&(i.log=r.log),typeof r.nextEmbedded=="string"&&(i.nextEmbedded=r.nextEmbedded,a.usesEmbedded=!0),i}}else if(Array.isArray(r)){let i=[];for(let n=0,t=r.length;n0&&i[0]==="^",this.name=this.name+": "+i,this.regex=d(u,"^(?:"+(this.matchOnlyAtLineStart?i.substr(1):i)+")")}setAction(u,r){this.action=c(u,this.name,r)}}function s(a,u){if(!u||typeof u!="object")throw new Error("Monarch: expecting a language definition object");let r={};r.languageId=a,r.includeLF=M(u.includeLF,!1),r.noThrow=!1,r.maxStack=100,r.start=typeof u.start=="string"?u.start:null,r.ignoreCase=M(u.ignoreCase,!1),r.unicode=M(u.unicode,!1),r.tokenPostfix=w(u.tokenPostfix,"."+r.languageId),r.defaultToken=w(u.defaultToken,"source"),r.usesEmbedded=!1;let i=u;i.languageId=a,i.includeLF=r.includeLF,i.ignoreCase=r.ignoreCase,i.unicode=r.unicode,i.noThrow=r.noThrow,i.usesEmbedded=r.usesEmbedded,i.stateNames=u.tokenizer,i.defaultToken=r.defaultToken;function n(l,h,m){for(const _ of m){let f=_.include;if(f){if(typeof f!="string")throw b.createError(r,"an 'include' attribute must be a string at: "+l);if(f[0]==="@"&&(f=f.substr(1)),!u.tokenizer[f])throw b.createError(r,"include target '"+f+"' is not defined at: "+l);n(l+"."+f,h,u.tokenizer[f])}else{const v=new o(l);if(Array.isArray(_)&&_.length>=1&&_.length<=3)if(v.setRegex(i,_[0]),_.length>=3)if(typeof _[1]=="string")v.setAction(i,{token:_[1],next:_[2]});else if(typeof _[1]=="object"){const y=_[1];y.next=_[2],v.setAction(i,y)}else throw b.createError(r,"a next state as the last element of a rule can only be given if the action is either an object or a string, at: "+l);else v.setAction(i,_[1]);else{if(!_.regex)throw b.createError(r,"a rule must either be an array, or an object with a 'regex' or 'include' field at: "+l);_.name&&typeof _.name=="string"&&(v.name=_.name),_.matchOnlyAtStart&&(v.matchOnlyAtLineStart=M(_.matchOnlyAtLineStart,!1)),v.setRegex(i,_.regex),v.setAction(i,_.action)}h.push(v)}}}if(!u.tokenizer||typeof u.tokenizer!="object")throw b.createError(r,"a language definition must define the 'tokenizer' attribute as an object");r.tokenizer=[];for(let l in u.tokenizer)if(u.tokenizer.hasOwnProperty(l)){r.start||(r.start=l);const h=u.tokenizer[l];r.tokenizer[l]=new Array,n("tokenizer."+l,r.tokenizer[l],h)}if(r.usesEmbedded=i.usesEmbedded,u.brackets){if(!Array.isArray(u.brackets))throw b.createError(r,"the 'brackets' attribute must be defined as an array")}else u.brackets=[{open:"{",close:"}",token:"delimiter.curly"},{open:"[",close:"]",token:"delimiter.square"},{open:"(",close:")",token:"delimiter.parenthesis"},{open:"<",close:">",token:"delimiter.angle"}];let t=[];for(let l of u.brackets){let h=l;if(h&&Array.isArray(h)&&h.length===3&&(h={token:h[2],open:h[0],close:h[1]}),h.open===h.close)throw b.createError(r,"open and close brackets in a 'brackets' attribute must be different: "+h.open+` + hint: use the 'bracket' attribute if matching on equal brackets is required.`);if(typeof h.open=="string"&&typeof h.token=="string"&&typeof h.close=="string")t.push({token:h.token+r.tokenPostfix,open:b.fixCase(r,h.open),close:b.fixCase(r,h.close)});else throw b.createError(r,"every element in the 'brackets' array must be a '{open,close,token}' object or array")}return r.brackets=t,r.noThrow=!0,r}e.compile=s}),define(Q[418],J([4,5]),function(q,e){return q.create("vs/base/browser/ui/actionbar/actionViewItems",e)}),define(Q[419],J([4,5]),function(q,e){return q.create("vs/base/browser/ui/findinput/findInput",e)}),define(Q[420],J([4,5]),function(q,e){return q.create("vs/base/browser/ui/findinput/findInputCheckboxes",e)}),define(Q[233],J([0,1,160,420,27]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RegexCheckbox=e.WholeWordsCheckbox=e.CaseSensitiveCheckbox=void 0;const w=N.localize(0,null),S=N.localize(1,null),C=N.localize(2,null);class d extends b.Checkbox{constructor(o){super({icon:M.Codicon.caseSensitive,title:w+o.appendTitle,isChecked:o.isChecked,inputActiveOptionBorder:o.inputActiveOptionBorder,inputActiveOptionForeground:o.inputActiveOptionForeground,inputActiveOptionBackground:o.inputActiveOptionBackground})}}e.CaseSensitiveCheckbox=d;class g extends b.Checkbox{constructor(o){super({icon:M.Codicon.wholeWord,title:S+o.appendTitle,isChecked:o.isChecked,inputActiveOptionBorder:o.inputActiveOptionBorder,inputActiveOptionForeground:o.inputActiveOptionForeground,inputActiveOptionBackground:o.inputActiveOptionBackground})}}e.WholeWordsCheckbox=g;class p extends b.Checkbox{constructor(o){super({icon:M.Codicon.regex,title:C+o.appendTitle,isChecked:o.isChecked,inputActiveOptionBorder:o.inputActiveOptionBorder,inputActiveOptionForeground:o.inputActiveOptionForeground,inputActiveOptionBackground:o.inputActiveOptionBackground})}}e.RegexCheckbox=p}),define(Q[421],J([4,5]),function(q,e){return q.create("vs/base/browser/ui/findinput/replaceInput",e)}),define(Q[422],J([4,5]),function(q,e){return q.create("vs/base/browser/ui/iconLabel/iconLabel",e)}),define(Q[175],J([0,1,7,157,2,120,40,17,20,55,422,23,304]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IconLabel=void 0;class o{constructor(n){this._element=n}get element(){return this._element}set textContent(n){this.disposed||n===this._textContent||(this._textContent=n,this._element.textContent=n)}set className(n){this.disposed||n===this._className||(this._className=n,this._element.className=n)}set empty(n){this.disposed||n===this._empty||(this._empty=n,this._element.style.marginLeft=n?"0":"")}dispose(){this.disposed=!0}}class s extends M.Disposable{constructor(n,t){super();this.hoverDelegate=void 0,this.customHovers=new Map,this.domNode=this._register(new o(b.append(n,b.$(".monaco-icon-label")))),this.labelContainer=b.append(this.domNode.element,b.$(".monaco-icon-label-container"));const l=b.append(this.labelContainer,b.$("span.monaco-icon-name-container"));this.descriptionContainer=this._register(new o(b.append(this.labelContainer,b.$("span.monaco-icon-description-container")))),(t==null?void 0:t.supportHighlights)?this.nameNode=new r(l,!!t.supportIcons):this.nameNode=new a(l),(t==null?void 0:t.supportDescriptionHighlights)?this.descriptionNodeFactory=()=>new N.HighlightedLabel(b.append(this.descriptionContainer.element,b.$("span.label-description")),!!t.supportIcons):this.descriptionNodeFactory=()=>this._register(new o(b.append(this.descriptionContainer.element,b.$("span.label-description")))),(t==null?void 0:t.hoverDelegate)&&(this.hoverDelegate=t.hoverDelegate)}setLabel(n,t,l){const h=["monaco-icon-label"];l&&(l.extraClasses&&h.push(...l.extraClasses),l.italic&&h.push("italic"),l.strikethrough&&h.push("strikethrough")),this.domNode.className=h.join(" "),this.setupHover(this.labelContainer,l==null?void 0:l.title),this.nameNode.setLabel(n,l),(t||this.descriptionNode)&&(this.descriptionNode||(this.descriptionNode=this.descriptionNodeFactory()),this.descriptionNode instanceof N.HighlightedLabel?(this.descriptionNode.set(t||"",l?l.descriptionMatches:void 0),this.setupHover(this.descriptionNode.element,l==null?void 0:l.descriptionTitle)):(this.descriptionNode.textContent=t||"",this.setupHover(this.descriptionNode.element,(l==null?void 0:l.descriptionTitle)||""),this.descriptionNode.empty=!t))}setupHover(n,t){const l=this.customHovers.get(n);if(l&&(l.dispose(),this.customHovers.delete(n)),!t){n.removeAttribute("title");return}return this.hoverDelegate?this.setupCustomHover(this.hoverDelegate,n,t):this.setupNativeHover(n,t)}static adjustXAndShowCustomHover(n,t,l,h){if(n&&h)return t!==void 0&&(n.target.x=t+10),l.showHover(n)}getTooltipForCustom(n){if(d.isString(n))return()=>Ie(this,void 0,void 0,function*(){return n});if(d.isFunction(n.markdown))return n.markdown;{const t=n.markdown;return()=>Ie(this,void 0,void 0,function*(){return t})}}setupCustomHover(n,t,l){t.setAttribute("title",""),t.removeAttribute("title");let h=this.getTooltipForCustom(l);const m=C.isMacintosh?1500:500;let _,f,v=!1,y,L;function I(E){if(!v){y=new c.CancellationTokenSource;function T(D){(D.type===b.EventType.MOUSE_DOWN||D.fromElement===t)&&(L==null||L.dispose(),L=void 0,v=!1,_=void 0,y.dispose(!0),O.dispose(),A.dispose())}const O=g.domEvent(t,b.EventType.MOUSE_LEAVE,!0)(T.bind(t)),A=g.domEvent(t,b.EventType.MOUSE_DOWN,!0)(T.bind(t));v=!0;function B(D){f=D.x}const F=g.domEvent(t,b.EventType.MOUSE_MOVE,!0)(B.bind(t));setTimeout(()=>Ie(this,void 0,void 0,function*(){var D;if(v&&h&&!_){const R={targetElements:[this],dispose:()=>{}};_={text:p.localize(0,null),target:R,anchorPosition:0},L=s.adjustXAndShowCustomHover(_,f,n,v);const W=(D=yield h(y.token))!==null&&D!==void 0?D:d.isString(l)?void 0:l.markdownNotSupportedFallback;W?(_={text:W,target:R,anchorPosition:0},L=s.adjustXAndShowCustomHover(_,f,n,v)):L&&(L.dispose(),L=void 0)}F.dispose()}),m)}}const k=this._register(g.domEvent(t,b.EventType.MOUSE_OVER,!0)(I.bind(t)));this.customHovers.set(t,k)}setupNativeHover(n,t){let l="";d.isString(t)?l=t:(t==null?void 0:t.markdownNotSupportedFallback)&&(l=t.markdownNotSupportedFallback),n.title=l}}e.IconLabel=s;class a{constructor(n){this.container=n,this.label=void 0,this.singleLabel=void 0}setLabel(n,t){if(!(this.label===n&&S.equals(this.options,t)))if(this.label=n,this.options=t,typeof n=="string")this.singleLabel||(this.container.innerText="",this.container.classList.remove("multiple"),this.singleLabel=b.append(this.container,b.$("a.label-name",{id:t==null?void 0:t.domId}))),this.singleLabel.textContent=n;else{this.container.innerText="",this.container.classList.add("multiple"),this.singleLabel=void 0;for(let l=0;l{const m={start:l,end:l+h.length},_=t.map(f=>w.Range.intersect(m,f)).filter(f=>!w.Range.isEmpty(f)).map(({start:f,end:v})=>({start:f-l,end:v-l}));return l=m.end+n.length,_})}}class r{constructor(n,t){this.container=n,this.supportIcons=t,this.label=void 0,this.singleLabel=void 0}setLabel(n,t){if(!(this.label===n&&S.equals(this.options,t)))if(this.label=n,this.options=t,typeof n=="string")this.singleLabel||(this.container.innerText="",this.container.classList.remove("multiple"),this.singleLabel=new N.HighlightedLabel(b.append(this.container,b.$("a.label-name",{id:t==null?void 0:t.domId})),this.supportIcons)),this.singleLabel.set(n,t==null?void 0:t.matches,void 0,t==null?void 0:t.labelEscapeNewLines);else{this.container.innerText="",this.container.classList.add("multiple"),this.singleLabel=void 0;const l=(t==null?void 0:t.separator)||"/",h=u(n,l,t==null?void 0:t.matches);for(let m=0;mW.element));this.data=R}}function h(D){return D instanceof p.ElementsDragAndDropData?new l(D):D}class m{constructor(R,W){this.modelProvider=R,this.dnd=W,this.autoExpandDisposable=b.Disposable.None}getDragURI(R){return this.dnd.getDragURI(R.element)}getDragLabel(R,W){if(this.dnd.getDragLabel)return this.dnd.getDragLabel(R.map(x=>x.element),W)}onDragStart(R,W){this.dnd.onDragStart&&this.dnd.onDragStart(h(R),W)}onDragOver(R,W,x,K,Y=!0){const ee=this.dnd.onDragOver(h(R),W&&W.element,x,K),se=this.autoExpandNode!==W;if(se&&(this.autoExpandDisposable.dispose(),this.autoExpandNode=W),typeof W=="undefined")return ee;if(se&&typeof ee!="boolean"&&ee.autoExpand&&(this.autoExpandDisposable=u.disposableTimeout(()=>{const P=this.modelProvider(),V=P.getNodeLocation(W);P.isCollapsed(V)&&P.setCollapsed(V,!1),this.autoExpandNode=void 0},500)),typeof ee=="boolean"||!ee.accept||typeof ee.bubble=="undefined"||ee.feedback){if(!Y){const P=typeof ee=="boolean"?ee:ee.accept,V=typeof ee=="boolean"?void 0:ee.effect;return{accept:P,effect:V,feedback:[x]}}return ee}if(ee.bubble===1){const P=this.modelProvider(),V=P.getNodeLocation(W),U=P.getParentNodeLocation(V),H=P.getNode(U),$=U&&P.getListIndex(U);return this.onDragOver(R,H,$,K,!1)}const ne=this.modelProvider(),le=ne.getNodeLocation(W),X=ne.getListIndex(le),z=ne.getListRenderCount(le);return Object.assign(Object.assign({},ee),{feedback:g.range(X,X+z)})}drop(R,W,x,K){this.autoExpandDisposable.dispose(),this.autoExpandNode=void 0,this.dnd.drop(h(R),W&&W.element,x,K)}onDragEnd(R){this.dnd.onDragEnd&&this.dnd.onDragEnd(R)}}function _(D,R){return R&&Object.assign(Object.assign({},R),{identityProvider:R.identityProvider&&{getId(W){return R.identityProvider.getId(W.element)}},dnd:R.dnd&&new m(D,R.dnd),multipleSelectionController:R.multipleSelectionController&&{isSelectionSingleChangeEvent(W){return R.multipleSelectionController.isSelectionSingleChangeEvent(Object.assign(Object.assign({},W),{element:W.element}))},isSelectionRangeChangeEvent(W){return R.multipleSelectionController.isSelectionRangeChangeEvent(Object.assign(Object.assign({},W),{element:W.element}))}},accessibilityProvider:R.accessibilityProvider&&Object.assign(Object.assign({},R.accessibilityProvider),{getSetSize(W){const x=D(),K=x.getNodeLocation(W),Y=x.getParentNodeLocation(K);return x.getNode(Y).visibleChildrenCount},getPosInSet(W){return W.visibleChildIndex+1},isChecked:R.accessibilityProvider&&R.accessibilityProvider.isChecked?W=>R.accessibilityProvider.isChecked(W.element):void 0,getRole:R.accessibilityProvider&&R.accessibilityProvider.getRole?W=>R.accessibilityProvider.getRole(W.element):()=>"treeitem",getAriaLabel(W){return R.accessibilityProvider.getAriaLabel(W.element)},getWidgetAriaLabel(){return R.accessibilityProvider.getWidgetAriaLabel()},getWidgetRole:R.accessibilityProvider&&R.accessibilityProvider.getWidgetRole?()=>R.accessibilityProvider.getWidgetRole():()=>"tree",getAriaLevel:R.accessibilityProvider&&R.accessibilityProvider.getAriaLevel?W=>R.accessibilityProvider.getAriaLevel(W.element):W=>W.depth,getActiveDescendantId:R.accessibilityProvider.getActiveDescendantId&&(W=>R.accessibilityProvider.getActiveDescendantId(W.element))}),keyboardNavigationLabelProvider:R.keyboardNavigationLabelProvider&&Object.assign(Object.assign({},R.keyboardNavigationLabelProvider),{getKeyboardNavigationLabel(W){return R.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(W.element)}}),enableKeyboardNavigation:R.simpleKeyboardNavigation})}class f{constructor(R){this.delegate=R}getHeight(R){return this.delegate.getHeight(R.element)}getTemplateId(R){return this.delegate.getTemplateId(R.element)}hasDynamicHeight(R){return!!this.delegate.hasDynamicHeight&&this.delegate.hasDynamicHeight(R.element)}setDynamicHeight(R,W){this.delegate.setDynamicHeight&&this.delegate.setDynamicHeight(R.element,W)}}e.ComposedTreeDelegate=f;var v;(function(D){D.None="none",D.OnHover="onHover",D.Always="always"})(v=e.RenderIndentGuides||(e.RenderIndentGuides={}));class y{constructor(R,W=[]){this._elements=W,this.onDidChange=w.Event.forEach(R,x=>this._elements=x)}get elements(){return this._elements}}class L{constructor(R,W,x,K,Y={}){this.renderer=R,this.modelProvider=W,this.activeNodes=K,this.renderedElements=new Map,this.renderedNodes=new Map,this.indent=L.DefaultIndent,this.hideTwistiesOfChildlessElements=!1,this.shouldRenderIndentGuides=!1,this.renderedIndentGuides=new n.SetMap,this.activeIndentNodes=new Set,this.indentGuidesDisposable=b.Disposable.None,this.disposables=new b.DisposableStore,this.templateId=R.templateId,this.updateOptions(Y),w.Event.map(x,ee=>ee.node)(this.onDidChangeNodeTwistieState,this,this.disposables),R.onDidChangeTwistieState&&R.onDidChangeTwistieState(this.onDidChangeTwistieState,this,this.disposables)}updateOptions(R={}){if(typeof R.indent!="undefined"&&(this.indent=i.clamp(R.indent,0,40)),typeof R.renderIndentGuides!="undefined"){const W=R.renderIndentGuides!==v.None;if(W!==this.shouldRenderIndentGuides&&(this.shouldRenderIndentGuides=W,this.indentGuidesDisposable.dispose(),W)){const x=new b.DisposableStore;this.activeNodes.onDidChange(this._onDidChangeActiveNodes,this,x),this.indentGuidesDisposable=x,this._onDidChangeActiveNodes(this.activeNodes.elements)}}typeof R.hideTwistiesOfChildlessElements!="undefined"&&(this.hideTwistiesOfChildlessElements=R.hideTwistiesOfChildlessElements)}renderTemplate(R){const W=M.append(R,M.$(".monaco-tl-row")),x=M.append(W,M.$(".monaco-tl-indent")),K=M.append(W,M.$(".monaco-tl-twistie")),Y=M.append(W,M.$(".monaco-tl-contents")),ee=this.renderer.renderTemplate(Y);return{container:R,indent:x,twistie:K,indentGuidesDisposable:b.Disposable.None,templateData:ee}}renderElement(R,W,x,K){typeof K=="number"&&(this.renderedNodes.set(R,{templateData:x,height:K}),this.renderedElements.set(R.element,R));const Y=L.DefaultIndent+(R.depth-1)*this.indent;x.twistie.style.paddingLeft=`${Y}px`,x.indent.style.width=`${Y+this.indent-16}px`,this.renderTwistie(R,x),typeof K=="number"&&this.renderIndentGuides(R,x),this.renderer.renderElement(R,W,x.templateData,K)}disposeElement(R,W,x,K){x.indentGuidesDisposable.dispose(),this.renderer.disposeElement&&this.renderer.disposeElement(R,W,x.templateData,K),typeof K=="number"&&(this.renderedNodes.delete(R),this.renderedElements.delete(R.element))}disposeTemplate(R){this.renderer.disposeTemplate(R.templateData)}onDidChangeTwistieState(R){const W=this.renderedElements.get(R);!W||this.onDidChangeNodeTwistieState(W)}onDidChangeNodeTwistieState(R){const W=this.renderedNodes.get(R);!W||(this.renderTwistie(R,W.templateData),this._onDidChangeActiveNodes(this.activeNodes.elements),this.renderIndentGuides(R,W.templateData))}renderTwistie(R,W){W.twistie.classList.remove(...t.treeItemExpandedIcon.classNamesArray);let x=!1;this.renderer.renderTwistie&&(x=this.renderer.renderTwistie(R.element,W.twistie)),R.collapsible&&(!this.hideTwistiesOfChildlessElements||R.visibleChildrenCount>0)?(x||W.twistie.classList.add(...t.treeItemExpandedIcon.classNamesArray),W.twistie.classList.add("collapsible"),W.twistie.classList.toggle("collapsed",R.collapsed)):W.twistie.classList.remove("collapsible","collapsed"),R.collapsible?W.container.setAttribute("aria-expanded",String(!R.collapsed)):W.container.removeAttribute("aria-expanded")}renderIndentGuides(R,W){if(M.clearNode(W.indent),W.indentGuidesDisposable.dispose(),!!this.shouldRenderIndentGuides){const x=new b.DisposableStore,K=this.modelProvider();let Y=R;for(;;){const ee=K.getNodeLocation(Y),se=K.getParentNodeLocation(ee);if(!se)break;const ne=K.getNode(se),le=M.$(".indent-guide",{style:`width: ${this.indent}px`});this.activeIndentNodes.has(ne)&&le.classList.add("active"),W.indent.childElementCount===0?W.indent.appendChild(le):W.indent.insertBefore(le,W.indent.firstElementChild),this.renderedIndentGuides.add(ne,le),x.add(b.toDisposable(()=>this.renderedIndentGuides.delete(ne,le))),Y=ne}W.indentGuidesDisposable=x}}_onDidChangeActiveNodes(R){if(!!this.shouldRenderIndentGuides){const W=new Set,x=this.modelProvider();R.forEach(K=>{const Y=x.getNodeLocation(K);try{const ee=x.getParentNodeLocation(Y);K.collapsible&&K.children.length>0&&!K.collapsed?W.add(K):ee&&W.add(x.getNode(ee))}catch(ee){}}),this.activeIndentNodes.forEach(K=>{W.has(K)||this.renderedIndentGuides.forEach(K,Y=>Y.classList.remove("active"))}),W.forEach(K=>{this.activeIndentNodes.has(K)||this.renderedIndentGuides.forEach(K,Y=>Y.classList.add("active"))}),this.activeIndentNodes=W}}dispose(){this.renderedNodes.clear(),this.renderedElements.clear(),this.indentGuidesDisposable.dispose(),b.dispose(this.disposables)}}L.DefaultIndent=8;class I{constructor(R,W,x){this.tree=R,this.keyboardNavigationLabelProvider=W,this._filter=x,this._totalCount=0,this._matchCount=0,this._pattern="",this._lowercasePattern="",this.disposables=new b.DisposableStore,R.onWillRefilter(this.reset,this,this.disposables)}get totalCount(){return this._totalCount}get matchCount(){return this._matchCount}set pattern(R){this._pattern=R,this._lowercasePattern=R.toLowerCase()}filter(R,W){if(this._filter){const Y=this._filter.filter(R,W);if(this.tree.options.simpleKeyboardNavigation)return Y;let ee;if(typeof Y=="boolean"?ee=Y?1:0:s.isFilterResult(Y)?ee=s.getVisibleState(Y.visibility):ee=Y,ee===0)return!1}if(this._totalCount++,this.tree.options.simpleKeyboardNavigation||!this._pattern)return this._matchCount++,{data:o.FuzzyScore.Default,visibility:!0};const x=this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(R),K=Array.isArray(x)?x:[x];for(const Y of K){const ee=Y&&Y.toString();if(typeof ee=="undefined")return{data:o.FuzzyScore.Default,visibility:!0};const se=o.fuzzyScore(this._pattern,this._lowercasePattern,0,ee,ee.toLowerCase(),0,!0);if(se)return this._matchCount++,K.length===1?{data:se,visibility:!0}:{data:{label:ee,score:se},visibility:!0}}return this.tree.options.filterOnType?2:{data:o.FuzzyScore.Default,visibility:!0}}reset(){this._totalCount=0,this._matchCount=0}dispose(){b.dispose(this.disposables)}}class k{constructor(R,W,x,K,Y){this.tree=R,this.view=x,this.filter=K,this.keyboardNavigationDelegate=Y,this._enabled=!1,this._pattern="",this._empty=!1,this._onDidChangeEmptyState=new w.Emitter,this.positionClassName="ne",this.automaticKeyboardNavigation=!0,this.triggered=!1,this._onDidChangePattern=new w.Emitter,this.enabledDisposables=new b.DisposableStore,this.disposables=new b.DisposableStore,this.domNode=M.$(`.monaco-list-type-filter.${this.positionClassName}`),this.domNode.draggable=!0,c.domEvent(this.domNode,"dragstart")(this.onDragStart,this,this.disposables),this.messageDomNode=M.append(x.getHTMLElement(),M.$(".monaco-list-type-filter-message")),this.labelDomNode=M.append(this.domNode,M.$("span.label"));const ee=M.append(this.domNode,M.$(".controls"));this._filterOnType=!!R.options.filterOnType,this.filterOnTypeDomNode=M.append(ee,M.$("input.filter")),this.filterOnTypeDomNode.type="checkbox",this.filterOnTypeDomNode.checked=this._filterOnType,this.filterOnTypeDomNode.tabIndex=-1,this.updateFilterOnTypeTitleAndIcon(),c.domEvent(this.filterOnTypeDomNode,"input")(this.onDidChangeFilterOnType,this,this.disposables),this.clearDomNode=M.append(ee,M.$("button.clear"+t.treeFilterClearIcon.cssSelector)),this.clearDomNode.tabIndex=-1,this.clearDomNode.title=a.localize(0,null),this.keyboardNavigationEventFilter=R.options.keyboardNavigationEventFilter,W.onDidSplice(this.onDidSpliceModel,this,this.disposables),this.updateOptions(R.options)}get enabled(){return this._enabled}get pattern(){return this._pattern}get filterOnType(){return this._filterOnType}updateOptions(R){R.simpleKeyboardNavigation?this.disable():this.enable(),typeof R.filterOnType!="undefined"&&(this._filterOnType=!!R.filterOnType,this.filterOnTypeDomNode.checked=this._filterOnType),typeof R.automaticKeyboardNavigation!="undefined"&&(this.automaticKeyboardNavigation=R.automaticKeyboardNavigation),this.tree.refilter(),this.render(),this.automaticKeyboardNavigation||this.onEventOrInput("")}enable(){if(!this._enabled){const R=w.Event.chain(c.domEvent(this.view.getHTMLElement(),"keydown")).filter(x=>!N.isInputElement(x.target)||x.target===this.filterOnTypeDomNode).filter(x=>x.key!=="Dead"&&!/^Media/.test(x.key)).map(x=>new S.StandardKeyboardEvent(x)).filter(this.keyboardNavigationEventFilter||(()=>!0)).filter(()=>this.automaticKeyboardNavigation||this.triggered).filter(x=>this.keyboardNavigationDelegate.mightProducePrintableCharacter(x)&&!(x.keyCode===18||x.keyCode===16||x.keyCode===15||x.keyCode===17)||(this.pattern.length>0||this.triggered)&&(x.keyCode===9||x.keyCode===1)&&!x.altKey&&!x.ctrlKey&&!x.metaKey||x.keyCode===1&&(r.isMacintosh?x.altKey&&!x.metaKey:x.ctrlKey)&&!x.shiftKey).forEach(x=>{x.stopPropagation(),x.preventDefault()}).event,W=c.domEvent(this.clearDomNode,"click");w.Event.chain(w.Event.any(R,W)).event(this.onEventOrInput,this,this.enabledDisposables),this.filter.pattern="",this.tree.refilter(),this.render(),this._enabled=!0,this.triggered=!1}}disable(){!this._enabled||(this.domNode.remove(),this.enabledDisposables.clear(),this.tree.refilter(),this.render(),this._enabled=!1,this.triggered=!1)}onEventOrInput(R){typeof R=="string"?this.onInput(R):R instanceof MouseEvent||R.keyCode===9||R.keyCode===1&&(r.isMacintosh?R.altKey:R.ctrlKey)?this.onInput(""):R.keyCode===1?this.onInput(this.pattern.length===0?"":this.pattern.substr(0,this.pattern.length-1)):this.onInput(this.pattern+R.browserEvent.key)}onInput(R){const W=this.view.getHTMLElement();R&&!this.domNode.parentElement?W.append(this.domNode):!R&&this.domNode.parentElement&&(this.domNode.remove(),this.tree.domFocus()),this._pattern=R,this._onDidChangePattern.fire(R),this.filter.pattern=R,this.tree.refilter(),R&&this.tree.focusNext(0,!0,void 0,K=>!o.FuzzyScore.isDefault(K.filterData));const x=this.tree.getFocus();if(x.length>0){const K=x[0];this.tree.getRelativeTop(K)===null&&this.tree.reveal(K,.5)}this.render(),R||(this.triggered=!1)}onDragStart(){const R=this.view.getHTMLElement(),{left:W}=M.getDomNodePagePosition(R),x=R.clientWidth,K=x/2,Y=this.domNode.clientWidth,ee=new b.DisposableStore;let se=this.positionClassName;const ne=()=>{switch(se){case"nw":this.domNode.style.top="4px",this.domNode.style.left="4px";break;case"ne":this.domNode.style.top="4px",this.domNode.style.left=`${x-Y-6}px`;break}},le=z=>{z.preventDefault();const P=z.clientX-W;z.dataTransfer&&(z.dataTransfer.dropEffect="none"),P{this.positionClassName=se,this.domNode.className=`monaco-list-type-filter ${this.positionClassName}`,this.domNode.style.top="",this.domNode.style.left="",b.dispose(ee)};ne(),this.domNode.classList.remove(se),this.domNode.classList.add("dragging"),ee.add(b.toDisposable(()=>this.domNode.classList.remove("dragging"))),c.domEvent(document,"dragover")(le,null,ee),c.domEvent(this.domNode,"dragend")(X,null,ee),d.StaticDND.CurrentDragAndDropData=new d.DragAndDropData("vscode-ui"),ee.add(b.toDisposable(()=>d.StaticDND.CurrentDragAndDropData=void 0))}onDidSpliceModel(){!this._enabled||this.pattern.length===0||(this.tree.refilter(),this.render())}onDidChangeFilterOnType(){this.tree.updateOptions({filterOnType:this.filterOnTypeDomNode.checked}),this.tree.refilter(),this.tree.domFocus(),this.render(),this.updateFilterOnTypeTitleAndIcon()}updateFilterOnTypeTitleAndIcon(){this.filterOnType?(this.filterOnTypeDomNode.classList.remove(...t.treeFilterOnTypeOffIcon.classNamesArray),this.filterOnTypeDomNode.classList.add(...t.treeFilterOnTypeOnIcon.classNamesArray),this.filterOnTypeDomNode.title=a.localize(1,null)):(this.filterOnTypeDomNode.classList.remove(...t.treeFilterOnTypeOnIcon.classNamesArray),this.filterOnTypeDomNode.classList.add(...t.treeFilterOnTypeOffIcon.classNamesArray),this.filterOnTypeDomNode.title=a.localize(2,null))}render(){const R=this.filter.totalCount>0&&this.filter.matchCount===0;this.pattern&&this.tree.options.filterOnType&&R?(this.messageDomNode.textContent=a.localize(3,null),this._empty=!0):(this.messageDomNode.innerText="",this._empty=!1),this.domNode.classList.toggle("no-matches",R),this.domNode.title=a.localize(4,null,this.filter.matchCount,this.filter.totalCount),this.labelDomNode.textContent=this.pattern.length>16?"\u2026"+this.pattern.substr(this.pattern.length-16):this.pattern,this._onDidChangeEmptyState.fire(this._empty)}shouldAllowFocus(R){return!this.enabled||!this.pattern||this.filterOnType||this.filter.totalCount>0&&this.filter.matchCount<=1?!0:!o.FuzzyScore.isDefault(R.filterData)}dispose(){this._enabled&&(this.domNode.remove(),this.enabledDisposables.dispose(),this._enabled=!1,this.triggered=!1),this._onDidChangePattern.dispose(),b.dispose(this.disposables)}}function E(D){let R=C.TreeMouseEventTarget.Unknown;return M.hasParentWithClass(D.browserEvent.target,"monaco-tl-twistie","monaco-tl-row")?R=C.TreeMouseEventTarget.Twistie:M.hasParentWithClass(D.browserEvent.target,"monaco-tl-contents","monaco-tl-row")&&(R=C.TreeMouseEventTarget.Element),{browserEvent:D.browserEvent,element:D.element?D.element.element:null,target:R}}function T(D,R){R(D),D.children.forEach(W=>T(W,R))}class O{constructor(R){this.identityProvider=R,this.nodes=[],this._onDidChange=new w.Emitter,this.onDidChange=this._onDidChange.event}get nodeSet(){return this._nodeSet||(this._nodeSet=this.createNodeSet()),this._nodeSet}set(R,W){var x;!((x=W)===null||x===void 0?void 0:x.__forceEvent)&&g.equals(this.nodes,R)||this._set(R,!1,W)}_set(R,W,x){if(this.nodes=[...R],this.elements=void 0,this._nodeSet=void 0,!W){const K=this;this._onDidChange.fire({get elements(){return K.get()},browserEvent:x})}}get(){return this.elements||(this.elements=this.nodes.map(R=>R.element)),[...this.elements]}getNodes(){return this.nodes}has(R){return this.nodeSet.has(R)}onDidModelSplice({insertedNodes:R,deletedNodes:W}){if(!this.identityProvider){const ne=this.createNodeSet(),le=X=>ne.delete(X);W.forEach(X=>T(X,le)),this.set([...ne.values()]);return}const x=new Set,K=ne=>x.add(this.identityProvider.getId(ne.element).toString());W.forEach(ne=>T(ne,K));const Y=new Map,ee=ne=>Y.set(this.identityProvider.getId(ne.element).toString(),ne);R.forEach(ne=>T(ne,ee));const se=[];for(const ne of this.nodes){const le=this.identityProvider.getId(ne.element).toString();if(!x.has(le))se.push(ne);else{const z=Y.get(le);z&&se.push(z)}}this._set(se,!0)}createNodeSet(){const R=new Set;for(const W of this.nodes)R.add(W);return R}}class A extends N.MouseController{constructor(R,W){super(R);this.tree=W}onViewPointer(R){if(!(N.isInputElement(R.browserEvent.target)||N.isMonacoEditor(R.browserEvent.target))){const W=R.element;if(!W)return super.onViewPointer(R);if(this.isSelectionRangeChangeEvent(R)||this.isSelectionSingleChangeEvent(R))return super.onViewPointer(R);const x=R.browserEvent.target,K=x.classList.contains("monaco-tl-twistie")||x.classList.contains("monaco-icon-label")&&x.classList.contains("folder-icon")&&R.browserEvent.offsetX<16;let Y=!1;if(typeof this.tree.expandOnlyOnTwistieClick=="function"?Y=this.tree.expandOnlyOnTwistieClick(W.element):Y=!!this.tree.expandOnlyOnTwistieClick,Y&&!K&&R.browserEvent.detail!==2)return super.onViewPointer(R);if(!this.tree.expandOnDoubleClick&&R.browserEvent.detail===2)return super.onViewPointer(R);if(W.collapsible){const ee=this.tree.model,se=ee.getNodeLocation(W),ne=R.browserEvent.altKey;if(this.tree.setFocus([se]),ee.setCollapsed(se,void 0,ne),Y&&K)return}super.onViewPointer(R)}}onDoubleClick(R){R.browserEvent.target.classList.contains("monaco-tl-twistie")||!this.tree.expandOnDoubleClick||super.onDoubleClick(R)}}class B extends N.List{constructor(R,W,x,K,Y,ee,se){super(R,W,x,K,se);this.focusTrait=Y,this.selectionTrait=ee}createMouseController(R){return new A(this,R.tree)}splice(R,W,x=[]){if(super.splice(R,W,x),x.length!==0){const K=[],Y=[];x.forEach((ee,se)=>{this.focusTrait.has(ee)&&K.push(R+se),this.selectionTrait.has(ee)&&Y.push(R+se)}),K.length>0&&super.setFocus(g.distinctES6([...super.getFocus(),...K])),Y.length>0&&super.setSelection(g.distinctES6([...super.getSelection(),...Y]))}}setFocus(R,W,x=!1){super.setFocus(R,W),x||this.focusTrait.set(R.map(K=>this.element(K)),W)}setSelection(R,W,x=!1){super.setSelection(R,W),x||this.selectionTrait.set(R.map(K=>this.element(K)),W)}}class F{constructor(R,W,x,K,Y={}){this._options=Y,this.eventBufferer=new w.EventBufferer,this.disposables=new b.DisposableStore,this._onWillRefilter=new w.Emitter,this.onWillRefilter=this._onWillRefilter.event,this._onDidUpdateOptions=new w.Emitter;const ee=new f(x),se=new w.Relay,ne=new w.Relay,le=new y(ne.event);this.renderers=K.map(P=>new L(P,()=>this.model,se.event,le,Y));for(let P of this.renderers)this.disposables.add(P);let X;Y.keyboardNavigationLabelProvider&&(X=new I(this,Y.keyboardNavigationLabelProvider,Y.filter),Y=Object.assign(Object.assign({},Y),{filter:X}),this.disposables.add(X)),this.focus=new O(Y.identityProvider),this.selection=new O(Y.identityProvider),this.view=new B(R,W,ee,this.renderers,this.focus,this.selection,Object.assign(Object.assign({},_(()=>this.model,Y)),{tree:this})),this.model=this.createModel(R,this.view,Y),se.input=this.model.onDidChangeCollapseState;const z=w.Event.forEach(this.model.onDidSplice,P=>{this.eventBufferer.bufferEvents(()=>{this.focus.onDidModelSplice(P),this.selection.onDidModelSplice(P)})});if(z(()=>null,null,this.disposables),ne.input=w.Event.chain(w.Event.any(z,this.focus.onDidChange,this.selection.onDidChange)).debounce(()=>null,0).map(()=>{const P=new Set;for(const V of this.focus.getNodes())P.add(V);for(const V of this.selection.getNodes())P.add(V);return[...P.values()]}).event,Y.keyboardSupport!==!1){const P=w.Event.chain(this.view.onKeyDown).filter(V=>!N.isInputElement(V.target)).map(V=>new S.StandardKeyboardEvent(V));P.filter(V=>V.keyCode===15).on(this.onLeftArrow,this,this.disposables),P.filter(V=>V.keyCode===17).on(this.onRightArrow,this,this.disposables),P.filter(V=>V.keyCode===10).on(this.onSpace,this,this.disposables)}if(Y.keyboardNavigationLabelProvider){const P=Y.keyboardNavigationDelegate||N.DefaultKeyboardNavigationDelegate;this.typeFilterController=new k(this,this.model,this.view,X,P),this.focusNavigationFilter=V=>this.typeFilterController.shouldAllowFocus(V),this.disposables.add(this.typeFilterController)}this.styleElement=M.createStyleSheet(this.view.getHTMLElement()),this.getHTMLElement().classList.toggle("always",this._options.renderIndentGuides===v.Always)}get onDidChangeFocus(){return this.eventBufferer.wrapEvent(this.focus.onDidChange)}get onDidChangeSelection(){return this.eventBufferer.wrapEvent(this.selection.onDidChange)}get onMouseDblClick(){return w.Event.map(this.view.onMouseDblClick,E)}get onPointer(){return w.Event.map(this.view.onPointer,E)}get onDidFocus(){return this.view.onDidFocus}get onDidChangeCollapseState(){return this.model.onDidChangeCollapseState}get expandOnDoubleClick(){return typeof this._options.expandOnDoubleClick=="undefined"?!0:this._options.expandOnDoubleClick}get expandOnlyOnTwistieClick(){return typeof this._options.expandOnlyOnTwistieClick=="undefined"?!0:this._options.expandOnlyOnTwistieClick}get onDidDispose(){return this.view.onDidDispose}updateOptions(R={}){this._options=Object.assign(Object.assign({},this._options),R);for(const W of this.renderers)W.updateOptions(R);this.view.updateOptions({enableKeyboardNavigation:this._options.simpleKeyboardNavigation,automaticKeyboardNavigation:this._options.automaticKeyboardNavigation,smoothScrolling:this._options.smoothScrolling,horizontalScrolling:this._options.horizontalScrolling}),this.typeFilterController&&this.typeFilterController.updateOptions(this._options),this._onDidUpdateOptions.fire(this._options),this.getHTMLElement().classList.toggle("always",this._options.renderIndentGuides===v.Always)}get options(){return this._options}getHTMLElement(){return this.view.getHTMLElement()}get scrollTop(){return this.view.scrollTop}set scrollTop(R){this.view.scrollTop=R}domFocus(){this.view.domFocus()}layout(R,W){this.view.layout(R,W)}style(R){const W=`.${this.view.domId}`,x=[];R.treeIndentGuidesStroke&&(x.push(`.monaco-list${W}:hover .monaco-tl-indent > .indent-guide, .monaco-list${W}.always .monaco-tl-indent > .indent-guide { border-color: ${R.treeIndentGuidesStroke.transparent(.4)}; }`),x.push(`.monaco-list${W} .monaco-tl-indent > .indent-guide.active { border-color: ${R.treeIndentGuidesStroke}; }`)),this.styleElement.textContent=x.join(` +`),this.view.style(R)}collapse(R,W=!1){return this.model.setCollapsed(R,!0,W)}expand(R,W=!1){return this.model.setCollapsed(R,!1,W)}isCollapsible(R){return this.model.isCollapsible(R)}setCollapsible(R,W){return this.model.setCollapsible(R,W)}isCollapsed(R){return this.model.isCollapsed(R)}refilter(){this._onWillRefilter.fire(void 0),this.model.refilter()}setSelection(R,W){const x=R.map(Y=>this.model.getNode(Y));this.selection.set(x,W);const K=R.map(Y=>this.model.getListIndex(Y)).filter(Y=>Y>-1);this.view.setSelection(K,W,!0)}getSelection(){return this.selection.get()}setFocus(R,W){const x=R.map(Y=>this.model.getNode(Y));this.focus.set(x,W);const K=R.map(Y=>this.model.getListIndex(Y)).filter(Y=>Y>-1);this.view.setFocus(K,W,!0)}focusNext(R=1,W=!1,x,K=this.focusNavigationFilter){this.view.focusNext(R,W,x,K)}getFocus(){return this.focus.get()}reveal(R,W){this.model.expandTo(R);const x=this.model.getListIndex(R);x!==-1&&this.view.reveal(x,W)}getRelativeTop(R){const W=this.model.getListIndex(R);return W===-1?null:this.view.getRelativeTop(W)}onLeftArrow(R){R.preventDefault(),R.stopPropagation();const W=this.view.getFocusedElements();if(W.length!==0){const x=W[0],K=this.model.getNodeLocation(x);if(!this.model.setCollapsed(K,!0)){const ee=this.model.getParentNodeLocation(K);if(!ee)return;const se=this.model.getListIndex(ee);this.view.reveal(se),this.view.setFocus([se])}}}onRightArrow(R){R.preventDefault(),R.stopPropagation();const W=this.view.getFocusedElements();if(W.length!==0){const x=W[0],K=this.model.getNodeLocation(x);if(!this.model.setCollapsed(K,!1)){if(!x.children.some(ne=>ne.visible))return;const[ee]=this.view.getFocus(),se=ee+1;this.view.reveal(se),this.view.setFocus([se])}}}onSpace(R){R.preventDefault(),R.stopPropagation();const W=this.view.getFocusedElements();if(W.length!==0){const x=W[0],K=this.model.getNodeLocation(x),Y=R.browserEvent.altKey;this.model.setCollapsed(K,void 0,Y)}}dispose(){b.dispose(this.disposables),this.view.dispose()}}e.AbstractTree=F}),define(Q[427],J([0,1,176,155]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DataTree=void 0;class M extends b.AbstractTree{constructor(S,C,d,g,p,c={}){super(S,C,d,g,c);this.user=S,this.dataSource=p,this.identityProvider=c.identityProvider}createModel(S,C,d){return new N.ObjectTreeModel(S,C,d)}}e.DataTree=M}),define(Q[234],J([0,1,54,176,155,284,99]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CompressibleObjectTree=e.ObjectTree=void 0;class C extends N.AbstractTree{constructor(o,s,a,u,r={}){super(o,s,a,u,r)}get onDidChangeCollapseState(){return this.model.onDidChangeCollapseState}setChildren(o,s=b.Iterable.empty(),a){this.model.setChildren(o,s,a)}rerender(o){if(o===void 0){this.view.rerender();return}this.model.rerender(o)}hasElement(o){return this.model.has(o)}createModel(o,s,a){return new M.ObjectTreeModel(o,s,a)}}e.ObjectTree=C;class d{constructor(o,s){this._compressedTreeNodeProvider=o,this.renderer=s,this.templateId=s.templateId,s.onDidChangeTwistieState&&(this.onDidChangeTwistieState=s.onDidChangeTwistieState)}get compressedTreeNodeProvider(){return this._compressedTreeNodeProvider()}renderTemplate(o){const s=this.renderer.renderTemplate(o);return{compressedTreeNode:void 0,data:s}}renderElement(o,s,a,u){const r=this.compressedTreeNodeProvider.getCompressedTreeNode(o.element);r.element.elements.length===1?(a.compressedTreeNode=void 0,this.renderer.renderElement(o,s,a.data,u)):(a.compressedTreeNode=r,this.renderer.renderCompressedElements(r,s,a.data,u))}disposeElement(o,s,a,u){a.compressedTreeNode?this.renderer.disposeCompressedElements&&this.renderer.disposeCompressedElements(a.compressedTreeNode,s,a.data,u):this.renderer.disposeElement&&this.renderer.disposeElement(o,s,a.data,u)}disposeTemplate(o){this.renderer.disposeTemplate(o.data)}renderTwistie(o,s){return this.renderer.renderTwistie?this.renderer.renderTwistie(o,s):!1}}Me([S.memoize],d.prototype,"compressedTreeNodeProvider",null);function g(c,o){return o&&Object.assign(Object.assign({},o),{keyboardNavigationLabelProvider:o.keyboardNavigationLabelProvider&&{getKeyboardNavigationLabel(s){let a;try{a=c().getCompressedTreeNode(s)}catch(u){return o.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(s)}return a.element.elements.length===1?o.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(s):o.keyboardNavigationLabelProvider.getCompressedNodeKeyboardNavigationLabel(a.element.elements)}}})}class p extends C{constructor(o,s,a,u,r={}){const i=()=>this,n=u.map(t=>new d(i,t));super(o,s,a,n,g(i,r))}setChildren(o,s=b.Iterable.empty(),a){this.model.setChildren(o,s,a)}createModel(o,s,a){return new w.CompressibleObjectTreeModel(o,s,a)}updateOptions(o={}){super.updateOptions(o),typeof o.compressionEnabled!="undefined"&&this.model.setCompressionEnabled(o.compressionEnabled)}getCompressedTreeNode(o=null){return this.model.getCompressedTreeNode(o)}}e.CompressibleObjectTree=p}),define(Q[428],J([0,1,176,234,98,2,6,15,54,161,12,154,197]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CompressibleAsyncDataTree=e.AsyncDataTree=void 0;function s(T){return Object.assign(Object.assign({},T),{children:[],refreshPromise:void 0,stale:!0,slow:!1,collapsedByDefault:void 0})}function a(T,O){return O.parent?O.parent===T?!0:a(T,O.parent):!1}function u(T,O){return T===O||a(T,O)||a(O,T)}class r{constructor(O){this.node=O}get element(){return this.node.element.element}get children(){return this.node.children.map(O=>new r(O))}get depth(){return this.node.depth}get visibleChildrenCount(){return this.node.visibleChildrenCount}get visibleChildIndex(){return this.node.visibleChildIndex}get collapsible(){return this.node.collapsible}get collapsed(){return this.node.collapsed}get visible(){return this.node.visible}get filterData(){return this.node.filterData}}class i{constructor(O,A,B){this.renderer=O,this.nodeMapper=A,this.onDidChangeTwistieState=B,this.renderedNodes=new Map,this.templateId=O.templateId}renderTemplate(O){return{templateData:this.renderer.renderTemplate(O)}}renderElement(O,A,B,F){this.renderer.renderElement(this.nodeMapper.map(O),A,B.templateData,F)}renderTwistie(O,A){return O.slow?(A.classList.add(...o.treeItemLoadingIcon.classNamesArray),!0):(A.classList.remove(...o.treeItemLoadingIcon.classNamesArray),!1)}disposeElement(O,A,B,F){this.renderer.disposeElement&&this.renderer.disposeElement(this.nodeMapper.map(O),A,B.templateData,F)}disposeTemplate(O){this.renderer.disposeTemplate(O.templateData)}dispose(){this.renderedNodes.clear()}}function n(T){return{browserEvent:T.browserEvent,elements:T.elements.map(O=>O.element)}}function t(T){return{browserEvent:T.browserEvent,element:T.element&&T.element.element,target:T.target}}class l extends g.ElementsDragAndDropData{constructor(O){super(O.elements.map(A=>A.element));this.data=O}}function h(T){return T instanceof g.ElementsDragAndDropData?new l(T):T}class m{constructor(O){this.dnd=O}getDragURI(O){return this.dnd.getDragURI(O.element)}getDragLabel(O,A){if(this.dnd.getDragLabel)return this.dnd.getDragLabel(O.map(B=>B.element),A)}onDragStart(O,A){this.dnd.onDragStart&&this.dnd.onDragStart(h(O),A)}onDragOver(O,A,B,F,D=!0){return this.dnd.onDragOver(h(O),A&&A.element,B,F)}drop(O,A,B,F){this.dnd.drop(h(O),A&&A.element,B,F)}onDragEnd(O){this.dnd.onDragEnd&&this.dnd.onDragEnd(O)}}function _(T){return T&&Object.assign(Object.assign({},T),{collapseByDefault:!0,identityProvider:T.identityProvider&&{getId(O){return T.identityProvider.getId(O.element)}},dnd:T.dnd&&new m(T.dnd),multipleSelectionController:T.multipleSelectionController&&{isSelectionSingleChangeEvent(O){return T.multipleSelectionController.isSelectionSingleChangeEvent(Object.assign(Object.assign({},O),{element:O.element}))},isSelectionRangeChangeEvent(O){return T.multipleSelectionController.isSelectionRangeChangeEvent(Object.assign(Object.assign({},O),{element:O.element}))}},accessibilityProvider:T.accessibilityProvider&&Object.assign(Object.assign({},T.accessibilityProvider),{getPosInSet:void 0,getSetSize:void 0,getRole:T.accessibilityProvider.getRole?O=>T.accessibilityProvider.getRole(O.element):()=>"treeitem",isChecked:T.accessibilityProvider.isChecked?O=>{var A;return!!((A=T.accessibilityProvider)===null||A===void 0?void 0:A.isChecked(O.element))}:void 0,getAriaLabel(O){return T.accessibilityProvider.getAriaLabel(O.element)},getWidgetAriaLabel(){return T.accessibilityProvider.getWidgetAriaLabel()},getWidgetRole:T.accessibilityProvider.getWidgetRole?()=>T.accessibilityProvider.getWidgetRole():()=>"tree",getAriaLevel:T.accessibilityProvider.getAriaLevel&&(O=>T.accessibilityProvider.getAriaLevel(O.element)),getActiveDescendantId:T.accessibilityProvider.getActiveDescendantId&&(O=>T.accessibilityProvider.getActiveDescendantId(O.element))}),filter:T.filter&&{filter(O,A){return T.filter.filter(O.element,A)}},keyboardNavigationLabelProvider:T.keyboardNavigationLabelProvider&&Object.assign(Object.assign({},T.keyboardNavigationLabelProvider),{getKeyboardNavigationLabel(O){return T.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(O.element)}}),sorter:void 0,expandOnlyOnTwistieClick:typeof T.expandOnlyOnTwistieClick=="undefined"?void 0:typeof T.expandOnlyOnTwistieClick!="function"?T.expandOnlyOnTwistieClick:O=>T.expandOnlyOnTwistieClick(O.element),additionalScrollHeight:T.additionalScrollHeight})}function f(T,O){O(T),T.children.forEach(A=>f(A,O))}class v{constructor(O,A,B,F,D,R={}){this.user=O,this.dataSource=D,this.nodes=new Map,this.subTreeRefreshPromises=new Map,this.refreshPromises=new Map,this._onDidRender=new S.Emitter,this._onDidChangeNodeSlowState=new S.Emitter,this.nodeMapper=new M.WeakMapper(W=>new r(W)),this.disposables=new w.DisposableStore,this.identityProvider=R.identityProvider,this.autoExpandSingleChildren=typeof R.autoExpandSingleChildren=="undefined"?!1:R.autoExpandSingleChildren,this.sorter=R.sorter,this.collapseByDefault=R.collapseByDefault,this.tree=this.createTree(O,A,B,F,R),this.root=s({element:void 0,parent:null,hasChildren:!0}),this.identityProvider&&(this.root=Object.assign(Object.assign({},this.root),{id:null})),this.nodes.set(null,this.root),this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState,this,this.disposables)}get onDidChangeFocus(){return S.Event.map(this.tree.onDidChangeFocus,n)}get onDidChangeSelection(){return S.Event.map(this.tree.onDidChangeSelection,n)}get onMouseDblClick(){return S.Event.map(this.tree.onMouseDblClick,t)}get onPointer(){return S.Event.map(this.tree.onPointer,t)}get onDidFocus(){return this.tree.onDidFocus}get onDidDispose(){return this.tree.onDidDispose}createTree(O,A,B,F,D){const R=new b.ComposedTreeDelegate(B),W=F.map(K=>new i(K,this.nodeMapper,this._onDidChangeNodeSlowState.event)),x=_(D)||{};return new N.ObjectTree(O,A,R,W,x)}updateOptions(O={}){this.tree.updateOptions(O)}getHTMLElement(){return this.tree.getHTMLElement()}get scrollTop(){return this.tree.scrollTop}set scrollTop(O){this.tree.scrollTop=O}domFocus(){this.tree.domFocus()}layout(O,A){this.tree.layout(O,A)}style(O){this.tree.style(O)}getInput(){return this.root.element}setInput(O,A){return Ie(this,void 0,void 0,function*(){this.refreshPromises.forEach(F=>F.cancel()),this.refreshPromises.clear(),this.root.element=O;const B=A&&{viewState:A,focus:[],selection:[]};yield this._updateChildren(O,!0,!1,B),B&&(this.tree.setFocus(B.focus),this.tree.setSelection(B.selection)),A&&typeof A.scrollTop=="number"&&(this.scrollTop=A.scrollTop)})}_updateChildren(O=this.root.element,A=!0,B=!1,F,D){return Ie(this,void 0,void 0,function*(){if(typeof this.root.element=="undefined")throw new M.TreeError(this.user,"Tree input not set");this.root.refreshPromise&&(yield this.root.refreshPromise,yield S.Event.toPromise(this._onDidRender.event));const R=this.getDataNode(O);if(yield this.refreshAndRenderNode(R,A,F,D),B)try{this.tree.rerender(R)}catch(W){}})}rerender(O){if(O===void 0||O===this.root.element){this.tree.rerender();return}const A=this.getDataNode(O);this.tree.rerender(A)}collapse(O,A=!1){const B=this.getDataNode(O);return this.tree.collapse(B===this.root?null:B,A)}expand(O,A=!1){return Ie(this,void 0,void 0,function*(){if(typeof this.root.element=="undefined")throw new M.TreeError(this.user,"Tree input not set");this.root.refreshPromise&&(yield this.root.refreshPromise,yield S.Event.toPromise(this._onDidRender.event));const B=this.getDataNode(O);if(this.tree.hasElement(B)&&!this.tree.isCollapsible(B)||(B.refreshPromise&&(yield this.root.refreshPromise,yield S.Event.toPromise(this._onDidRender.event)),B!==this.root&&!B.refreshPromise&&!this.tree.isCollapsed(B)))return!1;const F=this.tree.expand(B===this.root?null:B,A);return B.refreshPromise&&(yield this.root.refreshPromise,yield S.Event.toPromise(this._onDidRender.event)),F})}setSelection(O,A){const B=O.map(F=>this.getDataNode(F));this.tree.setSelection(B,A)}getSelection(){return this.tree.getSelection().map(A=>A.element)}setFocus(O,A){const B=O.map(F=>this.getDataNode(F));this.tree.setFocus(B,A)}getFocus(){return this.tree.getFocus().map(A=>A.element)}reveal(O,A){this.tree.reveal(this.getDataNode(O),A)}getDataNode(O){const A=this.nodes.get(O===this.root.element?null:O);if(!A)throw new M.TreeError(this.user,`Data tree node not found: ${O}`);return A}refreshAndRenderNode(O,A,B,F){return Ie(this,void 0,void 0,function*(){yield this.refreshNode(O,A,B),this.render(O,B,F)})}refreshNode(O,A,B){return Ie(this,void 0,void 0,function*(){let F;return this.subTreeRefreshPromises.forEach((D,R)=>{!F&&u(R,O)&&(F=D.then(()=>this.refreshNode(O,A,B)))}),F||this.doRefreshSubTree(O,A,B)})}doRefreshSubTree(O,A,B){return Ie(this,void 0,void 0,function*(){let F;O.refreshPromise=new Promise(D=>F=D),this.subTreeRefreshPromises.set(O,O.refreshPromise),O.refreshPromise.finally(()=>{O.refreshPromise=void 0,this.subTreeRefreshPromises.delete(O)});try{const D=yield this.doRefreshNode(O,A,B);O.stale=!1,yield C.Promises.settled(D.map(R=>this.doRefreshSubTree(R,A,B)))}finally{F()}})}doRefreshNode(O,A,B){return Ie(this,void 0,void 0,function*(){O.hasChildren=!!this.dataSource.hasChildren(O.element);let F;if(!O.hasChildren)F=Promise.resolve(d.Iterable.empty());else{const D=C.timeout(800);D.then(()=>{O.slow=!0,this._onDidChangeNodeSlowState.fire(O)},R=>null),F=this.doGetChildren(O).finally(()=>D.cancel())}try{const D=yield F;return this.setChildren(O,D,A,B)}catch(D){if(O!==this.root&&this.tree.hasElement(O)&&this.tree.collapse(O),p.isPromiseCanceledError(D))return[];throw D}finally{O.slow&&(O.slow=!1,this._onDidChangeNodeSlowState.fire(O))}})}doGetChildren(O){let A=this.refreshPromises.get(O);return A||(A=C.createCancelablePromise(()=>Ie(this,void 0,void 0,function*(){const B=yield this.dataSource.getChildren(O.element);return this.processChildren(B)})),this.refreshPromises.set(O,A),A.finally(()=>{this.refreshPromises.delete(O)}))}_onDidChangeCollapseState({node:O,deep:A}){O.element!==null&&!O.collapsed&&O.element.stale&&(A?this.collapse(O.element.element):this.refreshAndRenderNode(O.element,!1).catch(p.onUnexpectedError))}setChildren(O,A,B,F){const D=[...A];if(O.children.length===0&&D.length===0)return[];const R=new Map,W=new Map;for(const Y of O.children)if(R.set(Y.element,Y),this.identityProvider){const ee=this.tree.isCollapsed(Y);W.set(Y.id,{node:Y,collapsed:ee})}const x=[],K=D.map(Y=>{const ee=!!this.dataSource.hasChildren(Y);if(!this.identityProvider){const X=s({element:Y,parent:O,hasChildren:ee});return ee&&this.collapseByDefault&&!this.collapseByDefault(Y)&&(X.collapsedByDefault=!1,x.push(X)),X}const se=this.identityProvider.getId(Y).toString(),ne=W.get(se);if(ne){const X=ne.node;return R.delete(X.element),this.nodes.delete(X.element),this.nodes.set(Y,X),X.element=Y,X.hasChildren=ee,B?ne.collapsed?(X.children.forEach(z=>f(z,P=>this.nodes.delete(P.element))),X.children.splice(0,X.children.length),X.stale=!0):x.push(X):ee&&this.collapseByDefault&&!this.collapseByDefault(Y)&&(X.collapsedByDefault=!1,x.push(X)),X}const le=s({element:Y,parent:O,id:se,hasChildren:ee});return F&&F.viewState.focus&&F.viewState.focus.indexOf(se)>-1&&F.focus.push(le),F&&F.viewState.selection&&F.viewState.selection.indexOf(se)>-1&&F.selection.push(le),F&&F.viewState.expanded&&F.viewState.expanded.indexOf(se)>-1?x.push(le):ee&&this.collapseByDefault&&!this.collapseByDefault(Y)&&(le.collapsedByDefault=!1,x.push(le)),le});for(const Y of R.values())f(Y,ee=>this.nodes.delete(ee.element));for(const Y of K)this.nodes.set(Y.element,Y);return O.children.splice(0,O.children.length,...K),O!==this.root&&this.autoExpandSingleChildren&&K.length===1&&x.length===0&&(K[0].collapsedByDefault=!1,x.push(K[0])),x}render(O,A,B){const F=O.children.map(R=>this.asTreeElement(R,A)),D=B&&Object.assign(Object.assign({},B),{diffIdentityProvider:B.diffIdentityProvider&&{getId(R){return B.diffIdentityProvider.getId(R.element)}}});this.tree.setChildren(O===this.root?null:O,F,D),O!==this.root&&this.tree.setCollapsible(O,O.hasChildren),this._onDidRender.fire()}asTreeElement(O,A){if(O.stale)return{element:O,collapsible:O.hasChildren,collapsed:!0};let B;return A&&A.viewState.expanded&&O.id&&A.viewState.expanded.indexOf(O.id)>-1?B=!1:B=O.collapsedByDefault,O.collapsedByDefault=void 0,{element:O,children:O.hasChildren?d.Iterable.map(O.children,F=>this.asTreeElement(F,A)):[],collapsible:O.hasChildren,collapsed:B}}processChildren(O){return this.sorter&&(O=[...O].sort(this.sorter.compare.bind(this.sorter))),O}dispose(){this.disposables.dispose()}}e.AsyncDataTree=v;class y{constructor(O){this.node=O}get element(){return{elements:this.node.element.elements.map(O=>O.element),incompressible:this.node.element.incompressible}}get children(){return this.node.children.map(O=>new y(O))}get depth(){return this.node.depth}get visibleChildrenCount(){return this.node.visibleChildrenCount}get visibleChildIndex(){return this.node.visibleChildIndex}get collapsible(){return this.node.collapsible}get collapsed(){return this.node.collapsed}get visible(){return this.node.visible}get filterData(){return this.node.filterData}}class L{constructor(O,A,B,F){this.renderer=O,this.nodeMapper=A,this.compressibleNodeMapperProvider=B,this.onDidChangeTwistieState=F,this.renderedNodes=new Map,this.disposables=[],this.templateId=O.templateId}renderTemplate(O){return{templateData:this.renderer.renderTemplate(O)}}renderElement(O,A,B,F){this.renderer.renderElement(this.nodeMapper.map(O),A,B.templateData,F)}renderCompressedElements(O,A,B,F){this.renderer.renderCompressedElements(this.compressibleNodeMapperProvider().map(O),A,B.templateData,F)}renderTwistie(O,A){return O.slow?(A.classList.add(...o.treeItemLoadingIcon.classNamesArray),!0):(A.classList.remove(...o.treeItemLoadingIcon.classNamesArray),!1)}disposeElement(O,A,B,F){this.renderer.disposeElement&&this.renderer.disposeElement(this.nodeMapper.map(O),A,B.templateData,F)}disposeCompressedElements(O,A,B,F){this.renderer.disposeCompressedElements&&this.renderer.disposeCompressedElements(this.compressibleNodeMapperProvider().map(O),A,B.templateData,F)}disposeTemplate(O){this.renderer.disposeTemplate(O.templateData)}dispose(){this.renderedNodes.clear(),this.disposables=w.dispose(this.disposables)}}function I(T){const O=T&&_(T);return O&&Object.assign(Object.assign({},O),{keyboardNavigationLabelProvider:O.keyboardNavigationLabelProvider&&Object.assign(Object.assign({},O.keyboardNavigationLabelProvider),{getCompressedNodeKeyboardNavigationLabel(A){return T.keyboardNavigationLabelProvider.getCompressedNodeKeyboardNavigationLabel(A.map(B=>B.element))}})})}class k extends v{constructor(O,A,B,F,D,R,W={}){super(O,A,B,D,R,W);this.compressionDelegate=F,this.compressibleNodeMapper=new M.WeakMapper(x=>new y(x)),this.filter=W.filter}createTree(O,A,B,F,D){const R=new b.ComposedTreeDelegate(B),W=F.map(K=>new L(K,this.nodeMapper,()=>this.compressibleNodeMapper,this._onDidChangeNodeSlowState.event)),x=I(D)||{};return new N.CompressibleObjectTree(O,A,R,W,x)}asTreeElement(O,A){return Object.assign({incompressible:this.compressionDelegate.isIncompressible(O.element)},super.asTreeElement(O,A))}updateOptions(O={}){this.tree.updateOptions(O)}render(O,A){if(!this.identityProvider)return super.render(O,A);const B=se=>this.identityProvider.getId(se).toString(),F=se=>{const ne=new Set;for(const le of se){const X=this.tree.getCompressedTreeNode(le===this.root?null:le);if(!!X.element)for(const z of X.element.elements)ne.add(B(z.element))}return ne},D=F(this.tree.getSelection()),R=F(this.tree.getFocus());super.render(O,A);const W=this.getSelection();let x=!1;const K=this.getFocus();let Y=!1;const ee=se=>{const ne=se.element;if(ne)for(let le=0;le{const B=this.filter.filter(A,1),F=E(B);if(F===2)throw new Error("Recursive tree visibility not supported in async data compressed trees");return F===1})),super.processChildren(O)}}e.CompressibleAsyncDataTree=k;function E(T){return typeof T=="boolean"?T?1:0:c.isFilterResult(T)?c.getVisibleState(T.visibility):c.getVisibleState(T)}}),define(Q[429],J([4,5]),function(q,e){return q.create("vs/base/common/actions",e)}),define(Q[48],J([0,1,429,2,6]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EmptySubmenuAction=e.SubmenuAction=e.Separator=e.ActionRunner=e.Action=void 0;class w extends N.Disposable{constructor(c,o="",s="",a=!0,u){super();this._onDidChange=this._register(new M.Emitter),this.onDidChange=this._onDidChange.event,this._enabled=!0,this._checked=!1,this._id=c,this._label=o,this._cssClass=s,this._enabled=a,this._actionCallback=u}get id(){return this._id}get label(){return this._label}set label(c){this._setLabel(c)}_setLabel(c){this._label!==c&&(this._label=c,this._onDidChange.fire({label:c}))}get tooltip(){return this._tooltip||""}set tooltip(c){this._setTooltip(c)}_setTooltip(c){this._tooltip!==c&&(this._tooltip=c,this._onDidChange.fire({tooltip:c}))}get class(){return this._cssClass}set class(c){this._setClass(c)}_setClass(c){this._cssClass!==c&&(this._cssClass=c,this._onDidChange.fire({class:c}))}get enabled(){return this._enabled}set enabled(c){this._setEnabled(c)}_setEnabled(c){this._enabled!==c&&(this._enabled=c,this._onDidChange.fire({enabled:c}))}get checked(){return this._checked}set checked(c){this._setChecked(c)}_setChecked(c){this._checked!==c&&(this._checked=c,this._onDidChange.fire({checked:c}))}run(c,o){return this._actionCallback?this._actionCallback(c):Promise.resolve(!0)}}e.Action=w;class S extends N.Disposable{constructor(){super(...arguments);this._onBeforeRun=this._register(new M.Emitter),this.onBeforeRun=this._onBeforeRun.event,this._onDidRun=this._register(new M.Emitter),this.onDidRun=this._onDidRun.event}run(c,o){return Ie(this,void 0,void 0,function*(){if(!c.enabled)return Promise.resolve(null);this._onBeforeRun.fire({action:c});try{const s=yield this.runAction(c,o);this._onDidRun.fire({action:c,result:s})}catch(s){this._onDidRun.fire({action:c,error:s})}})}runAction(c,o){const s=o?c.run(o):c.run();return Promise.resolve(s)}}e.ActionRunner=S;class C extends w{constructor(c){super(C.ID,c,c?"separator text":"separator");this.checked=!1,this.enabled=!1}}e.Separator=C,C.ID="vs.actions.separator";class d{constructor(c,o,s,a){this.tooltip="",this.enabled=!0,this.checked=!1,this.id=c,this.label=o,this.class=a,this._actions=s}dispose(){}get actions(){return this._actions}run(){return Ie(this,void 0,void 0,function*(){})}}e.SubmenuAction=d;class g extends w{constructor(){super(g.ID,b.localize(0,null),void 0,!1)}}e.EmptySubmenuAction=g,g.ID="vs.actions.empty"}),define(Q[112],J([0,1,17,418,2,48,20,60,149,35,7,203]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ActionViewItem=e.BaseActionViewItem=void 0;class c extends M.Disposable{constructor(a,u,r={}){super();this.options=r,this._context=a||this,this._action=u,u instanceof w.Action&&this._register(u.onDidChange(i=>{!this.element||this.handleActionChangeEvent(i)}))}handleActionChangeEvent(a){a.enabled!==void 0&&this.updateEnabled(),a.checked!==void 0&&this.updateChecked(),a.class!==void 0&&this.updateClass(),a.label!==void 0&&(this.updateLabel(),this.updateTooltip()),a.tooltip!==void 0&&this.updateTooltip()}get actionRunner(){return this._actionRunner||(this._actionRunner=this._register(new w.ActionRunner)),this._actionRunner}set actionRunner(a){this._actionRunner=a}getAction(){return this._action}isEnabled(){return this._action.enabled}setActionContext(a){this._context=a}render(a){const u=this.element=a;this._register(C.Gesture.addTarget(a));const r=this.options&&this.options.draggable;r&&(a.draggable=!0,g.isFirefox&&this._register(p.addDisposableListener(a,p.EventType.DRAG_START,i=>{var n;return(n=i.dataTransfer)===null||n===void 0?void 0:n.setData(d.DataTransfers.TEXT,this._action.label)}))),this._register(p.addDisposableListener(u,C.EventType.Tap,i=>this.onClick(i))),this._register(p.addDisposableListener(u,p.EventType.MOUSE_DOWN,i=>{r||p.EventHelper.stop(i,!0),this._action.enabled&&i.button===0&&u.classList.add("active")})),b.isMacintosh&&this._register(p.addDisposableListener(u,p.EventType.CONTEXT_MENU,i=>{i.button===0&&i.ctrlKey===!0&&this.onClick(i)})),this._register(p.addDisposableListener(u,p.EventType.CLICK,i=>{p.EventHelper.stop(i,!0),this.options&&this.options.isMenu||b.setImmediate(()=>this.onClick(i))})),this._register(p.addDisposableListener(u,p.EventType.DBLCLICK,i=>{p.EventHelper.stop(i,!0)})),[p.EventType.MOUSE_UP,p.EventType.MOUSE_OUT].forEach(i=>{this._register(p.addDisposableListener(u,i,n=>{p.EventHelper.stop(n),u.classList.remove("active")}))})}onClick(a){var u;p.EventHelper.stop(a,!0);const r=S.isUndefinedOrNull(this._context)?((u=this.options)===null||u===void 0?void 0:u.useEventAsContext)?a:void 0:this._context;this.actionRunner.run(this._action,r)}focus(){this.element&&(this.element.tabIndex=0,this.element.focus(),this.element.classList.add("focused"))}blur(){this.element&&(this.element.blur(),this.element.tabIndex=-1,this.element.classList.remove("focused"))}setFocusable(a){this.element&&(this.element.tabIndex=a?0:-1)}get trapsArrowNavigation(){return!1}updateEnabled(){}updateLabel(){}updateTooltip(){}updateClass(){}updateChecked(){}dispose(){this.element&&(this.element.remove(),this.element=void 0),super.dispose()}}e.BaseActionViewItem=c;class o extends c{constructor(a,u,r={}){super(a,u,r);this.options=r,this.options.icon=r.icon!==void 0?r.icon:!1,this.options.label=r.label!==void 0?r.label:!0,this.cssClass=""}render(a){super.render(a),this.element&&(this.label=p.append(this.element,p.$("a.action-label"))),this.label&&(this._action.id===w.Separator.ID?this.label.setAttribute("role","presentation"):this.options.isMenu?this.label.setAttribute("role","menuitem"):this.label.setAttribute("role","button")),this.options.label&&this.options.keybinding&&this.element&&(p.append(this.element,p.$("span.keybinding")).textContent=this.options.keybinding),this.updateClass(),this.updateLabel(),this.updateTooltip(),this.updateEnabled(),this.updateChecked()}focus(){this.label&&(this.label.tabIndex=0,this.label.focus())}blur(){this.label&&(this.label.tabIndex=-1)}setFocusable(a){this.label&&(this.label.tabIndex=a?0:-1)}updateLabel(){this.options.label&&this.label&&(this.label.textContent=this.getAction().label)}updateTooltip(){let a=null;this.getAction().tooltip?a=this.getAction().tooltip:!this.options.label&&this.getAction().label&&this.options.icon&&(a=this.getAction().label,this.options.keybinding&&(a=N.localize(0,null,a,this.options.keybinding))),a&&this.label&&(this.label.title=a)}updateClass(){this.cssClass&&this.label&&this.label.classList.remove(...this.cssClass.split(" ")),this.options.icon?(this.cssClass=this.getAction().class,this.label&&(this.label.classList.add("codicon"),this.cssClass&&this.label.classList.add(...this.cssClass.split(" "))),this.updateEnabled()):this.label&&this.label.classList.remove("codicon")}updateEnabled(){this.getAction().enabled?(this.label&&(this.label.removeAttribute("aria-disabled"),this.label.classList.remove("disabled")),this.element&&this.element.classList.remove("disabled")):(this.label&&(this.label.setAttribute("aria-disabled","true"),this.label.classList.add("disabled")),this.element&&this.element.classList.add("disabled"))}updateChecked(){this.label&&(this.getAction().checked?this.label.classList.add("checked"):this.label.classList.remove("checked"))}}e.ActionViewItem=o}),define(Q[83],J([0,1,2,48,7,20,56,6,112,203]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ActionBar=void 0;class g extends b.Disposable{constructor(c,o={}){var s,a,u,r,i,n;super();this.triggerKeyDown=!1,this.focusable=!0,this._onDidBlur=this._register(new C.Emitter),this.onDidBlur=this._onDidBlur.event,this._onDidCancel=this._register(new C.Emitter({onFirstListenerAdd:()=>this.cancelHasListener=!0})),this.onDidCancel=this._onDidCancel.event,this.cancelHasListener=!1,this._onDidRun=this._register(new C.Emitter),this.onDidRun=this._onDidRun.event,this._onBeforeRun=this._register(new C.Emitter),this.onBeforeRun=this._onBeforeRun.event,this.options=o,this._context=(s=o.context)!==null&&s!==void 0?s:null,this._orientation=(a=this.options.orientation)!==null&&a!==void 0?a:0,this._triggerKeys={keyDown:(r=(u=this.options.triggerKeys)===null||u===void 0?void 0:u.keyDown)!==null&&r!==void 0?r:!1,keys:(n=(i=this.options.triggerKeys)===null||i===void 0?void 0:i.keys)!==null&&n!==void 0?n:[3,10]},this.options.actionRunner?this._actionRunner=this.options.actionRunner:(this._actionRunner=new N.ActionRunner,this._register(this._actionRunner)),this._register(this._actionRunner.onDidRun(h=>this._onDidRun.fire(h))),this._register(this._actionRunner.onBeforeRun(h=>this._onBeforeRun.fire(h))),this._actionIds=[],this.viewItems=[],this.focusedItem=void 0,this.domNode=document.createElement("div"),this.domNode.className="monaco-action-bar",o.animated!==!1&&this.domNode.classList.add("animated");let t,l;switch(this._orientation){case 0:t=[15],l=[17];break;case 1:t=[17],l=[15],this.domNode.className+=" reverse";break;case 2:t=[16],l=[18],this.domNode.className+=" vertical";break;case 3:t=[18],l=[16],this.domNode.className+=" vertical reverse";break}this._register(M.addDisposableListener(this.domNode,M.EventType.KEY_DOWN,h=>{const m=new S.StandardKeyboardEvent(h);let _=!0;const f=typeof this.focusedItem=="number"?this.viewItems[this.focusedItem]:void 0;t&&(m.equals(t[0])||m.equals(t[1]))?_=this.focusPrevious():l&&(m.equals(l[0])||m.equals(l[1]))?_=this.focusNext():m.equals(9)&&this.cancelHasListener?this._onDidCancel.fire():m.equals(2)&&f instanceof d.BaseActionViewItem&&f.trapsArrowNavigation?this.focusNext():this.isTriggerKeyEvent(m)?this._triggerKeys.keyDown?this.doTrigger(m):this.triggerKeyDown=!0:_=!1,_&&(m.preventDefault(),m.stopPropagation())})),this._register(M.addDisposableListener(this.domNode,M.EventType.KEY_UP,h=>{const m=new S.StandardKeyboardEvent(h);this.isTriggerKeyEvent(m)?(!this._triggerKeys.keyDown&&this.triggerKeyDown&&(this.triggerKeyDown=!1,this.doTrigger(m)),m.preventDefault(),m.stopPropagation()):(m.equals(2)||m.equals(1024|2))&&this.updateFocusedItem()})),this.focusTracker=this._register(M.trackFocus(this.domNode)),this._register(this.focusTracker.onDidBlur(()=>{(M.getActiveElement()===this.domNode||!M.isAncestor(M.getActiveElement(),this.domNode))&&(this._onDidBlur.fire(),this.focusedItem=void 0,this.triggerKeyDown=!1)})),this._register(this.focusTracker.onDidFocus(()=>this.updateFocusedItem())),this.actionsList=document.createElement("ul"),this.actionsList.className="actions-container",this.actionsList.setAttribute("role","toolbar"),this.options.ariaLabel&&this.actionsList.setAttribute("aria-label",this.options.ariaLabel),this.domNode.appendChild(this.actionsList),c.appendChild(this.domNode)}isTriggerKeyEvent(c){let o=!1;return this._triggerKeys.keys.forEach(s=>{o=o||c.equals(s)}),o}updateFocusedItem(){for(let c=0;co.setActionContext(c))}get actionRunner(){return this._actionRunner}set actionRunner(c){c&&(this._actionRunner=c,this.viewItems.forEach(o=>o.actionRunner=c))}getContainer(){return this.domNode}push(c,o={}){const s=Array.isArray(c)?c:[c];let a=w.isNumber(o.index)?o.index:null;s.forEach(u=>{const r=document.createElement("li");r.className="action-item",r.setAttribute("role","presentation"),this.options.allowContextMenu||this._register(M.addDisposableListener(r,M.EventType.CONTEXT_MENU,n=>{M.EventHelper.stop(n,!0)}));let i;this.options.actionViewItemProvider&&(i=this.options.actionViewItemProvider(u)),i||(i=new d.ActionViewItem(this.context,u,o)),i.actionRunner=this._actionRunner,i.setActionContext(this.context),i.render(r),this.focusable&&i instanceof d.BaseActionViewItem&&this.viewItems.length===0&&i.setFocusable(!0),a===null||a<0||a>=this.actionsList.children.length?(this.actionsList.appendChild(r),this.viewItems.push(i),this._actionIds.push(u.id)):(this.actionsList.insertBefore(r,this.actionsList.children[a]),this.viewItems.splice(a,0,i),this._actionIds.splice(a,0,u.id),a++)}),typeof this.focusedItem=="number"&&this.focus(this.focusedItem)}clear(){b.dispose(this.viewItems),this.viewItems=[],this._actionIds=[],M.clearNode(this.actionsList)}focus(c){let o=!1,s;if(c===void 0?o=!0:typeof c=="number"?s=c:typeof c=="boolean"&&(o=c),o&&typeof this.focusedItem=="undefined"){const a=this.viewItems.findIndex(u=>u.isEnabled());this.focusedItem=a===-1?void 0:a,this.updateFocus()}else s!==void 0&&(this.focusedItem=s),this.updateFocus()}focusNext(){typeof this.focusedItem=="undefined"&&(this.focusedItem=this.viewItems.length-1);const c=this.focusedItem;let o;do{if(this.options.preventLoopNavigation&&this.focusedItem+1>=this.viewItems.length)return this.focusedItem=c,!1;this.focusedItem=(this.focusedItem+1)%this.viewItems.length,o=this.viewItems[this.focusedItem]}while(this.focusedItem!==c&&this.options.focusOnlyEnabledItems&&!o.isEnabled());return this.updateFocus(),!0}focusPrevious(){typeof this.focusedItem=="undefined"&&(this.focusedItem=0);const c=this.focusedItem;let o;do{if(this.focusedItem=this.focusedItem-1,this.focusedItem<0){if(this.options.preventLoopNavigation)return this.focusedItem=c,!1;this.focusedItem=this.viewItems.length-1}o=this.viewItems[this.focusedItem]}while(this.focusedItem!==c&&this.options.focusOnlyEnabledItems&&!o.isEnabled());return this.updateFocus(!0),!0}updateFocus(c,o){typeof this.focusedItem=="undefined"&&this.actionsList.focus({preventScroll:o});for(let s=0;s(a.textContent=c.label||"",null));for(const a of[M.EventType.CLICK,M.EventType.MOUSE_DOWN,b.EventType.Tap])this._register(M.addDisposableListener(this.element,a,u=>M.EventHelper.stop(u,!0)));for(const a of[M.EventType.MOUSE_DOWN,b.EventType.Tap])this._register(M.addDisposableListener(this._label,a,u=>{u instanceof MouseEvent&&u.detail>1||(this.visible?this.hide():this.show())}));this._register(M.addDisposableListener(this._label,M.EventType.KEY_UP,a=>{const u=new w.StandardKeyboardEvent(a);(u.equals(3)||u.equals(10))&&(M.EventHelper.stop(a,!0),this.visible?this.hide():this.show())}));const s=o(this._label);s&&this._register(s),this._register(b.Gesture.addTarget(this._label))}get element(){return this._element}show(){this.visible||(this.visible=!0,this._onDidChangeVisibility.fire(!0))}hide(){this.visible&&(this.visible=!1,this._onDidChangeVisibility.fire(!1))}dispose(){super.dispose(),this.hide(),this.boxContainer&&(this.boxContainer.remove(),this.boxContainer=void 0),this.contents&&(this.contents.remove(),this.contents=void 0),this._label&&(this._label.remove(),this._label=void 0)}}e.BaseDropdown=C;class d extends C{constructor(p,c){super(p,c);this._actions=[],this._contextMenuProvider=c.contextMenuProvider,this.actions=c.actions||[],this.actionProvider=c.actionProvider,this.menuClassName=c.menuClassName||"",this.menuAsChild=!!c.menuAsChild}set menuOptions(p){this._menuOptions=p}get menuOptions(){return this._menuOptions}get actions(){return this.actionProvider?this.actionProvider.getActions():this._actions}set actions(p){this._actions=p}show(){super.show(),this.element.classList.add("active"),this._contextMenuProvider.showContextMenu({getAnchor:()=>this.element,getActions:()=>this.actions,getActionsContext:()=>this.menuOptions?this.menuOptions.context:null,getActionViewItem:p=>this.menuOptions&&this.menuOptions.actionViewItemProvider?this.menuOptions.actionViewItemProvider(p):void 0,getKeyBinding:p=>this.menuOptions&&this.menuOptions.getKeyBinding?this.menuOptions.getKeyBinding(p):void 0,getMenuClassName:()=>this.menuClassName,onHide:()=>this.onHide(),actionRunner:this.menuOptions?this.menuOptions.actionRunner:void 0,anchorAlignment:this.menuOptions?this.menuOptions.anchorAlignment:0,domForShadowRoot:this.menuAsChild?this.element:void 0})}hide(){super.hide()}onHide(){this.hide(),this.element.classList.remove("active")}}e.DropdownMenu=d}),define(Q[431],J([0,1,7,6,112,430,206]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DropdownMenuActionViewItem=void 0;class S extends M.BaseActionViewItem{constructor(d,g,p,c={}){super(null,d,c);this.options=c,this.actionItem=null,this._onDidChangeVisibility=this._register(new N.Emitter),this.menuActionsOrProvider=g,this.contextMenuProvider=p,this.options.actionRunner&&(this.actionRunner=this.options.actionRunner)}render(d){this.actionItem=d;const g=o=>{this.element=b.append(o,b.$("a.action-label"));let s=[];return typeof this.options.classNames=="string"?s=this.options.classNames.split(/\s+/g).filter(a=>!!a):this.options.classNames&&(s=this.options.classNames),s.find(a=>a==="icon")||s.push("codicon"),this.element.classList.add(...s),this.element.setAttribute("role","button"),this.element.setAttribute("aria-haspopup","true"),this.element.setAttribute("aria-expanded","false"),this.element.title=this._action.label||"",null},p=Array.isArray(this.menuActionsOrProvider),c={contextMenuProvider:this.contextMenuProvider,labelRenderer:g,menuAsChild:this.options.menuAsChild,actions:p?this.menuActionsOrProvider:void 0,actionProvider:p?void 0:this.menuActionsOrProvider};if(this.dropdownMenu=this._register(new w.DropdownMenu(d,c)),this._register(this.dropdownMenu.onDidChangeVisibility(o=>{var s;(s=this.element)===null||s===void 0||s.setAttribute("aria-expanded",`${o}`),this._onDidChangeVisibility.fire(o)})),this.dropdownMenu.menuOptions={actionViewItemProvider:this.options.actionViewItemProvider,actionRunner:this.actionRunner,getKeyBinding:this.options.keybindingProvider,context:this._context},this.options.anchorAlignmentProvider){const o=this;this.dropdownMenu.menuOptions=Object.assign(Object.assign({},this.dropdownMenu.menuOptions),{get anchorAlignment(){return o.options.anchorAlignmentProvider()}})}this.updateEnabled()}setActionContext(d){super.setActionContext(d),this.dropdownMenu&&(this.dropdownMenu.menuOptions?this.dropdownMenu.menuOptions.context=d:this.dropdownMenu.menuOptions={context:d})}updateEnabled(){var d,g;const p=!this.getAction().enabled;(d=this.actionItem)===null||d===void 0||d.classList.toggle("disabled",p),(g=this.element)===null||g===void 0||g.classList.toggle("disabled",p)}}e.DropdownMenuActionViewItem=S}),define(Q[177],J([0,1,423,7,156,47,83,6,52,29,40,279,61,55,305]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.HistoryInputBox=e.InputBox=void 0;const a=N.$,u={inputBackground:g.Color.fromHex("#3C3C3C"),inputForeground:g.Color.fromHex("#CCCCCC"),inputValidationInfoBorder:g.Color.fromHex("#55AAFF"),inputValidationInfoBackground:g.Color.fromHex("#063B49"),inputValidationWarningBorder:g.Color.fromHex("#B89500"),inputValidationWarningBackground:g.Color.fromHex("#352A05"),inputValidationErrorBorder:g.Color.fromHex("#BE1100"),inputValidationErrorBackground:g.Color.fromHex("#5A1D1D")};class r extends d.Widget{constructor(t,l,h){super();this.state="idle",this.maxHeight=Number.POSITIVE_INFINITY,this._onDidChange=this._register(new C.Emitter),this.onDidChange=this._onDidChange.event,this._onDidHeightChange=this._register(new C.Emitter),this.onDidHeightChange=this._onDidHeightChange.event,this.contextViewProvider=l,this.options=h||Object.create(null),p.mixin(this.options,u,!1),this.message=null,this.placeholder=this.options.placeholder||"",this.ariaLabel=this.options.ariaLabel||"",this.inputBackground=this.options.inputBackground,this.inputForeground=this.options.inputForeground,this.inputBorder=this.options.inputBorder,this.inputValidationInfoBorder=this.options.inputValidationInfoBorder,this.inputValidationInfoBackground=this.options.inputValidationInfoBackground,this.inputValidationInfoForeground=this.options.inputValidationInfoForeground,this.inputValidationWarningBorder=this.options.inputValidationWarningBorder,this.inputValidationWarningBackground=this.options.inputValidationWarningBackground,this.inputValidationWarningForeground=this.options.inputValidationWarningForeground,this.inputValidationErrorBorder=this.options.inputValidationErrorBorder,this.inputValidationErrorBackground=this.options.inputValidationErrorBackground,this.inputValidationErrorForeground=this.options.inputValidationErrorForeground,this.options.validationOptions&&(this.validation=this.options.validationOptions.validation),this.element=N.append(t,a(".monaco-inputbox.idle"));let m=this.options.flexibleHeight?"textarea":"input",_=N.append(this.element,a(".ibwrapper"));if(this.input=N.append(_,a(m+".input.empty")),this.input.setAttribute("autocorrect","off"),this.input.setAttribute("autocapitalize","off"),this.input.setAttribute("spellcheck","false"),this.onfocus(this.input,()=>this.element.classList.add("synthetic-focus")),this.onblur(this.input,()=>this.element.classList.remove("synthetic-focus")),this.options.flexibleHeight){this.maxHeight=typeof this.options.flexibleMaxHeight=="number"?this.options.flexibleMaxHeight:Number.POSITIVE_INFINITY,this.mirror=N.append(_,a("div.mirror")),this.mirror.innerText="\xA0",this.scrollableElement=new o.ScrollableElement(this.element,{vertical:1}),this.options.flexibleWidth&&(this.input.setAttribute("wrap","off"),this.mirror.style.whiteSpace="pre",this.mirror.style.wordWrap="initial"),N.append(t,this.scrollableElement.getDomNode()),this._register(this.scrollableElement),this._register(this.scrollableElement.onScroll(v=>this.input.scrollTop=v.scrollTop));const f=C.Event.filter(s.domEvent(document,"selectionchange"),()=>{const v=document.getSelection();return(v==null?void 0:v.anchorNode)===_});this._register(f(this.updateScrollDimensions,this)),this._register(this.onDidHeightChange(this.updateScrollDimensions,this))}else this.input.type=this.options.type||"text",this.input.setAttribute("wrap","off");this.ariaLabel&&this.input.setAttribute("aria-label",this.ariaLabel),this.placeholder&&this.setPlaceHolder(this.placeholder),this.oninput(this.input,()=>this.onValueChange()),this.onblur(this.input,()=>this.onBlur()),this.onfocus(this.input,()=>this.onFocus()),this.ignoreGesture(this.input),setTimeout(()=>this.updateMirror(),0),this.options.actions&&(this.actionbar=this._register(new S.ActionBar(this.element)),this.actionbar.push(this.options.actions,{icon:!0,label:!1})),this.applyStyles()}onBlur(){this._hideMessage()}onFocus(){this._showMessage()}setPlaceHolder(t){this.placeholder=t,this.input.setAttribute("placeholder",t),this.input.title=t}setAriaLabel(t){this.ariaLabel=t,t?this.input.setAttribute("aria-label",this.ariaLabel):this.input.removeAttribute("aria-label")}getAriaLabel(){return this.ariaLabel}get inputElement(){return this.input}get value(){return this.input.value}set value(t){this.input.value!==t&&(this.input.value=t,this.onValueChange())}get height(){return typeof this.cachedHeight=="number"?this.cachedHeight:N.getTotalHeight(this.element)}focus(){this.input.focus()}blur(){this.input.blur()}hasFocus(){return document.activeElement===this.input}select(t=null){this.input.select(),t&&(this.input.setSelectionRange(t.start,t.end),t.end===this.input.value.length&&(this.input.scrollLeft=this.input.scrollWidth))}isSelectionAtEnd(){return this.input.selectionEnd===this.input.value.length&&this.input.selectionStart===this.input.selectionEnd}enable(){this.input.removeAttribute("disabled")}disable(){this.blur(),this.input.disabled=!0,this._hideMessage()}get width(){return N.getTotalWidth(this.input)}set width(t){if(this.options.flexibleHeight&&this.options.flexibleWidth){let l=0;if(this.mirror){const h=parseFloat(this.mirror.style.paddingLeft||"")||0,m=parseFloat(this.mirror.style.paddingRight||"")||0;l=h+m}this.input.style.width=t-l+"px"}else this.input.style.width=t+"px";this.mirror&&(this.mirror.style.width=t+"px")}set paddingRight(t){this.options.flexibleHeight&&this.options.flexibleWidth?this.input.style.width=`calc(100% - ${t}px)`:this.input.style.paddingRight=t+"px",this.mirror&&(this.mirror.style.paddingRight=t+"px")}updateScrollDimensions(){if(!(typeof this.cachedContentHeight!="number"||typeof this.cachedHeight!="number"||!this.scrollableElement)){const t=this.cachedContentHeight,l=this.cachedHeight,h=this.input.scrollTop;this.scrollableElement.setScrollDimensions({scrollHeight:t,height:l}),this.scrollableElement.setScrollPosition({scrollTop:h})}}showMessage(t,l){this.message=t,this.element.classList.remove("idle"),this.element.classList.remove("info"),this.element.classList.remove("warning"),this.element.classList.remove("error"),this.element.classList.add(this.classForType(t.type));const h=this.stylesForType(this.message.type);this.element.style.border=h.border?`1px solid ${h.border}`:"",(this.hasFocus()||l)&&this._showMessage()}hideMessage(){this.message=null,this.element.classList.remove("info"),this.element.classList.remove("warning"),this.element.classList.remove("error"),this.element.classList.add("idle"),this._hideMessage(),this.applyStyles()}validate(){let t=null;return this.validation&&(t=this.validation(this.value),t?(this.inputElement.setAttribute("aria-invalid","true"),this.showMessage(t)):this.inputElement.hasAttribute("aria-invalid")&&(this.inputElement.removeAttribute("aria-invalid"),this.hideMessage())),t==null?void 0:t.type}stylesForType(t){switch(t){case 1:return{border:this.inputValidationInfoBorder,background:this.inputValidationInfoBackground,foreground:this.inputValidationInfoForeground};case 2:return{border:this.inputValidationWarningBorder,background:this.inputValidationWarningBackground,foreground:this.inputValidationWarningForeground};default:return{border:this.inputValidationErrorBorder,background:this.inputValidationErrorBackground,foreground:this.inputValidationErrorForeground}}}classForType(t){switch(t){case 1:return"info";case 2:return"warning";default:return"error"}}_showMessage(){if(!(!this.contextViewProvider||!this.message)){let t,l=()=>t.style.width=N.getTotalWidth(this.element)+"px";this.contextViewProvider.showContextView({getAnchor:()=>this.element,anchorAlignment:1,render:m=>{if(!this.message)return null;t=N.append(m,a(".monaco-inputbox-container")),l();const _={inline:!0,className:"monaco-inputbox-message"},f=this.message.formatContent?M.renderFormattedText(this.message.content,_):M.renderText(this.message.content,_);f.classList.add(this.classForType(this.message.type));const v=this.stylesForType(this.message.type);return f.style.backgroundColor=v.background?v.background.toString():"",f.style.color=v.foreground?v.foreground.toString():"",f.style.border=v.border?`1px solid ${v.border}`:"",N.append(t,f),null},onHide:()=>{this.state="closed"},layout:l});let h;this.message.type===3?h=b.localize(0,null,this.message.content):this.message.type===2?h=b.localize(1,null,this.message.content):h=b.localize(2,null,this.message.content),w.alert(h),this.state="open"}}_hideMessage(){!this.contextViewProvider||(this.state==="open"&&this.contextViewProvider.hideContextView(),this.state="idle")}onValueChange(){this._onDidChange.fire(this.value),this.validate(),this.updateMirror(),this.input.classList.toggle("empty",!this.value),this.state==="open"&&this.contextViewProvider&&this.contextViewProvider.layout()}updateMirror(){if(!!this.mirror){const t=this.value,h=t.charCodeAt(t.length-1)===10?" ":"";t+h?this.mirror.textContent=t+h:this.mirror.innerText="\xA0",this.layout()}}style(t){this.inputBackground=t.inputBackground,this.inputForeground=t.inputForeground,this.inputBorder=t.inputBorder,this.inputValidationInfoBackground=t.inputValidationInfoBackground,this.inputValidationInfoForeground=t.inputValidationInfoForeground,this.inputValidationInfoBorder=t.inputValidationInfoBorder,this.inputValidationWarningBackground=t.inputValidationWarningBackground,this.inputValidationWarningForeground=t.inputValidationWarningForeground,this.inputValidationWarningBorder=t.inputValidationWarningBorder,this.inputValidationErrorBackground=t.inputValidationErrorBackground,this.inputValidationErrorForeground=t.inputValidationErrorForeground,this.inputValidationErrorBorder=t.inputValidationErrorBorder,this.applyStyles()}applyStyles(){const t=this.inputBackground?this.inputBackground.toString():"",l=this.inputForeground?this.inputForeground.toString():"",h=this.inputBorder?this.inputBorder.toString():"";this.element.style.backgroundColor=t,this.element.style.color=l,this.input.style.backgroundColor="inherit",this.input.style.color=l,this.element.style.borderWidth=h?"1px":"",this.element.style.borderStyle=h?"solid":"",this.element.style.borderColor=h}layout(){if(!!this.mirror){const t=this.cachedContentHeight;this.cachedContentHeight=N.getTotalHeight(this.mirror),t!==this.cachedContentHeight&&(this.cachedHeight=Math.min(this.cachedContentHeight,this.maxHeight),this.input.style.height=this.cachedHeight+"px",this._onDidHeightChange.fire(this.cachedContentHeight))}}insertAtCursor(t){const l=this.inputElement,h=l.selectionStart,m=l.selectionEnd,_=l.value;h!==null&&m!==null&&(this.value=_.substr(0,h)+t+_.substr(m),l.setSelectionRange(h+1,h+1),this.layout())}dispose(){this._hideMessage(),this.message=null,this.actionbar&&this.actionbar.dispose(),super.dispose()}}e.InputBox=r;class i extends r{constructor(t,l,h){super(t,l,h);this.history=new c.HistoryNavigator(h.history,100)}addToHistory(){this.value&&this.value!==this.getCurrentValue()&&this.history.add(this.value)}showNextValue(){this.history.has(this.value)||this.addToHistory();let t=this.getNextValue();t&&(t=t===this.value?this.getNextValue():t),t&&(this.value=t,w.status(this.value))}showPreviousValue(){this.history.has(this.value)||this.addToHistory();let t=this.getPreviousValue();t&&(t=t===this.value?this.getPreviousValue():t),t&&(this.value=t,w.status(this.value))}getCurrentValue(){let t=this.history.current();return t||(t=this.history.last(),this.history.next()),t}getPreviousValue(){return this.history.previous()||this.history.first()}getNextValue(){return this.history.next()||this.history.last()}}e.HistoryInputBox=i}),define(Q[432],J([0,1,419,7,177,52,6,233,207]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FindInput=void 0;const d=b.localize(0,null);class g extends w.Widget{constructor(c,o,s,a){super();this._showOptionButtons=s,this.fixFocusOnOptionClickEnabled=!0,this._onDidOptionChange=this._register(new S.Emitter),this.onDidOptionChange=this._onDidOptionChange.event,this._onKeyDown=this._register(new S.Emitter),this.onKeyDown=this._onKeyDown.event,this._onMouseDown=this._register(new S.Emitter),this.onMouseDown=this._onMouseDown.event,this._onInput=this._register(new S.Emitter),this._onKeyUp=this._register(new S.Emitter),this._onCaseSensitiveKeyDown=this._register(new S.Emitter),this.onCaseSensitiveKeyDown=this._onCaseSensitiveKeyDown.event,this._onRegexKeyDown=this._register(new S.Emitter),this.onRegexKeyDown=this._onRegexKeyDown.event,this._lastHighlightFindOptions=0,this.contextViewProvider=o,this.placeholder=a.placeholder||"",this.validation=a.validation,this.label=a.label||d,this.inputActiveOptionBorder=a.inputActiveOptionBorder,this.inputActiveOptionForeground=a.inputActiveOptionForeground,this.inputActiveOptionBackground=a.inputActiveOptionBackground,this.inputBackground=a.inputBackground,this.inputForeground=a.inputForeground,this.inputBorder=a.inputBorder,this.inputValidationInfoBorder=a.inputValidationInfoBorder,this.inputValidationInfoBackground=a.inputValidationInfoBackground,this.inputValidationInfoForeground=a.inputValidationInfoForeground,this.inputValidationWarningBorder=a.inputValidationWarningBorder,this.inputValidationWarningBackground=a.inputValidationWarningBackground,this.inputValidationWarningForeground=a.inputValidationWarningForeground,this.inputValidationErrorBorder=a.inputValidationErrorBorder,this.inputValidationErrorBackground=a.inputValidationErrorBackground,this.inputValidationErrorForeground=a.inputValidationErrorForeground;const u=a.appendCaseSensitiveLabel||"",r=a.appendWholeWordsLabel||"",i=a.appendRegexLabel||"",n=a.history||[],t=!!a.flexibleHeight,l=!!a.flexibleWidth,h=a.flexibleMaxHeight;this.domNode=document.createElement("div"),this.domNode.classList.add("monaco-findInput"),this.inputBox=this._register(new M.HistoryInputBox(this.domNode,this.contextViewProvider,{placeholder:this.placeholder||"",ariaLabel:this.label||"",validationOptions:{validation:this.validation},inputBackground:this.inputBackground,inputForeground:this.inputForeground,inputBorder:this.inputBorder,inputValidationInfoBackground:this.inputValidationInfoBackground,inputValidationInfoForeground:this.inputValidationInfoForeground,inputValidationInfoBorder:this.inputValidationInfoBorder,inputValidationWarningBackground:this.inputValidationWarningBackground,inputValidationWarningForeground:this.inputValidationWarningForeground,inputValidationWarningBorder:this.inputValidationWarningBorder,inputValidationErrorBackground:this.inputValidationErrorBackground,inputValidationErrorForeground:this.inputValidationErrorForeground,inputValidationErrorBorder:this.inputValidationErrorBorder,history:n,flexibleHeight:t,flexibleWidth:l,flexibleMaxHeight:h})),this.regex=this._register(new C.RegexCheckbox({appendTitle:i,isChecked:!1,inputActiveOptionBorder:this.inputActiveOptionBorder,inputActiveOptionForeground:this.inputActiveOptionForeground,inputActiveOptionBackground:this.inputActiveOptionBackground})),this._register(this.regex.onChange(f=>{this._onDidOptionChange.fire(f),!f&&this.fixFocusOnOptionClickEnabled&&this.inputBox.focus(),this.validate()})),this._register(this.regex.onKeyDown(f=>{this._onRegexKeyDown.fire(f)})),this.wholeWords=this._register(new C.WholeWordsCheckbox({appendTitle:r,isChecked:!1,inputActiveOptionBorder:this.inputActiveOptionBorder,inputActiveOptionForeground:this.inputActiveOptionForeground,inputActiveOptionBackground:this.inputActiveOptionBackground})),this._register(this.wholeWords.onChange(f=>{this._onDidOptionChange.fire(f),!f&&this.fixFocusOnOptionClickEnabled&&this.inputBox.focus(),this.validate()})),this.caseSensitive=this._register(new C.CaseSensitiveCheckbox({appendTitle:u,isChecked:!1,inputActiveOptionBorder:this.inputActiveOptionBorder,inputActiveOptionForeground:this.inputActiveOptionForeground,inputActiveOptionBackground:this.inputActiveOptionBackground})),this._register(this.caseSensitive.onChange(f=>{this._onDidOptionChange.fire(f),!f&&this.fixFocusOnOptionClickEnabled&&this.inputBox.focus(),this.validate()})),this._register(this.caseSensitive.onKeyDown(f=>{this._onCaseSensitiveKeyDown.fire(f)})),this._showOptionButtons&&(this.inputBox.paddingRight=this.caseSensitive.width()+this.wholeWords.width()+this.regex.width());let m=[this.caseSensitive.domNode,this.wholeWords.domNode,this.regex.domNode];this.onkeydown(this.domNode,f=>{if(f.equals(15)||f.equals(17)||f.equals(9)){let v=m.indexOf(document.activeElement);if(v>=0){let y=-1;f.equals(17)?y=(v+1)%m.length:f.equals(15)&&(v===0?y=m.length-1:y=v-1),f.equals(9)?(m[v].blur(),this.inputBox.focus()):y>=0&&m[y].focus(),N.EventHelper.stop(f,!0)}}});let _=document.createElement("div");_.className="controls",_.style.display=this._showOptionButtons?"block":"none",_.appendChild(this.caseSensitive.domNode),_.appendChild(this.wholeWords.domNode),_.appendChild(this.regex.domNode),this.domNode.appendChild(_),c&&c.appendChild(this.domNode),this.onkeydown(this.inputBox.inputElement,f=>this._onKeyDown.fire(f)),this.onkeyup(this.inputBox.inputElement,f=>this._onKeyUp.fire(f)),this.oninput(this.inputBox.inputElement,f=>this._onInput.fire()),this.onmousedown(this.inputBox.inputElement,f=>this._onMouseDown.fire(f))}enable(){this.domNode.classList.remove("disabled"),this.inputBox.enable(),this.regex.enable(),this.wholeWords.enable(),this.caseSensitive.enable()}disable(){this.domNode.classList.add("disabled"),this.inputBox.disable(),this.regex.disable(),this.wholeWords.disable(),this.caseSensitive.disable()}setFocusInputOnOptionClick(c){this.fixFocusOnOptionClickEnabled=c}setEnabled(c){c?this.enable():this.disable()}getValue(){return this.inputBox.value}setValue(c){this.inputBox.value!==c&&(this.inputBox.value=c)}style(c){this.inputActiveOptionBorder=c.inputActiveOptionBorder,this.inputActiveOptionForeground=c.inputActiveOptionForeground,this.inputActiveOptionBackground=c.inputActiveOptionBackground,this.inputBackground=c.inputBackground,this.inputForeground=c.inputForeground,this.inputBorder=c.inputBorder,this.inputValidationInfoBackground=c.inputValidationInfoBackground,this.inputValidationInfoForeground=c.inputValidationInfoForeground,this.inputValidationInfoBorder=c.inputValidationInfoBorder,this.inputValidationWarningBackground=c.inputValidationWarningBackground,this.inputValidationWarningForeground=c.inputValidationWarningForeground,this.inputValidationWarningBorder=c.inputValidationWarningBorder,this.inputValidationErrorBackground=c.inputValidationErrorBackground,this.inputValidationErrorForeground=c.inputValidationErrorForeground,this.inputValidationErrorBorder=c.inputValidationErrorBorder,this.applyStyles()}applyStyles(){if(this.domNode){const c={inputActiveOptionBorder:this.inputActiveOptionBorder,inputActiveOptionForeground:this.inputActiveOptionForeground,inputActiveOptionBackground:this.inputActiveOptionBackground};this.regex.style(c),this.wholeWords.style(c),this.caseSensitive.style(c);const o={inputBackground:this.inputBackground,inputForeground:this.inputForeground,inputBorder:this.inputBorder,inputValidationInfoBackground:this.inputValidationInfoBackground,inputValidationInfoForeground:this.inputValidationInfoForeground,inputValidationInfoBorder:this.inputValidationInfoBorder,inputValidationWarningBackground:this.inputValidationWarningBackground,inputValidationWarningForeground:this.inputValidationWarningForeground,inputValidationWarningBorder:this.inputValidationWarningBorder,inputValidationErrorBackground:this.inputValidationErrorBackground,inputValidationErrorForeground:this.inputValidationErrorForeground,inputValidationErrorBorder:this.inputValidationErrorBorder};this.inputBox.style(o)}}select(){this.inputBox.select()}focus(){this.inputBox.focus()}getCaseSensitive(){return this.caseSensitive.checked}setCaseSensitive(c){this.caseSensitive.checked=c}getWholeWords(){return this.wholeWords.checked}setWholeWords(c){this.wholeWords.checked=c}getRegex(){return this.regex.checked}setRegex(c){this.regex.checked=c,this.validate()}focusOnCaseSensitive(){this.caseSensitive.focus()}highlightFindOptions(){this.domNode.classList.remove("highlight-"+this._lastHighlightFindOptions),this._lastHighlightFindOptions=1-this._lastHighlightFindOptions,this.domNode.classList.add("highlight-"+this._lastHighlightFindOptions)}validate(){this.inputBox.validate()}clearMessage(){this.inputBox.hideMessage()}}e.FindInput=g}),define(Q[433],J([0,1,421,7,177,52,6,160,27,207]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ReplaceInput=e.PreserveCaseCheckbox=void 0;const g=b.localize(0,null),p=b.localize(1,null);class c extends C.Checkbox{constructor(a){super({icon:d.Codicon.preserveCase,title:p+a.appendTitle,isChecked:a.isChecked,inputActiveOptionBorder:a.inputActiveOptionBorder,inputActiveOptionForeground:a.inputActiveOptionForeground,inputActiveOptionBackground:a.inputActiveOptionBackground})}}e.PreserveCaseCheckbox=c;class o extends w.Widget{constructor(a,u,r,i){super();this._showOptionButtons=r,this.fixFocusOnOptionClickEnabled=!0,this.cachedOptionsWidth=0,this._onDidOptionChange=this._register(new S.Emitter),this.onDidOptionChange=this._onDidOptionChange.event,this._onKeyDown=this._register(new S.Emitter),this.onKeyDown=this._onKeyDown.event,this._onMouseDown=this._register(new S.Emitter),this._onInput=this._register(new S.Emitter),this._onKeyUp=this._register(new S.Emitter),this._onPreserveCaseKeyDown=this._register(new S.Emitter),this.onPreserveCaseKeyDown=this._onPreserveCaseKeyDown.event,this.contextViewProvider=u,this.placeholder=i.placeholder||"",this.validation=i.validation,this.label=i.label||g,this.inputActiveOptionBorder=i.inputActiveOptionBorder,this.inputActiveOptionForeground=i.inputActiveOptionForeground,this.inputActiveOptionBackground=i.inputActiveOptionBackground,this.inputBackground=i.inputBackground,this.inputForeground=i.inputForeground,this.inputBorder=i.inputBorder,this.inputValidationInfoBorder=i.inputValidationInfoBorder,this.inputValidationInfoBackground=i.inputValidationInfoBackground,this.inputValidationInfoForeground=i.inputValidationInfoForeground,this.inputValidationWarningBorder=i.inputValidationWarningBorder,this.inputValidationWarningBackground=i.inputValidationWarningBackground,this.inputValidationWarningForeground=i.inputValidationWarningForeground,this.inputValidationErrorBorder=i.inputValidationErrorBorder,this.inputValidationErrorBackground=i.inputValidationErrorBackground,this.inputValidationErrorForeground=i.inputValidationErrorForeground;const n=i.appendPreserveCaseLabel||"",t=i.history||[],l=!!i.flexibleHeight,h=!!i.flexibleWidth,m=i.flexibleMaxHeight;this.domNode=document.createElement("div"),this.domNode.classList.add("monaco-findInput"),this.inputBox=this._register(new M.HistoryInputBox(this.domNode,this.contextViewProvider,{ariaLabel:this.label||"",placeholder:this.placeholder||"",validationOptions:{validation:this.validation},inputBackground:this.inputBackground,inputForeground:this.inputForeground,inputBorder:this.inputBorder,inputValidationInfoBackground:this.inputValidationInfoBackground,inputValidationInfoForeground:this.inputValidationInfoForeground,inputValidationInfoBorder:this.inputValidationInfoBorder,inputValidationWarningBackground:this.inputValidationWarningBackground,inputValidationWarningForeground:this.inputValidationWarningForeground,inputValidationWarningBorder:this.inputValidationWarningBorder,inputValidationErrorBackground:this.inputValidationErrorBackground,inputValidationErrorForeground:this.inputValidationErrorForeground,inputValidationErrorBorder:this.inputValidationErrorBorder,history:t,flexibleHeight:l,flexibleWidth:h,flexibleMaxHeight:m})),this.preserveCase=this._register(new c({appendTitle:n,isChecked:!1,inputActiveOptionBorder:this.inputActiveOptionBorder,inputActiveOptionForeground:this.inputActiveOptionForeground,inputActiveOptionBackground:this.inputActiveOptionBackground})),this._register(this.preserveCase.onChange(v=>{this._onDidOptionChange.fire(v),!v&&this.fixFocusOnOptionClickEnabled&&this.inputBox.focus(),this.validate()})),this._register(this.preserveCase.onKeyDown(v=>{this._onPreserveCaseKeyDown.fire(v)})),this._showOptionButtons?this.cachedOptionsWidth=this.preserveCase.width():this.cachedOptionsWidth=0;let _=[this.preserveCase.domNode];this.onkeydown(this.domNode,v=>{if(v.equals(15)||v.equals(17)||v.equals(9)){let y=_.indexOf(document.activeElement);if(y>=0){let L=-1;v.equals(17)?L=(y+1)%_.length:v.equals(15)&&(y===0?L=_.length-1:L=y-1),v.equals(9)?(_[y].blur(),this.inputBox.focus()):L>=0&&_[L].focus(),N.EventHelper.stop(v,!0)}}});let f=document.createElement("div");f.className="controls",f.style.display=this._showOptionButtons?"block":"none",f.appendChild(this.preserveCase.domNode),this.domNode.appendChild(f),a&&a.appendChild(this.domNode),this.onkeydown(this.inputBox.inputElement,v=>this._onKeyDown.fire(v)),this.onkeyup(this.inputBox.inputElement,v=>this._onKeyUp.fire(v)),this.oninput(this.inputBox.inputElement,v=>this._onInput.fire()),this.onmousedown(this.inputBox.inputElement,v=>this._onMouseDown.fire(v))}enable(){this.domNode.classList.remove("disabled"),this.inputBox.enable(),this.preserveCase.enable()}disable(){this.domNode.classList.add("disabled"),this.inputBox.disable(),this.preserveCase.disable()}setEnabled(a){a?this.enable():this.disable()}style(a){this.inputActiveOptionBorder=a.inputActiveOptionBorder,this.inputActiveOptionForeground=a.inputActiveOptionForeground,this.inputActiveOptionBackground=a.inputActiveOptionBackground,this.inputBackground=a.inputBackground,this.inputForeground=a.inputForeground,this.inputBorder=a.inputBorder,this.inputValidationInfoBackground=a.inputValidationInfoBackground,this.inputValidationInfoForeground=a.inputValidationInfoForeground,this.inputValidationInfoBorder=a.inputValidationInfoBorder,this.inputValidationWarningBackground=a.inputValidationWarningBackground,this.inputValidationWarningForeground=a.inputValidationWarningForeground,this.inputValidationWarningBorder=a.inputValidationWarningBorder,this.inputValidationErrorBackground=a.inputValidationErrorBackground,this.inputValidationErrorForeground=a.inputValidationErrorForeground,this.inputValidationErrorBorder=a.inputValidationErrorBorder,this.applyStyles()}applyStyles(){if(this.domNode){const a={inputActiveOptionBorder:this.inputActiveOptionBorder,inputActiveOptionForeground:this.inputActiveOptionForeground,inputActiveOptionBackground:this.inputActiveOptionBackground};this.preserveCase.style(a);const u={inputBackground:this.inputBackground,inputForeground:this.inputForeground,inputBorder:this.inputBorder,inputValidationInfoBackground:this.inputValidationInfoBackground,inputValidationInfoForeground:this.inputValidationInfoForeground,inputValidationInfoBorder:this.inputValidationInfoBorder,inputValidationWarningBackground:this.inputValidationWarningBackground,inputValidationWarningForeground:this.inputValidationWarningForeground,inputValidationWarningBorder:this.inputValidationWarningBorder,inputValidationErrorBackground:this.inputValidationErrorBackground,inputValidationErrorForeground:this.inputValidationErrorForeground,inputValidationErrorBorder:this.inputValidationErrorBorder};this.inputBox.style(u)}}select(){this.inputBox.select()}focus(){this.inputBox.focus()}getPreserveCase(){return this.preserveCase.checked}setPreserveCase(a){this.preserveCase.checked=a}focusOnPreserve(){this.preserveCase.focus()}validate(){this.inputBox&&this.inputBox.validate()}set width(a){this.inputBox.paddingRight=this.cachedOptionsWidth,this.inputBox.width=a,this.domNode.style.width=a+"px"}dispose(){super.dispose()}}e.ReplaceInput=o}),define(Q[434],J([0,1,425,8,48,83,7,56,15,2,61,204,17,27,112,123,35,50,102]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.cleanMnemonic=e.Menu=e.Direction=e.MENU_ESCAPED_MNEMONIC_REGEX=e.MENU_MNEMONIC_REGEX=void 0,e.MENU_MNEMONIC_REGEX=/\(&([^\s&])\)|(^|[^&])&([^\s&])/,e.MENU_ESCAPED_MNEMONIC_REGEX=/(&)?(&)([^\s&])/g;const t=s.registerCodicon("menu-selection",s.Codicon.check),l=s.registerCodicon("menu-submenu",s.Codicon.chevronRight);var h;(function(I){I[I.Right=0]="Right",I[I.Left=1]="Left"})(h=e.Direction||(e.Direction={}));class m extends w.ActionBar{constructor(k,E,T={}){k.classList.add("monaco-menu-container"),k.setAttribute("role","presentation");const O=document.createElement("div");O.classList.add("monaco-menu"),O.setAttribute("role","presentation"),super(O,{orientation:2,actionViewItemProvider:F=>this.doGetActionViewItem(F,T,A),context:T.context,actionRunner:T.actionRunner,ariaLabel:T.ariaLabel,focusOnlyEnabledItems:!0,triggerKeys:{keys:[3,...o.isMacintosh||o.isLinux?[10]:[]],keyDown:!0}}),this.menuElement=O,this.actionsList.setAttribute("role","menu"),this.actionsList.tabIndex=0,this.menuDisposables=this._register(new g.DisposableStore),this.initializeStyleSheet(k),S.addDisposableListener(O,S.EventType.KEY_DOWN,F=>{new C.StandardKeyboardEvent(F).equals(2)&&F.preventDefault()}),T.enableMnemonics&&this.menuDisposables.add(S.addDisposableListener(O,S.EventType.KEY_DOWN,F=>{const D=F.key.toLocaleLowerCase();if(this.mnemonics.has(D)){S.EventHelper.stop(F,!0);const R=this.mnemonics.get(D);if(R.length===1&&(R[0]instanceof f&&R[0].container&&this.focusItemByElement(R[0].container),R[0].onClick(F)),R.length>1){const W=R.shift();W&&W.container&&(this.focusItemByElement(W.container),R.push(W)),this.mnemonics.set(D,R)}}})),o.isLinux&&this._register(S.addDisposableListener(O,S.EventType.KEY_DOWN,F=>{const D=new C.StandardKeyboardEvent(F);D.equals(14)||D.equals(11)?(this.focusedItem=this.viewItems.length-1,this.focusNext(),S.EventHelper.stop(F,!0)):(D.equals(13)||D.equals(12))&&(this.focusedItem=0,this.focusPrevious(),S.EventHelper.stop(F,!0))})),this._register(S.addDisposableListener(this.domNode,S.EventType.MOUSE_OUT,F=>{let D=F.relatedTarget;S.isAncestor(D,this.domNode)||(this.focusedItem=void 0,this.updateFocus(),F.stopPropagation())})),this._register(S.addDisposableListener(this.actionsList,S.EventType.MOUSE_OVER,F=>{let D=F.target;if(!(!D||!S.isAncestor(D,this.actionsList)||D===this.actionsList)){for(;D.parentElement!==this.actionsList&&D.parentElement!==null;)D=D.parentElement;if(D.classList.contains("action-item")){const R=this.focusedItem;this.setFocusedItem(D),R!==this.focusedItem&&this.updateFocus()}}}));let A={parent:this};this.mnemonics=new Map,this.scrollableElement=this._register(new p.DomScrollableElement(O,{alwaysConsumeMouseWheel:!0,horizontal:2,vertical:3,verticalScrollbarSize:7,handleMouseWheel:!0,useShadows:!0}));const B=this.scrollableElement.getDomNode();B.style.position="",this._register(S.addDisposableListener(B,S.EventType.MOUSE_UP,F=>{F.preventDefault()})),O.style.maxHeight=`${Math.max(10,window.innerHeight-k.getBoundingClientRect().top-35)}px`,E=E.filter(F=>{var D;return((D=T.submenuIds)===null||D===void 0?void 0:D.has(F.id))?(console.warn(`Found submenu cycle: ${F.id}`),!1):!0}),this.push(E,{icon:!0,label:!0,isMenu:!0}),k.appendChild(this.scrollableElement.getDomNode()),this.scrollableElement.scanDomNode(),this.viewItems.filter(F=>!(F instanceof v)).forEach((F,D,R)=>{F.updatePositionInSet(D+1,R.length)})}initializeStyleSheet(k){S.isInShadowDOM(k)?(this.styleSheet=S.createStyleSheet(k),this.styleSheet.textContent=L):(m.globalStyleSheet||(m.globalStyleSheet=S.createStyleSheet(),m.globalStyleSheet.textContent=L),this.styleSheet=m.globalStyleSheet)}style(k){const E=this.getContainer(),T=k.foregroundColor?`${k.foregroundColor}`:"",O=k.backgroundColor?`${k.backgroundColor}`:"",A=k.borderColor?`1px solid ${k.borderColor}`:"",B=k.shadowColor?`0 2px 4px ${k.shadowColor}`:"";E.style.border=A,this.domNode.style.color=T,this.domNode.style.backgroundColor=O,E.style.boxShadow=B,this.viewItems&&this.viewItems.forEach(F=>{(F instanceof _||F instanceof v)&&F.style(k)})}getContainer(){return this.scrollableElement.getDomNode()}get onScroll(){return this.scrollableElement.onScroll}focusItemByElement(k){const E=this.focusedItem;this.setFocusedItem(k),E!==this.focusedItem&&this.updateFocus()}setFocusedItem(k){for(let E=0;E{!this.element||(this._register(S.addDisposableListener(this.element,S.EventType.MOUSE_UP,O=>{if(S.EventHelper.stop(O,!0),r.isFirefox){if(new i.StandardMouseEvent(O).rightButton)return;this.onClick(O)}else setTimeout(()=>{this.onClick(O)},0)})),this._register(S.addDisposableListener(this.element,S.EventType.CONTEXT_MENU,O=>{S.EventHelper.stop(O,!0)})))},100),this._register(this.runOnceToEnableMouseUp)}render(k){super.render(k),!!this.element&&(this.container=k,this.item=S.append(this.element,S.$("a.action-menu-item")),this._action.id===M.Separator.ID?this.item.setAttribute("role","presentation"):(this.item.setAttribute("role","menuitem"),this.mnemonic&&this.item.setAttribute("aria-keyshortcuts",`${this.mnemonic}`)),this.check=S.append(this.item,S.$("span.menu-item-check"+t.cssSelector)),this.check.setAttribute("role","none"),this.label=S.append(this.item,S.$("span.action-label")),this.options.label&&this.options.keybinding&&(S.append(this.item,S.$("span.keybinding")).textContent=this.options.keybinding),this.runOnceToEnableMouseUp.schedule(),this.updateClass(),this.updateLabel(),this.updateTooltip(),this.updateEnabled(),this.updateChecked())}blur(){super.blur(),this.applyStyle()}focus(){super.focus(),this.item&&this.item.focus(),this.applyStyle()}updatePositionInSet(k,E){this.item&&(this.item.setAttribute("aria-posinset",`${k}`),this.item.setAttribute("aria-setsize",`${E}`))}updateLabel(){if(!!this.label&&this.options.label){S.clearNode(this.label);let k=n.stripIcons(this.getAction().label);if(k){const E=y(k);this.options.enableMnemonics||(k=E),this.label.setAttribute("aria-label",E.replace(/&&/g,"&"));const T=e.MENU_MNEMONIC_REGEX.exec(k);if(T){k=N.escape(k),e.MENU_ESCAPED_MNEMONIC_REGEX.lastIndex=0;let O=e.MENU_ESCAPED_MNEMONIC_REGEX.exec(k);for(;O&&O[1];)O=e.MENU_ESCAPED_MNEMONIC_REGEX.exec(k);const A=B=>B.replace(/&&/g,"&");O?this.label.append(N.ltrim(A(k.substr(0,O.index))," "),S.$("u",{"aria-hidden":"true"},O[3]),N.rtrim(A(k.substr(O.index+O[0].length))," ")):this.label.innerText=A(k).trim(),this.item&&this.item.setAttribute("aria-keyshortcuts",(T[1]?T[1]:T[3]).toLocaleLowerCase())}else this.label.innerText=k.replace(/&&/g,"&").trim()}}}updateTooltip(){let k=null;this.getAction().tooltip?k=this.getAction().tooltip:!this.options.label&&this.getAction().label&&this.options.icon&&(k=this.getAction().label,this.options.keybinding&&(k=b.localize(0,null,k,this.options.keybinding))),k&&this.item&&(this.item.title=k)}updateClass(){this.cssClass&&this.item&&this.item.classList.remove(...this.cssClass.split(" ")),this.options.icon&&this.label?(this.cssClass=this.getAction().class||"",this.label.classList.add("icon"),this.cssClass&&this.label.classList.add(...this.cssClass.split(" ")),this.updateEnabled()):this.label&&this.label.classList.remove("icon")}updateEnabled(){this.getAction().enabled?(this.element&&(this.element.classList.remove("disabled"),this.element.removeAttribute("aria-disabled")),this.item&&(this.item.classList.remove("disabled"),this.item.removeAttribute("aria-disabled"),this.item.tabIndex=0)):(this.element&&(this.element.classList.add("disabled"),this.element.setAttribute("aria-disabled","true")),this.item&&(this.item.classList.add("disabled"),this.item.setAttribute("aria-disabled","true")))}updateChecked(){!this.item||(this.getAction().checked?(this.item.classList.add("checked"),this.item.setAttribute("role","menuitemcheckbox"),this.item.setAttribute("aria-checked","true")):(this.item.classList.remove("checked"),this.item.setAttribute("role","menuitem"),this.item.setAttribute("aria-checked","false")))}getMnemonic(){return this.mnemonic}applyStyle(){if(!!this.menuStyle){const k=this.element&&this.element.classList.contains("focused"),E=k&&this.menuStyle.selectionForegroundColor?this.menuStyle.selectionForegroundColor:this.menuStyle.foregroundColor,T=k&&this.menuStyle.selectionBackgroundColor?this.menuStyle.selectionBackgroundColor:void 0,O=k&&this.menuStyle.selectionBorderColor?`thin solid ${this.menuStyle.selectionBorderColor}`:"";this.item&&(this.item.style.color=E?E.toString():"",this.item.style.backgroundColor=T?T.toString():""),this.check&&(this.check.style.color=E?E.toString():""),this.container&&(this.container.style.border=O)}}style(k){this.menuStyle=k,this.applyStyle()}}class f extends _{constructor(k,E,T,O){super(k,k,O);this.submenuActions=E,this.parentData=T,this.submenuOptions=O,this.mysubmenu=null,this.submenuDisposables=this._register(new g.DisposableStore),this.mouseOver=!1,this.expandDirection=O&&O.expandDirection!==void 0?O.expandDirection:h.Right,this.showScheduler=new d.RunOnceScheduler(()=>{this.mouseOver&&(this.cleanupExistingSubmenu(!1),this.createSubmenu(!1))},250),this.hideScheduler=new d.RunOnceScheduler(()=>{this.element&&!S.isAncestor(S.getActiveElement(),this.element)&&this.parentData.submenu===this.mysubmenu&&(this.parentData.parent.focus(!1),this.cleanupExistingSubmenu(!0))},750)}render(k){super.render(k),!!this.element&&(this.item&&(this.item.classList.add("monaco-submenu-item"),this.item.tabIndex=0,this.item.setAttribute("aria-haspopup","true"),this.updateAriaExpanded("false"),this.submenuIndicator=S.append(this.item,S.$("span.submenu-indicator"+l.cssSelector)),this.submenuIndicator.setAttribute("aria-hidden","true")),this._register(S.addDisposableListener(this.element,S.EventType.KEY_UP,E=>{let T=new C.StandardKeyboardEvent(E);(T.equals(17)||T.equals(3))&&(S.EventHelper.stop(E,!0),this.createSubmenu(!0))})),this._register(S.addDisposableListener(this.element,S.EventType.KEY_DOWN,E=>{let T=new C.StandardKeyboardEvent(E);S.getActiveElement()===this.item&&(T.equals(17)||T.equals(3))&&S.EventHelper.stop(E,!0)})),this._register(S.addDisposableListener(this.element,S.EventType.MOUSE_OVER,E=>{this.mouseOver||(this.mouseOver=!0,this.showScheduler.schedule())})),this._register(S.addDisposableListener(this.element,S.EventType.MOUSE_LEAVE,E=>{this.mouseOver=!1})),this._register(S.addDisposableListener(this.element,S.EventType.FOCUS_OUT,E=>{this.element&&!S.isAncestor(S.getActiveElement(),this.element)&&this.hideScheduler.schedule()})),this._register(this.parentData.parent.onScroll(()=>{this.parentData.parent.focus(!1),this.cleanupExistingSubmenu(!1)})))}updateEnabled(){}onClick(k){S.EventHelper.stop(k,!0),this.cleanupExistingSubmenu(!1),this.createSubmenu(!0)}cleanupExistingSubmenu(k){if(this.parentData.submenu&&(k||this.parentData.submenu!==this.mysubmenu)){try{this.parentData.submenu.dispose()}catch(E){}this.parentData.submenu=void 0,this.updateAriaExpanded("false"),this.submenuContainer&&(this.submenuDisposables.clear(),this.submenuContainer=void 0)}}calculateSubmenuMenuLayout(k,E,T,O){const A={top:0,left:0};return A.left=c.layout(k.width,E.width,{position:O===h.Right?0:1,offset:T.left,size:T.width}),A.left>=T.left&&A.left{new C.StandardKeyboardEvent(R).equals(15)&&(S.EventHelper.stop(R,!0),this.parentData.parent.focus(),this.cleanupExistingSubmenu(!0))})),this.submenuDisposables.add(S.addDisposableListener(this.submenuContainer,S.EventType.KEY_DOWN,R=>{new C.StandardKeyboardEvent(R).equals(15)&&S.EventHelper.stop(R,!0)})),this.submenuDisposables.add(this.parentData.submenu.onDidCancel(()=>{this.parentData.parent.focus(),this.cleanupExistingSubmenu(!0)})),this.parentData.submenu.focus(k),this.mysubmenu=this.parentData.submenu}}updateAriaExpanded(k){var E;this.item&&((E=this.item)===null||E===void 0||E.setAttribute("aria-expanded",k))}applyStyle(){if(super.applyStyle(),!!this.menuStyle){const E=this.element&&this.element.classList.contains("focused")&&this.menuStyle.selectionForegroundColor?this.menuStyle.selectionForegroundColor:this.menuStyle.foregroundColor;this.submenuIndicator&&(this.submenuIndicator.style.color=E?`${E}`:""),this.parentData.submenu&&this.parentData.submenu.style(this.menuStyle)}}dispose(){super.dispose(),this.hideScheduler.dispose(),this.mysubmenu&&(this.mysubmenu.dispose(),this.mysubmenu=null),this.submenuContainer&&(this.submenuContainer=void 0)}}class v extends a.ActionViewItem{style(k){this.label&&(this.label.style.borderBottomColor=k.separatorColor?`${k.separatorColor}`:"")}}function y(I){const k=e.MENU_MNEMONIC_REGEX,E=k.exec(I);if(!E)return I;const T=!E[1];return I.replace(k,T?"$2$3":"").trim()}e.cleanMnemonic=y;let L=` +.monaco-menu { + font-size: 13px; + +} + +${u.formatRule(t)} +${u.formatRule(l)} + +.monaco-menu .monaco-action-bar { + text-align: right; + overflow: hidden; + white-space: nowrap; +} + +.monaco-menu .monaco-action-bar .actions-container { + display: flex; + margin: 0 auto; + padding: 0; + width: 100%; + justify-content: flex-end; +} + +.monaco-menu .monaco-action-bar.vertical .actions-container { + display: inline-block; +} + +.monaco-menu .monaco-action-bar.reverse .actions-container { + flex-direction: row-reverse; +} + +.monaco-menu .monaco-action-bar .action-item { + cursor: pointer; + display: inline-block; + transition: transform 50ms ease; + position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */ +} + +.monaco-menu .monaco-action-bar .action-item.disabled { + cursor: default; +} + +.monaco-menu .monaco-action-bar.animated .action-item.active { + transform: scale(1.272019649, 1.272019649); /* 1.272019649 = \u221A\u03C6 */ +} + +.monaco-menu .monaco-action-bar .action-item .icon, +.monaco-menu .monaco-action-bar .action-item .codicon { + display: inline-block; +} + +.monaco-menu .monaco-action-bar .action-item .codicon { + display: flex; + align-items: center; +} + +.monaco-menu .monaco-action-bar .action-label { + font-size: 11px; + margin-right: 4px; +} + +.monaco-menu .monaco-action-bar .action-item.disabled .action-label, +.monaco-menu .monaco-action-bar .action-item.disabled .action-label:hover { + opacity: 0.4; +} + +/* Vertical actions */ + +.monaco-menu .monaco-action-bar.vertical { + text-align: left; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + display: block; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator { + display: block; + border-bottom: 1px solid #bbb; + padding-top: 1px; + margin-left: .8em; + margin-right: .8em; +} + +.monaco-menu .secondary-actions .monaco-action-bar .action-label { + margin-left: 6px; +} + +/* Action Items */ +.monaco-menu .monaco-action-bar .action-item.select-container { + overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */ + flex: 1; + max-width: 170px; + min-width: 60px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 10px; +} + +.monaco-menu .monaco-action-bar.vertical { + margin-left: 0; + overflow: visible; +} + +.monaco-menu .monaco-action-bar.vertical .actions-container { + display: block; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + padding: 0; + transform: none; + display: flex; +} + +.monaco-menu .monaco-action-bar.vertical .action-item.active { + transform: none; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item { + flex: 1 1 auto; + display: flex; + height: 2em; + align-items: center; + position: relative; +} + +.monaco-menu .monaco-action-bar.vertical .action-label { + flex: 1 1 auto; + text-decoration: none; + padding: 0 1em; + background: none; + font-size: 12px; + line-height: 1; +} + +.monaco-menu .monaco-action-bar.vertical .keybinding, +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + display: inline-block; + flex: 2 1 auto; + padding: 0 1em; + text-align: right; + font-size: 12px; + line-height: 1; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + height: 100%; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon { + font-size: 16px !important; + display: flex; + align-items: center; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator.codicon::before { + margin-left: auto; + margin-right: -20px; +} + +.monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding, +.monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator { + opacity: 0.4; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator) { + display: inline-block; + box-sizing: border-box; + margin: 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + position: static; + overflow: visible; +} + +.monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu { + position: absolute; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator { + padding: 0.5em 0 0 0; + margin-bottom: 0.5em; + width: 100%; + height: 0px !important; + margin-left: .8em !important; + margin-right: .8em !important; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator.text { + padding: 0.7em 1em 0.1em 1em; + font-weight: bold; + opacity: 1; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:hover { + color: inherit; +} + +.monaco-menu .monaco-action-bar.vertical .menu-item-check { + position: absolute; + visibility: hidden; + width: 1em; + height: 100%; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check { + visibility: visible; + display: flex; + align-items: center; + justify-content: center; +} + +/* Context Menu */ + +.context-view.monaco-menu-container { + outline: 0; + border: none; + animation: fadeIn 0.083s linear; + -webkit-app-region: no-drag; +} + +.context-view.monaco-menu-container :focus, +.context-view.monaco-menu-container .monaco-action-bar.vertical:focus, +.context-view.monaco-menu-container .monaco-action-bar.vertical :focus { + outline: 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-item { + border: thin solid transparent; /* prevents jumping behaviour on hover or focus */ +} + + +/* High Contrast Theming */ +:host-context(.hc-black) .context-view.monaco-menu-container { + box-shadow: none; +} + +:host-context(.hc-black) .monaco-menu .monaco-action-bar.vertical .action-item.focused { + background: none; +} + +/* Vertical Action Bar Styles */ + +.monaco-menu .monaco-action-bar.vertical { + padding: .5em 0; +} + +.monaco-menu .monaco-action-bar.vertical .action-menu-item { + height: 1.8em; +} + +.monaco-menu .monaco-action-bar.vertical .action-label:not(.separator), +.monaco-menu .monaco-action-bar.vertical .keybinding { + font-size: inherit; + padding: 0 2em; +} + +.monaco-menu .monaco-action-bar.vertical .menu-item-check { + font-size: inherit; + width: 2em; +} + +.monaco-menu .monaco-action-bar.vertical .action-label.separator { + font-size: inherit; + padding: 0.2em 0 0 0; + margin-bottom: 0.2em; +} + +:host-context(.linux) .monaco-menu .monaco-action-bar.vertical .action-label.separator { + margin-left: 0; + margin-right: 0; +} + +.monaco-menu .monaco-action-bar.vertical .submenu-indicator { + font-size: 60%; + padding: 0 1.8em; +} + +:host-context(.linux) .monaco-menu .monaco-action-bar.vertical .submenu-indicator { + height: 100%; + mask-size: 10px 10px; + -webkit-mask-size: 10px 10px; +} + +.monaco-menu .action-item { + cursor: default; +} + +/* Arrows */ +.monaco-scrollable-element > .scrollbar > .scra { + cursor: pointer; + font-size: 11px !important; +} + +.monaco-scrollable-element > .visible { + opacity: 1; + + /* Background rule added for IE9 - to allow clicks on dom node */ + background:rgba(0,0,0,0); + + transition: opacity 100ms linear; +} +.monaco-scrollable-element > .invisible { + opacity: 0; + pointer-events: none; +} +.monaco-scrollable-element > .invisible.fade { + transition: opacity 800ms linear; +} + +/* Scrollable Content Inset Shadow */ +.monaco-scrollable-element > .shadow { + position: absolute; + display: none; +} +.monaco-scrollable-element > .shadow.top { + display: block; + top: 0; + left: 3px; + height: 3px; + width: 100%; + box-shadow: #DDD 0 6px 6px -6px inset; +} +.monaco-scrollable-element > .shadow.left { + display: block; + top: 3px; + left: 0; + height: 100%; + width: 3px; + box-shadow: #DDD 6px 0 6px -6px inset; +} +.monaco-scrollable-element > .shadow.top-left-corner { + display: block; + top: 0; + left: 0; + height: 3px; + width: 3px; +} +.monaco-scrollable-element > .shadow.top.left { + box-shadow: #DDD 6px 6px 6px -6px inset; +} + +/* ---------- Default Style ---------- */ + +:host-context(.vs) .monaco-scrollable-element > .scrollbar > .slider { + background: rgba(100, 100, 100, .4); +} +:host-context(.vs-dark) .monaco-scrollable-element > .scrollbar > .slider { + background: rgba(121, 121, 121, .4); +} +:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider { + background: rgba(111, 195, 223, .6); +} + +.monaco-scrollable-element > .scrollbar > .slider:hover { + background: rgba(100, 100, 100, .7); +} +:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider:hover { + background: rgba(111, 195, 223, .8); +} + +.monaco-scrollable-element > .scrollbar > .slider.active { + background: rgba(0, 0, 0, .6); +} +:host-context(.vs-dark) .monaco-scrollable-element > .scrollbar > .slider.active { + background: rgba(191, 191, 191, .4); +} +:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider.active { + background: rgba(111, 195, 223, 1); +} + +:host-context(.vs-dark) .monaco-scrollable-element .shadow.top { + box-shadow: none; +} + +:host-context(.vs-dark) .monaco-scrollable-element .shadow.left { + box-shadow: #000 6px 0 6px -6px inset; +} + +:host-context(.vs-dark) .monaco-scrollable-element .shadow.top.left { + box-shadow: #000 6px 6px 6px -6px inset; +} + +:host-context(.hc-black) .monaco-scrollable-element .shadow.top { + box-shadow: none; +} + +:host-context(.hc-black) .monaco-scrollable-element .shadow.left { + box-shadow: none; +} + +:host-context(.hc-black) .monaco-scrollable-element .shadow.top.left { + box-shadow: none; +} +`}),define(Q[435],J([0,1,7,177,2,56,82,50,125]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.QuickInputBox=void 0;const d=b.$;class g extends M.Disposable{constructor(c){super();this.parent=c,this.onKeyDown=o=>b.addDisposableListener(this.inputBox.inputElement,b.EventType.KEY_DOWN,s=>{o(new w.StandardKeyboardEvent(s))}),this.onMouseDown=o=>b.addDisposableListener(this.inputBox.inputElement,b.EventType.MOUSE_DOWN,s=>{o(new C.StandardMouseEvent(s))}),this.onDidChange=o=>this.inputBox.onDidChange(o),this.container=b.append(this.parent,d(".quick-input-box")),this.inputBox=this._register(new N.InputBox(this.container,void 0))}get value(){return this.inputBox.value}set value(c){this.inputBox.value=c}select(c=null){this.inputBox.select(c)}isSelectionAtEnd(){return this.inputBox.isSelectionAtEnd()}get placeholder(){return this.inputBox.inputElement.getAttribute("placeholder")||""}set placeholder(c){this.inputBox.setPlaceHolder(c)}get ariaLabel(){return this.inputBox.getAriaLabel()}set ariaLabel(c){this.inputBox.setAriaLabel(c)}get password(){return this.inputBox.inputElement.type==="password"}set password(c){this.inputBox.inputElement.type=c?"password":"text"}setAttribute(c,o){this.inputBox.inputElement.setAttribute(c,o)}removeAttribute(c){this.inputBox.inputElement.removeAttribute(c)}showDecoration(c){c===S.default.Ignore?this.inputBox.hideMessage():this.inputBox.showMessage({type:c===S.default.Info?1:c===S.default.Warning?2:3,content:""})}stylesForType(c){return this.inputBox.stylesForType(c===S.default.Info?1:c===S.default.Warning?2:3)}setFocus(){this.inputBox.focus()}layout(){this.inputBox.layout()}style(c){this.inputBox.style(c)}}e.QuickInputBox=g}),define(Q[436],J([4,5]),function(q,e){return q.create("vs/base/common/errorMessage",e)}),define(Q[437],J([0,1,436,20,19]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.toErrorMessage=void 0;function w(g,p){return p&&(g.stack||g.stacktrace)?b.localize(0,null,C(g),S(g.stack)||S(g.stacktrace)):C(g)}function S(g){return Array.isArray(g)?g.join(` +`):g}function C(g){return typeof g.code=="string"&&typeof g.errno=="number"&&typeof g.syscall=="string"?b.localize(1,null,g.message):g.message||b.localize(2,null)}function d(g=null,p=!1){if(!g)return b.localize(3,null);if(Array.isArray(g)){const c=M.coalesce(g),o=d(c[0],p);return c.length>1?b.localize(4,null,o,c.length):o}if(N.isString(g))return g;if(g.detail){const c=g.detail;if(c.error)return w(c.error,p);if(c.exception)return w(c.exception,p)}return g.stack?w(g,p):g.message?g.message:b.localize(5,null)}e.toErrorMessage=d}),define(Q[438],J([4,5]),function(q,e){return q.create("vs/base/common/keybindingLabels",e)}),define(Q[235],J([0,1,438]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AriaLabelProvider=e.UILabelProvider=e.ModifierLabelProvider=void 0;class N{constructor(S,C,d=C){this.modifierLabels=[null],this.modifierLabels[2]=S,this.modifierLabels[1]=C,this.modifierLabels[3]=d}toLabel(S,C,d){if(C.length===0)return null;const g=[];for(let p=0,c=C.length;p{k.checkbox.offsetParent||D.preventDefault()})),k.checkbox=b.append(E,t("input.quick-input-list-checkbox")),k.checkbox.type="checkbox",k.toDisposeTemplate.push(b.addStandardDisposableListener(k.checkbox,b.EventType.CHANGE,D=>{k.element.checked=k.checkbox.checked}));const T=b.append(E,t(".quick-input-list-rows")),O=b.append(T,t(".quick-input-list-row")),A=b.append(T,t(".quick-input-list-row"));k.label=new d.IconLabel(O,{supportHighlights:!0,supportDescriptionHighlights:!0,supportIcons:!0});const B=b.append(O,t(".quick-input-list-entry-keybinding"));k.keybinding=new i.KeybindingLabel(B,o.OS);const F=b.append(A,t(".quick-input-list-label-meta"));return k.detail=new g.HighlightedLabel(F,!0),k.separator=b.append(k.entry,t(".quick-input-list-separator")),k.actionBar=new s.ActionBar(k.entry),k.actionBar.domNode.classList.add("quick-input-list-entry-action-bar"),k.toDisposeTemplate.push(k.actionBar),k}renderElement(I,k,E){E.toDisposeElement=N.dispose(E.toDisposeElement),E.element=I,E.checkbox.checked=I.checked,E.toDisposeElement.push(I.onChecked(D=>E.checkbox.checked=D));const{labelHighlights:T,descriptionHighlights:O,detailHighlights:A}=I,B=Object.create(null);B.matches=T||[],B.descriptionTitle=I.saneDescription,B.descriptionMatches=O||[],B.extraClasses=I.item.iconClasses,B.italic=I.item.italic,B.strikethrough=I.item.strikethrough,E.label.setLabel(I.saneLabel,I.saneDescription,B),E.keybinding.set(I.item.keybinding),E.detail.set(I.saneDetail,A),I.separator&&I.separator.label?(E.separator.textContent=I.separator.label,E.separator.style.display=""):E.separator.style.display="none",E.entry.classList.toggle("quick-input-list-separator-border",!!I.separator),E.actionBar.clear();const F=I.item.buttons;F&&F.length?(E.actionBar.push(F.map((D,R)=>{let W=D.iconClass||(D.iconPath?u.getIconClass(D.iconPath):void 0);D.alwaysVisible&&(W=W?`${W} always-visible`:"always-visible");const x=new a.Action(`id-${R}`,"",W,!0,()=>Ie(this,void 0,void 0,function*(){I.fireButtonTriggered({button:D,item:I.item})}));return x.tooltip=D.tooltip||"",x}),{icon:!0,label:!1}),E.entry.classList.add("has-actions")):E.entry.classList.remove("has-actions")}disposeElement(I,k,E){E.toDisposeElement=N.dispose(E.toDisposeElement)}disposeTemplate(I){I.toDisposeElement=N.dispose(I.toDisposeElement),I.toDisposeTemplate=N.dispose(I.toDisposeTemplate)}}h.ID="listelement";class m{getHeight(I){return I.saneDetail?44:22}getTemplateId(I){return h.ID}}var _;(function(L){L[L.First=1]="First",L[L.Second=2]="Second",L[L.Last=3]="Last",L[L.Next=4]="Next",L[L.Previous=5]="Previous",L[L.NextPage=6]="NextPage",L[L.PreviousPage=7]="PreviousPage"})(_=e.QuickInputListFocus||(e.QuickInputListFocus={}));class f{constructor(I,k,E){this.parent=I,this.inputElements=[],this.elements=[],this.elementsToIndexes=new Map,this.matchOnDescription=!1,this.matchOnDetail=!1,this.matchOnLabel=!0,this.matchOnMeta=!0,this.sortByLabel=!0,this._onChangedAllVisibleChecked=new S.Emitter,this.onChangedAllVisibleChecked=this._onChangedAllVisibleChecked.event,this._onChangedCheckedCount=new S.Emitter,this.onChangedCheckedCount=this._onChangedCheckedCount.event,this._onChangedVisibleCount=new S.Emitter,this.onChangedVisibleCount=this._onChangedVisibleCount.event,this._onChangedCheckedElements=new S.Emitter,this.onChangedCheckedElements=this._onChangedCheckedElements.event,this._onButtonTriggered=new S.Emitter,this.onButtonTriggered=this._onButtonTriggered.event,this._onKeyDown=new S.Emitter,this.onKeyDown=this._onKeyDown.event,this._onLeave=new S.Emitter,this.onLeave=this._onLeave.event,this._fireCheckedEvents=!0,this.elementDisposables=[],this.disposables=[],this.id=k,this.container=b.append(this.parent,t(".quick-input-list"));const T=new m,O=new y;this.list=E.createList("QuickInput",this.container,T,[new h],{identityProvider:{getId:A=>A.saneLabel},setRowLineHeight:!1,multipleSelectionSupport:!1,horizontalScrolling:!1,accessibilityProvider:O}),this.list.getHTMLElement().id=k,this.disposables.push(this.list),this.disposables.push(this.list.onKeyDown(A=>{const B=new C.StandardKeyboardEvent(A);switch(B.keyCode){case 10:this.toggleCheckbox();break;case 31:(o.isMacintosh?A.metaKey:A.ctrlKey)&&this.list.setFocus(c.range(this.list.length));break;case 16:const F=this.list.getFocus();F.length===1&&F[0]===0&&this._onLeave.fire();break;case 18:const D=this.list.getFocus();D.length===1&&D[0]===this.list.length-1&&this._onLeave.fire();break}this._onKeyDown.fire(B)})),this.disposables.push(this.list.onMouseDown(A=>{A.browserEvent.button!==2&&A.browserEvent.preventDefault()})),this.disposables.push(b.addDisposableListener(this.container,b.EventType.CLICK,A=>{(A.x||A.y)&&this._onLeave.fire()})),this.disposables.push(this.list.onMouseMiddleClick(A=>{this._onLeave.fire()})),this.disposables.push(this.list.onContextMenu(A=>{typeof A.index=="number"&&(A.browserEvent.preventDefault(),this.list.setSelection([A.index]))})),this.disposables.push(this._onChangedAllVisibleChecked,this._onChangedCheckedCount,this._onChangedVisibleCount,this._onChangedCheckedElements,this._onButtonTriggered,this._onLeave,this._onKeyDown)}get onDidChangeFocus(){return S.Event.map(this.list.onDidChangeFocus,I=>I.elements.map(k=>k.item))}get onDidChangeSelection(){return S.Event.map(this.list.onDidChangeSelection,I=>({items:I.elements.map(k=>k.item),event:I.browserEvent}))}getAllVisibleChecked(){return this.allVisibleChecked(this.elements,!1)}allVisibleChecked(I,k=!0){for(let E=0,T=I.length;E{k.hidden||(k.checked=I)})}finally{this._fireCheckedEvents=!0,this.fireCheckedEvents()}}setElements(I){this.elementDisposables=N.dispose(this.elementDisposables);const k=E=>this.fireButtonTriggered(E);this.inputElements=I,this.elements=I.reduce((E,T,O)=>{var A,B,F;if(T.type!=="separator"){const D=O&&I[O-1],R=T.label&&T.label.replace(/\r?\n/g," "),W=T.meta&&T.meta.replace(/\r?\n/g," "),x=T.description&&T.description.replace(/\r?\n/g," "),K=T.detail&&T.detail.replace(/\r?\n/g," "),Y=T.ariaLabel||[R,x,K].map(ee=>ee&&M.parseLabelWithIcons(ee).text).filter(ee=>!!ee).join(", ");E.push(new l({index:O,item:T,saneLabel:R,saneMeta:W,saneAriaLabel:Y,saneDescription:x,saneDetail:K,labelHighlights:(A=T.highlights)===null||A===void 0?void 0:A.label,descriptionHighlights:(B=T.highlights)===null||B===void 0?void 0:B.description,detailHighlights:(F=T.highlights)===null||F===void 0?void 0:F.detail,checked:!1,separator:D&&D.type==="separator"?D:void 0,fireButtonTriggered:k}))}return E},[]),this.elementDisposables.push(...this.elements),this.elementDisposables.push(...this.elements.map(E=>E.onChecked(()=>this.fireCheckedEvents()))),this.elementsToIndexes=this.elements.reduce((E,T,O)=>(E.set(T.item,O),E),new Map),this.list.splice(0,this.list.length),this.list.splice(0,this.list.length,this.elements),this._onChangedVisibleCount.fire(this.elements.length)}getFocusedElements(){return this.list.getFocusedElements().map(I=>I.item)}setFocusedElements(I){if(this.list.setFocus(I.filter(k=>this.elementsToIndexes.has(k)).map(k=>this.elementsToIndexes.get(k))),I.length>0){const k=this.list.getFocus()[0];typeof k=="number"&&this.list.reveal(k)}}getActiveDescendant(){return this.list.getHTMLElement().getAttribute("aria-activedescendant")}setSelectedElements(I){this.list.setSelection(I.filter(k=>this.elementsToIndexes.has(k)).map(k=>this.elementsToIndexes.get(k)))}getCheckedElements(){return this.elements.filter(I=>I.checked).map(I=>I.item)}setCheckedElements(I){try{this._fireCheckedEvents=!1;const k=new Set;for(const E of I)k.add(E);for(const E of this.elements)E.checked=k.has(E.item)}finally{this._fireCheckedEvents=!0,this.fireCheckedEvents()}}set enabled(I){this.list.getHTMLElement().style.pointerEvents=I?"":"none"}focus(I){if(!!this.list.length){switch(I===_.Next&&this.list.getFocus()[0]===this.list.length-1&&(I=_.First),I===_.Previous&&this.list.getFocus()[0]===0&&(I=_.Last),I===_.Second&&this.list.length<2&&(I=_.First),I){case _.First:this.list.focusFirst();break;case _.Second:this.list.focusNth(1);break;case _.Last:this.list.focusLast();break;case _.Next:this.list.focusNext();break;case _.Previous:this.list.focusPrevious();break;case _.NextPage:this.list.focusNextPage();break;case _.PreviousPage:this.list.focusPreviousPage();break}const k=this.list.getFocus()[0];typeof k=="number"&&this.list.reveal(k)}}clearFocus(){this.list.setFocus([])}domFocus(){this.list.domFocus()}layout(I){this.list.getHTMLElement().style.maxHeight=I?`calc(${Math.floor(I/44)*44}px)`:"",this.list.layout()}filter(I){if(!(this.sortByLabel||this.matchOnLabel||this.matchOnDescription||this.matchOnDetail))return this.list.layout(),!1;I=I.trim(),!I||!(this.matchOnLabel||this.matchOnDescription||this.matchOnDetail)?this.elements.forEach(E=>{E.labelHighlights=void 0,E.descriptionHighlights=void 0,E.detailHighlights=void 0,E.hidden=!1;const T=E.index&&this.inputElements[E.index-1];E.separator=T&&T.type==="separator"?T:void 0}):this.elements.forEach(E=>{const T=this.matchOnLabel?r.withNullAsUndefined(M.matchesFuzzyIconAware(I,M.parseLabelWithIcons(E.saneLabel))):void 0,O=this.matchOnDescription?r.withNullAsUndefined(M.matchesFuzzyIconAware(I,M.parseLabelWithIcons(E.saneDescription||""))):void 0,A=this.matchOnDetail?r.withNullAsUndefined(M.matchesFuzzyIconAware(I,M.parseLabelWithIcons(E.saneDetail||""))):void 0,B=this.matchOnMeta?r.withNullAsUndefined(M.matchesFuzzyIconAware(I,M.parseLabelWithIcons(E.saneMeta||""))):void 0;T||O||A||B?(E.labelHighlights=T,E.descriptionHighlights=O,E.detailHighlights=A,E.hidden=!1):(E.labelHighlights=void 0,E.descriptionHighlights=void 0,E.detailHighlights=void 0,E.hidden=!E.item.alwaysShow),E.separator=void 0});const k=this.elements.filter(E=>!E.hidden);if(this.sortByLabel&&I){const E=I.toLowerCase();k.sort((T,O)=>v(T,O,E))}return this.elementsToIndexes=k.reduce((E,T,O)=>(E.set(T.item,O),E),new Map),this.list.splice(0,this.list.length,k),this.list.setFocus([]),this.list.layout(),this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()),this._onChangedVisibleCount.fire(k.length),!0}toggleCheckbox(){try{this._fireCheckedEvents=!1;const I=this.list.getFocusedElements(),k=this.allVisibleChecked(I);for(const E of I)E.checked=!k}finally{this._fireCheckedEvents=!0,this.fireCheckedEvents()}}display(I){this.container.style.display=I?"":"none"}isDisplayed(){return this.container.style.display!=="none"}dispose(){this.elementDisposables=N.dispose(this.elementDisposables),this.disposables=N.dispose(this.disposables)}fireCheckedEvents(){this._fireCheckedEvents&&(this._onChangedAllVisibleChecked.fire(this.getAllVisibleChecked()),this._onChangedCheckedCount.fire(this.getCheckedCount()),this._onChangedCheckedElements.fire(this.getCheckedElements()))}fireButtonTriggered(I){this._onButtonTriggered.fire(I)}style(I){this.list.style(I)}}Me([p.memoize],f.prototype,"onDidChangeFocus",null),Me([p.memoize],f.prototype,"onDidChangeSelection",null),e.QuickInputList=f;function v(L,I,k){const E=L.labelHighlights||[],T=I.labelHighlights||[];return E.length&&!T.length?-1:!E.length&&T.length?1:E.length===0&&T.length===0?0:w.compareAnything(L.saneLabel,I.saneLabel,k)}class y{getWidgetAriaLabel(){return n.localize(0,null)}getAriaLabel(I){return I.saneAriaLabel}getWidgetRole(){return"listbox"}getRole(){return"option"}}}),define(Q[443],J([0,1,202,7,23,442,435,56,440,205,309,6,297,2,82,83,48,19,15,211,27,8,103,125]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.QuickInputController=void 0;const _=N.$,v={iconClass:l.registerCodicon("quick-input-back",l.Codicon.arrowLeft).classNames,tooltip:d.localize(0,null),handle:-1};class y extends s.Disposable{constructor(E){super();this.ui=E,this.visible=!1,this._enabled=!0,this._busy=!1,this._ignoreFocusOut=!1,this._buttons=[],this.buttonsUpdated=!1,this.onDidTriggerButtonEmitter=this._register(new c.Emitter),this.onDidHideEmitter=this._register(new c.Emitter),this.onDisposeEmitter=this._register(new c.Emitter),this.visibleDisposables=this._register(new s.DisposableStore),this.onDidHide=this.onDidHideEmitter.event}get title(){return this._title}set title(E){this._title=E,this.update()}get description(){return this._description}set description(E){this._description=E,this.update()}get step(){return this._steps}set step(E){this._steps=E,this.update()}get totalSteps(){return this._totalSteps}set totalSteps(E){this._totalSteps=E,this.update()}get enabled(){return this._enabled}set enabled(E){this._enabled=E,this.update()}get contextKey(){return this._contextKey}set contextKey(E){this._contextKey=E,this.update()}get busy(){return this._busy}set busy(E){this._busy=E,this.update()}get ignoreFocusOut(){return this._ignoreFocusOut}set ignoreFocusOut(E){this._ignoreFocusOut=E,this.update()}get buttons(){return this._buttons}set buttons(E){this._buttons=E,this.buttonsUpdated=!0,this.update()}show(){this.visible||(this.visibleDisposables.add(this.ui.onDidTriggerButton(E=>{this.buttons.indexOf(E)!==-1&&this.onDidTriggerButtonEmitter.fire(E)})),this.ui.show(this),this.visible=!0,this.update())}hide(){!this.visible||this.ui.hide()}didHide(){this.visible=!1,this.visibleDisposables.clear(),this.onDidHideEmitter.fire()}update(){if(!!this.visible){const E=this.getTitle();E&&this.ui.title.textContent!==E?this.ui.title.textContent=E:!E&&this.ui.title.innerHTML!==" "&&(this.ui.title.innerText="\xA0;");const T=this.getDescription();if(this.ui.description1.textContent!==T&&(this.ui.description1.textContent=T),this.ui.description2.textContent!==T&&(this.ui.description2.textContent=T),this.busy&&!this.busyDelay&&(this.busyDelay=new n.TimeoutTimer,this.busyDelay.setIfNotSet(()=>{this.visible&&this.ui.progressBar.infinite()},800)),!this.busy&&this.busyDelay&&(this.ui.progressBar.stop(),this.busyDelay.cancel(),this.busyDelay=void 0),this.buttonsUpdated){this.buttonsUpdated=!1,this.ui.leftActionBar.clear();const O=this.buttons.filter(B=>B===v);this.ui.leftActionBar.push(O.map((B,F)=>{const D=new r.Action(`id-${F}`,"",B.iconClass||t.getIconClass(B.iconPath),!0,()=>Ie(this,void 0,void 0,function*(){this.onDidTriggerButtonEmitter.fire(B)}));return D.tooltip=B.tooltip||"",D}),{icon:!0,label:!1}),this.ui.rightActionBar.clear();const A=this.buttons.filter(B=>B!==v);this.ui.rightActionBar.push(A.map((B,F)=>{const D=new r.Action(`id-${F}`,"",B.iconClass||t.getIconClass(B.iconPath),!0,()=>Ie(this,void 0,void 0,function*(){this.onDidTriggerButtonEmitter.fire(B)}));return D.tooltip=B.tooltip||"",D}),{icon:!0,label:!1})}this.ui.ignoreFocusOut=this.ignoreFocusOut,this.ui.setEnabled(this.enabled),this.ui.setContextKey(this.contextKey)}}getTitle(){return this.title&&this.step?`${this.title} (${this.getSteps()})`:this.title?this.title:this.step?this.getSteps():""}getDescription(){return this.description||""}getSteps(){return this.step&&this.totalSteps?d.localize(1,null,this.step,this.totalSteps):this.step?String(this.step):""}showMessageDecoration(E){if(this.ui.inputBox.showDecoration(E),E===a.default.Error){const T=this.ui.inputBox.stylesForType(E);this.ui.message.style.color=T.foreground?`${T.foreground}`:"",this.ui.message.style.backgroundColor=T.background?`${T.background}`:"",this.ui.message.style.border=T.border?`1px solid ${T.border}`:"",this.ui.message.style.paddingBottom="4px"}else this.ui.message.style.color="",this.ui.message.style.backgroundColor="",this.ui.message.style.border="",this.ui.message.style.paddingBottom=""}dispose(){this.hide(),this.onDisposeEmitter.fire(),super.dispose()}}class L extends y{constructor(){super(...arguments);this._value="",this.onDidChangeValueEmitter=this._register(new c.Emitter),this.onDidAcceptEmitter=this._register(new c.Emitter),this.onDidCustomEmitter=this._register(new c.Emitter),this._items=[],this.itemsUpdated=!1,this._canSelectMany=!1,this._canAcceptInBackground=!1,this._matchOnDescription=!1,this._matchOnDetail=!1,this._matchOnLabel=!0,this._sortByLabel=!0,this._autoFocusOnList=!0,this._itemActivation=this.ui.isScreenReaderOptimized()?b.ItemActivation.NONE:b.ItemActivation.FIRST,this._activeItems=[],this.activeItemsUpdated=!1,this.activeItemsToConfirm=[],this.onDidChangeActiveEmitter=this._register(new c.Emitter),this._selectedItems=[],this.selectedItemsUpdated=!1,this.selectedItemsToConfirm=[],this.onDidChangeSelectionEmitter=this._register(new c.Emitter),this.onDidTriggerItemButtonEmitter=this._register(new c.Emitter),this.valueSelectionUpdated=!0,this._ok="default",this._customButton=!1,this.filterValue=E=>E,this.onDidChangeValue=this.onDidChangeValueEmitter.event,this.onDidAccept=this.onDidAcceptEmitter.event,this.onDidChangeActive=this.onDidChangeActiveEmitter.event,this.onDidChangeSelection=this.onDidChangeSelectionEmitter.event,this.onDidTriggerItemButton=this.onDidTriggerItemButtonEmitter.event}get quickNavigate(){return this._quickNavigate}set quickNavigate(E){this._quickNavigate=E,this.update()}get value(){return this._value}set value(E){this._value=E||"",this.update()}set ariaLabel(E){this._ariaLabel=E,this.update()}get ariaLabel(){return this._ariaLabel}get placeholder(){return this._placeholder}set placeholder(E){this._placeholder=E,this.update()}get items(){return this._items}set items(E){this._items=E,this.itemsUpdated=!0,this.update()}get canSelectMany(){return this._canSelectMany}set canSelectMany(E){this._canSelectMany=E,this.update()}get canAcceptInBackground(){return this._canAcceptInBackground}set canAcceptInBackground(E){this._canAcceptInBackground=E}get matchOnDescription(){return this._matchOnDescription}set matchOnDescription(E){this._matchOnDescription=E,this.update()}get matchOnDetail(){return this._matchOnDetail}set matchOnDetail(E){this._matchOnDetail=E,this.update()}get matchOnLabel(){return this._matchOnLabel}set matchOnLabel(E){this._matchOnLabel=E,this.update()}get sortByLabel(){return this._sortByLabel}set sortByLabel(E){this._sortByLabel=E,this.update()}get autoFocusOnList(){return this._autoFocusOnList}set autoFocusOnList(E){this._autoFocusOnList=E,this.update()}get itemActivation(){return this._itemActivation}set itemActivation(E){this._itemActivation=E}get activeItems(){return this._activeItems}set activeItems(E){this._activeItems=E,this.activeItemsUpdated=!0,this.update()}get selectedItems(){return this._selectedItems}set selectedItems(E){this._selectedItems=E,this.selectedItemsUpdated=!0,this.update()}get keyMods(){return this._quickNavigate?b.NO_KEY_MODS:this.ui.keyMods}set valueSelection(E){this._valueSelection=E,this.valueSelectionUpdated=!0,this.update()}get validationMessage(){return this._validationMessage}set validationMessage(E){this._validationMessage=E,this.update()}get customButton(){return this._customButton}set customButton(E){this._customButton=E,this.update()}get customLabel(){return this._customButtonLabel}set customLabel(E){this._customButtonLabel=E,this.update()}get customHover(){return this._customButtonHover}set customHover(E){this._customButtonHover=E,this.update()}get ok(){return this._ok}set ok(E){this._ok=E,this.update()}get hideInput(){return!!this._hideInput}set hideInput(E){this._hideInput=E,this.update()}trySelectFirst(){this.autoFocusOnList&&(this.canSelectMany||this.ui.list.focus(w.QuickInputListFocus.First))}show(){this.visible||(this.visibleDisposables.add(this.ui.inputBox.onDidChange(E=>{E!==this.value&&(this._value=E,this.ui.list.filter(this.filterValue(this.ui.inputBox.value))&&this.trySelectFirst(),this.onDidChangeValueEmitter.fire(E))})),this.visibleDisposables.add(this.ui.inputBox.onMouseDown(E=>{this.autoFocusOnList||this.ui.list.clearFocus()})),this.visibleDisposables.add((this._hideInput?this.ui.list:this.ui.inputBox).onKeyDown(E=>{switch(E.keyCode){case 18:this.ui.list.focus(w.QuickInputListFocus.Next),this.canSelectMany&&this.ui.list.domFocus(),N.EventHelper.stop(E,!0);break;case 16:this.ui.list.getFocusedElements().length?this.ui.list.focus(w.QuickInputListFocus.Previous):this.ui.list.focus(w.QuickInputListFocus.Last),this.canSelectMany&&this.ui.list.domFocus(),N.EventHelper.stop(E,!0);break;case 12:this.ui.list.focus(w.QuickInputListFocus.NextPage),this.canSelectMany&&this.ui.list.domFocus(),N.EventHelper.stop(E,!0);break;case 11:this.ui.list.focus(w.QuickInputListFocus.PreviousPage),this.canSelectMany&&this.ui.list.domFocus(),N.EventHelper.stop(E,!0);break;case 17:if(!this._canAcceptInBackground||!this.ui.inputBox.isSelectionAtEnd())return;this.activeItems[0]&&(this._selectedItems=[this.activeItems[0]],this.onDidChangeSelectionEmitter.fire(this.selectedItems),this.onDidAcceptEmitter.fire({inBackground:!0}));break;case 14:(E.ctrlKey||E.metaKey)&&!E.shiftKey&&!E.altKey&&(this.ui.list.focus(w.QuickInputListFocus.First),N.EventHelper.stop(E,!0));break;case 13:(E.ctrlKey||E.metaKey)&&!E.shiftKey&&!E.altKey&&(this.ui.list.focus(w.QuickInputListFocus.Last),N.EventHelper.stop(E,!0));break}})),this.visibleDisposables.add(this.ui.onDidAccept(()=>{!this.canSelectMany&&this.activeItems[0]&&(this._selectedItems=[this.activeItems[0]],this.onDidChangeSelectionEmitter.fire(this.selectedItems)),this.onDidAcceptEmitter.fire({inBackground:!1})})),this.visibleDisposables.add(this.ui.onDidCustom(()=>{this.onDidCustomEmitter.fire()})),this.visibleDisposables.add(this.ui.list.onDidChangeFocus(E=>{this.activeItemsUpdated||this.activeItemsToConfirm!==this._activeItems&&i.equals(E,this._activeItems,(T,O)=>T===O)||(this._activeItems=E,this.onDidChangeActiveEmitter.fire(E))})),this.visibleDisposables.add(this.ui.list.onDidChangeSelection(({items:E,event:T})=>{if(this.canSelectMany){E.length&&this.ui.list.setSelectedElements([]);return}this.selectedItemsToConfirm!==this._selectedItems&&i.equals(E,this._selectedItems,(O,A)=>O===A)||(this._selectedItems=E,this.onDidChangeSelectionEmitter.fire(E),E.length&&this.onDidAcceptEmitter.fire({inBackground:T instanceof MouseEvent&&T.button===1}))})),this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(E=>{!this.canSelectMany||this.selectedItemsToConfirm!==this._selectedItems&&i.equals(E,this._selectedItems,(T,O)=>T===O)||(this._selectedItems=E,this.onDidChangeSelectionEmitter.fire(E))})),this.visibleDisposables.add(this.ui.list.onButtonTriggered(E=>this.onDidTriggerItemButtonEmitter.fire(E))),this.visibleDisposables.add(this.registerQuickNavigation()),this.valueSelectionUpdated=!0),super.show()}registerQuickNavigation(){return N.addDisposableListener(this.ui.container,N.EventType.KEY_UP,E=>{if(!(this.canSelectMany||!this._quickNavigate)){const T=new C.StandardKeyboardEvent(E),O=T.keyCode;this._quickNavigate.keybindings.some(F=>{const[D,R]=F.getParts();return R?!1:D.shiftKey&&O===4?!(T.ctrlKey||T.altKey||T.metaKey):!!(D.altKey&&O===6||D.ctrlKey&&O===5||D.metaKey&&O===57)})&&(this.activeItems[0]&&(this._selectedItems=[this.activeItems[0]],this.onDidChangeSelectionEmitter.fire(this.selectedItems),this.onDidAcceptEmitter.fire({inBackground:!1})),this._quickNavigate=void 0)}})}update(){if(!!this.visible){let E=!1,T=!1;!!this._hideInput&&this._items.length>0&&(this.ui.isScreenReaderOptimized()?T=!0:E=!0),this.ui.container.classList.toggle("hidden-input",E&&!this.description);const O={title:!!this.title||!!this.step||!!this.buttons.length,description:!!this.description,checkAll:this.canSelectMany&&!this._hideCheckAll,checkBox:this.canSelectMany,inputBox:!E,progressBar:!E,visibleCount:!0,count:this.canSelectMany,ok:this.ok==="default"?this.canSelectMany:this.ok,list:!0,message:!!this.validationMessage,customButton:this.customButton};if(this.ui.setVisibilities(O),super.update(),this.ui.inputBox.value!==this.value&&(this.ui.inputBox.value=this.value),this.valueSelectionUpdated&&(this.valueSelectionUpdated=!1,this.ui.inputBox.select(this._valueSelection&&{start:this._valueSelection[0],end:this._valueSelection[1]})),this.ui.inputBox.placeholder!==(this.placeholder||"")&&(this.ui.inputBox.placeholder=this.placeholder||""),T)this.ui.inputBox.ariaLabel="";else{const B=this.ariaLabel||this.placeholder||L.DEFAULT_ARIA_LABEL;this.ui.inputBox.ariaLabel!==B&&(this.ui.inputBox.ariaLabel=B)}if(this.ui.list.matchOnDescription=this.matchOnDescription,this.ui.list.matchOnDetail=this.matchOnDetail,this.ui.list.matchOnLabel=this.matchOnLabel,this.ui.list.sortByLabel=this.sortByLabel,this.itemsUpdated)switch(this.itemsUpdated=!1,this.ui.list.setElements(this.items),this.ui.list.filter(this.filterValue(this.ui.inputBox.value)),this.ui.checkAll.checked=this.ui.list.getAllVisibleChecked(),this.ui.visibleCount.setCount(this.ui.list.getVisibleCount()),this.ui.count.setCount(this.ui.list.getCheckedCount()),this._itemActivation){case b.ItemActivation.NONE:this._itemActivation=b.ItemActivation.FIRST;break;case b.ItemActivation.SECOND:this.ui.list.focus(w.QuickInputListFocus.Second),this._itemActivation=b.ItemActivation.FIRST;break;case b.ItemActivation.LAST:this.ui.list.focus(w.QuickInputListFocus.Last),this._itemActivation=b.ItemActivation.FIRST;break;default:this.trySelectFirst();break}this.ui.container.classList.contains("show-checkboxes")!==!!this.canSelectMany&&(this.canSelectMany?this.ui.list.clearFocus():this.trySelectFirst()),this.activeItemsUpdated&&(this.activeItemsUpdated=!1,this.activeItemsToConfirm=this._activeItems,this.ui.list.setFocusedElements(this.activeItems),this.activeItemsToConfirm===this._activeItems&&(this.activeItemsToConfirm=null)),this.selectedItemsUpdated&&(this.selectedItemsUpdated=!1,this.selectedItemsToConfirm=this._selectedItems,this.canSelectMany?this.ui.list.setCheckedElements(this.selectedItems):this.ui.list.setSelectedElements(this.selectedItems),this.selectedItemsToConfirm===this._selectedItems&&(this.selectedItemsToConfirm=null));const A=this.validationMessage||"";this._lastValidationMessage!==A&&(this._lastValidationMessage=A,N.reset(this.ui.message,...m.renderLabelWithIcons(h.escape(A))),this.showMessageDecoration(this.validationMessage?a.default.Error:a.default.Ignore)),this.ui.customButton.label=this.customLabel||"",this.ui.customButton.element.title=this.customHover||"",this.ui.setComboboxAccessibility(!0),O.inputBox||(this.ui.list.domFocus(),this.canSelectMany&&this.ui.list.focus(w.QuickInputListFocus.First))}}}L.DEFAULT_ARIA_LABEL=d.localize(2,null);class I extends s.Disposable{constructor(E){super();this.options=E,this.comboboxAccessibility=!1,this.enabled=!0,this.onDidAcceptEmitter=this._register(new c.Emitter),this.onDidCustomEmitter=this._register(new c.Emitter),this.onDidTriggerButtonEmitter=this._register(new c.Emitter),this.keyMods={ctrlCmd:!1,alt:!1},this.controller=null,this.onShowEmitter=this._register(new c.Emitter),this.onShow=this.onShowEmitter.event,this.onHideEmitter=this._register(new c.Emitter),this.onHide=this.onHideEmitter.event,this.idPrefix=E.idPrefix,this.parentElement=E.container,this.styles=E.styles,this.registerKeyModsListeners()}registerKeyModsListeners(){const E=T=>{this.keyMods.ctrlCmd=T.ctrlKey||T.metaKey,this.keyMods.alt=T.altKey};this._register(N.addDisposableListener(window,N.EventType.KEY_DOWN,E,!0)),this._register(N.addDisposableListener(window,N.EventType.KEY_UP,E,!0)),this._register(N.addDisposableListener(window,N.EventType.MOUSE_DOWN,E,!0))}getUI(){if(this.ui)return this.ui;const E=N.append(this.parentElement,_(".quick-input-widget.show-file-icons"));E.tabIndex=-1,E.style.display="none";const T=N.createStyleSheet(E),O=N.append(E,_(".quick-input-titlebar")),A=this._register(new u.ActionBar(O));A.domNode.classList.add("quick-input-left-action-bar");const B=N.append(O,_(".quick-input-title")),F=this._register(new u.ActionBar(O));F.domNode.classList.add("quick-input-right-action-bar");const D=N.append(E,_(".quick-input-description")),R=N.append(E,_(".quick-input-header")),W=N.append(R,_("input.quick-input-check-all"));W.type="checkbox",this._register(N.addStandardDisposableListener(W,N.EventType.CHANGE,ae=>{const G=W.checked;ie.setAllVisibleChecked(G)})),this._register(N.addDisposableListener(W,N.EventType.CLICK,ae=>{(ae.x||ae.y)&&ee.setFocus()}));const x=N.append(R,_(".quick-input-description")),K=N.append(R,_(".quick-input-and-message")),Y=N.append(K,_(".quick-input-filter")),ee=this._register(new S.QuickInputBox(Y));ee.setAttribute("aria-describedby",`${this.idPrefix}message`);const se=N.append(Y,_(".quick-input-visible-count"));se.setAttribute("aria-live","polite"),se.setAttribute("aria-atomic","true");const ne=new g.CountBadge(se,{countFormat:d.localize(3,null)}),le=N.append(Y,_(".quick-input-count"));le.setAttribute("aria-live","polite");const X=new g.CountBadge(le,{countFormat:d.localize(4,null)}),z=N.append(R,_(".quick-input-action")),P=new o.Button(z);P.label=d.localize(5,null),this._register(P.onDidClick(ae=>{this.onDidAcceptEmitter.fire()}));const V=N.append(R,_(".quick-input-action")),U=new o.Button(V);U.label=d.localize(6,null),this._register(U.onDidClick(ae=>{this.onDidCustomEmitter.fire()}));const H=N.append(K,_(`#${this.idPrefix}message.quick-input-message`)),$=new p.ProgressBar(E);$.getContainer().classList.add("quick-input-progress");const ie=this._register(new w.QuickInputList(E,this.idPrefix+"list",this.options));this._register(ie.onChangedAllVisibleChecked(ae=>{W.checked=ae})),this._register(ie.onChangedVisibleCount(ae=>{ne.setCount(ae)})),this._register(ie.onChangedCheckedCount(ae=>{X.setCount(ae)})),this._register(ie.onLeave(()=>{setTimeout(()=>{ee.setFocus(),this.controller instanceof L&&this.controller.canSelectMany&&ie.clearFocus()},0)})),this._register(ie.onDidChangeFocus(()=>{this.comboboxAccessibility&&this.getUI().inputBox.setAttribute("aria-activedescendant",this.getUI().list.getActiveDescendant()||"")}));const oe=N.trackFocus(E);return this._register(oe),this._register(N.addDisposableListener(E,N.EventType.FOCUS,ae=>{this.previousFocusElement=ae.relatedTarget instanceof HTMLElement?ae.relatedTarget:void 0},!0)),this._register(oe.onDidBlur(()=>{!this.getUI().ignoreFocusOut&&!this.options.ignoreFocusOut()&&this.hide(),this.previousFocusElement=void 0})),this._register(N.addDisposableListener(E,N.EventType.FOCUS,ae=>{ee.setFocus()})),this._register(N.addDisposableListener(E,N.EventType.KEY_DOWN,ae=>{const G=new C.StandardKeyboardEvent(ae);switch(G.keyCode){case 3:N.EventHelper.stop(ae,!0),this.onDidAcceptEmitter.fire();break;case 9:N.EventHelper.stop(ae,!0),this.hide();break;case 2:if(!G.altKey&&!G.ctrlKey&&!G.metaKey){const j=[".action-label.codicon"];E.classList.contains("show-checkboxes")?j.push("input"):j.push("input[type=text]"),this.getUI().list.isDisplayed()&&j.push(".monaco-list");const te=E.querySelectorAll(j.join(", "));G.shiftKey&&G.target===te[0]?(N.EventHelper.stop(ae,!0),te[te.length-1].focus()):!G.shiftKey&&G.target===te[te.length-1]&&(N.EventHelper.stop(ae,!0),te[0].focus())}break}})),this.ui={container:E,styleSheet:T,leftActionBar:A,titleBar:O,title:B,description1:D,description2:x,rightActionBar:F,checkAll:W,filterContainer:Y,inputBox:ee,visibleCountContainer:se,visibleCount:ne,countContainer:le,count:X,okContainer:z,ok:P,message:H,customButtonContainer:V,customButton:U,progressBar:$,list:ie,onDidAccept:this.onDidAcceptEmitter.event,onDidCustom:this.onDidCustomEmitter.event,onDidTriggerButton:this.onDidTriggerButtonEmitter.event,ignoreFocusOut:!1,keyMods:this.keyMods,isScreenReaderOptimized:()=>this.options.isScreenReaderOptimized(),show:ae=>this.show(ae),hide:()=>this.hide(),setVisibilities:ae=>this.setVisibilities(ae),setComboboxAccessibility:ae=>this.setComboboxAccessibility(ae),setEnabled:ae=>this.setEnabled(ae),setContextKey:ae=>this.options.setContextKey(ae)},this.updateStyles(),this.ui}pick(E,T={},O=M.CancellationToken.None){return new Promise((A,B)=>{let F=x=>{F=A,T.onKeyMods&&T.onKeyMods(D.keyMods),A(x)};if(O.isCancellationRequested){F(void 0);return}const D=this.createQuickPick();let R;const W=[D,D.onDidAccept(()=>{if(D.canSelectMany)F(D.selectedItems.slice()),D.hide();else{const x=D.activeItems[0];x&&(F(x),D.hide())}}),D.onDidChangeActive(x=>{const K=x[0];K&&T.onDidFocus&&T.onDidFocus(K)}),D.onDidChangeSelection(x=>{if(!D.canSelectMany){const K=x[0];K&&(F(K),D.hide())}}),D.onDidTriggerItemButton(x=>T.onDidTriggerItemButton&&T.onDidTriggerItemButton(Object.assign(Object.assign({},x),{removeItem:()=>{const K=D.items.indexOf(x.item);if(K!==-1){const Y=D.items.slice();Y.splice(K,1),D.items=Y}}}))),D.onDidChangeValue(x=>{R&&!x&&(D.activeItems.length!==1||D.activeItems[0]!==R)&&(D.activeItems=[R])}),O.onCancellationRequested(()=>{D.hide()}),D.onDidHide(()=>{s.dispose(W),F(void 0)})];D.canSelectMany=!!T.canPickMany,D.placeholder=T.placeHolder,D.ignoreFocusOut=!!T.ignoreFocusLost,D.matchOnDescription=!!T.matchOnDescription,D.matchOnDetail=!!T.matchOnDetail,D.matchOnLabel=T.matchOnLabel===void 0||T.matchOnLabel,D.autoFocusOnList=T.autoFocusOnList===void 0||T.autoFocusOnList,D.quickNavigate=T.quickNavigate,D.contextKey=T.contextKey,D.busy=!0,Promise.all([E,T.activeItem]).then(([x,K])=>{R=K,D.busy=!1,D.items=x,D.canSelectMany&&(D.selectedItems=x.filter(Y=>Y.type!=="separator"&&Y.picked)),R&&(D.activeItems=[R])}),D.show(),Promise.resolve(E).then(void 0,x=>{B(x),D.hide()})})}createQuickPick(){const E=this.getUI();return new L(E)}show(E){const T=this.getUI();this.onShowEmitter.fire();const O=this.controller;this.controller=E,O&&O.didHide(),this.setEnabled(!0),T.leftActionBar.clear(),T.title.textContent="",T.description1.textContent="",T.description2.textContent="",T.rightActionBar.clear(),T.checkAll.checked=!1,T.inputBox.placeholder="",T.inputBox.password=!1,T.inputBox.showDecoration(a.default.Ignore),T.visibleCount.setCount(0),T.count.setCount(0),N.reset(T.message),T.progressBar.stop(),T.list.setElements([]),T.list.matchOnDescription=!1,T.list.matchOnDetail=!1,T.list.matchOnLabel=!0,T.list.sortByLabel=!0,T.ignoreFocusOut=!1,this.setComboboxAccessibility(!1),T.inputBox.ariaLabel="";const A=this.options.backKeybindingLabel();v.tooltip=A?d.localize(7,null,A):d.localize(8,null),T.container.style.display="",this.updateLayout(),T.inputBox.setFocus()}setVisibilities(E){const T=this.getUI();T.title.style.display=E.title?"":"none",T.description1.style.display=E.description&&(E.inputBox||E.checkAll)?"":"none",T.description2.style.display=E.description&&!(E.inputBox||E.checkAll)?"":"none",T.checkAll.style.display=E.checkAll?"":"none",T.filterContainer.style.display=E.inputBox?"":"none",T.visibleCountContainer.style.display=E.visibleCount?"":"none",T.countContainer.style.display=E.count?"":"none",T.okContainer.style.display=E.ok?"":"none",T.customButtonContainer.style.display=E.customButton?"":"none",T.message.style.display=E.message?"":"none",T.progressBar.getContainer().style.display=E.progressBar?"":"none",T.list.display(!!E.list),T.container.classList[E.checkBox?"add":"remove"]("show-checkboxes"),this.updateLayout()}setComboboxAccessibility(E){if(E!==this.comboboxAccessibility){const T=this.getUI();this.comboboxAccessibility=E,this.comboboxAccessibility?(T.inputBox.setAttribute("role","combobox"),T.inputBox.setAttribute("aria-haspopup","true"),T.inputBox.setAttribute("aria-autocomplete","list"),T.inputBox.setAttribute("aria-activedescendant",T.list.getActiveDescendant()||"")):(T.inputBox.removeAttribute("role"),T.inputBox.removeAttribute("aria-haspopup"),T.inputBox.removeAttribute("aria-autocomplete"),T.inputBox.removeAttribute("aria-activedescendant"))}}setEnabled(E){if(E!==this.enabled){this.enabled=E;for(const T of this.getUI().leftActionBar.viewItems)T.getAction().enabled=E;for(const T of this.getUI().rightActionBar.viewItems)T.getAction().enabled=E;this.getUI().checkAll.disabled=!E,this.getUI().ok.enabled=E,this.getUI().list.enabled=E}}hide(){var E;const T=this.controller;if(T){const O=!((E=this.ui)===null||E===void 0?void 0:E.container.contains(document.activeElement));this.controller=null,this.onHideEmitter.fire(),this.getUI().container.style.display="none",O||(this.previousFocusElement&&this.previousFocusElement.offsetParent?(this.previousFocusElement.focus(),this.previousFocusElement=void 0):this.options.returnFocus()),T.didHide()}}layout(E,T){this.dimension=E,this.titleBarOffset=T,this.updateLayout()}updateLayout(){if(this.ui){this.ui.container.style.top=`${this.titleBarOffset}px`;const E=this.ui.container.style,T=Math.min(this.dimension.width*.62,I.MAX_WIDTH);E.width=T+"px",E.marginLeft="-"+T/2+"px",this.ui.inputBox.layout(),this.ui.list.layout(this.dimension&&this.dimension.height*.4)}}applyStyles(E){this.styles=E,this.updateStyles()}updateStyles(){if(this.ui){const{quickInputTitleBackground:E,quickInputBackground:T,quickInputForeground:O,contrastBorder:A,widgetShadow:B}=this.styles.widget;this.ui.titleBar.style.backgroundColor=E?E.toString():"",this.ui.container.style.backgroundColor=T?T.toString():"",this.ui.container.style.color=O?O.toString():"",this.ui.container.style.border=A?`1px solid ${A}`:"",this.ui.container.style.boxShadow=B?`0 0 8px 2px ${B}`:"",this.ui.inputBox.style(this.styles.inputBox),this.ui.count.style(this.styles.countBadge),this.ui.ok.style(this.styles.button),this.ui.customButton.style(this.styles.button),this.ui.progressBar.style(this.styles.progressBar),this.ui.list.style(this.styles.list);const F=[];this.styles.list.pickerGroupBorder&&F.push(`.quick-input-list .quick-input-list-entry { border-top-color: ${this.styles.list.pickerGroupBorder}; }`),this.styles.list.pickerGroupForeground&&F.push(`.quick-input-list .quick-input-list-separator { color: ${this.styles.list.pickerGroupForeground}; }`);const D=F.join(` +`);D!==this.ui.styleSheet.textContent&&(this.ui.styleSheet.textContent=D)}}}e.QuickInputController=I,I.MAX_WIDTH=600}),define(Q[444],J([4,5]),function(q,e){return q.create("vs/editor/browser/controller/coreCommands",e)}),define(Q[445],J([4,5]),function(q,e){return q.create("vs/editor/browser/controller/textAreaHandler",e)}),define(Q[446],J([4,5]),function(q,e){return q.create("vs/editor/browser/core/keybindingCancellation",e)}),define(Q[447],J([4,5]),function(q,e){return q.create("vs/editor/browser/editorExtensions",e)}),define(Q[448],J([4,5]),function(q,e){return q.create("vs/editor/browser/widget/codeEditorWidget",e)}),define(Q[449],J([4,5]),function(q,e){return q.create("vs/editor/browser/widget/diffEditorWidget",e)}),define(Q[450],J([4,5]),function(q,e){return q.create("vs/editor/browser/widget/diffReview",e)}),define(Q[451],J([4,5]),function(q,e){return q.create("vs/editor/browser/widget/inlineDiffMargin",e)}),define(Q[452],J([0,1,451,7,48,2,3,27]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InlineDiffMargin=void 0;class d extends w.Disposable{constructor(p,c,o,s,a,u){super();this._viewZoneId=p,this._marginDomNode=c,this.editor=o,this.diff=s,this._contextMenuService=a,this._clipboardService=u,this._visibility=!1,this._marginDomNode.style.zIndex="10",this._diffActions=document.createElement("div"),this._diffActions.className=C.Codicon.lightBulb.classNames+" lightbulb-glyph",this._diffActions.style.position="absolute";const r=o.getOption(53),i=o.getModel().getEOL();this._diffActions.style.right="0px",this._diffActions.style.visibility="hidden",this._diffActions.style.height=`${r}px`,this._diffActions.style.lineHeight=`${r}px`,this._marginDomNode.appendChild(this._diffActions);const n=[];n.push(new M.Action("diff.clipboard.copyDeletedContent",s.originalEndLineNumber>s.modifiedStartLineNumber?b.localize(0,null):b.localize(1,null),void 0,!0,()=>Ie(this,void 0,void 0,function*(){const _=new S.Range(s.originalStartLineNumber,1,s.originalEndLineNumber+1,1),f=s.originalModel.getValueInRange(_);yield this._clipboardService.writeText(f)})));let t=0,l;s.originalEndLineNumber>s.modifiedStartLineNumber&&(l=new M.Action("diff.clipboard.copyDeletedLineContent",b.localize(2,null,s.originalStartLineNumber),void 0,!0,()=>Ie(this,void 0,void 0,function*(){const _=s.originalModel.getLineContent(s.originalStartLineNumber+t);yield this._clipboardService.writeText(_)})),n.push(l)),o.getOption(75)||n.push(new M.Action("diff.inline.revertChange",b.localize(3,null),void 0,!0,()=>Ie(this,void 0,void 0,function*(){const _=new S.Range(s.originalStartLineNumber,1,s.originalEndLineNumber,s.originalModel.getLineMaxColumn(s.originalEndLineNumber)),f=s.originalModel.getValueInRange(_);if(s.modifiedEndLineNumber===0){const v=o.getModel().getLineMaxColumn(s.modifiedStartLineNumber);o.executeEdits("diffEditor",[{range:new S.Range(s.modifiedStartLineNumber,v,s.modifiedStartLineNumber,v),text:i+f}])}else{const v=o.getModel().getLineMaxColumn(s.modifiedEndLineNumber);o.executeEdits("diffEditor",[{range:new S.Range(s.modifiedStartLineNumber,1,s.modifiedEndLineNumber,v),text:f}])}})));const m=(_,f)=>{this._contextMenuService.showContextMenu({getAnchor:()=>({x:_,y:f}),getActions:()=>(l&&(l.label=b.localize(4,null,s.originalStartLineNumber+t)),n),autoSelectFirstItem:!0})};this._register(N.addStandardDisposableListener(this._diffActions,"mousedown",_=>{const{top:f,height:v}=N.getDomNodePagePosition(this._diffActions);let y=Math.floor(r/3);_.preventDefault(),m(_.posx,f+v+y)})),this._register(o.onMouseMove(_=>{_.target.type===8||_.target.type===5?_.target.detail.viewZoneId===this._viewZoneId?(this.visibility=!0,t=this._updateLightBulbPosition(this._marginDomNode,_.event.browserEvent.y,r)):this.visibility=!1:this.visibility=!1})),this._register(o.onMouseDown(_=>{!_.event.rightButton||(_.target.type===8||_.target.type===5)&&_.target.detail.viewZoneId===this._viewZoneId&&(_.event.preventDefault(),t=this._updateLightBulbPosition(this._marginDomNode,_.event.browserEvent.y,r),m(_.event.posx,_.event.posy+r))}))}get visibility(){return this._visibility}set visibility(p){this._visibility!==p&&(this._visibility=p,p?this._diffActions.style.visibility="visible":this._diffActions.style.visibility="hidden")}_updateLightBulbPosition(p,c,o){const{top:s}=N.getDomNodePagePosition(p),a=c-s,u=Math.floor(a/o),r=u*o;if(this._diffActions.style.top=`${r}px`,this.diff.viewLineCounts){let i=0;for(let n=0;nhe?he:Z}static float(Z,ue){if(typeof Z=="number")return Z;if(typeof Z=="undefined")return ue;const he=parseFloat(Z);return isNaN(he)?ue:he}validate(Z){return this.validationFn(a.float(Z,this.defaultValue))}}class u extends p{static string(Z,ue){return typeof Z!="string"?ue:Z}constructor(Z,ue,he,re=void 0){typeof re!="undefined"&&(re.type="string",re.default=he),super(Z,ue,he,re)}validate(Z){return u.string(Z,this.defaultValue)}}function r(te,Z,ue){return typeof te!="string"||ue.indexOf(te)===-1?Z:te}e.stringSet=r;class i extends p{constructor(Z,ue,he,re,ce=void 0){typeof ce!="undefined"&&(ce.type="string",ce.enum=re,ce.default=he),super(Z,ue,he,ce),this._allowedValues=re}validate(Z){return r(Z,this.defaultValue,this._allowedValues)}}class n extends d{constructor(Z,ue,he,re,ce,me,Ce=void 0){typeof Ce!="undefined"&&(Ce.type="string",Ce.enum=ce,Ce.default=re),super(Z,ue,he,Ce),this._allowedValues=ce,this._convert=me}validate(Z){return typeof Z!="string"?this.defaultValue:this._allowedValues.indexOf(Z)===-1?this.defaultValue:this._convert(Z)}}function t(te){switch(te){case"none":return 0;case"keep":return 1;case"brackets":return 2;case"advanced":return 3;case"full":return 4}}class l extends d{constructor(){super(2,"accessibilitySupport",0,{type:"string",enum:["auto","on","off"],enumDescriptions:[b.localize(0,null),b.localize(1,null),b.localize(2,null)],default:"auto",description:b.localize(3,null)})}validate(Z){switch(Z){case"auto":return 0;case"off":return 1;case"on":return 2}return this.defaultValue}compute(Z,ue,he){return he===0?Z.accessibilitySupport:he}}class h extends d{constructor(){const Z={insertSpace:!0,ignoreEmptyLines:!0};super(16,"comments",Z,{"editor.comments.insertSpace":{type:"boolean",default:Z.insertSpace,description:b.localize(4,null)},"editor.comments.ignoreEmptyLines":{type:"boolean",default:Z.ignoreEmptyLines,description:b.localize(5,null)}})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z;return{insertSpace:c(ue.insertSpace,this.defaultValue.insertSpace),ignoreEmptyLines:c(ue.ignoreEmptyLines,this.defaultValue.ignoreEmptyLines)}}}function m(te){switch(te){case"blink":return 1;case"smooth":return 2;case"phase":return 3;case"expand":return 4;case"solid":return 5}}var _;(function(te){te[te.Line=1]="Line",te[te.Block=2]="Block",te[te.Underline=3]="Underline",te[te.LineThin=4]="LineThin",te[te.BlockOutline=5]="BlockOutline",te[te.UnderlineThin=6]="UnderlineThin"})(_=e.TextEditorCursorStyle||(e.TextEditorCursorStyle={}));function f(te){switch(te){case"line":return _.Line;case"block":return _.Block;case"underline":return _.Underline;case"line-thin":return _.LineThin;case"block-outline":return _.BlockOutline;case"underline-thin":return _.UnderlineThin}}class v extends g{constructor(){super(121,[60,29])}compute(Z,ue,he){const re=["monaco-editor"];return ue.get(29)&&re.push(ue.get(29)),Z.extraEditorClassName&&re.push(Z.extraEditorClassName),ue.get(60)==="default"?re.push("mouse-default"):ue.get(60)==="copy"&&re.push("mouse-copy"),ue.get(95)&&re.push("showUnused"),ue.get(119)&&re.push("showDeprecated"),re.join(" ")}}class y extends o{constructor(){super(28,"emptySelectionClipboard",!0,{description:b.localize(6,null)})}compute(Z,ue,he){return he&&Z.emptySelectionClipboard}}class L extends d{constructor(){const Z={cursorMoveOnType:!0,seedSearchStringFromSelection:!0,autoFindInSelection:"never",globalFindClipboard:!1,addExtraSpaceOnTop:!0,loop:!0};super(31,"find",Z,{"editor.find.cursorMoveOnType":{type:"boolean",default:Z.cursorMoveOnType,description:b.localize(7,null)},"editor.find.seedSearchStringFromSelection":{type:"boolean",default:Z.seedSearchStringFromSelection,description:b.localize(8,null)},"editor.find.autoFindInSelection":{type:"string",enum:["never","always","multiline"],default:Z.autoFindInSelection,enumDescriptions:[b.localize(9,null),b.localize(10,null),b.localize(11,null)],description:b.localize(12,null)},"editor.find.globalFindClipboard":{type:"boolean",default:Z.globalFindClipboard,description:b.localize(13,null),included:N.isMacintosh},"editor.find.addExtraSpaceOnTop":{type:"boolean",default:Z.addExtraSpaceOnTop,description:b.localize(14,null)},"editor.find.loop":{type:"boolean",default:Z.loop,description:b.localize(15,null)}})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z;return{cursorMoveOnType:c(ue.cursorMoveOnType,this.defaultValue.cursorMoveOnType),seedSearchStringFromSelection:c(ue.seedSearchStringFromSelection,this.defaultValue.seedSearchStringFromSelection),autoFindInSelection:typeof Z.autoFindInSelection=="boolean"?Z.autoFindInSelection?"always":"never":r(ue.autoFindInSelection,this.defaultValue.autoFindInSelection,["never","always","multiline"]),globalFindClipboard:c(ue.globalFindClipboard,this.defaultValue.globalFindClipboard),addExtraSpaceOnTop:c(ue.addExtraSpaceOnTop,this.defaultValue.addExtraSpaceOnTop),loop:c(ue.loop,this.defaultValue.loop)}}}class I extends d{constructor(){super(39,"fontLigatures",I.OFF,{anyOf:[{type:"boolean",description:b.localize(16,null)},{type:"string",description:b.localize(17,null)}],description:b.localize(18,null),default:!1})}validate(Z){return typeof Z=="undefined"?this.defaultValue:typeof Z=="string"?Z==="false"?I.OFF:Z==="true"?I.ON:Z:Boolean(Z)?I.ON:I.OFF}}e.EditorFontLigatures=I,I.OFF='"liga" off, "calt" off',I.ON='"liga" on, "calt" on';class k extends g{constructor(){super(38)}compute(Z,ue,he){return Z.fontInfo}}class E extends p{constructor(){super(40,"fontSize",e.EDITOR_FONT_DEFAULTS.fontSize,{type:"number",minimum:6,maximum:100,default:e.EDITOR_FONT_DEFAULTS.fontSize,description:b.localize(19,null)})}validate(Z){let ue=a.float(Z,this.defaultValue);return ue===0?e.EDITOR_FONT_DEFAULTS.fontSize:a.clamp(ue,6,100)}compute(Z,ue,he){return Z.fontInfo.fontSize}}class T extends d{constructor(){super(41,"fontWeight",e.EDITOR_FONT_DEFAULTS.fontWeight,{anyOf:[{type:"number",minimum:T.MINIMUM_VALUE,maximum:T.MAXIMUM_VALUE,errorMessage:b.localize(20,null)},{type:"string",pattern:"^(normal|bold|1000|[1-9][0-9]{0,2})$"},{enum:T.SUGGESTION_VALUES}],default:e.EDITOR_FONT_DEFAULTS.fontWeight,description:b.localize(21,null)})}validate(Z){return Z==="normal"||Z==="bold"?Z:String(s.clampedInt(Z,e.EDITOR_FONT_DEFAULTS.fontWeight,T.MINIMUM_VALUE,T.MAXIMUM_VALUE))}}T.SUGGESTION_VALUES=["normal","bold","100","200","300","400","500","600","700","800","900"],T.MINIMUM_VALUE=1,T.MAXIMUM_VALUE=1e3;class O extends d{constructor(){const Z={multiple:"peek",multipleDefinitions:"peek",multipleTypeDefinitions:"peek",multipleDeclarations:"peek",multipleImplementations:"peek",multipleReferences:"peek",alternativeDefinitionCommand:"editor.action.goToReferences",alternativeTypeDefinitionCommand:"editor.action.goToReferences",alternativeDeclarationCommand:"editor.action.goToReferences",alternativeImplementationCommand:"",alternativeReferenceCommand:""},ue={type:"string",enum:["peek","gotoAndPeek","goto"],default:Z.multiple,enumDescriptions:[b.localize(22,null),b.localize(23,null),b.localize(24,null)]};super(45,"gotoLocation",Z,{"editor.gotoLocation.multiple":{deprecationMessage:b.localize(25,null)},"editor.gotoLocation.multipleDefinitions":Object.assign({description:b.localize(26,null)},ue),"editor.gotoLocation.multipleTypeDefinitions":Object.assign({description:b.localize(27,null)},ue),"editor.gotoLocation.multipleDeclarations":Object.assign({description:b.localize(28,null)},ue),"editor.gotoLocation.multipleImplementations":Object.assign({description:b.localize(29,null)},ue),"editor.gotoLocation.multipleReferences":Object.assign({description:b.localize(30,null)},ue),"editor.gotoLocation.alternativeDefinitionCommand":{type:"string",default:Z.alternativeDefinitionCommand,description:b.localize(31,null)},"editor.gotoLocation.alternativeTypeDefinitionCommand":{type:"string",default:Z.alternativeTypeDefinitionCommand,description:b.localize(32,null)},"editor.gotoLocation.alternativeDeclarationCommand":{type:"string",default:Z.alternativeDeclarationCommand,description:b.localize(33,null)},"editor.gotoLocation.alternativeImplementationCommand":{type:"string",default:Z.alternativeImplementationCommand,description:b.localize(34,null)},"editor.gotoLocation.alternativeReferenceCommand":{type:"string",default:Z.alternativeReferenceCommand,description:b.localize(35,null)}})}validate(Z){var ue,he,re,ce,me;if(!Z||typeof Z!="object")return this.defaultValue;const Ce=Z;return{multiple:r(Ce.multiple,this.defaultValue.multiple,["peek","gotoAndPeek","goto"]),multipleDefinitions:(ue=Ce.multipleDefinitions)!==null&&ue!==void 0?ue:r(Ce.multipleDefinitions,"peek",["peek","gotoAndPeek","goto"]),multipleTypeDefinitions:(he=Ce.multipleTypeDefinitions)!==null&&he!==void 0?he:r(Ce.multipleTypeDefinitions,"peek",["peek","gotoAndPeek","goto"]),multipleDeclarations:(re=Ce.multipleDeclarations)!==null&&re!==void 0?re:r(Ce.multipleDeclarations,"peek",["peek","gotoAndPeek","goto"]),multipleImplementations:(ce=Ce.multipleImplementations)!==null&&ce!==void 0?ce:r(Ce.multipleImplementations,"peek",["peek","gotoAndPeek","goto"]),multipleReferences:(me=Ce.multipleReferences)!==null&&me!==void 0?me:r(Ce.multipleReferences,"peek",["peek","gotoAndPeek","goto"]),alternativeDefinitionCommand:u.string(Ce.alternativeDefinitionCommand,this.defaultValue.alternativeDefinitionCommand),alternativeTypeDefinitionCommand:u.string(Ce.alternativeTypeDefinitionCommand,this.defaultValue.alternativeTypeDefinitionCommand),alternativeDeclarationCommand:u.string(Ce.alternativeDeclarationCommand,this.defaultValue.alternativeDeclarationCommand),alternativeImplementationCommand:u.string(Ce.alternativeImplementationCommand,this.defaultValue.alternativeImplementationCommand),alternativeReferenceCommand:u.string(Ce.alternativeReferenceCommand,this.defaultValue.alternativeReferenceCommand)}}}class A extends d{constructor(){const Z={enabled:!0,delay:300,sticky:!0};super(48,"hover",Z,{"editor.hover.enabled":{type:"boolean",default:Z.enabled,description:b.localize(36,null)},"editor.hover.delay":{type:"number",default:Z.delay,description:b.localize(37,null)},"editor.hover.sticky":{type:"boolean",default:Z.sticky,description:b.localize(38,null)}})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z;return{enabled:c(ue.enabled,this.defaultValue.enabled),delay:s.clampedInt(ue.delay,this.defaultValue.delay,0,1e4),sticky:c(ue.sticky,this.defaultValue.sticky)}}}class B extends g{constructor(){super(124,[44,52,33,59,87,54,55,89,111,114,115,116,2])}compute(Z,ue,he){return B.computeLayout(ue,{memory:Z.memory,outerWidth:Z.outerWidth,outerHeight:Z.outerHeight,isDominatedByLongLines:Z.isDominatedByLongLines,lineHeight:Z.fontInfo.lineHeight,viewLineCount:Z.viewLineCount,lineNumbersDigitCount:Z.lineNumbersDigitCount,typicalHalfwidthCharacterWidth:Z.fontInfo.typicalHalfwidthCharacterWidth,maxDigitWidth:Z.fontInfo.maxDigitWidth,pixelRatio:Z.pixelRatio})}static computeContainedMinimapLineCount(Z){const ue=Z.height/Z.lineHeight,he=Z.scrollBeyondLastLine?ue-1:0,re=(Z.viewLineCount+he)/(Z.pixelRatio*Z.height),ce=Math.floor(Z.viewLineCount/re);return{typicalViewportLineCount:ue,extraLinesBeyondLastLine:he,desiredRatio:re,minimapLineCount:ce}}static _computeMinimapLayout(Z,ue){const he=Z.outerWidth,re=Z.outerHeight,ce=Z.pixelRatio;if(!Z.minimap.enabled)return{renderMinimap:0,minimapLeft:0,minimapWidth:0,minimapHeightIsEditorHeight:!1,minimapIsSampling:!1,minimapScale:1,minimapLineHeight:1,minimapCanvasInnerWidth:0,minimapCanvasInnerHeight:Math.floor(ce*re),minimapCanvasOuterWidth:0,minimapCanvasOuterHeight:re};const me=ue.stableMinimapLayoutInput,Ce=me&&Z.outerHeight===me.outerHeight&&Z.lineHeight===me.lineHeight&&Z.typicalHalfwidthCharacterWidth===me.typicalHalfwidthCharacterWidth&&Z.pixelRatio===me.pixelRatio&&Z.scrollBeyondLastLine===me.scrollBeyondLastLine&&Z.minimap.enabled===me.minimap.enabled&&Z.minimap.side===me.minimap.side&&Z.minimap.size===me.minimap.size&&Z.minimap.showSlider===me.minimap.showSlider&&Z.minimap.renderCharacters===me.minimap.renderCharacters&&Z.minimap.maxColumn===me.minimap.maxColumn&&Z.minimap.scale===me.minimap.scale&&Z.verticalScrollbarWidth===me.verticalScrollbarWidth&&Z.isViewportWrapping===me.isViewportWrapping,be=Z.lineHeight,Le=Z.typicalHalfwidthCharacterWidth,De=Z.scrollBeyondLastLine,Re=Z.minimap.renderCharacters;let Ee=ce>=2?Math.round(Z.minimap.scale*2):Z.minimap.scale;const Ae=Z.minimap.maxColumn,Se=Z.minimap.size,we=Z.minimap.side,ye=Z.verticalScrollbarWidth,fe=Z.viewLineCount,de=Z.remainingWidth,ge=Z.isViewportWrapping,pe=Re?2:3;let ve=Math.floor(ce*re);const ke=ve/ce;let Ne=!1,Te=!1,Oe=pe*Ee,Fe=Ee/ce,Pe=1;if(Se==="fill"||Se==="fit"){const{typicalViewportLineCount:Ve,extraLinesBeyondLastLine:Ue,desiredRatio:Ye,minimapLineCount:je}=B.computeContainedMinimapLineCount({viewLineCount:fe,scrollBeyondLastLine:De,height:re,lineHeight:be,pixelRatio:ce});if(fe/je>1)Ne=!0,Te=!0,Ee=1,Oe=1,Fe=Ee/ce;else{let $e=!1,Ze=Ee+1;if(Se==="fit"){const Qe=Math.ceil((fe+Ue)*Oe);ge&&Ce&&de<=ue.stableFitRemainingWidth?($e=!0,Ze=ue.stableFitMaxMinimapScale):($e=Qe>ve,ge&&$e?(ue.stableMinimapLayoutInput=Z,ue.stableFitRemainingWidth=de):(ue.stableMinimapLayoutInput=null,ue.stableFitRemainingWidth=0))}if(Se==="fill"||$e){Ne=!0;const Qe=Ee;Oe=Math.min(be*ce,Math.max(1,Math.floor(1/Ye))),Ee=Math.min(Ze,Math.max(1,Math.floor(Oe/pe))),Ee>Qe&&(Pe=Math.min(2,Ee/Qe)),Fe=Ee/ce/Pe,ve=Math.ceil(Math.max(Ve,fe+Ue)*Oe),ge&&$e&&(ue.stableFitMaxMinimapScale=Ee)}}}const xe=Math.floor(Ae*Fe),We=Math.min(xe,Math.max(0,Math.floor((de-ye-2)*Fe/(Le+Fe)))+e.MINIMAP_GUTTER_WIDTH);let ze=Math.floor(ce*We);const Ke=ze/ce;ze=Math.floor(ze*Pe);const Be=Re?1:2,He=we==="left"?0:he-We-ye;return{renderMinimap:Be,minimapLeft:He,minimapWidth:We,minimapHeightIsEditorHeight:Ne,minimapIsSampling:Te,minimapScale:Ee,minimapLineHeight:Oe,minimapCanvasInnerWidth:ze,minimapCanvasInnerHeight:ve,minimapCanvasOuterWidth:Ke,minimapCanvasOuterHeight:ke}}static computeLayout(Z,ue){const he=ue.outerWidth|0,re=ue.outerHeight|0,ce=ue.lineHeight|0,me=ue.lineNumbersDigitCount|0,Ce=ue.typicalHalfwidthCharacterWidth,be=ue.maxDigitWidth,Le=ue.pixelRatio,De=ue.viewLineCount,Re=Z.get(116),Ee=Re==="inherit"?Z.get(115):Re,Ae=Ee==="inherit"?Z.get(111):Ee,Se=Z.get(114),we=Z.get(2),ye=ue.isDominatedByLongLines,fe=Z.get(44),de=Z.get(54).renderType!==0,ge=Z.get(55),pe=Z.get(89),ve=Z.get(59),ke=Z.get(87),Ne=ke.verticalScrollbarSize,Te=ke.verticalHasArrows,Oe=ke.arrowSize,Fe=ke.horizontalScrollbarSize,Pe=Z.get(52),xe=Z.get(33);let We;if(typeof Pe=="string"&&/^\d+(\.\d+)?ch$/.test(Pe)){const nt=parseFloat(Pe.substr(0,Pe.length-2));We=s.clampedInt(nt*Ce,0,0,1e3)}else We=s.clampedInt(Pe,0,0,1e3);xe&&(We+=16);let ze=0;if(de){const nt=Math.max(me,ge);ze=Math.round(nt*be)}let Ke=0;fe&&(Ke=ce);let Be=0,He=Be+Ke,Ve=He+ze,Ue=Ve+We;const Ye=he-Ke-ze-We;let je=!1,Xe=!1,$e=-1;we!==2&&(Ee==="inherit"&&ye?(je=!0,Xe=!0):Ae==="on"||Ae==="bounded"?Xe=!0:Ae==="wordWrapColumn"&&($e=Se));const Ze=B._computeMinimapLayout({outerWidth:he,outerHeight:re,lineHeight:ce,typicalHalfwidthCharacterWidth:Ce,pixelRatio:Le,scrollBeyondLastLine:pe,minimap:ve,verticalScrollbarWidth:Ne,viewLineCount:De,remainingWidth:Ye,isViewportWrapping:Xe},ue.memory||new C);Ze.renderMinimap!==0&&Ze.minimapLeft===0&&(Be+=Ze.minimapWidth,He+=Ze.minimapWidth,Ve+=Ze.minimapWidth,Ue+=Ze.minimapWidth);const Qe=Ye-Ze.minimapWidth,it=Math.max(1,Math.floor((Qe-Ne-2)/Ce)),Je=Te?Oe:0;return Xe&&($e=Math.max(1,it),Ae==="bounded"&&($e=Math.min($e,Se))),{width:he,height:re,glyphMarginLeft:Be,glyphMarginWidth:Ke,lineNumbersLeft:He,lineNumbersWidth:ze,decorationsLeft:Ve,decorationsWidth:We,contentLeft:Ue,contentWidth:Qe,minimap:Ze,viewportColumn:it,isWordWrapMinified:je,isViewportWrapping:Xe,wrappingColumn:$e,verticalScrollbarWidth:Ne,horizontalScrollbarHeight:Fe,overviewRuler:{top:Je,width:Ne,height:re-2*Je,right:0}}}}e.EditorLayoutInfoComputer=B;class F extends d{constructor(){const Z={enabled:!0};super(51,"lightbulb",Z,{"editor.lightbulb.enabled":{type:"boolean",default:Z.enabled,description:b.localize(39,null)}})}validate(Z){return!Z||typeof Z!="object"?this.defaultValue:{enabled:c(Z.enabled,this.defaultValue.enabled)}}}class D extends d{constructor(){const Z={enabled:!0,fontSize:0,fontFamily:e.EDITOR_FONT_DEFAULTS.fontFamily};super(120,"inlineHints",Z,{"editor.inlineHints.enabled":{type:"boolean",default:Z.enabled,description:b.localize(40,null)},"editor.inlineHints.fontSize":{type:"number",default:Z.fontSize,description:b.localize(41,null)},"editor.inlineHints.fontFamily":{type:"string",default:Z.fontFamily,description:b.localize(42,null)}})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z;return{enabled:c(ue.enabled,this.defaultValue.enabled),fontSize:s.clampedInt(ue.fontSize,this.defaultValue.fontSize,0,100),fontFamily:u.string(ue.fontFamily,this.defaultValue.fontFamily)}}}class R extends s{constructor(){super(53,"lineHeight",e.EDITOR_FONT_DEFAULTS.lineHeight,0,150,{description:b.localize(43,null)})}compute(Z,ue,he){return Z.fontInfo.lineHeight}}class W extends d{constructor(){const Z={enabled:!0,size:"proportional",side:"right",showSlider:"mouseover",renderCharacters:!0,maxColumn:120,scale:1};super(59,"minimap",Z,{"editor.minimap.enabled":{type:"boolean",default:Z.enabled,description:b.localize(44,null)},"editor.minimap.size":{type:"string",enum:["proportional","fill","fit"],enumDescriptions:[b.localize(45,null),b.localize(46,null),b.localize(47,null)],default:Z.size,description:b.localize(48,null)},"editor.minimap.side":{type:"string",enum:["left","right"],default:Z.side,description:b.localize(49,null)},"editor.minimap.showSlider":{type:"string",enum:["always","mouseover"],default:Z.showSlider,description:b.localize(50,null)},"editor.minimap.scale":{type:"number",default:Z.scale,minimum:1,maximum:3,enum:[1,2,3],description:b.localize(51,null)},"editor.minimap.renderCharacters":{type:"boolean",default:Z.renderCharacters,description:b.localize(52,null)},"editor.minimap.maxColumn":{type:"number",default:Z.maxColumn,description:b.localize(53,null)}})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z;return{enabled:c(ue.enabled,this.defaultValue.enabled),size:r(ue.size,this.defaultValue.size,["proportional","fill","fit"]),side:r(ue.side,this.defaultValue.side,["right","left"]),showSlider:r(ue.showSlider,this.defaultValue.showSlider,["always","mouseover"]),renderCharacters:c(ue.renderCharacters,this.defaultValue.renderCharacters),scale:s.clampedInt(ue.scale,1,1,3),maxColumn:s.clampedInt(ue.maxColumn,this.defaultValue.maxColumn,1,1e4)}}}function x(te){return te==="ctrlCmd"?N.isMacintosh?"metaKey":"ctrlKey":"altKey"}class K extends d{constructor(){super(69,"padding",{top:0,bottom:0},{"editor.padding.top":{type:"number",default:0,minimum:0,maximum:1e3,description:b.localize(54,null)},"editor.padding.bottom":{type:"number",default:0,minimum:0,maximum:1e3,description:b.localize(55,null)}})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z;return{top:s.clampedInt(ue.top,0,0,1e3),bottom:s.clampedInt(ue.bottom,0,0,1e3)}}}class Y extends d{constructor(){const Z={enabled:!0,cycle:!1};super(70,"parameterHints",Z,{"editor.parameterHints.enabled":{type:"boolean",default:Z.enabled,description:b.localize(56,null)},"editor.parameterHints.cycle":{type:"boolean",default:Z.cycle,description:b.localize(57,null)}})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z;return{enabled:c(ue.enabled,this.defaultValue.enabled),cycle:c(ue.cycle,this.defaultValue.cycle)}}}class ee extends g{constructor(){super(122)}compute(Z,ue,he){return Z.pixelRatio}}class se extends d{constructor(){const Z={other:!0,comments:!1,strings:!1};super(73,"quickSuggestions",Z,{anyOf:[{type:"boolean"},{type:"object",properties:{strings:{type:"boolean",default:Z.strings,description:b.localize(58,null)},comments:{type:"boolean",default:Z.comments,description:b.localize(59,null)},other:{type:"boolean",default:Z.other,description:b.localize(60,null)}}}],default:Z,description:b.localize(61,null)});this.defaultValue=Z}validate(Z){if(typeof Z=="boolean")return Z;if(Z&&typeof Z=="object"){const ue=Z,he={other:c(ue.other,this.defaultValue.other),comments:c(ue.comments,this.defaultValue.comments),strings:c(ue.strings,this.defaultValue.strings)};return he.other&&he.comments&&he.strings?!0:!he.other&&!he.comments&&!he.strings?!1:he}return this.defaultValue}}class ne extends d{constructor(){super(54,"lineNumbers",{renderType:1,renderFn:null},{type:"string",enum:["off","on","relative","interval"],enumDescriptions:[b.localize(62,null),b.localize(63,null),b.localize(64,null),b.localize(65,null)],default:"on",description:b.localize(66,null)})}validate(Z){let ue=this.defaultValue.renderType,he=this.defaultValue.renderFn;return typeof Z!="undefined"&&(typeof Z=="function"?(ue=4,he=Z):Z==="interval"?ue=3:Z==="relative"?ue=2:Z==="on"?ue=1:ue=0),{renderType:ue,renderFn:he}}}function le(te){const Z=te.get(82);return Z==="editable"?te.get(75):Z!=="on"}e.filterValidationDecorations=le;class X extends d{constructor(){const Z=[],ue={type:"number",description:b.localize(67,null)};super(86,"rulers",Z,{type:"array",items:{anyOf:[ue,{type:["object"],properties:{column:ue,color:{type:"string",description:b.localize(68,null),format:"color-hex"}}}]},default:Z,description:b.localize(69,null)})}validate(Z){if(Array.isArray(Z)){let ue=[];for(let he of Z)if(typeof he=="number")ue.push({column:s.clampedInt(he,0,0,1e4),color:null});else if(he&&typeof he=="object"){const re=he;ue.push({column:s.clampedInt(re.column,0,0,1e4),color:re.color})}return ue.sort((he,re)=>he.column-re.column),ue}return this.defaultValue}}function z(te,Z){if(typeof te!="string")return Z;switch(te){case"hidden":return 2;case"visible":return 3;default:return 1}}class P extends d{constructor(){super(87,"scrollbar",{vertical:1,horizontal:1,arrowSize:11,useShadows:!0,verticalHasArrows:!1,horizontalHasArrows:!1,horizontalScrollbarSize:12,horizontalSliderSize:12,verticalScrollbarSize:14,verticalSliderSize:14,handleMouseWheel:!0,alwaysConsumeMouseWheel:!0,scrollByPage:!1})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z,he=s.clampedInt(ue.horizontalScrollbarSize,this.defaultValue.horizontalScrollbarSize,0,1e3),re=s.clampedInt(ue.verticalScrollbarSize,this.defaultValue.verticalScrollbarSize,0,1e3);return{arrowSize:s.clampedInt(ue.arrowSize,this.defaultValue.arrowSize,0,1e3),vertical:z(ue.vertical,this.defaultValue.vertical),horizontal:z(ue.horizontal,this.defaultValue.horizontal),useShadows:c(ue.useShadows,this.defaultValue.useShadows),verticalHasArrows:c(ue.verticalHasArrows,this.defaultValue.verticalHasArrows),horizontalHasArrows:c(ue.horizontalHasArrows,this.defaultValue.horizontalHasArrows),handleMouseWheel:c(ue.handleMouseWheel,this.defaultValue.handleMouseWheel),alwaysConsumeMouseWheel:c(ue.alwaysConsumeMouseWheel,this.defaultValue.alwaysConsumeMouseWheel),horizontalScrollbarSize:he,horizontalSliderSize:s.clampedInt(ue.horizontalSliderSize,he,0,1e3),verticalScrollbarSize:re,verticalSliderSize:s.clampedInt(ue.verticalSliderSize,re,0,1e3),scrollByPage:c(ue.scrollByPage,this.defaultValue.scrollByPage)}}}class V extends d{constructor(){const Z={insertMode:"insert",filterGraceful:!0,snippetsPreventQuickSuggestions:!0,localityBonus:!1,shareSuggestSelections:!1,showIcons:!0,showStatusBar:!1,showInlineDetails:!0,showMethods:!0,showFunctions:!0,showConstructors:!0,showFields:!0,showVariables:!0,showClasses:!0,showStructs:!0,showInterfaces:!0,showModules:!0,showProperties:!0,showEvents:!0,showOperators:!0,showUnits:!0,showValues:!0,showConstants:!0,showEnums:!0,showEnumMembers:!0,showKeywords:!0,showWords:!0,showColors:!0,showFiles:!0,showReferences:!0,showFolders:!0,showTypeParameters:!0,showSnippets:!0,showUsers:!0,showIssues:!0};super(101,"suggest",Z,{"editor.suggest.insertMode":{type:"string",enum:["insert","replace"],enumDescriptions:[b.localize(70,null),b.localize(71,null)],default:Z.insertMode,description:b.localize(72,null)},"editor.suggest.filterGraceful":{type:"boolean",default:Z.filterGraceful,description:b.localize(73,null)},"editor.suggest.localityBonus":{type:"boolean",default:Z.localityBonus,description:b.localize(74,null)},"editor.suggest.shareSuggestSelections":{type:"boolean",default:Z.shareSuggestSelections,markdownDescription:b.localize(75,null)},"editor.suggest.snippetsPreventQuickSuggestions":{type:"boolean",default:Z.snippetsPreventQuickSuggestions,description:b.localize(76,null)},"editor.suggest.showIcons":{type:"boolean",default:Z.showIcons,description:b.localize(77,null)},"editor.suggest.showStatusBar":{type:"boolean",default:Z.showStatusBar,description:b.localize(78,null)},"editor.suggest.showInlineDetails":{type:"boolean",default:Z.showInlineDetails,description:b.localize(79,null)},"editor.suggest.maxVisibleSuggestions":{type:"number",deprecationMessage:b.localize(80,null)},"editor.suggest.filteredTypes":{type:"object",deprecationMessage:b.localize(81,null)},"editor.suggest.showMethods":{type:"boolean",default:!0,markdownDescription:b.localize(82,null)},"editor.suggest.showFunctions":{type:"boolean",default:!0,markdownDescription:b.localize(83,null)},"editor.suggest.showConstructors":{type:"boolean",default:!0,markdownDescription:b.localize(84,null)},"editor.suggest.showFields":{type:"boolean",default:!0,markdownDescription:b.localize(85,null)},"editor.suggest.showVariables":{type:"boolean",default:!0,markdownDescription:b.localize(86,null)},"editor.suggest.showClasses":{type:"boolean",default:!0,markdownDescription:b.localize(87,null)},"editor.suggest.showStructs":{type:"boolean",default:!0,markdownDescription:b.localize(88,null)},"editor.suggest.showInterfaces":{type:"boolean",default:!0,markdownDescription:b.localize(89,null)},"editor.suggest.showModules":{type:"boolean",default:!0,markdownDescription:b.localize(90,null)},"editor.suggest.showProperties":{type:"boolean",default:!0,markdownDescription:b.localize(91,null)},"editor.suggest.showEvents":{type:"boolean",default:!0,markdownDescription:b.localize(92,null)},"editor.suggest.showOperators":{type:"boolean",default:!0,markdownDescription:b.localize(93,null)},"editor.suggest.showUnits":{type:"boolean",default:!0,markdownDescription:b.localize(94,null)},"editor.suggest.showValues":{type:"boolean",default:!0,markdownDescription:b.localize(95,null)},"editor.suggest.showConstants":{type:"boolean",default:!0,markdownDescription:b.localize(96,null)},"editor.suggest.showEnums":{type:"boolean",default:!0,markdownDescription:b.localize(97,null)},"editor.suggest.showEnumMembers":{type:"boolean",default:!0,markdownDescription:b.localize(98,null)},"editor.suggest.showKeywords":{type:"boolean",default:!0,markdownDescription:b.localize(99,null)},"editor.suggest.showWords":{type:"boolean",default:!0,markdownDescription:b.localize(100,null)},"editor.suggest.showColors":{type:"boolean",default:!0,markdownDescription:b.localize(101,null)},"editor.suggest.showFiles":{type:"boolean",default:!0,markdownDescription:b.localize(102,null)},"editor.suggest.showReferences":{type:"boolean",default:!0,markdownDescription:b.localize(103,null)},"editor.suggest.showCustomcolors":{type:"boolean",default:!0,markdownDescription:b.localize(104,null)},"editor.suggest.showFolders":{type:"boolean",default:!0,markdownDescription:b.localize(105,null)},"editor.suggest.showTypeParameters":{type:"boolean",default:!0,markdownDescription:b.localize(106,null)},"editor.suggest.showSnippets":{type:"boolean",default:!0,markdownDescription:b.localize(107,null)},"editor.suggest.showUsers":{type:"boolean",default:!0,markdownDescription:b.localize(108,null)},"editor.suggest.showIssues":{type:"boolean",default:!0,markdownDescription:b.localize(109,null)}})}validate(Z){if(!Z||typeof Z!="object")return this.defaultValue;const ue=Z;return{insertMode:r(ue.insertMode,this.defaultValue.insertMode,["insert","replace"]),filterGraceful:c(ue.filterGraceful,this.defaultValue.filterGraceful),snippetsPreventQuickSuggestions:c(ue.snippetsPreventQuickSuggestions,this.defaultValue.filterGraceful),localityBonus:c(ue.localityBonus,this.defaultValue.localityBonus),shareSuggestSelections:c(ue.shareSuggestSelections,this.defaultValue.shareSuggestSelections),showIcons:c(ue.showIcons,this.defaultValue.showIcons),showStatusBar:c(ue.showStatusBar,this.defaultValue.showStatusBar),showInlineDetails:c(ue.showInlineDetails,this.defaultValue.showInlineDetails),showMethods:c(ue.showMethods,this.defaultValue.showMethods),showFunctions:c(ue.showFunctions,this.defaultValue.showFunctions),showConstructors:c(ue.showConstructors,this.defaultValue.showConstructors),showFields:c(ue.showFields,this.defaultValue.showFields),showVariables:c(ue.showVariables,this.defaultValue.showVariables),showClasses:c(ue.showClasses,this.defaultValue.showClasses),showStructs:c(ue.showStructs,this.defaultValue.showStructs),showInterfaces:c(ue.showInterfaces,this.defaultValue.showInterfaces),showModules:c(ue.showModules,this.defaultValue.showModules),showProperties:c(ue.showProperties,this.defaultValue.showProperties),showEvents:c(ue.showEvents,this.defaultValue.showEvents),showOperators:c(ue.showOperators,this.defaultValue.showOperators),showUnits:c(ue.showUnits,this.defaultValue.showUnits),showValues:c(ue.showValues,this.defaultValue.showValues),showConstants:c(ue.showConstants,this.defaultValue.showConstants),showEnums:c(ue.showEnums,this.defaultValue.showEnums),showEnumMembers:c(ue.showEnumMembers,this.defaultValue.showEnumMembers),showKeywords:c(ue.showKeywords,this.defaultValue.showKeywords),showWords:c(ue.showWords,this.defaultValue.showWords),showColors:c(ue.showColors,this.defaultValue.showColors),showFiles:c(ue.showFiles,this.defaultValue.showFiles),showReferences:c(ue.showReferences,this.defaultValue.showReferences),showFolders:c(ue.showFolders,this.defaultValue.showFolders),showTypeParameters:c(ue.showTypeParameters,this.defaultValue.showTypeParameters),showSnippets:c(ue.showSnippets,this.defaultValue.showSnippets),showUsers:c(ue.showUsers,this.defaultValue.showUsers),showIssues:c(ue.showIssues,this.defaultValue.showIssues)}}}class U extends d{constructor(){super(97,"smartSelect",{selectLeadingAndTrailingWhitespace:!0},{"editor.smartSelect.selectLeadingAndTrailingWhitespace":{description:b.localize(110,null),default:!0,type:"boolean"}})}validate(Z){return!Z||typeof Z!="object"?this.defaultValue:{selectLeadingAndTrailingWhitespace:c(Z.selectLeadingAndTrailingWhitespace,this.defaultValue.selectLeadingAndTrailingWhitespace)}}}class H extends g{constructor(){super(123,[75])}compute(Z,ue,he){return ue.get(75)?!0:Z.tabFocusMode}}function $(te){switch(te){case"none":return 0;case"same":return 1;case"indent":return 2;case"deepIndent":return 3}}class ie extends g{constructor(){super(125,[124])}compute(Z,ue,he){const re=ue.get(124);return{isDominatedByLongLines:Z.isDominatedByLongLines,isWordWrapMinified:re.isWordWrapMinified,isViewportWrapping:re.isViewportWrapping,wrappingColumn:re.wrappingColumn}}}const oe="Consolas, 'Courier New', monospace",ae="Menlo, Monaco, 'Courier New', monospace",G="'Droid Sans Mono', 'monospace', monospace, 'Droid Sans Fallback'";e.EDITOR_FONT_DEFAULTS={fontFamily:N.isMacintosh?ae:N.isLinux?G:oe,fontWeight:"normal",fontSize:N.isMacintosh?12:14,lineHeight:0,letterSpacing:0},e.EDITOR_MODEL_DEFAULTS={tabSize:4,indentSize:4,insertSpaces:!0,detectIndentation:!0,trimAutoWhitespace:!0,largeFileOptimizations:!0},e.editorOptionsRegistry=[];function j(te){return e.editorOptionsRegistry[te.id]=te,te}e.EditorOptions={acceptSuggestionOnCommitCharacter:j(new o(0,"acceptSuggestionOnCommitCharacter",!0,{markdownDescription:b.localize(111,null)})),acceptSuggestionOnEnter:j(new i(1,"acceptSuggestionOnEnter","on",["on","smart","off"],{markdownEnumDescriptions:["",b.localize(112,null),""],markdownDescription:b.localize(113,null)})),accessibilitySupport:j(new l),accessibilityPageSize:j(new s(3,"accessibilityPageSize",10,1,1073741824,{description:b.localize(114,null)})),ariaLabel:j(new u(4,"ariaLabel",b.localize(115,null))),autoClosingBrackets:j(new i(5,"autoClosingBrackets","languageDefined",["always","languageDefined","beforeWhitespace","never"],{enumDescriptions:["",b.localize(116,null),b.localize(117,null),""],description:b.localize(118,null)})),autoClosingOvertype:j(new i(6,"autoClosingOvertype","auto",["always","auto","never"],{enumDescriptions:["",b.localize(119,null),""],description:b.localize(120,null)})),autoClosingQuotes:j(new i(7,"autoClosingQuotes","languageDefined",["always","languageDefined","beforeWhitespace","never"],{enumDescriptions:["",b.localize(121,null),b.localize(122,null),""],description:b.localize(123,null)})),autoIndent:j(new n(8,"autoIndent",4,"full",["none","keep","brackets","advanced","full"],t,{enumDescriptions:[b.localize(124,null),b.localize(125,null),b.localize(126,null),b.localize(127,null),b.localize(128,null)],description:b.localize(129,null)})),automaticLayout:j(new o(9,"automaticLayout",!1)),autoSurround:j(new i(10,"autoSurround","languageDefined",["languageDefined","quotes","brackets","never"],{enumDescriptions:[b.localize(130,null),b.localize(131,null),b.localize(132,null),""],description:b.localize(133,null)})),stickyTabStops:j(new o(99,"stickyTabStops",!1,{description:b.localize(134,null)})),codeLens:j(new o(11,"codeLens",!0,{description:b.localize(135,null)})),codeLensFontFamily:j(new u(12,"codeLensFontFamily","",{description:b.localize(136,null)})),codeLensFontSize:j(new s(13,"codeLensFontSize",0,0,100,{type:"number",default:0,minimum:0,maximum:100,description:b.localize(137,null)})),colorDecorators:j(new o(14,"colorDecorators",!0,{description:b.localize(138,null)})),columnSelection:j(new o(15,"columnSelection",!1,{description:b.localize(139,null)})),comments:j(new h),contextmenu:j(new o(17,"contextmenu",!0)),copyWithSyntaxHighlighting:j(new o(18,"copyWithSyntaxHighlighting",!0,{description:b.localize(140,null)})),cursorBlinking:j(new n(19,"cursorBlinking",1,"blink",["blink","smooth","phase","expand","solid"],m,{description:b.localize(141,null)})),cursorSmoothCaretAnimation:j(new o(20,"cursorSmoothCaretAnimation",!1,{description:b.localize(142,null)})),cursorStyle:j(new n(21,"cursorStyle",_.Line,"line",["line","block","underline","line-thin","block-outline","underline-thin"],f,{description:b.localize(143,null)})),cursorSurroundingLines:j(new s(22,"cursorSurroundingLines",0,0,1073741824,{description:b.localize(144,null)})),cursorSurroundingLinesStyle:j(new i(23,"cursorSurroundingLinesStyle","default",["default","all"],{enumDescriptions:[b.localize(145,null),b.localize(146,null)],description:b.localize(147,null)})),cursorWidth:j(new s(24,"cursorWidth",0,0,1073741824,{markdownDescription:b.localize(148,null)})),disableLayerHinting:j(new o(25,"disableLayerHinting",!1)),disableMonospaceOptimizations:j(new o(26,"disableMonospaceOptimizations",!1)),dragAndDrop:j(new o(27,"dragAndDrop",!0,{description:b.localize(149,null)})),emptySelectionClipboard:j(new y),extraEditorClassName:j(new u(29,"extraEditorClassName","")),fastScrollSensitivity:j(new a(30,"fastScrollSensitivity",5,te=>te<=0?5:te,{markdownDescription:b.localize(150,null)})),find:j(new L),fixedOverflowWidgets:j(new o(32,"fixedOverflowWidgets",!1)),folding:j(new o(33,"folding",!0,{description:b.localize(151,null)})),foldingStrategy:j(new i(34,"foldingStrategy","auto",["auto","indentation"],{enumDescriptions:[b.localize(152,null),b.localize(153,null)],description:b.localize(154,null)})),foldingHighlight:j(new o(35,"foldingHighlight",!0,{description:b.localize(155,null)})),unfoldOnClickAfterEndOfLine:j(new o(36,"unfoldOnClickAfterEndOfLine",!1,{description:b.localize(156,null)})),fontFamily:j(new u(37,"fontFamily",e.EDITOR_FONT_DEFAULTS.fontFamily,{description:b.localize(157,null)})),fontInfo:j(new k),fontLigatures2:j(new I),fontSize:j(new E),fontWeight:j(new T),formatOnPaste:j(new o(42,"formatOnPaste",!1,{description:b.localize(158,null)})),formatOnType:j(new o(43,"formatOnType",!1,{description:b.localize(159,null)})),glyphMargin:j(new o(44,"glyphMargin",!0,{description:b.localize(160,null)})),gotoLocation:j(new O),hideCursorInOverviewRuler:j(new o(46,"hideCursorInOverviewRuler",!1,{description:b.localize(161,null)})),highlightActiveIndentGuide:j(new o(47,"highlightActiveIndentGuide",!0,{description:b.localize(162,null)})),hover:j(new A),inDiffEditor:j(new o(49,"inDiffEditor",!1)),letterSpacing:j(new a(50,"letterSpacing",e.EDITOR_FONT_DEFAULTS.letterSpacing,te=>a.clamp(te,-5,20),{description:b.localize(163,null)})),lightbulb:j(new F),lineDecorationsWidth:j(new p(52,"lineDecorationsWidth",10)),lineHeight:j(new R),lineNumbers:j(new ne),lineNumbersMinChars:j(new s(55,"lineNumbersMinChars",5,1,300)),linkedEditing:j(new o(56,"linkedEditing",!1,{description:b.localize(164,null)})),links:j(new o(57,"links",!0,{description:b.localize(165,null)})),matchBrackets:j(new i(58,"matchBrackets","always",["always","near","never"],{description:b.localize(166,null)})),minimap:j(new W),mouseStyle:j(new i(60,"mouseStyle","text",["text","default","copy"])),mouseWheelScrollSensitivity:j(new a(61,"mouseWheelScrollSensitivity",1,te=>te===0?1:te,{markdownDescription:b.localize(167,null)})),mouseWheelZoom:j(new o(62,"mouseWheelZoom",!1,{markdownDescription:b.localize(168,null)})),multiCursorMergeOverlapping:j(new o(63,"multiCursorMergeOverlapping",!0,{description:b.localize(169,null)})),multiCursorModifier:j(new n(64,"multiCursorModifier","altKey","alt",["ctrlCmd","alt"],x,{markdownEnumDescriptions:[b.localize(170,null),b.localize(171,null)],markdownDescription:b.localize(172,null)})),multiCursorPaste:j(new i(65,"multiCursorPaste","spread",["spread","full"],{markdownEnumDescriptions:[b.localize(173,null),b.localize(174,null)],markdownDescription:b.localize(175,null)})),occurrencesHighlight:j(new o(66,"occurrencesHighlight",!0,{description:b.localize(176,null)})),overviewRulerBorder:j(new o(67,"overviewRulerBorder",!0,{description:b.localize(177,null)})),overviewRulerLanes:j(new s(68,"overviewRulerLanes",3,0,3)),padding:j(new K),parameterHints:j(new Y),peekWidgetDefaultFocus:j(new i(71,"peekWidgetDefaultFocus","tree",["tree","editor"],{enumDescriptions:[b.localize(178,null),b.localize(179,null)],description:b.localize(180,null)})),definitionLinkOpensInPeek:j(new o(72,"definitionLinkOpensInPeek",!1,{description:b.localize(181,null)})),quickSuggestions:j(new se),quickSuggestionsDelay:j(new s(74,"quickSuggestionsDelay",10,0,1073741824,{description:b.localize(182,null)})),readOnly:j(new o(75,"readOnly",!1)),renameOnType:j(new o(76,"renameOnType",!1,{description:b.localize(183,null),markdownDeprecationMessage:b.localize(184,null)})),renderControlCharacters:j(new o(77,"renderControlCharacters",!1,{description:b.localize(185,null)})),renderIndentGuides:j(new o(78,"renderIndentGuides",!0,{description:b.localize(186,null)})),renderFinalNewline:j(new o(79,"renderFinalNewline",!0,{description:b.localize(187,null)})),renderLineHighlight:j(new i(80,"renderLineHighlight","line",["none","gutter","line","all"],{enumDescriptions:["","","",b.localize(188,null)],description:b.localize(189,null)})),renderLineHighlightOnlyWhenFocus:j(new o(81,"renderLineHighlightOnlyWhenFocus",!1,{description:b.localize(190,null)})),renderValidationDecorations:j(new i(82,"renderValidationDecorations","editable",["editable","on","off"])),renderWhitespace:j(new i(83,"renderWhitespace","selection",["none","boundary","selection","trailing","all"],{enumDescriptions:["",b.localize(191,null),b.localize(192,null),b.localize(193,null),""],description:b.localize(194,null)})),revealHorizontalRightPadding:j(new s(84,"revealHorizontalRightPadding",30,0,1e3)),roundedSelection:j(new o(85,"roundedSelection",!0,{description:b.localize(195,null)})),rulers:j(new X),scrollbar:j(new P),scrollBeyondLastColumn:j(new s(88,"scrollBeyondLastColumn",5,0,1073741824,{description:b.localize(196,null)})),scrollBeyondLastLine:j(new o(89,"scrollBeyondLastLine",!0,{description:b.localize(197,null)})),scrollPredominantAxis:j(new o(90,"scrollPredominantAxis",!0,{description:b.localize(198,null)})),selectionClipboard:j(new o(91,"selectionClipboard",!0,{description:b.localize(199,null),included:N.isLinux})),selectionHighlight:j(new o(92,"selectionHighlight",!0,{description:b.localize(200,null)})),selectOnLineNumbers:j(new o(93,"selectOnLineNumbers",!0)),showFoldingControls:j(new i(94,"showFoldingControls","mouseover",["always","mouseover"],{enumDescriptions:[b.localize(201,null),b.localize(202,null)],description:b.localize(203,null)})),showUnused:j(new o(95,"showUnused",!0,{description:b.localize(204,null)})),showDeprecated:j(new o(119,"showDeprecated",!0,{description:b.localize(205,null)})),inlineHints:j(new D),snippetSuggestions:j(new i(96,"snippetSuggestions","inline",["top","bottom","inline","none"],{enumDescriptions:[b.localize(206,null),b.localize(207,null),b.localize(208,null),b.localize(209,null)],description:b.localize(210,null)})),smartSelect:j(new U),smoothScrolling:j(new o(98,"smoothScrolling",!1,{description:b.localize(211,null)})),stopRenderingLineAfter:j(new s(100,"stopRenderingLineAfter",1e4,-1,1073741824)),suggest:j(new V),suggestFontSize:j(new s(102,"suggestFontSize",0,0,1e3,{markdownDescription:b.localize(212,null)})),suggestLineHeight:j(new s(103,"suggestLineHeight",0,0,1e3,{markdownDescription:b.localize(213,null)})),suggestOnTriggerCharacters:j(new o(104,"suggestOnTriggerCharacters",!0,{description:b.localize(214,null)})),suggestSelection:j(new i(105,"suggestSelection","recentlyUsed",["first","recentlyUsed","recentlyUsedByPrefix"],{markdownEnumDescriptions:[b.localize(215,null),b.localize(216,null),b.localize(217,null)],description:b.localize(218,null)})),tabCompletion:j(new i(106,"tabCompletion","off",["on","off","onlySnippets"],{enumDescriptions:[b.localize(219,null),b.localize(220,null),b.localize(221,null)],description:b.localize(222,null)})),tabIndex:j(new s(107,"tabIndex",0,-1,1073741824)),unusualLineTerminators:j(new i(108,"unusualLineTerminators","prompt",["auto","off","prompt"],{enumDescriptions:[b.localize(223,null),b.localize(224,null),b.localize(225,null)],description:b.localize(226,null)})),useTabStops:j(new o(109,"useTabStops",!0,{description:b.localize(227,null)})),wordSeparators:j(new u(110,"wordSeparators",M.USUAL_WORD_SEPARATORS,{description:b.localize(228,null)})),wordWrap:j(new i(111,"wordWrap","off",["off","on","wordWrapColumn","bounded"],{markdownEnumDescriptions:[b.localize(229,null),b.localize(230,null),b.localize(231,null),b.localize(232,null)],description:b.localize(233,null)})),wordWrapBreakAfterCharacters:j(new u(112,"wordWrapBreakAfterCharacters"," })]?|/&.,;\xA2\xB0\u2032\u2033\u2030\u2103\u3001\u3002\uFF61\uFF64\uFFE0\uFF0C\uFF0E\uFF1A\uFF1B\uFF1F\uFF01\uFF05\u30FB\uFF65\u309D\u309E\u30FD\u30FE\u30FC\u30A1\u30A3\u30A5\u30A7\u30A9\u30C3\u30E3\u30E5\u30E7\u30EE\u30F5\u30F6\u3041\u3043\u3045\u3047\u3049\u3063\u3083\u3085\u3087\u308E\u3095\u3096\u31F0\u31F1\u31F2\u31F3\u31F4\u31F5\u31F6\u31F7\u31F8\u31F9\u31FA\u31FB\u31FC\u31FD\u31FE\u31FF\u3005\u303B\uFF67\uFF68\uFF69\uFF6A\uFF6B\uFF6C\uFF6D\uFF6E\uFF6F\uFF70\u201D\u3009\u300B\u300D\u300F\u3011\u3015\uFF09\uFF3D\uFF5D\uFF63")),wordWrapBreakBeforeCharacters:j(new u(113,"wordWrapBreakBeforeCharacters","([{\u2018\u201C\u3008\u300A\u300C\u300E\u3010\u3014\uFF08\uFF3B\uFF5B\uFF62\xA3\xA5\uFF04\uFFE1\uFFE5+\uFF0B")),wordWrapColumn:j(new s(114,"wordWrapColumn",80,1,1073741824,{markdownDescription:b.localize(234,null)})),wordWrapOverride1:j(new i(115,"wordWrapOverride1","inherit",["off","on","inherit"])),wordWrapOverride2:j(new i(116,"wordWrapOverride2","inherit",["off","on","inherit"])),wrappingIndent:j(new n(117,"wrappingIndent",1,"same",["none","same","indent","deepIndent"],$,{enumDescriptions:[b.localize(235,null),b.localize(236,null),b.localize(237,null),b.localize(238,null)],description:b.localize(239,null)})),wrappingStrategy:j(new i(118,"wrappingStrategy","simple",["simple","advanced"],{enumDescriptions:[b.localize(240,null),b.localize(241,null)],description:b.localize(242,null)})),editorClassName:j(new v),pixelRatio:j(new ee),tabFocusMode:j(new H),layoutInfo:j(new B),wrappingInfo:j(new ie)}}),define(Q[455],J([0,1,14,3,63,38]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewModelDecorations=void 0;class S{constructor(d,g,p,c,o){this.editorId=d,this.model=g,this.configuration=p,this._linesCollection=c,this._coordinatesConverter=o,this._decorationsCache=Object.create(null),this._cachedModelDecorationsResolver=null,this._cachedModelDecorationsResolverViewRange=null}_clearCachedModelDecorationsResolver(){this._cachedModelDecorationsResolver=null,this._cachedModelDecorationsResolverViewRange=null}dispose(){this._decorationsCache=Object.create(null),this._clearCachedModelDecorationsResolver()}reset(){this._decorationsCache=Object.create(null),this._clearCachedModelDecorationsResolver()}onModelDecorationsChanged(){this._decorationsCache=Object.create(null),this._clearCachedModelDecorationsResolver()}onLineMappingChanged(){this._decorationsCache=Object.create(null),this._clearCachedModelDecorationsResolver()}_getOrCreateViewModelDecoration(d){const g=d.id;let p=this._decorationsCache[g];if(!p){const c=d.range,o=d.options;let s;if(o.isWholeLine){const a=this._coordinatesConverter.convertModelPositionToViewPosition(new b.Position(c.startLineNumber,1)),u=this._coordinatesConverter.convertModelPositionToViewPosition(new b.Position(c.endLineNumber,this.model.getLineMaxColumn(c.endLineNumber)));s=new N.Range(a.lineNumber,a.column,u.lineNumber,u.column)}else s=this._coordinatesConverter.convertModelRangeToViewRange(c);p=new M.ViewModelDecoration(s,o),this._decorationsCache[g]=p}return p}getDecorationsViewportData(d){let g=this._cachedModelDecorationsResolver!==null;return g=g&&d.equalsRange(this._cachedModelDecorationsResolverViewRange),g||(this._cachedModelDecorationsResolver=this._getDecorationsViewportData(d),this._cachedModelDecorationsResolverViewRange=d),this._cachedModelDecorationsResolver}_getDecorationsViewportData(d){const g=this._linesCollection.getDecorationsInRange(d,this.editorId,w.filterValidationDecorations(this.configuration.options)),p=d.startLineNumber,c=d.endLineNumber;let o=[],s=0,a=[];for(let u=p;u<=c;u++)a[u-p]=[];for(let u=0,r=g.length;u0&&(this.changes=S.compressConsecutiveTextChanges(this.changes,n)),this.afterEOL=t,this.afterVersionId=l,this.afterCursorState=h}static _writeSelectionsSize(i){return 4+4*4*(i?i.length:0)}static _writeSelections(i,n,t){if(C.writeUInt32BE(i,n?n.length:0,t),t+=4,n)for(const l of n)C.writeUInt32BE(i,l.selectionStartLineNumber,t),t+=4,C.writeUInt32BE(i,l.selectionStartColumn,t),t+=4,C.writeUInt32BE(i,l.positionLineNumber,t),t+=4,C.writeUInt32BE(i,l.positionColumn,t),t+=4;return t}static _readSelections(i,n,t){const l=C.readUInt32BE(i,n);n+=4;for(let h=0;hn.toString()).join(", ")}matchesResource(i){return(w.URI.isUri(this.model)?this.model:this.model.uri).toString()===i.toString()}setModel(i){this.model=i}canAppend(i){return this.model===i&&this._data instanceof p}append(i,n,t,l,h){this._data instanceof p&&this._data.append(i,n,t,l,h)}close(){this._data instanceof p&&(this._data=this._data.serialize())}open(){this._data instanceof p||(this._data=p.deserialize(this._data))}undo(){if(w.URI.isUri(this.model))throw new Error("Invalid SingleModelEditStackElement");this._data instanceof p&&(this._data=this._data.serialize());const i=p.deserialize(this._data);this.model._applyUndo(i.changes,i.beforeEOL,i.beforeVersionId,i.beforeCursorState)}redo(){if(w.URI.isUri(this.model))throw new Error("Invalid SingleModelEditStackElement");this._data instanceof p&&(this._data=this._data.serialize());const i=p.deserialize(this._data);this.model._applyRedo(i.changes,i.afterEOL,i.afterVersionId,i.afterCursorState)}heapSize(){return this._data instanceof p&&(this._data=this._data.serialize()),this._data.byteLength+168}}e.SingleModelEditStackElement=c;class o{constructor(i,n){this.type=1,this.label=i,this._isOpen=!0,this._editStackElementsArr=n.slice(0),this._editStackElementsMap=new Map;for(const t of this._editStackElementsArr){const l=g(t.resource);this._editStackElementsMap.set(l,t)}this._delegate=null}get resources(){return this._editStackElementsArr.map(i=>i.resource)}prepareUndoRedo(){if(this._delegate)return this._delegate.prepareUndoRedo(this)}matchesResource(i){const n=g(i);return this._editStackElementsMap.has(n)}setModel(i){const n=g(w.URI.isUri(i)?i:i.uri);this._editStackElementsMap.has(n)&&this._editStackElementsMap.get(n).setModel(i)}canAppend(i){if(!this._isOpen)return!1;const n=g(i.uri);return this._editStackElementsMap.has(n)?this._editStackElementsMap.get(n).canAppend(i):!1}append(i,n,t,l,h){const m=g(i.uri);this._editStackElementsMap.get(m).append(i,n,t,l,h)}close(){this._isOpen=!1}open(){}undo(){this._isOpen=!1;for(const i of this._editStackElementsArr)i.undo()}redo(){for(const i of this._editStackElementsArr)i.redo()}heapSize(i){const n=g(i);return this._editStackElementsMap.has(n)?this._editStackElementsMap.get(n).heapSize():0}split(){return this._editStackElementsArr}toString(){let i=[];for(const n of this._editStackElementsArr)i.push(`${d.basename(n.resource)}: ${n}`);return`{${i.join(", ")}}`}}e.MultiModelEditStackElement=o;function s(r){return r.getEOL()===` +`?0:1}function a(r){return r?r instanceof c||r instanceof o:!1}e.isEditStackElement=a;class u{constructor(i,n){this._model=i,this._undoRedoService=n}pushStackElement(){const i=this._undoRedoService.getLastElement(this._model.uri);a(i)&&i.close()}popStackElement(){const i=this._undoRedoService.getLastElement(this._model.uri);a(i)&&i.open()}clear(){this._undoRedoService.removeElements(this._model.uri)}_getOrCreateEditStackElement(i){const n=this._undoRedoService.getLastElement(this._model.uri);if(a(n)&&n.canAppend(this._model))return n;const t=new c(this._model,i);return this._undoRedoService.pushElement(t),t}pushEOL(i){const n=this._getOrCreateEditStackElement(null);this._model.setEOL(i),n.append(this._model,[],s(this._model),this._model.getAlternativeVersionId(),null)}pushEditOperation(i,n,t){const l=this._getOrCreateEditStackElement(i),h=this._model.applyEdits(n,!0),m=u._computeCursorState(t,h),_=h.map((f,v)=>({index:v,textChange:f.textChange}));return _.sort((f,v)=>f.textChange.oldPosition===v.textChange.oldPosition?f.index-v.index:f.textChange.oldPosition-v.textChange.oldPosition),l.append(this._model,_.map(f=>f.textChange),s(this._model),this._model.getAlternativeVersionId(),m),m}static _computeCursorState(i,n){try{return i?i(n):null}catch(t){return N.onUnexpectedError(t),null}}}e.EditStack=u}),define(Q[458],J([4,5]),function(q,e){return q.create("vs/editor/common/modes/modesRegistry",e)}),define(Q[459],J([4,5]),function(q,e){return q.create("vs/editor/common/standaloneStrings",e)}),define(Q[64],J([0,1,459]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SimpleServicesNLS=e.ToggleHighContrastNLS=e.StandaloneCodeEditorNLS=e.QuickOutlineNLS=e.QuickCommandNLS=e.QuickHelpNLS=e.GoToLineNLS=e.InspectTokensNLS=e.AccessibilityHelpNLS=void 0;var N;(function(o){o.noSelection=b.localize(0,null),o.singleSelectionRange=b.localize(1,null),o.singleSelection=b.localize(2,null),o.multiSelectionRange=b.localize(3,null),o.multiSelection=b.localize(4,null),o.emergencyConfOn=b.localize(5,null),o.openingDocs=b.localize(6,null),o.readonlyDiffEditor=b.localize(7,null),o.editableDiffEditor=b.localize(8,null),o.readonlyEditor=b.localize(9,null),o.editableEditor=b.localize(10,null),o.changeConfigToOnMac=b.localize(11,null),o.changeConfigToOnWinLinux=b.localize(12,null),o.auto_on=b.localize(13,null),o.auto_off=b.localize(14,null),o.tabFocusModeOnMsg=b.localize(15,null),o.tabFocusModeOnMsgNoKb=b.localize(16,null),o.tabFocusModeOffMsg=b.localize(17,null),o.tabFocusModeOffMsgNoKb=b.localize(18,null),o.openDocMac=b.localize(19,null),o.openDocWinLinux=b.localize(20,null),o.outroMsg=b.localize(21,null),o.showAccessibilityHelpAction=b.localize(22,null)})(N=e.AccessibilityHelpNLS||(e.AccessibilityHelpNLS={}));var M;(function(o){o.inspectTokensAction=b.localize(23,null)})(M=e.InspectTokensNLS||(e.InspectTokensNLS={}));var w;(function(o){o.gotoLineActionLabel=b.localize(24,null)})(w=e.GoToLineNLS||(e.GoToLineNLS={}));var S;(function(o){o.helpQuickAccessActionLabel=b.localize(25,null)})(S=e.QuickHelpNLS||(e.QuickHelpNLS={}));var C;(function(o){o.quickCommandActionLabel=b.localize(26,null),o.quickCommandHelp=b.localize(27,null)})(C=e.QuickCommandNLS||(e.QuickCommandNLS={}));var d;(function(o){o.quickOutlineActionLabel=b.localize(28,null),o.quickOutlineByCategoryActionLabel=b.localize(29,null)})(d=e.QuickOutlineNLS||(e.QuickOutlineNLS={}));var g;(function(o){o.editorViewAccessibleLabel=b.localize(30,null),o.accessibilityHelpMessage=b.localize(31,null)})(g=e.StandaloneCodeEditorNLS||(e.StandaloneCodeEditorNLS={}));var p;(function(o){o.toggleHighContrast=b.localize(32,null)})(p=e.ToggleHighContrastNLS||(e.ToggleHighContrastNLS={}));var c;(function(o){o.bulkEditServiceSummary=b.localize(33,null)})(c=e.SimpleServicesNLS||(e.SimpleServicesNLS={}))}),define(Q[460],J([4,5]),function(q,e){return q.create("vs/editor/common/view/editorColorRegistry",e)}),define(Q[461],J([4,5]),function(q,e){return q.create("vs/editor/contrib/anchorSelect/anchorSelect",e)}),define(Q[462],J([4,5]),function(q,e){return q.create("vs/editor/contrib/bracketMatching/bracketMatching",e)}),define(Q[463],J([4,5]),function(q,e){return q.create("vs/editor/contrib/caretOperations/caretOperations",e)}),define(Q[464],J([4,5]),function(q,e){return q.create("vs/editor/contrib/caretOperations/transpose",e)}),define(Q[465],J([4,5]),function(q,e){return q.create("vs/editor/contrib/clipboard/clipboard",e)}),define(Q[466],J([4,5]),function(q,e){return q.create("vs/editor/contrib/codeAction/codeActionCommands",e)}),define(Q[467],J([4,5]),function(q,e){return q.create("vs/editor/contrib/codeAction/lightBulbWidget",e)}),define(Q[468],J([4,5]),function(q,e){return q.create("vs/editor/contrib/codelens/codelensController",e)}),define(Q[469],J([4,5]),function(q,e){return q.create("vs/editor/contrib/comment/comment",e)}),define(Q[470],J([4,5]),function(q,e){return q.create("vs/editor/contrib/contextmenu/contextmenu",e)}),define(Q[471],J([4,5]),function(q,e){return q.create("vs/editor/contrib/cursorUndo/cursorUndo",e)}),define(Q[472],J([4,5]),function(q,e){return q.create("vs/editor/contrib/find/findController",e)}),define(Q[473],J([4,5]),function(q,e){return q.create("vs/editor/contrib/find/findWidget",e)}),define(Q[474],J([4,5]),function(q,e){return q.create("vs/editor/contrib/folding/folding",e)}),define(Q[475],J([4,5]),function(q,e){return q.create("vs/editor/contrib/folding/foldingDecorations",e)}),define(Q[476],J([4,5]),function(q,e){return q.create("vs/editor/contrib/fontZoom/fontZoom",e)}),define(Q[477],J([4,5]),function(q,e){return q.create("vs/editor/contrib/format/format",e)}),define(Q[478],J([4,5]),function(q,e){return q.create("vs/editor/contrib/format/formatActions",e)}),define(Q[479],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoError/gotoError",e)}),define(Q[480],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoError/gotoErrorWidget",e)}),define(Q[481],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoSymbol/goToCommands",e)}),define(Q[482],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition",e)}),define(Q[483],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoSymbol/peek/referencesController",e)}),define(Q[484],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoSymbol/peek/referencesTree",e)}),define(Q[485],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoSymbol/peek/referencesWidget",e)}),define(Q[486],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoSymbol/referencesModel",e)}),define(Q[132],J([0,1,486,6,44,2,8,119,3,51,12]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ReferencesModel=e.FileReferences=e.FilePreview=e.OneReference=void 0;class c{constructor(r,i,n,t,l){this.isProviderFirst=r,this.parent=i,this.uri=n,this._range=t,this._rangeCallback=l,this.id=C.defaultGenerator.nextId()}get range(){return this._range}set range(r){this._range=r,this._rangeCallback(this)}get ariaMessage(){var r;const i=(r=this.parent.getPreview(this))===null||r===void 0?void 0:r.preview(this.range);return i?b.localize(1,null,M.basename(this.uri),this.range.startLineNumber,this.range.startColumn,i.value):b.localize(0,null,M.basename(this.uri),this.range.startLineNumber,this.range.startColumn)}}e.OneReference=c;class o{constructor(r){this._modelReference=r}dispose(){this._modelReference.dispose()}preview(r,i=8){const n=this._modelReference.object.textEditorModel;if(!!n){const{startLineNumber:t,startColumn:l,endLineNumber:h,endColumn:m}=r,_=n.getWordUntilPosition({lineNumber:t,column:l-i}),f=new d.Range(t,_.startColumn,t,l),v=new d.Range(h,m,h,1073741824),y=n.getValueInRange(f).replace(/^\s+/,""),L=n.getValueInRange(r),I=n.getValueInRange(v).replace(/\s+$/,"");return{value:y+L+I,highlight:{start:y.length,end:y.length+L.length}}}}}e.FilePreview=o;class s{constructor(r,i){this.parent=r,this.uri=i,this.children=[],this._previews=new g.ResourceMap}dispose(){w.dispose(this._previews.values()),this._previews.clear()}getPreview(r){return this._previews.get(r.uri)}get ariaMessage(){const r=this.children.length;return r===1?b.localize(2,null,M.basename(this.uri),this.uri.fsPath):b.localize(3,null,r,M.basename(this.uri),this.uri.fsPath)}resolve(r){return Ie(this,void 0,void 0,function*(){if(this._previews.size!==0)return this;for(let i of this.children)if(!this._previews.has(i.uri))try{const n=yield r.createModelReference(i.uri);this._previews.set(i.uri,new o(n))}catch(n){p.onUnexpectedError(n)}return this})}}e.FileReferences=s;class a{constructor(r,i){this.groups=[],this.references=[],this._onDidChangeReferenceRange=new N.Emitter,this.onDidChangeReferenceRange=this._onDidChangeReferenceRange.event,this._links=r,this._title=i;const[n]=r;r.sort(a._compareReferences);let t;for(let l of r)if((!t||!M.extUri.isEqual(t.uri,l.uri,!0))&&(t=new s(this,l.uri),this.groups.push(t)),t.children.length===0||a._compareReferences(l,t.children[t.children.length-1])!==0){const h=new c(n===l,t,l.uri,l.targetSelectionRange||l.range,m=>this._onDidChangeReferenceRange.fire(m));this.references.push(h),t.children.push(h)}}dispose(){w.dispose(this.groups),this._onDidChangeReferenceRange.dispose(),this.groups.length=0}clone(){return new a(this._links,this._title)}get title(){return this._title}get isEmpty(){return this.groups.length===0}get ariaMessage(){return this.isEmpty?b.localize(4,null):this.references.length===1?b.localize(5,null,this.references[0].uri.fsPath):this.groups.length===1?b.localize(6,null,this.references.length,this.groups[0].uri.fsPath):b.localize(7,null,this.references.length,this.groups.length)}nextOrPreviousReference(r,i){let{parent:n}=r,t=n.children.indexOf(r),l=n.children.length,h=n.parent.groups.length;return h===1||i&&t+10?(i?t=(t+1)%l:t=(t+l-1)%l,n.children[t]):(t=n.parent.groups.indexOf(n),i?(t=(t+1)%h,n.parent.groups[t].children[0]):(t=(t+h-1)%h,n.parent.groups[t].children[n.parent.groups[t].children.length-1]))}nearestReference(r,i){const n=this.references.map((t,l)=>({idx:l,prefixLen:S.commonPrefixLength(t.uri.toString(),r.toString()),offsetDist:Math.abs(t.range.startLineNumber-i.lineNumber)*100+Math.abs(t.range.startColumn-i.column)})).sort((t,l)=>t.prefixLen>l.prefixLen?-1:t.prefixLenl.offsetDist?1:0)[0];if(n)return this.references[n.idx]}referenceAt(r,i){for(const n of this.references)if(n.uri.toString()===r.toString()&&d.Range.containsPosition(n.range,i))return n}firstReference(){for(const r of this.references)if(r.isProviderFirst)return r;return this.references[0]}static _compareReferences(r,i){return M.extUri.compare(r.uri,i.uri)||d.Range.compareRangesUsingStarts(r.range,i.range)}}e.ReferencesModel=a}),define(Q[487],J([4,5]),function(q,e){return q.create("vs/editor/contrib/gotoSymbol/symbolNavigation",e)}),define(Q[488],J([4,5]),function(q,e){return q.create("vs/editor/contrib/hover/hover",e)}),define(Q[489],J([4,5]),function(q,e){return q.create("vs/editor/contrib/hover/markdownHoverParticipant",e)}),define(Q[490],J([4,5]),function(q,e){return q.create("vs/editor/contrib/hover/markerHoverParticipant",e)}),define(Q[491],J([4,5]),function(q,e){return q.create("vs/editor/contrib/inPlaceReplace/inPlaceReplace",e)}),define(Q[492],J([4,5]),function(q,e){return q.create("vs/editor/contrib/indentation/indentation",e)}),define(Q[493],J([4,5]),function(q,e){return q.create("vs/editor/contrib/linesOperations/linesOperations",e)}),define(Q[494],J([4,5]),function(q,e){return q.create("vs/editor/contrib/linkedEditing/linkedEditing",e)}),define(Q[495],J([4,5]),function(q,e){return q.create("vs/editor/contrib/links/links",e)}),define(Q[496],J([4,5]),function(q,e){return q.create("vs/editor/contrib/message/messageController",e)}),define(Q[497],J([4,5]),function(q,e){return q.create("vs/editor/contrib/multicursor/multicursor",e)}),define(Q[498],J([4,5]),function(q,e){return q.create("vs/editor/contrib/parameterHints/parameterHints",e)}),define(Q[499],J([4,5]),function(q,e){return q.create("vs/editor/contrib/parameterHints/parameterHintsWidget",e)}),define(Q[500],J([4,5]),function(q,e){return q.create("vs/editor/contrib/peekView/peekView",e)}),define(Q[501],J([4,5]),function(q,e){return q.create("vs/editor/contrib/quickAccess/gotoLineQuickAccess",e)}),define(Q[502],J([4,5]),function(q,e){return q.create("vs/editor/contrib/quickAccess/gotoSymbolQuickAccess",e)}),define(Q[503],J([4,5]),function(q,e){return q.create("vs/editor/contrib/rename/rename",e)}),define(Q[504],J([4,5]),function(q,e){return q.create("vs/editor/contrib/rename/renameInputField",e)}),define(Q[505],J([4,5]),function(q,e){return q.create("vs/editor/contrib/smartSelect/smartSelect",e)}),define(Q[506],J([4,5]),function(q,e){return q.create("vs/editor/contrib/snippet/snippetVariables",e)}),define(Q[507],J([4,5]),function(q,e){return q.create("vs/editor/contrib/suggest/suggestController",e)}),define(Q[508],J([4,5]),function(q,e){return q.create("vs/editor/contrib/suggest/suggestWidget",e)}),define(Q[509],J([4,5]),function(q,e){return q.create("vs/editor/contrib/suggest/suggestWidgetDetails",e)}),define(Q[510],J([4,5]),function(q,e){return q.create("vs/editor/contrib/suggest/suggestWidgetRenderer",e)}),define(Q[511],J([4,5]),function(q,e){return q.create("vs/editor/contrib/suggest/suggestWidgetStatus",e)}),define(Q[512],J([4,5]),function(q,e){return q.create("vs/editor/contrib/symbolIcons/symbolIcons",e)}),define(Q[513],J([4,5]),function(q,e){return q.create("vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode",e)}),define(Q[514],J([4,5]),function(q,e){return q.create("vs/editor/contrib/tokenization/tokenization",e)}),define(Q[515],J([4,5]),function(q,e){return q.create("vs/editor/contrib/unusualLineTerminators/unusualLineTerminators",e)}),define(Q[516],J([4,5]),function(q,e){return q.create("vs/editor/contrib/wordHighlighter/wordHighlighter",e)}),define(Q[517],J([4,5]),function(q,e){return q.create("vs/editor/contrib/wordOperations/wordOperations",e)}),define(Q[518],J([4,5]),function(q,e){return q.create("vs/platform/actions/browser/menuEntryActionViewItem",e)}),define(Q[519],J([4,5]),function(q,e){return q.create("vs/platform/configuration/common/configurationRegistry",e)}),define(Q[520],J([4,5]),function(q,e){return q.create("vs/platform/contextkey/browser/contextKeyService",e)}),define(Q[521],J([4,5]),function(q,e){return q.create("vs/platform/contextkey/common/contextkeys",e)}),define(Q[522],J([4,5]),function(q,e){return q.create("vs/platform/keybinding/common/abstractKeybindingService",e)}),define(Q[523],J([4,5]),function(q,e){return q.create("vs/platform/list/browser/listService",e)}),define(Q[524],J([4,5]),function(q,e){return q.create("vs/platform/markers/common/markers",e)}),define(Q[525],J([4,5]),function(q,e){return q.create("vs/platform/quickinput/browser/commandsQuickAccess",e)}),define(Q[526],J([4,5]),function(q,e){return q.create("vs/platform/quickinput/browser/helpQuickAccess",e)}),define(Q[527],J([4,5]),function(q,e){return q.create("vs/platform/theme/common/colorRegistry",e)}),define(Q[528],J([4,5]),function(q,e){return q.create("vs/platform/theme/common/iconRegistry",e)}),define(Q[529],J([4,5]),function(q,e){return q.create("vs/platform/undoRedo/common/undoRedoService",e)}),define(Q[530],J([0,1,7]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.BrowserClipboardService=void 0;class N{constructor(){this.mapTextToType=new Map,this.findText=""}writeText(w,S){return Ie(this,void 0,void 0,function*(){if(S){this.mapTextToType.set(S,w);return}try{return yield navigator.clipboard.writeText(w)}catch(g){console.error(g)}const C=document.activeElement,d=document.body.appendChild(b.$("textarea",{"aria-hidden":!0}));d.style.height="1px",d.style.width="1px",d.style.position="absolute",d.value=w,d.focus(),d.select(),document.execCommand("copy"),C instanceof HTMLElement&&C.focus(),document.body.removeChild(d)})}readText(w){return Ie(this,void 0,void 0,function*(){if(w)return this.mapTextToType.get(w)||"";try{return yield navigator.clipboard.readText()}catch(S){return console.error(S),""}})}readFindText(){return Ie(this,void 0,void 0,function*(){return this.findText})}writeFindText(w){return Ie(this,void 0,void 0,function*(){this.findText=w})}}e.BrowserClipboardService=N}),define(Q[531],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EditorOpenContext=void 0;var b;(function(N){N[N.API=0]="API",N[N.USER=1]="USER"})(b=e.EditorOpenContext||(e.EditorOpenContext={}))}),define(Q[532],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ExtensionIdentifier=void 0;class b{constructor(M){this.value=M,this._lower=M.toLowerCase()}static toKey(M){return typeof M=="string"?M.toLowerCase():M._lower}}e.ExtensionIdentifier=b}),define(Q[237],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FileKind=void 0;var b;(function(N){N[N.FILE=0]="FILE",N[N.FOLDER=1]="FOLDER",N[N.ROOT_FOLDER=2]="ROOT_FOLDER"})(b=e.FileKind||(e.FileKind={}))}),define(Q[238],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SyncDescriptor=void 0;class b{constructor(M,w=[],S=!1){this.ctor=M,this.staticArguments=w,this.supportsDelayedInstantiation=S}}e.SyncDescriptor=b}),define(Q[74],J([0,1,238]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getSingletonServiceDescriptors=e.registerSingleton=void 0;const N=[];function M(S,C,d){C instanceof b.SyncDescriptor||(C=new b.SyncDescriptor(C,[],d)),N.push([S,C])}e.registerSingleton=M;function w(){return N}e.getSingletonServiceDescriptors=w}),define(Q[533],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Graph=e.Node=void 0;class b{constructor(w){this.incoming=new Map,this.outgoing=new Map,this.data=w}}e.Node=b;class N{constructor(w){this._hashFn=w,this._nodes=new Map}roots(){const w=[];for(let S of this._nodes.values())S.outgoing.size===0&&w.push(S);return w}insertEdge(w,S){const C=this.lookupOrInsertNode(w),d=this.lookupOrInsertNode(S);C.outgoing.set(this._hashFn(S),d),d.incoming.set(this._hashFn(w),C)}removeNode(w){const S=this._hashFn(w);this._nodes.delete(S);for(let C of this._nodes.values())C.outgoing.delete(S),C.incoming.delete(S)}lookupOrInsertNode(w){const S=this._hashFn(w);let C=this._nodes.get(S);return C||(C=new b(w),this._nodes.set(S,C)),C}isEmpty(){return this._nodes.size===0}toString(){let w=[];for(let[S,C]of this._nodes)w.push(`${S}, (incoming)[${[...C.incoming.keys()].join(", ")}], (outgoing)[${[...C.outgoing.keys()].join(",")}]`);return w.join(` +`)}}e.Graph=N}),define(Q[9],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.optional=e.createDecorator=e.IInstantiationService=e._util=void 0;var b;(function(S){S.serviceIds=new Map,S.DI_TARGET="$di$target",S.DI_DEPENDENCIES="$di$dependencies";function C(d){return d[S.DI_DEPENDENCIES]||[]}S.getServiceDependencies=C})(b=e._util||(e._util={})),e.IInstantiationService=M("instantiationService");function N(S,C,d,g){C[b.DI_TARGET]===C?C[b.DI_DEPENDENCIES].push({id:S,index:d,optional:g}):(C[b.DI_DEPENDENCIES]=[{id:S,index:d,optional:g}],C[b.DI_TARGET]=C)}function M(S){if(b.serviceIds.has(S))return b.serviceIds.get(S);const C=function(d,g,p){if(arguments.length!==3)throw new Error("@IServiceName-decorator can only be used to decorate a parameter");N(C,d,p,!1)};return C.toString=()=>S,b.serviceIds.set(S,C),C}e.createDecorator=M;function w(S){return function(C,d,g){if(arguments.length!==3)throw new Error("@optional-decorator can only be used to decorate a parameter");N(S,C,g,!0)}}e.optional=w}),define(Q[133],J([0,1,9,24,20]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ResourceFileEdit=e.ResourceTextEdit=e.ResourceEdit=e.IBulkEditService=void 0,e.IBulkEditService=b.createDecorator("IWorkspaceEditService");function w(p){return M.isObject(p)&&(Boolean(p.newUri)||Boolean(p.oldUri))}function S(p){return M.isObject(p)&&N.URI.isUri(p.resource)&&M.isObject(p.edit)}class C{constructor(c){this.metadata=c}static convert(c){return c.edits.map(o=>{if(S(o))return new d(o.resource,o.edit,o.modelVersionId,o.metadata);if(w(o))return new g(o.oldUri,o.newUri,o.options,o.metadata);throw new Error("Unsupported edit")})}}e.ResourceEdit=C;class d extends C{constructor(c,o,s,a){super(a);this.resource=c,this.textEdit=o,this.versionId=s,this.metadata=a}}e.ResourceTextEdit=d;class g extends C{constructor(c,o,s,a){super(a);this.oldResource=c,this.newResource=o,this.options=s,this.metadata=a}}e.ResourceFileEdit=g}),define(Q[28],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ICodeEditorService=void 0,e.ICodeEditorService=b.createDecorator("codeEditorService")}),define(Q[75],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IEditorWorkerService=e.ID_EDITOR_WORKER_SERVICE=void 0,e.ID_EDITOR_WORKER_SERVICE="editorWorkerService",e.IEditorWorkerService=b.createDecorator(e.ID_EDITOR_WORKER_SERVICE)}),define(Q[178],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IMarkerDecorationsService=void 0,e.IMarkerDecorationsService=b.createDecorator("markerDecorationsService")}),define(Q[57],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IModeService=void 0,e.IModeService=b.createDecorator("modeService")}),define(Q[36],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.shouldSynchronizeModel=e.IModelService=void 0,e.IModelService=b.createDecorator("modelService");function N(M){return!M.isTooLargeForSyncing()&&!M.isForSimpleWidget}e.shouldSynchronizeModel=N}),define(Q[134],J([0,1,6,89,2,51,100,374,36]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LanguageFeatureRequestDelays=e.LanguageFeatureRegistry=void 0;function g(o){return typeof o=="string"?!1:Array.isArray(o)?o.every(g):!!o.exclusive}class p{constructor(){this._clock=0,this._entries=[],this._onDidChange=new b.Emitter}get onDidChange(){return this._onDidChange.event}register(s,a){let u={selector:s,provider:a,_score:-1,_time:this._clock++};return this._entries.push(u),this._lastCandidate=void 0,this._onDidChange.fire(this._entries.length),M.toDisposable(()=>{if(u){let r=this._entries.indexOf(u);r>=0&&(this._entries.splice(r,1),this._lastCandidate=void 0,this._onDidChange.fire(this._entries.length),u=void 0)}})}has(s){return this.all(s).length>0}all(s){if(!s)return[];this._updateScores(s);const a=[];for(let u of this._entries)u._score>0&&a.push(u.provider);return a}ordered(s){const a=[];return this._orderedForEach(s,u=>a.push(u.provider)),a}orderedGroups(s){const a=[];let u,r;return this._orderedForEach(s,i=>{u&&r===i._score?u.push(i.provider):(r=i._score,u=[i.provider],a.push(u))}),a}_orderedForEach(s,a){if(!!s){this._updateScores(s);for(const u of this._entries)u._score>0&&a(u)}}_updateScores(s){let a={uri:s.uri.toString(),language:s.getLanguageIdentifier().language};if(!(this._lastCandidate&&this._lastCandidate.language===a.language&&this._lastCandidate.uri===a.uri)){this._lastCandidate=a;for(let u of this._entries)if(u._score=C.score(u.selector,s.uri,s.getLanguageIdentifier().language,d.shouldSynchronizeModel(s)),g(u.selector)&&u._score>0){for(let r of this._entries)r._score=0;u._score=1e3;break}this._entries.sort(p._compareByScoreAndTime)}}static _compareByScoreAndTime(s,a){return s._scorea._score?-1:s._timea._time?-1:0}}e.LanguageFeatureRegistry=p;class c{constructor(s,a,u=Number.MAX_SAFE_INTEGER){this._registry=s,this.min=a,this.max=u,this._cache=new w.LRUCache(50,.7)}_key(s){return s.id+N.hash(this._registry.all(s))}_clamp(s){return s===void 0?this.min:Math.min(this.max,Math.max(this.min,Math.floor(s*1.3)))}get(s){const a=this._key(s),u=this._cache.get(a);return this._clamp(u==null?void 0:u.value)}update(s,a){const u=this._key(s);let r=this._cache.get(u);return r||(r=new S.MovingAverage,this._cache.set(u,r)),r.update(a),this.get(s)}}e.LanguageFeatureRequestDelays=c}),define(Q[18],J([0,1,24,3,134,382,27]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokenizationRegistry=e.DocumentRangeSemanticTokensProviderRegistry=e.DocumentSemanticTokensProviderRegistry=e.FoldingRangeProviderRegistry=e.SelectionRangeRegistry=e.ColorProviderRegistry=e.LinkProviderRegistry=e.OnTypeFormattingEditProviderRegistry=e.DocumentRangeFormattingEditProviderRegistry=e.DocumentFormattingEditProviderRegistry=e.CodeActionProviderRegistry=e.InlineHintsProviderRegistry=e.CodeLensProviderRegistry=e.TypeDefinitionProviderRegistry=e.ImplementationProviderRegistry=e.DeclarationProviderRegistry=e.DefinitionProviderRegistry=e.LinkedEditingRangeProviderRegistry=e.DocumentHighlightProviderRegistry=e.DocumentSymbolProviderRegistry=e.HoverProviderRegistry=e.SignatureHelpProviderRegistry=e.CompletionProviderRegistry=e.RenameProviderRegistry=e.ReferenceProviderRegistry=e.InlineHintKind=e.FoldingRangeKind=e.SymbolKinds=e.isLocationLink=e.DocumentHighlightKind=e.SignatureHelpTriggerKind=e.completionKindFromString=e.completionKindToCssClass=e.TokenMetadata=e.LanguageIdentifier=void 0;class C{constructor(r,i){this.language=r,this.id=i}}e.LanguageIdentifier=C;class d{static getLanguageId(r){return(r&255)>>>0}static getTokenType(r){return(r&1792)>>>8}static getFontStyle(r){return(r&14336)>>>11}static getForeground(r){return(r&8372224)>>>14}static getBackground(r){return(r&4286578688)>>>23}static getClassNameFromMetadata(r){let i=this.getForeground(r),n="mtk"+i,t=this.getFontStyle(r);return t&1&&(n+=" mtki"),t&2&&(n+=" mtkb"),t&4&&(n+=" mtku"),n}static getInlineStyleFromMetadata(r,i){const n=this.getForeground(r),t=this.getFontStyle(r);let l=`color: ${i[n]};`;return t&1&&(l+="font-style: italic;"),t&2&&(l+="font-weight: bold;"),t&4&&(l+="text-decoration: underline;"),l}}e.TokenMetadata=d,e.completionKindToCssClass=function(){let u=Object.create(null);return u[0]="symbol-method",u[1]="symbol-function",u[2]="symbol-constructor",u[3]="symbol-field",u[4]="symbol-variable",u[5]="symbol-class",u[6]="symbol-struct",u[7]="symbol-interface",u[8]="symbol-module",u[9]="symbol-property",u[10]="symbol-event",u[11]="symbol-operator",u[12]="symbol-unit",u[13]="symbol-value",u[14]="symbol-constant",u[15]="symbol-enum",u[16]="symbol-enum-member",u[17]="symbol-keyword",u[27]="symbol-snippet",u[18]="symbol-text",u[19]="symbol-color",u[20]="symbol-file",u[21]="symbol-reference",u[22]="symbol-customcolor",u[23]="symbol-folder",u[24]="symbol-type-parameter",u[25]="account",u[26]="issues",function(r){const i=u[r];let n=i&&S.iconRegistry.get(i);return n||(console.info("No codicon found for CompletionItemKind "+r),n=S.Codicon.symbolProperty),n.classNames}}(),e.completionKindFromString=function(){let u=Object.create(null);return u.method=0,u.function=1,u.constructor=2,u.field=3,u.variable=4,u.class=5,u.struct=6,u.interface=7,u.module=8,u.property=9,u.event=10,u.operator=11,u.unit=12,u.value=13,u.constant=14,u.enum=15,u["enum-member"]=16,u.enumMember=16,u.keyword=17,u.snippet=27,u.text=18,u.color=19,u.file=20,u.reference=21,u.customcolor=22,u.folder=23,u["type-parameter"]=24,u.typeParameter=24,u.account=25,u.issue=26,function(r,i){let n=u[r];return typeof n=="undefined"&&!i&&(n=9),n}}();var g;(function(u){u[u.Invoke=1]="Invoke",u[u.TriggerCharacter=2]="TriggerCharacter",u[u.ContentChange=3]="ContentChange"})(g=e.SignatureHelpTriggerKind||(e.SignatureHelpTriggerKind={}));var p;(function(u){u[u.Text=0]="Text",u[u.Read=1]="Read",u[u.Write=2]="Write"})(p=e.DocumentHighlightKind||(e.DocumentHighlightKind={}));function c(u){return u&&b.URI.isUri(u.uri)&&N.Range.isIRange(u.range)&&(N.Range.isIRange(u.originSelectionRange)||N.Range.isIRange(u.targetSelectionRange))}e.isLocationLink=c;var o;(function(u){const r=new Map;r.set("file",0),r.set("module",1),r.set("namespace",2),r.set("package",3),r.set("class",4),r.set("method",5),r.set("property",6),r.set("field",7),r.set("constructor",8),r.set("enum",9),r.set("interface",10),r.set("function",11),r.set("variable",12),r.set("constant",13),r.set("string",14),r.set("number",15),r.set("boolean",16),r.set("array",17),r.set("object",18),r.set("key",19),r.set("null",20),r.set("enum-member",21),r.set("struct",22),r.set("event",23),r.set("operator",24),r.set("type-parameter",25);const i=new Map;i.set(0,"file"),i.set(1,"module"),i.set(2,"namespace"),i.set(3,"package"),i.set(4,"class"),i.set(5,"method"),i.set(6,"property"),i.set(7,"field"),i.set(8,"constructor"),i.set(9,"enum"),i.set(10,"interface"),i.set(11,"function"),i.set(12,"variable"),i.set(13,"constant"),i.set(14,"string"),i.set(15,"number"),i.set(16,"boolean"),i.set(17,"array"),i.set(18,"object"),i.set(19,"key"),i.set(20,"null"),i.set(21,"enum-member"),i.set(22,"struct"),i.set(23,"event"),i.set(24,"operator"),i.set(25,"type-parameter");function n(h){return r.get(h)}u.fromString=n;function t(h){return i.get(h)}u.toString=t;function l(h,m){const _=i.get(h);let f=_&&S.iconRegistry.get("symbol-"+_);return f||(console.info("No codicon found for SymbolKind "+h),f=S.Codicon.symbolProperty),`${m?"inline":"block"} ${f.classNames}`}u.toCssClassName=l})(o=e.SymbolKinds||(e.SymbolKinds={}));class s{constructor(r){this.value=r}}e.FoldingRangeKind=s,s.Comment=new s("comment"),s.Imports=new s("imports"),s.Region=new s("region");var a;(function(u){u[u.Other=0]="Other",u[u.Type=1]="Type",u[u.Parameter=2]="Parameter"})(a=e.InlineHintKind||(e.InlineHintKind={})),e.ReferenceProviderRegistry=new M.LanguageFeatureRegistry,e.RenameProviderRegistry=new M.LanguageFeatureRegistry,e.CompletionProviderRegistry=new M.LanguageFeatureRegistry,e.SignatureHelpProviderRegistry=new M.LanguageFeatureRegistry,e.HoverProviderRegistry=new M.LanguageFeatureRegistry,e.DocumentSymbolProviderRegistry=new M.LanguageFeatureRegistry,e.DocumentHighlightProviderRegistry=new M.LanguageFeatureRegistry,e.LinkedEditingRangeProviderRegistry=new M.LanguageFeatureRegistry,e.DefinitionProviderRegistry=new M.LanguageFeatureRegistry,e.DeclarationProviderRegistry=new M.LanguageFeatureRegistry,e.ImplementationProviderRegistry=new M.LanguageFeatureRegistry,e.TypeDefinitionProviderRegistry=new M.LanguageFeatureRegistry,e.CodeLensProviderRegistry=new M.LanguageFeatureRegistry,e.InlineHintsProviderRegistry=new M.LanguageFeatureRegistry,e.CodeActionProviderRegistry=new M.LanguageFeatureRegistry,e.DocumentFormattingEditProviderRegistry=new M.LanguageFeatureRegistry,e.DocumentRangeFormattingEditProviderRegistry=new M.LanguageFeatureRegistry,e.OnTypeFormattingEditProviderRegistry=new M.LanguageFeatureRegistry,e.LinkProviderRegistry=new M.LanguageFeatureRegistry,e.ColorProviderRegistry=new M.LanguageFeatureRegistry,e.SelectionRangeRegistry=new M.LanguageFeatureRegistry,e.FoldingRangeProviderRegistry=new M.LanguageFeatureRegistry,e.DocumentSemanticTokensProviderRegistry=new M.LanguageFeatureRegistry,e.DocumentRangeSemanticTokensProviderRegistry=new M.LanguageFeatureRegistry,e.TokenizationRegistry=new w.TokenizationRegistryImpl}),define(Q[113],J([0,1,18]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SlicedLineTokens=e.LineTokens=void 0;class N{constructor(S,C){this._tokens=S,this._tokensCount=this._tokens.length>>>1,this._text=C}equals(S){return S instanceof N?this.slicedEquals(S,0,this._tokensCount):!1}slicedEquals(S,C,d){if(this._text!==S._text||this._tokensCount!==S._tokensCount)return!1;const g=C<<1,p=g+(d<<1);for(let c=g;c0?this._tokens[S-1<<1]:0}getMetadata(S){return this._tokens[(S<<1)+1]}getLanguageId(S){const C=this._tokens[(S<<1)+1];return b.TokenMetadata.getLanguageId(C)}getStandardTokenType(S){const C=this._tokens[(S<<1)+1];return b.TokenMetadata.getTokenType(C)}getForeground(S){const C=this._tokens[(S<<1)+1];return b.TokenMetadata.getForeground(C)}getClassName(S){const C=this._tokens[(S<<1)+1];return b.TokenMetadata.getClassNameFromMetadata(C)}getInlineStyle(S,C){const d=this._tokens[(S<<1)+1];return b.TokenMetadata.getInlineStyleFromMetadata(d,C)}getEndOffset(S){return this._tokens[S<<1]}findTokenIndexAtOffset(S){return N.findIndexInTokensArray(this._tokens,S)}inflate(){return this}sliceAndInflate(S,C,d){return new M(this,S,C,d)}static convertToEndOffset(S,C){const g=(S.length>>>1)-1;for(let p=0;p>>1)-1;for(;dC&&(g=p)}return d}}e.LineTokens=N;class M{constructor(S,C,d,g){this._source=S,this._startOffset=C,this._endOffset=d,this._deltaOffset=g,this._firstTokenIndex=S.findTokenIndexAtOffset(C),this._tokensCount=0;for(let p=this._firstTokenIndex,c=S.getCount();p=d);p++)this._tokensCount++}equals(S){return S instanceof M?this._startOffset===S._startOffset&&this._endOffset===S._endOffset&&this._deltaOffset===S._deltaOffset&&this._source.slicedEquals(S._source,this._firstTokenIndex,this._tokensCount):!1}getCount(){return this._tokensCount}getForeground(S){return this._source.getForeground(this._firstTokenIndex+S)}getEndOffset(S){const C=this._source.getEndOffset(this._firstTokenIndex+S);return Math.min(this._endOffset,C)-this._startOffset+this._deltaOffset}getClassName(S){return this._source.getClassName(this._firstTokenIndex+S)}getInlineStyle(S,C){return this._source.getInlineStyle(this._firstTokenIndex+S,C)}findTokenIndexAtOffset(S){return this._source.findTokenIndexAtOffset(S+this._startOffset-this._deltaOffset)-this._firstTokenIndex}}e.SlicedLineTokens=M}),define(Q[135],J([0,1,19,113,14,3,18]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TokensStore=e.TokensStore2=e.MultilineTokens=e.MultilineTokens2=e.LineTokens2=e.SparseEncodedTokens=e.MultilineTokensBuilder=e.countEOL=void 0;function C(n){let t=0,l=0,h=0,m=0;for(let _=0,f=n.length;_>>0}const g=new Uint32Array(0).buffer;class p{constructor(){this.tokens=[]}add(t,l){if(this.tokens.length>0){const h=this.tokens[this.tokens.length-1];if(h.startLineNumber+h.tokens.length-1+1===t){h.tokens.push(l);return}}this.tokens.push(new a(t,[l]))}}e.MultilineTokensBuilder=p;class c{constructor(t){this._tokens=t,this._tokenCount=t.length/4}toString(t){let l=[];for(let h=0;ht)h=m-1;else{let f=m;for(;f>l&&this._getDeltaLine(f-1)===t;)f--;let v=m;for(;vt||E===t&&O>=l)&&(Et||O===t&&B>=l){if(O_?A-=_-h:A=h;else if(T===l&&O===h)if(T===m&&A>_)A-=_-h;else{I=!0;continue}else if(T_)T===l?(O=h,A=O+(A-_)):(O=0,A=O+(A-_));else{I=!0;continue}else if(T>m){if(y===0&&!I){L=v;break}T-=y}else if(T===m&&O>=_)t&&T===0&&(O+=t,A+=t),T-=y,O-=_-h,A-=_-h;else throw new Error("Not possible!");const F=4*L;f[F]=T,f[F+1]=O,f[F+2]=A,f[F+3]=B,L++}this._tokenCount=L}acceptInsertText(t,l,h,m,_,f){const v=h===0&&m===1&&(f>=48&&f<=57||f>=65&&f<=90||f>=97&&f<=122),y=this._tokens,L=this._tokenCount;for(let I=0;I0?l.charCodeAt(0):0)}acceptEdit(t,l,h,m,_){this._acceptDeleteRange(t),this._acceptInsertText(new M.Position(t.startLineNumber,t.startColumn),l,h,m,_),this._updateEndLineNumber()}_acceptDeleteRange(t){if(!(t.startLineNumber===t.endLineNumber&&t.startColumn===t.endColumn)){const l=t.startLineNumber-this.startLineNumber,h=t.endLineNumber-this.startLineNumber;if(h<0){const _=h-l;this.startLineNumber-=_;return}const m=this.tokens.getMaxDeltaLine();if(!(l>=m+1)){if(l<0&&h>=m+1){this.startLineNumber=0,this.tokens.clear();return}if(l<0){const _=-l;this.startLineNumber-=_,this.tokens.acceptDeleteRange(t.startColumn-1,0,0,h,t.endColumn-1)}else this.tokens.acceptDeleteRange(0,l,t.startColumn-1,h,t.endColumn-1)}}}_acceptInsertText(t,l,h,m,_){if(!(l===0&&h===0)){const f=t.lineNumber-this.startLineNumber;if(f<0){this.startLineNumber+=l;return}const v=this.tokens.getMaxDeltaLine();f>=v+1||this.tokens.acceptInsertText(f,t.column-1,l,h,m,_)}}}e.MultilineTokens2=s;class a{constructor(t,l){this.startLineNumber=t,this.tokens=l}}e.MultilineTokens=a;function u(n){return n instanceof Uint32Array?n:new Uint32Array(n)}class r{constructor(){this._pieces=[],this._isComplete=!1}flush(){this._pieces=[],this._isComplete=!1}isEmpty(){return this._pieces.length===0}set(t,l){this._pieces=t||[],this._isComplete=l}setPartial(t,l){let h=t;if(l.length>0){const _=l[0].getRange(),f=l[l.length-1].getRange();if(!_||!f)return t;h=t.plusRange(_).plusRange(f)}let m=null;for(let _=0,f=this._pieces.length;_h.endLineNumber){m=m||{index:_};break}if(v.removeTokens(h),v.isEmpty()){this._pieces.splice(_,1),_--,f--;continue}if(!(v.endLineNumberh.endLineNumber){m=m||{index:_};continue}const[y,L]=v.split(h);if(y.isEmpty()){m=m||{index:_};continue}L.isEmpty()||(this._pieces.splice(_,1,y,L),_++,f++,m=m||{index:_})}}}return m=m||{index:this._pieces.length},l.length>0&&(this._pieces=b.arrayInsert(this._pieces,m.index,l)),h}isComplete(){return this._isComplete}addSemanticTokens(t,l){const h=this._pieces;if(h.length===0)return l;const m=r._findFirstPieceWithLine(h,t),_=h[m].getLineTokens(t);if(!_)return l;const f=l.getCount(),v=_.getCount();let y=0,L=[],I=0,k=0;const E=(T,O)=>{T!==k&&(k=T,L[I++]=T,L[I++]=O)};for(let T=0;T>>0,D=~F>>>0;for(;yl)m=_-1;else{for(;_>h&&t[_-1].startLineNumber<=l&&l<=t[_-1].endLineNumber;)_--;return _}}return h}acceptEdit(t,l,h,m,_){for(const f of this._pieces)f.acceptEdit(t,l,h,m,_)}}e.TokensStore2=r;class i{constructor(){this._lineTokens=[],this._len=0}flush(){this._lineTokens=[],this._len=0}getTokens(t,l,h){let m=null;if(l1&&(_=S.TokenMetadata.getLanguageId(m[1])!==t),!_)return g}if(!m||m.length===0){const _=new Uint32Array(2);return _[0]=l,_[1]=d(t),_.buffer}return m[m.length-2]=l,m.byteOffset===0&&m.byteLength===m.buffer.byteLength?m.buffer:m}_ensureLine(t){for(;t>=this._len;)this._lineTokens[this._len]=null,this._len++}_deleteLines(t,l){l!==0&&(t+l>this._len&&(l=this._len-t),this._lineTokens.splice(t,l),this._len-=l)}_insertLines(t,l){if(l!==0){let h=[];for(let m=0;m=this._len)){if(t.startLineNumber===t.endLineNumber){if(t.startColumn===t.endColumn)return;this._lineTokens[l]=i._delete(this._lineTokens[l],t.startColumn-1,t.endColumn-1);return}this._lineTokens[l]=i._deleteEnding(this._lineTokens[l],t.startColumn-1);const h=t.endLineNumber-1;let m=null;h=this._len)){if(l===0){this._lineTokens[m]=i._insert(this._lineTokens[m],t.column-1,h);return}this._lineTokens[m]=i._deleteEnding(this._lineTokens[m],t.column-1),this._lineTokens[m]=i._insert(this._lineTokens[m],t.column-1,h),this._insertLines(t.lineNumber,l)}}}static _deleteBeginning(t,l){return t===null||t===g?t:i._delete(t,0,l)}static _deleteEnding(t,l){if(t===null||t===g)return t;const h=u(t),m=h[h.length-2];return i._delete(t,l,m)}static _delete(t,l,h){if(t===null||t===g||l===h)return t;const m=u(t),_=m.length>>>1;if(l===0&&m[m.length-2]===h)return g;const f=N.LineTokens.findIndexInTokensArray(m,l),v=f>0?m[f-1<<1]:0,y=m[f<<1];if(hI&&(m[L++]=O,m[L++]=m[(T<<1)+1],I=O)}if(L===m.length)return t;let E=new Uint32Array(L);return E.set(m.subarray(0,L),0),E.buffer}static _append(t,l){if(l===g)return t;if(t===g)return l;if(t===null)return t;if(l===null)return null;const h=u(t),m=u(l),_=m.length>>>1;let f=new Uint32Array(h.length+m.length);f.set(h,0);let v=h.length;const y=h[h.length-2];for(let L=0;L<_;L++)f[v++]=m[L<<1]+y,f[v++]=m[(L<<1)+1];return f.buffer}static _insert(t,l,h){if(t===null||t===g)return t;const m=u(t),_=m.length>>>1;let f=N.LineTokens.findIndexInTokensArray(m,l);f>0&&m[f-1<<1]===l&&f--;for(let v=f;v<_;v++)m[v<<1]+=h;return t}}e.TokensStore=i}),define(Q[239],J([0,1,6,8,3,53,219,135,218,2]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PieceTreeTextBuffer=void 0;class p extends g.Disposable{constructor(o,s,a,u,r,i,n){super();this._onDidChangeContent=this._register(new b.Emitter),this._BOM=s,this._mightContainNonBasicASCII=!i,this._mightContainRTL=u,this._mightContainUnusualLineTerminators=r,this._pieceTree=new S.PieceTreeBase(o,a,n)}mightContainRTL(){return this._mightContainRTL}mightContainUnusualLineTerminators(){return this._mightContainUnusualLineTerminators}resetMightContainUnusualLineTerminators(){this._mightContainUnusualLineTerminators=!1}mightContainNonBasicASCII(){return this._mightContainNonBasicASCII}getBOM(){return this._BOM}getEOL(){return this._pieceTree.getEOL()}createSnapshot(o){return this._pieceTree.createSnapshot(o?this._BOM:"")}getOffsetAt(o,s){return this._pieceTree.getOffsetAt(o,s)}getPositionAt(o){return this._pieceTree.getPositionAt(o)}getRangeAt(o,s){let a=o+s;const u=this.getPositionAt(o),r=this.getPositionAt(a);return new M.Range(u.lineNumber,u.column,r.lineNumber,r.column)}getValueInRange(o,s=0){if(o.isEmpty())return"";const a=this._getEndOfLine(s);return this._pieceTree.getValueInRange(o,a)}getValueLengthInRange(o,s=0){if(o.isEmpty())return 0;if(o.startLineNumber===o.endLineNumber)return o.endColumn-o.startColumn;let a=this.getOffsetAt(o.startLineNumber,o.startColumn);return this.getOffsetAt(o.endLineNumber,o.endColumn)-a}getCharacterCountInRange(o,s=0){if(this._mightContainNonBasicASCII){let a=0;const u=o.startLineNumber,r=o.endLineNumber;for(let i=u;i<=r;i++){const n=this.getLineContent(i),t=i===u?o.startColumn-1:0,l=i===r?o.endColumn-1:n.length;for(let h=t;hL.sortIndex-I.sortIndex)}this._mightContainRTL=u,this._mightContainUnusualLineTerminators=r,this._mightContainNonBasicASCII=i;const f=this._doApplyEdits(t);let v=null;if(s&&m.length>0){m.sort((y,L)=>L.lineNumber-y.lineNumber),v=[];for(let y=0,L=m.length;y0&&m[y-1].lineNumber===I)){let k=m[y].oldContent,E=this.getLineContent(I);E.length===0||E===k||N.firstNonWhitespaceIndex(E)!==-1||v.push(I)}}}return this._onDidChangeContent.fire(),new w.ApplyEditsResult(_,f,v)}_reduceOperations(o){return o.length<1e3?o:[this._toSingleEditOperation(o)]}_toSingleEditOperation(o){let s=!1;const a=o[0].range,u=o[o.length-1].range,r=new M.Range(a.startLineNumber,a.startColumn,u.endLineNumber,u.endColumn);let i=a.startLineNumber,n=a.startColumn;const t=[];for(let f=0,v=o.length;f0&&t.push(y.text),i=L.endLineNumber,n=L.endColumn}const l=t.join(""),[h,m,_]=C.countEOL(l);return{sortIndex:0,identifier:o[0].identifier,range:r,rangeOffset:this.getOffsetAt(r.startLineNumber,r.startColumn),rangeLength:this.getValueLengthInRange(r,0),text:l,eolCount:h,firstLineLength:m,lastLineLength:_,forceMoveMarkers:s,isAutoWhitespaceEdit:!1}}_doApplyEdits(o){o.sort(p._sortOpsDescending);let s=[];for(let a=0;a0){const _=t.eolCount+1;_===1?m=new M.Range(l,h,l,h+t.firstLineLength):m=new M.Range(l,h,l+_-1,t.lastLineLength+1)}else m=new M.Range(l,h,l,h);a=m.endLineNumber,u=m.endColumn,s.push(m),r=t}return s}static _sortOpsAscending(o,s){let a=M.Range.compareRangesUsingEnds(o.range,s.range);return a===0?o.sortIndex-s.sortIndex:a}static _sortOpsDescending(o,s){let a=M.Range.compareRangesUsingEnds(o.range,s.range);return a===0?s.sortIndex-o.sortIndex:-a}}e.PieceTreeTextBuffer=p}),define(Q[534],J([0,1,8,219,239]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PieceTreeTextBufferBuilder=e.PieceTreeTextBufferFactory=void 0;class w{constructor(d,g,p,c,o,s,a,u,r){this._chunks=d,this._bom=g,this._cr=p,this._lf=c,this._crlf=o,this._containsRTL=s,this._containsUnusualLineTerminators=a,this._isBasicASCII=u,this._normalizeEOL=r}_getEOL(d){const g=this._cr+this._lf+this._crlf,p=this._cr+this._crlf;return g===0?d===1?` +`:`\r +`:p>g/2?`\r +`:` +`}create(d){const g=this._getEOL(d);let p=this._chunks;if(this._normalizeEOL&&(g===`\r +`&&(this._cr>0||this._lf>0)||g===` +`&&(this._cr>0||this._crlf>0)))for(let o=0,s=p.length;o=55296&&g<=56319?(this._acceptChunk1(d.substr(0,d.length-1),!1),this._hasPreviousChar=!0,this._previousChar=g):(this._acceptChunk1(d,!1),this._hasPreviousChar=!1,this._previousChar=g)}}_acceptChunk1(d,g){!g&&d.length===0||(this._hasPreviousChar?this._acceptChunk2(String.fromCharCode(this._previousChar)+d):this._acceptChunk2(d))}_acceptChunk2(d){const g=N.createLineStarts(this._tmpLineStarts,d);this.chunks.push(new N.StringBuffer(d,g.lineStarts)),this.cr+=g.cr,this.lf+=g.lf,this.crlf+=g.crlf,this.isBasicASCII&&(this.isBasicASCII=g.isBasicASCII),!this.isBasicASCII&&!this.containsRTL&&(this.containsRTL=b.containsRTL(d)),!this.isBasicASCII&&!this.containsUnusualLineTerminators&&(this.containsUnusualLineTerminators=b.containsUnusualLineTerminators(d))}finish(d=!0){return this._finish(),new w(this.chunks,this.BOM,this.cr,this.lf,this.crlf,this.containsRTL,this.containsUnusualLineTerminators,this.isBasicASCII,d)}_finish(){if(this.chunks.length===0&&this._acceptChunk1("",!0),this._hasPreviousChar){this._hasPreviousChar=!1;let d=this.chunks[this.chunks.length-1];d.buffer+=String.fromCharCode(this._previousChar);let g=N.createLineStartsFast(d.buffer);d.lineStarts=g,this._previousChar===13&&this.cr++}}}e.PieceTreeTextBufferBuilder=S}),define(Q[76],J([0,1,127,18]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.nullTokenize2=e.nullTokenize=e.NULL_LANGUAGE_IDENTIFIER=e.NULL_MODE_ID=e.NULL_STATE=void 0;class M{clone(){return this}equals(d){return this===d}}e.NULL_STATE=new M,e.NULL_MODE_ID="vs.editor.nullMode",e.NULL_LANGUAGE_IDENTIFIER=new N.LanguageIdentifier(e.NULL_MODE_ID,0);function w(C,d,g,p){return new b.TokenizationResult([new b.Token(p,"",C)],g)}e.nullTokenize=w;function S(C,d,g,p){let c=new Uint32Array(2);return c[0]=p,c[1]=(C<<0|0<<8|0<<11|1<<14|2<<23)>>>0,new b.TokenizationResult2(c,g===null?e.NULL_STATE:g)}e.nullTokenize2=S}),define(Q[535],J([0,1,19,12,113,14,18,76,2,81,135,17]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TextModelTokenization=e.TokenizationStateStore=void 0;class o{constructor(){this._beginState=[],this._valid=[],this._len=0,this._invalidLineStartIndex=0}_reset(i){this._beginState=[],this._valid=[],this._len=0,this._invalidLineStartIndex=0,i&&this._setBeginState(0,i)}flush(i){this._reset(i)}get invalidLineStartIndex(){return this._invalidLineStartIndex}_invalidateLine(i){i=this._len;)this._beginState[this._len]=null,this._valid[this._len]=!1,this._len++}_deleteLines(i,n){n!==0&&(i+n>this._len&&(n=this._len-i),this._beginState.splice(i,n),this._valid.splice(i,n),this._len-=n)}_insertLines(i,n){if(n!==0){let t=[],l=[];for(let h=0;h=0;m--)this._invalidateLine(i.startLineNumber+m-1);this._acceptDeleteRange(i),this._acceptInsertText(new w.Position(i.startLineNumber,i.startColumn),n)}_acceptDeleteRange(i){i.startLineNumber-1>=this._len||this._deleteLines(i.startLineNumber,i.endLineNumber-i.startLineNumber)}_acceptInsertText(i,n){i.lineNumber-1>=this._len||this._insertLines(i.lineNumber,n)}}e.TokenizationStateStore=o;class s extends d.Disposable{constructor(i){super();this._isDisposed=!1,this._textModel=i,this._tokenizationStateStore=new o,this._tokenizationSupport=null,this._register(S.TokenizationRegistry.onDidChange(n=>{const t=this._textModel.getLanguageIdentifier();n.changedLanguages.indexOf(t.language)!==-1&&(this._resetTokenizationState(),this._textModel.clearTokens())})),this._register(this._textModel.onDidChangeRawContentFast(n=>{if(n.containsEvent(1)){this._resetTokenizationState();return}})),this._register(this._textModel.onDidChangeContentFast(n=>{for(let t=0,l=n.changes.length;t{this._beginBackgroundTokenization()})),this._register(this._textModel.onDidChangeLanguage(()=>{this._resetTokenizationState(),this._textModel.clearTokens()})),this._resetTokenizationState()}dispose(){this._isDisposed=!0,super.dispose()}_resetTokenizationState(){const[i,n]=a(this._textModel);this._tokenizationSupport=i,this._tokenizationStateStore.flush(n),this._beginBackgroundTokenization()}_beginBackgroundTokenization(){this._textModel.isAttachedToEditor()&&this._hasLinesToTokenize()&&c.setImmediate(()=>{this._isDisposed||this._revalidateTokensNow()})}_revalidateTokensNow(i=this._textModel.getLineCount()){const n=1,t=new p.MultilineTokensBuilder,l=g.StopWatch.create(!1);for(;this._hasLinesToTokenize()&&!(l.elapsed()>n||this._tokenizeOneInvalidLine(t)>=i););this._beginBackgroundTokenization(),this._textModel.setTokens(t.tokens)}tokenizeViewport(i,n){const t=new p.MultilineTokensBuilder;this._tokenizeViewport(t,i,n),this._textModel.setTokens(t.tokens)}reset(){this._resetTokenizationState(),this._textModel.clearTokens()}forceTokenization(i){const n=new p.MultilineTokensBuilder;this._updateTokensUntilLine(n,i),this._textModel.setTokens(n.tokens)}isCheapToTokenize(i){if(!this._tokenizationSupport)return!0;const n=this._tokenizationStateStore.invalidLineStartIndex+1;return i>n?!1:i0&&v>=1;v--){let y=this._textModel.getLineFirstNonWhitespaceColumn(v);if(y!==0&&y=0;v--)f=u(_,this._tokenizationSupport,h[v],!1,f).endState;for(let v=n;v<=t;v++){let y=this._textModel.getLineContent(v),L=u(_,this._tokenizationSupport,y,!0,f);i.add(v,L.tokens),this._tokenizationStateStore.setFakeTokens(v-1),f=L.endState}}}}e.TextModelTokenization=s;function a(r){const i=r.getLanguageIdentifier();let n=r.isTooLargeForTokenization()?null:S.TokenizationRegistry.get(i.language),t=null;if(n)try{t=n.getInitialState()}catch(l){N.onUnexpectedError(l),n=null}return[n,t]}function u(r,i,n,t,l){let h=null;if(i)try{h=i.tokenize2(n,t,l.clone(),0)}catch(m){N.onUnexpectedError(m)}return h||(h=C.nullTokenize2(r.id,n,l,0)),M.LineTokens.convertToEndOffset(h.tokens,n.length),h}}),define(Q[31],J([0,1,12,6,2,8,24,38,14,3,21,53,236,369,370,534,372,166,535,128,41,76,167,168,135,29,239]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DidChangeContentEmitter=e.DidChangeDecorationsEmitter=e.ModelDecorationOptions=e.ModelDecorationMinimapOptions=e.ModelDecorationOverviewRulerOptions=e.TextModel=e.LONG_LINE_BOUNDARY=e.createTextBuffer=e.createTextBufferFactory=void 0;function L(){return new u.PieceTreeTextBufferBuilder}function I(z){const P=L();return P.acceptChunk(z),P.finish()}e.createTextBufferFactory=I;function k(z,P){return(typeof z=="string"?I(z):z).create(P)}e.createTextBuffer=k;let E=0;const T=999;e.LONG_LINE_BOUNDARY=1e4;class O{constructor(P){this._source=P,this._eos=!1}read(){if(this._eos)return null;let P=[],V=0,U=0;do{let H=this._source.read();if(H===null)return this._eos=!0,V===0?null:P.join("");if(H.length>0&&(P[V++]=H,U+=H.length),U>=64*1024)return P.join("")}while(!0)}}const A=()=>{throw new Error("Invalid change accessor")};class B{constructor(){this._searchCanceledBrand=void 0}}B.INSTANCE=new B;function F(z){return z instanceof B?null:z}class D extends M.Disposable{constructor(P,V,U,H=null,$){super();this._onWillDispose=this._register(new N.Emitter),this.onWillDispose=this._onWillDispose.event,this._onDidChangeDecorations=this._register(new le),this.onDidChangeDecorations=this._onDidChangeDecorations.event,this._onDidChangeLanguage=this._register(new N.Emitter),this.onDidChangeLanguage=this._onDidChangeLanguage.event,this._onDidChangeLanguageConfiguration=this._register(new N.Emitter),this.onDidChangeLanguageConfiguration=this._onDidChangeLanguageConfiguration.event,this._onDidChangeTokens=this._register(new N.Emitter),this.onDidChangeTokens=this._onDidChangeTokens.event,this._onDidChangeOptions=this._register(new N.Emitter),this.onDidChangeOptions=this._onDidChangeOptions.event,this._onDidChangeAttached=this._register(new N.Emitter),this.onDidChangeAttached=this._onDidChangeAttached.event,this._eventEmitter=this._register(new X),E++,this.id="$model"+E,this.isForSimpleWidget=V.isForSimpleWidget,typeof H=="undefined"||H===null?this._associatedResource=S.URI.parse("inmemory://model/"+E):this._associatedResource=H,this._undoRedoService=$,this._attachedEditorCount=0;const{textBuffer:ie,disposable:oe}=k(P,V.defaultEOL);this._buffer=ie,this._bufferDisposable=oe,this._options=D.resolveOptions(this._buffer,V);const ae=this._buffer.getLineCount(),G=this._buffer.getValueLengthInRange(new g.Range(1,1,ae,this._buffer.getLineLength(ae)+1),0);V.largeFileOptimizations?this._isTooLargeForTokenization=G>D.LARGE_FILE_SIZE_THRESHOLD||ae>D.LARGE_FILE_LINE_COUNT_THRESHOLD:this._isTooLargeForTokenization=!1,this._isTooLargeForSyncing=G>D.MODEL_SYNC_LIMIT,this._versionId=1,this._alternativeVersionId=1,this._initialUndoRedoSnapshot=null,this._isDisposed=!1,this._isDisposing=!1,this._languageIdentifier=U||h.NULL_LANGUAGE_IDENTIFIER,this._languageRegistryListener=l.LanguageConfigurationRegistry.onDidChange(j=>{j.languageIdentifier.id===this._languageIdentifier.id&&this._onDidChangeLanguageConfiguration.fire({})}),this._instanceId=w.singleLetterHash(E),this._lastDecorationId=0,this._decorations=Object.create(null),this._decorationsTree=new R,this._commandManager=new o.EditStack(this,$),this._isUndoing=!1,this._isRedoing=!1,this._trimAutoWhitespaceLines=null,this._tokens=new f.TokensStore,this._tokens2=new f.TokensStore2,this._tokenization=new n.TextModelTokenization(this)}static resolveOptions(P,V){if(V.detectIndentation){const U=s.guessIndentation(P,V.tabSize,V.insertSpaces);return new c.TextModelResolvedOptions({tabSize:U.tabSize,indentSize:U.tabSize,insertSpaces:U.insertSpaces,trimAutoWhitespace:V.trimAutoWhitespace,defaultEOL:V.defaultEOL})}return new c.TextModelResolvedOptions({tabSize:V.tabSize,indentSize:V.indentSize,insertSpaces:V.insertSpaces,trimAutoWhitespace:V.trimAutoWhitespace,defaultEOL:V.defaultEOL})}onDidChangeRawContentFast(P){return this._eventEmitter.fastEvent(V=>P(V.rawContentChangedEvent))}onDidChangeContentFast(P){return this._eventEmitter.fastEvent(V=>P(V.contentChangedEvent))}onDidChangeContent(P){return this._eventEmitter.slowEvent(V=>P(V.contentChangedEvent))}dispose(){this._isDisposing=!0,this._onWillDispose.fire(),this._languageRegistryListener.dispose(),this._tokenization.dispose(),this._isDisposed=!0,super.dispose(),this._bufferDisposable.dispose(),this._isDisposing=!1;const P=new y.PieceTreeTextBuffer([],"",` +`,!1,!1,!0,!0);P.dispose(),this._buffer=P}_assertNotDisposed(){if(this._isDisposed)throw new Error("Model is disposed!")}_emitContentChangedEvent(P,V){this._isDisposing||this._eventEmitter.fire(new r.InternalModelContentChangeEvent(P,V))}setValue(P){if(this._assertNotDisposed(),P!==null){const{textBuffer:V,disposable:U}=k(P,this._options.defaultEOL);this._setValueFromTextBuffer(V,U)}}_createContentChanged2(P,V,U,H,$,ie,oe){return{changes:[{range:P,rangeOffset:V,rangeLength:U,text:H}],eol:this._buffer.getEOL(),versionId:this.getVersionId(),isUndoing:$,isRedoing:ie,isFlush:oe}}_setValueFromTextBuffer(P,V){this._assertNotDisposed();const U=this.getFullModelRange(),H=this.getValueLengthInRange(U),$=this.getLineCount(),ie=this.getLineMaxColumn($);this._buffer=P,this._bufferDisposable.dispose(),this._bufferDisposable=V,this._increaseVersionId(),this._tokens.flush(),this._tokens2.flush(),this._decorations=Object.create(null),this._decorationsTree=new R,this._commandManager.clear(),this._trimAutoWhitespaceLines=null,this._emitContentChangedEvent(new r.ModelRawContentChangedEvent([new r.ModelRawFlush],this._versionId,!1,!1),this._createContentChanged2(new g.Range(1,1,$,ie),0,H,this.getValue(),!1,!1,!0))}setEOL(P){this._assertNotDisposed();const V=P===1?`\r +`:` +`;if(this._buffer.getEOL()!==V){const U=this.getFullModelRange(),H=this.getValueLengthInRange(U),$=this.getLineCount(),ie=this.getLineMaxColumn($);this._onBeforeEOLChange(),this._buffer.setEOL(V),this._increaseVersionId(),this._onAfterEOLChange(),this._emitContentChangedEvent(new r.ModelRawContentChangedEvent([new r.ModelRawEOLChanged],this._versionId,!1,!1),this._createContentChanged2(new g.Range(1,1,$,ie),0,H,this.getValue(),!1,!1,!1))}}_onBeforeEOLChange(){const P=this.getVersionId(),V=this._decorationsTree.search(0,!1,!1,P);this._ensureNodesHaveRanges(V)}_onAfterEOLChange(){const P=this.getVersionId(),V=this._decorationsTree.collectNodesPostOrder();for(let U=0,H=V.length;U0}getAttachedEditorCount(){return this._attachedEditorCount}isTooLargeForSyncing(){return this._isTooLargeForSyncing}isTooLargeForTokenization(){return this._isTooLargeForTokenization}isDisposed(){return this._isDisposed}isDominatedByLongLines(){if(this._assertNotDisposed(),this.isTooLargeForTokenization())return!1;let P=0,V=0;const U=this._buffer.getLineCount();for(let H=1;H<=U;H++){const $=this._buffer.getLineLength(H);$>=e.LONG_LINE_BOUNDARY?V+=$:P+=$}return V>P}get uri(){return this._associatedResource}getOptions(){return this._assertNotDisposed(),this._options}getFormattingOptions(){return{tabSize:this._options.indentSize,insertSpaces:this._options.insertSpaces}}updateOptions(P){this._assertNotDisposed();let V=typeof P.tabSize!="undefined"?P.tabSize:this._options.tabSize,U=typeof P.indentSize!="undefined"?P.indentSize:this._options.indentSize,H=typeof P.insertSpaces!="undefined"?P.insertSpaces:this._options.insertSpaces,$=typeof P.trimAutoWhitespace!="undefined"?P.trimAutoWhitespace:this._options.trimAutoWhitespace,ie=new c.TextModelResolvedOptions({tabSize:V,indentSize:U,insertSpaces:H,defaultEOL:this._options.defaultEOL,trimAutoWhitespace:$});if(!this._options.equals(ie)){let oe=this._options.createChangeEvent(ie);this._options=ie,this._onDidChangeOptions.fire(oe)}}detectIndentation(P,V){this._assertNotDisposed();let U=s.guessIndentation(this._buffer,V,P);this.updateOptions({insertSpaces:U.insertSpaces,tabSize:U.tabSize,indentSize:U.tabSize})}static _normalizeIndentationFromWhitespace(P,V,U){let H=0;for(let ie=0;ie({range:U.range,text:null})),()=>null)}mightContainNonBasicASCII(){return this._buffer.mightContainNonBasicASCII()}getAlternativeVersionId(){return this._assertNotDisposed(),this._alternativeVersionId}getInitialUndoRedoSnapshot(){return this._assertNotDisposed(),this._initialUndoRedoSnapshot}getOffsetAt(P){this._assertNotDisposed();let V=this._validatePosition(P.lineNumber,P.column,0);return this._buffer.getOffsetAt(V.lineNumber,V.column)}getPositionAt(P){this._assertNotDisposed();let V=Math.min(this._buffer.getLength(),Math.max(0,P));return this._buffer.getPositionAt(V)}_increaseVersionId(){this._versionId=this._versionId+1,this._alternativeVersionId=this._versionId}_overwriteVersionId(P){this._versionId=P}_overwriteAlternativeVersionId(P){this._alternativeVersionId=P}_overwriteInitialUndoRedoSnapshot(P){this._initialUndoRedoSnapshot=P}getValue(P,V=!1){this._assertNotDisposed();const U=this.getFullModelRange(),H=this.getValueInRange(U,P);return V?this._buffer.getBOM()+H:H}createSnapshot(P=!1){return new O(this._buffer.createSnapshot(P))}getValueLength(P,V=!1){this._assertNotDisposed();const U=this.getFullModelRange(),H=this.getValueLengthInRange(U,P);return V?this._buffer.getBOM().length+H:H}getValueInRange(P,V=0){return this._assertNotDisposed(),this._buffer.getValueInRange(this.validateRange(P),V)}getValueLengthInRange(P,V=0){return this._assertNotDisposed(),this._buffer.getValueLengthInRange(this.validateRange(P),V)}getCharacterCountInRange(P,V=0){return this._assertNotDisposed(),this._buffer.getCharacterCountInRange(this.validateRange(P),V)}getLineCount(){return this._assertNotDisposed(),this._buffer.getLineCount()}getLineContent(P){if(this._assertNotDisposed(),P<1||P>this.getLineCount())throw new Error("Illegal value for lineNumber");return this._buffer.getLineContent(P)}getLineLength(P){if(this._assertNotDisposed(),P<1||P>this.getLineCount())throw new Error("Illegal value for lineNumber");return this._buffer.getLineLength(P)}getLinesContent(){return this._assertNotDisposed(),this._buffer.getLinesContent()}getEOL(){return this._assertNotDisposed(),this._buffer.getEOL()}getEndOfLineSequence(){return this._assertNotDisposed(),this._buffer.getEOL()===` +`?0:1}getLineMinColumn(P){return this._assertNotDisposed(),1}getLineMaxColumn(P){if(this._assertNotDisposed(),P<1||P>this.getLineCount())throw new Error("Illegal value for lineNumber");return this._buffer.getLineLength(P)+1}getLineFirstNonWhitespaceColumn(P){if(this._assertNotDisposed(),P<1||P>this.getLineCount())throw new Error("Illegal value for lineNumber");return this._buffer.getLineFirstNonWhitespaceColumn(P)}getLineLastNonWhitespaceColumn(P){if(this._assertNotDisposed(),P<1||P>this.getLineCount())throw new Error("Illegal value for lineNumber");return this._buffer.getLineLastNonWhitespaceColumn(P)}_validateRangeRelaxedNoAllocations(P){const V=this._buffer.getLineCount(),U=P.startLineNumber,H=P.startColumn;let $,ie;if(U<1)$=1,ie=1;else if(U>V)$=V,ie=this.getLineMaxColumn($);else if($=U|0,H<=1)ie=1;else{const te=this.getLineMaxColumn($);H>=te?ie=te:ie=H|0}const oe=P.endLineNumber,ae=P.endColumn;let G,j;if(oe<1)G=1,j=1;else if(oe>V)G=V,j=this.getLineMaxColumn(G);else if(G=oe|0,ae<=1)j=1;else{const te=this.getLineMaxColumn(G);ae>=te?j=te:j=ae|0}return U===$&&H===ie&&oe===G&&ae===j&&P instanceof g.Range&&!(P instanceof p.Selection)?P:new g.Range($,ie,G,j)}_isValidPosition(P,V,U){if(typeof P!="number"||typeof V!="number"||isNaN(P)||isNaN(V)||P<1||V<1||(P|0)!==P||(V|0)!==V)return!1;const H=this._buffer.getLineCount();if(P>H)return!1;if(V===1)return!0;const $=this.getLineMaxColumn(P);if(V>$)return!1;if(U===1){const ie=this._buffer.getLineCharCode(P,V-2);if(w.isHighSurrogate(ie))return!1}return!0}_validatePosition(P,V,U){const H=Math.floor(typeof P=="number"&&!isNaN(P)?P:1),$=Math.floor(typeof V=="number"&&!isNaN(V)?V:1),ie=this._buffer.getLineCount();if(H<1)return new d.Position(1,1);if(H>ie)return new d.Position(ie,this.getLineMaxColumn(ie));if($<=1)return new d.Position(H,1);const oe=this.getLineMaxColumn(H);if($>=oe)return new d.Position(H,oe);if(U===1){const ae=this._buffer.getLineCharCode(H,$-2);if(w.isHighSurrogate(ae))return new d.Position(H,$-1)}return new d.Position(H,$)}validatePosition(P){const V=1;return this._assertNotDisposed(),P instanceof d.Position&&this._isValidPosition(P.lineNumber,P.column,V)?P:this._validatePosition(P.lineNumber,P.column,V)}_isValidRange(P,V){const U=P.startLineNumber,H=P.startColumn,$=P.endLineNumber,ie=P.endColumn;if(!this._isValidPosition(U,H,0)||!this._isValidPosition($,ie,0))return!1;if(V===1){const oe=H>1?this._buffer.getLineCharCode(U,H-2):0,ae=ie>1&&ie<=this._buffer.getLineLength($)?this._buffer.getLineCharCode($,ie-2):0,G=w.isHighSurrogate(oe),j=w.isHighSurrogate(ae);return!G&&!j}return!0}validateRange(P){const V=1;if(this._assertNotDisposed(),P instanceof g.Range&&!(P instanceof p.Selection)&&this._isValidRange(P,V))return P;const U=this._validatePosition(P.startLineNumber,P.startColumn,0),H=this._validatePosition(P.endLineNumber,P.endColumn,0),$=U.lineNumber,ie=U.column,oe=H.lineNumber,ae=H.column;if(V===1){const G=ie>1?this._buffer.getLineCharCode($,ie-2):0,j=ae>1&&ae<=this._buffer.getLineLength(oe)?this._buffer.getLineCharCode(oe,ae-2):0,te=w.isHighSurrogate(G),Z=w.isHighSurrogate(j);return!te&&!Z?new g.Range($,ie,oe,ae):$===oe&&ie===ae?new g.Range($,ie-1,oe,ae-1):te&&Z?new g.Range($,ie-1,oe,ae+1):te?new g.Range($,ie-1,oe,ae):new g.Range($,ie,oe,ae+1)}return new g.Range($,ie,oe,ae)}modifyPosition(P,V){this._assertNotDisposed();let U=this.getOffsetAt(P)+V;return this.getPositionAt(Math.min(this._buffer.getLength(),Math.max(0,U)))}getFullModelRange(){this._assertNotDisposed();const P=this.getLineCount();return new g.Range(1,1,P,this.getLineMaxColumn(P))}findMatchesLineByLine(P,V,U,H){return this._buffer.findMatchesLineByLine(P,V,U,H)}findMatches(P,V,U,H,$,ie,oe=T){this._assertNotDisposed();let ae=null;V!==null&&(Array.isArray(V)||(V=[V]),V.every(te=>g.Range.isIRange(te))&&(ae=V.map(te=>this.validateRange(te)))),ae===null&&(ae=[this.getFullModelRange()]),ae=ae.sort((te,Z)=>te.startLineNumber-Z.startLineNumber||te.startColumn-Z.startColumn);const G=[];G.push(ae.reduce((te,Z)=>g.Range.areIntersecting(te,Z)?te.plusRange(Z):(G.push(te),Z)));let j;if(!U&&P.indexOf(` +`)<0){const Z=new i.SearchParams(P,U,H,$).parseSearchRequest();if(!Z)return[];j=ue=>this.findMatchesLineByLine(ue,Z,ie,oe)}else j=te=>i.TextModelSearch.findMatches(this,new i.SearchParams(P,U,H,$),te,ie,oe);return G.map(j).reduce((te,Z)=>te.concat(Z),[])}findNextMatch(P,V,U,H,$,ie){this._assertNotDisposed();const oe=this.validatePosition(V);if(!U&&P.indexOf(` +`)<0){const G=new i.SearchParams(P,U,H,$).parseSearchRequest();if(!G)return null;const j=this.getLineCount();let te=new g.Range(oe.lineNumber,oe.column,j,this.getLineMaxColumn(j)),Z=this.findMatchesLineByLine(te,G,ie,1);return i.TextModelSearch.findNextMatch(this,new i.SearchParams(P,U,H,$),oe,ie),Z.length>0||(te=new g.Range(1,1,oe.lineNumber,this.getLineMaxColumn(oe.lineNumber)),Z=this.findMatchesLineByLine(te,G,ie,1),Z.length>0)?Z[0]:null}return i.TextModelSearch.findNextMatch(this,new i.SearchParams(P,U,H,$),oe,ie)}findPreviousMatch(P,V,U,H,$,ie){this._assertNotDisposed();const oe=this.validatePosition(V);return i.TextModelSearch.findPreviousMatch(this,new i.SearchParams(P,U,H,$),oe,ie)}pushStackElement(){this._commandManager.pushStackElement()}popStackElement(){this._commandManager.popStackElement()}pushEOL(P){if((this.getEOL()===` +`?0:1)!==P)try{this._onDidChangeDecorations.beginDeferredEmit(),this._eventEmitter.beginDeferredEmit(),this._initialUndoRedoSnapshot===null&&(this._initialUndoRedoSnapshot=this._undoRedoService.createSnapshot(this.uri)),this._commandManager.pushEOL(P)}finally{this._eventEmitter.endDeferredEmit(),this._onDidChangeDecorations.endDeferredEmit()}}_validateEditOperation(P){return P instanceof c.ValidAnnotatedEditOperation?P:new c.ValidAnnotatedEditOperation(P.identifier||null,this.validateRange(P.range),P.text,P.forceMoveMarkers||!1,P.isAutoWhitespaceEdit||!1,P._isTracked||!1)}_validateEditOperations(P){const V=[];for(let U=0,H=P.length;U({range:this.validateRange(ie.range),text:ie.text})),$=!0;if(P)for(let ie=0,oe=P.length;ieae.endLineNumber,he=ae.startLineNumber>Z.endLineNumber;if(!ue&&!he){G=!0;break}}if(!G){$=!1;break}}if($)for(let ie=0,oe=this._trimAutoWhitespaceLines.length;ieue.endLineNumber)&&!(ae===ue.startLineNumber&&ue.startColumn===G&&ue.isEmpty()&&he&&he.length>0&&he.charAt(0)===` +`)&&!(ae===ue.startLineNumber&&ue.startColumn===1&&ue.isEmpty()&&he&&he.length>0&&he.charAt(he.length-1)===` +`)){j=!1;break}}if(j){const te=new g.Range(ae,1,ae,G);V.push(new c.ValidAnnotatedEditOperation(null,te,null,!1,!1,!1))}}this._trimAutoWhitespaceLines=null}return this._initialUndoRedoSnapshot===null&&(this._initialUndoRedoSnapshot=this._undoRedoService.createSnapshot(this.uri)),this._commandManager.pushEditOperation(P,V,U)}_applyUndo(P,V,U,H){const $=P.map(ie=>{const oe=this.getPositionAt(ie.newPosition),ae=this.getPositionAt(ie.newEnd);return{range:new g.Range(oe.lineNumber,oe.column,ae.lineNumber,ae.column),text:ie.oldText}});this._applyUndoRedoEdits($,V,!0,!1,U,H)}_applyRedo(P,V,U,H){const $=P.map(ie=>{const oe=this.getPositionAt(ie.oldPosition),ae=this.getPositionAt(ie.oldEnd);return{range:new g.Range(oe.lineNumber,oe.column,ae.lineNumber,ae.column),text:ie.newText}});this._applyUndoRedoEdits($,V,!1,!0,U,H)}_applyUndoRedoEdits(P,V,U,H,$,ie){try{this._onDidChangeDecorations.beginDeferredEmit(),this._eventEmitter.beginDeferredEmit(),this._isUndoing=U,this._isRedoing=H,this.applyEdits(P,!1),this.setEOL(V),this._overwriteAlternativeVersionId($)}finally{this._isUndoing=!1,this._isRedoing=!1,this._eventEmitter.endDeferredEmit(ie),this._onDidChangeDecorations.endDeferredEmit()}}applyEdits(P,V=!1){try{this._onDidChangeDecorations.beginDeferredEmit(),this._eventEmitter.beginDeferredEmit();const U=this._validateEditOperations(P);return this._doApplyEdits(U,V)}finally{this._eventEmitter.endDeferredEmit(),this._onDidChangeDecorations.endDeferredEmit()}}_doApplyEdits(P,V){const U=this._buffer.getLineCount(),H=this._buffer.applyEdits(P,this._options.trimAutoWhitespace,V),$=this._buffer.getLineCount(),ie=H.changes;if(this._trimAutoWhitespaceLines=H.trimAutoWhitespaceLineNumbers,ie.length!==0){let oe=[],ae=U;for(let G=0,j=ie.length;G0?te.text.charCodeAt(0):0),this._onDidChangeDecorations.fire(),this._decorationsTree.acceptReplace(te.rangeOffset,te.rangeLength,te.text.length,te.forceMoveMarkers);const re=te.range.startLineNumber,ce=te.range.endLineNumber,me=ce-re,Ce=Z,be=Math.min(me,Ce),Le=Ce-me;for(let De=be;De>=0;De--){const Re=re+De,Ee=$-ae-Le+Re;oe.push(new r.ModelRawLineChanged(Re,this.getLineContent(Ee)))}if(bethis._deltaDecorationsImpl(P,[],[{range:$,options:ie}])[0],changeDecoration:($,ie)=>{this._changeDecorationImpl($,ie)},changeDecorationOptions:($,ie)=>{this._changeDecorationOptionsImpl($,ne(ie))},removeDecoration:$=>{this._deltaDecorationsImpl(P,[$],[])},deltaDecorations:($,ie)=>$.length===0&&ie.length===0?[]:this._deltaDecorationsImpl(P,$,ie)},H=null;try{H=V(U)}catch($){b.onUnexpectedError($)}return U.addDecoration=A,U.changeDecoration=A,U.changeDecorationOptions=A,U.removeDecoration=A,U.deltaDecorations=A,H}deltaDecorations(P,V,U=0){if(this._assertNotDisposed(),P||(P=[]),P.length===0&&V.length===0)return[];try{return this._onDidChangeDecorations.beginDeferredEmit(),this._deltaDecorationsImpl(U,P,V)}finally{this._onDidChangeDecorations.endDeferredEmit()}}_getTrackedRange(P){return this.getDecorationRange(P)}_setTrackedRange(P,V,U){const H=P?this._decorations[P]:null;if(!H)return V?this._deltaDecorationsImpl(0,[],[{range:V,options:se[U]}])[0]:null;if(!V)return this._decorationsTree.delete(H),delete this._decorations[H.id],null;const $=this._validateRangeRelaxedNoAllocations(V),ie=this._buffer.getOffsetAt($.startLineNumber,$.startColumn),oe=this._buffer.getOffsetAt($.endLineNumber,$.endColumn);return this._decorationsTree.delete(H),H.reset(this.getVersionId(),ie,oe,$),H.setOptions(se[U]),this._decorationsTree.insert(H),H.id}removeAllDecorationsWithOwnerId(P){if(!this._isDisposed){const V=this._decorationsTree.collectNodesFromOwner(P);for(let U=0,H=V.length;Uthis.getLineCount()?[]:this.getLinesDecorations(P,P,V,U)}getLinesDecorations(P,V,U=0,H=!1){let $=this.getLineCount(),ie=Math.min($,Math.max(1,P)),oe=Math.min($,Math.max(1,V)),ae=this.getLineMaxColumn(oe);return this._getDecorationsInRange(new g.Range(ie,1,oe,ae),U,H)}getDecorationsInRange(P,V=0,U=!1){let H=this.validateRange(P);return this._getDecorationsInRange(H,V,U)}getOverviewRulerDecorations(P=0,V=!1){const U=this.getVersionId(),H=this._decorationsTree.search(P,V,!0,U);return this._ensureNodesHaveRanges(H)}getAllDecorations(P=0,V=!1){const U=this.getVersionId(),H=this._decorationsTree.search(P,V,!1,U);return this._ensureNodesHaveRanges(H)}_getDecorationsInRange(P,V,U){const H=this._buffer.getOffsetAt(P.startLineNumber,P.startColumn),$=this._buffer.getOffsetAt(P.endLineNumber,P.endColumn),ie=this.getVersionId(),oe=this._decorationsTree.intervalSearch(H,$,V,U,ie);return this._ensureNodesHaveRanges(oe)}_ensureNodesHaveRanges(P){for(let V=0,U=P.length;V0&&this._emitModelTokensChangedEvent({tokenizationSupportChanged:!1,semanticTokensApplied:!1,ranges:V})}}setSemanticTokens(P,V){this._tokens2.set(P,V),this._emitModelTokensChangedEvent({tokenizationSupportChanged:!1,semanticTokensApplied:P!==null,ranges:[{fromLineNumber:1,toLineNumber:this.getLineCount()}]})}hasCompleteSemanticTokens(){return this._tokens2.isComplete()}hasSomeSemanticTokens(){return!this._tokens2.isEmpty()}setPartialSemanticTokens(P,V){if(!this.hasCompleteSemanticTokens()){const U=this._tokens2.setPartial(P,V);this._emitModelTokensChangedEvent({tokenizationSupportChanged:!1,semanticTokensApplied:!0,ranges:[{fromLineNumber:U.startLineNumber,toLineNumber:U.endLineNumber}]})}}tokenizeViewport(P,V){P=Math.max(1,P),V=Math.min(this._buffer.getLineCount(),V),this._tokenization.tokenizeViewport(P,V)}clearTokens(){this._tokens.flush(),this._emitModelTokensChangedEvent({tokenizationSupportChanged:!0,semanticTokensApplied:!1,ranges:[{fromLineNumber:1,toLineNumber:this._buffer.getLineCount()}]})}_emitModelTokensChangedEvent(P){this._isDisposing||this._onDidChangeTokens.fire(P)}resetTokenization(){this._tokenization.reset()}forceTokenization(P){if(P<1||P>this.getLineCount())throw new Error("Illegal value for lineNumber");this._tokenization.forceTokenization(P)}isCheapToTokenize(P){return this._tokenization.isCheapToTokenize(P)}tokenizeIfCheap(P){this.isCheapToTokenize(P)&&this.forceTokenization(P)}getLineTokens(P){if(P<1||P>this.getLineCount())throw new Error("Illegal value for lineNumber");return this._getLineTokens(P)}_getLineTokens(P){const V=this.getLineContent(P),U=this._tokens.getTokens(this._languageIdentifier.id,P-1,V);return this._tokens2.addSemanticTokens(P,U)}getLanguageIdentifier(){return this._languageIdentifier}getModeId(){return this._languageIdentifier.language}setMode(P){if(this._languageIdentifier.id!==P.id){let V={oldLanguage:this._languageIdentifier.language,newLanguage:P.language};this._languageIdentifier=P,this._onDidChangeLanguage.fire(V),this._onDidChangeLanguageConfiguration.fire({})}}getLanguageIdAtPosition(P,V){const U=this.validatePosition(new d.Position(P,V)),H=this.getLineTokens(U.lineNumber);return H.getLanguageId(H.findTokenIndexAtOffset(U.column-1))}getWordAtPosition(P){this._assertNotDisposed();const V=this.validatePosition(P),U=this.getLineContent(V.lineNumber),H=this._getLineTokens(V.lineNumber),$=H.findTokenIndexAtOffset(V.column-1),[ie,oe]=D._findLanguageBoundaries(H,$),ae=t.getWordAtText(V.column,l.LanguageConfigurationRegistry.getWordDefinition(H.getLanguageId($)),U.substring(ie,oe),ie);if(ae&&ae.startColumn<=P.column&&P.column<=ae.endColumn)return ae;if($>0&&ie===V.column-1){const[G,j]=D._findLanguageBoundaries(H,$-1),te=t.getWordAtText(V.column,l.LanguageConfigurationRegistry.getWordDefinition(H.getLanguageId($-1)),U.substring(G,j),G);if(te&&te.startColumn<=P.column&&P.column<=te.endColumn)return te}return null}static _findLanguageBoundaries(P,V){const U=P.getLanguageId(V);let H=0;for(let ie=V;ie>=0&&P.getLanguageId(ie)===U;ie--)H=P.getStartOffset(ie);let $=P.getLineContent().length;for(let ie=V,oe=P.getCount();ie=0;te--){const Z=U.getEndOffset(te);if(Z<=ae)break;m.ignoreBracketsInToken(U.getStandardTokenType(te))&&(ae=Z)}const G=Math.min($.length,P.column-1+oe.maxBracketLength);let j=null;for(;;){const te=_.BracketsUtils.findNextBracketInRange(oe.forwardRegex,V,$,ae,G);if(!te)break;if(te.startColumn<=P.column&&P.column<=te.endColumn){const Z=$.substring(te.startColumn-1,te.endColumn-1).toLowerCase(),ue=this._matchFoundBracket(te,oe.textIsBracket[Z],oe.textIsOpenBracket[Z],null);if(ue){if(ue instanceof B)return null;j=ue}}ae=te.endColumn-1}if(j)return j}if(ie>0&&U.getStartOffset(ie)===P.column-1){const ae=ie-1,G=l.LanguageConfigurationRegistry.getBracketsSupport(U.getLanguageId(ae));if(G&&!m.ignoreBracketsInToken(U.getStandardTokenType(ae))){const j=Math.max(0,P.column-1-G.maxBracketLength);let te=Math.min($.length,P.column-1+G.maxBracketLength);for(let ue=ae+1;ue=te)break;m.ignoreBracketsInToken(U.getStandardTokenType(ue))&&(te=he)}const Z=_.BracketsUtils.findPrevBracketInRange(G.reversedRegex,V,$,j,te);if(Z&&Z.startColumn<=P.column&&P.column<=Z.endColumn){const ue=$.substring(Z.startColumn-1,Z.endColumn-1).toLowerCase(),he=this._matchFoundBracket(Z,G.textIsBracket[ue],G.textIsOpenBracket[ue],null);if(he)return he instanceof B?null:he}}}return null}_matchFoundBracket(P,V,U,H){if(!V)return null;const $=U?this._findMatchingBracketDown(V,P.getEndPosition(),H):this._findMatchingBracketUp(V,P.getStartPosition(),H);return $?$ instanceof B?$:[P,$]:null}_findMatchingBracketUp(P,V,U){const H=P.languageIdentifier.id,$=P.reversedRegex;let ie=-1,oe=0;const ae=(G,j,te,Z)=>{for(;;){if(U&&++oe%100==0&&!U())return B.INSTANCE;const ue=_.BracketsUtils.findPrevBracketInRange($,G,j,te,Z);if(!ue)break;const he=j.substring(ue.startColumn-1,ue.endColumn-1).toLowerCase();if(P.isOpen(he)?ie++:P.isClose(he)&&ie--,ie===0)return ue;Z=ue.startColumn-1}return null};for(let G=V.lineNumber;G>=1;G--){const j=this._getLineTokens(G),te=j.getCount(),Z=this._buffer.getLineContent(G);let ue=te-1,he=Z.length,re=Z.length;G===V.lineNumber&&(ue=j.findTokenIndexAtOffset(V.column-1),he=V.column-1,re=V.column-1);let ce=!0;for(;ue>=0;ue--){const me=j.getLanguageId(ue)===H&&!m.ignoreBracketsInToken(j.getStandardTokenType(ue));if(me)ce?he=j.getStartOffset(ue):(he=j.getStartOffset(ue),re=j.getEndOffset(ue));else if(ce&&he!==re){const Ce=ae(G,Z,he,re);if(Ce)return Ce}ce=me}if(ce&&he!==re){const me=ae(G,Z,he,re);if(me)return me}}return null}_findMatchingBracketDown(P,V,U){const H=P.languageIdentifier.id,$=P.forwardRegex;let ie=1,oe=0;const ae=(j,te,Z,ue)=>{for(;;){if(U&&++oe%100==0&&!U())return B.INSTANCE;const he=_.BracketsUtils.findNextBracketInRange($,j,te,Z,ue);if(!he)break;const re=te.substring(he.startColumn-1,he.endColumn-1).toLowerCase();if(P.isOpen(re)?ie++:P.isClose(re)&&ie--,ie===0)return he;Z=he.endColumn-1}return null},G=this.getLineCount();for(let j=V.lineNumber;j<=G;j++){const te=this._getLineTokens(j),Z=te.getCount(),ue=this._buffer.getLineContent(j);let he=0,re=0,ce=0;j===V.lineNumber&&(he=te.findTokenIndexAtOffset(V.column-1),re=V.column-1,ce=V.column-1);let me=!0;for(;he=1;$--){const ie=this._getLineTokens($),oe=ie.getCount(),ae=this._buffer.getLineContent($);let G=oe-1,j=ae.length,te=ae.length;if($===V.lineNumber){G=ie.findTokenIndexAtOffset(V.column-1),j=V.column-1,te=V.column-1;const ue=ie.getLanguageId(G);U!==ue&&(U=ue,H=l.LanguageConfigurationRegistry.getBracketsSupport(U))}let Z=!0;for(;G>=0;G--){const ue=ie.getLanguageId(G);if(U!==ue){if(H&&Z&&j!==te){const re=_.BracketsUtils.findPrevBracketInRange(H.reversedRegex,$,ae,j,te);if(re)return this._toFoundBracket(H,re);Z=!1}U=ue,H=l.LanguageConfigurationRegistry.getBracketsSupport(U)}const he=!!H&&!m.ignoreBracketsInToken(ie.getStandardTokenType(G));if(he)Z?j=ie.getStartOffset(G):(j=ie.getStartOffset(G),te=ie.getEndOffset(G));else if(H&&Z&&j!==te){const re=_.BracketsUtils.findPrevBracketInRange(H.reversedRegex,$,ae,j,te);if(re)return this._toFoundBracket(H,re)}Z=he}if(H&&Z&&j!==te){const ue=_.BracketsUtils.findPrevBracketInRange(H.reversedRegex,$,ae,j,te);if(ue)return this._toFoundBracket(H,ue)}}return null}findNextBracket(P){const V=this.validatePosition(P),U=this.getLineCount();let H=-1,$=null;for(let ie=V.lineNumber;ie<=U;ie++){const oe=this._getLineTokens(ie),ae=oe.getCount(),G=this._buffer.getLineContent(ie);let j=0,te=0,Z=0;if(ie===V.lineNumber){j=oe.findTokenIndexAtOffset(V.column-1),te=V.column-1,Z=V.column-1;const he=oe.getLanguageId(j);H!==he&&(H=he,$=l.LanguageConfigurationRegistry.getBracketsSupport(H))}let ue=!0;for(;jDate.now()-ue<=V}const H=this.validatePosition(P),$=this.getLineCount(),ie=new Map;let oe=[];const ae=(ue,he)=>{if(!ie.has(ue)){let re=[];for(let ce=0,me=he?he.brackets.length:0;ce{for(;;){if(U&&++G%100==0&&!U())return B.INSTANCE;const Ce=_.BracketsUtils.findNextBracketInRange(ue.forwardRegex,he,re,ce,me);if(!Ce)break;const be=re.substring(Ce.startColumn-1,Ce.endColumn-1).toLowerCase(),Le=ue.textIsBracket[be];if(Le&&(Le.isOpen(be)?oe[Le.index]++:Le.isClose(be)&&oe[Le.index]--,oe[Le.index]===-1))return this._matchFoundBracket(Ce,Le,!1,U);ce=Ce.endColumn-1}return null};let te=-1,Z=null;for(let ue=H.lineNumber;ue<=$;ue++){const he=this._getLineTokens(ue),re=he.getCount(),ce=this._buffer.getLineContent(ue);let me=0,Ce=0,be=0;if(ue===H.lineNumber){me=he.findTokenIndexAtOffset(H.column-1),Ce=H.column-1,be=H.column-1;const De=he.getLanguageId(me);te!==De&&(te=De,Z=l.LanguageConfigurationRegistry.getBracketsSupport(te),ae(te,Z))}let Le=!0;for(;meH)throw new Error("Illegal value for lineNumber");const $=l.LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id),ie=Boolean($&&$.offSide);let oe=-2,ae=-1,G=-2,j=-1;const te=Ee=>{if(oe!==-1&&(oe===-2||oe>Ee-1)){oe=-1,ae=-1;for(let Ae=Ee-2;Ae>=0;Ae--){let Se=this._computeIndentLevel(Ae);if(Se>=0){oe=Ae,ae=Se;break}}}if(G===-2){G=-1,j=-1;for(let Ae=Ee;Ae=0){G=Ae,j=Se;break}}}};let Z=-2,ue=-1,he=-2,re=-1;const ce=Ee=>{if(Z===-2){Z=-1,ue=-1;for(let Ae=Ee-2;Ae>=0;Ae--){let Se=this._computeIndentLevel(Ae);if(Se>=0){Z=Ae,ue=Se;break}}}if(he!==-1&&(he===-2||he=0){he=Ae,re=Se;break}}}};let me=0,Ce=!0,be=0,Le=!0,De=0,Re=0;for(let Ee=0;Ce||Le;Ee++){const Ae=P-Ee,Se=P+Ee;Ee>1&&(Ae<1||Ae1&&(Se>H||Se>U)&&(Le=!1),Ee>5e4&&(Ce=!1,Le=!1);let we=-1;if(Ce){const fe=this._computeIndentLevel(Ae-1);fe>=0?(G=Ae-1,j=fe,we=Math.ceil(fe/this._options.indentSize)):(te(Ae),we=this._getIndentLevelForWhitespaceLine(ie,ae,j))}let ye=-1;if(Le){const fe=this._computeIndentLevel(Se-1);fe>=0?(Z=Se-1,ue=fe,ye=Math.ceil(fe/this._options.indentSize)):(ce(Se),ye=this._getIndentLevelForWhitespaceLine(ie,ue,re))}if(Ee===0){Re=we;continue}if(Ee===1){if(Se<=H&&ye>=0&&Re+1===ye){Ce=!1,me=Se,be=Se,De=ye;continue}if(Ae>=1&&we>=0&&we-1===Re){Le=!1,me=Ae,be=Ae,De=we;continue}if(me=P,be=P,De=Re,De===0)return{startLineNumber:me,endLineNumber:be,indent:De}}Ce&&(we>=De?me=Ae:Ce=!1),Le&&(ye>=De?be=Se:Le=!1)}return{startLineNumber:me,endLineNumber:be,indent:De}}getLinesIndentGuides(P,V){this._assertNotDisposed();const U=this.getLineCount();if(P<1||P>U)throw new Error("Illegal value for startLineNumber");if(V<1||V>U)throw new Error("Illegal value for endLineNumber");const H=l.LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id),$=Boolean(H&&H.offSide);let ie=new Array(V-P+1),oe=-2,ae=-1,G=-2,j=-1;for(let te=P;te<=V;te++){let Z=te-P;const ue=this._computeIndentLevel(te-1);if(ue>=0){oe=te-1,ae=ue,ie[Z]=Math.ceil(ue/this._options.indentSize);continue}if(oe===-2){oe=-1,ae=-1;for(let he=te-2;he>=0;he--){let re=this._computeIndentLevel(he);if(re>=0){oe=he,ae=re;break}}}if(G!==-1&&(G===-2||G=0){G=he,j=re;break}}}ie[Z]=this._getIndentLevelForWhitespaceLine($,ae,j)}return ie}_getIndentLevelForWhitespaceLine(P,V,U){return V===-1||U===-1?0:V0){this._deferredEvent?this._deferredEvent=this._deferredEvent.merge(P):this._deferredEvent=P;return}this._fastEmitter.fire(P),this._slowEmitter.fire(P)}}e.DidChangeContentEmitter=X}),define(Q[42],J([0,1,12,8,14,3,21,31,41]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.isQuote=e.CursorColumns=e.EditOperationResult=e.CursorState=e.PartialViewCursorState=e.PartialModelCursorState=e.CursorContext=e.SingleCursorState=e.CursorConfiguration=void 0;const g=()=>!0,p=()=>!1,c=h=>h===" "||h===" ";class o{constructor(m,_,f){this._languageIdentifier=m;const v=f.options,y=v.get(124);this.readOnly=v.get(75),this.tabSize=_.tabSize,this.indentSize=_.indentSize,this.insertSpaces=_.insertSpaces,this.stickyTabStops=v.get(99),this.lineHeight=v.get(53),this.pageSize=Math.max(1,Math.floor(y.height/this.lineHeight)-2),this.useTabStops=v.get(109),this.wordSeparators=v.get(110),this.emptySelectionClipboard=v.get(28),this.copyWithSyntaxHighlighting=v.get(18),this.multiCursorMergeOverlapping=v.get(63),this.multiCursorPaste=v.get(65),this.autoClosingBrackets=v.get(5),this.autoClosingQuotes=v.get(7),this.autoClosingOvertype=v.get(6),this.autoSurround=v.get(10),this.autoIndent=v.get(8),this.surroundingPairs={},this._electricChars=null,this.shouldAutoCloseBefore={quote:o._getShouldAutoClose(m,this.autoClosingQuotes),bracket:o._getShouldAutoClose(m,this.autoClosingBrackets)},this.autoClosingPairs=d.LanguageConfigurationRegistry.getAutoClosingPairs(m.id);let L=o._getSurroundingPairs(m);if(L)for(const I of L)this.surroundingPairs[I.open]=I.close}static shouldRecreate(m){return m.hasChanged(124)||m.hasChanged(110)||m.hasChanged(28)||m.hasChanged(63)||m.hasChanged(65)||m.hasChanged(5)||m.hasChanged(7)||m.hasChanged(6)||m.hasChanged(10)||m.hasChanged(109)||m.hasChanged(53)||m.hasChanged(75)}get electricChars(){if(!this._electricChars){this._electricChars={};let m=o._getElectricCharacters(this._languageIdentifier);if(m)for(const _ of m)this._electricChars[_]=!0}return this._electricChars}normalizeIndentation(m){return C.TextModel.normalizeIndentation(m,this.indentSize,this.insertSpaces)}static _getElectricCharacters(m){try{return d.LanguageConfigurationRegistry.getElectricCharacters(m.id)}catch(_){return b.onUnexpectedError(_),null}}static _getShouldAutoClose(m,_){switch(_){case"beforeWhitespace":return c;case"languageDefined":return o._getLanguageDefinedShouldAutoClose(m);case"always":return g;case"never":return p}}static _getLanguageDefinedShouldAutoClose(m){try{const _=d.LanguageConfigurationRegistry.getAutoCloseBeforeSet(m.id);return f=>_.indexOf(f)!==-1}catch(_){return b.onUnexpectedError(_),p}}static _getSurroundingPairs(m){try{return d.LanguageConfigurationRegistry.getSurroundingPairs(m.id)}catch(_){return b.onUnexpectedError(_),null}}}e.CursorConfiguration=o;class s{constructor(m,_,f,v){this.selectionStart=m,this.selectionStartLeftoverVisibleColumns=_,this.position=f,this.leftoverVisibleColumns=v,this.selection=s._computeSelection(this.selectionStart,this.position)}equals(m){return this.selectionStartLeftoverVisibleColumns===m.selectionStartLeftoverVisibleColumns&&this.leftoverVisibleColumns===m.leftoverVisibleColumns&&this.position.equals(m.position)&&this.selectionStart.equalsRange(m.selectionStart)}hasSelection(){return!this.selection.isEmpty()||!this.selectionStart.isEmpty()}move(m,_,f,v){return m?new s(this.selectionStart,this.selectionStartLeftoverVisibleColumns,new M.Position(_,f),v):new s(new w.Range(_,f,_,f),v,new M.Position(_,f),v)}static _computeSelection(m,_){let f,v,y,L;return m.isEmpty()?(f=m.startLineNumber,v=m.startColumn,y=_.lineNumber,L=_.column):_.isBeforeOrEqual(m.getStartPosition())?(f=m.endLineNumber,v=m.endColumn,y=_.lineNumber,L=_.column):(f=m.startLineNumber,v=m.startColumn,y=_.lineNumber,L=_.column),new S.Selection(f,v,y,L)}}e.SingleCursorState=s;class a{constructor(m,_,f){this.model=m,this.coordinatesConverter=_,this.cursorConfig=f}}e.CursorContext=a;class u{constructor(m){this.modelState=m,this.viewState=null}}e.PartialModelCursorState=u;class r{constructor(m){this.modelState=null,this.viewState=m}}e.PartialViewCursorState=r;class i{constructor(m,_){this.modelState=m,this.viewState=_}static fromModelState(m){return new u(m)}static fromViewState(m){return new r(m)}static fromModelSelection(m){const _=m.selectionStartLineNumber,f=m.selectionStartColumn,v=m.positionLineNumber,y=m.positionColumn,L=new s(new w.Range(_,f,_,f),0,new M.Position(v,y),0);return i.fromModelState(L)}static fromModelSelections(m){let _=[];for(let f=0,v=m.length;f=65536?2:1,k===9)L=t.nextRenderTabStop(L,f);else{let E=N.getGraphemeBreakType(k);for(;I=65536?2:1,E=O}N.isFullWidthCharacter(k)||N.isEmojiImprecise(k)?L=L+2:L=L+1}}return L}static visibleColumnFromColumn2(m,_,f){return this.visibleColumnFromColumn(_.getLineContent(f.lineNumber),f.column,m.tabSize)}static columnFromVisibleColumn(m,_,f){if(_<=0)return 1;const v=m.length;let y=0,L=1,I=0;for(;I=65536?2:1;let E;if(k===9)E=t.nextRenderTabStop(y,f);else{let O=N.getGraphemeBreakType(k);for(;I=65536?2:1,O=B}N.isFullWidthCharacter(k)||N.isEmojiImprecise(k)?E=y+2:E=y+1}const T=I+1;if(E>=_){const O=_-y;return E-_I?I:y}static nextRenderTabStop(m,_){return m+_-m%_}static nextIndentTabStop(m,_){return m+_-m%_}static prevRenderTabStop(m,_){return m-1-(m-1)%_}static prevIndentTabStop(m,_){return m-1-(m-1)%_}}e.CursorColumns=t;function l(h){return h==="'"||h==='"'||h==="`"}e.isQuote=l}),define(Q[179],J([0,1,8,42,3,21,41]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ShiftCommand=e.cachedStringRepeat=void 0;const C=Object.create(null);function d(p,c){C[p]||(C[p]=["",p]);const o=C[p];for(let s=o.length;s<=c;s++)o[s]=o[s-1]+p;return o[c]}e.cachedStringRepeat=d;class g{constructor(c,o){this._opts=o,this._selection=c,this._selectionId=null,this._useLastEditRangeForCursorEndPosition=!1,this._selectionStartColumnStaysPut=!1}static unshiftIndent(c,o,s,a,u){const r=N.CursorColumns.visibleColumnFromColumn(c,o,s);if(u){const i=d(" ",a),t=N.CursorColumns.prevIndentTabStop(r,a)/a;return d(i,t)}else{const i=" ",t=N.CursorColumns.prevRenderTabStop(r,s)/s;return d(i,t)}}static shiftIndent(c,o,s,a,u){const r=N.CursorColumns.visibleColumnFromColumn(c,o,s);if(u){const i=d(" ",a),t=N.CursorColumns.nextIndentTabStop(r,a)/a;return d(i,t)}else{const i=" ",t=N.CursorColumns.nextRenderTabStop(r,s)/s;return d(i,t)}}_addEditOperation(c,o,s){this._useLastEditRangeForCursorEndPosition?c.addTrackedEditOperation(o,s):c.addEditOperation(o,s)}getEditOperations(c,o){const s=this._selection.startLineNumber;let a=this._selection.endLineNumber;this._selection.endColumn===1&&s!==a&&(a=a-1);const{tabSize:u,indentSize:r,insertSpaces:i}=this._opts,n=s===a;if(this._opts.useTabStops){this._selection.isEmpty()&&/^\s*$/.test(c.getLineContent(s))&&(this._useLastEditRangeForCursorEndPosition=!0);let t=0,l=0;for(let h=s;h<=a;h++,t=l){l=0;let m=c.getLineContent(h),_=b.firstNonWhitespaceIndex(m);if(!(this._opts.isUnshift&&(m.length===0||_===0))&&!(!n&&!this._opts.isUnshift&&m.length===0)){if(_===-1&&(_=m.length),h>1&&N.CursorColumns.visibleColumnFromColumn(m,_+1,u)%r!=0&&c.isCheapToTokenize(h-1)){let y=S.LanguageConfigurationRegistry.getEnterAction(this._opts.autoIndent,c,new M.Range(h-1,c.getLineMaxColumn(h-1),h-1,c.getLineMaxColumn(h-1)));if(y){if(l=t,y.appendText)for(let L=0,I=y.appendText.length;Lc,u=p>o,r=po||_p||m1&&p--,w.columnSelect(C,d,g.fromViewLineNumber,g.fromViewVisualColumn,g.toViewLineNumber,p)}static columnSelectRight(C,d,g){let p=0;const c=Math.min(g.fromViewLineNumber,g.toViewLineNumber),o=Math.max(g.fromViewLineNumber,g.toViewLineNumber);for(let a=c;a<=o;a++){const u=d.getLineMaxColumn(a),r=b.CursorColumns.visibleColumnFromColumn2(C,d,new N.Position(a,u));p=Math.max(p,r)}let s=g.toViewVisualColumn;return sp.getLineMinColumn(c)?o=o-w.prevCharLength(p.getLineContent(c),o-1):c>1&&(c=c-1,o=p.getLineMaxColumn(c)),new N.Position(c,o)}static leftPositionAtomicSoftTabs(p,c,o,s){const a=p.getLineMinColumn(c),u=p.getLineContent(c),r=S.AtomicTabMoveOperations.atomicPosition(u,o-1,s,0);return r===-1||r+1n?(o=n,r?s=c.getLineMaxColumn(o):s=Math.min(c.getLineMaxColumn(o),s)):s=b.CursorColumns.columnFromVisibleColumn2(p,c,o,i),t?a=0:a=i-b.CursorColumns.visibleColumnFromColumn(c.getLineContent(o),s,p.tabSize),new C(o,s,a)}static moveDown(p,c,o,s,a){let u,r;o.hasSelection()&&!s?(u=o.selection.endLineNumber,r=o.selection.endColumn):(u=o.position.lineNumber,r=o.position.column);let i=d.down(p,c,u,r,o.leftoverVisibleColumns,a,!0);return o.move(s,i.lineNumber,i.column,i.leftoverVisibleColumns)}static translateDown(p,c,o){let s=o.selection,a=d.down(p,c,s.selectionStartLineNumber,s.selectionStartColumn,o.selectionStartLeftoverVisibleColumns,1,!1),u=d.down(p,c,s.positionLineNumber,s.positionColumn,o.leftoverVisibleColumns,1,!1);return new b.SingleCursorState(new M.Range(a.lineNumber,a.column,a.lineNumber,a.column),a.leftoverVisibleColumns,new N.Position(u.lineNumber,u.column),u.leftoverVisibleColumns)}static up(p,c,o,s,a,u,r){const i=b.CursorColumns.visibleColumnFromColumn(c.getLineContent(o),s,p.tabSize)+a,n=o===1&&s===1;return o=o-u,o<1?(o=1,r?s=c.getLineMinColumn(o):s=Math.min(c.getLineMaxColumn(o),s)):s=b.CursorColumns.columnFromVisibleColumn2(p,c,o,i),n?a=0:a=i-b.CursorColumns.visibleColumnFromColumn(c.getLineContent(o),s,p.tabSize),new C(o,s,a)}static moveUp(p,c,o,s,a){let u,r;o.hasSelection()&&!s?(u=o.selection.startLineNumber,r=o.selection.startColumn):(u=o.position.lineNumber,r=o.position.column);let i=d.up(p,c,u,r,o.leftoverVisibleColumns,a,!0);return o.move(s,i.lineNumber,i.column,i.leftoverVisibleColumns)}static translateUp(p,c,o){let s=o.selection,a=d.up(p,c,s.selectionStartLineNumber,s.selectionStartColumn,o.selectionStartLeftoverVisibleColumns,1,!1),u=d.up(p,c,s.positionLineNumber,s.positionColumn,o.leftoverVisibleColumns,1,!1);return new b.SingleCursorState(new M.Range(a.lineNumber,a.column,a.lineNumber,a.column),a.leftoverVisibleColumns,new N.Position(u.lineNumber,u.column),u.leftoverVisibleColumns)}static _isBlankLine(p,c){return p.getLineFirstNonWhitespaceColumn(c)===0}static moveToPrevBlankLine(p,c,o,s){let a=o.position.lineNumber;for(;a>1&&this._isBlankLine(c,a);)a--;for(;a>1&&!this._isBlankLine(c,a);)a--;return o.move(s,a,c.getLineMinColumn(a),0)}static moveToNextBlankLine(p,c,o,s){const a=c.getLineCount();let u=o.position.lineNumber;for(;u=n.length+1)return!1;const t=n.charAt(i.column-2),l=c.get(t);if(!l)return!1;if(M.isQuote(t)){if(p==="never")return!1}else if(g==="never")return!1;const h=n.charAt(i.column-1);let m=!1;for(const _ of l)_.open===t&&_.close===h&&(m=!0);if(!m)return!1}return!0}static _runAutoClosingPairDelete(g,p,c){let o=[];for(let s=0,a=c.length;s1){let l=c.getLineContent(t.lineNumber),h=b.firstNonWhitespaceIndex(l),m=h===-1?l.length+1:h+1;if(t.column<=m){let _=M.CursorColumns.visibleColumnFromColumn2(p,c,t),f=M.CursorColumns.prevIndentTabStop(_,p.indentSize),v=M.CursorColumns.columnFromVisibleColumn2(p,c,t.lineNumber,f);n=new S.Range(t.lineNumber,v,t.lineNumber,t.column)}else n=new S.Range(t.lineNumber,t.column-1,t.lineNumber,t.column)}else{let l=w.MoveOperations.left(p,c,t.lineNumber,t.column);n=new S.Range(l.lineNumber,l.column,t.lineNumber,t.column)}}if(n.isEmpty()){s[u]=null;continue}n.startLineNumber!==n.endLineNumber&&(a=!0),s[u]=new N.ReplaceCommand(n,"")}return[a,s]}static cut(g,p,c){let o=[];for(let s=0,a=c.length;s1?(i=r.lineNumber-1,n=p.getLineMaxColumn(r.lineNumber-1),t=r.lineNumber,l=p.getLineMaxColumn(r.lineNumber)):(i=r.lineNumber,n=1,t=r.lineNumber,l=p.getLineMaxColumn(r.lineNumber));let h=new S.Range(i,n,t,l);h.isEmpty()?o[s]=null:o[s]=new N.ReplaceCommand(h,"")}else o[s]=null;else o[s]=new N.ReplaceCommand(u,"")}return new M.EditOperationResult(0,o,{shouldPushStackElementBefore:!0,shouldPushStackElementAfter:!0})}}e.DeleteOperations=C}),define(Q[182],J([0,1,12,8,92,179,367,42,106,3,21,109,41]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TypeWithAutoClosingCommand=e.TypeOperations=void 0;class s{static indent(r,i,n){if(i===null||n===null)return[];let t=[];for(let l=0,h=n.length;l1){let m;for(m=n-1;m>=1;m--){const v=i.getLineContent(m);if(N.lastNonWhitespaceIndex(v)>=0)break}if(m<1)return null;const _=i.getLineMaxColumn(m),f=o.LanguageConfigurationRegistry.getEnterAction(r.autoIndent,i,new g.Range(m,_,m,_));f&&(l=f.indentation+f.appendText)}return t&&(t===c.IndentAction.Indent&&(l=s.shiftIndent(r,l)),t===c.IndentAction.Outdent&&(l=s.unshiftIndent(r,l)),l=r.normalizeIndentation(l)),l||null}static _replaceJumpToNextIndent(r,i,n,t){let l="",h=n.getStartPosition();if(r.insertSpaces){let m=C.CursorColumns.visibleColumnFromColumn2(r,i,h),_=r.indentSize,f=_-m%_;for(let v=0;vthis._compositionType(n,v,l,h,m,_));return new C.EditOperationResult(1,f,{shouldPushStackElementBefore:r!==1,shouldPushStackElementAfter:!1})}static _compositionType(r,i,n,t,l,h){if(!i.isEmpty())return null;const m=i.getPosition(),_=Math.max(1,m.column-t),f=Math.min(r.getLineMaxColumn(m.lineNumber),m.column+l),v=new g.Range(m.lineNumber,_,m.lineNumber,f);return r.getValueInRange(v)===n&&h===0?null:new M.ReplaceCommandWithOffsetCursorState(v,n,0,h)}static _typeCommand(r,i,n){return n?new M.ReplaceCommandWithoutChangingPosition(r,i,!0):new M.ReplaceCommand(r,i,!0)}static _enter(r,i,n,t){if(r.autoIndent===0)return s._typeCommand(t,` +`,n);if(!i.isCheapToTokenize(t.getStartPosition().lineNumber)||r.autoIndent===1){let _=i.getLineContent(t.startLineNumber),f=N.getLeadingWhitespace(_).substring(0,t.startColumn-1);return s._typeCommand(t,` +`+r.normalizeIndentation(f),n)}const l=o.LanguageConfigurationRegistry.getEnterAction(r.autoIndent,i,t);if(l){if(l.indentAction===c.IndentAction.None)return s._typeCommand(t,` +`+r.normalizeIndentation(l.indentation+l.appendText),n);if(l.indentAction===c.IndentAction.Indent)return s._typeCommand(t,` +`+r.normalizeIndentation(l.indentation+l.appendText),n);if(l.indentAction===c.IndentAction.IndentOutdent){const _=r.normalizeIndentation(l.indentation),f=r.normalizeIndentation(l.indentation+l.appendText),v=` +`+f+` +`+_;return n?new M.ReplaceCommandWithoutChangingPosition(t,v,!0):new M.ReplaceCommandWithOffsetCursorState(t,v,-1,f.length-_.length,!0)}else if(l.indentAction===c.IndentAction.Outdent){const _=s.unshiftIndent(r,l.indentation);return s._typeCommand(t,` +`+r.normalizeIndentation(_+l.appendText),n)}}const h=i.getLineContent(t.startLineNumber),m=N.getLeadingWhitespace(h).substring(0,t.startColumn-1);if(r.autoIndent>=4){const _=o.LanguageConfigurationRegistry.getIndentForEnter(r.autoIndent,i,t,{unshiftIndent:f=>s.unshiftIndent(r,f),shiftIndent:f=>s.shiftIndent(r,f),normalizeIndentation:f=>r.normalizeIndentation(f)});if(_){let f=C.CursorColumns.visibleColumnFromColumn2(r,i,t.getEndPosition());const v=t.endColumn,y=i.getLineContent(t.endLineNumber),L=N.firstNonWhitespaceIndex(y);if(L>=0?t=t.setEndPosition(t.endLineNumber,Math.max(t.endColumn,L+1)):t=t.setEndPosition(t.endLineNumber,i.getLineMaxColumn(t.endLineNumber)),n)return new M.ReplaceCommandWithoutChangingPosition(t,` +`+r.normalizeIndentation(_.afterEnter),!0);{let I=0;return v<=L+1&&(r.insertSpaces||(f=Math.ceil(f/r.indentSize)),I=Math.min(f+1-r.normalizeIndentation(_.afterEnter).length-1,0)),new M.ReplaceCommandWithOffsetCursorState(t,` +`+r.normalizeIndentation(_.afterEnter),0,I,!0)}}}return s._typeCommand(t,` +`+r.normalizeIndentation(m),n)}static _isAutoIndentType(r,i,n){if(r.autoIndent<4)return!1;for(let t=0,l=n.length;ts.shiftIndent(r,m),unshiftIndent:m=>s.unshiftIndent(r,m)});if(h===null)return null;if(h!==r.normalizeIndentation(l)){const m=i.getLineFirstNonWhitespaceColumn(n.startLineNumber);return m===0?s._typeCommand(new g.Range(n.startLineNumber,1,n.endLineNumber,n.endColumn),r.normalizeIndentation(h)+t,!1):s._typeCommand(new g.Range(n.startLineNumber,1,n.endLineNumber,n.endColumn),r.normalizeIndentation(h)+i.getLineContent(n.startLineNumber).substring(m-1,n.startColumn-1)+t,!1)}return null}static _isAutoClosingOvertype(r,i,n,t,l){if(r.autoClosingOvertype==="never"||!r.autoClosingPairs.autoClosingPairsCloseSingleChar.has(l))return!1;for(let h=0,m=n.length;h2?v.charCodeAt(f.column-2):0)===92&&L)return!1;if(r.autoClosingOvertype==="auto"){let k=!1;for(let E=0,T=t.length;Ei.startsWith(_.open)),m=l.some(_=>i.startsWith(_.close));return!h&&m}static _findAutoClosingPairOpen(r,i,n,t){const l=r.autoClosingPairs.autoClosingPairsOpenByEnd.get(t);if(!l)return null;let h=null;for(const m of l)if(h===null||m.open.length>h.open.length){let _=!0;for(const f of n)if(i.getValueInRange(new g.Range(f.lineNumber,f.column-m.open.length+1,f.lineNumber,f.column))+t!==m.open){_=!1;break}_&&(h=m)}return h}static _findSubAutoClosingPairClose(r,i){if(i.open.length<=1)return"";const n=i.close.charAt(i.close.length-1),t=r.autoClosingPairs.autoClosingPairsCloseByEnd.get(n)||[];let l=null;for(const h of t)h.open!==i.open&&i.open.includes(h.open)&&i.close.endsWith(h.close)&&(!l||h.open.length>l.open.length)&&(l=h);return l?l.close:""}static _getAutoClosingPairClose(r,i,n,t,l){const h=C.isQuote(t),m=h?r.autoClosingQuotes:r.autoClosingBrackets;if(m==="never")return null;const _=this._findAutoClosingPairOpen(r,i,n.map(L=>L.getPosition()),t);if(!_)return null;const f=this._findSubAutoClosingPairClose(r,_);let v=!0;const y=h?r.shouldAutoCloseBefore.quote:r.shouldAutoCloseBefore.bracket;for(let L=0,I=n.length;LE.column-1){const F=T.charAt(E.column-1);if(!s._isBeforeClosingBrace(r,O)&&!y(F))return null}if(!i.isCheapToTokenize(E.lineNumber))return null;if(_.open.length===1&&h&&m!=="always"){const F=d.getMapForWordSeparators(r.wordSeparators);if(l&&E.column>1&&F.get(T.charCodeAt(E.column-2))===0||!l&&E.column>2&&F.get(T.charCodeAt(E.column-3))===0)return null}i.forceTokenization(E.lineNumber);const A=i.getLineTokens(E.lineNumber);let B=!1;try{B=o.LanguageConfigurationRegistry.shouldAutoClosePair(_,A,l?E.column:E.column-1)}catch(F){b.onUnexpectedError(F)}if(!B)return null}return v?_.close.substring(0,_.close.length-f.length):_.close}static _runAutoClosingOpenCharType(r,i,n,t,l,h,m){let _=[];for(let f=0,v=t.length;fnew M.ReplaceCommand(new g.Range(v.positionLineNumber,v.positionColumn,v.positionLineNumber,v.positionColumn+1),"",!1));return new C.EditOperationResult(1,f,{shouldPushStackElementBefore:!0,shouldPushStackElementAfter:!1})}const _=this._getAutoClosingPairClose(i,n,l,m,!1);return _!==null?this._runAutoClosingOpenCharType(r,i,n,l,m,!1,_):null}static typeWithInterceptors(r,i,n,t,l,h,m){if(!r&&m===` +`){let v=[];for(let y=0,L=l.length;y=0;r--){let i=o.charCodeAt(r),n=s.get(i);if(n===0){if(u===2)return this._createWord(o,u,n,r+1,this._findEndOfWord(o,s,u,r+1));u=1}else if(n===2){if(u===1)return this._createWord(o,u,n,r+1,this._findEndOfWord(o,s,u,r+1));u=2}else if(n===1&&u!==0)return this._createWord(o,u,n,r+1,this._findEndOfWord(o,s,u,r+1))}return u!==0?this._createWord(o,u,1,0,this._findEndOfWord(o,s,u,0)):null}static _findEndOfWord(o,s,a,u){let r=o.length;for(let i=u;i=0;r--){let i=o.charCodeAt(r),n=s.get(i);if(n===1||a===1&&n===2||a===2&&n===0)return r+1}return 0}static moveWordLeft(o,s,a,u){let r=a.lineNumber,i=a.column;i===1&&r>1&&(r=r-1,i=s.getLineMaxColumn(r));let n=d._findPreviousWordOnLine(o,s,new S.Position(r,i));if(u===0)return new S.Position(r,n?n.start+1:1);if(u===1)return n&&n.wordType===2&&n.end-n.start==1&&n.nextCharClass===0&&(n=d._findPreviousWordOnLine(o,s,new S.Position(r,n.start+1))),new S.Position(r,n?n.start+1:1);if(u===3){for(;n&&n.wordType===2;)n=d._findPreviousWordOnLine(o,s,new S.Position(r,n.start+1));return new S.Position(r,n?n.start+1:1)}return n&&i<=n.end+1&&(n=d._findPreviousWordOnLine(o,s,new S.Position(r,n.start+1))),new S.Position(r,n?n.end+1:1)}static _moveWordPartLeft(o,s){const a=s.lineNumber,u=o.getLineMaxColumn(a);if(s.column===1)return a>1?new S.Position(a-1,o.getLineMaxColumn(a-1)):s;const r=o.getLineContent(a);for(let i=s.column-1;i>1;i--){const n=r.charCodeAt(i-2),t=r.charCodeAt(i-1);if(n===95&&t!==95)return new S.Position(a,i);if(b.isLowerAsciiLetter(n)&&b.isUpperAsciiLetter(t))return new S.Position(a,i);if(b.isUpperAsciiLetter(n)&&b.isUpperAsciiLetter(t)&&i+1=t.start+1&&(t=d._findNextWordOnLine(o,s,new S.Position(r,t.end+1))),t?i=t.start+1:i=s.getLineMaxColumn(r);return new S.Position(r,i)}static _moveWordPartRight(o,s){const a=s.lineNumber,u=o.getLineMaxColumn(a);if(s.column===u)return a1?l=1:(t--,l=u.getLineMaxColumn(t)):(h&&l<=h.end+1&&(h=d._findPreviousWordOnLine(a,u,new S.Position(t,h.start+1))),h?l=h.end+1:l>1?l=1:(t--,l=u.getLineMaxColumn(t))),new C.Range(t,l,n.lineNumber,n.column)}static deleteInsideWord(o,s,a){if(!a.isEmpty())return a;const u=new S.Position(a.positionLineNumber,a.positionColumn);let r=this._deleteInsideWordWhitespace(s,u);return r||this._deleteInsideWordDetermineDeleteRange(o,s,u)}static _charAtIsWhitespace(o,s){const a=o.charCodeAt(s);return a===32||a===9}static _deleteInsideWordWhitespace(o,s){const a=o.getLineContent(s.lineNumber),u=a.length;if(u===0)return null;let r=Math.max(s.column-2,0);if(!this._charAtIsWhitespace(a,r))return null;let i=Math.min(s.column-1,u-1);if(!this._charAtIsWhitespace(a,i))return null;for(;r>0&&this._charAtIsWhitespace(a,r-1);)r--;for(;i+11?new C.Range(a.lineNumber-1,s.getLineMaxColumn(a.lineNumber-1),a.lineNumber,1):a.lineNumberm.start+1<=a.column&&a.column<=m.end+1,n=(m,_)=>(m=Math.min(m,a.column),_=Math.max(_,a.column),new C.Range(a.lineNumber,m,a.lineNumber,_)),t=m=>{let _=m.start+1,f=m.end+1,v=!1;for(;f-11&&this._charAtIsWhitespace(u,_-2);)_--;return n(_,f)},l=d._findPreviousWordOnLine(o,s,a);if(l&&i(l))return t(l);const h=d._findNextWordOnLine(o,s,a);return h&&i(h)?t(h):l&&h?n(l.end+1,h.start+1):l?n(l.start+1,l.end+1):h?n(h.start+1,h.end+1):n(1,r+1)}static _deleteWordPartLeft(o,s){if(!s.isEmpty())return s;const a=s.getPosition(),u=d._moveWordPartLeft(o,a);return new C.Range(a.lineNumber,a.column,u.lineNumber,u.column)}static _findFirstNonWhitespaceChar(o,s){let a=o.length;for(let u=s;u=_.start+1&&(_=d._findNextWordOnLine(a,u,new S.Position(t,_.end+1))),_?l=_.start+1:lBoolean(o))}}),define(Q[241],J([0,1,20,42,180,136,14,3]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CursorMove=e.CursorMoveCommands=void 0;class d{static addCursorDown(c,o,s){let a=[],u=0;for(let r=0,i=o.length;rt&&(l=t,h=c.model.getLineMaxColumn(l)),N.CursorState.fromModelState(new N.SingleCursorState(new C.Range(r.lineNumber,1,l,h),0,new S.Position(l,h),0))}const n=o.modelState.selectionStart.getStartPosition().lineNumber;if(r.lineNumbern){const t=c.getLineCount();let l=i.lineNumber+1,h=1;return l>t&&(l=t,h=c.getLineMaxColumn(l)),N.CursorState.fromViewState(o.viewState.move(o.modelState.hasSelection(),l,h,0))}else{const t=o.modelState.selectionStart.getEndPosition();return N.CursorState.fromModelState(o.modelState.move(o.modelState.hasSelection(),t.lineNumber,t.column,0))}}static word(c,o,s,a){const u=c.model.validatePosition(a);return N.CursorState.fromModelState(w.WordOperations.word(c.cursorConfig,c.model,o.modelState,s,u))}static cancelSelection(c,o){if(!o.modelState.hasSelection())return new N.CursorState(o.modelState,o.viewState);const s=o.viewState.position.lineNumber,a=o.viewState.position.column;return N.CursorState.fromViewState(new N.SingleCursorState(new C.Range(s,a,s,a),0,new S.Position(s,a),0))}static moveTo(c,o,s,a,u){const r=c.model.validatePosition(a),i=u?c.coordinatesConverter.validateViewPosition(new S.Position(u.lineNumber,u.column),r):c.coordinatesConverter.convertModelPositionToViewPosition(r);return N.CursorState.fromViewState(o.viewState.move(s,i.lineNumber,i.column,0))}static simpleMove(c,o,s,a,u,r){switch(s){case 0:return r===4?this._moveHalfLineLeft(c,o,a):this._moveLeft(c,o,a,u);case 1:return r===4?this._moveHalfLineRight(c,o,a):this._moveRight(c,o,a,u);case 2:return r===2?this._moveUpByViewLines(c,o,a,u):this._moveUpByModelLines(c,o,a,u);case 3:return r===2?this._moveDownByViewLines(c,o,a,u):this._moveDownByModelLines(c,o,a,u);case 4:return r===2?o.map(i=>N.CursorState.fromViewState(M.MoveOperations.moveToPrevBlankLine(c.cursorConfig,c,i.viewState,a))):o.map(i=>N.CursorState.fromModelState(M.MoveOperations.moveToPrevBlankLine(c.cursorConfig,c.model,i.modelState,a)));case 5:return r===2?o.map(i=>N.CursorState.fromViewState(M.MoveOperations.moveToNextBlankLine(c.cursorConfig,c,i.viewState,a))):o.map(i=>N.CursorState.fromModelState(M.MoveOperations.moveToNextBlankLine(c.cursorConfig,c.model,i.modelState,a)));case 6:return this._moveToViewMinColumn(c,o,a);case 7:return this._moveToViewFirstNonWhitespaceColumn(c,o,a);case 8:return this._moveToViewCenterColumn(c,o,a);case 9:return this._moveToViewMaxColumn(c,o,a);case 10:return this._moveToViewLastNonWhitespaceColumn(c,o,a);default:return null}}static viewportMove(c,o,s,a,u){const r=c.getCompletelyVisibleViewRange(),i=c.coordinatesConverter.convertViewRangeToModelRange(r);switch(s){case 11:{const n=this._firstLineNumberInRange(c.model,i,u),t=c.model.getLineFirstNonWhitespaceColumn(n);return[this._moveToModelPosition(c,o[0],a,n,t)]}case 13:{const n=this._lastLineNumberInRange(c.model,i,u),t=c.model.getLineFirstNonWhitespaceColumn(n);return[this._moveToModelPosition(c,o[0],a,n,t)]}case 12:{const n=Math.round((i.startLineNumber+i.endLineNumber)/2),t=c.model.getLineFirstNonWhitespaceColumn(n);return[this._moveToModelPosition(c,o[0],a,n,t)]}case 14:{let n=[];for(let t=0,l=o.length;ts.endLineNumber-1&&(u=s.endLineNumber-1),u1;let r=[];for(let i=0,n=o.length;i1;let r=[];for(let i=0,n=o.length;ig){let p=d-g;for(let c=0;c=C+1&&this.lastAddedCursorIndex--,this.secondaryCursors[C].dispose(this.context),this.secondaryCursors.splice(C,1)}_getAll(){let C=[];C[0]=this.primaryCursor;for(let d=0,g=this.secondaryCursors.length;dg.selection.startLineNumber===p.selection.startLineNumber?g.selection.startColumn-p.selection.startColumn:g.selection.startLineNumber-p.selection.startLineNumber);for(let g=0;gi&&h.index--;C.splice(i,1),d.splice(r,1),this._removeSecondaryCursor(i-1),g--}}}}}}e.CursorCollection=w}),define(Q[242],J([0,1,12,8,538,42,181,182,3,21,170,2,173]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Cursor=e.CursorModelState=void 0;class s{constructor(n,t){this.modelVersionId=n.getVersionId(),this.cursorState=t.getCursorStates()}equals(n){if(!n||this.modelVersionId!==n.modelVersionId||this.cursorState.length!==n.cursorState.length)return!1;for(let t=0,l=this.cursorState.length;t=t.length||!t[l].strictContainsRange(n[l]))return!1;return!0}}class u extends c.Disposable{constructor(n,t,l,h){super();this._model=n,this._knownModelVersionId=this._model.getVersionId(),this._viewModel=t,this._coordinatesConverter=l,this.context=new w.CursorContext(this._model,this._coordinatesConverter,h),this._cursors=new M.CursorCollection(this.context),this._hasFocus=!1,this._isHandling=!1,this._isDoingComposition=!1,this._selectionsWhenCompositionStarted=null,this._columnSelectData=null,this._autoClosedActions=[],this._prevEditOperationType=0}dispose(){this._cursors.dispose(),this._autoClosedActions=c.dispose(this._autoClosedActions),super.dispose()}updateConfiguration(n){this.context=new w.CursorContext(this._model,this._coordinatesConverter,n),this._cursors.updateContext(this.context)}onLineMappingChanged(n){this._knownModelVersionId===this._model.getVersionId()&&this.setStates(n,"viewModel",0,this.getCursorStates())}setHasFocus(n){this._hasFocus=n}_validateAutoClosedActions(){if(this._autoClosedActions.length>0){let n=this._cursors.getSelections();for(let t=0;tu.MAX_CURSOR_COUNT&&(h=h.slice(0,u.MAX_CURSOR_COUNT),m=!0);const _=new s(this._model,this);return this._cursors.setStates(h),this._cursors.normalize(),this._columnSelectData=null,this._validateAutoClosedActions(),this._emitStateChangedIfNecessary(n,t,l,_,m)}setCursorColumnSelectData(n){this._columnSelectData=n}revealPrimary(n,t,l,h){const m=this._cursors.getViewPositions();if(m.length>1){this._emitCursorRevealRange(n,t,null,this._cursors.getViewSelections(),0,l,h);return}else{const _=m[0],f=new d.Range(_.lineNumber,_.column,_.lineNumber,_.column);this._emitCursorRevealRange(n,t,f,null,0,l,h)}}_revealPrimaryCursor(n,t,l,h,m){const _=this._cursors.getViewPositions();if(_.length>1)this._emitCursorRevealRange(n,t,null,this._cursors.getViewSelections(),l,h,m);else{const f=_[0],v=new d.Range(f.lineNumber,f.column,f.lineNumber,f.column);this._emitCursorRevealRange(n,t,v,null,l,h,m)}}_emitCursorRevealRange(n,t,l,h,m,_,f){n.emitViewEvent(new p.ViewRevealRangeRequestEvent(t,l,h,m,_,f))}saveState(){let n=[];const t=this._cursors.getSelections();for(let l=0,h=t.length;l0){const h=w.CursorState.fromModelSelections(t.resultingSelection);this.setStates(n,"modelChange",t.isUndoing?5:t.isRedoing?6:2,h)&&this._revealPrimaryCursor(n,"modelChange",0,!0,0)}else{const h=this._cursors.readSelectionFromMarkers();this.setStates(n,"modelChange",2,w.CursorState.fromModelSelections(h))}}}getSelection(){return this._cursors.getPrimaryCursor().modelState.selection}getTopMostViewPosition(){return this._cursors.getTopMostViewPosition()}getBottomMostViewPosition(){return this._cursors.getBottomMostViewPosition()}getCursorColumnSelectData(){if(this._columnSelectData)return this._columnSelectData;const n=this._cursors.getPrimaryCursor(),t=n.viewState.selectionStart.getStartPosition(),l=n.viewState.position;return{isReal:!1,fromViewLineNumber:t.lineNumber,fromViewVisualColumn:w.CursorColumns.visibleColumnFromColumn2(this.context.cursorConfig,this._viewModel,t),toViewLineNumber:l.lineNumber,toViewVisualColumn:w.CursorColumns.visibleColumnFromColumn2(this.context.cursorConfig,this._viewModel,l)}}getSelections(){return this._cursors.getSelections()}setSelections(n,t,l,h){this.setStates(n,t,h,w.CursorState.fromModelSelections(l))}getPrevEditOperationType(){return this._prevEditOperationType}setPrevEditOperationType(n){this._prevEditOperationType=n}_pushAutoClosedAction(n,t){let l=[],h=[];for(let f=0,v=n.length;f0&&this._pushAutoClosedAction(l,h),this._prevEditOperationType=n.type}n.shouldPushStackElementAfter&&this._model.pushStackElement()}}_interpretCommandResult(n){(!n||n.length===0)&&(n=this._cursors.readSelectionFromMarkers()),this._columnSelectData=null,this._cursors.setSelections(n),this._cursors.normalize()}_emitStateChangedIfNecessary(n,t,l,h,m){const _=new s(this._model,this);if(_.equals(h))return!1;const f=this._cursors.getSelections(),v=this._cursors.getViewSelections();if(n.emitViewEvent(new p.ViewCursorStateChangedEvent(v,f)),!h||h.cursorState.length!==_.cursorState.length||_.cursorState.some((y,L)=>!y.modelState.equals(h.cursorState[L].modelState))){const y=h?h.cursorState.map(I=>I.modelState.selection):null,L=h?h.modelVersionId:0;n.emitOutgoingEvent(new o.CursorStateChangedEvent(y,f,L,_.modelVersionId,t||"keyboard",l,m))}return!0}_findAutoClosingPairs(n){if(!n.length)return null;let t=[];for(let l=0,h=n.length;l=0)return null;const _=m.text.match(/([)\]}>'"`])([^)\]}>'"`]*)$/);if(!_)return null;const f=_[1],v=this.context.cursorConfig.autoClosingPairs.autoClosingPairsCloseSingleChar.get(f);if(!v||v.length!==1)return null;const y=v[0].open,L=m.text.length-_[2].length-1,I=m.text.lastIndexOf(y,L-1);if(I===-1)return null;t.push([I,L])}return t}executeEdits(n,t,l,h){let m=null;t==="snippet"&&(m=this._findAutoClosingPairs(l)),m&&(l[0]._isTracked=!0);let _=[],f=[];const v=this._model.pushEditOperations(this.getSelections(),l,y=>{if(m)for(let I=0,k=m.length;I0&&this._pushAutoClosedAction(_,f)}_executeEdit(n,t,l,h=0){if(!this.context.cursorConfig.readOnly){const m=new s(this._model,this);this._cursors.stopTrackingSelections(),this._isHandling=!0;try{this._cursors.ensureValidState(),n()}catch(_){b.onUnexpectedError(_)}this._isHandling=!1,this._cursors.startTrackingSelections(),this._validateAutoClosedActions(),this._emitStateChangedIfNecessary(t,l,h,m,!1)&&this._revealPrimaryCursor(t,l,0,!0,0)}}setIsDoingComposition(n){this._isDoingComposition=n}startComposition(n){this._selectionsWhenCompositionStarted=this.getSelections().slice(0)}endComposition(n,t){this._executeEdit(()=>{if(t==="keyboard"){const l=a.getAllAutoClosedCharacters(this._autoClosedActions);this._executeEditOperation(C.TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType,this.context.cursorConfig,this._model,this._selectionsWhenCompositionStarted,this.getSelections(),l)),this._selectionsWhenCompositionStarted=null}},n,t)}type(n,t,l){this._executeEdit(()=>{if(l==="keyboard"){const h=t.length;let m=0;for(;m{const y=v.getPosition();return new g.Selection(y.lineNumber,y.column+m,y.lineNumber,y.column+m)});this.setSelections(n,_,f,0)}return}this._executeEdit(()=>{this._executeEditOperation(C.TypeOperations.compositionType(this._prevEditOperationType,this.context.cursorConfig,this._model,this.getSelections(),t,l,h,m))},n,_)}paste(n,t,l,h,m){this._executeEdit(()=>{this._executeEditOperation(C.TypeOperations.paste(this.context.cursorConfig,this._model,this.getSelections(),t,l,h||[]))},n,m,4)}cut(n,t){this._executeEdit(()=>{this._executeEditOperation(S.DeleteOperations.cut(this.context.cursorConfig,this._model,this.getSelections()))},n,t)}executeCommand(n,t,l){this._executeEdit(()=>{this._cursors.killSecondaryCursors(),this._executeEditOperation(new w.EditOperationResult(0,[t],{shouldPushStackElementBefore:!1,shouldPushStackElementAfter:!1}))},n,l)}executeCommands(n,t,l){this._executeEdit(()=>{this._executeEditOperation(new w.EditOperationResult(0,t,{shouldPushStackElementBefore:!1,shouldPushStackElementAfter:!1}))},n,l)}}e.Cursor=u,u.MAX_CURSOR_COUNT=1e4;class r{static executeCommands(n,t,l){const h={model:n,selectionsBefore:t,trackedRanges:[],trackedRangesDirection:[]},m=this._innerExecuteCommands(h,l);for(let _=0,f=h.trackedRanges.length;_0&&(_[0]._isTracked=!0);let f=n.model.pushEditOperations(n.selectionsBefore,_,y=>{let L=[];for(let E=0;EE.identifier.minor-T.identifier.minor;let k=[];for(let E=0;E0?(L[E].sort(I),k[E]=t[E].computeCursorState(n.model,{getInverseEditOperations:()=>L[E],getTrackedSelection:T=>{const O=parseInt(T,10),A=n.model._getTrackedRange(n.trackedRanges[O]);return n.trackedRangesDirection[O]===0?new g.Selection(A.startLineNumber,A.startColumn,A.endLineNumber,A.endColumn):new g.Selection(A.endLineNumber,A.endColumn,A.startLineNumber,A.startColumn)}})):k[E]=n.selectionsBefore[E];return k});f||(f=n.selectionsBefore);let v=[];for(let y in m)m.hasOwnProperty(y)&&v.push(parseInt(y,10));v.sort((y,L)=>L-y);for(const y of v)f.splice(y,1);return f}static _arrayIsEmpty(n){for(let t=0,l=n.length;t{d.Range.isEmpty(I)&&k===""||h.push({identifier:{major:t,minor:m++},range:I,text:k,forceMoveMarkers:E,isAutoWhitespaceEdit:l.insertsAutoWhitespace})};let f=!1;const L={addEditOperation:_,addTrackedEditOperation:(I,k,E)=>{f=!0,_(I,k,E)},trackSelection:(I,k)=>{const E=g.Selection.liftSelection(I);let T;if(E.isEmpty())if(typeof k=="boolean")k?T=2:T=3;else{const B=n.model.getLineMaxColumn(E.startLineNumber);E.startColumn===B?T=2:T=3}else T=1;const O=n.trackedRanges.length,A=n.model._setTrackedRange(null,E,T);return n.trackedRanges[O]=A,n.trackedRangesDirection[O]=E.getDirection(),O.toString()}};try{l.getEditOperations(n.model,L)}catch(I){return b.onUnexpectedError(I),{operations:[],hadTrackedEditOperation:!1}}return{operations:h,hadTrackedEditOperation:f}}static _getLoserCursorMap(n){n=n.slice(0),n.sort((l,h)=>-d.Range.compareRangesUsingEnds(l.range,h.range));let t={};for(let l=1;lm.identifier.major?_=h.identifier.major:_=m.identifier.major,t[_.toString()]=!0;for(let f=0;f0&&l--}}return t}}}),define(Q[243],J([0,1,8,113,76]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.tokenizeLineToHTML=e.tokenizeToString=void 0;const w={getInitialState:()=>M.NULL_STATE,tokenize2:(g,p,c,o)=>M.nullTokenize2(0,g,c,o)};function S(g,p=w){return d(g,p||w)}e.tokenizeToString=S;function C(g,p,c,o,s,a,u){let r="
    ",i=o,n=0;for(let t=0,l=p.getCount();t0;)m+=u?" ":" ",f--;break;case 60:m+="<";break;case 62:m+=">";break;case 38:m+="&";break;case 0:m+="�";break;case 65279:case 8232:case 8233:case 133:m+="\uFFFD";break;case 13:m+="​";break;case 32:m+=u?" ":" ";break;default:m+=String.fromCharCode(_)}}if(r+=`${m}`,h>s||i>=s)break}}return r+="
    ",r}e.tokenizeLineToHTML=C;function d(g,p){let c='
    ',o=b.splitLines(g),s=p.getInitialState();for(let a=0,u=o.length;a0&&(c+="
    ");let i=p.tokenize2(r,!0,s,0);N.LineTokens.convertToEndOffset(i.tokens,r.length);let t=new N.LineTokens(i.tokens,r).inflate(),l=0;for(let h=0,m=t.getCount();h${b.escape(r.substring(l,f))}`,l=f}s=i.endState}return c+="
    ",c}}),define(Q[67],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ITextModelService=void 0,e.ITextModelService=b.createDecorator("textModelService")}),define(Q[137],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ITextResourcePropertiesService=e.ITextResourceConfigurationService=void 0,e.ITextResourceConfigurationService=b.createDecorator("textResourceConfigurationService"),e.ITextResourcePropertiesService=b.createDecorator("textResourcePropertiesService")}),define(Q[244],J([0,1,6,216,18]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MinimapTokensColorTracker=void 0;class w{constructor(){this._onDidChange=new b.Emitter,this.onDidChange=this._onDidChange.event,this._updateColorMap(),M.TokenizationRegistry.onDidChange(C=>{C.changedColorMap&&this._updateColorMap()})}static getInstance(){return this._INSTANCE||(this._INSTANCE=new w),this._INSTANCE}_updateColorMap(){const C=M.TokenizationRegistry.getColorMap();if(!C){this._colors=[N.RGBA8.Empty],this._backgroundIsLight=!0;return}this._colors=[N.RGBA8.Empty];for(let g=1;g=.5,this._onDidChange.fire(void 0)}getColor(C){return(C<1||C>=this._colors.length)&&(C=2),this._colors[C]}backgroundIsLight(){return this._backgroundIsLight}}e.MinimapTokensColorTracker=w,w._INSTANCE=null}),define(Q[539],J([0,1,19,14,3,31,170,222,63]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IdentityLinesCollection=e.IdentityCoordinatesConverter=e.SplitLine=e.SplitLinesCollection=e.CoordinatesConverter=void 0;class g{constructor(_){this._lines=_}convertViewPositionToModelPosition(_){return this._lines.convertViewPositionToModelPosition(_.lineNumber,_.column)}convertViewRangeToModelRange(_){return this._lines.convertViewRangeToModelRange(_)}validateViewPosition(_,f){return this._lines.validateViewPosition(_.lineNumber,_.column,f)}validateViewRange(_,f){return this._lines.validateViewRange(_,f)}convertModelPositionToViewPosition(_){return this._lines.convertModelPositionToViewPosition(_.lineNumber,_.column)}convertModelRangeToViewRange(_){return this._lines.convertModelRangeToViewRange(_)}modelPositionIsVisible(_){return this._lines.modelPositionIsVisible(_.lineNumber,_.column)}getModelLineViewLineCount(_){return this._lines.getModelLineViewLineCount(_)}}e.CoordinatesConverter=g;class p{constructor(_){this._counts=_,this._isValid=!1,this._validEndIndex=-1,this._modelToView=[],this._viewToModel=[]}_invalidate(_){this._isValid=!1,this._validEndIndex=Math.min(this._validEndIndex,_-1)}_ensureValid(){if(!this._isValid){for(let _=this._validEndIndex+1,f=this._counts.length;_0?this._modelToView[_-1]:0;this._modelToView[_]=y+v;for(let L=0;L0?this._modelToView[f-1]:0;return new C.PrefixSumIndexOfResult(f,_-v)}}class c{constructor(_,f,v,y,L,I,k,E){this.model=_,this._validModelVersionId=-1,this._domLineBreaksComputerFactory=f,this._monospaceLineBreaksComputerFactory=v,this.fontInfo=y,this.tabSize=L,this.wrappingStrategy=I,this.wrappingColumn=k,this.wrappingIndent=E,this._constructLines(!0,null)}dispose(){this.hiddenAreasIds=this.model.deltaDecorations(this.hiddenAreasIds,[])}createCoordinatesConverter(){return new g(this)}_constructLines(_,f){this.lines=[],_&&(this.hiddenAreasIds=[]);let v=this.model.getLinesContent();const y=v.length,L=this.createLineBreaksComputer();for(let F=0;Fthis.model.getDecorationRange(F)).sort(M.Range.compareRangesUsingStarts),T=1,O=0,A=-1,B=A+1=T&&D<=O,W=n(I[F],!R);k[F]=W.getViewLineCount(),this.lines[F]=W}this._validModelVersionId=this.model.getVersionId(),this.prefixSumComputer=new p(k)}getHiddenAreas(){return this.hiddenAreasIds.map(_=>this.model.getDecorationRange(_))}_reduceRanges(_){if(_.length===0)return[];let f=_.map(I=>this.model.validateRange(I)).sort(M.Range.compareRangesUsingStarts),v=[],y=f[0].startLineNumber,L=f[0].endLineNumber;for(let I=1,k=f.length;IL+1?(v.push(new M.Range(y,1,L,1)),y=E.startLineNumber,L=E.endLineNumber):E.endLineNumber>L&&(L=E.endLineNumber)}return v.push(new M.Range(y,1,L,1)),v}setHiddenAreas(_){let f=this._reduceRanges(_),v=this.hiddenAreasIds.map(A=>this.model.getDecorationRange(A)).sort(M.Range.compareRangesUsingStarts);if(f.length===v.length){let A=!1;for(let B=0;B=I&&B<=k?this.lines[A].isVisible()&&(this.lines[A]=this.lines[A].setVisible(!1),F=!0):(O=!0,this.lines[A].isVisible()||(this.lines[A]=this.lines[A].setVisible(!0),F=!0)),F){let D=this.lines[A].getViewLineCount();this.prefixSumComputer.changeValue(A,D)}}return O||this.setHiddenAreas([]),!0}modelPositionIsVisible(_,f){return _<1||_>this.lines.length?!1:this.lines[_-1].isVisible()}getModelLineViewLineCount(_){return _<1||_>this.lines.length?1:this.lines[_-1].getViewLineCount()}setTabSize(_){return this.tabSize===_?!1:(this.tabSize=_,this._constructLines(!1,null),!0)}setWrappingSettings(_,f,v,y){const L=this.fontInfo.equals(_),I=this.wrappingStrategy===f,k=this.wrappingColumn===v,E=this.wrappingIndent===y;if(L&&I&&k&&E)return!1;const T=L&&I&&!k&&E;this.fontInfo=_,this.wrappingStrategy=f,this.wrappingColumn=v,this.wrappingIndent=y;let O=null;if(T){O=[];for(let A=0,B=this.lines.length;A2&&!this.lines[f-2].isVisible();let I=f===1?1:this.prefixSumComputer.getAccumulatedValue(f-2)+1,k=0,E=[],T=[];for(let O=0,A=y.length;OE?(O=f===1?1:this.prefixSumComputer.getAccumulatedValue(f-2)+1,A=O+E-1,D=A+1,R=D+(L-E)-1,T=!0):Lf?f:_|0}getActiveIndentGuide(_,f,v){_=this._toValidViewLineNumber(_),f=this._toValidViewLineNumber(f),v=this._toValidViewLineNumber(v);const y=this.convertViewPositionToModelPosition(_,this.getViewLineMinColumn(_)),L=this.convertViewPositionToModelPosition(f,this.getViewLineMinColumn(f)),I=this.convertViewPositionToModelPosition(v,this.getViewLineMinColumn(v)),k=this.model.getActiveIndentGuide(y.lineNumber,L.lineNumber,I.lineNumber),E=this.convertModelPositionToViewPosition(k.startLineNumber,1),T=this.convertModelPositionToViewPosition(k.endLineNumber,this.model.getLineMaxColumn(k.endLineNumber));return{startLineNumber:E.lineNumber,endLineNumber:T.lineNumber,indent:k.indent}}getViewLinesIndentGuides(_,f){_=this._toValidViewLineNumber(_),f=this._toValidViewLineNumber(f);const v=this.convertViewPositionToModelPosition(_,this.getViewLineMinColumn(_)),y=this.convertViewPositionToModelPosition(f,this.getViewLineMaxColumn(f));let L=[],I=[],k=[];const E=v.lineNumber-1,T=y.lineNumber-1;let O=null;for(let D=E;D<=T;D++){const R=this.lines[D];if(R.isVisible()){let W=R.getViewLineNumberOfModelPosition(0,D===E?v.column:1),x=R.getViewLineNumberOfModelPosition(0,this.model.getLineMaxColumn(D+1)),K=x-W+1,Y=0;K>1&&R.getViewLineMinColumn(this.model,D+1,x)===1&&(Y=W===0?1:2),I.push(K),k.push(Y),O===null&&(O=new N.Position(D+1,0))}else O!==null&&(L=L.concat(this.model.getLinesIndentGuides(O.lineNumber,D)),O=null)}O!==null&&(L=L.concat(this.model.getLinesIndentGuides(O.lineNumber,y.lineNumber)),O=null);const A=f-_+1;let B=new Array(A),F=0;for(let D=0,R=L.length;Df&&(D=!0,F=f-L+1);let R=B+F;if(A.getViewLinesData(this.model,T+1,B,R,L-_,v,E),L+=F,D)break}}return E}validateViewPosition(_,f,v){_=this._toValidViewLineNumber(_);let y=this.prefixSumComputer.getIndexOf(_-1),L=y.index,I=y.remainder,k=this.lines[L],E=k.getViewLineMinColumn(this.model,L+1,I),T=k.getViewLineMaxColumn(this.model,L+1,I);fT&&(f=T);let O=k.getModelColumnOfViewPosition(I,f);return this.model.validatePosition(new N.Position(L+1,O)).equals(v)?new N.Position(_,f):this.convertModelPositionToViewPosition(v.lineNumber,v.column)}validateViewRange(_,f){const v=this.validateViewPosition(_.startLineNumber,_.startColumn,f.getStartPosition()),y=this.validateViewPosition(_.endLineNumber,_.endColumn,f.getEndPosition());return new M.Range(v.lineNumber,v.column,y.lineNumber,y.column)}convertViewPositionToModelPosition(_,f){_=this._toValidViewLineNumber(_);let v=this.prefixSumComputer.getIndexOf(_-1),y=v.index,L=v.remainder,I=this.lines[y].getModelColumnOfViewPosition(L,f);return this.model.validatePosition(new N.Position(y+1,I))}convertViewRangeToModelRange(_){const f=this.convertViewPositionToModelPosition(_.startLineNumber,_.startColumn),v=this.convertViewPositionToModelPosition(_.endLineNumber,_.endColumn);return new M.Range(f.lineNumber,f.column,v.lineNumber,v.column)}convertModelPositionToViewPosition(_,f){const v=this.model.validatePosition(new N.Position(_,f)),y=v.lineNumber,L=v.column;let I=y-1,k=!1;for(;I>0&&!this.lines[I].isVisible();)I--,k=!0;if(I===0&&!this.lines[I].isVisible())return new N.Position(1,1);const E=1+(I===0?0:this.prefixSumComputer.getAccumulatedValue(I-1));let T;return k?T=this.lines[I].getViewPositionOfModelPosition(E,this.model.getLineMaxColumn(I+1)):T=this.lines[y-1].getViewPositionOfModelPosition(E,L),T}convertModelRangeToViewRange(_){let f=this.convertModelPositionToViewPosition(_.startLineNumber,_.startColumn),v=this.convertModelPositionToViewPosition(_.endLineNumber,_.endColumn);return _.startLineNumber===_.endLineNumber&&f.lineNumber!==v.lineNumber&&v.column===this.getViewLineMinColumn(v.lineNumber)?new M.Range(f.lineNumber,f.column,v.lineNumber-1,this.getViewLineMaxColumn(v.lineNumber-1)):new M.Range(f.lineNumber,f.column,v.lineNumber,v.column)}_getViewLineNumberForModelPosition(_,f){let v=_-1;if(this.lines[v].isVisible()){const L=1+(v===0?0:this.prefixSumComputer.getAccumulatedValue(v-1));return this.lines[v].getViewLineNumberOfModelPosition(L,f)}for(;v>0&&!this.lines[v].isVisible();)v--;if(v===0&&!this.lines[v].isVisible())return 1;const y=1+(v===0?0:this.prefixSumComputer.getAccumulatedValue(v-1));return this.lines[v].getViewLineNumberOfModelPosition(y,this.model.getLineMaxColumn(v+1))}getAllOverviewRulerDecorations(_,f,v){const y=this.model.getOverviewRulerDecorations(_,f),L=new h;for(const I of y){const k=I.options.overviewRuler,E=k?k.position:0;if(E!==0){const T=k.getColor(v),O=this._getViewLineNumberForModelPosition(I.range.startLineNumber,I.range.startColumn),A=this._getViewLineNumberForModelPosition(I.range.endLineNumber,I.range.endColumn);L.accept(T,O,A,E)}}return L.result}getDecorationsInRange(_,f,v){const y=this.convertViewPositionToModelPosition(_.startLineNumber,_.startColumn),L=this.convertViewPositionToModelPosition(_.endLineNumber,_.endColumn);if(L.lineNumber-y.lineNumber<=_.endLineNumber-_.startLineNumber)return this.model.getDecorationsInRange(new M.Range(y.lineNumber,1,L.lineNumber,L.column),f,v);let I=[];const k=y.lineNumber-1,E=L.lineNumber-1;let T=null;for(let F=k;F<=E;F++)if(this.lines[F].isVisible())T===null&&(T=new N.Position(F+1,F===k?y.column:1));else if(T!==null){const R=this.model.getLineMaxColumn(F);I=I.concat(this.model.getDecorationsInRange(new M.Range(T.lineNumber,T.column,F,R),f,v)),T=null}T!==null&&(I=I.concat(this.model.getDecorationsInRange(new M.Range(T.lineNumber,T.column,L.lineNumber,L.column),f,v)),T=null),I.sort((F,D)=>{const R=M.Range.compareRangesUsingStarts(F.range,D.range);return R===0?F.idD.id?1:0:R});let O=[],A=0,B=null;for(const F of I){const D=F.id;B!==D&&(B=D,O[A++]=F)}return O}}e.SplitLinesCollection=c;class o{constructor(){}isVisible(){return!0}setVisible(_){return _?this:s.INSTANCE}getLineBreakData(){return null}getViewLineCount(){return 1}getViewLineContent(_,f,v){return _.getLineContent(f)}getViewLineLength(_,f,v){return _.getLineLength(f)}getViewLineMinColumn(_,f,v){return _.getLineMinColumn(f)}getViewLineMaxColumn(_,f,v){return _.getLineMaxColumn(f)}getViewLineData(_,f,v){let y=_.getLineTokens(f),L=y.getLineContent();return new d.ViewLineData(L,!1,1,L.length+1,0,y.inflate())}getViewLinesData(_,f,v,y,L,I,k){if(!I[L]){k[L]=null;return}k[L]=this.getViewLineData(_,f,0)}getModelColumnOfViewPosition(_,f){return f}getViewPositionOfModelPosition(_,f){return new N.Position(_,f)}getViewLineNumberOfModelPosition(_,f){return _}}o.INSTANCE=new o;class s{constructor(){}isVisible(){return!1}setVisible(_){return _?o.INSTANCE:this}getLineBreakData(){return null}getViewLineCount(){return 0}getViewLineContent(_,f,v){throw new Error("Not supported")}getViewLineLength(_,f,v){throw new Error("Not supported")}getViewLineMinColumn(_,f,v){throw new Error("Not supported")}getViewLineMaxColumn(_,f,v){throw new Error("Not supported")}getViewLineData(_,f,v){throw new Error("Not supported")}getViewLinesData(_,f,v,y,L,I,k){throw new Error("Not supported")}getModelColumnOfViewPosition(_,f){throw new Error("Not supported")}getViewPositionOfModelPosition(_,f){throw new Error("Not supported")}getViewLineNumberOfModelPosition(_,f){throw new Error("Not supported")}}s.INSTANCE=new s;class a{constructor(_,f){this._lineBreakData=_,this._isVisible=f}isVisible(){return this._isVisible}setVisible(_){return this._isVisible=_,this}getLineBreakData(){return this._lineBreakData}getViewLineCount(){return this._isVisible?this._lineBreakData.breakOffsets.length:0}getInputStartOffsetOfOutputLineIndex(_){return d.LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets,_,0)}getInputEndOffsetOfOutputLineIndex(_,f,v){return v+1===this._lineBreakData.breakOffsets.length?_.getLineMaxColumn(f)-1:d.LineBreakData.getInputOffsetOfOutputPosition(this._lineBreakData.breakOffsets,v+1,0)}getViewLineContent(_,f,v){if(!this._isVisible)throw new Error("Not supported");let y=this.getInputStartOffsetOfOutputLineIndex(v),L=this.getInputEndOffsetOfOutputLineIndex(_,f,v),I=_.getValueInRange({startLineNumber:f,startColumn:y+1,endLineNumber:f,endColumn:L+1});return v>0&&(I=r(this._lineBreakData.wrappedTextIndentLength)+I),I}getViewLineLength(_,f,v){if(!this._isVisible)throw new Error("Not supported");let y=this.getInputStartOffsetOfOutputLineIndex(v),I=this.getInputEndOffsetOfOutputLineIndex(_,f,v)-y;return v>0&&(I=this._lineBreakData.wrappedTextIndentLength+I),I}getViewLineMinColumn(_,f,v){if(!this._isVisible)throw new Error("Not supported");return v>0?this._lineBreakData.wrappedTextIndentLength+1:1}getViewLineMaxColumn(_,f,v){if(!this._isVisible)throw new Error("Not supported");return this.getViewLineContent(_,f,v).length+1}getViewLineData(_,f,v){if(!this._isVisible)throw new Error("Not supported");let y=this.getInputStartOffsetOfOutputLineIndex(v),L=this.getInputEndOffsetOfOutputLineIndex(_,f,v),I=_.getValueInRange({startLineNumber:f,startColumn:y+1,endLineNumber:f,endColumn:L+1});v>0&&(I=r(this._lineBreakData.wrappedTextIndentLength)+I);let k=v>0?this._lineBreakData.wrappedTextIndentLength+1:1,E=I.length+1,T=v+10&&(O=this._lineBreakData.wrappedTextIndentLength);let A=_.getLineTokens(f);const B=v===0?0:this._lineBreakData.breakOffsetsVisibleColumn[v-1];return new d.ViewLineData(I,T,k,E,B,A.sliceAndInflate(y,L,O))}getViewLinesData(_,f,v,y,L,I,k){if(!this._isVisible)throw new Error("Not supported");for(let E=v;E0&&(v0&&(L+=this._lineBreakData.wrappedTextIndentLength),new N.Position(_+y,L)}getViewLineNumberOfModelPosition(_,f){if(!this._isVisible)throw new Error("Not supported");const v=d.LineBreakData.getOutputPositionOfInputOffset(this._lineBreakData.breakOffsets,f-1);return _+v.outputLineIndex}}e.SplitLine=a;let u=[""];function r(m){if(m>=u.length)for(let _=1;_<=m;_++)u[_]=i(_);return u[m]}function i(m){return new Array(m+1).join(" ")}function n(m,_){return m===null?_?o.INSTANCE:s.INSTANCE:new a(m,_)}class t{constructor(_){this._lines=_}_validPosition(_){return this._lines.model.validatePosition(_)}_validRange(_){return this._lines.model.validateRange(_)}convertViewPositionToModelPosition(_){return this._validPosition(_)}convertViewRangeToModelRange(_){return this._validRange(_)}validateViewPosition(_,f){return this._validPosition(f)}validateViewRange(_,f){return this._validRange(f)}convertModelPositionToViewPosition(_){return this._validPosition(_)}convertModelRangeToViewRange(_){return this._validRange(_)}modelPositionIsVisible(_){const f=this._lines.model.getLineCount();return!(_.lineNumber<1||_.lineNumber>f)}getModelLineViewLineCount(_){return 1}}e.IdentityCoordinatesConverter=t;class l{constructor(_){this.model=_}dispose(){}createCoordinatesConverter(){return new t(this)}getHiddenAreas(){return[]}setHiddenAreas(_){return!1}setTabSize(_){return!1}setWrappingSettings(_,f,v,y){return!1}createLineBreaksComputer(){let _=[];return{addRequest:(f,v)=>{_.push(null)},finalize:()=>_}}onModelFlushed(){}onModelLinesDeleted(_,f,v){return new S.ViewLinesDeletedEvent(f,v)}onModelLinesInserted(_,f,v,y){return new S.ViewLinesInsertedEvent(f,v)}onModelLineChanged(_,f,v){return[!1,new S.ViewLinesChangedEvent(f,f),null,null]}acceptVersionId(_){}getViewLineCount(){return this.model.getLineCount()}getActiveIndentGuide(_,f,v){return{startLineNumber:_,endLineNumber:_,indent:0}}getViewLinesIndentGuides(_,f){const v=f-_+1;let y=new Array(v);for(let L=0;L=f){v>k&&(L[L.length-1]=v);return}L.push(y,f,v)}else this.result[_]=[y,f,v]}}}),define(Q[540],J([0,1,29,2,8,38,14,3,18,243,244,170,398,539,63,455,15,17,242,42,173]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewModel=void 0;const h=!0;class m extends N.Disposable{constructor(f,v,y,L,I,k){super();if(this._editorId=f,this._configuration=v,this.model=y,this._eventDispatcher=new l.ViewModelEventDispatcher,this.onEvent=this._eventDispatcher.onEvent,this.cursorConfig=new t.CursorConfiguration(this.model.getLanguageIdentifier(),this.model.getOptions(),this._configuration),this._tokenizeViewportSoon=this._register(new r.RunOnceScheduler(()=>this.tokenizeViewport(),50)),this._updateConfigurationViewLineCount=this._register(new r.RunOnceScheduler(()=>this._updateConfigurationViewLineCountNow(),0)),this._hasFocus=!1,this._viewportStartLine=-1,this._viewportStartLineTrackedRange=null,this._viewportStartLineDelta=0,h&&this.model.isTooLargeForTokenization())this._lines=new s.IdentityLinesCollection(this.model);else{const E=this._configuration.options,T=E.get(38),O=E.get(118),A=E.get(125),B=E.get(117);this._lines=new s.SplitLinesCollection(this.model,L,I,T,this.model.getOptions().tabSize,O,A.wrappingColumn,B)}this.coordinatesConverter=this._lines.createCoordinatesConverter(),this._cursor=this._register(new n.Cursor(y,this,this.coordinatesConverter,this.cursorConfig)),this.viewLayout=this._register(new o.ViewLayout(this._configuration,this.getLineCount(),k)),this._register(this.viewLayout.onDidScroll(E=>{E.scrollTopChanged&&this._tokenizeViewportSoon.schedule(),this._eventDispatcher.emitSingleViewEvent(new c.ViewScrollChangedEvent(E)),this._eventDispatcher.emitOutgoingEvent(new l.ScrollChangedEvent(E.oldScrollWidth,E.oldScrollLeft,E.oldScrollHeight,E.oldScrollTop,E.scrollWidth,E.scrollLeft,E.scrollHeight,E.scrollTop))})),this._register(this.viewLayout.onDidContentSizeChange(E=>{this._eventDispatcher.emitOutgoingEvent(E)})),this._decorations=new u.ViewModelDecorations(this._editorId,this.model,this._configuration,this._lines,this.coordinatesConverter),this._registerModelEvents(),this._register(this._configuration.onDidChangeFast(E=>{try{const T=this._eventDispatcher.beginEmitViewEvents();this._onConfigurationChanged(T,E)}finally{this._eventDispatcher.endEmitViewEvents()}})),this._register(p.MinimapTokensColorTracker.getInstance().onDidChange(()=>{this._eventDispatcher.emitSingleViewEvent(new c.ViewTokensColorsChangedEvent)})),this._updateConfigurationViewLineCountNow()}dispose(){super.dispose(),this._decorations.dispose(),this._lines.dispose(),this.invalidateMinimapColorCache(),this._viewportStartLineTrackedRange=this.model._setTrackedRange(this._viewportStartLineTrackedRange,null,1),this._eventDispatcher.dispose()}createLineBreaksComputer(){return this._lines.createLineBreaksComputer()}addViewEventHandler(f){this._eventDispatcher.addViewEventHandler(f)}removeViewEventHandler(f){this._eventDispatcher.removeViewEventHandler(f)}_updateConfigurationViewLineCountNow(){this._configuration.setViewLineCount(this._lines.getViewLineCount())}tokenizeViewport(){const f=this.viewLayout.getLinesViewportData(),v=this.coordinatesConverter.convertViewPositionToModelPosition(new S.Position(f.startLineNumber,1)),y=this.coordinatesConverter.convertViewPositionToModelPosition(new S.Position(f.endLineNumber,1));this.model.tokenizeViewport(v.lineNumber,y.lineNumber)}setHasFocus(f){this._hasFocus=f,this._cursor.setHasFocus(f),this._eventDispatcher.emitSingleViewEvent(new c.ViewFocusChangedEvent(f)),this._eventDispatcher.emitOutgoingEvent(new l.FocusChangedEvent(!f,f))}onCompositionStart(){this._eventDispatcher.emitSingleViewEvent(new c.ViewCompositionStartEvent)}onCompositionEnd(){this._eventDispatcher.emitSingleViewEvent(new c.ViewCompositionEndEvent)}onDidColorThemeChange(){this._eventDispatcher.emitSingleViewEvent(new c.ViewThemeChangedEvent)}_onConfigurationChanged(f,v){let y=null;if(this._viewportStartLine!==-1){let A=new S.Position(this._viewportStartLine,this.getLineMinColumn(this._viewportStartLine));y=this.coordinatesConverter.convertViewPositionToModelPosition(A)}let L=!1;const I=this._configuration.options,k=I.get(38),E=I.get(118),T=I.get(125),O=I.get(117);if(this._lines.setWrappingSettings(k,E,T.wrappingColumn,O)&&(f.emitViewEvent(new c.ViewFlushedEvent),f.emitViewEvent(new c.ViewLineMappingChangedEvent),f.emitViewEvent(new c.ViewDecorationsChangedEvent(null)),this._cursor.onLineMappingChanged(f),this._decorations.onLineMappingChanged(),this.viewLayout.onFlushed(this.getLineCount()),this.viewLayout.getCurrentScrollTop()!==0&&(L=!0),this._updateConfigurationViewLineCount.schedule()),v.hasChanged(75)&&(this._decorations.reset(),f.emitViewEvent(new c.ViewDecorationsChangedEvent(null))),f.emitViewEvent(new c.ViewConfigurationChangedEvent(v)),this.viewLayout.onConfigurationChanged(v),L&&y){const A=this.coordinatesConverter.convertModelPositionToViewPosition(y),B=this.viewLayout.getVerticalOffsetForLineNumber(A.lineNumber);this.viewLayout.setScrollPosition({scrollTop:B+this._viewportStartLineDelta},1)}t.CursorConfiguration.shouldRecreate(v)&&(this.cursorConfig=new t.CursorConfiguration(this.model.getLanguageIdentifier(),this.model.getOptions(),this._configuration),this._cursor.updateConfiguration(this.cursorConfig))}_registerModelEvents(){this._register(this.model.onDidChangeRawContentFast(f=>{try{const v=this._eventDispatcher.beginEmitViewEvents();let y=!1,L=!1;const I=f.changes,k=f.versionId,E=this._lines.createLineBreaksComputer();for(const A of I)switch(A.changeType){case 4:{for(const B of A.detail)E.addRequest(B,null);break}case 2:{E.addRequest(A.detail,null);break}}const T=E.finalize();let O=0;for(const A of I)switch(A.changeType){case 1:{this._lines.onModelFlushed(),v.emitViewEvent(new c.ViewFlushedEvent),this._decorations.reset(),this.viewLayout.onFlushed(this.getLineCount()),y=!0;break}case 3:{const B=this._lines.onModelLinesDeleted(k,A.fromLineNumber,A.toLineNumber);B!==null&&(v.emitViewEvent(B),this.viewLayout.onLinesDeleted(B.fromLineNumber,B.toLineNumber)),y=!0;break}case 4:{const B=T.slice(O,O+A.detail.length);O+=A.detail.length;const F=this._lines.onModelLinesInserted(k,A.fromLineNumber,A.toLineNumber,B);F!==null&&(v.emitViewEvent(F),this.viewLayout.onLinesInserted(F.fromLineNumber,F.toLineNumber)),y=!0;break}case 2:{const B=T[O];O++;const[F,D,R,W]=this._lines.onModelLineChanged(k,A.lineNumber,B);L=F,D&&v.emitViewEvent(D),R&&(v.emitViewEvent(R),this.viewLayout.onLinesInserted(R.fromLineNumber,R.toLineNumber)),W&&(v.emitViewEvent(W),this.viewLayout.onLinesDeleted(W.fromLineNumber,W.toLineNumber));break}case 5:break}this._lines.acceptVersionId(k),this.viewLayout.onHeightMaybeChanged(),!y&&L&&(v.emitViewEvent(new c.ViewLineMappingChangedEvent),v.emitViewEvent(new c.ViewDecorationsChangedEvent(null)),this._cursor.onLineMappingChanged(v),this._decorations.onLineMappingChanged())}finally{this._eventDispatcher.endEmitViewEvents()}if(this._viewportStartLine=-1,this._configuration.setMaxLineNumber(this.model.getLineCount()),this._updateConfigurationViewLineCountNow(),!this._hasFocus&&this.model.getAttachedEditorCount()>=2&&this._viewportStartLineTrackedRange){const v=this.model._getTrackedRange(this._viewportStartLineTrackedRange);if(v){const y=this.coordinatesConverter.convertModelPositionToViewPosition(v.getStartPosition()),L=this.viewLayout.getVerticalOffsetForLineNumber(y.lineNumber);this.viewLayout.setScrollPosition({scrollTop:L+this._viewportStartLineDelta},1)}}try{const v=this._eventDispatcher.beginEmitViewEvents();this._cursor.onModelContentChanged(v,f)}finally{this._eventDispatcher.endEmitViewEvents()}})),this._register(this.model.onDidChangeTokens(f=>{let v=[];for(let y=0,L=f.ranges.length;y{this._eventDispatcher.emitSingleViewEvent(new c.ViewLanguageConfigurationEvent),this.cursorConfig=new t.CursorConfiguration(this.model.getLanguageIdentifier(),this.model.getOptions(),this._configuration),this._cursor.updateConfiguration(this.cursorConfig)})),this._register(this.model.onDidChangeLanguage(f=>{this.cursorConfig=new t.CursorConfiguration(this.model.getLanguageIdentifier(),this.model.getOptions(),this._configuration),this._cursor.updateConfiguration(this.cursorConfig)})),this._register(this.model.onDidChangeOptions(f=>{if(this._lines.setTabSize(this.model.getOptions().tabSize)){try{const v=this._eventDispatcher.beginEmitViewEvents();v.emitViewEvent(new c.ViewFlushedEvent),v.emitViewEvent(new c.ViewLineMappingChangedEvent),v.emitViewEvent(new c.ViewDecorationsChangedEvent(null)),this._cursor.onLineMappingChanged(v),this._decorations.onLineMappingChanged(),this.viewLayout.onFlushed(this.getLineCount())}finally{this._eventDispatcher.endEmitViewEvents()}this._updateConfigurationViewLineCount.schedule()}this.cursorConfig=new t.CursorConfiguration(this.model.getLanguageIdentifier(),this.model.getOptions(),this._configuration),this._cursor.updateConfiguration(this.cursorConfig)})),this._register(this.model.onDidChangeDecorations(f=>{this._decorations.onModelDecorationsChanged(),this._eventDispatcher.emitSingleViewEvent(new c.ViewDecorationsChangedEvent(f))}))}setHiddenAreas(f){try{const v=this._eventDispatcher.beginEmitViewEvents();this._lines.setHiddenAreas(f)&&(v.emitViewEvent(new c.ViewFlushedEvent),v.emitViewEvent(new c.ViewLineMappingChangedEvent),v.emitViewEvent(new c.ViewDecorationsChangedEvent(null)),this._cursor.onLineMappingChanged(v),this._decorations.onLineMappingChanged(),this.viewLayout.onFlushed(this.getLineCount()),this.viewLayout.onHeightMaybeChanged())}finally{this._eventDispatcher.endEmitViewEvents()}this._updateConfigurationViewLineCount.schedule()}getVisibleRangesPlusViewportAboveBelow(){const f=this._configuration.options.get(124),v=this._configuration.options.get(53),y=Math.max(20,Math.round(f.height/v)),L=this.viewLayout.getLinesViewportData(),I=Math.max(1,L.completelyVisibleStartLineNumber-y),k=Math.min(this.getLineCount(),L.completelyVisibleEndLineNumber+y);return this._toModelVisibleRanges(new C.Range(I,this.getLineMinColumn(I),k,this.getLineMaxColumn(k)))}getVisibleRanges(){const f=this.getCompletelyVisibleViewRange();return this._toModelVisibleRanges(f)}_toModelVisibleRanges(f){const v=this.coordinatesConverter.convertViewRangeToModelRange(f),y=this._lines.getHiddenAreas();if(y.length===0)return[v];let L=[],I=0,k=v.startLineNumber,E=v.startColumn,T=v.endLineNumber,O=v.endColumn;for(let A=0,B=y.length;AT||(kA.startLineNumber);let O="";for(let A=0;A0&&T[A-1]===T[A]||(O+=this.model.getLineContent(T[A])+L);return O}if(I&&v){let T=[],O=0;for(const A of f){const B=A.startLineNumber;A.isEmpty()?B!==O&&T.push(this.model.getLineContent(B)):T.push(this.model.getValueInRange(A,y?2:0)),O=B}return T.length===1?T[0]:T}let E=[];for(const T of f)T.isEmpty()||E.push(this.model.getValueInRange(T,y?2:0));return E.length===1?E[0]:E}getRichTextToCopy(f,v){const y=this.model.getLanguageIdentifier();if(y.id===1||f.length!==1)return null;let L=f[0];if(L.isEmpty()){if(!v)return null;const T=L.startLineNumber;L=new C.Range(T,this.model.getLineMinColumn(T),T,this.model.getLineMaxColumn(T))}const I=this._configuration.options.get(38),k=this._getColorMap(),E=I.fontFamily===w.EDITOR_FONT_DEFAULTS.fontFamily?I.fontFamily:`'${I.fontFamily}', ${w.EDITOR_FONT_DEFAULTS.fontFamily}`;return{mode:y.language,html:`
    `+this._getHTMLToCopy(L,k)+"
    "}}_getHTMLToCopy(f,v){const y=f.startLineNumber,L=f.startColumn,I=f.endLineNumber,k=f.endColumn,E=this.getTabSize();let T="";for(let O=y;O<=I;O++){const A=this.model.getLineTokens(O),B=A.getLineContent(),F=O===y?L-1:0,D=O===I?k-1:B.length;B===""?T+="
    ":T+=g.tokenizeLineToHTML(B,A.inflate(),v,F,D,E,i.isWindows)}return T}_getColorMap(){let f=d.TokenizationRegistry.getColorMap(),v=["#000000"];if(f)for(let y=1,L=f.length;ythis._cursor.setStates(L,f,v,y))}getCursorColumnSelectData(){return this._cursor.getCursorColumnSelectData()}setCursorColumnSelectData(f){this._cursor.setCursorColumnSelectData(f)}getPrevEditOperationType(){return this._cursor.getPrevEditOperationType()}setPrevEditOperationType(f){this._cursor.setPrevEditOperationType(f)}getSelection(){return this._cursor.getSelection()}getSelections(){return this._cursor.getSelections()}getPosition(){return this._cursor.getPrimaryCursorState().modelState.position}setSelections(f,v,y=0){this._withViewEventsCollector(L=>this._cursor.setSelections(L,f,v,y))}saveCursorState(){return this._cursor.saveState()}restoreCursorState(f){this._withViewEventsCollector(v=>this._cursor.restoreState(v,f))}_executeCursorEdit(f){if(this._cursor.context.cursorConfig.readOnly){this._eventDispatcher.emitOutgoingEvent(new l.ReadOnlyEditAttemptEvent);return}this._withViewEventsCollector(f)}executeEdits(f,v,y){this._executeCursorEdit(L=>this._cursor.executeEdits(L,f,v,y))}startComposition(){this._cursor.setIsDoingComposition(!0),this._executeCursorEdit(f=>this._cursor.startComposition(f))}endComposition(f){this._cursor.setIsDoingComposition(!1),this._executeCursorEdit(v=>this._cursor.endComposition(v,f))}type(f,v){this._executeCursorEdit(y=>this._cursor.type(y,f,v))}compositionType(f,v,y,L,I){this._executeCursorEdit(k=>this._cursor.compositionType(k,f,v,y,L,I))}paste(f,v,y,L){this._executeCursorEdit(I=>this._cursor.paste(I,f,v,y,L))}cut(f){this._executeCursorEdit(v=>this._cursor.cut(v,f))}executeCommand(f,v){this._executeCursorEdit(y=>this._cursor.executeCommand(y,f,v))}executeCommands(f,v){this._executeCursorEdit(y=>this._cursor.executeCommands(y,f,v))}revealPrimaryCursor(f,v){this._withViewEventsCollector(y=>this._cursor.revealPrimary(y,f,v,0))}revealTopMostCursor(f){const v=this._cursor.getTopMostViewPosition(),y=new C.Range(v.lineNumber,v.column,v.lineNumber,v.column);this._withViewEventsCollector(L=>L.emitViewEvent(new c.ViewRevealRangeRequestEvent(f,y,null,0,!0,0)))}revealBottomMostCursor(f){const v=this._cursor.getBottomMostViewPosition(),y=new C.Range(v.lineNumber,v.column,v.lineNumber,v.column);this._withViewEventsCollector(L=>L.emitViewEvent(new c.ViewRevealRangeRequestEvent(f,y,null,0,!0,0)))}revealRange(f,v,y,L,I){this._withViewEventsCollector(k=>k.emitViewEvent(new c.ViewRevealRangeRequestEvent(f,y,null,L,v,I)))}getVerticalOffsetForLineNumber(f){return this.viewLayout.getVerticalOffsetForLineNumber(f)}getScrollTop(){return this.viewLayout.getCurrentScrollTop()}setScrollTop(f,v){this.viewLayout.setScrollPosition({scrollTop:f},v)}setScrollPosition(f,v){this.viewLayout.setScrollPosition(f,v)}deltaScrollNow(f,v){this.viewLayout.deltaScrollNow(f,v)}changeWhitespace(f){this.viewLayout.changeWhitespace(f)&&(this._eventDispatcher.emitSingleViewEvent(new c.ViewZonesChangedEvent),this._eventDispatcher.emitOutgoingEvent(new l.ViewZonesChangedEvent))}setMaxLineWidth(f){this.viewLayout.setMaxLineWidth(f)}_withViewEventsCollector(f){try{const v=this._eventDispatcher.beginEmitViewEvents();f(v)}finally{this._eventDispatcher.endEmitViewEvents()}}}e.ViewModel=m}),define(Q[245],J([0,1,19,23,12,51,3,18,54,134]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.OutlineModel=e.OutlineGroup=e.OutlineElement=e.TreeElement=void 0;class p{remove(){this.parent&&this.parent.children.delete(this.id)}static findId(u,r){let i;typeof u=="string"?i=`${r.id}/${u}`:(i=`${r.id}/${u.name}`,r.children.get(i)!==void 0&&(i=`${r.id}/${u.name}_${u.range.startLineNumber}_${u.range.startColumn}`));let n=i;for(let t=0;r.children.get(n)!==void 0;t++)n=`${i}_${t}`;return n}static empty(u){return u.children.size===0}}e.TreeElement=p;class c extends p{constructor(u,r,i){super();this.id=u,this.parent=r,this.symbol=i,this.children=new Map}}e.OutlineElement=c;class o extends p{constructor(u,r,i,n){super();this.id=u,this.parent=r,this.label=i,this.order=n,this.children=new Map}}e.OutlineGroup=o;class s extends p{constructor(u){super();this.uri=u,this.id="root",this.parent=void 0,this._groups=new Map,this.children=new Map,this.id="root",this.parent=void 0}static create(u,r){let i=this._keys.for(u,!0),n=s._requests.get(i);if(!n){let t=new N.CancellationTokenSource;n={promiseCnt:0,source:t,promise:s._create(u,t.token),model:void 0},s._requests.set(i,n);const l=Date.now();n.promise.then(()=>{this._requestDurations.update(u,Date.now()-l)})}return n.model?Promise.resolve(n.model):(n.promiseCnt+=1,r.onCancellationRequested(()=>{--n.promiseCnt==0&&(n.source.cancel(),s._requests.delete(i))}),new Promise((t,l)=>{n.promise.then(h=>{n.model=h,t(h)},h=>{s._requests.delete(i),l(h)})}))}static _create(u,r){const i=new N.CancellationTokenSource(r),n=new s(u.uri),t=C.DocumentSymbolProviderRegistry.ordered(u),l=t.map((m,_)=>{var f;let v=p.findId(`provider_${_}`,n),y=new o(v,n,(f=m.displayName)!==null&&f!==void 0?f:"Unknown Outline Provider",_);return Promise.resolve(m.provideDocumentSymbols(u,i.token)).then(L=>{for(const I of L||[])s._makeOutlineElement(I,y);return y},L=>(M.onUnexpectedExternalError(L),y)).then(L=>{p.empty(L)?L.remove():n._groups.set(v,L)})}),h=C.DocumentSymbolProviderRegistry.onDidChange(()=>{const m=C.DocumentSymbolProviderRegistry.ordered(u);b.equals(m,t)||i.cancel()});return Promise.all(l).then(()=>i.token.isCancellationRequested&&!r.isCancellationRequested?s._create(u,r):n._compact()).finally(()=>{h.dispose()})}static _makeOutlineElement(u,r){let i=p.findId(u,r),n=new c(i,r,u);if(u.children)for(const t of u.children)s._makeOutlineElement(t,n);r.children.set(n.id,n)}_compact(){let u=0;for(const[r,i]of this._groups)i.children.size===0?this._groups.delete(r):u+=1;if(u!==1)this.children=this._groups;else{let r=d.Iterable.first(this._groups.values());for(let[,i]of r.children)i.parent=this,this.children.set(i.id,i)}return this}getTopLevelSymbols(){const u=[];for(const r of this.children.values())r instanceof c?u.push(r.symbol):u.push(...d.Iterable.map(r.children.values(),i=>i.symbol));return u.sort((r,i)=>S.Range.compareRangesUsingStarts(r.range,i.range))}asListOfDocumentSymbols(){const u=this.getTopLevelSymbols(),r=[];return s._flattenDocumentSymbols(r,u,""),r.sort((i,n)=>S.Range.compareRangesUsingStarts(i.range,n.range))}static _flattenDocumentSymbols(u,r,i){for(const n of r)u.push({kind:n.kind,tags:n.tags,name:n.name,detail:n.detail,containerName:n.containerName||i,range:n.range,selectionRange:n.selectionRange,children:void 0}),n.children&&s._flattenDocumentSymbols(u,n.children,n.name)}}e.OutlineModel=s,s._requestDurations=new g.LanguageFeatureRequestDelays(C.DocumentSymbolProviderRegistry,350),s._requests=new w.LRUCache(9,.75),s._keys=new class{constructor(){this._counter=1,this._data=new WeakMap}for(a,u){return`${a.id}/${u?a.getVersionId():""}/${this._hash(C.DocumentSymbolProviderRegistry.all(a))}`}_hash(a){let u="";for(const r of a){let i=this._data.get(r);typeof i=="undefined"&&(i=this._counter++,this._data.set(r,i)),u+=i}return u}}}),define(Q[541],J([0,1,174,31,41]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.computeRanges=e.RangesCollector=e.IndentRangeProvider=e.ID_INDENT_PROVIDER=void 0;const w=5e3;e.ID_INDENT_PROVIDER="indent";class S{constructor(p){this.editorModel=p,this.id=e.ID_INDENT_PROVIDER}dispose(){}compute(p){let c=M.LanguageConfigurationRegistry.getFoldingRules(this.editorModel.getLanguageIdentifier().id),o=c&&!!c.offSide,s=c&&c.markers;return Promise.resolve(d(this.editorModel,o,s))}}e.IndentRangeProvider=S;class C{constructor(p){this._startIndexes=[],this._endIndexes=[],this._indentOccurrences=[],this._length=0,this._foldingRangesLimit=p}insertFirst(p,c,o){if(!(p>b.MAX_LINE_NUMBER||c>b.MAX_LINE_NUMBER)){let s=this._length;this._startIndexes[s]=p,this._endIndexes[s]=c,this._length++,o<1e3&&(this._indentOccurrences[o]=(this._indentOccurrences[o]||0)+1)}}toIndentRanges(p){if(this._length<=this._foldingRangesLimit){let c=new Uint32Array(this._length),o=new Uint32Array(this._length);for(let s=this._length-1,a=0;s>=0;s--,a++)c[a]=this._startIndexes[s],o[a]=this._endIndexes[s];return new b.FoldingRegions(c,o)}else{let c=0,o=this._indentOccurrences.length;for(let r=0;rthis._foldingRangesLimit){o=r;break}c+=i}}const s=p.getOptions().tabSize;let a=new Uint32Array(this._foldingRangesLimit),u=new Uint32Array(this._foldingRangesLimit);for(let r=this._length-1,i=0;r>=0;r--){let n=this._startIndexes[r],t=p.getLineContent(n),l=N.TextModel.computeIndentLevel(t,s);(l0;n--){let t=g.getLineContent(n),l=N.TextModel.computeIndentLevel(t,s),h=r[r.length-1];if(l===-1){p&&(h.endAbove=n);continue}let m;if(u&&(m=t.match(u)))if(m[1]){let _=r.length-1;for(;_>0&&r[_].indent!==-2;)_--;if(_>0){r.length=_+1,h=r[_],a.insertFirst(n,h.line,l),h.line=n,h.indent=l,h.endAbove=n;continue}}else{r.push({indent:-2,endAbove:n,line:n});continue}if(h.indent>l){do r.pop(),h=r[r.length-1];while(h.indent>l);let _=h.endAbove-1;_-n>=1&&a.insertFirst(n,_,l)}h.indent===l?h.endAbove=n:r.push({indent:l,endAbove:n,line:n})}return a.toIndentRanges(g)}e.computeRanges=d}),define(Q[542],J([0,1,8,179,3,21,109,41,229]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MoveLinesCommand=void 0;class g{constructor(c,o,s){this._selection=c,this._isMovingDown=o,this._autoIndent=s,this._selectionId=null,this._moveEndLineSelectionShrink=!1}getEditOperations(c,o){let s=c.getLineCount();if(this._isMovingDown&&this._selection.endLineNumber===s){this._selectionId=o.trackSelection(this._selection);return}if(!this._isMovingDown&&this._selection.startLineNumber===1){this._selectionId=o.trackSelection(this._selection);return}this._moveEndPositionDown=!1;let a=this._selection;a.startLineNumberc.getLineTokens(l),getLanguageIdentifier:()=>c.getLanguageIdentifier(),getLanguageIdAtPosition:(l,h)=>c.getLanguageIdAtPosition(l,h),getLineContent:null};if(a.startLineNumber===a.endLineNumber&&c.getLineMaxColumn(a.startLineNumber)===1){let l=a.startLineNumber,h=this._isMovingDown?l+1:l-1;c.getLineMaxColumn(h)===1?o.addEditOperation(new M.Range(1,1,1,1),null):(o.addEditOperation(new M.Range(l,1,l,1),c.getLineContent(h)),o.addEditOperation(new M.Range(h,1,h,c.getLineMaxColumn(h)),null)),a=new w.Selection(h,1,h,1)}else{let l,h;if(this._isMovingDown){l=a.endLineNumber+1,h=c.getLineContent(l),o.addEditOperation(new M.Range(l-1,c.getLineMaxColumn(l-1),l,c.getLineMaxColumn(l)),null);let m=h;if(this.shouldAutoIndent(c,a)){let _=this.matchEnterRule(c,n,u,l,a.startLineNumber-1);if(_!==null){let v=b.getLeadingWhitespace(c.getLineContent(l)),y=_+d.getSpaceCnt(v,u);m=d.generateIndent(y,u,i)+this.trimLeft(h)}else{t.getLineContent=y=>y===a.startLineNumber?c.getLineContent(l):c.getLineContent(y);let v=C.LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent,t,c.getLanguageIdAtPosition(l,1),a.startLineNumber,n);if(v!==null){let y=b.getLeadingWhitespace(c.getLineContent(l)),L=d.getSpaceCnt(v,u),I=d.getSpaceCnt(y,u);L!==I&&(m=d.generateIndent(L,u,i)+this.trimLeft(h))}}o.addEditOperation(new M.Range(a.startLineNumber,1,a.startLineNumber,1),m+` +`);let f=this.matchEnterRuleMovingDown(c,n,u,a.startLineNumber,l,m);if(f!==null)f!==0&&this.getIndentEditsOfMovingBlock(c,o,a,u,i,f);else{t.getLineContent=y=>y===a.startLineNumber?m:y>=a.startLineNumber+1&&y<=a.endLineNumber+1?c.getLineContent(y-1):c.getLineContent(y);let v=C.LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent,t,c.getLanguageIdAtPosition(l,1),a.startLineNumber+1,n);if(v!==null){const y=b.getLeadingWhitespace(c.getLineContent(a.startLineNumber)),L=d.getSpaceCnt(v,u),I=d.getSpaceCnt(y,u);if(L!==I){const k=L-I;this.getIndentEditsOfMovingBlock(c,o,a,u,i,k)}}}}else o.addEditOperation(new M.Range(a.startLineNumber,1,a.startLineNumber,1),m+` +`)}else if(l=a.startLineNumber-1,h=c.getLineContent(l),o.addEditOperation(new M.Range(l,1,l+1,1),null),o.addEditOperation(new M.Range(a.endLineNumber,c.getLineMaxColumn(a.endLineNumber),a.endLineNumber,c.getLineMaxColumn(a.endLineNumber)),` +`+h),this.shouldAutoIndent(c,a)){t.getLineContent=_=>_===l?c.getLineContent(a.startLineNumber):c.getLineContent(_);let m=this.matchEnterRule(c,n,u,a.startLineNumber,a.startLineNumber-2);if(m!==null)m!==0&&this.getIndentEditsOfMovingBlock(c,o,a,u,i,m);else{let _=C.LanguageConfigurationRegistry.getGoodIndentForLine(this._autoIndent,t,c.getLanguageIdAtPosition(a.startLineNumber,1),l,n);if(_!==null){let f=b.getLeadingWhitespace(c.getLineContent(a.startLineNumber)),v=d.getSpaceCnt(_,u),y=d.getSpaceCnt(f,u);if(v!==y){let L=v-y;this.getIndentEditsOfMovingBlock(c,o,a,u,i,L)}}}}}this._selectionId=o.trackSelection(a)}buildIndentConverter(c,o,s){return{shiftIndent:a=>N.ShiftCommand.shiftIndent(a,a.length+1,c,o,s),unshiftIndent:a=>N.ShiftCommand.unshiftIndent(a,a.length+1,c,o,s)}}parseEnterResult(c,o,s,a,u){if(u){let r=u.indentation;u.indentAction===S.IndentAction.None||u.indentAction===S.IndentAction.Indent?r=u.indentation+u.appendText:u.indentAction===S.IndentAction.IndentOutdent?r=u.indentation:u.indentAction===S.IndentAction.Outdent&&(r=o.unshiftIndent(u.indentation)+u.appendText);let i=c.getLineContent(a);if(this.trimLeft(i).indexOf(this.trimLeft(r))>=0){let n=b.getLeadingWhitespace(c.getLineContent(a)),t=b.getLeadingWhitespace(r),l=C.LanguageConfigurationRegistry.getIndentMetadata(c,a);l!==null&&l&2&&(t=o.unshiftIndent(t));let h=d.getSpaceCnt(t,s),m=d.getSpaceCnt(n,s);return h-m}}return null}matchEnterRuleMovingDown(c,o,s,a,u,r){if(b.lastNonWhitespaceIndex(r)>=0){let i=c.getLineMaxColumn(u),n=C.LanguageConfigurationRegistry.getEnterAction(this._autoIndent,c,new M.Range(u,i,u,i));return this.parseEnterResult(c,o,s,a,n)}else{let i=a-1;for(;i>=1;){let l=c.getLineContent(i);if(b.lastNonWhitespaceIndex(l)>=0)break;i--}if(i<1||a>c.getLineCount())return null;let n=c.getLineMaxColumn(i),t=C.LanguageConfigurationRegistry.getEnterAction(this._autoIndent,c,new M.Range(i,n,i,n));return this.parseEnterResult(c,o,s,a,t)}}matchEnterRule(c,o,s,a,u,r){let i=u;for(;i>=1;){let l;if(i===u&&r!==void 0?l=r:l=c.getLineContent(i),b.lastNonWhitespaceIndex(l)>=0)break;i--}if(i<1||a>c.getLineCount())return null;let n=c.getLineMaxColumn(i),t=C.LanguageConfigurationRegistry.getEnterAction(this._autoIndent,c,new M.Range(i,n,i,n));return this.parseEnterResult(c,o,s,a,t)}trimLeft(c){return c.replace(/^\s+/,"")}shouldAutoIndent(c,o){if(this._autoIndent<4||!c.isCheapToTokenize(o.startLineNumber))return!1;let s=c.getLanguageIdAtPosition(o.startLineNumber,1),a=c.getLanguageIdAtPosition(o.endLineNumber,1);return!(s!==a||C.LanguageConfigurationRegistry.getIndentRulesSupport(s)===null)}getIndentEditsOfMovingBlock(c,o,s,a,u,r){for(let i=s.startLineNumber;i<=s.endLineNumber;i++){let n=c.getLineContent(i),t=b.getLeadingWhitespace(n),h=d.getSpaceCnt(t,a)+r,m=d.generateIndent(h,a,u);m!==t&&(o.addEditOperation(new M.Range(i,1,i,t.length+1),m),i===s.endLineNumber&&s.endColumn<=t.length+1&&m===""&&(this._moveEndLineSelectionShrink=!0))}}computeCursorState(c,o){let s=o.getTrackedSelection(this._selectionId);return this._moveEndPositionDown&&(s=s.setEndPosition(s.endLineNumber+1,1)),this._moveEndLineSelectionShrink&&s.startLineNumber{const h=this._getWidth(l);this.domNode.style.width=h+"px",this.domNode.style.left=this._getLeft(l)+"px",this._onWidth(h)}))}dispose(){this._overlayWidget&&(this.editor.removeOverlayWidget(this._overlayWidget),this._overlayWidget=null),this._viewZone&&this.editor.changeViewZones(n=>{this._viewZone&&n.removeZone(this._viewZone.id),this._viewZone=null}),this.editor.deltaDecorations(this._positionMarkerId,[]),this._positionMarkerId=[],this._disposables.dispose()}create(){this.domNode.classList.add("zone-widget"),this.options.className&&this.domNode.classList.add(this.options.className),this.container=document.createElement("div"),this.container.classList.add("zone-widget-container"),this.domNode.appendChild(this.container),this.options.showArrow&&(this._arrow=new u(this.editor),this._disposables.add(this._arrow)),this._fillContainer(this.container),this._initSash(),this._applyStyles()}style(n){n.frameColor&&(this.options.frameColor=n.frameColor),n.arrowColor&&(this.options.arrowColor=n.arrowColor),this._applyStyles()}_applyStyles(){if(this.container&&this.options.frameColor){let n=this.options.frameColor.toString();this.container.style.borderTopColor=n,this.container.style.borderBottomColor=n}if(this._arrow&&this.options.arrowColor){let n=this.options.arrowColor.toString();this._arrow.color=n}}_getWidth(n){return n.width-n.minimap.minimapWidth-n.verticalScrollbarWidth}_getLeft(n){return n.minimap.minimapWidth>0&&n.minimap.minimapLeft===0?n.minimap.minimapWidth:0}_onViewZoneTop(n){this.domNode.style.top=n+"px"}_onViewZoneHeight(n){if(this.domNode.style.height=`${n}px`,this.container){let t=n-this._decoratingElementsHeight();this.container.style.height=`${t}px`;const l=this.editor.getLayoutInfo();this._doLayout(t,this._getWidth(l))}this._resizeSash&&this._resizeSash.layout()}get position(){const[n]=this._positionMarkerId;if(!!n){const t=this.editor.getModel();if(!!t){const l=t.getDecorationRange(n);if(!!l)return l.getStartPosition()}}}show(n,t){const l=d.Range.isIRange(n)?d.Range.lift(n):d.Range.fromPositions(n);this._isShowing=!0,this._showImpl(l,t),this._isShowing=!1,this._positionMarkerId=this.editor.deltaDecorations(this._positionMarkerId,[{range:l,options:g.ModelDecorationOptions.EMPTY}])}hide(){this._viewZone&&(this.editor.changeViewZones(n=>{this._viewZone&&n.removeZone(this._viewZone.id)}),this._viewZone=null),this._overlayWidget&&(this.editor.removeOverlayWidget(this._overlayWidget),this._overlayWidget=null),this._arrow&&this._arrow.hide()}_decoratingElementsHeight(){let n=this.editor.getOption(53),t=0;if(this.options.showArrow){let l=Math.round(n/3);t+=2*l}if(this.options.showFrame){let l=Math.round(n/9);t+=2*l}return t}_showImpl(n,t){const l=n.getStartPosition(),h=this.editor.getLayoutInfo(),m=this._getWidth(h);this.domNode.style.width=`${m}px`,this.domNode.style.left=this._getLeft(h)+"px";const _=document.createElement("div");_.style.overflow="hidden";const f=this.editor.getOption(53),v=Math.max(12,this.editor.getLayoutInfo().height/f*.8);t=Math.min(t,v);let y=0,L=0;if(this._arrow&&this.options.showArrow&&(y=Math.round(f/3),this._arrow.height=y,this._arrow.show(l)),this.options.showFrame&&(L=Math.round(f/9)),this.editor.changeViewZones(E=>{this._viewZone&&E.removeZone(this._viewZone.id),this._overlayWidget&&(this.editor.removeOverlayWidget(this._overlayWidget),this._overlayWidget=null),this.domNode.style.top="-1000px",this._viewZone=new s(_,l.lineNumber,l.column,t,T=>this._onViewZoneTop(T),T=>this._onViewZoneHeight(T)),this._viewZone.id=E.addZone(this._viewZone),this._overlayWidget=new a(o+this._viewZone.id,this.domNode),this.editor.addOverlayWidget(this._overlayWidget)}),this.container&&this.options.showFrame){const E=this.options.frameWidth?this.options.frameWidth:L;this.container.style.borderTopWidth=E+"px",this.container.style.borderBottomWidth=E+"px"}let I=t*f-this._decoratingElementsHeight();this.container&&(this.container.style.top=y+"px",this.container.style.height=I+"px",this.container.style.overflow="hidden"),this._doLayout(I,m),this.options.keepEditorSelection||this.editor.setSelection(n);const k=this.editor.getModel();if(k){const E=n.endLineNumber+1;E<=k.getLineCount()?this.revealLine(E,!1):this.revealLine(k.getLineCount(),!0)}}revealLine(n,t){t?this.editor.revealLineInCenter(n,0):this.editor.revealLine(n,0)}setCssClass(n,t){!this.container||(t&&this.container.classList.remove(t),this.container.classList.add(n))}_onWidth(n){}_doLayout(n,t){}_relayout(n){this._viewZone&&this._viewZone.heightInLines!==n&&this.editor.changeViewZones(t=>{this._viewZone&&(this._viewZone.heightInLines=n,t.layoutZone(this._viewZone.id))})}_initSash(){if(!this._resizeSash){this._resizeSash=this._disposables.add(new N.Sash(this.domNode,this,{orientation:1})),this.options.isResizeable||(this._resizeSash.hide(),this._resizeSash.state=0);let n;this._disposables.add(this._resizeSash.onDidStart(t=>{this._viewZone&&(n={startY:t.startY,heightInLines:this._viewZone.heightInLines})})),this._disposables.add(this._resizeSash.onDidEnd(()=>{n=void 0})),this._disposables.add(this._resizeSash.onDidChange(t=>{if(n){let l=(t.currentY-n.startY)/this.editor.getOption(53),h=l<0?Math.ceil(l):Math.floor(l),m=n.heightInLines+h;m>5&&m<35&&this._relayout(m)}}))}}getHorizontalSashLeft(){return 0}getHorizontalSashTop(){return(this.domNode.style.height===null?0:parseInt(this.domNode.style.height))-this._decoratingElementsHeight()/2}getHorizontalSashWidth(){const n=this.editor.getLayoutInfo();return n.width-n.minimap.minimapWidth}}e.ZoneWidget=r}),define(Q[246],J([0,1,127,18,76,232]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createTokenizationSupport=e.MonarchTokenizer=void 0;const S=5;class C{constructor(n){this._maxCacheDepth=n,this._entries=Object.create(null)}static create(n,t){return this._INSTANCE.create(n,t)}create(n,t){if(n!==null&&n.depth>=this._maxCacheDepth)return new d(n,t);let l=d.getStackElementId(n);l.length>0&&(l+="|"),l+=t;let h=this._entries[l];return h||(h=new d(n,t),this._entries[l]=h,h)}}C._INSTANCE=new C(S);class d{constructor(n,t){this.parent=n,this.state=t,this.depth=(this.parent?this.parent.depth:0)+1}static getStackElementId(n){let t="";for(;n!==null;)t.length>0&&(t+="|"),t+=n.state,n=n.parent;return t}static _equals(n,t){for(;n!==null&&t!==null;){if(n===t)return!0;if(n.state!==t.state)return!1;n=n.parent,t=t.parent}return n===null&&t===null}equals(n){return d._equals(this,n)}push(n){return C.create(this,n)}pop(){return this.parent}popall(){let n=this;for(;n.parent;)n=n.parent;return n}switchTo(n){return C.create(this.parent,n)}}class g{constructor(n,t){this.modeId=n,this.state=t}equals(n){return this.modeId===n.modeId&&this.state.equals(n.state)}clone(){return this.state.clone()===this.state?this:new g(this.modeId,this.state)}}class p{constructor(n){this._maxCacheDepth=n,this._entries=Object.create(null)}static create(n,t){return this._INSTANCE.create(n,t)}create(n,t){if(t!==null)return new c(n,t);if(n!==null&&n.depth>=this._maxCacheDepth)return new c(n,t);let l=d.getStackElementId(n),h=this._entries[l];return h||(h=new c(n,null),this._entries[l]=h,h)}}p._INSTANCE=new p(S);class c{constructor(n,t){this.stack=n,this.embeddedModeData=t}clone(){return(this.embeddedModeData?this.embeddedModeData.clone():null)===this.embeddedModeData?this:p.create(this.stack,this.embeddedModeData)}equals(n){return!(n instanceof c)||!this.stack.equals(n.stack)?!1:this.embeddedModeData===null&&n.embeddedModeData===null?!0:this.embeddedModeData===null||n.embeddedModeData===null?!1:this.embeddedModeData.equals(n.embeddedModeData)}}class o{constructor(){this._tokens=[],this._language=null,this._lastTokenType=null,this._lastTokenLanguage=null}enterMode(n,t){this._language=t}emit(n,t){this._lastTokenType===t&&this._lastTokenLanguage===this._language||(this._lastTokenType=t,this._lastTokenLanguage=this._language,this._tokens.push(new b.Token(n,t,this._language)))}nestedModeTokenize(n,t,l,h){const m=l.modeId,_=l.state,f=N.TokenizationRegistry.get(m);if(!f)return this.enterMode(h,m),this.emit(h,""),_;let v=f.tokenize(n,t,_,h);return this._tokens=this._tokens.concat(v.tokens),this._lastTokenType=null,this._lastTokenLanguage=null,this._language=null,v.endState}finalize(n){return new b.TokenizationResult(this._tokens,n)}}class s{constructor(n,t){this._modeService=n,this._theme=t,this._prependTokens=null,this._tokens=[],this._currentLanguageId=0,this._lastTokenMetadata=0}enterMode(n,t){this._currentLanguageId=this._modeService.getLanguageIdentifier(t).id}emit(n,t){let l=this._theme.match(this._currentLanguageId,t);this._lastTokenMetadata!==l&&(this._lastTokenMetadata=l,this._tokens.push(n),this._tokens.push(l))}static _merge(n,t,l){let h=n!==null?n.length:0,m=t.length,_=l!==null?l.length:0;if(h===0&&m===0&&_===0)return new Uint32Array(0);if(h===0&&m===0)return l;if(m===0&&_===0)return n;let f=new Uint32Array(h+m+_);n!==null&&f.set(n);for(let v=0;v{if(!m){let f=!1;for(let v=0,y=_.changedLanguages.length;v{})}}getInitialState(){let n=C.create(null,this._lexer.start);return p.create(n,null)}tokenize(n,t,l,h){let m=new o,_=this._tokenize(n,t,l,h,m);return m.finalize(_)}tokenize2(n,t,l,h){let m=new s(this._modeService,this._standaloneThemeService.getColorTheme().tokenTheme),_=this._tokenize(n,t,l,h,m);return m.finalize(_)}_tokenize(n,t,l,h,m){return l.embeddedModeData?this._nestedTokenize(n,t,l,h,m):this._myTokenize(n,t,l,h,m)}_findLeavingNestedModeOffset(n,t){let l=this._lexer.tokenizer[t.stack.state];if(!l&&(l=w.findRules(this._lexer,t.stack.state),!l))throw w.createError(this._lexer,"tokenizer state is not defined: "+t.stack.state);let h=-1,m=!1;for(const _ of l)if(!(!w.isIAction(_.action)||_.action.nextEmbedded!=="@pop")){m=!0;let f=_.regex,v=_.regex.source;if(v.substr(0,4)==="^(?:"&&v.substr(v.length-1,1)===")"){let L=(f.ignoreCase?"i":"")+(f.unicode?"u":"");f=new RegExp(v.substr(4,v.length-5),L)}let y=n.search(f);y===-1||y!==0&&_.matchOnlyAtLineStart||(h===-1||y0&&m.nestedModeTokenize(f,!1,l.embeddedModeData,h);let v=n.substring(_);return this._myTokenize(v,t,l,h+_,m)}_safeRuleName(n){return n?n.name:"(unknown)"}_myTokenize(n,t,l,h,m){m.enterMode(h,this._modeId);const _=n.length,f=t&&this._lexer.includeLF?n+` +`:n,v=f.length;let y=l.embeddedModeData,L=l.stack,I=0,k=null,E=!0;for(;E||I=v)break;E=!1;let ee=this._lexer.tokenizer[B];if(!ee&&(ee=w.findRules(this._lexer,B),!ee))throw w.createError(this._lexer,"tokenizer state is not defined: "+B);let se=f.substr(I);for(const ne of ee)if((I===0||!ne.matchOnlyAtLineStart)&&(F=se.match(ne.regex),F)){D=F[0],R=ne.action;break}}if(F||(F=[""],D=""),R||(I=this._lexer.maxStack)throw w.createError(this._lexer,"maximum tokenizer stack size reached: ["+L.state+","+L.parent.state+",...]");L=L.push(B)}else if(R.next==="@pop"){if(L.depth<=1)throw w.createError(this._lexer,"trying to pop an empty stack in rule: "+this._safeRuleName(W));L=L.pop()}else if(R.next==="@popall")L=L.popall();else{let ee=w.substituteMatches(this._lexer,R.next,D,F,B);if(ee[0]==="@"&&(ee=ee.substr(1)),w.findRules(this._lexer,ee))L=L.push(ee);else throw w.createError(this._lexer,"trying to set a next state '"+ee+"' that is undefined in rule: "+this._safeRuleName(W))}}R.log&&typeof R.log=="string"&&w.log(this._lexer,this._lexer.languageId+": "+w.substituteMatches(this._lexer,R.log,D,F,B))}if(K===null)throw w.createError(this._lexer,"lexer rule has no well-defined action in rule: "+this._safeRuleName(W));const Y=ee=>{let se=this._modeService.getModeIdForLanguageName(ee);se&&(ee=se);const ne=this._getNestedEmbeddedModeData(ee);if(I0)throw w.createError(this._lexer,"groups cannot be nested: "+this._safeRuleName(W));if(F.length!==K.length+1)throw w.createError(this._lexer,"matched number of groups does not match the number of actions in rule: "+this._safeRuleName(W));let ee=0;for(let se=1;seu});class c{static colorizeElement(r,i,n,t){t=t||{};let l=t.theme||"vs",h=t.mimeType||n.getAttribute("lang")||n.getAttribute("data-lang");if(!h)return console.error("Mode not detected"),Promise.resolve();r.setTheme(l);let m=n.firstChild?n.firstChild.nodeValue:"";n.className+=" "+l;let _=f=>{var v;const y=(v=p==null?void 0:p.createHTML(f))!==null&&v!==void 0?v:f;n.innerHTML=y};return this.colorize(i,m||"",h,t).then(_,f=>console.error(f))}static colorize(r,i,n,t){let l=4;t&&typeof t.tabSize=="number"&&(l=t.tabSize),N.startsWithUTF8BOM(i)&&(i=i.substr(1));let h=N.splitLines(i),m=r.getModeId(n);if(!m)return Promise.resolve(s(h,l));r.triggerMode(m);const _=w.TokenizationRegistry.get(m);if(_)return o(h,l,_);const f=w.TokenizationRegistry.getPromise(m);return f?new Promise((v,y)=>{f.then(L=>{o(h,l,L).then(v,y)},y)}):new Promise((v,y)=>{let L=null,I=null;const k=()=>{L&&(L.dispose(),L=null),I&&(I.dispose(),I=null);const E=w.TokenizationRegistry.get(m);if(E){o(h,l,E).then(v,y);return}v(s(h,l))};I=new b.TimeoutTimer,I.cancelAndSet(k,500),L=w.TokenizationRegistry.onDidChange(E=>{E.changedLanguages.indexOf(m)>=0&&k()})})}static colorizeLine(r,i,n,t,l=4){const h=C.ViewLineRenderingData.isBasicASCII(r,i),m=C.ViewLineRenderingData.containsRTL(r,h,n);return S.renderViewLine2(new S.RenderLineInput(!1,!0,r,!1,h,m,0,t,[],l,0,0,0,0,-1,"none",!1,!1,null)).html}static colorizeModelLine(r,i,n=4){let t=r.getLineContent(i);r.forceTokenization(i);let h=r.getLineTokens(i).inflate();return this.colorizeLine(t,r.mightContainNonBasicASCII(),r.mightContainRTL(),h,n)}}e.Colorizer=c;function o(u,r,i){return new Promise((n,t)=>{const l=()=>{const h=a(u,r,i);if(i instanceof d.MonarchTokenizer){const m=i.getLoadStatus();if(m.loaded===!1){m.promise.then(l,t);return}}n(h)};l()})}function s(u,r){let i=[];const n=(0<<11|1<<14|2<<23)>>>0,t=new Uint32Array(2);t[0]=0,t[1]=n;for(let l=0,h=u.length;l")}return i.join("")}function a(u,r,i){let n=[],t=i.getInitialState();for(let l=0,h=u.length;l"),t=_.endState}return n.join("")}}),define(Q[114],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IStandaloneThemeService=void 0,e.IStandaloneThemeService=b.createDecorator("themeService")}),define(Q[84],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IClipboardService=void 0,e.IClipboardService=b.createDecorator("clipboardService")}),define(Q[26],J([0,1,2,20,9,6,71,54]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CommandsRegistry=e.ICommandService=void 0,e.ICommandService=M.createDecorator("commandService"),e.CommandsRegistry=new class{constructor(){this._commands=new Map,this._onDidRegisterCommand=new w.Emitter,this.onDidRegisterCommand=this._onDidRegisterCommand.event}registerCommand(d,g){if(!d)throw new Error("invalid command");if(typeof d=="string"){if(!g)throw new Error("invalid command");return this.registerCommand({id:d,handler:g})}if(d.description){const a=[];for(let r of d.description.args)a.push(r.constraint);const u=d.handler;d.handler=function(r,...i){return N.validateConstraints(i,a),u(r,...i)}}const{id:p}=d;let c=this._commands.get(p);c||(c=new S.LinkedList,this._commands.set(p,c));let o=c.unshift(d),s=b.toDisposable(()=>{o();const a=this._commands.get(p);(a==null?void 0:a.isEmpty())&&this._commands.delete(p)});return this._onDidRegisterCommand.fire(p),s}registerCommandAlias(d,g){return e.CommandsRegistry.registerCommand(d,(p,...c)=>p.get(e.ICommandService).executeCommand(g,...c))}getCommand(d){const g=this._commands.get(d);if(!(!g||g.isEmpty()))return C.Iterable.first(g)}getCommands(){const d=new Map;for(const g of this._commands.keys()){const p=this.getCommand(g);p&&d.set(g,p)}return d}}}),define(Q[247],J([0,1,23,12,24,18,36,26,20,383,3]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getDocumentRangeSemanticTokensProvider=e.getDocumentSemanticTokens=e.isSemanticTokensEdits=e.isSemanticTokens=void 0;function c(r){return r&&!!r.data}e.isSemanticTokens=c;function o(r){return r&&Array.isArray(r.edits)}e.isSemanticTokensEdits=o;function s(r,i,n){const t=a(r);return t?{provider:t,request:Promise.resolve(t.provideDocumentSemanticTokens(r,i,n))}:null}e.getDocumentSemanticTokens=s;function a(r){const i=w.DocumentSemanticTokensProviderRegistry.ordered(r);return i.length>0?i[0]:null}function u(r){const i=w.DocumentRangeSemanticTokensProviderRegistry.ordered(r);return i.length>0?i[0]:null}e.getDocumentRangeSemanticTokensProvider=u,C.CommandsRegistry.registerCommand("_provideDocumentSemanticTokensLegend",(r,...i)=>Ie(void 0,void 0,void 0,function*(){const[n]=i;d.assertType(n instanceof M.URI);const t=r.get(S.IModelService).getModel(n);if(!!t){const l=a(t);return l?l.getLegend():r.get(C.ICommandService).executeCommand("_provideDocumentRangeSemanticTokensLegend",n)}})),C.CommandsRegistry.registerCommand("_provideDocumentSemanticTokens",(r,...i)=>Ie(void 0,void 0,void 0,function*(){const[n]=i;d.assertType(n instanceof M.URI);const t=r.get(S.IModelService).getModel(n);if(!!t){const l=s(t,null,b.CancellationToken.None);if(!l)return r.get(C.ICommandService).executeCommand("_provideDocumentRangeSemanticTokens",n,t.getFullModelRange());const{provider:h,request:m}=l;let _;try{_=yield m}catch(v){N.onUnexpectedExternalError(v);return}if(!(!_||!c(_))){const f=g.encodeSemanticTokensDto({id:0,type:"full",data:_.data});return _.resultId&&h.releaseDocumentSemanticTokens(_.resultId),f}}})),C.CommandsRegistry.registerCommand("_provideDocumentRangeSemanticTokensLegend",(r,...i)=>Ie(void 0,void 0,void 0,function*(){const[n]=i;d.assertType(n instanceof M.URI);const t=r.get(S.IModelService).getModel(n);if(!!t){const l=u(t);if(!!l)return l.getLegend()}})),C.CommandsRegistry.registerCommand("_provideDocumentRangeSemanticTokens",(r,...i)=>Ie(void 0,void 0,void 0,function*(){const[n,t]=i;d.assertType(n instanceof M.URI),d.assertType(p.Range.isIRange(t));const l=r.get(S.IModelService).getModel(n);if(!!l){const h=u(l);if(!!h){let m;try{m=yield h.provideDocumentRangeSemanticTokens(l,p.Range.lift(t),b.CancellationToken.None)}catch(_){N.onUnexpectedExternalError(_);return}if(!(!m||!c(m)))return g.encodeSemanticTokensDto({id:0,type:"full",data:m.data})}}}))}),define(Q[248],J([0,1,19,23,12,24,18,36,2,26,20]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getCodeLensModel=e.CodeLensModel=void 0;class c{constructor(){this.lenses=[],this._disposables=new d.DisposableStore}dispose(){this._disposables.dispose()}add(a,u){this._disposables.add(a);for(const r of a.lenses)this.lenses.push({symbol:r,provider:u})}}e.CodeLensModel=c;function o(s,a){return Ie(this,void 0,void 0,function*(){const u=S.CodeLensProviderRegistry.ordered(s),r=new Map,i=new c,n=u.map((t,l)=>Ie(this,void 0,void 0,function*(){r.set(t,l);try{const h=yield Promise.resolve(t.provideCodeLenses(s,a));h&&i.add(h,t)}catch(h){M.onUnexpectedExternalError(h)}}));return yield Promise.all(n),i.lenses=b.mergeSort(i.lenses,(t,l)=>t.symbol.range.startLineNumberl.symbol.range.startLineNumber?1:r.get(t.provider)r.get(l.provider)?1:t.symbol.range.startColumnl.symbol.range.startColumn?1:0),i})}e.getCodeLensModel=o,g.CommandsRegistry.registerCommand("_executeCodeLensProvider",function(s,...a){let[u,r]=a;p.assertType(w.URI.isUri(u)),p.assertType(typeof r=="number"||!r);const i=s.get(C.IModelService).getModel(u);if(!i)throw M.illegalArgument();const n=[],t=new d.DisposableStore;return o(i,N.CancellationToken.None).then(l=>{t.add(l);let h=[];for(const m of l.lenses)r==null||Boolean(m.symbol.command)?n.push(m.symbol):r-- >0&&m.provider.resolveCodeLens&&h.push(Promise.resolve(m.provider.resolveCodeLens(i,m.symbol,N.CancellationToken.None)).then(_=>n.push(_||m.symbol)));return Promise.all(h)}).then(()=>n).finally(()=>{setTimeout(()=>t.dispose(),100)})})}),define(Q[249],J([0,1,23,12,24,3,18,36,26]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getColorPresentations=e.getColors=void 0;function g(c,o){const s=[],u=S.ColorProviderRegistry.ordered(c).reverse().map(r=>Promise.resolve(r.provideDocumentColors(c,o)).then(i=>{if(Array.isArray(i))for(let n of i)s.push({colorInfo:n,provider:r})}));return Promise.all(u).then(()=>s)}e.getColors=g;function p(c,o,s,a){return Promise.resolve(s.provideColorPresentations(c,o,a))}e.getColorPresentations=p,d.CommandsRegistry.registerCommand("_executeDocumentColorProvider",function(c,...o){const[s]=o;if(!(s instanceof M.URI))throw N.illegalArgument();const a=c.get(C.IModelService).getModel(s);if(!a)throw N.illegalArgument();const u=[],i=S.ColorProviderRegistry.ordered(a).reverse().map(n=>Promise.resolve(n.provideDocumentColors(a,b.CancellationToken.None)).then(t=>{if(Array.isArray(t))for(let l of t)u.push({range:l.range,color:[l.color.red,l.color.green,l.color.blue,l.color.alpha]})}));return Promise.all(i).then(()=>u)}),d.CommandsRegistry.registerCommand("_executeColorPresentationProvider",function(c,...o){const[s,a]=o,{uri:u,range:r}=a;if(!(u instanceof M.URI)||!Array.isArray(s)||s.length!==4||!w.Range.isIRange(r))throw N.illegalArgument();const[i,n,t,l]=s,h=c.get(C.IModelService).getModel(u);if(!h)throw N.illegalArgument();const m={range:r,color:{red:i,green:n,blue:t,alpha:l}},_=[],v=S.ColorProviderRegistry.ordered(h).reverse().map(y=>Promise.resolve(y.provideColorPresentations(h,m,b.CancellationToken.None)).then(L=>{Array.isArray(L)&&_.push(...L)}));return Promise.all(v).then(()=>_)})}),define(Q[545],J([0,1,24,36,23,67,245,26,20]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getDocumentSymbols=void 0;function g(p,c,o){return Ie(this,void 0,void 0,function*(){const s=yield S.OutlineModel.create(p,o);return c?s.asListOfDocumentSymbols():s.getTopLevelSymbols()})}e.getDocumentSymbols=g,C.CommandsRegistry.registerCommand("_executeDocumentSymbolProvider",function(p,...c){return Ie(this,void 0,void 0,function*(){const[o]=c;d.assertType(b.URI.isUri(o));const s=p.get(N.IModelService).getModel(o);if(s)return g(s,!1,M.CancellationToken.None);const a=yield p.get(w.ITextModelService).createModelReference(o);try{return yield g(a.object.textEditorModel,!1,M.CancellationToken.None)}finally{a.dispose()}})})}),define(Q[546],J([0,1,23,12,24,3,18,36,26,2,19,20]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getLinks=e.LinksList=e.Link=void 0;class o{constructor(r,i){this._link=r,this._provider=i}toJSON(){return{range:this.range,url:this.url,tooltip:this.tooltip}}get range(){return this._link.range}get url(){return this._link.url}get tooltip(){return this._link.tooltip}resolve(r){return Ie(this,void 0,void 0,function*(){return this._link.url?this._link.url:typeof this._provider.resolveLink=="function"?Promise.resolve(this._provider.resolveLink(this._link,r)).then(i=>(this._link=i||this._link,this._link.url?this.resolve(r):Promise.reject(new Error("missing")))):Promise.reject(new Error("missing"))})}}e.Link=o;class s{constructor(r){this._disposables=new g.DisposableStore;let i=[];for(const[n,t]of r){const l=n.links.map(h=>new o(h,t));i=s._union(i,l),g.isDisposable(n)&&this._disposables.add(n)}this.links=i}dispose(){this._disposables.dispose(),this.links.length=0}static _union(r,i){let n=[],t,l,h,m;for(t=0,h=0,l=r.length,m=i.length;tPromise.resolve(t.provideLinks(u,r)).then(h=>{h&&(i[l]=[h,t])},N.onUnexpectedExternalError));return Promise.all(n).then(()=>{const t=new s(p.coalesce(i));return r.isCancellationRequested?(t.dispose(),new s([])):t})}e.getLinks=a,d.CommandsRegistry.registerCommand("_executeLinkProvider",(u,...r)=>Ie(void 0,void 0,void 0,function*(){let[i,n]=r;c.assertType(i instanceof M.URI),typeof n!="number"&&(n=0);const t=u.get(C.IModelService).getModel(i);if(!t)return[];const l=yield a(t,b.CancellationToken.None);if(!l)return[];for(let m=0;m=0),S.set("isFirefox",w.indexOf("Firefox")>=0),S.set("isChrome",w.indexOf("Chrome")>=0),S.set("isSafari",w.indexOf("Safari")>=0),S.set("isIPad",w.indexOf("iPad")>=0);const C=Object.prototype.hasOwnProperty;class d{static has(E){return o.create(E)}static equals(E,T){return s.create(E,T)}static regex(E,T){return m.create(E,T)}static not(E){return i.create(E)}static and(...E){return f.create(E)}static or(...E){return v.create(E)}static deserialize(E,T=!1){if(!!E)return this._deserializeOrExpression(E,T)}static _deserializeOrExpression(E,T){let O=E.split("||");return v.create(O.map(A=>this._deserializeAndExpression(A,T)))}static _deserializeAndExpression(E,T){let O=E.split("&&");return f.create(O.map(A=>this._deserializeOne(A,T)))}static _deserializeOne(E,T){if(E=E.trim(),E.indexOf("!=")>=0){let O=E.split("!=");return r.create(O[0].trim(),this._deserializeValue(O[1],T))}if(E.indexOf("==")>=0){let O=E.split("==");return s.create(O[0].trim(),this._deserializeValue(O[1],T))}if(E.indexOf("=~")>=0){let O=E.split("=~");return m.create(O[0].trim(),this._deserializeRegexValue(O[1],T))}if(E.indexOf(" in ")>=0){let O=E.split(" in ");return a.create(O[0].trim(),O[1].trim())}if(/^[^<=>]+>=[^<=>]+$/.test(E)){const O=E.split(">=");return t.create(O[0].trim(),O[1].trim())}if(/^[^<=>]+>[^<=>]+$/.test(E)){const O=E.split(">");return n.create(O[0].trim(),O[1].trim())}if(/^[^<=>]+<=[^<=>]+$/.test(E)){const O=E.split("<=");return h.create(O[0].trim(),O[1].trim())}if(/^[^<=>]+<[^<=>]+$/.test(E)){const O=E.split("<");return l.create(O[0].trim(),O[1].trim())}return/^\!\s*/.test(E)?i.create(E.substr(1).trim()):o.create(E)}static _deserializeValue(E,T){if(E=E.trim(),E==="true")return!0;if(E==="false")return!1;let O=/^'([^']*)'$/.exec(E);return O?O[1].trim():E}static _deserializeRegexValue(E,T){if(b.isFalsyOrWhitespace(E)){if(T)throw new Error("missing regexp-value for =~-expression");return console.warn("missing regexp-value for =~-expression"),null}let O=E.indexOf("/"),A=E.lastIndexOf("/");if(O===A||O<0){if(T)throw new Error(`bad regexp-value '${E}', missing /-enclosure`);return console.warn(`bad regexp-value '${E}', missing /-enclosure`),null}let B=E.slice(O+1,A),F=E[A+1]==="i"?"i":"";try{return new RegExp(B,F)}catch(D){if(T)throw new Error(`bad regexp-value '${E}', parse error: ${D}`);return console.warn(`bad regexp-value '${E}', parse error: ${D}`),null}}}e.ContextKeyExpr=d;function g(k,E){return k.cmp(E)}class p{constructor(){this.type=0}cmp(E){return this.type-E.type}equals(E){return E.type===this.type}evaluate(E){return!1}serialize(){return"false"}keys(){return[]}negate(){return c.INSTANCE}}e.ContextKeyFalseExpr=p,p.INSTANCE=new p;class c{constructor(){this.type=1}cmp(E){return this.type-E.type}equals(E){return E.type===this.type}evaluate(E){return!0}serialize(){return"true"}keys(){return[]}negate(){return p.INSTANCE}}e.ContextKeyTrueExpr=c,c.INSTANCE=new c;class o{constructor(E){this.key=E,this.type=2}static create(E){const T=S.get(E);return typeof T=="boolean"?T?c.INSTANCE:p.INSTANCE:new o(E)}cmp(E){return E.type!==this.type?this.type-E.type:L(this.key,E.key)}equals(E){return E.type===this.type?this.key===E.key:!1}evaluate(E){return!!E.getValue(this.key)}serialize(){return this.key}keys(){return[this.key]}negate(){return i.create(this.key)}}e.ContextKeyDefinedExpr=o;class s{constructor(E,T){this.key=E,this.value=T,this.type=4}static create(E,T){if(typeof T=="boolean")return T?o.create(E):i.create(E);const O=S.get(E);return typeof O=="boolean"?T===(O?"true":"false")?c.INSTANCE:p.INSTANCE:new s(E,T)}cmp(E){return E.type!==this.type?this.type-E.type:I(this.key,this.value,E.key,E.value)}equals(E){return E.type===this.type?this.key===E.key&&this.value===E.value:!1}evaluate(E){return E.getValue(this.key)==this.value}serialize(){return`${this.key} == '${this.value}'`}keys(){return[this.key]}negate(){return r.create(this.key,this.value)}}e.ContextKeyEqualsExpr=s;class a{constructor(E,T){this.key=E,this.valueKey=T,this.type=10}static create(E,T){return new a(E,T)}cmp(E){return E.type!==this.type?this.type-E.type:I(this.key,this.valueKey,E.key,E.valueKey)}equals(E){return E.type===this.type?this.key===E.key&&this.valueKey===E.valueKey:!1}evaluate(E){const T=E.getValue(this.valueKey),O=E.getValue(this.key);return Array.isArray(T)?T.indexOf(O)>=0:typeof O=="string"&&typeof T=="object"&&T!==null?C.call(T,O):!1}serialize(){return`${this.key} in '${this.valueKey}'`}keys(){return[this.key,this.valueKey]}negate(){return u.create(this)}}e.ContextKeyInExpr=a;class u{constructor(E){this._actual=E,this.type=11}static create(E){return new u(E)}cmp(E){return E.type!==this.type?this.type-E.type:this._actual.cmp(E._actual)}equals(E){return E.type===this.type?this._actual.equals(E._actual):!1}evaluate(E){return!this._actual.evaluate(E)}serialize(){throw new Error("Method not implemented.")}keys(){return this._actual.keys()}negate(){return this._actual}}e.ContextKeyNotInExpr=u;class r{constructor(E,T){this.key=E,this.value=T,this.type=5}static create(E,T){if(typeof T=="boolean")return T?i.create(E):o.create(E);const O=S.get(E);return typeof O=="boolean"?T===(O?"true":"false")?p.INSTANCE:c.INSTANCE:new r(E,T)}cmp(E){return E.type!==this.type?this.type-E.type:I(this.key,this.value,E.key,E.value)}equals(E){return E.type===this.type?this.key===E.key&&this.value===E.value:!1}evaluate(E){return E.getValue(this.key)!=this.value}serialize(){return`${this.key} != '${this.value}'`}keys(){return[this.key]}negate(){return s.create(this.key,this.value)}}e.ContextKeyNotEqualsExpr=r;class i{constructor(E){this.key=E,this.type=3}static create(E){const T=S.get(E);return typeof T=="boolean"?T?p.INSTANCE:c.INSTANCE:new i(E)}cmp(E){return E.type!==this.type?this.type-E.type:L(this.key,E.key)}equals(E){return E.type===this.type?this.key===E.key:!1}evaluate(E){return!E.getValue(this.key)}serialize(){return`!${this.key}`}keys(){return[this.key]}negate(){return o.create(this.key)}}e.ContextKeyNotExpr=i;class n{constructor(E,T){this.key=E,this.value=T,this.type=12}static create(E,T){return new n(E,T)}cmp(E){return E.type!==this.type?this.type-E.type:I(this.key,this.value,E.key,E.value)}equals(E){return E.type===this.type?this.key===E.key&&this.value===E.value:!1}evaluate(E){return parseFloat(E.getValue(this.key))>parseFloat(this.value)}serialize(){return`${this.key} > ${this.value}`}keys(){return[this.key]}negate(){return h.create(this.key,this.value)}}e.ContextKeyGreaterExpr=n;class t{constructor(E,T){this.key=E,this.value=T,this.type=13}static create(E,T){return new t(E,T)}cmp(E){return E.type!==this.type?this.type-E.type:I(this.key,this.value,E.key,E.value)}equals(E){return E.type===this.type?this.key===E.key&&this.value===E.value:!1}evaluate(E){return parseFloat(E.getValue(this.key))>=parseFloat(this.value)}serialize(){return`${this.key} >= ${this.value}`}keys(){return[this.key]}negate(){return l.create(this.key,this.value)}}e.ContextKeyGreaterEqualsExpr=t;class l{constructor(E,T){this.key=E,this.value=T,this.type=14}static create(E,T){return new l(E,T)}cmp(E){return E.type!==this.type?this.type-E.type:I(this.key,this.value,E.key,E.value)}equals(E){return E.type===this.type?this.key===E.key&&this.value===E.value:!1}evaluate(E){return parseFloat(E.getValue(this.key))E.key)return 1;const T=this.regexp?this.regexp.source:"",O=E.regexp?E.regexp.source:"";return TO?1:0}equals(E){if(E.type===this.type){const T=this.regexp?this.regexp.source:"",O=E.regexp?E.regexp.source:"";return this.key===E.key&&T===O}return!1}evaluate(E){let T=E.getValue(this.key);return this.regexp?this.regexp.test(T):!1}serialize(){const E=this.regexp?`/${this.regexp.source}/${this.regexp.ignoreCase?"i":""}`:"/invalid/";return`${this.key} =~ ${E}`}keys(){return[this.key]}negate(){return _.create(this)}}e.ContextKeyRegexExpr=m;class _{constructor(E){this._actual=E,this.type=8}static create(E){return new _(E)}cmp(E){return E.type!==this.type?this.type-E.type:this._actual.cmp(E._actual)}equals(E){return E.type===this.type?this._actual.equals(E._actual):!1}evaluate(E){return!this._actual.evaluate(E)}serialize(){throw new Error("Method not implemented.")}keys(){return this._actual.keys()}negate(){return this._actual}}e.ContextKeyNotRegexExpr=_;class f{constructor(E){this.expr=E,this.type=6}static create(E){return f._normalizeArr(E)}cmp(E){if(E.type!==this.type)return this.type-E.type;if(this.expr.lengthE.expr.length)return 1;for(let T=0,O=this.expr.length;T1;){const A=T[T.length-1];if(A.type!==9)break;T.pop();const B=T.pop(),F=v.create(A.expr.map(D=>f.create([D,B])));F&&(T.push(F),T.sort(g))}return T.length===1?T[0]:new f(T)}}serialize(){return this.expr.map(E=>E.serialize()).join(" && ")}keys(){const E=[];for(let T of this.expr)E.push(...T.keys());return E}negate(){let E=[];for(let T of this.expr)E.push(T.negate());return v.create(E)}}e.ContextKeyAndExpr=f;class v{constructor(E){this.expr=E,this.type=9}static create(E){const T=v._normalizeArr(E);if(T.length!==0)return T.length===1?T[0]:new v(T)}cmp(E){if(E.type!==this.type)return this.type-E.type;if(this.expr.lengthE.expr.length)return 1;for(let T=0,O=this.expr.length;TE.serialize()).join(" || ")}keys(){const E=[];for(let T of this.expr)E.push(...T.keys());return E}negate(){let E=[];for(let O of this.expr)E.push(O.negate());const T=O=>O.type===9?O.expr:[O];for(;E.length>1;){const O=E.shift(),A=E.shift(),B=[];for(const F of T(O))for(const D of T(A))B.push(d.and(F,D));E.unshift(d.or(...B))}return E[0]}}e.ContextKeyOrExpr=v;class y extends o{constructor(E,T,O){super(E);this.key=E,this._defaultValue=T,typeof O=="object"?y._info.push(Object.assign(Object.assign({},O),{key:E})):O!==!0&&y._info.push({key:E,description:O,type:T!=null?typeof T:void 0})}static all(){return y._info.values()}bindTo(E){return E.createKey(this.key,this._defaultValue)}getValue(E){return E.getContextKeyValue(this.key)}toNegated(){return d.not(this.key)}isEqualTo(E){return d.equals(this.key,E)}}e.RawContextKey=y,y._info=[],e.IContextKeyService=N.createDecorator("contextKeyService"),e.SET_CONTEXT_COMMAND_ID="setContext";function L(k,E){return kE?1:0}function I(k,E,T,O){return kT?1:EO?1:0}}),define(Q[25],J([0,1,456,16]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EditorContextKeys=void 0;var M;(function(w){w.editorSimpleInput=new N.RawContextKey("editorSimpleInput",!1,!0),w.editorTextFocus=new N.RawContextKey("editorTextFocus",!1,b.localize(0,null)),w.focus=new N.RawContextKey("editorFocus",!1,b.localize(1,null)),w.textInputFocus=new N.RawContextKey("textInputFocus",!1,b.localize(2,null)),w.readOnly=new N.RawContextKey("editorReadonly",!1,b.localize(3,null)),w.inDiffEditor=new N.RawContextKey("inDiffEditor",!1,b.localize(4,null)),w.columnSelection=new N.RawContextKey("editorColumnSelection",!1,b.localize(5,null)),w.writable=w.readOnly.toNegated(),w.hasNonEmptySelection=new N.RawContextKey("editorHasSelection",!1,b.localize(6,null)),w.hasOnlyEmptySelection=w.hasNonEmptySelection.toNegated(),w.hasMultipleSelections=new N.RawContextKey("editorHasMultipleSelections",!1,b.localize(7,null)),w.hasSingleSelection=w.hasMultipleSelections.toNegated(),w.tabMovesFocus=new N.RawContextKey("editorTabMovesFocus",!1,b.localize(8,null)),w.tabDoesNotMoveFocus=w.tabMovesFocus.toNegated(),w.isInWalkThroughSnippet=new N.RawContextKey("isInEmbeddedEditor",!1,!0),w.canUndo=new N.RawContextKey("canUndo",!1,!0),w.canRedo=new N.RawContextKey("canRedo",!1,!0),w.hoverVisible=new N.RawContextKey("editorHoverVisible",!1,b.localize(9,null)),w.inCompositeEditor=new N.RawContextKey("inCompositeEditor",void 0,b.localize(10,null)),w.notInCompositeEditor=w.inCompositeEditor.toNegated(),w.languageId=new N.RawContextKey("editorLangId","",b.localize(11,null)),w.hasCompletionItemProvider=new N.RawContextKey("editorHasCompletionItemProvider",!1,b.localize(12,null)),w.hasCodeActionsProvider=new N.RawContextKey("editorHasCodeActionsProvider",!1,b.localize(13,null)),w.hasCodeLensProvider=new N.RawContextKey("editorHasCodeLensProvider",!1,b.localize(14,null)),w.hasDefinitionProvider=new N.RawContextKey("editorHasDefinitionProvider",!1,b.localize(15,null)),w.hasDeclarationProvider=new N.RawContextKey("editorHasDeclarationProvider",!1,b.localize(16,null)),w.hasImplementationProvider=new N.RawContextKey("editorHasImplementationProvider",!1,b.localize(17,null)),w.hasTypeDefinitionProvider=new N.RawContextKey("editorHasTypeDefinitionProvider",!1,b.localize(18,null)),w.hasHoverProvider=new N.RawContextKey("editorHasHoverProvider",!1,b.localize(19,null)),w.hasDocumentHighlightProvider=new N.RawContextKey("editorHasDocumentHighlightProvider",!1,b.localize(20,null)),w.hasDocumentSymbolProvider=new N.RawContextKey("editorHasDocumentSymbolProvider",!1,b.localize(21,null)),w.hasReferenceProvider=new N.RawContextKey("editorHasReferenceProvider",!1,b.localize(22,null)),w.hasRenameProvider=new N.RawContextKey("editorHasRenameProvider",!1,b.localize(23,null)),w.hasSignatureHelpProvider=new N.RawContextKey("editorHasSignatureHelpProvider",!1,b.localize(24,null)),w.hasInlineHintsProvider=new N.RawContextKey("editorHasInlineHintsProvider",!1,b.localize(25,null)),w.hasDocumentFormattingProvider=new N.RawContextKey("editorHasDocumentFormattingProvider",!1,b.localize(26,null)),w.hasDocumentSelectionFormattingProvider=new N.RawContextKey("editorHasDocumentSelectionFormattingProvider",!1,b.localize(27,null)),w.hasMultipleDocumentFormattingProvider=new N.RawContextKey("editorHasMultipleDocumentFormattingProvider",!1,b.localize(28,null)),w.hasMultipleDocumentSelectionFormattingProvider=new N.RawContextKey("editorHasMultipleDocumentSelectionFormattingProvider",!1,b.localize(29,null))})(M=e.EditorContextKeys||(e.EditorContextKeys={}))}),define(Q[183],J([0,1,12,14,18,16,23,26,24,20,67]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.provideSignatureHelp=e.Context=void 0,e.Context={Visible:new w.RawContextKey("parameterHintsVisible",!1),MultipleSignatures:new w.RawContextKey("parameterHintsMultipleSignatures",!1)};function c(o,s,a,u){return Ie(this,void 0,void 0,function*(){const r=M.SignatureHelpProviderRegistry.ordered(o);for(const i of r)try{const n=yield i.provideSignatureHelp(o,s,u,a);if(n)return n}catch(n){b.onUnexpectedExternalError(n)}})}e.provideSignatureHelp=c,C.CommandsRegistry.registerCommand("_executeSignatureHelpProvider",(o,...s)=>Ie(void 0,void 0,void 0,function*(){const[a,u,r]=s;g.assertType(d.URI.isUri(a)),g.assertType(N.Position.isIPosition(u)),g.assertType(typeof r=="string"||!r);const i=yield o.get(p.ITextModelService).createModelReference(a);try{const n=yield c(i.object.textEditorModel,N.Position.lift(u),{triggerKind:M.SignatureHelpTriggerKind.Invoke,isRetrigger:!1,triggerCharacter:r},S.CancellationToken.None);return n?(setTimeout(()=>n.dispose(),0),n.value):void 0}finally{i.dispose()}}))}),define(Q[547],J([0,1,15,12,6,2,91,18,183]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ParameterHintsModel=void 0;var g;(function(o){o.Default={type:0};class s{constructor(r,i){this.request=r,this.previouslyActiveHints=i,this.type=2}}o.Pending=s;class a{constructor(r){this.hints=r,this.type=1}}o.Active=a})(g||(g={}));class p extends w.Disposable{constructor(s,a=p.DEFAULT_DELAY){super();this._onChangedHints=this._register(new M.Emitter),this.onChangedHints=this._onChangedHints.event,this.triggerOnType=!1,this._state=g.Default,this._pendingTriggers=[],this._lastSignatureHelpResult=this._register(new w.MutableDisposable),this.triggerChars=new S.CharacterSet,this.retriggerChars=new S.CharacterSet,this.triggerId=0,this.editor=s,this.throttledDelayer=new b.Delayer(a),this._register(this.editor.onDidChangeConfiguration(()=>this.onEditorConfigurationChange())),this._register(this.editor.onDidChangeModel(u=>this.onModelChanged())),this._register(this.editor.onDidChangeModelLanguage(u=>this.onModelChanged())),this._register(this.editor.onDidChangeCursorSelection(u=>this.onCursorChange(u))),this._register(this.editor.onDidChangeModelContent(u=>this.onModelContentChange())),this._register(C.SignatureHelpProviderRegistry.onDidChange(this.onModelChanged,this)),this._register(this.editor.onDidType(u=>this.onDidType(u))),this.onEditorConfigurationChange(),this.onModelChanged()}get state(){return this._state}set state(s){this._state.type===2&&this._state.request.cancel(),this._state=s}cancel(s=!1){this.state=g.Default,this.throttledDelayer.cancel(),s||this._onChangedHints.fire(void 0)}trigger(s,a){const u=this.editor.getModel();if(!(!u||!C.SignatureHelpProviderRegistry.has(u))){const r=++this.triggerId;this._pendingTriggers.push(s),this.throttledDelayer.trigger(()=>this.doTrigger(r),a).catch(N.onUnexpectedError)}}next(){if(this.state.type===1){const s=this.state.hints.signatures.length,a=this.state.hints.activeSignature,u=a%s==s-1,r=this.editor.getOption(70).cycle;if((s<2||u)&&!r){this.cancel();return}this.updateActiveSignature(u&&r?0:a+1)}}previous(){if(this.state.type===1){const s=this.state.hints.signatures.length,a=this.state.hints.activeSignature,u=a===0,r=this.editor.getOption(70).cycle;if((s<2||u)&&!r){this.cancel();return}this.updateActiveSignature(u&&r?s-1:a-1)}}updateActiveSignature(s){this.state.type===1&&(this.state=new g.Active(Object.assign(Object.assign({},this.state.hints),{activeSignature:s})),this._onChangedHints.fire(this.state.hints))}doTrigger(s){return Ie(this,void 0,void 0,function*(){const a=this.state.type===1||this.state.type===2,u=this.getLastActiveHints();if(this.cancel(!0),this._pendingTriggers.length===0)return!1;const r=this._pendingTriggers.reduce(c);this._pendingTriggers=[];const i={triggerKind:r.triggerKind,triggerCharacter:r.triggerCharacter,isRetrigger:a,activeSignatureHelp:u};if(!this.editor.hasModel())return!1;const n=this.editor.getModel(),t=this.editor.getPosition();this.state=new g.Pending(b.createCancelablePromise(l=>d.provideSignatureHelp(n,t,i,l)),u);try{const l=yield this.state.request;return s!==this.triggerId?(l==null||l.dispose(),!1):!l||!l.value.signatures||l.value.signatures.length===0?(l==null||l.dispose(),this._lastSignatureHelpResult.clear(),this.cancel(),!1):(this.state=new g.Active(l.value),this._lastSignatureHelpResult.value=l,this._onChangedHints.fire(this.state.hints),!0)}catch(l){return s===this.triggerId&&(this.state=g.Default),N.onUnexpectedError(l),!1}})}getLastActiveHints(){switch(this.state.type){case 1:return this.state.hints;case 2:return this.state.previouslyActiveHints;default:return}}get isTriggered(){return this.state.type===1||this.state.type===2||this.throttledDelayer.isTriggered()}onModelChanged(){this.cancel(),this.triggerChars=new S.CharacterSet,this.retriggerChars=new S.CharacterSet;const s=this.editor.getModel();if(!!s)for(const a of C.SignatureHelpProviderRegistry.ordered(s)){for(const u of a.signatureHelpTriggerCharacters||[])this.triggerChars.add(u.charCodeAt(0)),this.retriggerChars.add(u.charCodeAt(0));for(const u of a.signatureHelpRetriggerCharacters||[])this.retriggerChars.add(u.charCodeAt(0))}}onDidType(s){if(!!this.triggerOnType){const a=s.length-1,u=s.charCodeAt(a);(this.triggerChars.has(u)||this.isTriggered&&this.retriggerChars.has(u))&&this.trigger({triggerKind:C.SignatureHelpTriggerKind.TriggerCharacter,triggerCharacter:s.charAt(a)})}}onCursorChange(s){s.source==="mouse"?this.cancel():this.isTriggered&&this.trigger({triggerKind:C.SignatureHelpTriggerKind.ContentChange})}onModelContentChange(){this.isTriggered&&this.trigger({triggerKind:C.SignatureHelpTriggerKind.ContentChange})}onEditorConfigurationChange(){this.triggerOnType=this.editor.getOption(70).enabled,this.triggerOnType||this.cancel()}dispose(){this.cancel(!0),super.dispose()}}e.ParameterHintsModel=p,p.DEFAULT_DELAY=120;function c(o,s){switch(s.triggerKind){case C.SignatureHelpTriggerKind.Invoke:return s;case C.SignatureHelpTriggerKind.ContentChange:return o;case C.SignatureHelpTriggerKind.TriggerCharacter:default:return s}}});var _e=this&&this.__param||function(q,e){return function(b,N){e(b,N,q)}};define(Q[548],J([0,1,16]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SuggestAlternatives=void 0;let N=class at{constructor(w,S){this._editor=w,this._index=0,this._ckOtherSuggestions=at.OtherSuggestions.bindTo(S)}dispose(){this.reset()}reset(){var w;this._ckOtherSuggestions.reset(),(w=this._listener)===null||w===void 0||w.dispose(),this._model=void 0,this._acceptNext=void 0,this._ignore=!1}set({model:w,index:S},C){if(w.items.length===0){this.reset();return}if(at._moveIndex(!0,w,S)===S){this.reset();return}this._acceptNext=C,this._model=w,this._index=S,this._listener=this._editor.onDidChangeCursorPosition(()=>{this._ignore||this.reset()}),this._ckOtherSuggestions.set(!0)}static _moveIndex(w,S,C){let d=C;for(;d=(d+S.items.length+(w?1:-1))%S.items.length,!(d===C||!S.items[d].completion.additionalTextEdits););return d}next(){this._move(!0)}prev(){this._move(!1)}_move(w){if(!!this._model)try{this._ignore=!0,this._index=at._moveIndex(w,this._model,this._index),this._acceptNext({index:this._index,item:this._model.items[this._index],model:this._model})}finally{this._ignore=!1}}};N.OtherSuggestions=new b.RawContextKey("hasOtherSuggestions",!1),N=Me([_e(1,b.IContextKeyService)],N),e.SuggestAlternatives=N}),define(Q[549],J([0,1,16]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WordContextKey=void 0;let N=class Mt{constructor(w,S){this._editor=w,this._enabled=!1,this._ckAtEnd=Mt.AtEnd.bindTo(S),this._configListener=this._editor.onDidChangeConfiguration(C=>C.hasChanged(106)&&this._update()),this._update()}dispose(){var w;this._configListener.dispose(),(w=this._selectionListener)===null||w===void 0||w.dispose(),this._ckAtEnd.reset()}_update(){const w=this._editor.getOption(106)==="on";if(this._enabled!==w)if(this._enabled=w,this._enabled){const S=()=>{if(!this._editor.hasModel()){this._ckAtEnd.set(!1);return}const C=this._editor.getModel(),d=this._editor.getSelection(),g=C.getWordAtPosition(d.getStartPosition());if(!g){this._ckAtEnd.set(!1);return}this._ckAtEnd.set(g.endColumn===d.getStartPosition().column)};this._selectionListener=this._editor.onDidChangeCursorSelection(S),S()}else this._selectionListener&&(this._ckAtEnd.reset(),this._selectionListener.dispose(),this._selectionListener=void 0)}};N.AtEnd=new b.RawContextKey("atEndOfWord",!1),N=Me([_e(1,b.IContextKeyService)],N),e.WordContextKey=N}),define(Q[65],J([0,1,9,16]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CONTEXT_ACCESSIBILITY_MODE_ENABLED=e.IAccessibilityService=void 0,e.IAccessibilityService=b.createDecorator("accessibilityService"),e.CONTEXT_ACCESSIBILITY_MODE_ENABLED=new N.RawContextKey("accessibilityModeEnabled",!1)}),define(Q[250],J([0,1,521,16,17]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InputFocusedContextKey=e.IsWindowsContext=void 0,e.IsWindowsContext=new N.RawContextKey("isWindows",M.isWindows,b.localize(0,null)),e.InputFocusedContextKey="inputFocus"}),define(Q[68],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IContextMenuService=e.IContextViewService=void 0,e.IContextViewService=b.createDecorator("contextViewService"),e.IContextMenuService=b.createDecorator("contextMenuService")}),define(Q[184],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IDialogService=void 0,e.IDialogService=b.createDecorator("dialogService")}),define(Q[138],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ServiceCollection=void 0;class b{constructor(...M){this._entries=new Map;for(let[w,S]of M)this.set(w,S)}set(M,w){const S=this._entries.get(M);return this._entries.set(M,w),S}has(M){return this._entries.has(M)}get(M){return this._entries.get(M)}}e.ServiceCollection=b}),define(Q[550],J([0,1,12,533,238,9,138,15]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InstantiationService=void 0;const d=!1;class g extends Error{constructor(s){super("cyclic dependency between services");this.message=s.toString()}}class p{constructor(s=new S.ServiceCollection,a=!1,u){this._activeInstantiations=new Set,this._services=s,this._strict=a,this._parent=u,this._services.set(w.IInstantiationService,this)}createChild(s){return new p(s,this._strict,this)}invokeFunction(s,...a){let u=c.traceInvocation(s),r=!1;try{return s({get:(n,t)=>{if(r)throw b.illegalState("service accessor is only valid during the invocation of its target method");const l=this._getOrCreateServiceInstance(n,u);if(!l&&t!==w.optional)throw new Error(`[invokeFunction] unknown service '${n}'`);return l}},...a)}finally{r=!0,u.stop()}}createInstance(s,...a){let u,r;return s instanceof M.SyncDescriptor?(u=c.traceCreation(s.ctor),r=this._createInstance(s.ctor,s.staticArguments.concat(a),u)):(u=c.traceCreation(s),r=this._createInstance(s,a,u)),u.stop(),r}_createInstance(s,a=[],u){let r=w._util.getServiceDependencies(s).sort((t,l)=>t.index-l.index),i=[];for(const t of r){let l=this._getOrCreateServiceInstance(t.id,u);if(!l&&this._strict&&!t.optional)throw new Error(`[createInstance] ${s.name} depends on UNKNOWN service ${t.id}.`);i.push(l)}let n=r.length>0?r[0].index:a.length;if(a.length!==n){console.warn(`[createInstance] First service dependency of ${s.name} at position ${n+1} conflicts with ${a.length} static arguments`);let t=n-a.length;t>0?a=a.concat(new Array(t)):a=a.slice(0,n)}return new s(...[...a,...i])}_setServiceInstance(s,a){if(this._services.get(s)instanceof M.SyncDescriptor)this._services.set(s,a);else if(this._parent)this._parent._setServiceInstance(s,a);else throw new Error("illegalState - setting UNKNOWN service instance")}_getServiceInstanceOrDescriptor(s){let a=this._services.get(s);return!a&&this._parent?this._parent._getServiceInstanceOrDescriptor(s):a}_getOrCreateServiceInstance(s,a){let u=this._getServiceInstanceOrDescriptor(s);return u instanceof M.SyncDescriptor?this._safeCreateAndCacheServiceInstance(s,u,a.branch(s,!0)):(a.branch(s,!1),u)}_safeCreateAndCacheServiceInstance(s,a,u){if(this._activeInstantiations.has(s))throw new Error(`illegal state - RECURSIVELY instantiating service '${s}'`);this._activeInstantiations.add(s);try{return this._createAndCacheServiceInstance(s,a,u)}finally{this._activeInstantiations.delete(s)}}_createAndCacheServiceInstance(s,a,u){const r=new N.Graph(t=>t.id.toString());let i=0;const n=[{id:s,desc:a,_trace:u}];for(;n.length;){const t=n.pop();if(r.lookupOrInsertNode(t),i++>1e3)throw new g(r);for(let l of w._util.getServiceDependencies(t.desc.ctor)){let h=this._getServiceInstanceOrDescriptor(l.id);if(!h&&!l.optional&&console.warn(`[createInstance] ${s} depends on ${l.id} which is NOT registered.`),h instanceof M.SyncDescriptor){const m={id:l.id,desc:h,_trace:t._trace.branch(l.id,!0)};r.insertEdge(t,m),n.push(m)}}}for(;;){const t=r.roots();if(t.length===0){if(!r.isEmpty())throw new g(r);break}for(const{data:l}of t){if(this._getServiceInstanceOrDescriptor(l.id)instanceof M.SyncDescriptor){const m=this._createServiceInstanceWithOwner(l.id,l.desc.ctor,l.desc.staticArguments,l.desc.supportsDelayedInstantiation,l._trace);this._setServiceInstance(l.id,m)}r.removeNode(l)}}return this._getServiceInstanceOrDescriptor(s)}_createServiceInstanceWithOwner(s,a,u=[],r,i){if(this._services.get(s)instanceof M.SyncDescriptor)return this._createServiceInstance(a,u,r,i);if(this._parent)return this._parent._createServiceInstanceWithOwner(s,a,u,r,i);throw new Error(`illegalState - creating UNKNOWN service instance ${a.name}`)}_createServiceInstance(s,a=[],u,r){if(u){const i=new C.IdleValue(()=>this._createInstance(s,a,r));return new Proxy(Object.create(null),{get(n,t){if(t in n)return n[t];let l=i.value,h=l[t];return typeof h!="function"||(h=h.bind(l),n[t]=h),h},set(n,t,l){return i.value[t]=l,!0}})}else return this._createInstance(s,a,r)}}e.InstantiationService=p;class c{constructor(s,a){this.type=s,this.name=a,this._start=Date.now(),this._dep=[]}static traceInvocation(s){return d?new c(1,s.name||s.toString().substring(0,42).replace(/\n/g,"")):c._None}static traceCreation(s){return d?new c(0,s.name):c._None}branch(s,a){let u=new c(2,s.toString());return this._dep.push([s,a,u]),u}stop(){let s=Date.now()-this._start;c._totals+=s;let a=!1;function u(i,n){let t=[],l=new Array(i+1).join(" ");for(const[h,m,_]of n._dep)if(m&&_){a=!0,t.push(`${l}CREATES -> ${h}`);let f=u(i+1,_);f&&t.push(f)}else t.push(`${l}uses -> ${h}`);return t.join(` +`)}let r=[`${this.type===0?"CREATE":"CALL"} ${this.name}`,`${u(1,this)}`,`DONE, took ${s.toFixed(2)}ms (grand total ${c._totals.toFixed(2)}ms)`];(s>2||a)&&console.log(r.join(` +`))}}c._None=new class extends c{constructor(){super(-1,null)}stop(){}branch(){return this}},c._totals=0}),define(Q[551],J([0,1,522,15,6,2]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractKeybindingService=void 0;class S extends w.Disposable{constructor(d,g,p,c,o){super();this._contextKeyService=d,this._commandService=g,this._telemetryService=p,this._notificationService=c,this._logService=o,this._onDidUpdateKeybindings=this._register(new M.Emitter),this._currentChord=null,this._currentChordChecker=new N.IntervalTimer,this._currentChordStatusMessage=null,this._currentSingleModifier=null,this._currentSingleModifierClearTimeout=new N.TimeoutTimer,this._logging=!1}get onDidUpdateKeybindings(){return this._onDidUpdateKeybindings?this._onDidUpdateKeybindings.event:M.Event.None}dispose(){super.dispose()}_log(d){this._logging&&this._logService.info(`[KeybindingService]: ${d}`)}getKeybindings(){return this._getResolver().getKeybindings()}lookupKeybinding(d){const g=this._getResolver().lookupPrimaryKeybinding(d);if(!!g)return g.resolvedKeybinding}dispatchEvent(d,g){return this._dispatch(d,g)}softDispatch(d,g){const p=this.resolveKeyboardEvent(d);if(p.isChord())return console.warn("Unexpected keyboard event mapped to a chord"),null;const[c]=p.getDispatchParts();if(c===null)return null;const o=this._contextKeyService.getContext(g),s=this._currentChord?this._currentChord.keypress:null;return this._getResolver().resolve(o,s,c)}_enterChordMode(d,g){this._currentChord={keypress:d,label:g},this._currentChordStatusMessage=this._notificationService.status(b.localize(0,null,g));const p=Date.now();this._currentChordChecker.cancelAndSet(()=>{if(!this._documentHasFocus()){this._leaveChordMode();return}Date.now()-p>5e3&&this._leaveChordMode()},500)}_leaveChordMode(){this._currentChordStatusMessage&&(this._currentChordStatusMessage.dispose(),this._currentChordStatusMessage=null),this._currentChordChecker.cancel(),this._currentChord=null}_dispatch(d,g){return this._doDispatch(this.resolveKeyboardEvent(d),g,!1)}_singleModifierDispatch(d,g){const p=this.resolveKeyboardEvent(d),[c]=p.getSingleModifierDispatchParts();return c!==null&&this._currentSingleModifier===null?(this._log(`+ Storing single modifier for possible chord ${c}.`),this._currentSingleModifier=c,this._currentSingleModifierClearTimeout.cancelAndSet(()=>{this._log("+ Clearing single modifier due to 300ms elapsed."),this._currentSingleModifier=null},300),!1):c!==null&&c===this._currentSingleModifier?(this._log(`/ Dispatching single modifier chord ${c} ${c}`),this._currentSingleModifierClearTimeout.cancel(),this._currentSingleModifier=null,this._doDispatch(p,g,!0)):(this._currentSingleModifierClearTimeout.cancel(),this._currentSingleModifier=null,!1)}_doDispatch(d,g,p=!1){let c=!1;if(d.isChord())return console.warn("Unexpected keyboard event mapped to a chord"),!1;let o=null,s=null;if(p){const[i]=d.getSingleModifierDispatchParts();o=i,s=i}else[o]=d.getDispatchParts(),s=this._currentChord?this._currentChord.keypress:null;if(o===null)return this._log("\\ Keyboard event cannot be dispatched in keydown phase."),c;const a=this._contextKeyService.getContext(g),u=d.getLabel(),r=this._getResolver().resolve(a,s,o);return this._logService.trace("KeybindingService#dispatch",u,r==null?void 0:r.commandId),r&&r.enterChord?(c=!0,this._enterChordMode(o,u),c):(this._currentChord&&(!r||!r.commandId)&&(this._notificationService.status(b.localize(1,null,this._currentChord.label,u),{hideAfter:10*1e3}),c=!0),this._leaveChordMode(),r&&r.commandId&&(r.bubble||(c=!0),typeof r.commandArgs=="undefined"?this._commandService.executeCommand(r.commandId).then(void 0,i=>this._notificationService.warn(i)):this._commandService.executeCommand(r.commandId,r.commandArgs).then(void 0,i=>this._notificationService.warn(i)),this._telemetryService.publicLog2("workbenchActionExecuted",{id:r.commandId,from:"keybinding"})),c)}mightProducePrintableCharacter(d){return d.ctrlKey||d.metaKey?!1:d.keyCode>=31&&d.keyCode<=56||d.keyCode>=21&&d.keyCode<=30}}e.AbstractKeybindingService=S}),define(Q[552],J([0,1,12,235,39]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.BaseResolvedKeybinding=void 0;class w extends M.ResolvedKeybinding{constructor(C,d){super();if(d.length===0)throw b.illegalArgument("parts");this._os=C,this._parts=d}getLabel(){return N.UILabelProvider.toLabel(this._os,this._parts,C=>this._getLabel(C))}getAriaLabel(){return N.AriaLabelProvider.toLabel(this._os,this._parts,C=>this._getAriaLabel(C))}isChord(){return this._parts.length>1}getParts(){return this._parts.map(C=>this._getPart(C))}_getPart(C){return new M.ResolvedKeybindingPart(C.ctrlKey,C.shiftKey,C.altKey,C.metaKey,this._getLabel(C),this._getAriaLabel(C))}getDispatchParts(){return this._parts.map(C=>this._getDispatchPart(C))}getSingleModifierDispatchParts(){return this._parts.map(C=>this._getSingleModifierDispatchPart(C))}}e.BaseResolvedKeybinding=w}),define(Q[37],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IKeybindingService=void 0,e.IKeybindingService=b.createDecorator("keybindingService")}),define(Q[251],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.KeybindingResolver=void 0;class b{constructor(S,C,d){this._log=d,this._defaultKeybindings=S,this._defaultBoundCommands=new Map;for(let g=0,p=S.length;g=0;a--)this._isTargetedForRemoval(S[a],c,o,p,s)&&S.splice(a,1)}return S.concat(d)}_addKeyPress(S,C){const d=this._map.get(S);if(typeof d=="undefined"){this._map.set(S,[C]),this._addToLookupMap(C);return}for(let g=d.length-1;g>=0;g--){let p=d[g];if(p.command!==C.command){const c=p.keypressParts.length>1,o=C.keypressParts.length>1;c&&o&&p.keypressParts[1]!==C.keypressParts[1]||b.whenIsEntirelyIncluded(p.when,C.when)&&this._removeFromLookupMap(p)}}d.push(C),this._addToLookupMap(C)}_addToLookupMap(S){if(!!S.command){let C=this._lookupMap.get(S.command);typeof C=="undefined"?(C=[S],this._lookupMap.set(S.command,C)):C.push(S)}}_removeFromLookupMap(S){if(!!S.command){let C=this._lookupMap.get(S.command);if(typeof C!="undefined"){for(let d=0,g=C.length;dc.type===9?c.expr:[c];let p=g(d).concat(g(C));for(let c=0;c1&&p.keypressParts[1]!==null?(this._log(`\\ From ${g.length} keybinding entries, matched chord, when: ${N(p.when)}, source: ${M(p)}.`),{enterChord:!0,leaveChord:!1,commandId:null,commandArgs:null,bubble:!1}):(this._log(`\\ From ${g.length} keybinding entries, matched ${p.command}, when: ${N(p.when)}, source: ${M(p)}.`),{enterChord:!1,leaveChord:p.keypressParts.length>1,commandId:p.command,commandArgs:p.commandArgs,bubble:p.bubble}):(this._log(`\\ From ${g.length} keybinding entries, no when clauses matched the context.`),null)}_findCommand(S,C){for(let d=C.length-1;d>=0;d--){let g=C[d];if(!!b.contextMatchesRules(S,g.when))return g}return null}static contextMatchesRules(S,C){return C?C.evaluate(S):!0}}e.KeybindingResolver=b;function N(w){return w?`${w.serialize()}`:"no when condition"}function M(w){return w.extensionId?w.isBuiltinExtension?`built-in extension ${w.extensionId}`:`user extension ${w.extensionId}`:w.isDefault?"built-in":"user"}}),define(Q[553],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.removeElementsAfterNulls=e.ResolvedKeybindingItem=void 0;class b{constructor(w,S,C,d,g,p,c){this.resolvedKeybinding=w,this.keypressParts=w?N(w.getDispatchParts()):[],w&&this.keypressParts.length===0&&(this.keypressParts=N(w.getSingleModifierDispatchParts())),this.bubble=S?S.charCodeAt(0)===94:!1,this.command=this.bubble?S.substr(1):S,this.commandArgs=C,this.when=d,this.isDefault=g,this.extensionId=p,this.isBuiltinExtension=c}}e.ResolvedKeybindingItem=b;function N(M){let w=[];for(let S=0,C=M.length;Sthis.layout()))}setContainer(C,d){this.contextView.setContainer(C,d||1)}showContextView(C,d,g){d?d!==this.container&&(this.container=d,this.setContainer(d,g?3:2)):this.container!==this.layoutService.container&&(this.container=this.layoutService.container,this.setContainer(this.container,1)),this.contextView.show(C);const p=N.toDisposable(()=>{this.currentViewDisposable===p&&this.hideContextView()});return this.currentViewDisposable=p,p}getContextViewElement(){return this.contextView.getViewElement()}layout(){this.contextView.layout()}hideContextView(C){this.contextView.hide(C)}};w=Me([_e(0,M.ILayoutService)],w),e.ContextViewService=w}),define(Q[77],J([0,1,9,2,6]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LogService=e.ConsoleLogger=e.AbstractLogger=e.DEFAULT_LOG_LEVEL=e.LogLevel=e.ILogService=void 0,e.ILogService=b.createDecorator("logService");var w;(function(g){g[g.Trace=0]="Trace",g[g.Debug=1]="Debug",g[g.Info=2]="Info",g[g.Warning=3]="Warning",g[g.Error=4]="Error",g[g.Critical=5]="Critical",g[g.Off=6]="Off"})(w=e.LogLevel||(e.LogLevel={})),e.DEFAULT_LOG_LEVEL=w.Info;class S extends N.Disposable{constructor(){super(...arguments);this.level=e.DEFAULT_LOG_LEVEL,this._onDidChangeLogLevel=this._register(new M.Emitter)}setLevel(p){this.level!==p&&(this.level=p,this._onDidChangeLogLevel.fire(this.level))}getLevel(){return this.level}}e.AbstractLogger=S;class C extends S{constructor(p=e.DEFAULT_LOG_LEVEL){super();this.setLevel(p)}trace(p,...c){this.getLevel()<=w.Trace&&console.log("%cTRACE","color: #888",p,...c)}debug(p,...c){this.getLevel()<=w.Debug&&console.log("%cDEBUG","background: #eee; color: #888",p,...c)}info(p,...c){this.getLevel()<=w.Info&&console.log("%c INFO","color: #33f",p,...c)}error(p,...c){this.getLevel()<=w.Error&&console.log("%c ERR","color: #f33",p,...c)}dispose(){}}e.ConsoleLogger=C;class d extends N.Disposable{constructor(p){super();this.logger=p,this._register(p)}getLevel(){return this.logger.getLevel()}trace(p,...c){this.logger.trace(p,...c)}debug(p,...c){this.logger.debug(p,...c)}info(p,...c){this.logger.info(p,...c)}error(p,...c){this.logger.error(p,...c)}}e.LogService=d}),define(Q[252],J([0,1,15,2,201,294,3,18,41,389,36,137,8,19,77,81,12]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EditorWorkerClient=e.EditorWorkerHost=e.EditorWorkerServiceImpl=void 0;const i=60*1e3,n=5*60*1e3;function t(L,I){let k=L.getModel(I);return!(!k||k.isTooLargeForSyncing())}let l=class extends N.Disposable{constructor(I,k,E){super();this._modelService=I,this._workerManager=this._register(new m(this._modelService)),this._logService=E,this._register(C.LinkProviderRegistry.register("*",{provideLinks:(T,O)=>t(this._modelService,T.uri)?this._workerManager.withWorker().then(A=>A.computeLinks(T.uri)).then(A=>A&&{links:A}):Promise.resolve({links:[]})})),this._register(C.CompletionProviderRegistry.register("*",new h(this._workerManager,k,this._modelService)))}dispose(){super.dispose()}canComputeDiff(I,k){return t(this._modelService,I)&&t(this._modelService,k)}computeDiff(I,k,E,T){return this._workerManager.withWorker().then(O=>O.computeDiff(I,k,E,T))}computeMoreMinimalEdits(I,k){if(s.isNonEmptyArray(k)){if(!t(this._modelService,I))return Promise.resolve(k);const E=u.StopWatch.create(!0),T=this._workerManager.withWorker().then(O=>O.computeMoreMinimalEdits(I,k));return T.finally(()=>this._logService.trace("FORMAT#computeMoreMinimalEdits",I.toString(!0),E.elapsed())),Promise.race([T,b.timeout(1e3).then(()=>k)])}else return Promise.resolve(void 0)}canNavigateValueSet(I){return t(this._modelService,I)}navigateValueSet(I,k,E){return this._workerManager.withWorker().then(T=>T.navigateValueSet(I,k,E))}canComputeWordRanges(I){return t(this._modelService,I)}computeWordRanges(I,k){return this._workerManager.withWorker().then(E=>E.computeWordRanges(I,k))}};l=Me([_e(0,p.IModelService),_e(1,c.ITextResourceConfigurationService),_e(2,a.ILogService)],l),e.EditorWorkerServiceImpl=l;class h{constructor(I,k,E){this._debugDisplayName="wordbasedCompletions",this._workerManager=I,this._configurationService=k,this._modelService=E}provideCompletionItems(I,k){return Ie(this,void 0,void 0,function*(){const E=this._configurationService.getValue(I.uri,k,"editor");if(!!E.wordBasedSuggestions){const T=[];if(E.wordBasedSuggestionsMode==="currentDocument")t(this._modelService,I.uri)&&T.push(I.uri);else for(const W of this._modelService.getModels())!t(this._modelService,W.uri)||(W===I?T.unshift(W.uri):(E.wordBasedSuggestionsMode==="allDocuments"||W.getLanguageIdentifier().id===I.getLanguageIdentifier().id)&&T.push(W.uri));if(T.length!==0){const O=d.LanguageConfigurationRegistry.getWordDefinition(I.getLanguageIdentifier().id),A=I.getWordAtPosition(k),B=A?new S.Range(k.lineNumber,A.startColumn,k.lineNumber,A.endColumn):S.Range.fromPositions(k),F=B.setEndPosition(k.lineNumber,k.column),R=yield(yield this._workerManager.withWorker()).textualSuggest(T,A==null?void 0:A.word,O);if(!!R)return{duration:R.duration,suggestions:R.words.map(W=>({kind:18,label:W,insertText:W,range:{insert:F,replace:B}}))}}}})}}class m extends N.Disposable{constructor(I){super();this._modelService=I,this._editorWorkerClient=null,this._lastWorkerUsedTime=new Date().getTime(),this._register(new b.IntervalTimer).cancelAndSet(()=>this._checkStopIdleWorker(),Math.round(n/2)),this._register(this._modelService.onModelRemoved(E=>this._checkStopEmptyWorker()))}dispose(){this._editorWorkerClient&&(this._editorWorkerClient.dispose(),this._editorWorkerClient=null),super.dispose()}_checkStopEmptyWorker(){!this._editorWorkerClient||this._modelService.getModels().length===0&&(this._editorWorkerClient.dispose(),this._editorWorkerClient=null)}_checkStopIdleWorker(){!this._editorWorkerClient||new Date().getTime()-this._lastWorkerUsedTime>n&&(this._editorWorkerClient.dispose(),this._editorWorkerClient=null)}withWorker(){return this._lastWorkerUsedTime=new Date().getTime(),this._editorWorkerClient||(this._editorWorkerClient=new y(this._modelService,!1,"editorWorkerService")),Promise.resolve(this._editorWorkerClient)}}class _ extends N.Disposable{constructor(I,k,E){super();if(this._syncedModels=Object.create(null),this._syncedModelsLastUsedTime=Object.create(null),this._proxy=I,this._modelService=k,!E){let T=new b.IntervalTimer;T.cancelAndSet(()=>this._checkStopModelSync(),Math.round(i/2)),this._register(T)}}dispose(){for(let I in this._syncedModels)N.dispose(this._syncedModels[I]);this._syncedModels=Object.create(null),this._syncedModelsLastUsedTime=Object.create(null),super.dispose()}ensureSyncedResources(I){for(const k of I){let E=k.toString();this._syncedModels[E]||this._beginModelSync(k),this._syncedModels[E]&&(this._syncedModelsLastUsedTime[E]=new Date().getTime())}}_checkStopModelSync(){let I=new Date().getTime(),k=[];for(let E in this._syncedModelsLastUsedTime)I-this._syncedModelsLastUsedTime[E]>i&&k.push(E);for(const E of k)this._stopModelSync(E)}_beginModelSync(I){let k=this._modelService.getModel(I);if(!!k&&!k.isTooLargeForSyncing()){let E=I.toString();this._proxy.acceptNewModel({url:k.uri.toString(),lines:k.getLinesContent(),EOL:k.getEOL(),versionId:k.getVersionId()});const T=new N.DisposableStore;T.add(k.onDidChangeContent(O=>{this._proxy.acceptModelChanged(E.toString(),O)})),T.add(k.onWillDispose(()=>{this._stopModelSync(E)})),T.add(N.toDisposable(()=>{this._proxy.acceptRemovedModel(E)})),this._syncedModels[E]=T}}_stopModelSync(I){let k=this._syncedModels[I];delete this._syncedModels[I],delete this._syncedModelsLastUsedTime[I],N.dispose(k)}}class f{constructor(I){this._instance=I,this._proxyObj=Promise.resolve(this._instance)}dispose(){this._instance.dispose()}getProxyObject(){return this._proxyObj}}class v{constructor(I){this._workerClient=I}fhr(I,k){return this._workerClient.fhr(I,k)}}e.EditorWorkerHost=v;class y extends N.Disposable{constructor(I,k,E){super();this._disposed=!1,this._modelService=I,this._keepIdleModels=k,this._workerFactory=new w.DefaultWorkerFactory(E),this._worker=null,this._modelManager=null}fhr(I,k){throw new Error("Not implemented!")}_getOrCreateWorker(){if(!this._worker)try{this._worker=this._register(new M.SimpleWorkerClient(this._workerFactory,"vs/editor/common/services/editorSimpleWorker",new v(this)))}catch(I){M.logOnceWebWorkerWarning(I),this._worker=new f(new g.EditorSimpleWorker(new v(this),null))}return this._worker}_getProxy(){return this._getOrCreateWorker().getProxyObject().then(void 0,I=>(M.logOnceWebWorkerWarning(I),this._worker=new f(new g.EditorSimpleWorker(new v(this),null)),this._getOrCreateWorker().getProxyObject()))}_getOrCreateModelManager(I){return this._modelManager||(this._modelManager=this._register(new _(I,this._modelService,this._keepIdleModels))),this._modelManager}_withSyncedResources(I){return this._disposed?Promise.reject(r.canceled()):this._getProxy().then(k=>(this._getOrCreateModelManager(k).ensureSyncedResources(I),k))}computeDiff(I,k,E,T){return this._withSyncedResources([I,k]).then(O=>O.computeDiff(I.toString(),k.toString(),E,T))}computeMoreMinimalEdits(I,k){return this._withSyncedResources([I]).then(E=>E.computeMoreMinimalEdits(I.toString(),k))}computeLinks(I){return this._withSyncedResources([I]).then(k=>k.computeLinks(I.toString()))}textualSuggest(I,k,E){return Ie(this,void 0,void 0,function*(){const T=yield this._withSyncedResources(I),O=E.source,A=o.regExpFlags(E);return T.textualSuggest(I.map(B=>B.toString()),k,O,A)})}computeWordRanges(I,k){return this._withSyncedResources([I]).then(E=>{let T=this._modelService.getModel(I);if(!T)return Promise.resolve(null);let O=d.LanguageConfigurationRegistry.getWordDefinition(T.getLanguageIdentifier().id),A=O.source,B=o.regExpFlags(O);return E.computeWordRanges(I.toString(),k,A,B)})}navigateValueSet(I,k,E){return this._withSyncedResources([I]).then(T=>{let O=this._modelService.getModel(I);if(!O)return null;let A=d.LanguageConfigurationRegistry.getWordDefinition(O.getLanguageIdentifier().id),B=A.source,F=o.regExpFlags(A);return T.navigateValueSet(I.toString(),k,E,B,F)})}dispose(){super.dispose(),this._disposed=!0}}e.EditorWorkerClient=y}),define(Q[253],J([0,1,18,77,135]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.toMultilineTokens2=e.SemanticTokensProviderStyling=void 0;class w{constructor(p,c,o){this._legend=p,this._themeService=c,this._logService=o,this._hashTable=new d,this._hasWarnedOverlappingTokens=!1}getMetadata(p,c,o){const s=this._hashTable.get(p,c,o.id);let a;if(s)a=s.metadata,this._logService.getLevel()===N.LogLevel.Trace&&this._logService.trace(`SemanticTokensProviderStyling [CACHED] ${p} / ${c}: foreground ${b.TokenMetadata.getForeground(a)}, fontStyle ${b.TokenMetadata.getFontStyle(a).toString(2)}`);else{let u=this._legend.tokenTypes[p];const r=[];if(u){let i=c;for(let t=0;i>0&&t>1;i>0&&this._logService.getLevel()===N.LogLevel.Trace&&(this._logService.trace(`SemanticTokensProviderStyling: unknown token modifier index: ${c.toString(2)} for legend: ${JSON.stringify(this._legend.tokenModifiers)}`),r.push("not-in-legend"));const n=this._themeService.getColorTheme().getTokenStyleMetadata(u,r,o.language);typeof n=="undefined"?a=2147483647:(a=0,typeof n.italic!="undefined"&&(a|=(n.italic?1:0)<<11|1),typeof n.bold!="undefined"&&(a|=(n.bold?2:0)<<11|2),typeof n.underline!="undefined"&&(a|=(n.underline?4:0)<<11|4),n.foreground&&(a|=n.foreground<<14|8),a===0&&(a=2147483647))}else this._logService.getLevel()===N.LogLevel.Trace&&this._logService.trace(`SemanticTokensProviderStyling: unknown token type index: ${p} for legend: ${JSON.stringify(this._legend.tokenTypes)}`),a=2147483647,u="not-in-legend";this._hashTable.add(p,c,o.id,a),this._logService.getLevel()===N.LogLevel.Trace&&this._logService.trace(`SemanticTokensProviderStyling ${p} (${u}) / ${c} (${r.join(" ")}): foreground ${b.TokenMetadata.getForeground(a)}, fontStyle ${b.TokenMetadata.getFontStyle(a).toString(2)}`)}return a}warnOverlappingSemanticTokens(p,c){this._hasWarnedOverlappingTokens||(this._hasWarnedOverlappingTokens=!0,console.warn(`Overlapping semantic tokens detected at lineNumber ${p}, column ${c}`))}}e.SemanticTokensProviderStyling=w;function S(g,p,c){const o=g.data,s=g.data.length/5|0,a=Math.max(Math.ceil(s/1024),400),u=[];let r=0,i=1,n=0;for(;rt&&o[5*I]===0;)I--;if(I-1===t){let k=l;for(;k+1O&&(p.warnOverlappingSemanticTokens(T,O+1),v=this._growCount){const a=this._elements;this._currentLengthIndex++,this._currentLength=d._SIZES[this._currentLengthIndex],this._growCount=Math.round(this._currentLengthIndex+1{const d=this._foreignModuleHost?N.getAllMethodNames(this._foreignModuleHost):[];return C.loadForeignModule(this._foreignModuleId,this._foreignModuleCreateData,d).then(g=>{this._foreignModuleCreateData=null;const p=(s,a)=>C.fmr(s,a),c=(s,a)=>function(){const u=Array.prototype.slice.call(arguments,0);return a(s,u)};let o={};for(const s of g)o[s]=c(s,p);return o})})),this._foreignProxy}getProxy(){return this._getForeignProxy()}withSyncedResources(C){return this._withSyncedResources(C).then(d=>this.getProxy())}}}),define(Q[85],J([0,1,9,524,82]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IMarkerService=e.IMarkerData=e.MarkerSeverity=void 0;var w;(function(C){C[C.Hint=1]="Hint",C[C.Info=2]="Info",C[C.Warning=4]="Warning",C[C.Error=8]="Error"})(w=e.MarkerSeverity||(e.MarkerSeverity={})),function(C){function d(s,a){return a-s}C.compare=d;const g=Object.create(null);g[C.Error]=N.localize(0,null),g[C.Warning]=N.localize(1,null),g[C.Info]=N.localize(2,null);function p(s){return g[s]||""}C.toString=p;function c(s){switch(s){case M.default.Error:return C.Error;case M.default.Warning:return C.Warning;case M.default.Info:return C.Info;case M.default.Ignore:return C.Hint}}C.fromSeverity=c;function o(s){switch(s){case C.Error:return M.default.Error;case C.Warning:return M.default.Warning;case C.Info:return M.default.Info;case C.Hint:return M.default.Ignore}}C.toSeverity=o}(w=e.MarkerSeverity||(e.MarkerSeverity={}));var S;(function(C){const d="";function g(c){return p(c,!0)}C.makeKey=g;function p(c,o){let s=[d];return c.source?s.push(c.source.replace("\xA6","\\\xA6")):s.push(d),c.code?typeof c.code=="string"?s.push(c.code.replace("\xA6","\\\xA6")):s.push(c.code.value.replace("\xA6","\\\xA6")):s.push(d),c.severity!==void 0&&c.severity!==null?s.push(w.toString(c.severity)):s.push(d),c.message&&o?s.push(c.message.replace("\xA6","\\\xA6")):s.push(d),c.startLineNumber!==void 0&&c.startLineNumber!==null?s.push(c.startLineNumber.toString()):s.push(d),c.startColumn!==void 0&&c.startColumn!==null?s.push(c.startColumn.toString()):s.push(d),c.endLineNumber!==void 0&&c.endLineNumber!==null?s.push(c.endLineNumber.toString()):s.push(d),c.endColumn!==void 0&&c.endColumn!==null?s.push(c.endColumn.toString()):s.push(d),s.push(d),s.join("\xA6")}C.makeKeyOptionalMessage=p})(S=e.IMarkerData||(e.IMarkerData={})),e.IMarkerService=b.createDecorator("markerService")}),define(Q[557],J([0,1,85,24,6,2,3,8,19,9,74,71]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IMarkerNavigationService=e.MarkerList=e.MarkerCoordinate=void 0;class o{constructor(r,i,n){this.marker=r,this.index=i,this.total=n}}e.MarkerCoordinate=o;let s=class Tt{constructor(r,i){this._markerService=i,this._onDidChange=new M.Emitter,this.onDidChange=this._onDidChange.event,this._dispoables=new w.DisposableStore,this._markers=[],this._nextIdx=-1,N.URI.isUri(r)?this._resourceFilter=t=>t.toString()===r.toString():r&&(this._resourceFilter=r);const n=()=>{this._markers=this._markerService.read({resource:N.URI.isUri(r)?r:void 0,severities:b.MarkerSeverity.Error|b.MarkerSeverity.Warning|b.MarkerSeverity.Info}),typeof r=="function"&&(this._markers=this._markers.filter(t=>this._resourceFilter(t.resource))),this._markers.sort(Tt._compareMarker)};n(),this._dispoables.add(i.onMarkerChanged(t=>{(!this._resourceFilter||t.some(l=>this._resourceFilter(l)))&&(n(),this._nextIdx=-1,this._onDidChange.fire())}))}dispose(){this._dispoables.dispose(),this._onDidChange.dispose()}matches(r){return!this._resourceFilter&&!r?!0:!this._resourceFilter||!r?!1:this._resourceFilter(r)}get selected(){const r=this._markers[this._nextIdx];return r&&new o(r,this._nextIdx+1,this._markers.length)}_initIdx(r,i,n){let t=!1,l=this._markers.findIndex(h=>h.resource.toString()===r.uri.toString());l<0&&(l=d.binarySearch(this._markers,{resource:r.uri},(h,m)=>C.compare(h.resource.toString(),m.resource.toString())),l<0&&(l=~l));for(let h=l;ht.resource.toString()===r.toString());if(!(n<0)){for(;nn[1])}}class p{constructor(s){this.errors=0,this.infos=0,this.warnings=0,this.unknowns=0,this._data=new C.ResourceMap,this._service=s,this._subscription=s.onMarkerChanged(this._update,this)}dispose(){this._subscription.dispose()}_update(s){for(const a of s){const u=this._data.get(a);u&&this._substract(u);const r=this._resourceStats(a);this._add(r),this._data.set(a,r)}}_resourceStats(s){const a={errors:0,warnings:0,infos:0,unknowns:0};if(s.scheme===N.Schemas.inMemory||s.scheme===N.Schemas.walkThrough||s.scheme===N.Schemas.walkThroughSnippet)return a;for(const{severity:u}of this._service.read({resource:s}))u===S.MarkerSeverity.Error?a.errors+=1:u===S.MarkerSeverity.Warning?a.warnings+=1:u===S.MarkerSeverity.Info?a.infos+=1:a.unknowns+=1;return a}_substract(s){this.errors-=s.errors,this.warnings-=s.warnings,this.infos-=s.infos,this.unknowns-=s.unknowns}_add(s){this.errors+=s.errors,this.warnings+=s.warnings,this.infos+=s.infos,this.unknowns+=s.unknowns}}class c{constructor(){this._onMarkerChanged=new w.Emitter,this.onMarkerChanged=w.Event.debounce(this._onMarkerChanged.event,c._debouncer,0),this._data=new g,this._stats=new p(this)}dispose(){this._stats.dispose(),this._onMarkerChanged.dispose()}remove(s,a){for(const u of a||[])this.changeOne(s,u,[])}changeOne(s,a,u){if(b.isFalsyOrEmpty(u))this._data.delete(a,s)&&this._onMarkerChanged.fire([a]);else{const r=[];for(const i of u){const n=c._toMarker(s,a,i);n&&r.push(n)}this._data.set(a,s,r),this._onMarkerChanged.fire([a])}}static _toMarker(s,a,u){let{code:r,severity:i,message:n,source:t,startLineNumber:l,startColumn:h,endLineNumber:m,endColumn:_,relatedInformation:f,tags:v}=u;if(!!n)return l=l>0?l:1,h=h>0?h:1,m=m>=l?m:l,_=_>0?_:h,{resource:a,owner:s,code:r,severity:i,message:n,source:t,startLineNumber:l,startColumn:h,endLineNumber:m,endColumn:_,relatedInformation:f,tags:v}}read(s=Object.create(null)){let{owner:a,resource:u,severities:r,take:i}=s;if((!i||i<0)&&(i=-1),a&&u){const n=this._data.get(u,a);if(n){const t=[];for(const l of n)if(c._accept(l,r)){const h=t.push(l);if(i>0&&h===i)break}return t}else return[]}else if(!a&&!u){const n=[];for(let t of this._data.values())for(let l of t)if(c._accept(l,r)){const h=n.push(l);if(i>0&&h===i)return n}return n}else{const n=this._data.values(u!=null?u:a),t=[];for(const l of n)for(const h of l)if(c._accept(h,r)){const m=t.push(h);if(i>0&&m===i)return t}return t}}static _accept(s,a){return a===void 0||(a&s.severity)===s.severity}static _debouncer(s,a){s||(c._dedupeMap=new C.ResourceMap,s=[]);for(const u of a)c._dedupeMap.has(u)||(c._dedupeMap.set(u,!0),s.push(u));return s}}e.MarkerService=c}),define(Q[32],J([0,1,82,9]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.NoOpNotification=e.INotificationService=e.Severity=void 0,e.Severity=b.default,e.INotificationService=N.createDecorator("notificationService");class M{}e.NoOpNotification=M}),define(Q[58],J([0,1,2,8,24,9]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.matchesScheme=e.NullOpenerService=e.IOpenerService=void 0,e.IOpenerService=w.createDecorator("openerService"),e.NullOpenerService=Object.freeze({_serviceBrand:void 0,registerOpener(){return b.Disposable.None},registerValidator(){return b.Disposable.None},registerExternalUriResolver(){return b.Disposable.None},setDefaultExternalOpener(){},registerExternalOpener(){return b.Disposable.None},open(){return Ie(this,void 0,void 0,function*(){return!1})},resolveExternalUri(C){return Ie(this,void 0,void 0,function*(){return{resolved:C,dispose(){}}})}});function S(C,d){return M.URI.isUri(C)?N.equalsIgnoreCase(C.scheme,d):N.startsWithIgnoreCase(C,d+":")}e.matchesScheme=S}),define(Q[140],J([0,1,290,58,57,12,243,6,2,18]),function(q,e,b,N,M,w,S,C,d,g){"use strict";var p;Object.defineProperty(e,"__esModule",{value:!0}),e.MarkdownRenderer=void 0;let c=class Rt{constructor(s,a,u){this._options=s,this._modeService=a,this._openerService=u,this._onDidRenderAsync=new C.Emitter,this.onDidRenderAsync=this._onDidRenderAsync.event}dispose(){this._onDidRenderAsync.dispose()}render(s,a,u){const r=new d.DisposableStore;let i;return s?i=b.renderMarkdown(s,Object.assign(Object.assign({},this._getRenderOptions(r)),a),u):i=document.createElement("span"),{element:i,dispose:()=>r.dispose()}}_getRenderOptions(s){return{baseUrl:this._options.baseUrl,codeBlockRenderer:(a,u)=>Ie(this,void 0,void 0,function*(){var r,i,n,t;let l;a?l=this._modeService.getModeIdForLanguageName(a):this._options.editor&&(l=(r=this._options.editor.getModel())===null||r===void 0?void 0:r.getLanguageIdentifier().language),l||(l="plaintext"),this._modeService.triggerMode(l);const h=(i=yield g.TokenizationRegistry.getPromise(l))!==null&&i!==void 0?i:void 0,m=document.createElement("span");m.innerHTML=(t=(n=Rt._ttpTokenizer)===null||n===void 0?void 0:n.createHTML(u,h))!==null&&t!==void 0?t:S.tokenizeToString(u,h);let _=this._options.codeBlockFontFamily;return this._options.editor&&(_=this._options.editor.getOption(38).fontFamily),_&&(m.style.fontFamily=_),m}),asyncRenderCallback:()=>this._onDidRenderAsync.fire(),actionHandler:{callback:a=>this._openerService.open(a,{fromUserGesture:!0,allowContributedOpeners:!0}).catch(w.onUnexpectedError),disposeables:s}}}};c._ttpTokenizer=(p=window.trustedTypes)===null||p===void 0?void 0:p.createPolicy("tokenizeToString",{createHTML(o,s){return S.tokenizeToString(o,s)}}),c=Me([_e(1,M.IModeService),_e(2,N.IOpenerService)],c),e.MarkdownRenderer=c}),define(Q[559],J([0,1,7,23,71,51,199,43,44,24,28,26,531,58]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.OpenerService=void 0;let a=class{constructor(n){this._commandService=n}open(n){return Ie(this,void 0,void 0,function*(){if(!s.matchesScheme(n,C.Schemas.command))return!1;typeof n=="string"&&(n=g.URI.parse(n));let t=[];try{t=S.parse(decodeURIComponent(n.query))}catch(l){try{t=S.parse(n.query)}catch(h){}}return Array.isArray(t)||(t=[t]),yield this._commandService.executeCommand(n.path,...t),!0})}};a=Me([_e(0,c.ICommandService)],a);let u=class{constructor(n){this._editorService=n}open(n,t){return Ie(this,void 0,void 0,function*(){typeof n=="string"&&(n=g.URI.parse(n));let l;const h=/^L?(\d+)(?:,(\d+))?/.exec(n.fragment);return h&&(l={startLineNumber:parseInt(h[1]),startColumn:h[2]?parseInt(h[2]):1},n=n.with({fragment:""})),n.scheme===C.Schemas.file&&(n=d.normalizePath(n)),yield this._editorService.openCodeEditor({resource:n,options:Object.assign({selection:l,context:(t==null?void 0:t.fromUserGesture)?o.EditorOpenContext.USER:o.EditorOpenContext.API},t==null?void 0:t.editorOptions)},this._editorService.getFocusedCodeEditor(),t==null?void 0:t.openToSide),!0})}};u=Me([_e(0,p.ICodeEditorService)],u);let r=class{constructor(n,t){this._openers=new M.LinkedList,this._validators=new M.LinkedList,this._resolvers=new M.LinkedList,this._resolvedUriTargets=new w.ResourceMap(l=>l.with({path:null,fragment:null,query:null}).toString()),this._externalOpeners=new M.LinkedList,this._defaultExternalOpener={openExternal:l=>Ie(this,void 0,void 0,function*(){return s.matchesScheme(l,C.Schemas.http)||s.matchesScheme(l,C.Schemas.https)?b.windowOpenNoOpener(l):window.location.href=l,!0})},this._openers.push({open:(l,h)=>Ie(this,void 0,void 0,function*(){return(h==null?void 0:h.openExternal)||s.matchesScheme(l,C.Schemas.mailto)||s.matchesScheme(l,C.Schemas.http)||s.matchesScheme(l,C.Schemas.https)?(yield this._doOpenExternal(l,h),!0):!1})}),this._openers.push(new a(t)),this._openers.push(new u(n))}registerOpener(n){return{dispose:this._openers.unshift(n)}}registerValidator(n){return{dispose:this._validators.push(n)}}registerExternalUriResolver(n){return{dispose:this._resolvers.push(n)}}setDefaultExternalOpener(n){this._defaultExternalOpener=n}registerExternalOpener(n){return{dispose:this._externalOpeners.push(n)}}open(n,t){var l;return Ie(this,void 0,void 0,function*(){const h=typeof n=="string"?g.URI.parse(n):n,m=(l=this._resolvedUriTargets.get(h))!==null&&l!==void 0?l:h;for(const _ of this._validators)if(!(yield _.shouldOpen(m)))return!1;for(const _ of this._openers)if(yield _.open(n,t))return!0;return!1})}resolveExternalUri(n,t){return Ie(this,void 0,void 0,function*(){for(const l of this._resolvers){const h=yield l.resolveExternalUri(n,t);if(h)return this._resolvedUriTargets.has(h.resolved)||this._resolvedUriTargets.set(h.resolved,n),h}return{resolved:n,dispose:()=>{}}})}_doOpenExternal(n,t){return Ie(this,void 0,void 0,function*(){const l=typeof n=="string"?g.URI.parse(n):n,{resolved:h}=yield this.resolveExternalUri(l,t);let m;if(typeof n=="string"&&l.toString()===h.toString()?m=n:m=encodeURI(h.toString(!0)),t==null?void 0:t.allowContributedOpeners){const _=typeof(t==null?void 0:t.allowContributedOpeners)=="string"?t==null?void 0:t.allowContributedOpeners:void 0;for(const f of this._externalOpeners)if(yield f.openExternal(m,{sourceUri:l,preferredOpenerId:_},N.CancellationToken.None))return!0}return this._defaultExternalOpener.openExternal(m,{sourceUri:l},N.CancellationToken.None)})}dispose(){this._validators.clear()}};r=Me([_e(0,p.ICodeEditorService),_e(1,c.ICommandService)],r),e.OpenerService=r}),define(Q[560],J([0,1,7,73,2,228,408,140,58,19]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ModesGlyphHoverWidget=void 0;class p{constructor(s){this._editor=s,this._lineNumber=-1,this._result=[]}setLineNumber(s){this._lineNumber=s,this._result=[]}clearResult(){this._result=[]}computeSync(){const s=r=>({value:r}),a=this._editor.getLineDecorations(this._lineNumber),u=[];if(!a)return u;for(const r of a)if(!!r.options.glyphMarginClassName){const i=r.options.glyphMarginHoverMessage;!i||N.isEmptyMarkdownString(i)||u.push(...g.asArray(i).map(s))}return u}onResult(s,a){this._result=this._result.concat(s)}getResult(){return this._result}getResultWithLoadingMessage(){return this.getResult()}}class c extends S.GlyphHoverWidget{constructor(s,a,u=d.NullOpenerService){super(c.ID,s);this._renderDisposeables=this._register(new M.DisposableStore),this._messages=[],this._lastLineNumber=-1,this._markdownRenderer=this._register(new C.MarkdownRenderer({editor:this._editor},a,u)),this._computer=new p(this._editor),this._hoverOperation=new w.HoverOperation(this._computer,r=>this._withResult(r),void 0,r=>this._withResult(r),300)}dispose(){this._hoverOperation.cancel(),super.dispose()}onModelDecorationsChanged(){this.isVisible&&(this._hoverOperation.cancel(),this._computer.clearResult(),this._hoverOperation.start(0))}startShowingAt(s){this._lastLineNumber!==s&&(this._hoverOperation.cancel(),this.hide(),this._lastLineNumber=s,this._computer.setLineNumber(s),this._hoverOperation.start(0))}hide(){this._lastLineNumber=-1,this._hoverOperation.cancel(),super.hide()}_withResult(s){this._messages=s,this._messages.length>0?this._renderMessages(this._lastLineNumber,this._messages):this.hide()}_renderMessages(s,a){this._renderDisposeables.clear();const u=document.createDocumentFragment();for(const r of a){const i=this._markdownRenderer.render(r.value);this._renderDisposeables.add(i),u.appendChild(b.$("div.hover-row",void 0,i.element))}this.updateContents(u),this.showAt(s)}}e.ModesGlyphHoverWidget=c,c.ID="editor.contrib.modesGlyphHoverWidget"}),define(Q[254],J([0,1,509,2,7,61,140,73,27,6,231,9]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SuggestDetailsOverlay=e.SuggestDetailsWidget=e.canExpandCompletionItem=void 0;function o(u){return!!u&&Boolean(u.completion.documentation||u.completion.detail&&u.completion.detail!==u.completion.label)}e.canExpandCompletionItem=o;let s=class{constructor(r,i){this._editor=r,this._onDidClose=new g.Emitter,this.onDidClose=this._onDidClose.event,this._onDidChangeContents=new g.Emitter,this.onDidChangeContents=this._onDidChangeContents.event,this._disposables=new N.DisposableStore,this._renderDisposeable=new N.DisposableStore,this._borderWidth=1,this._size=new M.Dimension(330,0),this.domNode=M.$(".suggest-details"),this.domNode.classList.add("no-docs"),this._markdownRenderer=i.createInstance(S.MarkdownRenderer,{editor:r}),this._body=M.$(".body"),this._scrollbar=new w.DomScrollableElement(this._body,{}),M.append(this.domNode,this._scrollbar.getDomNode()),this._disposables.add(this._scrollbar),this._header=M.append(this._body,M.$(".header")),this._close=M.append(this._header,M.$("span"+d.Codicon.close.cssSelector)),this._close.title=b.localize(0,null),this._type=M.append(this._header,M.$("p.type")),this._docs=M.append(this._body,M.$("p.docs")),this._configureFont(),this._disposables.add(this._editor.onDidChangeConfiguration(n=>{n.hasChanged(38)&&this._configureFont()}))}dispose(){this._disposables.dispose(),this._renderDisposeable.dispose()}_configureFont(){const r=this._editor.getOptions(),i=r.get(38),n=i.fontFamily,t=r.get(102)||i.fontSize,l=r.get(103)||i.lineHeight,h=i.fontWeight,m=`${t}px`,_=`${l}px`;this.domNode.style.fontSize=m,this.domNode.style.lineHeight=_,this.domNode.style.fontWeight=h,this.domNode.style.fontFeatureSettings=i.fontFeatureSettings,this._type.style.fontFamily=n,this._close.style.height=_,this._close.style.width=_}getLayoutInfo(){const r=this._editor.getOption(103)||this._editor.getOption(38).lineHeight,i=this._borderWidth,n=i*2;return{lineHeight:r,borderWidth:i,borderHeight:n,verticalPadding:22,horizontalPadding:14}}renderLoading(){this._type.textContent=b.localize(1,null),this._docs.textContent="",this.domNode.classList.remove("no-docs","no-type"),this.layout(this.size.width,this.getLayoutInfo().lineHeight*2),this._onDidChangeContents.fire(this)}renderItem(r,i){var n;this._renderDisposeable.clear();let{detail:t,documentation:l}=r.completion;if(i){let h="";h+=`score: ${r.score[0]}${r.word?`, compared '${r.completion.filterText&&r.completion.filterText+" (filterText)"||typeof r.completion.label=="string"?r.completion.label:r.completion.label.name}' with '${r.word}'`:" (no prefix)"} +`,h+=`distance: ${r.distance}, see localityBonus-setting +`,h+=`index: ${r.idx}, based on ${r.completion.sortText&&`sortText: "${r.completion.sortText}"`||"label"} +`,h+=`commit characters: ${(n=r.completion.commitCharacters)===null||n===void 0?void 0:n.join("")} +`,l=new C.MarkdownString().appendCodeblock("empty",h),t=`Provider: ${r.provider._debugDisplayName}`}if(!i&&!o(r)){this.clearContents();return}if(this.domNode.classList.remove("no-docs","no-type"),t){const h=t.length>1e5?`${t.substr(0,1e5)}\u2026`:t;this._type.textContent=h,this._type.title=h,M.show(this._type),this._type.classList.toggle("auto-wrap",!/\r?\n^\s+/gmi.test(h))}else M.clearNode(this._type),this._type.title="",M.hide(this._type),this.domNode.classList.add("no-type");if(M.clearNode(this._docs),typeof l=="string")this._docs.classList.remove("markdown-docs"),this._docs.textContent=l;else if(l){this._docs.classList.add("markdown-docs"),M.clearNode(this._docs);const h=this._markdownRenderer.render(l);this._docs.appendChild(h.element),this._renderDisposeable.add(h),this._renderDisposeable.add(this._markdownRenderer.onDidRenderAsync(()=>{this.layout(this._size.width,this._type.clientHeight+this._docs.clientHeight),this._onDidChangeContents.fire(this)}))}this.domNode.style.userSelect="text",this.domNode.tabIndex=-1,this._close.onmousedown=h=>{h.preventDefault(),h.stopPropagation()},this._close.onclick=h=>{h.preventDefault(),h.stopPropagation(),this._onDidClose.fire()},this._body.scrollTop=0,this.layout(this._size.width,this._type.clientHeight+this._docs.clientHeight),this._onDidChangeContents.fire(this)}clearContents(){this.domNode.classList.add("no-docs"),this._type.textContent="",this._docs.textContent=""}get size(){return this._size}layout(r,i){const n=new M.Dimension(r,i);M.Dimension.equals(n,this._size)||(this._size=n,M.size(this.domNode,r,i)),this._scrollbar.scanDomNode()}scrollDown(r=8){this._body.scrollTop+=r}scrollUp(r=8){this._body.scrollTop-=r}scrollTop(){this._body.scrollTop=0}scrollBottom(){this._body.scrollTop=this._body.scrollHeight}pageDown(){this.scrollDown(80)}pageUp(){this.scrollUp(80)}set borderWidth(r){this._borderWidth=r}get borderWidth(){return this._borderWidth}};s=Me([_e(1,c.IInstantiationService)],s),e.SuggestDetailsWidget=s;class a{constructor(r,i){this.widget=r,this._editor=i,this._disposables=new N.DisposableStore,this._added=!1,this._resizable=new p.ResizableHTMLElement,this._resizable.domNode.classList.add("suggest-details-container"),this._resizable.domNode.appendChild(r.domNode),this._resizable.enableSashes(!1,!0,!0,!1);let n,t,l=0,h=0;this._disposables.add(this._resizable.onDidWillResize(()=>{n=this._topLeft,t=this._resizable.size})),this._disposables.add(this._resizable.onDidResize(m=>{if(n&&t){this.widget.layout(m.dimension.width,m.dimension.height);let _=!1;m.west&&(h=t.width-m.dimension.width,_=!0),m.north&&(l=t.height-m.dimension.height,_=!0),_&&this._applyTopLeft({top:n.top+l,left:n.left+h})}m.done&&(n=void 0,t=void 0,l=0,h=0,this._userSize=m.dimension)})),this._disposables.add(this.widget.onDidChangeContents(()=>{var m;this._anchorBox&&this._placeAtAnchor(this._anchorBox,(m=this._userSize)!==null&&m!==void 0?m:this.widget.size)}))}dispose(){this._disposables.dispose(),this.hide()}getId(){return"suggest.details"}getDomNode(){return this._resizable.domNode}getPosition(){return null}show(){this._added||(this._editor.addOverlayWidget(this),this.getDomNode().style.position="fixed",this._added=!0)}hide(r=!1){this._added&&(this._editor.removeOverlayWidget(this),this._added=!1,this._anchorBox=void 0,this._topLeft=void 0),r&&(this._userSize=void 0,this.widget.clearContents())}placeAtAnchor(r){var i;const n=M.getDomNodePagePosition(r);this._anchorBox=n,this._placeAtAnchor(this._anchorBox,(i=this._userSize)!==null&&i!==void 0?i:this.widget.size)}_placeAtAnchor(r,i){const n=M.getClientArea(document.body),t=this.widget.getLayoutInfo();let l,h,m=new M.Dimension(220,2*t.lineHeight),_=0,f=r.top,v=r.top+r.height-t.borderHeight,y,L,I=n.width-(r.left+r.width+t.borderWidth+t.horizontalPadding);_=-t.borderWidth+r.left+r.width,L=!0,l=new M.Dimension(I,n.height-r.top-t.borderHeight-t.verticalPadding),h=l.with(void 0,r.top+r.height-t.borderHeight-t.verticalPadding),i.width>I&&(r.left>I&&(I=r.left-t.borderWidth-t.horizontalPadding,L=!1,_=Math.max(t.horizontalPadding,r.left-i.width-t.borderWidth),l=l.with(I),h=l.with(void 0,h.height)),r.width>I*1.3&&n.height-(r.top+r.height)>r.height&&(I=r.width,_=r.left,f=-t.borderWidth+r.top+r.height,l=new M.Dimension(r.width-t.borderHeight,n.height-r.top-r.height-t.verticalPadding),h=l.with(void 0,r.top-t.verticalPadding),m=m.with(l.width)));let k=i.height,E=Math.max(l.height,h.height);k>E&&(k=E);let T;k<=l.height?(y=!0,T=l):(y=!1,T=h),this._applyTopLeft({left:_,top:y?f:v-k}),this.getDomNode().style.position="fixed",this._resizable.enableSashes(!y,L,y,!L),this._resizable.minSize=m,this._resizable.maxSize=T,this._resizable.layout(k,Math.min(T.width,i.width)),this.widget.layout(this._resizable.size.width,this._resizable.size.height)}_applyTopLeft(r){this._topLeft=r,this.getDomNode().style.left=`${this._topLeft.left}px`,this.getDomNode().style.top=`${this._topLeft.top}px`}}e.SuggestDetailsOverlay=a}),define(Q[59],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IEditorProgressService=e.Progress=void 0;class N{constructor(w){this.callback=w}report(w){this._value=w,this.callback(this._value)}}e.Progress=N,N.None=Object.freeze({report(){}}),e.IEditorProgressService=b.createDecorator("editorProgressService")}),define(Q[561],J([0,1,23,2,15]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PickerQuickAccessProvider=e.TriggerAction=void 0;var w;(function(g){g[g.NO_ACTION=0]="NO_ACTION",g[g.CLOSE_PICKER=1]="CLOSE_PICKER",g[g.REFRESH_PICKER=2]="REFRESH_PICKER",g[g.REMOVE_ITEM=3]="REMOVE_ITEM"})(w=e.TriggerAction||(e.TriggerAction={}));function S(g){const p=g;return Array.isArray(p.items)}function C(g){const p=g;return!!p.picks&&p.additionalPicks instanceof Promise}class d extends N.Disposable{constructor(p,c){super();this.prefix=p,this.options=c}provide(p,c){var o;const s=new N.DisposableStore;p.canAcceptInBackground=!!((o=this.options)===null||o===void 0?void 0:o.canAcceptInBackground),p.matchOnLabel=p.matchOnDescription=p.matchOnDetail=p.sortByLabel=!1;let a;const u=s.add(new N.MutableDisposable),r=()=>Ie(this,void 0,void 0,function*(){const i=u.value=new N.DisposableStore;a==null||a.dispose(!0),p.busy=!1,a=new b.CancellationTokenSource(c);const n=a.token,t=p.value.substr(this.prefix.length).trim(),l=this.getPicks(t,i,n),h=(m,_)=>{var f;let v,y;if(S(m)?(v=m.items,y=m.active):v=m,v.length===0){if(_)return!1;t.length>0&&((f=this.options)===null||f===void 0?void 0:f.noResultsPick)&&(v=[this.options.noResultsPick])}return p.items=v,y&&(p.activeItems=[y]),!0};if(l!==null)if(C(l)){let m=!1,_=!1;yield Promise.all([(()=>Ie(this,void 0,void 0,function*(){yield M.timeout(d.FAST_PICKS_RACE_DELAY),!n.isCancellationRequested&&(_||(m=h(l.picks,!0)))}))(),(()=>Ie(this,void 0,void 0,function*(){p.busy=!0;try{const f=yield l.additionalPicks;if(n.isCancellationRequested)return;let v,y;S(l.picks)?(v=l.picks.items,y=l.picks.active):v=l.picks;let L,I;if(S(f)?(L=f.items,I=f.active):L=f,L.length>0||!m){let k;if(!y&&!I){const E=p.activeItems[0];E&&v.indexOf(E)!==-1&&(k=E)}h({items:[...v,...L],active:y||I||k})}}finally{n.isCancellationRequested||(p.busy=!1),_=!0}}))()])}else if(!(l instanceof Promise))h(l);else{p.busy=!0;try{const m=yield l;if(n.isCancellationRequested)return;h(m)}finally{n.isCancellationRequested||(p.busy=!1)}}});return s.add(p.onDidChangeValue(()=>r())),r(),s.add(p.onDidAccept(i=>{const[n]=p.selectedItems;typeof(n==null?void 0:n.accept)=="function"&&(i.inBackground||p.hide(),n.accept(p.keyMods,i))})),s.add(p.onDidTriggerItemButton(({button:i,item:n})=>Ie(this,void 0,void 0,function*(){var t,l;if(typeof n.trigger=="function"){const h=(l=(t=n.buttons)===null||t===void 0?void 0:t.indexOf(i))!==null&&l!==void 0?l:-1;if(h>=0){const m=n.trigger(h,p.keyMods),_=typeof m=="number"?m:yield m;if(c.isCancellationRequested)return;switch(_){case w.NO_ACTION:break;case w.CLOSE_PICKER:p.hide();break;case w.REFRESH_PICKER:r();break;case w.REMOVE_ITEM:const f=p.items.indexOf(n);if(f!==-1){const v=p.items.slice();v.splice(f,1),p.items=v}break}}}}))),s}}e.PickerQuickAccessProvider=d,d.FAST_PICKS_RACE_DELAY=200});var mt=this&&this.__createBinding||(Object.create?function(q,e,b,N){N===void 0&&(N=b),Object.defineProperty(q,N,{enumerable:!0,get:function(){return e[b]}})}:function(q,e,b,N){N===void 0&&(N=b),q[N]=e[b]}),pt=this&&this.__exportStar||function(q,e){for(var b in q)b!=="default"&&!Object.prototype.hasOwnProperty.call(e,b)&&mt(e,q,b)};define(Q[78],J([0,1,9,202]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IQuickInputService=void 0,pt(N,e),e.IQuickInputService=b.createDecorator("quickInputService")}),define(Q[33],J([0,1,20,195]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Registry=void 0;class M{constructor(){this.data=new Map}add(S,C){N.ok(b.isString(S)),N.ok(b.isObject(C)),N.ok(!this.data.has(S),"There is already an extension with this id"),this.data.set(S,C)}as(S){return this.data.get(S)||null}}e.Registry=new M}),define(Q[141],J([0,1,458,6,18,41,33]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PLAINTEXT_LANGUAGE_IDENTIFIER=e.PLAINTEXT_EXTENSION=e.PLAINTEXT_MODE_ID=e.ModesRegistry=e.EditorModesRegistry=e.Extensions=void 0,e.Extensions={ModesRegistry:"editor.modesRegistry"};class C{constructor(){this._onDidChangeLanguages=new N.Emitter,this.onDidChangeLanguages=this._onDidChangeLanguages.event,this._languages=[],this._dynamicLanguages=[]}registerLanguage(g){return this._languages.push(g),this._onDidChangeLanguages.fire(void 0),{dispose:()=>{for(let p=0,c=this._languages.length;p"},{open:'"',close:'"'},{open:"'",close:"'"},{open:"`",close:"`"}],folding:{offSide:!0}},0)}),define(Q[562],J([0,1,43,44,141,237]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.cssEscape=e.detectModeId=e.getIconClasses=void 0;function S(g,p,c,o){const s=o===w.FileKind.ROOT_FOLDER?["rootfolder-icon"]:o===w.FileKind.FOLDER?["folder-icon"]:["file-icon"];if(c){let a;if(c.scheme===b.Schemas.data?a=N.DataUri.parseMetaData(c).get(N.DataUri.META_DATA_LABEL):a=d(N.basenameOrAuthority(c).toLowerCase()),o===w.FileKind.FOLDER)s.push(`${a}-name-folder-icon`);else{if(a){if(s.push(`${a}-name-file-icon`),a.length<=255){const r=a.split(".");for(let i=1;i0&&C.charAt(C.length-1)==="#"?C.substring(0,C.length-1):C}class w{constructor(){this._onDidChangeSchema=new N.Emitter,this.schemasById={}}registerSchema(d,g){this.schemasById[M(d)]=g,this._onDidChangeSchema.fire(d)}notifySchemaChanged(d){this._onDidChangeSchema.fire(d)}}const S=new w;b.Registry.add(e.Extensions.JSONContribution,S)}),define(Q[95],J([0,1,519,6,33,20,185]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.validateProperty=e.getDefaultValue=e.overrideIdentifierFromKey=e.OVERRIDE_PROPERTY_PATTERN=e.resourceLanguageSettingsSchemaId=e.resourceSettings=e.windowSettings=e.machineOverridableSettings=e.machineSettings=e.applicationSettings=e.allSettings=e.Extensions=void 0,e.Extensions={Configuration:"base.contributions.configuration"},e.allSettings={properties:{},patternProperties:{}},e.applicationSettings={properties:{},patternProperties:{}},e.machineSettings={properties:{},patternProperties:{}},e.machineOverridableSettings={properties:{},patternProperties:{}},e.windowSettings={properties:{},patternProperties:{}},e.resourceSettings={properties:{},patternProperties:{}},e.resourceLanguageSettingsSchemaId="vscode://schemas/settings/resourceLanguage";const C=M.Registry.as(S.Extensions.JSONContribution);class d{constructor(){this.overrideIdentifiers=new Set,this._onDidSchemaChange=new N.Emitter,this._onDidUpdateConfiguration=new N.Emitter,this.defaultValues={},this.defaultLanguageConfigurationOverridesNode={id:"defaultOverrides",title:b.localize(0,null),properties:{}},this.configurationContributors=[this.defaultLanguageConfigurationOverridesNode],this.resourceLanguageSettingsSchema={properties:{},patternProperties:{},additionalProperties:!1,errorMessage:"Unknown editor configuration setting",allowTrailingCommas:!0,allowComments:!0},this.configurationProperties={},this.excludedConfigurationProperties={},C.registerSchema(e.resourceLanguageSettingsSchemaId,this.resourceLanguageSettingsSchema)}registerConfiguration(u,r=!0){this.registerConfigurations([u],r)}registerConfigurations(u,r=!0){const i=[];u.forEach(n=>{i.push(...this.validateAndRegisterProperties(n,r)),this.configurationContributors.push(n),this.registerJSONConfiguration(n)}),C.registerSchema(e.resourceLanguageSettingsSchemaId,this.resourceLanguageSettingsSchema),this._onDidSchemaChange.fire(),this._onDidUpdateConfiguration.fire(i)}registerOverrideIdentifiers(u){for(const r of u)this.overrideIdentifiers.add(r);this.updateOverridePropertyPatternKey()}validateAndRegisterProperties(u,r=!0,i=3){i=w.isUndefinedOrNull(u.scope)?i:u.scope;let n=[],t=u.properties;if(t)for(let h in t){if(r&&s(h)){delete t[h];continue}const m=t[h];if(this.updatePropertyDefaultValue(h,m),e.OVERRIDE_PROPERTY_PATTERN.test(h)?m.scope=void 0:m.scope=w.isUndefinedOrNull(m.scope)?i:m.scope,t[h].hasOwnProperty("included")&&!t[h].included){this.excludedConfigurationProperties[h]=t[h],delete t[h];continue}else this.configurationProperties[h]=t[h];!t[h].deprecationMessage&&t[h].markdownDeprecationMessage&&(t[h].deprecationMessage=t[h].markdownDeprecationMessage),n.push(h)}let l=u.allOf;if(l)for(let h of l)n.push(...this.validateAndRegisterProperties(h,r,i));return n}getConfigurationProperties(){return this.configurationProperties}registerJSONConfiguration(u){const r=i=>{let n=i.properties;if(n)for(const l in n)this.updateSchema(l,n[l]);let t=i.allOf;t&&t.forEach(r)};r(u)}updateSchema(u,r){switch(e.allSettings.properties[u]=r,r.scope){case 1:e.applicationSettings.properties[u]=r;break;case 2:e.machineSettings.properties[u]=r;break;case 6:e.machineOverridableSettings.properties[u]=r;break;case 3:e.windowSettings.properties[u]=r;break;case 4:e.resourceSettings.properties[u]=r;break;case 5:e.resourceSettings.properties[u]=r,this.resourceLanguageSettingsSchema.properties[u]=r;break}}updateOverridePropertyPatternKey(){for(const u of this.overrideIdentifiers.values()){const r=`[${u}]`,i={type:"object",description:b.localize(1,null),errorMessage:b.localize(2,null),$ref:e.resourceLanguageSettingsSchemaId};this.updatePropertyDefaultValue(r,i),e.allSettings.properties[r]=i,e.applicationSettings.properties[r]=i,e.machineSettings.properties[r]=i,e.machineOverridableSettings.properties[r]=i,e.windowSettings.properties[r]=i,e.resourceSettings.properties[r]=i}this._onDidSchemaChange.fire()}updatePropertyDefaultValue(u,r){let i=this.defaultValues[u];w.isUndefined(i)&&(i=r.default),w.isUndefined(i)&&(i=c(r.type)),r.default=i}}const g="\\[.*\\]$";e.OVERRIDE_PROPERTY_PATTERN=new RegExp(g);function p(a){return a.substring(1,a.length-1)}e.overrideIdentifierFromKey=p;function c(a){switch(Array.isArray(a)?a[0]:a){case"boolean":return!1;case"integer":case"number":return 0;case"string":return"";case"array":return[];case"object":return{};default:return null}}e.getDefaultValue=c;const o=new d;M.Registry.add(e.Extensions.Configuration,o);function s(a){return a.trim()?e.OVERRIDE_PROPERTY_PATTERN.test(a)?b.localize(4,null,a):o.getConfigurationProperties()[a]!==void 0?b.localize(5,null,a):null:b.localize(3,null)}e.validateProperty=s}),define(Q[186],J([0,1,453,6,2,40,19,38,126,163,95,33,196]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.isDiffEditorConfigurationKey=e.isEditorConfigurationKey=e.editorConfigurationBaseNode=e.CommonEditorConfiguration=e.ComputedEditorOptions=e.TabFocus=void 0,e.TabFocus=new class{constructor(){this._tabFocus=!1,this._onDidChangeTabFocus=new N.Emitter,this.onDidChangeTabFocus=this._onDidChangeTabFocus.event}getTabFocusMode(){return this._tabFocus}setTabFocusMode(L){this._tabFocus!==L&&(this._tabFocus=L,this._onDidChangeTabFocus.fire(this._tabFocus))}};const s=Object.hasOwnProperty;class a{constructor(){this._values=[]}_read(I){return this._values[I]}get(I){return this._values[I]}_write(I,k){this._values[I]=k}}e.ComputedEditorOptions=a;class u{constructor(){this._values=[]}_read(I){return this._values[I]}_write(I,k){this._values[I]=k}}class r{static readOptions(I){const k=I,E=new u;for(const T of C.editorOptionsRegistry){const O=T.name==="_never_"?void 0:k[T.name];E._write(T.id,O)}return E}static validateOptions(I){const k=new C.ValidatedEditorOptions;for(const E of C.editorOptionsRegistry)k._write(E.id,E.validate(I._read(E.id)));return k}static computeOptions(I,k){const E=new a;for(const T of C.editorOptionsRegistry)E._write(T.id,T.compute(k,E,I._read(T.id)));return E}static _deepEquals(I,k){if(typeof I!="object"||typeof k!="object")return I===k;if(Array.isArray(I)||Array.isArray(k))return Array.isArray(I)&&Array.isArray(k)?S.equals(I,k):!1;for(let E in I)if(!r._deepEquals(I[E],k[E]))return!1;return!0}static checkEquals(I,k){const E=[];let T=!1;for(const O of C.editorOptionsRegistry){const A=!r._deepEquals(I._read(O.id),k._read(O.id));E[O.id]=A,A&&(T=!0)}return T?new C.ConfigurationChangedEvent(E):null}}function i(L){const I=L.wordWrap;I===!0?L.wordWrap="on":I===!1&&(L.wordWrap="off");const k=L.lineNumbers;k===!0?L.lineNumbers="on":k===!1&&(L.lineNumbers="off"),L.autoClosingBrackets===!1&&(L.autoClosingBrackets="never",L.autoClosingQuotes="never",L.autoSurround="never"),L.cursorBlinking==="visible"&&(L.cursorBlinking="solid");const O=L.renderWhitespace;O===!0?L.renderWhitespace="boundary":O===!1&&(L.renderWhitespace="none");const A=L.renderLineHighlight;A===!0?L.renderLineHighlight="line":A===!1&&(L.renderLineHighlight="none");const B=L.acceptSuggestionOnEnter;B===!0?L.acceptSuggestionOnEnter="on":B===!1&&(L.acceptSuggestionOnEnter="off");const F=L.tabCompletion;F===!1?L.tabCompletion="off":F===!0&&(L.tabCompletion="onlySnippets");const D=L.suggest;if(D&&typeof D.filteredTypes=="object"&&D.filteredTypes){const Y={};Y.method="showMethods",Y.function="showFunctions",Y.constructor="showConstructors",Y.field="showFields",Y.variable="showVariables",Y.class="showClasses",Y.struct="showStructs",Y.interface="showInterfaces",Y.module="showModules",Y.property="showProperties",Y.event="showEvents",Y.operator="showOperators",Y.unit="showUnits",Y.value="showValues",Y.constant="showConstants",Y.enum="showEnums",Y.enumMember="showEnumMembers",Y.keyword="showKeywords",Y.text="showWords",Y.color="showColors",Y.file="showFiles",Y.reference="showReferences",Y.folder="showFolders",Y.typeParameter="showTypeParameters",Y.snippet="showSnippets",o.forEach(Y,ee=>{const se=D.filteredTypes[ee.key];se===!1&&(D[ee.value]=se)})}const R=L.hover;R===!0?L.hover={enabled:!0}:R===!1&&(L.hover={enabled:!1});const W=L.parameterHints;W===!0?L.parameterHints={enabled:!0}:W===!1&&(L.parameterHints={enabled:!1});const x=L.autoIndent;x===!0?L.autoIndent="full":x===!1&&(L.autoIndent="advanced");const K=L.matchBrackets;K===!0?L.matchBrackets="always":K===!1&&(L.matchBrackets="never")}function n(L){const I=w.deepClone(L);return i(I),I}class t extends M.Disposable{constructor(I,k){super();this._onDidChange=this._register(new N.Emitter),this.onDidChange=this._onDidChange.event,this._onDidChangeFast=this._register(new N.Emitter),this.onDidChangeFast=this._onDidChangeFast.event,this.isSimpleWidget=I,this._isDominatedByLongLines=!1,this._computeOptionsMemory=new C.ComputeOptionsMemory,this._viewLineCount=1,this._lineNumbersDigitCount=1,this._rawOptions=n(k),this._readOptions=r.readOptions(this._rawOptions),this._validatedOptions=r.validateOptions(this._readOptions),this._register(d.EditorZoom.onDidChangeZoomLevel(E=>this._recomputeOptions())),this._register(e.TabFocus.onDidChangeTabFocus(E=>this._recomputeOptions()))}observeReferenceElement(I){}updatePixelRatio(){}_recomputeOptions(){const I=this.options,k=this._computeInternalOptions();if(!I)this.options=k;else{const E=r.checkEquals(I,k);if(E===null)return;this.options=k,this._onDidChangeFast.fire(E),this._onDidChange.fire(E)}}getRawOptions(){return this._rawOptions}_computeInternalOptions(){const I=this._getEnvConfiguration(),k=g.BareFontInfo.createFromValidatedSettings(this._validatedOptions,I.zoomLevel,I.pixelRatio,this.isSimpleWidget),E={memory:this._computeOptionsMemory,outerWidth:I.outerWidth,outerHeight:I.outerHeight,fontInfo:this.readConfiguration(k),extraEditorClassName:I.extraEditorClassName,isDominatedByLongLines:this._isDominatedByLongLines,viewLineCount:this._viewLineCount,lineNumbersDigitCount:this._lineNumbersDigitCount,emptySelectionClipboard:I.emptySelectionClipboard,pixelRatio:I.pixelRatio,tabFocusMode:e.TabFocus.getTabFocusMode(),accessibilitySupport:I.accessibilitySupport};return r.computeOptions(this._validatedOptions,E)}static _subsetEquals(I,k){for(const E in k)if(s.call(k,E)){const T=k[E],O=I[E];if(O===T)continue;if(Array.isArray(O)&&Array.isArray(T)){if(!S.equals(O,T))return!1;continue}if(O&&typeof O=="object"&&T&&typeof T=="object"){if(!this._subsetEquals(O,T))return!1;continue}return!1}return!0}updateOptions(I){if(typeof I!="undefined"){const k=n(I);t._subsetEquals(this._rawOptions,k)||(this._rawOptions=w.mixin(this._rawOptions,k||{}),this._readOptions=r.readOptions(this._rawOptions),this._validatedOptions=r.validateOptions(this._readOptions),this._recomputeOptions())}}setIsDominatedByLongLines(I){this._isDominatedByLongLines=I,this._recomputeOptions()}setMaxLineNumber(I){const k=t._digitCount(I);this._lineNumbersDigitCount!==k&&(this._lineNumbersDigitCount=k,this._recomputeOptions())}setViewLineCount(I){this._viewLineCount!==I&&(this._viewLineCount=I,this._recomputeOptions())}static _digitCount(I){let k=0;for(;I;)I=Math.floor(I/10),k++;return k||1}}e.CommonEditorConfiguration=t,e.editorConfigurationBaseNode=Object.freeze({id:"editor",order:5,type:"object",title:b.localize(0,null),scope:5});const l=c.Registry.as(p.Extensions.Configuration),h=Object.assign(Object.assign({},e.editorConfigurationBaseNode),{properties:{"editor.tabSize":{type:"number",default:C.EDITOR_MODEL_DEFAULTS.tabSize,minimum:1,markdownDescription:b.localize(1,null)},"editor.insertSpaces":{type:"boolean",default:C.EDITOR_MODEL_DEFAULTS.insertSpaces,markdownDescription:b.localize(2,null)},"editor.detectIndentation":{type:"boolean",default:C.EDITOR_MODEL_DEFAULTS.detectIndentation,markdownDescription:b.localize(3,null)},"editor.trimAutoWhitespace":{type:"boolean",default:C.EDITOR_MODEL_DEFAULTS.trimAutoWhitespace,description:b.localize(4,null)},"editor.largeFileOptimizations":{type:"boolean",default:C.EDITOR_MODEL_DEFAULTS.largeFileOptimizations,description:b.localize(5,null)},"editor.wordBasedSuggestions":{type:"boolean",default:!0,description:b.localize(6,null)},"editor.wordBasedSuggestionsMode":{enum:["currentDocument","matchingDocuments","allDocuments"],default:"matchingDocuments",enumDescriptions:[b.localize(7,null),b.localize(8,null),b.localize(9,null)],description:b.localize(10,null)},"editor.semanticHighlighting.enabled":{enum:[!0,!1,"configuredByTheme"],enumDescriptions:[b.localize(11,null),b.localize(12,null),b.localize(13,null)],default:"configuredByTheme",description:b.localize(14,null)},"editor.stablePeek":{type:"boolean",default:!1,markdownDescription:b.localize(15,null)},"editor.maxTokenizationLineLength":{type:"integer",default:2e4,description:b.localize(16,null)},"diffEditor.maxComputationTime":{type:"number",default:5e3,description:b.localize(17,null)},"diffEditor.renderSideBySide":{type:"boolean",default:!0,description:b.localize(18,null)},"diffEditor.ignoreTrimWhitespace":{type:"boolean",default:!0,description:b.localize(19,null)},"diffEditor.renderIndicators":{type:"boolean",default:!0,description:b.localize(20,null)},"diffEditor.codeLens":{type:"boolean",default:!1,description:b.localize(21,null)},"diffEditor.wordWrap":{type:"string",enum:["off","on","inherit"],default:"inherit",markdownEnumDescriptions:[b.localize(22,null),b.localize(23,null),b.localize(24,null)]}}});function m(L){return typeof L.type!="undefined"||typeof L.anyOf!="undefined"}for(const L of C.editorOptionsRegistry){const I=L.schema;if(typeof I!="undefined")if(m(I))h.properties[`editor.${L.name}`]=I;else for(let k in I)s.call(I,k)&&(h.properties[k]=I[k])}let _=null;function f(){return _===null&&(_=Object.create(null),Object.keys(h.properties).forEach(L=>{_[L]=!0})),_}function v(L){return f()[`editor.${L}`]||!1}e.isEditorConfigurationKey=v;function y(L){return f()[`diffEditor.${L}`]||!1}e.isDiffEditorConfigurationKey=y,l.registerConfiguration(h)}),define(Q[69],J([0,1,35,6,2,17,361,212,186,38,163]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Configuration=e.clearAllFontInfos=void 0;class c{constructor(){this._keys=Object.create(null),this._values=Object.create(null)}has(r){const i=r.getId();return!!this._values[i]}get(r){const i=r.getId();return this._values[i]}put(r,i){const n=r.getId();this._keys[n]=r,this._values[n]=i}remove(r){const i=r.getId();delete this._keys[i],delete this._values[i]}getValues(){return Object.keys(this._keys).map(r=>this._values[r])}}function o(){s.INSTANCE.clearCache()}e.clearAllFontInfos=o;class s extends M.Disposable{constructor(){super();this._onDidChange=this._register(new N.Emitter),this.onDidChange=this._onDidChange.event,this._cache=new c,this._evictUntrustedReadingsTimeout=-1}dispose(){this._evictUntrustedReadingsTimeout!==-1&&(clearTimeout(this._evictUntrustedReadingsTimeout),this._evictUntrustedReadingsTimeout=-1),super.dispose()}clearCache(){this._cache=new c,this._onDidChange.fire()}_writeToCache(r,i){this._cache.put(r,i),!i.isTrusted&&this._evictUntrustedReadingsTimeout===-1&&(this._evictUntrustedReadingsTimeout=setTimeout(()=>{this._evictUntrustedReadingsTimeout=-1,this._evictUntrustedReadings()},5e3))}_evictUntrustedReadings(){const r=this._cache.getValues();let i=!1;for(const n of r)n.isTrusted||(i=!0,this._cache.remove(n));i&&this._onDidChange.fire()}readConfiguration(r){if(!this._cache.has(r)){let i=s._actualReadConfiguration(r);(i.typicalHalfwidthCharacterWidth<=2||i.typicalFullwidthCharacterWidth<=2||i.spaceWidth<=2||i.maxDigitWidth<=2)&&(i=new p.FontInfo({zoomLevel:b.getZoomLevel(),pixelRatio:b.getPixelRatio(),fontFamily:i.fontFamily,fontWeight:i.fontWeight,fontSize:i.fontSize,fontFeatureSettings:i.fontFeatureSettings,lineHeight:i.lineHeight,letterSpacing:i.letterSpacing,isMonospace:i.isMonospace,typicalHalfwidthCharacterWidth:Math.max(i.typicalHalfwidthCharacterWidth,5),typicalFullwidthCharacterWidth:Math.max(i.typicalFullwidthCharacterWidth,5),canUseHalfwidthRightwardsArrow:i.canUseHalfwidthRightwardsArrow,spaceWidth:Math.max(i.spaceWidth,5),middotWidth:Math.max(i.middotWidth,5),wsmiddotWidth:Math.max(i.wsmiddotWidth,5),maxDigitWidth:Math.max(i.maxDigitWidth,5)},!1)),this._writeToCache(r,i)}return this._cache.get(r)}static createRequest(r,i,n,t){const l=new S.CharWidthRequest(r,i);return n.push(l),t&&t.push(l),l}static _actualReadConfiguration(r){const i=[],n=[],t=this.createRequest("n",0,i,n),l=this.createRequest("\uFF4D",0,i,null),h=this.createRequest(" ",0,i,n),m=this.createRequest("0",0,i,n),_=this.createRequest("1",0,i,n),f=this.createRequest("2",0,i,n),v=this.createRequest("3",0,i,n),y=this.createRequest("4",0,i,n),L=this.createRequest("5",0,i,n),I=this.createRequest("6",0,i,n),k=this.createRequest("7",0,i,n),E=this.createRequest("8",0,i,n),T=this.createRequest("9",0,i,n),O=this.createRequest("\u2192",0,i,n),A=this.createRequest("\uFFEB",0,i,null),B=this.createRequest("\xB7",0,i,n),F=this.createRequest(String.fromCharCode(11825),0,i,null);this.createRequest("|",0,i,n),this.createRequest("/",0,i,n),this.createRequest("-",0,i,n),this.createRequest("_",0,i,n),this.createRequest("i",0,i,n),this.createRequest("l",0,i,n),this.createRequest("m",0,i,n),this.createRequest("|",1,i,n),this.createRequest("_",1,i,n),this.createRequest("i",1,i,n),this.createRequest("l",1,i,n),this.createRequest("m",1,i,n),this.createRequest("n",1,i,n),this.createRequest("|",2,i,n),this.createRequest("_",2,i,n),this.createRequest("i",2,i,n),this.createRequest("l",2,i,n),this.createRequest("m",2,i,n),this.createRequest("n",2,i,n),S.readCharWidths(r,i);const D=Math.max(m.width,_.width,f.width,v.width,y.width,L.width,I.width,k.width,E.width,T.width);let R=r.fontFeatureSettings===g.EditorFontLigatures.OFF;const W=n[0].width;for(let Y=1,ee=n.length;R&&Y.001){R=!1;break}}let x=!0;R&&A.width!==W&&(x=!1),A.width>O.width&&(x=!1);const K=b.getTimeSinceLastZoomLevelChanged()>2e3;return new p.FontInfo({zoomLevel:b.getZoomLevel(),pixelRatio:b.getPixelRatio(),fontFamily:r.fontFamily,fontWeight:r.fontWeight,fontSize:r.fontSize,fontFeatureSettings:r.fontFeatureSettings,lineHeight:r.lineHeight,letterSpacing:r.letterSpacing,isMonospace:R,typicalHalfwidthCharacterWidth:t.width,typicalFullwidthCharacterWidth:l.width,canUseHalfwidthRightwardsArrow:x,spaceWidth:h.width,middotWidth:B.width,wsmiddotWidth:F.width,maxDigitWidth:D},K)}}s.INSTANCE=new s;class a extends d.CommonEditorConfiguration{constructor(r,i,n=null,t){super(r,i);this.accessibilityService=t,this._elementSizeObserver=this._register(new C.ElementSizeObserver(n,i.dimension,()=>this._recomputeOptions())),this._register(s.INSTANCE.onDidChange(()=>this._recomputeOptions())),this._validatedOptions.get(9)&&this._elementSizeObserver.startObserving(),this._register(b.onDidChangeZoomLevel(l=>this._recomputeOptions())),this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(()=>this._recomputeOptions())),this._recomputeOptions()}static applyFontInfoSlow(r,i){r.style.fontFamily=i.getMassagedFontFamily(),r.style.fontWeight=i.fontWeight,r.style.fontSize=i.fontSize+"px",r.style.fontFeatureSettings=i.fontFeatureSettings,r.style.lineHeight=i.lineHeight+"px",r.style.letterSpacing=i.letterSpacing+"px"}static applyFontInfo(r,i){r.setFontFamily(i.getMassagedFontFamily()),r.setFontWeight(i.fontWeight),r.setFontSize(i.fontSize),r.setFontFeatureSettings(i.fontFeatureSettings),r.setLineHeight(i.lineHeight),r.setLetterSpacing(i.letterSpacing)}observeReferenceElement(r){this._elementSizeObserver.observe(r)}updatePixelRatio(){this._recomputeOptions()}static _getExtraEditorClassName(){let r="";return!b.isSafari&&!b.isWebkitWebView&&(r+="no-user-select "),b.isSafari&&(r+="no-minimap-shadow "),w.isMacintosh&&(r+="mac "),r}_getEnvConfiguration(){return{extraEditorClassName:a._getExtraEditorClassName(),outerWidth:this._elementSizeObserver.getWidth(),outerHeight:this._elementSizeObserver.getHeight(),emptySelectionClipboard:b.isWebKit||b.isFirefox,pixelRatio:b.getPixelRatio(),zoomLevel:b.getZoomLevel(),accessibilitySupport:this.accessibilityService.isScreenReaderOptimized()?2:this.accessibilityService.getAccessibilitySupport()}}readConfiguration(r){return s.INSTANCE.readConfiguration(r)}}e.Configuration=a}),define(Q[563],J([0,1,93,8,69,63]),function(q,e,b,N,M,w){"use strict";var S;Object.defineProperty(e,"__esModule",{value:!0}),e.DOMLineBreaksComputerFactory=void 0;const C=(S=window.trustedTypes)===null||S===void 0?void 0:S.createPolicy("domLineBreaksComputer",{createHTML:a=>a});class d{static create(){return new d}constructor(){}createLineBreaksComputer(u,r,i,n){r=r|0,i=+i;let t=[];return{addRequest:(l,h)=>{t.push(l)},finalize:()=>g(t,u,r,i,n)}}}e.DOMLineBreaksComputerFactory=d;function g(a,u,r,i,n){var t;if(i===-1){const A=[];for(let B=0,F=a.length;Bl?(F=0,D=0):R=l-K}const W=B.substr(F),x=p(W,D,r,R,m);_[A]=F,f[A]=D,v[A]=W,y[A]=x[0],L[A]=x[1]}const I=m.build(),k=(t=C==null?void 0:C.createHTML(I))!==null&&t!==void 0?t:I;h.innerHTML=k,h.style.position="absolute",h.style.top="10000",h.style.wordWrap="break-word",document.body.appendChild(h);let E=document.createRange();const T=Array.prototype.slice.call(h.children,0);let O=[];for(let A=0;A');const t=a.length;let l=u,h=0,m=[],_=[],f=0");for(let v=0;v"),m[v]=h,_[v]=l;const y=f;f=v+1"),m[a.length]=h,_[a.length]=l,n.appendASCIIString(""),[m,_]}function c(a,u,r,i){if(r.length<=1)return null;const n=Array.prototype.slice.call(u.children,0),t=[];try{o(a,n,i,0,null,r.length-1,null,t)}catch(l){return console.log(l),null}return t.length===0?null:(t.push(r.length),t)}function o(a,u,r,i,n,t,l,h){if(i!==t&&(n=n||s(a,u,r[i],r[i+1]),l=l||s(a,u,r[t],r[t+1]),!(Math.abs(n[0].top-l[0].top)<=.1))){if(i+1===t){h.push(t);return}const m=i+(t-i)/2|0,_=s(a,u,r[m],r[m+1]);o(a,u,r,i,n,m,_,h),o(a,u,r,m,_,t,l,h)}}function s(a,u,r,i){return a.setStart(u[r/16384|0].firstChild,r%16384),a.setEnd(u[i/16384|0].firstChild,i%16384),a.getClientRects()}}),define(Q[564],J([0,1,30,69,165,45]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MarginViewOverlays=e.ContentViewOverlays=e.ViewOverlayLine=e.ViewOverlays=void 0;class S extends w.ViewPart{constructor(c){super(c);this._visibleLines=new M.VisibleLinesCollection(this),this.domNode=this._visibleLines.domNode,this._dynamicOverlays=[],this._isFocused=!1,this.domNode.setClassName("view-overlays")}shouldRender(){if(super.shouldRender())return!0;for(let c=0,o=this._dynamicOverlays.length;cs.shouldRender());for(let s=0,a=o.length;s'),a.appendASCIIString(u),a.appendASCIIString(""),!0)}layoutLine(c,o){this._domNode&&(this._domNode.setTop(o),this._domNode.setHeight(this._lineHeight))}}e.ViewOverlayLine=C;class d extends S{constructor(c){super(c);const s=this._context.configuration.options.get(124);this._contentWidth=s.contentWidth,this.domNode.setHeight(0)}onConfigurationChanged(c){const s=this._context.configuration.options.get(124);return this._contentWidth=s.contentWidth,super.onConfigurationChanged(c)||!0}onScrollChanged(c){return super.onScrollChanged(c)||c.scrollWidthChanged}_viewOverlaysRender(c){super._viewOverlaysRender(c),this.domNode.setWidth(Math.max(c.scrollWidth,this._contentWidth))}}e.ContentViewOverlays=d;class g extends S{constructor(c){super(c);const o=this._context.configuration.options,s=o.get(124);this._contentLeft=s.contentLeft,this.domNode.setClassName("margin-view-overlays"),this.domNode.setWidth(1),N.Configuration.applyFontInfo(this.domNode,o.get(38))}onConfigurationChanged(c){const o=this._context.configuration.options;N.Configuration.applyFontInfo(this.domNode,o.get(38));const s=o.get(124);return this._contentLeft=s.contentLeft,super.onConfigurationChanged(c)||!0}onScrollChanged(c){return super.onScrollChanged(c)||c.scrollHeightChanged}_viewOverlaysRender(c){super._viewOverlaysRender(c);const o=Math.min(c.scrollHeight,1e6);this.domNode.setHeight(o),this.domNode.setWidth(this._contentLeft)}}e.MarginViewOverlays=g}),define(Q[565],J([0,1,7,30,8,69,38,14,3,124]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewCursor=void 0;class p{constructor(s,a,u,r,i,n){this.top=s,this.left=a,this.width=u,this.height=r,this.textContent=i,this.textContentClassName=n}}class c{constructor(s){this._context=s;const a=this._context.configuration.options,u=a.get(38);this._cursorStyle=a.get(21),this._lineHeight=a.get(53),this._typicalHalfwidthCharacterWidth=u.typicalHalfwidthCharacterWidth,this._lineCursorWidth=Math.min(a.get(24),this._typicalHalfwidthCharacterWidth),this._isVisible=!0,this._domNode=N.createFastDomNode(document.createElement("div")),this._domNode.setClassName(`cursor ${g.MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`),this._domNode.setHeight(this._lineHeight),this._domNode.setTop(0),this._domNode.setLeft(0),w.Configuration.applyFontInfo(this._domNode,u),this._domNode.setDisplay("none"),this._position=new C.Position(1,1),this._lastRenderedContent="",this._renderData=null}getDomNode(){return this._domNode}getPosition(){return this._position}show(){this._isVisible||(this._domNode.setVisibility("inherit"),this._isVisible=!0)}hide(){this._isVisible&&(this._domNode.setVisibility("hidden"),this._isVisible=!1)}onConfigurationChanged(s){const a=this._context.configuration.options,u=a.get(38);return this._cursorStyle=a.get(21),this._lineHeight=a.get(53),this._typicalHalfwidthCharacterWidth=u.typicalHalfwidthCharacterWidth,this._lineCursorWidth=Math.min(a.get(24),this._typicalHalfwidthCharacterWidth),w.Configuration.applyFontInfo(this._domNode,u),!0}onCursorPositionChanged(s){return this._position=s,!0}_prepareRender(s){let a="";if(this._cursorStyle===S.TextEditorCursorStyle.Line||this._cursorStyle===S.TextEditorCursorStyle.LineThin){const f=s.visibleRangeForPosition(this._position);if(!f||f.outsideRenderedLine)return null;let v;if(this._cursorStyle===S.TextEditorCursorStyle.Line){if(v=b.computeScreenAwareSize(this._lineCursorWidth>0?this._lineCursorWidth:2),v>2){const I=this._context.model.getLineContent(this._position.lineNumber),k=M.nextCharLength(I,this._position.column-1);a=I.substr(this._position.column-1,k)}}else v=b.computeScreenAwareSize(1);let y=f.left;v>=2&&y>=1&&(y-=1);const L=s.getVerticalOffsetForLineNumber(this._position.lineNumber)-s.bigNumbersDelta;return new p(L,y,v,this._lineHeight,a,"")}const u=this._context.model.getLineContent(this._position.lineNumber),r=M.nextCharLength(u,this._position.column-1),i=s.linesVisibleRangesForRange(new d.Range(this._position.lineNumber,this._position.column,this._position.lineNumber,this._position.column+r),!1);if(!i||i.length===0)return null;const n=i[0];if(n.outsideRenderedLine||n.ranges.length===0)return null;const t=n.ranges[0],l=t.width<1?this._typicalHalfwidthCharacterWidth:t.width;let h="";if(this._cursorStyle===S.TextEditorCursorStyle.Block){const f=this._context.model.getViewLineData(this._position.lineNumber);a=u.substr(this._position.column-1,r);const v=f.tokens.findTokenIndexAtOffset(this._position.column-1);h=f.tokens.getClassName(v)}let m=s.getVerticalOffsetForLineNumber(this._position.lineNumber)-s.bigNumbersDelta,_=this._lineHeight;return(this._cursorStyle===S.TextEditorCursorStyle.Underline||this._cursorStyle===S.TextEditorCursorStyle.UnderlineThin)&&(m+=this._lineHeight-2,_=2),new p(m,t.left,l,_,a,h)}prepareRender(s){this._renderData=this._prepareRender(s)}render(s){return this._renderData?(this._lastRenderedContent!==this._renderData.textContent&&(this._lastRenderedContent=this._renderData.textContent,this._domNode.domNode.textContent=this._lastRenderedContent),this._domNode.setClassName(`cursor ${g.MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ${this._renderData.textContentClassName}`),this._domNode.setDisplay("block"),this._domNode.setTop(this._renderData.top),this._domNode.setLeft(this._renderData.left),this._domNode.setWidth(this._renderData.width),this._domNode.setLineHeight(this._renderData.height),this._domNode.setHeight(this._renderData.height),{domNode:this._domNode.domNode,position:this._position,contentLeft:this._renderData.left,height:this._renderData.height,width:2}):(this._domNode.setDisplay("none"),null)}}e.ViewCursor=c}),define(Q[566],J([0,1,12,6,2,291,8,18,141,76,95,33]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LanguagesRegistry=void 0;const o=Object.prototype.hasOwnProperty;class s extends M.Disposable{constructor(u=!0,r=!1){super();this._onDidChange=this._register(new N.Emitter),this.onDidChange=this._onDidChange.event,this._warnOnOverwrite=r,this._nextLanguageId2=1,this._languageIdToLanguage=[],this._languageToLanguageId=Object.create(null),this._languages={},this._mimeTypesMap={},this._nameMap={},this._lowercaseNameMap={},u&&(this._initializeFromRegistry(),this._register(d.ModesRegistry.onDidChangeLanguages(i=>this._initializeFromRegistry())))}_initializeFromRegistry(){this._languages={},this._mimeTypesMap={},this._nameMap={},this._lowercaseNameMap={};const u=d.ModesRegistry.getLanguages();this._registerLanguages(u)}_registerLanguages(u){for(const r of u)this._registerLanguage(r);this._mimeTypesMap={},this._nameMap={},this._lowercaseNameMap={},Object.keys(this._languages).forEach(r=>{let i=this._languages[r];i.name&&(this._nameMap[i.name]=i.identifier),i.aliases.forEach(n=>{this._lowercaseNameMap[n.toLowerCase()]=i.identifier}),i.mimetypes.forEach(n=>{this._mimeTypesMap[n]=i.identifier})}),c.Registry.as(p.Extensions.Configuration).registerOverrideIdentifiers(d.ModesRegistry.getLanguages().map(r=>r.id)),this._onDidChange.fire()}_getLanguageId(u){if(this._languageToLanguageId[u])return this._languageToLanguageId[u];const r=this._nextLanguageId2++;return this._languageIdToLanguage[r]=u,this._languageToLanguageId[u]=r,r}_registerLanguage(u){const r=u.id;let i;if(o.call(this._languages,r))i=this._languages[r];else{const n=this._getLanguageId(r);i={identifier:new C.LanguageIdentifier(r,n),name:null,mimetypes:[],aliases:[],extensions:[],filenames:[],configurationFiles:[]},this._languages[r]=i}this._mergeLanguage(i,u)}_mergeLanguage(u,r){const i=r.id;let n=null;if(Array.isArray(r.mimetypes)&&r.mimetypes.length>0&&(u.mimetypes.push(...r.mimetypes),n=r.mimetypes[0]),n||(n=`text/x-${i}`,u.mimetypes.push(n)),Array.isArray(r.extensions)){r.configuration?u.extensions=r.extensions.concat(u.extensions):u.extensions=u.extensions.concat(r.extensions);for(let h of r.extensions)w.registerTextMime({id:i,mime:n,extension:h},this._warnOnOverwrite)}if(Array.isArray(r.filenames))for(let h of r.filenames)w.registerTextMime({id:i,mime:n,filename:h},this._warnOnOverwrite),u.filenames.push(h);if(Array.isArray(r.filenamePatterns))for(let h of r.filenamePatterns)w.registerTextMime({id:i,mime:n,filepattern:h},this._warnOnOverwrite);if(typeof r.firstLine=="string"&&r.firstLine.length>0){let h=r.firstLine;h.charAt(0)!=="^"&&(h="^"+h);try{let m=new RegExp(h);S.regExpLeadsToEndlessLoop(m)||w.registerTextMime({id:i,mime:n,firstline:m},this._warnOnOverwrite)}catch(m){b.onUnexpectedError(m)}}u.aliases.push(i);let t=null;if(typeof r.aliases!="undefined"&&Array.isArray(r.aliases)&&(r.aliases.length===0?t=[null]:t=r.aliases),t!==null)for(const h of t)!h||h.length===0||u.aliases.push(h);let l=t!==null&&t.length>0;if(!(l&&t[0]===null)){let h=(l?t[0]:null)||i;(l||!u.name)&&(u.name=h)}r.configuration&&u.configurationFiles.push(r.configuration)}isRegisteredMode(u){return o.call(this._mimeTypesMap,u)?!0:o.call(this._languages,u)}getModeIdForLanguageNameLowercase(u){return o.call(this._lowercaseNameMap,u)?this._lowercaseNameMap[u].language:null}extractModeIds(u){return u?u.split(",").map(r=>r.trim()).map(r=>o.call(this._mimeTypesMap,r)?this._mimeTypesMap[r].language:r).filter(r=>o.call(this._languages,r)):[]}getLanguageIdentifier(u){if(u===g.NULL_MODE_ID||u===0)return g.NULL_LANGUAGE_IDENTIFIER;let r;if(typeof u=="string")r=u;else if(r=this._languageIdToLanguage[u],!r)return null;return o.call(this._languages,r)?this._languages[r].identifier:null}getModeIdsFromFilepathOrFirstLine(u,r){if(!u&&!r)return[];let i=w.guessMimeTypes(u,r);return this.extractModeIds(i.join(","))}}e.LanguagesRegistry=s}),define(Q[567],J([0,1,6,2,373,76,566,19]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ModeServiceImpl=void 0;class d extends N.Disposable{constructor(c,o){super();this._onDidChange=this._register(new b.Emitter),this.onDidChange=this._onDidChange.event,this._selector=o,this.languageIdentifier=this._selector(),this._register(c(()=>this._evaluate()))}_evaluate(){let c=this._selector();c.id!==this.languageIdentifier.id&&(this.languageIdentifier=c,this._onDidChange.fire(this.languageIdentifier))}}class g extends N.Disposable{constructor(c=!1){super();this._onDidCreateMode=this._register(new b.Emitter),this.onDidCreateMode=this._onDidCreateMode.event,this._onLanguagesMaybeChanged=this._register(new b.Emitter),this.onLanguagesMaybeChanged=this._onLanguagesMaybeChanged.event,this._instantiatedModes={},this._registry=this._register(new S.LanguagesRegistry(!0,c)),this._register(this._registry.onDidChange(()=>this._onLanguagesMaybeChanged.fire()))}isRegisteredMode(c){return this._registry.isRegisteredMode(c)}getModeIdForLanguageName(c){return this._registry.getModeIdForLanguageNameLowercase(c)}getModeIdByFilepathOrFirstLine(c,o){const s=this._registry.getModeIdsFromFilepathOrFirstLine(c,o);return C.firstOrDefault(s,null)}getModeId(c){const o=this._registry.extractModeIds(c);return C.firstOrDefault(o,null)}getLanguageIdentifier(c){return this._registry.getLanguageIdentifier(c)}create(c){return new d(this.onLanguagesMaybeChanged,()=>{const o=this.getModeId(c);return this._createModeAndGetLanguageIdentifier(o)})}createByFilepathOrFirstLine(c,o){return new d(this.onLanguagesMaybeChanged,()=>{const s=this.getModeIdByFilepathOrFirstLine(c,o);return this._createModeAndGetLanguageIdentifier(s)})}_createModeAndGetLanguageIdentifier(c){const o=this.getLanguageIdentifier(c||"plaintext")||w.NULL_LANGUAGE_IDENTIFIER;return this._getOrCreateMode(o.language),o}triggerMode(c){const o=this.getModeId(c);this._getOrCreateMode(o||"plaintext")}_getOrCreateMode(c){if(!this._instantiatedModes.hasOwnProperty(c)){let o=this.getLanguageIdentifier(c)||w.NULL_LANGUAGE_IDENTIFIER;this._instantiatedModes[c]=new M.FrankensteinMode(o),this._onDidCreateMode.fire(this._instantiatedModes[c])}return this._instantiatedModes[c]}}e.ModeServiceImpl=g}),define(Q[46],J([0,1,33,9,95]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getDefaultValues=e.getConfigurationKeys=e.getConfigurationValue=e.removeFromValueTree=e.addToValueTree=e.toValuesTree=e.IConfigurationService=void 0,e.IConfigurationService=N.createDecorator("configurationService");function w(o,s){const a=Object.create(null);for(let u in o)S(a,u,o[u],s);return a}e.toValuesTree=w;function S(o,s,a,u){const r=s.split("."),i=r.pop();let n=o;for(let t=0;tconsole.error(`Conflict in default settings: ${r}`))}return o}e.getDefaultValues=c}),define(Q[568],J([0,1,2,65,6,16,46]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AccessibilityService=void 0;let C=class extends b.Disposable{constructor(g,p){super();this._contextKeyService=g,this._configurationService=p,this._accessibilitySupport=0,this._onDidChangeScreenReaderOptimized=new M.Emitter,this._accessibilityModeEnabledContext=N.CONTEXT_ACCESSIBILITY_MODE_ENABLED.bindTo(this._contextKeyService);const c=()=>this._accessibilityModeEnabledContext.set(this.isScreenReaderOptimized());this._register(this._configurationService.onDidChangeConfiguration(o=>{o.affectsConfiguration("editor.accessibilitySupport")&&(c(),this._onDidChangeScreenReaderOptimized.fire())})),c(),this.onDidChangeScreenReaderOptimized(()=>c())}get onDidChangeScreenReaderOptimized(){return this._onDidChangeScreenReaderOptimized.event}isScreenReaderOptimized(){const g=this._configurationService.getValue("editor.accessibilitySupport");return g==="on"||g==="auto"&&this._accessibilitySupport===2}getAccessibilitySupport(){return this._accessibilitySupport}};C=Me([_e(0,w.IContextKeyService),_e(1,S.IConfigurationService)],C),e.AccessibilityService=C}),define(Q[569],J([0,1,51,19,20,40,24,95,46]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ConfigurationChangeEvent=e.Configuration=e.DefaultConfigurationModel=e.ConfigurationModel=void 0;class g{constructor(a={},u=[],r=[]){this._contents=a,this._keys=u,this._overrides=r,this.isFrozen=!1}get contents(){return this.checkAndFreeze(this._contents)}get overrides(){return this.checkAndFreeze(this._overrides)}get keys(){return this.checkAndFreeze(this._keys)}isEmpty(){return this._keys.length===0&&Object.keys(this._contents).length===0&&this._overrides.length===0}getValue(a){return a?d.getConfigurationValue(this.contents,a):this.contents}override(a){const u=this.getContentsForOverrideIdentifer(a);if(!u||typeof u!="object"||!Object.keys(u).length)return this;let r={};for(const i of N.distinct([...Object.keys(this.contents),...Object.keys(u)])){let n=this.contents[i],t=u[i];t&&(typeof n=="object"&&typeof t=="object"?(n=w.deepClone(n),this.mergeContents(n,t)):n=t),r[i]=n}return new g(r,this.keys,this.overrides)}merge(...a){const u=w.deepClone(this.contents),r=w.deepClone(this.overrides),i=[...this.keys];for(const n of a){this.mergeContents(u,n.contents);for(const t of n.overrides){const[l]=r.filter(h=>N.equals(h.identifiers,t.identifiers));l?this.mergeContents(l.contents,t.contents):r.push(w.deepClone(t))}for(const t of n.keys)i.indexOf(t)===-1&&i.push(t)}return new g(u,i,r)}freeze(){return this.isFrozen=!0,this}mergeContents(a,u){for(const r of Object.keys(u)){if(r in a&&M.isObject(a[r])&&M.isObject(u[r])){this.mergeContents(a[r],u[r]);continue}a[r]=w.deepClone(u[r])}}checkAndFreeze(a){return this.isFrozen&&!Object.isFrozen(a)?w.deepFreeze(a):a}getContentsForOverrideIdentifer(a){for(const u of this.overrides)if(u.identifiers.indexOf(a)!==-1)return u.contents;return null}toJSON(){return{contents:this.contents,overrides:this.overrides,keys:this.keys}}setValue(a,u){this.addKey(a),d.addToValueTree(this.contents,a,u,r=>{throw new Error(r)})}removeValue(a){this.removeKey(a)&&d.removeFromValueTree(this.contents,a)}addKey(a){let u=this.keys.length;for(let r=0;rconsole.error(`Conflict in default settings file: ${n}`))});super(a,u,r)}}e.DefaultConfigurationModel=p;class c{constructor(a,u,r=new g,i=new g,n=new b.ResourceMap,t=new g,l=new b.ResourceMap,h=!0){this._defaultConfiguration=a,this._localUserConfiguration=u,this._remoteUserConfiguration=r,this._workspaceConfiguration=i,this._folderConfigurations=n,this._memoryConfiguration=t,this._memoryConfigurationByResource=l,this._freeze=h,this._workspaceConsolidatedConfiguration=null,this._foldersConsolidatedConfigurations=new b.ResourceMap,this._userConfiguration=null}getValue(a,u,r){return this.getConsolidateConfigurationModel(u,r).getValue(a)}updateValue(a,u,r={}){let i;r.resource?(i=this._memoryConfigurationByResource.get(r.resource),i||(i=new g,this._memoryConfigurationByResource.set(r.resource,i))):i=this._memoryConfiguration,u===void 0?i.removeValue(a):i.setValue(a,u),r.resource||(this._workspaceConsolidatedConfiguration=null)}get userConfiguration(){return this._userConfiguration||(this._userConfiguration=this._remoteUserConfiguration.isEmpty()?this._localUserConfiguration:this._localUserConfiguration.merge(this._remoteUserConfiguration),this._freeze&&this._userConfiguration.freeze()),this._userConfiguration}getConsolidateConfigurationModel(a,u){let r=this.getConsolidatedConfigurationModelForResource(a,u);return a.overrideIdentifier?r.override(a.overrideIdentifier):r}getConsolidatedConfigurationModelForResource({resource:a},u){let r=this.getWorkspaceConsolidatedConfiguration();if(u&&a){const i=u.getFolder(a);i&&(r=this.getFolderConsolidatedConfiguration(i.uri)||r);const n=this._memoryConfigurationByResource.get(a);n&&(r=r.merge(n))}return r}getWorkspaceConsolidatedConfiguration(){return this._workspaceConsolidatedConfiguration||(this._workspaceConsolidatedConfiguration=this._defaultConfiguration.merge(this.userConfiguration,this._workspaceConfiguration,this._memoryConfiguration),this._freeze&&(this._workspaceConfiguration=this._workspaceConfiguration.freeze())),this._workspaceConsolidatedConfiguration}getFolderConsolidatedConfiguration(a){let u=this._foldersConsolidatedConfigurations.get(a);if(!u){const r=this.getWorkspaceConsolidatedConfiguration(),i=this._folderConfigurations.get(a);i?(u=r.merge(i),this._freeze&&(u=u.freeze()),this._foldersConsolidatedConfigurations.set(a,u)):u=r}return u}toData(){return{defaults:{contents:this._defaultConfiguration.contents,overrides:this._defaultConfiguration.overrides,keys:this._defaultConfiguration.keys},user:{contents:this.userConfiguration.contents,overrides:this.userConfiguration.overrides,keys:this.userConfiguration.keys},workspace:{contents:this._workspaceConfiguration.contents,overrides:this._workspaceConfiguration.overrides,keys:this._workspaceConfiguration.keys},folders:[...this._folderConfigurations.keys()].reduce((a,u)=>{const{contents:r,overrides:i,keys:n}=this._folderConfigurations.get(u);return a.push([u,{contents:r,overrides:i,keys:n}]),a},[])}}static parse(a){const u=this.parseConfigurationModel(a.defaults),r=this.parseConfigurationModel(a.user),i=this.parseConfigurationModel(a.workspace),n=a.folders.reduce((t,l)=>(t.set(S.URI.revive(l[0]),this.parseConfigurationModel(l[1])),t),new b.ResourceMap);return new c(u,r,new g,i,n,new g,new b.ResourceMap,!1)}static parseConfigurationModel(a){return new g(a.contents,a.keys,a.overrides).freeze()}}e.Configuration=c;class o{constructor(a,u,r,i){this.change=a,this.previous=u,this.currentConfiguraiton=r,this.currentWorkspace=i,this._previousConfiguration=void 0;const n=new Set;a.keys.forEach(l=>n.add(l)),a.overrides.forEach(([,l])=>l.forEach(h=>n.add(h))),this.affectedKeys=[...n.values()];const t=new g;this.affectedKeys.forEach(l=>t.setValue(l,{})),this.affectedKeysTree=t.contents}get previousConfiguration(){return!this._previousConfiguration&&this.previous&&(this._previousConfiguration=c.parse(this.previous.data)),this._previousConfiguration}affectsConfiguration(a,u){var r;if(this.doesAffectedKeysTreeContains(this.affectedKeysTree,a)){if(u){const i=this.previousConfiguration?this.previousConfiguration.getValue(a,u,(r=this.previous)===null||r===void 0?void 0:r.workspace):void 0,n=this.currentConfiguraiton.getValue(a,u,this.currentWorkspace);return!w.equals(i,n)}return!0}return!1}doesAffectedKeysTreeContains(a,u){let r=d.toValuesTree({[u]:!0},()=>{}),i;for(;typeof r=="object"&&(i=Object.keys(r)[0]);){if(a=a[i],!a)return!1;r=r[i]}return!0}}e.ConfigurationChangeEvent=o}),define(Q[570],J([0,1,6,54,2,51,520,26,46,16,251]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ContextKeyService=e.AbstractContextKeyService=e.Context=void 0;const c="data-keybinding-context";class o{constructor(f,v){this._id=f,this._parent=v,this._value=Object.create(null),this._value._contextId=f}setValue(f,v){return this._value[f]!==v?(this._value[f]=v,!0):!1}removeValue(f){return f in this._value?(delete this._value[f],!0):!1}getValue(f){const v=this._value[f];return typeof v=="undefined"&&this._parent?this._parent.getValue(f):v}}e.Context=o;class s extends o{constructor(){super(-1,null)}setValue(f,v){return!1}removeValue(f){return!1}getValue(f){}}s.INSTANCE=new s;class a extends o{constructor(f,v,y){super(f,null);this._configurationService=v,this._values=w.TernarySearchTree.forConfigKeys(),this._listener=this._configurationService.onDidChangeConfiguration(L=>{if(L.source===6){const I=Array.from(N.Iterable.map(this._values,([k])=>k));this._values.clear(),y.fire(new i(I))}else{const I=[];for(const k of L.affectedKeys){const E=`config.${k}`,T=this._values.findSuperstr(E);T!==void 0&&(I.push(...N.Iterable.map(T,([O])=>O)),this._values.deleteSuperstr(E)),this._values.has(E)&&(I.push(E),this._values.delete(E))}y.fire(new i(I))}})}dispose(){this._listener.dispose()}getValue(f){if(f.indexOf(a._keyPrefix)!==0)return super.getValue(f);if(this._values.has(f))return this._values.get(f);const v=f.substr(a._keyPrefix.length),y=this._configurationService.getValue(v);let L;switch(typeof y){case"number":case"boolean":case"string":L=y;break;default:Array.isArray(y)?L=JSON.stringify(y):L=y}return this._values.set(f,L),L}setValue(f,v){return super.setValue(f,v)}removeValue(f){return super.removeValue(f)}}a._keyPrefix="config.";class u{constructor(f,v,y){this._service=f,this._key=v,this._defaultValue=y,this.reset()}set(f){this._service.setContext(this._key,f)}reset(){typeof this._defaultValue=="undefined"?this._service.removeContext(this._key):this._service.setContext(this._key,this._defaultValue)}get(){return this._service.getContextKeyValue(this._key)}}class r{constructor(f){this.key=f}affectsSome(f){return f.has(this.key)}}class i{constructor(f){this.keys=f}affectsSome(f){for(const v of this.keys)if(f.has(v))return!0;return!1}}class n{constructor(f){this.events=f}affectsSome(f){for(const v of this.events)if(v.affectsSome(f))return!0;return!1}}class t{constructor(f){this._onDidChangeContext=new b.PauseableEmitter({merge:v=>new n(v)}),this.onDidChangeContext=this._onDidChangeContext.event,this._isDisposed=!1,this._myContextId=f}createKey(f,v){if(this._isDisposed)throw new Error("AbstractContextKeyService has been disposed");return new u(this,f,v)}bufferChangeEvents(f){this._onDidChangeContext.pause();try{f()}finally{this._onDidChangeContext.resume()}}createScoped(f){if(this._isDisposed)throw new Error("AbstractContextKeyService has been disposed");return new h(this,f)}contextMatchesRules(f){if(this._isDisposed)throw new Error("AbstractContextKeyService has been disposed");const v=this.getContextValuesContainer(this._myContextId);return p.KeybindingResolver.contextMatchesRules(v,f)}getContextKeyValue(f){if(!this._isDisposed)return this.getContextValuesContainer(this._myContextId).getValue(f)}setContext(f,v){if(!this._isDisposed){const y=this.getContextValuesContainer(this._myContextId);!y||y.setValue(f,v)&&this._onDidChangeContext.fire(new r(f))}}removeContext(f){this._isDisposed||this.getContextValuesContainer(this._myContextId).removeValue(f)&&this._onDidChangeContext.fire(new r(f))}getContext(f){return this._isDisposed?s.INSTANCE:this.getContextValuesContainer(m(f))}}e.AbstractContextKeyService=t;let l=class extends t{constructor(f){super(0);this._contexts=new Map,this._toDispose=new M.DisposableStore,this._lastContextId=0;const v=new a(this._myContextId,f,this._onDidChangeContext);this._contexts.set(this._myContextId,v),this._toDispose.add(v)}dispose(){this._onDidChangeContext.dispose(),this._isDisposed=!0,this._toDispose.dispose()}getContextValuesContainer(f){return this._isDisposed?s.INSTANCE:this._contexts.get(f)||s.INSTANCE}createChildContext(f=this._myContextId){if(this._isDisposed)throw new Error("ContextKeyService has been disposed");let v=++this._lastContextId;return this._contexts.set(v,new o(v,this.getContextValuesContainer(f))),v}disposeContext(f){this._isDisposed||this._contexts.delete(f)}};l=Me([_e(0,d.IConfigurationService)],l),e.ContextKeyService=l;class h extends t{constructor(f,v){super(f.createChildContext());if(this._parentChangeListener=new M.MutableDisposable,this._parent=f,this._updateParentChangeListener(),this._domNode=v,this._domNode.hasAttribute(c)){let y="";this._domNode.classList&&(y=Array.from(this._domNode.classList.values()).join(", ")),console.error(`Element already has context attribute${y?": "+y:""}`)}this._domNode.setAttribute(c,String(this._myContextId))}_updateParentChangeListener(){this._parentChangeListener.value=this._parent.onDidChangeContext(this._onDidChangeContext.fire,this._onDidChangeContext)}dispose(){this._isDisposed||(this._onDidChangeContext.dispose(),this._parent.disposeContext(this._myContextId),this._parentChangeListener.dispose(),this._domNode.removeAttribute(c),this._isDisposed=!0)}getContextValuesContainer(f){return this._isDisposed?s.INSTANCE:this._parent.getContextValuesContainer(f)}createChildContext(f=this._myContextId){if(this._isDisposed)throw new Error("ScopedContextKeyService has been disposed");return this._parent.createChildContext(f)}disposeContext(f){this._isDisposed||this._parent.disposeContext(f)}}function m(_){for(;_;){if(_.hasAttribute(c)){const f=_.getAttribute(c);return f?parseInt(f,10):NaN}_=_.parentElement}return 0}C.CommandsRegistry.registerCommand(g.SET_CONTEXT_COMMAND_ID,function(_,f,v){_.get(g.IContextKeyService).createKey(String(f),v)}),C.CommandsRegistry.registerCommand({id:"getContextKeyInfo",handler(){return[...g.RawContextKey.all()].sort((_,f)=>_.key.localeCompare(f.key))},description:{description:S.localize(0,null),args:[]}}),C.CommandsRegistry.registerCommand("_generateContextKeyInfo",function(){const _=[],f=new Set;for(let v of g.RawContextKey.all())f.has(v.key)||(f.add(v.key),_.push(v));_.sort((v,y)=>v.key.localeCompare(y.key)),console.log(JSON.stringify(_,void 0,2))})}),define(Q[86],J([0,1,39,17,26,33]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Extensions=e.KeybindingsRegistry=void 0;class S{constructor(){this._coreKeybindings=[],this._extensionKeybindings=[],this._cachedMergedKeybindings=null}static bindToCurrentPlatform(g){if(N.OS===1){if(g&&g.win)return g.win}else if(N.OS===2){if(g&&g.mac)return g.mac}else if(g&&g.linux)return g.linux;return g}registerKeybindingRule(g){const p=S.bindToCurrentPlatform(g);if(p&&p.primary){const c=b.createKeybinding(p.primary,N.OS);c&&this._registerDefaultKeybinding(c,g.id,g.args,g.weight,0,g.when)}if(p&&Array.isArray(p.secondary))for(let c=0,o=p.secondary.length;c=21&&g<=30||g>=31&&g<=56?!0:g===80||g===81||g===82||g===83||g===84||g===85||g===86||g===110||g===111||g===87||g===88||g===89||g===90||g===91||g===92}_assertNoCtrlAlt(g,p){g.ctrlKey&&g.altKey&&!g.metaKey&&S._mightProduceChar(g.keyCode)&&console.warn("Ctrl+Alt+ keybindings should not be used by default under Windows. Offender: ",g," for ",p)}_registerDefaultKeybinding(g,p,c,o,s,a){N.OS===1&&this._assertNoCtrlAlt(g.parts[0],p),this._coreKeybindings.push({keybinding:g,command:p,commandArgs:c,when:a,weight1:o,weight2:s,extensionId:null,isBuiltinExtension:!1}),this._cachedMergedKeybindings=null}getDefaultKeybindings(){return this._cachedMergedKeybindings||(this._cachedMergedKeybindings=[].concat(this._coreKeybindings).concat(this._extensionKeybindings),this._cachedMergedKeybindings.sort(C)),this._cachedMergedKeybindings.slice(0)}}e.KeybindingsRegistry=new S,e.Extensions={EditorModes:"platform.keybindingsRegistry"},w.Registry.add(e.Extensions.EditorModes,e.KeybindingsRegistry);function C(d,g){return d.weight1!==g.weight1?d.weight1-g.weight1:d.commandg.command?1:d.weight2-g.weight2}}),define(Q[571],J([0,1,16,432,86,433]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ContextScopedReplaceInput=e.ContextScopedFindInput=e.createAndBindHistoryNavigationWidgetScopedContextKeyService=e.HistoryNavigationEnablementContext=e.HistoryNavigationWidgetContext=void 0,e.HistoryNavigationWidgetContext="historyNavigationWidget",e.HistoryNavigationEnablementContext="historyNavigationEnabled";function S(o,s,a){new b.RawContextKey(a,s).bindTo(o)}function C(o,s){return o.createScoped(s.target)}function d(o,s){return o.getContext(document.activeElement).getValue(s)}function g(o,s){const a=C(o,s);S(a,s,e.HistoryNavigationWidgetContext);const u=new b.RawContextKey(e.HistoryNavigationEnablementContext,!0).bindTo(a);return{scopedContextKeyService:a,historyNavigationEnablement:u}}e.createAndBindHistoryNavigationWidgetScopedContextKeyService=g;let p=class extends N.FindInput{constructor(s,a,u,r,i=!1){super(s,a,i,u);this._register(g(r,{target:this.inputBox.element,historyNavigator:this.inputBox}).scopedContextKeyService)}};p=Me([_e(3,b.IContextKeyService)],p),e.ContextScopedFindInput=p;let c=class extends w.ReplaceInput{constructor(s,a,u,r,i=!1){super(s,a,i,u);this._register(g(r,{target:this.inputBox.element,historyNavigator:this.inputBox}).scopedContextKeyService)}};c=Me([_e(3,b.IContextKeyService)],c),e.ContextScopedReplaceInput=c,M.KeybindingsRegistry.registerCommandAndKeybindingRule({id:"history.showPrevious",weight:200,when:b.ContextKeyExpr.and(b.ContextKeyExpr.has(e.HistoryNavigationWidgetContext),b.ContextKeyExpr.equals(e.HistoryNavigationEnablementContext,!0)),primary:16,secondary:[512|16],handler:(o,s)=>{const a=d(o.get(b.IContextKeyService),e.HistoryNavigationWidgetContext);a&&a.historyNavigator.showPreviousValue()}}),M.KeybindingsRegistry.registerCommandAndKeybindingRule({id:"history.showNext",weight:200,when:b.ContextKeyExpr.and(b.ContextKeyExpr.has(e.HistoryNavigationWidgetContext),b.ContextKeyExpr.equals(e.HistoryNavigationEnablementContext,!0)),primary:18,secondary:[512|18],handler:(o,s)=>{const a=d(o.get(b.IContextKeyService),e.HistoryNavigationWidgetContext);a&&a.historyNavigator.showNextValue()}})}),define(Q[96],J([0,1,33,19,2]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.QuickAccessRegistry=e.Extensions=e.DefaultQuickAccessFilterValue=void 0;var w;(function(C){C[C.PRESERVE=0]="PRESERVE",C[C.LAST=1]="LAST"})(w=e.DefaultQuickAccessFilterValue||(e.DefaultQuickAccessFilterValue={})),e.Extensions={Quickaccess:"workbench.contributions.quickaccess"};class S{constructor(){this.providers=[],this.defaultProvider=void 0}registerQuickAccessProvider(d){return d.prefix.length===0?this.defaultProvider=d:this.providers.push(d),this.providers.sort((g,p)=>p.prefix.length-g.prefix.length),M.toDisposable(()=>{this.providers.splice(this.providers.indexOf(d),1),this.defaultProvider===d&&(this.defaultProvider=void 0)})}getQuickAccessProviders(){return N.coalesce([this.defaultProvider,...this.providers])}getQuickAccessProvider(d){return d&&this.providers.find(p=>d.startsWith(p.prefix))||void 0||this.defaultProvider}}e.QuickAccessRegistry=S,b.Registry.add(e.Extensions.Quickaccess,new S)}),define(Q[572],J([0,1,78,96,33,526,2]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.HelpQuickAccessProvider=void 0;let C=class lt{constructor(g){this.quickInputService=g,this.registry=M.Registry.as(N.Extensions.Quickaccess)}provide(g){const p=new S.DisposableStore;p.add(g.onDidAccept(()=>{const[s]=g.selectedItems;s&&this.quickInputService.quickAccess.show(s.prefix,{preserveValue:!0})})),p.add(g.onDidChangeValue(s=>{const a=this.registry.getQuickAccessProvider(s.substr(lt.PREFIX.length));a&&a.prefix&&a.prefix!==lt.PREFIX&&this.quickInputService.quickAccess.show(a.prefix,{preserveValue:!0})}));const{editorProviders:c,globalProviders:o}=this.getQuickAccessProviders();return g.items=c.length===0||o.length===0?[...c.length===0?o:c]:[{label:w.localize(0,null),type:"separator"},...o,{label:w.localize(1,null),type:"separator"},...c],p}getQuickAccessProviders(){const g=[],p=[];for(const c of this.registry.getQuickAccessProviders().sort((o,s)=>o.prefix.localeCompare(s.prefix)))if(c.prefix!==lt.PREFIX)for(const o of c.helpEntries){const s=o.prefix||c.prefix,a=s||"\u2026";(o.needsEditor?p:g).push({prefix:s,label:a,ariaLabel:w.localize(2,null,a,o.description),description:o.description})}return{editorProviders:p,globalProviders:g}}};C.PREFIX="?",C=Me([_e(0,b.IQuickInputService)],C),e.HelpQuickAccessProvider=C}),define(Q[573],J([0,1,33,96,64,572]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),b.Registry.as(N.Extensions.Quickaccess).registerQuickAccessProvider({ctor:w.HelpQuickAccessProvider,prefix:"",helpEntries:[{description:M.QuickHelpNLS.helpQuickAccessActionLabel,needsEditor:!0}]})}),define(Q[574],J([0,1,78,2,96,33,23,9,88]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.QuickAccessController=void 0;let g=class extends N.Disposable{constructor(c,o){super();this.quickInputService=c,this.instantiationService=o,this.registry=w.Registry.as(M.Extensions.Quickaccess),this.mapProviderToDescriptor=new Map,this.lastAcceptedPickerValues=new Map,this.visibleQuickAccess=void 0}show(c="",o){var s;const[a,u]=this.getOrInstantiateProvider(c),r=this.visibleQuickAccess,i=r==null?void 0:r.descriptor;if(r&&u&&i===u){c!==u.prefix&&!(o==null?void 0:o.preserveValue)&&(r.picker.value=c),this.adjustValueSelection(r.picker,u,o);return}if(u&&!(o==null?void 0:o.preserveValue)){let h;if(r&&i&&i!==u){const m=r.value.substr(i.prefix.length);m&&(h=`${u.prefix}${m}`)}if(!h){const m=a==null?void 0:a.defaultFilterValue;m===M.DefaultQuickAccessFilterValue.LAST?h=this.lastAcceptedPickerValues.get(u):typeof m=="string"&&(h=`${u.prefix}${m}`)}typeof h=="string"&&(c=h)}const n=new N.DisposableStore,t=n.add(this.quickInputService.createQuickPick());t.value=c,this.adjustValueSelection(t,u,o),t.placeholder=u==null?void 0:u.placeholder,t.quickNavigate=o==null?void 0:o.quickNavigateConfiguration,t.hideInput=!!t.quickNavigate&&!r,(typeof(o==null?void 0:o.itemActivation)=="number"||(o==null?void 0:o.quickNavigateConfiguration))&&(t.itemActivation=(s=o==null?void 0:o.itemActivation)!==null&&s!==void 0?s:b.ItemActivation.SECOND),t.contextKey=u==null?void 0:u.contextKey,t.filterValue=h=>h.substring(u?u.prefix.length:0),(u==null?void 0:u.placeholder)&&(t.ariaLabel=u==null?void 0:u.placeholder);const l=this.registerPickerListeners(t,a,u,c,n);a&&n.add(a.provide(t,l)),t.show()}adjustValueSelection(c,o,s){var a;let u;(s==null?void 0:s.preserveValue)?u=[c.value.length,c.value.length]:u=[(a=o==null?void 0:o.prefix.length)!==null&&a!==void 0?a:0,c.value.length],c.valueSelection=u}registerPickerListeners(c,o,s,a,u){const r=this.visibleQuickAccess={picker:c,descriptor:s,value:a};u.add(N.toDisposable(()=>{r===this.visibleQuickAccess&&(this.visibleQuickAccess=void 0)})),u.add(c.onDidChangeValue(n=>{const[t]=this.getOrInstantiateProvider(n);t!==o?this.show(n,{preserveValue:!0}):r.value=n})),s&&u.add(c.onDidAccept(()=>{this.lastAcceptedPickerValues.set(s,c.value)}));const i=u.add(new S.CancellationTokenSource);return d.once(c.onDidHide)(()=>{c.selectedItems.length===0&&i.cancel(),u.dispose()}),i.token}getOrInstantiateProvider(c){const o=this.registry.getQuickAccessProvider(c);if(!o)return[void 0,void 0];let s=this.mapProviderToDescriptor.get(o);return s||(s=this.instantiationService.createInstance(o.ctor),this.mapProviderToDescriptor.set(o,s)),[s,o]}};g=Me([_e(0,b.IQuickInputService),_e(1,C.IInstantiationService)],g),e.QuickAccessController=g}),define(Q[79],J([0,1,9,6,2,20,293]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.InMemoryStorageService=e.AbstractStorageService=e.WillSaveStateReason=e.IStorageService=void 0;const C="__$__targetStorageMarker";e.IStorageService=b.createDecorator("storageService");var d;(function(c){c[c.NONE=0]="NONE",c[c.SHUTDOWN=1]="SHUTDOWN"})(d=e.WillSaveStateReason||(e.WillSaveStateReason={}));class g extends M.Disposable{constructor(o={flushInterval:g.DEFAULT_FLUSH_INTERVAL}){super();this.options=o,this._onDidChangeValue=this._register(new N.PauseableEmitter),this._onDidChangeTarget=this._register(new N.PauseableEmitter),this._onWillSaveState=this._register(new N.Emitter),this.onWillSaveState=this._onWillSaveState.event,this._workspaceKeyTargets=void 0,this._globalKeyTargets=void 0}emitDidChangeValue(o,s){s===C?(o===0?this._globalKeyTargets=void 0:o===1&&(this._workspaceKeyTargets=void 0),this._onDidChangeTarget.fire({scope:o})):this._onDidChangeValue.fire({scope:o,key:s,target:this.getKeyTargets(o)[s]})}get(o,s,a){var u;return(u=this.getStorage(s))===null||u===void 0?void 0:u.get(o,a)}getBoolean(o,s,a){var u;return(u=this.getStorage(s))===null||u===void 0?void 0:u.getBoolean(o,a)}getNumber(o,s,a){var u;return(u=this.getStorage(s))===null||u===void 0?void 0:u.getNumber(o,a)}store(o,s,a,u){if(w.isUndefinedOrNull(s)){this.remove(o,a);return}this.withPausedEmitters(()=>{var r;this.updateKeyTarget(o,a,u),(r=this.getStorage(a))===null||r===void 0||r.set(o,s)})}remove(o,s){this.withPausedEmitters(()=>{var a;this.updateKeyTarget(o,s,void 0),(a=this.getStorage(s))===null||a===void 0||a.delete(o)})}withPausedEmitters(o){this._onDidChangeValue.pause(),this._onDidChangeTarget.pause();try{o()}finally{this._onDidChangeValue.resume(),this._onDidChangeTarget.resume()}}updateKeyTarget(o,s,a){var u,r;const i=this.getKeyTargets(s);typeof a=="number"?i[o]!==a&&(i[o]=a,(u=this.getStorage(s))===null||u===void 0||u.set(C,JSON.stringify(i))):typeof i[o]=="number"&&(delete i[o],(r=this.getStorage(s))===null||r===void 0||r.set(C,JSON.stringify(i)))}get workspaceKeyTargets(){return this._workspaceKeyTargets||(this._workspaceKeyTargets=this.loadKeyTargets(1)),this._workspaceKeyTargets}get globalKeyTargets(){return this._globalKeyTargets||(this._globalKeyTargets=this.loadKeyTargets(0)),this._globalKeyTargets}getKeyTargets(o){return o===0?this.globalKeyTargets:this.workspaceKeyTargets}loadKeyTargets(o){const s=this.get(C,o);if(s)try{return JSON.parse(s)}catch(a){}return Object.create(null)}}e.AbstractStorageService=g,g.DEFAULT_FLUSH_INTERVAL=60*1e3;class p extends g{constructor(){super();this.globalStorage=new S.Storage(new S.InMemoryStorageDatabase),this.workspaceStorage=new S.Storage(new S.InMemoryStorageDatabase),this._register(this.workspaceStorage.onDidChangeStorage(o=>this.emitDidChangeValue(1,o))),this._register(this.globalStorage.onDidChangeStorage(o=>this.emitDidChangeValue(0,o)))}getStorage(o){return o===0?this.globalStorage:this.workspaceStorage}}e.InMemoryStorageService=p}),define(Q[575],J([0,1,9,74,248,51,79,3,15,88]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CodeLensCache=e.ICodeLensCache=void 0,e.ICodeLensCache=b.createDecorator("ICodeLensCache");class p{constructor(s,a){this.lineCount=s,this.data=a}}let c=class{constructor(s){this._fakeProvider=new class{provideCodeLenses(){throw new Error("not supported")}},this._cache=new w.LRUCache(20,.75);const a="codelens/cache";d.runWhenIdle(()=>s.remove(a,1));const u="codelens/cache2",r=s.get(u,1,"{}");this._deserialize(r),g.once(s.onWillSaveState)(i=>{i.reason===S.WillSaveStateReason.SHUTDOWN&&s.store(u,this._serialize(),1,1)})}put(s,a){const u=a.lenses.map(n=>{var t;return{range:n.symbol.range,command:n.symbol.command&&{id:"",title:(t=n.symbol.command)===null||t===void 0?void 0:t.title}}}),r=new M.CodeLensModel;r.add({lenses:u,dispose:()=>{}},this._fakeProvider);const i=new p(s.getLineCount(),r);this._cache.set(s.uri.toString(),i)}get(s){const a=this._cache.get(s.uri.toString());return a&&a.lineCount===s.getLineCount()?a.data:void 0}delete(s){this._cache.delete(s.uri.toString())}_serialize(){const s=Object.create(null);for(const[a,u]of this._cache){const r=new Set;for(const i of u.data.lenses)r.add(i.symbol.range.startLineNumber);s[a]={lineCount:u.lineCount,lines:[...r.values()]}}return JSON.stringify(s)}_deserialize(s){try{const a=JSON.parse(s);for(const u in a){const r=a[u],i=[];for(const t of r.lines)i.push({range:new C.Range(t,1,t,11)});const n=new M.CodeLensModel;n.add({lenses:i,dispose(){}},this._fakeProvider),this._cache.set(u,new p(r.lineCount,n))}}catch(a){}}};c=Me([_e(0,S.IStorageService)],c),e.CodeLensCache=c,N.registerSingleton(e.ICodeLensCache,c)}),define(Q[576],J([0,1,51,79,18,2,15,9,46,74,57]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ISuggestMemoryService=e.SuggestMemoryService=e.PrefixMemory=e.LRUMemory=e.NoMemory=e.Memory=void 0;class c{constructor(i){this.name=i}select(i,n,t){if(t.length===0)return 0;let l=t[0].score[0];for(let h=0;hf&&L.type===t[v].completion.kind&&L.insertText===t[v].completion.insertText&&(f=L.touch,_=v),t[v].completion.preselect&&m===-1)return m=v}return _!==-1?_:m!==-1?m:0}toJSON(){return this._cache.toJSON()}fromJSON(i){this._cache.clear();let n=0;for(const[t,l]of i)l.touch=n,l.type=typeof l.type=="number"?l.type:M.completionKindFromString(l.type),this._cache.set(t,l);this._seq=this._cache.size}}e.LRUMemory=s;class a extends c{constructor(){super("recentlyUsedByPrefix");this._trie=b.TernarySearchTree.forStrings(),this._seq=0}memorize(i,n,t){const{word:l}=i.getWordUntilPosition(n),h=`${i.getLanguageIdentifier().language}/${l}`;this._trie.set(h,{type:t.completion.kind,insertText:t.completion.insertText,touch:this._seq++})}select(i,n,t){let{word:l}=i.getWordUntilPosition(n);if(!l)return super.select(i,n,t);let h=`${i.getLanguageIdentifier().language}/${l}`,m=this._trie.get(h);if(m||(m=this._trie.findSubstr(h)),m)for(let _=0;_i.push([t,n])),i.sort((n,t)=>-(n[1].touch-t[1].touch)).forEach((n,t)=>n[1].touch=t),i.slice(0,200)}fromJSON(i){if(this._trie.clear(),i.length>0){this._seq=i[0][1].touch+1;for(const[n,t]of i)t.type=typeof t.type=="number"?t.type:M.completionKindFromString(t.type),this._trie.set(n,t)}}}e.PrefixMemory=a;let u=class dt{constructor(i,n,t){this._storageService=i,this._modeService=n,this._configService=t,this._disposables=new w.DisposableStore,this._persistSoon=new S.RunOnceScheduler(()=>this._saveState(),500),this._disposables.add(i.onWillSaveState(l=>{l.reason===N.WillSaveStateReason.SHUTDOWN&&this._saveState()}))}dispose(){this._disposables.dispose(),this._persistSoon.dispose()}memorize(i,n,t){this._withStrategy(i,n).memorize(i,n,t),this._persistSoon.schedule()}select(i,n,t){return this._withStrategy(i,n).select(i,n,t)}_withStrategy(i,n){var t,l;const h=this._configService.getValue("editor.suggestSelection",{overrideIdentifier:(t=this._modeService.getLanguageIdentifier(i.getLanguageIdAtPosition(n.lineNumber,n.column)))===null||t===void 0?void 0:t.language,resource:i.uri});if(((l=this._strategy)===null||l===void 0?void 0:l.name)!==h){this._saveState();const m=dt._strategyCtors.get(h)||o;this._strategy=new m;try{const f=this._configService.getValue("editor.suggest.shareSuggestSelections")?0:1,v=this._storageService.get(`${dt._storagePrefix}/${h}`,f);v&&this._strategy.fromJSON(JSON.parse(v))}catch(_){}}return this._strategy}_saveState(){if(this._strategy){const n=this._configService.getValue("editor.suggest.shareSuggestSelections")?0:1,t=JSON.stringify(this._strategy);this._storageService.store(`${dt._storagePrefix}/${this._strategy.name}`,t,n,1)}}};u._strategyCtors=new Map([["recentlyUsedByPrefix",a],["recentlyUsed",s],["first",o]]),u._storagePrefix="suggest/memories",u=Me([_e(0,N.IStorageService),_e(1,p.IModeService),_e(2,d.IConfigurationService)],u),e.SuggestMemoryService=u,e.ISuggestMemoryService=C.createDecorator("ISuggestMemories"),g.registerSingleton(e.ISuggestMemoryService,u,!0)}),define(Q[87],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ITelemetryService=void 0,e.ITelemetryService=b.createDecorator("telemetryService")}),define(Q[577],J([0,1,525,561,2,66,20,51,79,46,9,37,26,87,12,32,437]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CommandsHistory=e.AbstractCommandsQuickAccessProvider=void 0;let i=class ut extends N.PickerQuickAccessProvider{constructor(l,h,m,_,f,v){super(ut.PREFIX,l);this.options=l,this.instantiationService=h,this.keybindingService=m,this.commandService=_,this.telemetryService=f,this.notificationService=v,this.commandsHistory=this._register(this.instantiationService.createInstance(n))}getPicks(l,h,m){return Ie(this,void 0,void 0,function*(){const _=yield this.getCommandPicks(h,m);if(m.isCancellationRequested)return[];const f=[];for(const I of _){const k=S.withNullAsUndefined(ut.WORD_FILTER(l,I.label)),E=I.commandAlias?S.withNullAsUndefined(ut.WORD_FILTER(l,I.commandAlias)):void 0;k||E?(I.highlights={label:k,detail:this.options.showAlias?E:void 0},f.push(I)):l===I.commandId&&f.push(I)}const v=new Map;for(const I of f){const k=v.get(I.label);k?(I.description=I.commandId,k.description=k.commandId):v.set(I.label,I)}f.sort((I,k)=>{const E=this.commandsHistory.peek(I.commandId),T=this.commandsHistory.peek(k.commandId);return E&&T?E>T?-1:1:E?-1:T?1:I.label.localeCompare(k.label)});const y=[];let L=!1;for(let I=0;IIe(this,void 0,void 0,function*(){this.commandsHistory.push(k.commandId),this.telemetryService.publicLog2("workbenchActionExecuted",{id:k.commandId,from:"quick open"});try{yield this.commandService.executeCommand(k.commandId)}catch(O){a.isPromiseCanceledError(O)||this.notificationService.error(b.localize(3,null,k.label,r.toErrorMessage(O)))}})}))}return y})}};i.PREFIX=">",i.WORD_FILTER=w.or(w.matchesPrefix,w.matchesWords,w.matchesContiguousSubString),i=Me([_e(1,p.IInstantiationService),_e(2,c.IKeybindingService),_e(3,o.ICommandService),_e(4,s.ITelemetryService),_e(5,u.INotificationService)],i),e.AbstractCommandsQuickAccessProvider=i;let n=class qe extends M.Disposable{constructor(l,h){super();this.storageService=l,this.configurationService=h,this.configuredCommandsHistoryLength=0,this.updateConfiguration(),this.load(),this.registerListeners()}registerListeners(){this._register(this.configurationService.onDidChangeConfiguration(()=>this.updateConfiguration()))}updateConfiguration(){this.configuredCommandsHistoryLength=qe.getConfiguredCommandHistoryLength(this.configurationService),qe.cache&&qe.cache.limit!==this.configuredCommandsHistoryLength&&(qe.cache.limit=this.configuredCommandsHistoryLength,qe.saveState(this.storageService))}load(){const l=this.storageService.get(qe.PREF_KEY_CACHE,0);let h;if(l)try{h=JSON.parse(l)}catch(_){}const m=qe.cache=new C.LRUCache(this.configuredCommandsHistoryLength,1);if(h){let _;h.usesLRU?_=h.entries:_=h.entries.sort((f,v)=>f.value-v.value),_.forEach(f=>m.set(f.key,f.value))}qe.counter=this.storageService.getNumber(qe.PREF_KEY_COUNTER,0,qe.counter)}push(l){!qe.cache||(qe.cache.set(l,qe.counter++),qe.saveState(this.storageService))}peek(l){var h;return(h=qe.cache)===null||h===void 0?void 0:h.peek(l)}static saveState(l){if(!!qe.cache){const h={usesLRU:!0,entries:[]};qe.cache.forEach((m,_)=>h.entries.push({key:_,value:m})),l.store(qe.PREF_KEY_CACHE,JSON.stringify(h),0,0),l.store(qe.PREF_KEY_COUNTER,qe.counter,0,0)}}static getConfiguredCommandHistoryLength(l){var h,m;const f=(m=(h=l.getValue().workbench)===null||h===void 0?void 0:h.commandPalette)===null||m===void 0?void 0:m.history;return typeof f=="number"?f:qe.DEFAULT_COMMANDS_HISTORY_LENGTH}};n.DEFAULT_COMMANDS_HISTORY_LENGTH=50,n.PREF_KEY_CACHE="commandPalette.mru.cache",n.PREF_KEY_COUNTER="commandPalette.mru.counter",n.counter=1,n=Me([_e(0,d.IStorageService),_e(1,g.IConfigurationService)],n),e.CommandsHistory=n}),define(Q[578],J([0,1,577,102]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractEditorCommandsQuickAccessProvider=void 0;class M extends b.AbstractCommandsQuickAccessProvider{constructor(S,C,d,g,p,c){super(S,C,d,g,p,c)}getCodeEditorCommandPicks(){const S=this.activeTextEditorControl;if(!S)return[];const C=[];for(const d of S.getSupportedActions())C.push({commandId:d.id,commandAlias:d.alias,label:N.stripIcons(d.label)||d.id});return C}}e.AbstractEditorCommandsQuickAccessProvider=M}),define(Q[22],J([0,1,33,29,6,527,185,15]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.workbenchColorsSchemaId=e.resolveColorValue=e.oneOf=e.transparent=e.lighten=e.darken=e.problemsInfoIconForeground=e.problemsWarningIconForeground=e.problemsErrorIconForeground=e.minimapSliderActiveBackground=e.minimapSliderHoverBackground=e.minimapSliderBackground=e.minimapBackground=e.minimapWarning=e.minimapError=e.minimapSelection=e.minimapFindMatch=e.overviewRulerSelectionHighlightForeground=e.overviewRulerFindMatchForeground=e.snippetFinalTabstopHighlightBorder=e.snippetFinalTabstopHighlightBackground=e.snippetTabstopHighlightBorder=e.snippetTabstopHighlightBackground=e.menuSeparatorBackground=e.menuSelectionBorder=e.menuSelectionBackground=e.menuSelectionForeground=e.menuBackground=e.menuForeground=e.menuBorder=e.tableColumnsBorder=e.treeIndentGuidesStroke=e.listFilterWidgetNoMatchesOutline=e.listFilterWidgetOutline=e.listFilterWidgetBackground=e.listHighlightForeground=e.listDropBackground=e.listHoverForeground=e.listHoverBackground=e.listInactiveFocusOutline=e.listInactiveFocusBackground=e.listInactiveSelectionForeground=e.listInactiveSelectionBackground=e.listActiveSelectionForeground=e.listActiveSelectionBackground=e.listFocusOutline=e.listFocusForeground=e.listFocusBackground=e.diffDiagonalFill=e.diffBorder=e.diffRemovedOutline=e.diffInsertedOutline=e.diffRemoved=e.diffInserted=e.defaultRemoveColor=e.defaultInsertColor=e.editorLightBulbAutoFixForeground=e.editorLightBulbForeground=e.editorInlineHintBackground=e.editorInlineHintForeground=e.editorActiveLinkForeground=e.editorHoverStatusBarBackground=e.editorHoverBorder=e.editorHoverForeground=e.editorHoverBackground=e.editorHoverHighlight=e.editorFindRangeHighlightBorder=e.editorFindMatchHighlightBorder=e.editorFindMatchBorder=e.editorFindRangeHighlight=e.editorFindMatchHighlight=e.editorFindMatch=e.editorSelectionHighlightBorder=e.editorSelectionHighlight=e.editorInactiveSelection=e.editorSelectionForeground=e.editorSelectionBackground=e.pickerGroupBorder=e.pickerGroupForeground=e.quickInputListFocusBackground=e.quickInputTitleBackground=e.quickInputForeground=e.quickInputBackground=e.editorWidgetResizeBorder=e.editorWidgetBorder=e.editorWidgetForeground=e.editorWidgetBackground=e.editorForeground=e.editorBackground=e.editorHintBorder=e.editorHintForeground=e.editorInfoBorder=e.editorInfoForeground=e.editorInfoBackground=e.editorWarningBorder=e.editorWarningForeground=e.editorWarningBackground=e.editorErrorBorder=e.editorErrorForeground=e.editorErrorBackground=e.progressBarBackground=e.scrollbarSliderActiveBackground=e.scrollbarSliderHoverBackground=e.scrollbarSliderBackground=e.scrollbarShadow=e.badgeForeground=e.badgeBackground=e.buttonHoverBackground=e.buttonBackground=e.buttonForeground=e.selectForeground=e.selectBackground=e.inputValidationErrorBorder=e.inputValidationErrorForeground=e.inputValidationErrorBackground=e.inputValidationWarningBorder=e.inputValidationWarningForeground=e.inputValidationWarningBackground=e.inputValidationInfoBorder=e.inputValidationInfoForeground=e.inputValidationInfoBackground=e.inputActiveOptionForeground=e.inputActiveOptionBackground=e.inputActiveOptionBorder=e.inputBorder=e.inputForeground=e.inputBackground=e.widgetShadow=e.textCodeBlockBackground=e.textLinkForeground=e.activeContrastBorder=e.contrastBorder=e.focusBorder=e.iconForeground=e.errorForeground=e.foreground=e.registerColor=e.Extensions=void 0,e.Extensions={ColorContribution:"base.contributions.colors"};class d{constructor(){this._onDidChangeSchema=new M.Emitter,this.onDidChangeSchema=this._onDidChangeSchema.event,this.colorSchema={type:"object",properties:{}},this.colorReferenceSchema={type:"string",enum:[],enumDescriptions:[]},this.colorsById={}}registerColor(l,h,m,_=!1,f){let v={id:l,description:m,defaults:h,needsTransparency:_,deprecationMessage:f};this.colorsById[l]=v;let y={type:"string",description:m,format:"color-hex",defaultSnippets:[{body:"${1:#ff0000}"}]};return f&&(y.deprecationMessage=f),this.colorSchema.properties[l]=y,this.colorReferenceSchema.enum.push(l),this.colorReferenceSchema.enumDescriptions.push(m),this._onDidChangeSchema.fire(),l}resolveDefaultColor(l,h){const m=this.colorsById[l];if(m&&m.defaults){const _=m.defaults[h.type];return r(_,h)}}getColorSchema(){return this.colorSchema}toString(){let l=(h,m)=>{let _=h.indexOf(".")===-1?0:1,f=m.indexOf(".")===-1?0:1;return _!==f?_-f:h.localeCompare(m)};return Object.keys(this.colorsById).sort(l).map(h=>`- \`${h}\`: ${this.colorsById[h].description}`).join(` +`)}}const g=new d;b.Registry.add(e.Extensions.ColorContribution,g);function p(t,l,h,m,_){return g.registerColor(t,l,h,m,_)}e.registerColor=p,e.foreground=p("foreground",{dark:"#CCCCCC",light:"#616161",hc:"#FFFFFF"},w.localize(0,null)),e.errorForeground=p("errorForeground",{dark:"#F48771",light:"#A1260D",hc:"#F48771"},w.localize(1,null)),e.iconForeground=p("icon.foreground",{dark:"#C5C5C5",light:"#424242",hc:"#FFFFFF"},w.localize(2,null)),e.focusBorder=p("focusBorder",{dark:"#007FD4",light:"#0090F1",hc:"#F38518"},w.localize(3,null)),e.contrastBorder=p("contrastBorder",{light:null,dark:null,hc:"#6FC3DF"},w.localize(4,null)),e.activeContrastBorder=p("contrastActiveBorder",{light:null,dark:null,hc:e.focusBorder},w.localize(5,null)),e.textLinkForeground=p("textLink.foreground",{light:"#006AB1",dark:"#3794FF",hc:"#3794FF"},w.localize(6,null)),e.textCodeBlockBackground=p("textCodeBlock.background",{light:"#dcdcdc66",dark:"#0a0a0a66",hc:N.Color.black},w.localize(7,null)),e.widgetShadow=p("widget.shadow",{dark:s(N.Color.black,.36),light:s(N.Color.black,.16),hc:null},w.localize(8,null)),e.inputBackground=p("input.background",{dark:"#3C3C3C",light:N.Color.white,hc:N.Color.black},w.localize(9,null)),e.inputForeground=p("input.foreground",{dark:e.foreground,light:e.foreground,hc:e.foreground},w.localize(10,null)),e.inputBorder=p("input.border",{dark:null,light:null,hc:e.contrastBorder},w.localize(11,null)),e.inputActiveOptionBorder=p("inputOption.activeBorder",{dark:"#007ACC00",light:"#007ACC00",hc:e.contrastBorder},w.localize(12,null)),e.inputActiveOptionBackground=p("inputOption.activeBackground",{dark:s(e.focusBorder,.4),light:s(e.focusBorder,.2),hc:N.Color.transparent},w.localize(13,null)),e.inputActiveOptionForeground=p("inputOption.activeForeground",{dark:N.Color.white,light:N.Color.black,hc:null},w.localize(14,null)),e.inputValidationInfoBackground=p("inputValidation.infoBackground",{dark:"#063B49",light:"#D6ECF2",hc:N.Color.black},w.localize(15,null)),e.inputValidationInfoForeground=p("inputValidation.infoForeground",{dark:null,light:null,hc:null},w.localize(16,null)),e.inputValidationInfoBorder=p("inputValidation.infoBorder",{dark:"#007acc",light:"#007acc",hc:e.contrastBorder},w.localize(17,null)),e.inputValidationWarningBackground=p("inputValidation.warningBackground",{dark:"#352A05",light:"#F6F5D2",hc:N.Color.black},w.localize(18,null)),e.inputValidationWarningForeground=p("inputValidation.warningForeground",{dark:null,light:null,hc:null},w.localize(19,null)),e.inputValidationWarningBorder=p("inputValidation.warningBorder",{dark:"#B89500",light:"#B89500",hc:e.contrastBorder},w.localize(20,null)),e.inputValidationErrorBackground=p("inputValidation.errorBackground",{dark:"#5A1D1D",light:"#F2DEDE",hc:N.Color.black},w.localize(21,null)),e.inputValidationErrorForeground=p("inputValidation.errorForeground",{dark:null,light:null,hc:null},w.localize(22,null)),e.inputValidationErrorBorder=p("inputValidation.errorBorder",{dark:"#BE1100",light:"#BE1100",hc:e.contrastBorder},w.localize(23,null)),e.selectBackground=p("dropdown.background",{dark:"#3C3C3C",light:N.Color.white,hc:N.Color.black},w.localize(24,null)),e.selectForeground=p("dropdown.foreground",{dark:"#F0F0F0",light:null,hc:N.Color.white},w.localize(25,null)),e.buttonForeground=p("button.foreground",{dark:N.Color.white,light:N.Color.white,hc:N.Color.white},w.localize(26,null)),e.buttonBackground=p("button.background",{dark:"#0E639C",light:"#007ACC",hc:null},w.localize(27,null)),e.buttonHoverBackground=p("button.hoverBackground",{dark:o(e.buttonBackground,.2),light:c(e.buttonBackground,.2),hc:null},w.localize(28,null)),e.badgeBackground=p("badge.background",{dark:"#4D4D4D",light:"#C4C4C4",hc:N.Color.black},w.localize(29,null)),e.badgeForeground=p("badge.foreground",{dark:N.Color.white,light:"#333",hc:N.Color.white},w.localize(30,null)),e.scrollbarShadow=p("scrollbar.shadow",{dark:"#000000",light:"#DDDDDD",hc:null},w.localize(31,null)),e.scrollbarSliderBackground=p("scrollbarSlider.background",{dark:N.Color.fromHex("#797979").transparent(.4),light:N.Color.fromHex("#646464").transparent(.4),hc:s(e.contrastBorder,.6)},w.localize(32,null)),e.scrollbarSliderHoverBackground=p("scrollbarSlider.hoverBackground",{dark:N.Color.fromHex("#646464").transparent(.7),light:N.Color.fromHex("#646464").transparent(.7),hc:s(e.contrastBorder,.8)},w.localize(33,null)),e.scrollbarSliderActiveBackground=p("scrollbarSlider.activeBackground",{dark:N.Color.fromHex("#BFBFBF").transparent(.4),light:N.Color.fromHex("#000000").transparent(.6),hc:e.contrastBorder},w.localize(34,null)),e.progressBarBackground=p("progressBar.background",{dark:N.Color.fromHex("#0E70C0"),light:N.Color.fromHex("#0E70C0"),hc:e.contrastBorder},w.localize(35,null)),e.editorErrorBackground=p("editorError.background",{dark:null,light:null,hc:null},w.localize(36,null),!0),e.editorErrorForeground=p("editorError.foreground",{dark:"#F48771",light:"#E51400",hc:null},w.localize(37,null)),e.editorErrorBorder=p("editorError.border",{dark:null,light:null,hc:N.Color.fromHex("#E47777").transparent(.8)},w.localize(38,null)),e.editorWarningBackground=p("editorWarning.background",{dark:null,light:null,hc:null},w.localize(39,null),!0),e.editorWarningForeground=p("editorWarning.foreground",{dark:"#CCA700",light:"#BF8803",hc:null},w.localize(40,null)),e.editorWarningBorder=p("editorWarning.border",{dark:null,light:null,hc:N.Color.fromHex("#FFCC00").transparent(.8)},w.localize(41,null)),e.editorInfoBackground=p("editorInfo.background",{dark:null,light:null,hc:null},w.localize(42,null),!0),e.editorInfoForeground=p("editorInfo.foreground",{dark:"#75BEFF",light:"#75BEFF",hc:null},w.localize(43,null)),e.editorInfoBorder=p("editorInfo.border",{dark:null,light:null,hc:N.Color.fromHex("#75BEFF").transparent(.8)},w.localize(44,null)),e.editorHintForeground=p("editorHint.foreground",{dark:N.Color.fromHex("#eeeeee").transparent(.7),light:"#6c6c6c",hc:null},w.localize(45,null)),e.editorHintBorder=p("editorHint.border",{dark:null,light:null,hc:N.Color.fromHex("#eeeeee").transparent(.8)},w.localize(46,null)),e.editorBackground=p("editor.background",{light:"#fffffe",dark:"#1E1E1E",hc:N.Color.black},w.localize(47,null)),e.editorForeground=p("editor.foreground",{light:"#333333",dark:"#BBBBBB",hc:N.Color.white},w.localize(48,null)),e.editorWidgetBackground=p("editorWidget.background",{dark:"#252526",light:"#F3F3F3",hc:"#0C141F"},w.localize(49,null)),e.editorWidgetForeground=p("editorWidget.foreground",{dark:e.foreground,light:e.foreground,hc:e.foreground},w.localize(50,null)),e.editorWidgetBorder=p("editorWidget.border",{dark:"#454545",light:"#C8C8C8",hc:e.contrastBorder},w.localize(51,null)),e.editorWidgetResizeBorder=p("editorWidget.resizeBorder",{light:null,dark:null,hc:null},w.localize(52,null)),e.quickInputBackground=p("quickInput.background",{dark:e.editorWidgetBackground,light:e.editorWidgetBackground,hc:e.editorWidgetBackground},w.localize(53,null)),e.quickInputForeground=p("quickInput.foreground",{dark:e.editorWidgetForeground,light:e.editorWidgetForeground,hc:e.editorWidgetForeground},w.localize(54,null)),e.quickInputTitleBackground=p("quickInputTitle.background",{dark:new N.Color(new N.RGBA(255,255,255,.105)),light:new N.Color(new N.RGBA(0,0,0,.06)),hc:"#000000"},w.localize(55,null)),e.quickInputListFocusBackground=p("quickInput.list.focusBackground",{dark:"#062F4A",light:"#D6EBFF",hc:null},w.localize(56,null)),e.pickerGroupForeground=p("pickerGroup.foreground",{dark:"#3794FF",light:"#0066BF",hc:N.Color.white},w.localize(57,null)),e.pickerGroupBorder=p("pickerGroup.border",{dark:"#3F3F46",light:"#CCCEDB",hc:N.Color.white},w.localize(58,null)),e.editorSelectionBackground=p("editor.selectionBackground",{light:"#ADD6FF",dark:"#264F78",hc:"#f3f518"},w.localize(59,null)),e.editorSelectionForeground=p("editor.selectionForeground",{light:null,dark:null,hc:"#000000"},w.localize(60,null)),e.editorInactiveSelection=p("editor.inactiveSelectionBackground",{light:s(e.editorSelectionBackground,.5),dark:s(e.editorSelectionBackground,.5),hc:s(e.editorSelectionBackground,.5)},w.localize(61,null),!0),e.editorSelectionHighlight=p("editor.selectionHighlightBackground",{light:u(e.editorSelectionBackground,e.editorBackground,.3,.6),dark:u(e.editorSelectionBackground,e.editorBackground,.3,.6),hc:null},w.localize(62,null),!0),e.editorSelectionHighlightBorder=p("editor.selectionHighlightBorder",{light:null,dark:null,hc:e.activeContrastBorder},w.localize(63,null)),e.editorFindMatch=p("editor.findMatchBackground",{light:"#A8AC94",dark:"#515C6A",hc:null},w.localize(64,null)),e.editorFindMatchHighlight=p("editor.findMatchHighlightBackground",{light:"#EA5C0055",dark:"#EA5C0055",hc:null},w.localize(65,null),!0),e.editorFindRangeHighlight=p("editor.findRangeHighlightBackground",{dark:"#3a3d4166",light:"#b4b4b44d",hc:null},w.localize(66,null),!0),e.editorFindMatchBorder=p("editor.findMatchBorder",{light:null,dark:null,hc:e.activeContrastBorder},w.localize(67,null)),e.editorFindMatchHighlightBorder=p("editor.findMatchHighlightBorder",{light:null,dark:null,hc:e.activeContrastBorder},w.localize(68,null)),e.editorFindRangeHighlightBorder=p("editor.findRangeHighlightBorder",{dark:null,light:null,hc:s(e.activeContrastBorder,.4)},w.localize(69,null),!0),e.editorHoverHighlight=p("editor.hoverHighlightBackground",{light:"#ADD6FF26",dark:"#264f7840",hc:"#ADD6FF26"},w.localize(70,null),!0),e.editorHoverBackground=p("editorHoverWidget.background",{light:e.editorWidgetBackground,dark:e.editorWidgetBackground,hc:e.editorWidgetBackground},w.localize(71,null)),e.editorHoverForeground=p("editorHoverWidget.foreground",{light:e.editorWidgetForeground,dark:e.editorWidgetForeground,hc:e.editorWidgetForeground},w.localize(72,null)),e.editorHoverBorder=p("editorHoverWidget.border",{light:e.editorWidgetBorder,dark:e.editorWidgetBorder,hc:e.editorWidgetBorder},w.localize(73,null)),e.editorHoverStatusBarBackground=p("editorHoverWidget.statusBarBackground",{dark:o(e.editorHoverBackground,.2),light:c(e.editorHoverBackground,.05),hc:e.editorWidgetBackground},w.localize(74,null)),e.editorActiveLinkForeground=p("editorLink.activeForeground",{dark:"#4E94CE",light:N.Color.blue,hc:N.Color.cyan},w.localize(75,null)),e.editorInlineHintForeground=p("editorInlineHint.foreground",{dark:e.editorWidgetBackground,light:e.editorWidgetForeground,hc:e.editorWidgetBackground},w.localize(76,null)),e.editorInlineHintBackground=p("editorInlineHint.background",{dark:e.editorWidgetForeground,light:e.editorWidgetBackground,hc:e.editorWidgetForeground},w.localize(77,null)),e.editorLightBulbForeground=p("editorLightBulb.foreground",{dark:"#FFCC00",light:"#DDB100",hc:"#FFCC00"},w.localize(78,null)),e.editorLightBulbAutoFixForeground=p("editorLightBulbAutoFix.foreground",{dark:"#75BEFF",light:"#007ACC",hc:"#75BEFF"},w.localize(79,null)),e.defaultInsertColor=new N.Color(new N.RGBA(155,185,85,.2)),e.defaultRemoveColor=new N.Color(new N.RGBA(255,0,0,.2)),e.diffInserted=p("diffEditor.insertedTextBackground",{dark:e.defaultInsertColor,light:e.defaultInsertColor,hc:null},w.localize(80,null),!0),e.diffRemoved=p("diffEditor.removedTextBackground",{dark:e.defaultRemoveColor,light:e.defaultRemoveColor,hc:null},w.localize(81,null),!0),e.diffInsertedOutline=p("diffEditor.insertedTextBorder",{dark:null,light:null,hc:"#33ff2eff"},w.localize(82,null)),e.diffRemovedOutline=p("diffEditor.removedTextBorder",{dark:null,light:null,hc:"#FF008F"},w.localize(83,null)),e.diffBorder=p("diffEditor.border",{dark:null,light:null,hc:e.contrastBorder},w.localize(84,null)),e.diffDiagonalFill=p("diffEditor.diagonalFill",{dark:"#cccccc33",light:"#22222233",hc:null},w.localize(85,null)),e.listFocusBackground=p("list.focusBackground",{dark:null,light:null,hc:null},w.localize(86,null)),e.listFocusForeground=p("list.focusForeground",{dark:null,light:null,hc:null},w.localize(87,null)),e.listFocusOutline=p("list.focusOutline",{dark:e.focusBorder,light:e.focusBorder,hc:e.activeContrastBorder},w.localize(88,null)),e.listActiveSelectionBackground=p("list.activeSelectionBackground",{dark:"#094771",light:"#0060C0",hc:null},w.localize(89,null)),e.listActiveSelectionForeground=p("list.activeSelectionForeground",{dark:N.Color.white,light:N.Color.white,hc:null},w.localize(90,null)),e.listInactiveSelectionBackground=p("list.inactiveSelectionBackground",{dark:"#37373D",light:"#E4E6F1",hc:null},w.localize(91,null)),e.listInactiveSelectionForeground=p("list.inactiveSelectionForeground",{dark:null,light:null,hc:null},w.localize(92,null)),e.listInactiveFocusBackground=p("list.inactiveFocusBackground",{dark:null,light:null,hc:null},w.localize(93,null)),e.listInactiveFocusOutline=p("list.inactiveFocusOutline",{dark:null,light:null,hc:null},w.localize(94,null)),e.listHoverBackground=p("list.hoverBackground",{dark:"#2A2D2E",light:"#F0F0F0",hc:null},w.localize(95,null)),e.listHoverForeground=p("list.hoverForeground",{dark:null,light:null,hc:null},w.localize(96,null)),e.listDropBackground=p("list.dropBackground",{dark:e.listFocusBackground,light:e.listFocusBackground,hc:null},w.localize(97,null)),e.listHighlightForeground=p("list.highlightForeground",{dark:"#0097fb",light:"#0066BF",hc:e.focusBorder},w.localize(98,null)),e.listFilterWidgetBackground=p("listFilterWidget.background",{light:"#efc1ad",dark:"#653723",hc:N.Color.black},w.localize(99,null)),e.listFilterWidgetOutline=p("listFilterWidget.outline",{dark:N.Color.transparent,light:N.Color.transparent,hc:"#f38518"},w.localize(100,null)),e.listFilterWidgetNoMatchesOutline=p("listFilterWidget.noMatchesOutline",{dark:"#BE1100",light:"#BE1100",hc:e.contrastBorder},w.localize(101,null)),e.treeIndentGuidesStroke=p("tree.indentGuidesStroke",{dark:"#585858",light:"#a9a9a9",hc:"#a9a9a9"},w.localize(102,null)),e.tableColumnsBorder=p("tree.tableColumnsBorder",{dark:"#CCCCCC20",light:"#61616120",hc:null},w.localize(103,null)),e.menuBorder=p("menu.border",{dark:null,light:null,hc:e.contrastBorder},w.localize(104,null)),e.menuForeground=p("menu.foreground",{dark:e.selectForeground,light:e.foreground,hc:e.selectForeground},w.localize(105,null)),e.menuBackground=p("menu.background",{dark:e.selectBackground,light:e.selectBackground,hc:e.selectBackground},w.localize(106,null)),e.menuSelectionForeground=p("menu.selectionForeground",{dark:e.listActiveSelectionForeground,light:e.listActiveSelectionForeground,hc:e.listActiveSelectionForeground},w.localize(107,null)),e.menuSelectionBackground=p("menu.selectionBackground",{dark:e.listActiveSelectionBackground,light:e.listActiveSelectionBackground,hc:e.listActiveSelectionBackground},w.localize(108,null)),e.menuSelectionBorder=p("menu.selectionBorder",{dark:null,light:null,hc:e.activeContrastBorder},w.localize(109,null)),e.menuSeparatorBackground=p("menu.separatorBackground",{dark:"#BBBBBB",light:"#888888",hc:e.contrastBorder},w.localize(110,null)),e.snippetTabstopHighlightBackground=p("editor.snippetTabstopHighlightBackground",{dark:new N.Color(new N.RGBA(124,124,124,.3)),light:new N.Color(new N.RGBA(10,50,100,.2)),hc:new N.Color(new N.RGBA(124,124,124,.3))},w.localize(111,null)),e.snippetTabstopHighlightBorder=p("editor.snippetTabstopHighlightBorder",{dark:null,light:null,hc:null},w.localize(112,null)),e.snippetFinalTabstopHighlightBackground=p("editor.snippetFinalTabstopHighlightBackground",{dark:null,light:null,hc:null},w.localize(113,null)),e.snippetFinalTabstopHighlightBorder=p("editor.snippetFinalTabstopHighlightBorder",{dark:"#525252",light:new N.Color(new N.RGBA(10,50,100,.5)),hc:"#525252"},w.localize(114,null)),e.overviewRulerFindMatchForeground=p("editorOverviewRuler.findMatchForeground",{dark:"#d186167e",light:"#d186167e",hc:"#AB5A00"},w.localize(115,null),!0),e.overviewRulerSelectionHighlightForeground=p("editorOverviewRuler.selectionHighlightForeground",{dark:"#A0A0A0CC",light:"#A0A0A0CC",hc:"#A0A0A0CC"},w.localize(116,null),!0),e.minimapFindMatch=p("minimap.findMatchHighlight",{light:"#d18616",dark:"#d18616",hc:"#AB5A00"},w.localize(117,null),!0),e.minimapSelection=p("minimap.selectionHighlight",{light:"#ADD6FF",dark:"#264F78",hc:"#ffffff"},w.localize(118,null),!0),e.minimapError=p("minimap.errorHighlight",{dark:new N.Color(new N.RGBA(255,18,18,.7)),light:new N.Color(new N.RGBA(255,18,18,.7)),hc:new N.Color(new N.RGBA(255,50,50,1))},w.localize(119,null)),e.minimapWarning=p("minimap.warningHighlight",{dark:e.editorWarningForeground,light:e.editorWarningForeground,hc:e.editorWarningBorder},w.localize(120,null)),e.minimapBackground=p("minimap.background",{dark:null,light:null,hc:null},w.localize(121,null)),e.minimapSliderBackground=p("minimapSlider.background",{light:s(e.scrollbarSliderBackground,.5),dark:s(e.scrollbarSliderBackground,.5),hc:s(e.scrollbarSliderBackground,.5)},w.localize(122,null)),e.minimapSliderHoverBackground=p("minimapSlider.hoverBackground",{light:s(e.scrollbarSliderHoverBackground,.5),dark:s(e.scrollbarSliderHoverBackground,.5),hc:s(e.scrollbarSliderHoverBackground,.5)},w.localize(123,null)),e.minimapSliderActiveBackground=p("minimapSlider.activeBackground",{light:s(e.scrollbarSliderActiveBackground,.5),dark:s(e.scrollbarSliderActiveBackground,.5),hc:s(e.scrollbarSliderActiveBackground,.5)},w.localize(124,null)),e.problemsErrorIconForeground=p("problemsErrorIcon.foreground",{dark:e.editorErrorForeground,light:e.editorErrorForeground,hc:e.editorErrorForeground},w.localize(125,null)),e.problemsWarningIconForeground=p("problemsWarningIcon.foreground",{dark:e.editorWarningForeground,light:e.editorWarningForeground,hc:e.editorWarningForeground},w.localize(126,null)),e.problemsInfoIconForeground=p("problemsInfoIcon.foreground",{dark:e.editorInfoForeground,light:e.editorInfoForeground,hc:e.editorInfoForeground},w.localize(127,null));function c(t,l){return h=>{let m=r(t,h);if(m)return m.darken(l)}}e.darken=c;function o(t,l){return h=>{let m=r(t,h);if(m)return m.lighten(l)}}e.lighten=o;function s(t,l){return h=>{let m=r(t,h);if(m)return m.transparent(l)}}e.transparent=s;function a(...t){return l=>{for(let h of t){let m=r(h,l);if(m)return m}}}e.oneOf=a;function u(t,l,h,m){return _=>{let f=r(t,_);if(f){let v=r(l,_);return v?f.isDarkerThan(v)?N.Color.getLighterColor(f,v,h).transparent(m):N.Color.getDarkerColor(f,v,h).transparent(m):f.transparent(h*m)}}}function r(t,l){if(t!==null){if(typeof t=="string")return t[0]==="#"?N.Color.fromHex(t):l.getColor(t);if(t instanceof N.Color)return t;if(typeof t=="function")return t(l)}}e.resolveColorValue=r,e.workbenchColorsSchemaId="vscode://schemas/workbench-colors";let i=b.Registry.as(S.Extensions.JSONContribution);i.registerSchema(e.workbenchColorsSchemaId,g.getColorSchema());const n=new C.RunOnceScheduler(()=>i.notifySchemaChanged(e.workbenchColorsSchemaId),200);g.onDidChangeSchema(()=>{n.isScheduled()||n.schedule()})}),define(Q[116],J([0,1,22]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.attachMenuStyler=e.defaultMenuStyles=e.defaultListStyles=e.attachListStyler=e.attachBadgeStyler=e.attachStyler=e.computeStyles=void 0;function N(d,g){const p=Object.create(null);for(let c in g){const o=g[c];o&&(p[c]=b.resolveColorValue(o,d))}return p}e.computeStyles=N;function M(d,g,p){function c(o){const s=N(d.getColorTheme(),g);typeof p=="function"?p(s):p.style(s)}return c(d.getColorTheme()),d.onDidColorThemeChange(c)}e.attachStyler=M;function w(d,g,p){return M(g,{badgeBackground:p&&p.badgeBackground||b.badgeBackground,badgeForeground:p&&p.badgeForeground||b.badgeForeground,badgeBorder:b.contrastBorder},d)}e.attachBadgeStyler=w;function S(d,g,p){return M(g,Object.assign(Object.assign({},e.defaultListStyles),p||{}),d)}e.attachListStyler=S,e.defaultListStyles={listFocusBackground:b.listFocusBackground,listFocusForeground:b.listFocusForeground,listFocusOutline:b.listFocusOutline,listActiveSelectionBackground:b.listActiveSelectionBackground,listActiveSelectionForeground:b.listActiveSelectionForeground,listFocusAndSelectionBackground:b.listActiveSelectionBackground,listFocusAndSelectionForeground:b.listActiveSelectionForeground,listInactiveSelectionBackground:b.listInactiveSelectionBackground,listInactiveSelectionForeground:b.listInactiveSelectionForeground,listInactiveFocusBackground:b.listInactiveFocusBackground,listInactiveFocusOutline:b.listInactiveFocusOutline,listHoverBackground:b.listHoverBackground,listHoverForeground:b.listHoverForeground,listDropBackground:b.listDropBackground,listSelectionOutline:b.activeContrastBorder,listHoverOutline:b.activeContrastBorder,listFilterWidgetBackground:b.listFilterWidgetBackground,listFilterWidgetOutline:b.listFilterWidgetOutline,listFilterWidgetNoMatchesOutline:b.listFilterWidgetNoMatchesOutline,listMatchesShadow:b.widgetShadow,treeIndentGuidesStroke:b.treeIndentGuidesStroke,tableColumnsBorder:b.tableColumnsBorder},e.defaultMenuStyles={shadowColor:b.widgetShadow,borderColor:b.menuBorder,foregroundColor:b.menuForeground,backgroundColor:b.menuBackground,selectionForegroundColor:b.menuSelectionForeground,selectionBackgroundColor:b.menuSelectionBackground,selectionBorderColor:b.menuSelectionBorder,separatorColor:b.menuSeparatorBackground};function C(d,g,p){return M(g,Object.assign(Object.assign({},e.defaultMenuStyles),p),d)}e.attachMenuStyler=C}),define(Q[579],J([0,1,48,2,434,7,116,55,50,360]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ContextMenuHandler=void 0;class g{constructor(c,o,s,a,u){this.contextViewService=c,this.telemetryService=o,this.notificationService=s,this.keybindingService=a,this.themeService=u,this.focusToReturn=null,this.block=null,this.options={blockMouse:!0}}configure(c){this.options=c}showContextMenu(c){const o=c.getActions();if(!!o.length){this.focusToReturn=document.activeElement;let s,a=w.isHTMLElement(c.domForShadowRoot)?c.domForShadowRoot:void 0;this.contextViewService.showContextView({getAnchor:()=>c.getAnchor(),canRelayout:!1,anchorAlignment:c.anchorAlignment,anchorAxisAlignment:c.anchorAxisAlignment,render:u=>{let r=c.getMenuClassName?c.getMenuClassName():"";r&&(u.className+=" "+r),this.options.blockMouse&&(this.block=u.appendChild(w.$(".context-view-block")),this.block.style.position="fixed",this.block.style.cursor="initial",this.block.style.left="0",this.block.style.top="0",this.block.style.width="100%",this.block.style.height="100%",this.block.style.zIndex="-1",C.domEvent(this.block,w.EventType.MOUSE_DOWN)(t=>t.stopPropagation()));const i=new N.DisposableStore,n=c.actionRunner||new b.ActionRunner;return n.onBeforeRun(this.onActionRun,this,i),n.onDidRun(this.onDidActionRun,this,i),s=new M.Menu(u,o,{actionViewItemProvider:c.getActionViewItem,context:c.getActionsContext?c.getActionsContext():null,actionRunner:n,getKeyBinding:c.getKeyBinding?c.getKeyBinding:t=>this.keybindingService.lookupKeybinding(t.id)}),i.add(S.attachMenuStyler(s,this.themeService)),s.onDidCancel(()=>this.contextViewService.hideContextView(!0),null,i),s.onDidBlur(()=>this.contextViewService.hideContextView(!0),null,i),C.domEvent(window,w.EventType.BLUR)(()=>{this.contextViewService.hideContextView(!0)},null,i),C.domEvent(window,w.EventType.MOUSE_DOWN)(t=>{if(!t.defaultPrevented){let l=new d.StandardMouseEvent(t),h=l.target;if(!l.rightButton){for(;h;){if(h===u)return;h=h.parentElement}this.contextViewService.hideContextView(!0)}}},null,i),N.combinedDisposable(i,s)},focus:()=>{s&&s.focus(!!c.autoSelectFirstItem)},onHide:u=>{c.onHide&&c.onHide(!!u),this.block&&(this.block.remove(),this.block=null),this.focusToReturn&&this.focusToReturn.focus()}},a,!!a)}}onActionRun(c){this.telemetryService&&this.telemetryService.publicLog2("workbenchActionExecuted",{id:c.action.id,from:"contextMenu"}),this.contextViewService.hideContextView(!1),this.focusToReturn&&this.focusToReturn.focus()}onDidActionRun(c){c.error&&this.notificationService.error(c.error)}}e.ContextMenuHandler=g}),define(Q[97],J([0,1]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ColorScheme=void 0;var b;(function(N){N.DARK="dark",N.LIGHT="light",N.HIGH_CONTRAST="hc"})(b=e.ColorScheme||(e.ColorScheme={}))}),define(Q[255],J([0,1,35,30,17,384,110,171,129,97,38]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewLine=e.ViewLineOptions=e.DomReadingContext=void 0;const c=function(){return M.isNative?!0:!(M.isLinux||b.isFirefox||b.isSafari)}();let o=!0;class s{constructor(_,f){this._domNode=_,this._clientRectDeltaLeft=0,this._clientRectDeltaLeftRead=!1,this.endNode=f}get clientRectDeltaLeft(){return this._clientRectDeltaLeftRead||(this._clientRectDeltaLeftRead=!0,this._clientRectDeltaLeft=this._domNode.getBoundingClientRect().left),this._clientRectDeltaLeft}}e.DomReadingContext=s;class a{constructor(_,f){this.themeType=f;const v=_.options,y=v.get(38);this.renderWhitespace=v.get(83),this.renderControlCharacters=v.get(77),this.spaceWidth=y.spaceWidth,this.middotWidth=y.middotWidth,this.wsmiddotWidth=y.wsmiddotWidth,this.useMonospaceOptimizations=y.isMonospace&&!v.get(26),this.canUseHalfwidthRightwardsArrow=y.canUseHalfwidthRightwardsArrow,this.lineHeight=v.get(53),this.stopRenderingLineAfter=v.get(100),this.fontLigatures=v.get(39)}equals(_){return this.themeType===_.themeType&&this.renderWhitespace===_.renderWhitespace&&this.renderControlCharacters===_.renderControlCharacters&&this.spaceWidth===_.spaceWidth&&this.middotWidth===_.middotWidth&&this.wsmiddotWidth===_.wsmiddotWidth&&this.useMonospaceOptimizations===_.useMonospaceOptimizations&&this.canUseHalfwidthRightwardsArrow===_.canUseHalfwidthRightwardsArrow&&this.lineHeight===_.lineHeight&&this.stopRenderingLineAfter===_.stopRenderingLineAfter&&this.fontLigatures===_.fontLigatures}}e.ViewLineOptions=a;class u{constructor(_){this._options=_,this._isMaybeInvalid=!0,this._renderedViewLine=null}getDomNode(){return this._renderedViewLine&&this._renderedViewLine.domNode?this._renderedViewLine.domNode.domNode:null}setDomNode(_){if(this._renderedViewLine)this._renderedViewLine.domNode=N.createFastDomNode(_);else throw new Error("I have no rendered view line to set the dom node to...")}onContentChanged(){this._isMaybeInvalid=!0}onTokensChanged(){this._isMaybeInvalid=!0}onDecorationsChanged(){this._isMaybeInvalid=!0}onOptionsChanged(_){this._isMaybeInvalid=!0,this._options=_}onSelectionChanged(){return this._options.themeType===g.ColorScheme.HIGH_CONTRAST||this._options.renderWhitespace==="selection"?(this._isMaybeInvalid=!0,!0):!1}renderLine(_,f,v,y){if(this._isMaybeInvalid===!1)return!1;this._isMaybeInvalid=!1;const L=v.getViewLineRenderingData(_),I=this._options,k=C.LineDecoration.filter(L.inlineDecorations,_,L.minColumn,L.maxColumn);let E=null;if(I.themeType===g.ColorScheme.HIGH_CONTRAST||this._options.renderWhitespace==="selection"){const B=v.selections;for(const F of B)if(!(F.endLineNumber<_||F.startLineNumber>_)){const D=F.startLineNumber===_?F.startColumn:L.minColumn,R=F.endLineNumber===_?F.endColumn:L.maxColumn;D');const O=d.renderViewLine(T,y);y.appendASCIIString("");let A=null;return o&&c&&L.isBasicASCII&&I.useMonospaceOptimizations&&O.containsForeignElements===0&&L.content.length<300&&T.lineTokens.getCount()<100&&(A=new r(this._renderedViewLine?this._renderedViewLine.domNode:null,T,O.characterMapping)),A||(A=t(this._renderedViewLine?this._renderedViewLine.domNode:null,T,O.characterMapping,O.containsRTL,O.containsForeignElements)),this._renderedViewLine=A,!0}layoutLine(_,f){this._renderedViewLine&&this._renderedViewLine.domNode&&(this._renderedViewLine.domNode.setTop(f),this._renderedViewLine.domNode.setHeight(this._options.lineHeight))}getWidth(){return this._renderedViewLine?this._renderedViewLine.getWidth():0}getWidthIsFast(){return this._renderedViewLine?this._renderedViewLine.getWidthIsFast():!0}needsMonospaceFontCheck(){return this._renderedViewLine?this._renderedViewLine instanceof r:!1}monospaceAssumptionsAreValid(){return this._renderedViewLine&&this._renderedViewLine instanceof r?this._renderedViewLine.monospaceAssumptionsAreValid():o}onMonospaceAssumptionsInvalidated(){this._renderedViewLine&&this._renderedViewLine instanceof r&&(this._renderedViewLine=this._renderedViewLine.toSlowRenderedLine())}getVisibleRangesForRange(_,f,v){if(!this._renderedViewLine)return null;_=_|0,f=f|0,_=Math.min(this._renderedViewLine.input.lineContent.length+1,Math.max(1,_)),f=Math.min(this._renderedViewLine.input.lineContent.length+1,Math.max(1,f));const y=this._renderedViewLine.input.stopRenderingLineAfter|0;let L=!1;y!==-1&&_>y+1&&f>y+1&&(L=!0),y!==-1&&_>y+1&&(_=y+1),y!==-1&&f>y+1&&(f=y+1);const I=this._renderedViewLine.getVisibleRangesForRange(_,f,v);return I&&I.length>0?new S.VisibleRanges(L,I):null}getColumnOfNodeOffset(_,f,v){return this._renderedViewLine?this._renderedViewLine.getColumnOfNodeOffset(_,f,v):1}}e.ViewLine=u,u.CLASS_NAME="view-line";class r{constructor(_,f,v){this.domNode=_,this.input=f,this._characterMapping=v,this._charWidth=f.spaceWidth}getWidth(){return this._getCharPosition(this._characterMapping.length)}getWidthIsFast(){return!0}monospaceAssumptionsAreValid(){if(!this.domNode)return o;const _=this.getWidth(),f=this.domNode.domNode.firstChild.offsetWidth;return Math.abs(_-f)>=2&&(console.warn("monospace assumptions have been violated, therefore disabling monospace optimizations!"),o=!1),o}toSlowRenderedLine(){return t(this.domNode,this.input,this._characterMapping,!1,0)}getVisibleRangesForRange(_,f,v){const y=this._getCharPosition(_),L=this._getCharPosition(f);return[new S.HorizontalRange(y,L-y)]}_getCharPosition(_){const f=this._characterMapping.getAbsoluteOffsets();return f.length===0?0:Math.round(this._charWidth*f[_-1])}getColumnOfNodeOffset(_,f,v){const y=f.textContent.length;let L=-1;for(;f;)f=f.previousSibling,L++;return this._characterMapping.partDataToCharOffset(L,y,v)+1}}class i{constructor(_,f,v,y,L){if(this.domNode=_,this.input=f,this._characterMapping=v,this._isWhitespaceOnly=/^\s*$/.test(f.lineContent),this._containsForeignElements=L,this._cachedWidth=-1,this._pixelOffsetCache=null,!y||this._characterMapping.length===0){this._pixelOffsetCache=new Int32Array(Math.max(2,this._characterMapping.length+1));for(let I=0,k=this._characterMapping.length;I<=k;I++)this._pixelOffsetCache[I]=-1}}_getReadingTarget(_){return _.domNode.firstChild}getWidth(){return this.domNode?(this._cachedWidth===-1&&(this._cachedWidth=this._getReadingTarget(this.domNode).offsetWidth),this._cachedWidth):0}getWidthIsFast(){return this._cachedWidth!==-1}getVisibleRangesForRange(_,f,v){if(!this.domNode)return null;if(this._pixelOffsetCache!==null){const y=this._readPixelOffset(this.domNode,_,v);if(y===-1)return null;const L=this._readPixelOffset(this.domNode,f,v);return L===-1?null:[new S.HorizontalRange(y,L-y)]}return this._readVisibleRangesForRange(this.domNode,_,f,v)}_readVisibleRangesForRange(_,f,v,y){if(f===v){const L=this._readPixelOffset(_,f,y);return L===-1?null:[new S.HorizontalRange(L,0)]}else return this._readRawVisibleRangesForRange(_,f,v,y)}_readPixelOffset(_,f,v){if(this._characterMapping.length===0){if(this._containsForeignElements===0||this._containsForeignElements===2)return 0;if(this._containsForeignElements===1)return this.getWidth();const y=this._getReadingTarget(_);return y.firstChild?y.firstChild.offsetWidth:0}if(this._pixelOffsetCache!==null){const y=this._pixelOffsetCache[f];if(y!==-1)return y;const L=this._actualReadPixelOffset(_,f,v);return this._pixelOffsetCache[f]=L,L}return this._actualReadPixelOffset(_,f,v)}_actualReadPixelOffset(_,f,v){if(this._characterMapping.length===0){const T=w.RangeUtil.readHorizontalRanges(this._getReadingTarget(_),0,0,0,0,v.clientRectDeltaLeft,v.endNode);return!T||T.length===0?-1:T[0].left}if(f===this._characterMapping.length&&this._isWhitespaceOnly&&this._containsForeignElements===0)return this.getWidth();const y=this._characterMapping.charOffsetToPartData(f-1),L=d.CharacterMapping.getPartIndex(y),I=d.CharacterMapping.getCharIndex(y),k=w.RangeUtil.readHorizontalRanges(this._getReadingTarget(_),L,I,L,I,v.clientRectDeltaLeft,v.endNode);if(!k||k.length===0)return-1;const E=k[0].left;if(this.input.isBasicASCII){const T=this._characterMapping.getAbsoluteOffsets(),O=Math.round(this.input.spaceWidth*T[f-1]);if(Math.abs(O-E)<=1)return O}return E}_readRawVisibleRangesForRange(_,f,v,y){if(f===1&&v===this._characterMapping.length)return[new S.HorizontalRange(0,this.getWidth())];const L=this._characterMapping.charOffsetToPartData(f-1),I=d.CharacterMapping.getPartIndex(L),k=d.CharacterMapping.getCharIndex(L),E=this._characterMapping.charOffsetToPartData(v-1),T=d.CharacterMapping.getPartIndex(E),O=d.CharacterMapping.getCharIndex(E);return w.RangeUtil.readHorizontalRanges(this._getReadingTarget(_),I,k,T,O,y.clientRectDeltaLeft,y.endNode)}getColumnOfNodeOffset(_,f,v){const y=f.textContent.length;let L=-1;for(;f;)f=f.previousSibling,L++;return this._characterMapping.partDataToCharOffset(L,y,v)+1}}class n extends i{_readVisibleRangesForRange(_,f,v,y){const L=super._readVisibleRangesForRange(_,f,v,y);if(!L||L.length===0||f===v||f===1&&v===this._characterMapping.length)return L;if(!this.input.containsRTL){const I=this._readPixelOffset(_,v,y);if(I!==-1){const k=L[L.length-1];k.left=4&&m[0]===3&&m[3]===7}static isStrictChildOfViewLines(m){return m.length>4&&m[0]===3&&m[3]===7}static isChildOfScrollableElement(m){return m.length>=2&&m[0]===3&&m[1]===5}static isChildOfMinimap(m){return m.length>=2&&m[0]===3&&m[1]===8}static isChildOfContentWidgets(m){return m.length>=4&&m[0]===3&&m[3]===1}static isChildOfOverflowingContentWidgets(m){return m.length>=1&&m[0]===2}static isChildOfOverlayWidgets(m){return m.length>=2&&m[0]===3&&m[1]===4}}class s{constructor(m,_,f){this.model=m.model;const v=m.configuration.options;this.layoutInfo=v.get(124),this.viewDomNode=_.viewDomNode,this.lineHeight=v.get(53),this.stickyTabStops=v.get(99),this.typicalHalfwidthCharacterWidth=v.get(38).typicalHalfwidthCharacterWidth,this.lastRenderData=f,this._context=m,this._viewHelper=_}getZoneAtCoord(m){return s.getZoneAtCoord(this._context,m)}static getZoneAtCoord(m,_){const f=m.viewLayout.getWhitespaceAtVerticalOffset(_);if(f){const v=f.verticalOffset+f.height/2,y=m.model.getLineCount();let L=null,I,k=null;return f.afterLineNumber!==y&&(k=new w.Position(f.afterLineNumber+1,1)),f.afterLineNumber>0&&(L=new w.Position(f.afterLineNumber,m.model.getLineMaxColumn(f.afterLineNumber))),k===null?I=L:L===null?I=k:_=m.layoutInfo.glyphMarginLeft,this.isInContentArea=!this.isInMarginArea,this.mouseColumn=Math.max(0,n._getMouseColumn(this.mouseContentHorizontalOffset,m.typicalHalfwidthCharacterWidth))}}class u extends a{constructor(m,_,f,v){super(m,_,f);this._ctx=m,v?(this.target=v,this.targetPath=N.PartFingerprints.collect(v,m.viewDomNode)):(this.target=null,this.targetPath=new Uint8Array(0))}toString(){return`pos(${this.pos.x},${this.pos.y}), editorPos(${this.editorPos.x},${this.editorPos.y}), mouseVerticalOffset: ${this.mouseVerticalOffset}, mouseContentHorizontalOffset: ${this.mouseContentHorizontalOffset} + target: ${this.target?this.target.outerHTML:null}`}fulfill(m,_=null,f=null,v=null){let y=this.mouseColumn;return _&&_.columnL.contentLeft+L.width)){const I=m.getVerticalOffsetForLineNumber(L.position.lineNumber);if(I<=y&&y<=I+L.height)return _.fulfill(6,L.position)}}return null}static _hitTestViewZone(m,_){const f=m.getZoneAtCoord(_.mouseVerticalOffset);if(f){const v=_.isInContentArea?8:5;return _.fulfill(v,f.position,null,f)}return null}static _hitTestTextArea(m,_){return o.isTextArea(_.targetPath)?m.lastRenderData.lastTextareaPosition?_.fulfill(6,m.lastRenderData.lastTextareaPosition):_.fulfill(1,m.lastRenderData.lastTextareaPosition):null}static _hitTestMargin(m,_){if(_.isInMarginArea){const f=m.getFullLineRangeAtCoord(_.mouseVerticalOffset),v=f.range.getStartPosition();let y=Math.abs(_.pos.x-_.editorPos.x);const L={isAfterLines:f.isAfterLines,glyphMarginLeft:m.layoutInfo.glyphMarginLeft,glyphMarginWidth:m.layoutInfo.glyphMarginWidth,lineNumbersWidth:m.layoutInfo.lineNumbersWidth,offsetX:y};return y-=m.layoutInfo.glyphMarginLeft,y<=m.layoutInfo.glyphMarginWidth?_.fulfill(2,v,f.range,L):(y-=m.layoutInfo.glyphMarginWidth,y<=m.layoutInfo.lineNumbersWidth?_.fulfill(3,v,f.range,L):(y-=m.layoutInfo.lineNumbersWidth,_.fulfill(4,v,f.range,L)))}return null}static _hitTestViewLines(m,_,f){if(!o.isChildOfViewLines(_.targetPath))return null;if(m.isInTopPadding(_.mouseVerticalOffset))return _.fulfill(7,new w.Position(1,1),void 0,r);if(m.isAfterLines(_.mouseVerticalOffset)||m.isInBottomPadding(_.mouseVerticalOffset)){const y=m.model.getLineCount(),L=m.model.getLineMaxColumn(y);return _.fulfill(7,new w.Position(y,L),void 0,r)}if(f){if(o.isStrictChildOfViewLines(_.targetPath)){const y=m.getLineNumberAtVerticalOffset(_.mouseVerticalOffset);if(m.model.getLineLength(y)===0){const I=m.getLineWidth(y),k=i(_.mouseContentHorizontalOffset-I);return _.fulfill(7,new w.Position(y,1),void 0,k)}const L=m.getLineWidth(y);if(_.mouseContentHorizontalOffset>=L){const I=i(_.mouseContentHorizontalOffset-L),k=new w.Position(y,m.model.getLineMaxColumn(y));return _.fulfill(7,k,void 0,I)}}return _.fulfill(0)}const v=n._doHitTest(m,_);return v.position?n.createMouseTargetFromHitTestPosition(m,_,v.position.lineNumber,v.position.column):this._createMouseTarget(m,_.withTarget(v.hitTarget),!0)}static _hitTestMinimap(m,_){if(o.isChildOfMinimap(_.targetPath)){const f=m.getLineNumberAtVerticalOffset(_.mouseVerticalOffset),v=m.model.getLineMaxColumn(f);return _.fulfill(11,new w.Position(f,v))}return null}static _hitTestScrollbarSlider(m,_){if(o.isChildOfScrollableElement(_.targetPath)&&_.target&&_.target.nodeType===1){const f=_.target.className;if(f&&/\b(slider|scrollbar)\b/.test(f)){const v=m.getLineNumberAtVerticalOffset(_.mouseVerticalOffset),y=m.model.getLineMaxColumn(v);return _.fulfill(11,new w.Position(v,y))}}return null}static _hitTestScrollbar(m,_){if(o.isChildOfScrollableElement(_.targetPath)){const f=m.getLineNumberAtVerticalOffset(_.mouseVerticalOffset),v=m.model.getLineMaxColumn(f);return _.fulfill(11,new w.Position(f,v))}return null}getMouseColumn(m,_){const f=this._context.configuration.options,v=f.get(124),y=this._context.viewLayout.getCurrentScrollLeft()+_.x-m.x-v.contentLeft;return n._getMouseColumn(y,f.get(38).typicalHalfwidthCharacterWidth)}static _getMouseColumn(m,_){return m<0?1:Math.round(m/_)+1}static createMouseTargetFromHitTestPosition(m,_,f,v){const y=new w.Position(f,v),L=m.getLineWidth(f);if(_.mouseContentHorizontalOffset>L){const O=i(_.mouseContentHorizontalOffset-L);return _.fulfill(7,y,void 0,O)}const I=m.visibleRangeForPosition(f,v);if(!I)return _.fulfill(0,y);const k=I.left;if(_.mouseContentHorizontalOffset===k)return _.fulfill(6,y);const E=[];if(E.push({offset:I.left,column:v}),v>1){const O=m.visibleRangeForPosition(f,v-1);O&&E.push({offset:O.left,column:v-1})}const T=m.model.getLineMaxColumn(f);if(vO.offset-A.offset);for(let O=1;O=_.editorPos.y+m.layoutInfo.height&&(L=_.editorPos.y+m.layoutInfo.height-1);const I=new b.PageCoordinates(_.pos.x,L),k=this._actualDoHitTestWithCaretRangeFromPoint(m,I.toClientCoordinates());return k.position?k:this._actualDoHitTestWithCaretRangeFromPoint(m,_.pos.toClientCoordinates())}static _actualDoHitTestWithCaretRangeFromPoint(m,_){const f=d.getShadowRoot(m.viewDomNode);let v;if(f?typeof f.caretRangeFromPoint=="undefined"?v=t(f,_.clientX,_.clientY):v=f.caretRangeFromPoint(_.clientX,_.clientY):v=document.caretRangeFromPoint(_.clientX,_.clientY),!v||!v.startContainer)return{position:null,hitTarget:null};const y=v.startContainer;let L=null;if(y.nodeType===y.TEXT_NODE){const I=y.parentNode,k=I?I.parentNode:null,E=k?k.parentNode:null;if((E&&E.nodeType===E.ELEMENT_NODE?E.className:null)===M.ViewLine.CLASS_NAME)return{position:m.getPositionFromDOMInfo(I,v.startOffset),hitTarget:null};L=y.parentNode}else if(y.nodeType===y.ELEMENT_NODE){const I=y.parentNode,k=I?I.parentNode:null;if((k&&k.nodeType===k.ELEMENT_NODE?k.className:null)===M.ViewLine.CLASS_NAME)return{position:m.getPositionFromDOMInfo(y,y.textContent.length),hitTarget:null};L=y}return{position:null,hitTarget:L}}static _doHitTestWithCaretPositionFromPoint(m,_){const f=document.caretPositionFromPoint(_.clientX,_.clientY);if(f.offsetNode.nodeType===f.offsetNode.TEXT_NODE){const v=f.offsetNode.parentNode,y=v?v.parentNode:null,L=y?y.parentNode:null;return(L&&L.nodeType===L.ELEMENT_NODE?L.className:null)===M.ViewLine.CLASS_NAME?{position:m.getPositionFromDOMInfo(f.offsetNode.parentNode,f.offset),hitTarget:null}:{position:null,hitTarget:f.offsetNode.parentNode}}if(f.offsetNode.nodeType===f.offsetNode.ELEMENT_NODE){const v=f.offsetNode.parentNode,y=v&&v.nodeType===v.ELEMENT_NODE?v.className:null,L=v?v.parentNode:null,I=L&&L.nodeType===L.ELEMENT_NODE?L.className:null;if(y===M.ViewLine.CLASS_NAME){const k=f.offsetNode.childNodes[Math.min(f.offset,f.offsetNode.childNodes.length-1)];if(k)return{position:m.getPositionFromDOMInfo(k,0),hitTarget:null}}else if(I===M.ViewLine.CLASS_NAME)return{position:m.getPositionFromDOMInfo(f.offsetNode,0),hitTarget:null}}return{position:null,hitTarget:f.offsetNode}}static _snapToSoftTabBoundary(m,_){const f=_.getLineContent(m.lineNumber),{tabSize:v}=_.getTextModelOptions(),y=g.AtomicTabMoveOperations.atomicPosition(f,m.column-1,v,2);return y!==-1?new w.Position(m.lineNumber,y+1):m}static _doHitTest(m,_){let f;return typeof document.caretRangeFromPoint=="function"?f=this._doHitTestWithCaretRangeFromPoint(m,_):document.caretPositionFromPoint?f=this._doHitTestWithCaretPositionFromPoint(m,_.pos.toClientCoordinates()):f={position:null,hitTarget:null},f.position&&m.stickyTabStops&&(f.position=this._snapToSoftTabBoundary(f.position,m.model)),f}}e.MouseTargetFactory=n;function t(h,m,_){const f=document.createRange();let v=h.elementFromPoint(m,_);if(v!==null){for(;v&&v.firstChild&&v.firstChild.nodeType!==v.firstChild.TEXT_NODE&&v.lastChild&&v.lastChild.firstChild;)v=v.lastChild;const y=v.getBoundingClientRect(),L=window.getComputedStyle(v,null).getPropertyValue("font"),I=v.innerText;let k=y.left,E=0,T;if(m>y.left+y.width)E=I.length;else{const O=l.getInstance();for(let A=0;Athis._createMouseTarget(_,f),_=>this._getMouseColumn(_))),this.lastMouseLeaveTime=-1,this._height=this._context.configuration.options.get(124).height;const h=new d.EditorMouseEventFactory(this.viewHelper.viewDomNode);this._register(h.onContextMenu(this.viewHelper.viewDomNode,_=>this._onContextMenu(_,!0))),this._register(h.onMouseMoveThrottled(this.viewHelper.viewDomNode,_=>this._onMouseMove(_),s(this.mouseTargetFactory),a.MOUSE_MOVE_MINIMUM_TIME)),this._register(h.onMouseUp(this.viewHelper.viewDomNode,_=>this._onMouseUp(_))),this._register(h.onMouseLeave(this.viewHelper.viewDomNode,_=>this._onMouseLeave(_))),this._register(h.onMouseDown(this.viewHelper.viewDomNode,_=>this._onMouseDown(_)));const m=_=>{if(this.viewController.emitMouseWheel(_),!!this._context.configuration.options.get(62)){const f=new N.StandardWheelEvent(_);if(S.isMacintosh?(_.metaKey||_.ctrlKey)&&!_.shiftKey&&!_.altKey:_.ctrlKey&&!_.metaKey&&!_.shiftKey&&!_.altKey){const y=g.EditorZoom.getZoomLevel(),L=f.deltaY>0?1:-1;g.EditorZoom.setZoomLevel(y+L),f.preventDefault(),f.stopPropagation()}}};this._register(b.addDisposableListener(this.viewHelper.viewDomNode,b.EventType.MOUSE_WHEEL,m,{capture:!0,passive:!1})),this._context.addEventHandler(this)}dispose(){this._context.removeEventHandler(this),super.dispose()}onConfigurationChanged(n){if(n.hasChanged(124)){const t=this._context.configuration.options.get(124).height;this._height!==t&&(this._height=t,this._mouseDownOperation.onHeightChanged())}return!1}onCursorStateChanged(n){return this._mouseDownOperation.onCursorStateChanged(n),!1}onFocusChanged(n){return!1}onScrollChanged(n){return this._mouseDownOperation.onScrollChanged(),!1}getTargetAtClientPoint(n,t){const h=new d.ClientCoordinates(n,t).toPageCoordinates(),m=d.createEditorPagePosition(this.viewHelper.viewDomNode);return h.ym.y+m.height||h.xm.x+m.width?null:this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(),m,h,null)}_createMouseTarget(n,t){return this.mouseTargetFactory.createMouseTarget(this.viewHelper.getLastRenderData(),n.editorPos,n.pos,t?n.target:null)}_getMouseColumn(n){return this.mouseTargetFactory.getMouseColumn(n.editorPos,n.pos)}_onContextMenu(n,t){this.viewController.emitContextMenu({event:n,target:this._createMouseTarget(n,t)})}_onMouseMove(n){this._mouseDownOperation.isActive()||n.timestamp{n.preventDefault(),this.viewHelper.focusTextArea()};if(y&&(l||m&&_))L(),this._mouseDownOperation.start(t.type,n);else if(h)n.preventDefault();else if(f){const I=t.detail;this.viewHelper.shouldSuppressMouseDownOnViewZone(I.viewZoneId)&&(L(),this._mouseDownOperation.start(t.type,n),n.preventDefault())}else v&&this.viewHelper.shouldSuppressMouseDownOnWidget(t.detail)&&(L(),n.preventDefault());this.viewController.emitMouseDown({event:n,target:t})}}e.MouseHandler=a,a.MOUSE_MOVE_MINIMUM_TIME=100;class u extends w.Disposable{constructor(n,t,l,h,m){super();this._context=n,this._viewController=t,this._viewHelper=l,this._createMouseTarget=h,this._getMouseColumn=m,this._mouseMoveMonitor=this._register(new d.GlobalEditorMouseMoveMonitor(this._viewHelper.viewDomNode)),this._onScrollTimeout=this._register(new M.TimeoutTimer),this._mouseState=new r,this._currentSelection=new c.Selection(1,1,1,1),this._isActive=!1,this._lastMouseEvent=null}dispose(){super.dispose()}isActive(){return this._isActive}_onMouseDownThenMove(n){this._lastMouseEvent=n,this._mouseState.setModifiers(n);const t=this._findMousePosition(n,!0);!t||(this._mouseState.isDragAndDrop?this._viewController.emitMouseDrag({event:n,target:t}):this._dispatchMouse(t,!0))}start(n,t){this._lastMouseEvent=t,this._mouseState.setStartedOnLineNumbers(n===3),this._mouseState.setStartButtons(t),this._mouseState.setModifiers(t);const l=this._findMousePosition(t,!0);if(!(!l||!l.position)){this._mouseState.trySetCount(t.detail,l.position),t.detail=this._mouseState.count;const h=this._context.configuration.options;if(!h.get(75)&&h.get(27)&&!h.get(15)&&!this._mouseState.altKey&&t.detail<2&&!this._isActive&&!this._currentSelection.isEmpty()&&l.type===6&&l.position&&this._currentSelection.containsPosition(l.position)){this._mouseState.isDragAndDrop=!0,this._isActive=!0,this._mouseMoveMonitor.startMonitoring(t.target,t.buttons,s(null),m=>this._onMouseDownThenMove(m),m=>{const _=this._findMousePosition(this._lastMouseEvent,!0);m&&m instanceof KeyboardEvent?this._viewController.emitMouseDropCanceled():this._viewController.emitMouseDrop({event:this._lastMouseEvent,target:_?this._createMouseTarget(this._lastMouseEvent,!0):null}),this._stop()});return}this._mouseState.isDragAndDrop=!1,this._dispatchMouse(l,t.shiftKey),this._isActive||(this._isActive=!0,this._mouseMoveMonitor.startMonitoring(t.target,t.buttons,s(null),m=>this._onMouseDownThenMove(m),()=>this._stop()))}}_stop(){this._isActive=!1,this._onScrollTimeout.cancel()}onHeightChanged(){this._mouseMoveMonitor.stopMonitoring()}onScrollChanged(){!this._isActive||this._onScrollTimeout.setIfNotSet(()=>{if(!!this._lastMouseEvent){const n=this._findMousePosition(this._lastMouseEvent,!1);!n||this._mouseState.isDragAndDrop||this._dispatchMouse(n,!0)}},10)}onCursorStateChanged(n){this._currentSelection=n.selections[0]}_getPositionOutsideEditor(n){const t=n.editorPos,l=this._context.model,h=this._context.viewLayout,m=this._getMouseColumn(n);if(n.posyt.y+t.height){const f=h.getCurrentScrollTop()+(n.posy-t.y),v=C.HitTestContext.getZoneAtCoord(this._context,f);if(v){const L=this._helpPositionJumpOverViewZone(v);if(L)return new C.MouseTarget(null,13,m,L)}const y=h.getLineNumberAtVerticalOffset(f);return new C.MouseTarget(null,13,m,new p.Position(y,l.getLineMaxColumn(y)))}const _=h.getLineNumberAtVerticalOffset(h.getCurrentScrollTop()+(n.posy-t.y));return n.posxt.x+t.width?new C.MouseTarget(null,13,m,new p.Position(_,l.getLineMaxColumn(_))):null}_findMousePosition(n,t){const l=this._getPositionOutsideEditor(n);if(l)return l;const h=this._createMouseTarget(n,t);if(!h.position)return null;if(h.type===8||h.type===5){const _=this._helpPositionJumpOverViewZone(h.detail);if(_)return new C.MouseTarget(h.element,h.type,h.mouseColumn,_,null,h.detail)}return h}_helpPositionJumpOverViewZone(n){const t=new p.Position(this._currentSelection.selectionStartLineNumber,this._currentSelection.selectionStartColumn),l=n.positionBefore,h=n.positionAfter;return l&&h?l.isBefore(t)?l:h:null}_dispatchMouse(n,t){!n.position||this._viewController.dispatchMouse({position:n.position,mouseColumn:n.mouseColumn,startedOnLineNumbers:this._mouseState.startedOnLineNumbers,inSelectionMode:t,mouseDownCount:this._mouseState.count,altKey:this._mouseState.altKey,ctrlKey:this._mouseState.ctrlKey,metaKey:this._mouseState.metaKey,shiftKey:this._mouseState.shiftKey,leftButton:this._mouseState.leftButton,middleButton:this._mouseState.middleButton})}}class r{constructor(){this._altKey=!1,this._ctrlKey=!1,this._metaKey=!1,this._shiftKey=!1,this._leftButton=!1,this._middleButton=!1,this._startedOnLineNumbers=!1,this._lastMouseDownPosition=null,this._lastMouseDownPositionEqualCount=0,this._lastMouseDownCount=0,this._lastSetMouseDownCountTime=0,this.isDragAndDrop=!1}get altKey(){return this._altKey}get ctrlKey(){return this._ctrlKey}get metaKey(){return this._metaKey}get shiftKey(){return this._shiftKey}get leftButton(){return this._leftButton}get middleButton(){return this._middleButton}get startedOnLineNumbers(){return this._startedOnLineNumbers}get count(){return this._lastMouseDownCount}setModifiers(n){this._altKey=n.altKey,this._ctrlKey=n.ctrlKey,this._metaKey=n.metaKey,this._shiftKey=n.shiftKey}setStartButtons(n){this._leftButton=n.leftButton,this._middleButton=n.middleButton}setStartedOnLineNumbers(n){this._startedOnLineNumbers=n}trySetCount(n,t){const l=new Date().getTime();l-this._lastSetMouseDownCountTime>r.CLEAR_MOUSE_DOWN_COUNT_TIME&&(n=1),this._lastSetMouseDownCountTime=l,n>this._lastMouseDownCount+1&&(n=this._lastMouseDownCount+1),this._lastMouseDownPosition&&this._lastMouseDownPosition.equals(t)?this._lastMouseDownPositionEqualCount++:this._lastMouseDownPositionEqualCount=1,this._lastMouseDownPosition=t,this._lastMouseDownCount=Math.min(n,this._lastMouseDownPositionEqualCount)}}r.CLEAR_MOUSE_DOWN_COUNT_TIME=400}),define(Q[581],J([0,1,7,17,60,2,580,162,151,164]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PointerHandler=e.PointerEventHandler=void 0;class p extends S.MouseHandler{constructor(a,u,r){super(a,u,r);this._register(M.Gesture.addTarget(this.viewHelper.linesContentDomNode)),this._register(b.addDisposableListener(this.viewHelper.linesContentDomNode,M.EventType.Tap,n=>this.onTap(n))),this._register(b.addDisposableListener(this.viewHelper.linesContentDomNode,M.EventType.Change,n=>this.onChange(n))),this._register(b.addDisposableListener(this.viewHelper.linesContentDomNode,M.EventType.Contextmenu,n=>this._onContextMenu(new C.EditorMouseEvent(n,this.viewHelper.viewDomNode),!1))),this._lastPointerType="mouse",this._register(b.addDisposableListener(this.viewHelper.linesContentDomNode,"pointerdown",n=>{const t=n.pointerType;if(t==="mouse"){this._lastPointerType="mouse";return}else t==="touch"?this._lastPointerType="touch":this._lastPointerType="pen"}));const i=new C.EditorPointerEventFactory(this.viewHelper.viewDomNode);this._register(i.onPointerMoveThrottled(this.viewHelper.viewDomNode,n=>this._onMouseMove(n),S.createMouseMoveEventMerger(this.mouseTargetFactory),S.MouseHandler.MOUSE_MOVE_MINIMUM_TIME)),this._register(i.onPointerUp(this.viewHelper.viewDomNode,n=>this._onMouseUp(n))),this._register(i.onPointerLeave(this.viewHelper.viewDomNode,n=>this._onMouseLeave(n))),this._register(i.onPointerDown(this.viewHelper.viewDomNode,n=>this._onMouseDown(n)))}onTap(a){if(!(!a.initialTarget||!this.viewHelper.linesContentDomNode.contains(a.initialTarget))){a.preventDefault(),this.viewHelper.focusTextArea();const u=this._createMouseTarget(new C.EditorMouseEvent(a,this.viewHelper.viewDomNode),!1);u.position&&this.viewController.dispatchMouse({position:u.position,mouseColumn:u.position.column,startedOnLineNumbers:!1,mouseDownCount:a.tapCount,inSelectionMode:!1,altKey:!1,ctrlKey:!1,metaKey:!1,shiftKey:!1,leftButton:!1,middleButton:!1})}}onChange(a){this._lastPointerType==="touch"&&this._context.model.deltaScrollNow(-a.translationX,-a.translationY)}_onMouseDown(a){a.browserEvent.pointerType!=="touch"&&super._onMouseDown(a)}}e.PointerEventHandler=p;class c extends S.MouseHandler{constructor(a,u,r){super(a,u,r);this._register(M.Gesture.addTarget(this.viewHelper.linesContentDomNode)),this._register(b.addDisposableListener(this.viewHelper.linesContentDomNode,M.EventType.Tap,i=>this.onTap(i))),this._register(b.addDisposableListener(this.viewHelper.linesContentDomNode,M.EventType.Change,i=>this.onChange(i))),this._register(b.addDisposableListener(this.viewHelper.linesContentDomNode,M.EventType.Contextmenu,i=>this._onContextMenu(new C.EditorMouseEvent(i,this.viewHelper.viewDomNode),!1)))}onTap(a){a.preventDefault(),this.viewHelper.focusTextArea();const u=this._createMouseTarget(new C.EditorMouseEvent(a,this.viewHelper.viewDomNode),!1);if(u.position){const r=document.createEvent("CustomEvent");r.initEvent(g.TextAreaSyntethicEvents.Tap,!1,!0),this.viewHelper.dispatchTextAreaEvent(r),this.viewController.moveTo(u.position)}}onChange(a){this._context.model.deltaScrollNow(-a.translationX,-a.translationY)}}class o extends w.Disposable{constructor(a,u,r){super();N.isIOS&&d.BrowserFeatures.pointerEvents?this.handler=this._register(new p(a,u,r)):window.TouchEvent?this.handler=this._register(new c(a,u,r)):this.handler=this._register(new S.MouseHandler(a,u,r))}getTargetAtClientPoint(a,u){return this.handler.getTargetAtClientPoint(a,u)}}e.PointerHandler=o}),define(Q[256],J([0,1,187]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewUserInputEvents=void 0;class N{constructor(S){this.onKeyDown=null,this.onKeyUp=null,this.onContextMenu=null,this.onMouseMove=null,this.onMouseLeave=null,this.onMouseDown=null,this.onMouseUp=null,this.onMouseDrag=null,this.onMouseDrop=null,this.onMouseDropCanceled=null,this.onMouseWheel=null,this._coordinatesConverter=S}emitKeyDown(S){this.onKeyDown&&this.onKeyDown(S)}emitKeyUp(S){this.onKeyUp&&this.onKeyUp(S)}emitContextMenu(S){this.onContextMenu&&this.onContextMenu(this._convertViewToModelMouseEvent(S))}emitMouseMove(S){this.onMouseMove&&this.onMouseMove(this._convertViewToModelMouseEvent(S))}emitMouseLeave(S){this.onMouseLeave&&this.onMouseLeave(this._convertViewToModelMouseEvent(S))}emitMouseDown(S){this.onMouseDown&&this.onMouseDown(this._convertViewToModelMouseEvent(S))}emitMouseUp(S){this.onMouseUp&&this.onMouseUp(this._convertViewToModelMouseEvent(S))}emitMouseDrag(S){this.onMouseDrag&&this.onMouseDrag(this._convertViewToModelMouseEvent(S))}emitMouseDrop(S){this.onMouseDrop&&this.onMouseDrop(this._convertViewToModelMouseEvent(S))}emitMouseDropCanceled(){this.onMouseDropCanceled&&this.onMouseDropCanceled()}emitMouseWheel(S){this.onMouseWheel&&this.onMouseWheel(S)}_convertViewToModelMouseEvent(S){return S.target?{event:S.event,target:this._convertViewToModelMouseTarget(S.target)}:S}_convertViewToModelMouseTarget(S){return N.convertViewToModelMouseTarget(S,this._coordinatesConverter)}static convertViewToModelMouseTarget(S,C){return new M(S.element,S.type,S.mouseColumn,S.position?C.convertViewPositionToModelPosition(S.position):null,S.range?C.convertViewRangeToModelRange(S.range):null,S.detail)}}e.ViewUserInputEvents=N;class M{constructor(S,C,d,g,p,c){this.element=S,this.type=C,this.mouseColumn=d,this.position=g,this.range=p,this.detail=c}toString(){return b.MouseTarget.toString(this)}}}),define(Q[582],J([0,1,17,15,69,165,45,255,14,3,110,124,323]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewLines=void 0;class o{constructor(){this._currentVisibleRange=new g.Range(1,1,1,1)}getCurrentVisibleRange(){return this._currentVisibleRange}setCurrentVisibleRange(i){this._currentVisibleRange=i}}class s{constructor(i,n,t,l,h,m){this.lineNumber=i,this.startColumn=n,this.endColumn=t,this.startScrollTop=l,this.stopScrollTop=h,this.scrollType=m,this.type="range",this.minLineNumber=i,this.maxLineNumber=i}}class a{constructor(i,n,t,l){this.selections=i,this.startScrollTop=n,this.stopScrollTop=t,this.scrollType=l,this.type="selections";let h=i[0].startLineNumber,m=i[0].endLineNumber;for(let _=1,f=i.length;_{this._updateLineWidthsSlow()},200),this._asyncCheckMonospaceFontAssumptions=new N.RunOnceScheduler(()=>{this._checkMonospaceFontAssumptions()},2e3),this._lastRenderedData=new o,this._horizontalRevealRequest=null}dispose(){this._asyncUpdateLineWidths.dispose(),this._asyncCheckMonospaceFontAssumptions.dispose(),super.dispose()}getDomNode(){return this.domNode}createVisibleLine(){return new C.ViewLine(this._viewLineOptions)}onConfigurationChanged(i){this._visibleLines.onConfigurationChanged(i),i.hasChanged(125)&&(this._maxLineWidth=0);const n=this._context.configuration.options,t=n.get(38),l=n.get(125);return this._lineHeight=n.get(53),this._typicalHalfwidthCharacterWidth=t.typicalHalfwidthCharacterWidth,this._isViewportWrapping=l.isViewportWrapping,this._revealHorizontalRightPadding=n.get(84),this._cursorSurroundingLines=n.get(22),this._cursorSurroundingLinesStyle=n.get(23),this._canUseLayerHinting=!n.get(25),M.Configuration.applyFontInfo(this.domNode,t),this._onOptionsMaybeChanged(),i.hasChanged(124)&&(this._maxLineWidth=0),!0}_onOptionsMaybeChanged(){const i=this._context.configuration,n=new C.ViewLineOptions(i,this._context.theme.type);if(!this._viewLineOptions.equals(n)){this._viewLineOptions=n;const t=this._visibleLines.getStartLineNumber(),l=this._visibleLines.getEndLineNumber();for(let h=t;h<=l;h++)this._visibleLines.getVisibleLine(h).onOptionsChanged(this._viewLineOptions);return!0}return!1}onCursorStateChanged(i){const n=this._visibleLines.getStartLineNumber(),t=this._visibleLines.getEndLineNumber();let l=!1;for(let h=n;h<=t;h++)l=this._visibleLines.getVisibleLine(h).onSelectionChanged()||l;return l}onDecorationsChanged(i){{const n=this._visibleLines.getStartLineNumber(),t=this._visibleLines.getEndLineNumber();for(let l=n;l<=t;l++)this._visibleLines.getVisibleLine(l).onDecorationsChanged()}return!0}onFlushed(i){const n=this._visibleLines.onFlushed(i);return this._maxLineWidth=0,n}onLinesChanged(i){return this._visibleLines.onLinesChanged(i)}onLinesDeleted(i){return this._visibleLines.onLinesDeleted(i)}onLinesInserted(i){return this._visibleLines.onLinesInserted(i)}onRevealRangeRequest(i){const n=this._computeScrollTopToRevealRange(this._context.viewLayout.getFutureViewport(),i.source,i.range,i.selections,i.verticalType);if(n===-1)return!1;let t=this._context.viewLayout.validateScrollPosition({scrollTop:n});i.revealHorizontal?i.range&&i.range.startLineNumber!==i.range.endLineNumber?t={scrollTop:t.scrollTop,scrollLeft:0}:i.range?this._horizontalRevealRequest=new s(i.range.startLineNumber,i.range.startColumn,i.range.endColumn,this._context.viewLayout.getCurrentScrollTop(),t.scrollTop,i.scrollType):i.selections&&i.selections.length>0&&(this._horizontalRevealRequest=new a(i.selections,this._context.viewLayout.getCurrentScrollTop(),t.scrollTop,i.scrollType)):this._horizontalRevealRequest=null;const h=Math.abs(this._context.viewLayout.getCurrentScrollTop()-t.scrollTop)<=this._lineHeight?1:i.scrollType;return this._context.model.setScrollPosition(t,h),!0}onScrollChanged(i){if(this._horizontalRevealRequest&&i.scrollLeftChanged&&(this._horizontalRevealRequest=null),this._horizontalRevealRequest&&i.scrollTopChanged){const n=Math.min(this._horizontalRevealRequest.startScrollTop,this._horizontalRevealRequest.stopScrollTop),t=Math.max(this._horizontalRevealRequest.startScrollTop,this._horizontalRevealRequest.stopScrollTop);(i.scrollTopt)&&(this._horizontalRevealRequest=null)}return this.domNode.setWidth(i.scrollWidth),this._visibleLines.onScrollChanged(i)||!0}onTokensChanged(i){return this._visibleLines.onTokensChanged(i)}onZonesChanged(i){return this._context.model.setMaxLineWidth(this._maxLineWidth),this._visibleLines.onZonesChanged(i)}onThemeChanged(i){return this._onOptionsMaybeChanged()}getPositionFromDOMInfo(i,n){const t=this._getViewLineDomNode(i);if(t===null)return null;const l=this._getLineNumberFor(t);if(l===-1||l<1||l>this._context.model.getLineCount())return null;if(this._context.model.getLineMaxColumn(l)===1)return new d.Position(l,1);const h=this._visibleLines.getStartLineNumber(),m=this._visibleLines.getEndLineNumber();if(lm)return null;let _=this._visibleLines.getVisibleLine(l).getColumnOfNodeOffset(l,i,n);const f=this._context.model.getLineMinColumn(l);return _t?-1:this._visibleLines.getVisibleLine(i).getWidth()}linesVisibleRangesForRange(i,n){if(this.shouldRender())return null;const t=i.endLineNumber,l=g.Range.intersectRanges(i,this._lastRenderedData.getCurrentVisibleRange());if(!l)return null;let h=[],m=0;const _=new C.DomReadingContext(this.domNode.domNode,this._textRangeRestingSpot);let f=0;n&&(f=this._context.model.coordinatesConverter.convertViewPositionToModelPosition(new d.Position(l.startLineNumber,1)).lineNumber);const v=this._visibleLines.getStartLineNumber(),y=this._visibleLines.getEndLineNumber();for(let L=l.startLineNumber;L<=l.endLineNumber;L++)if(!(Ly)){const I=L===l.startLineNumber?l.startColumn:1,k=L===l.endLineNumber?l.endColumn:this._context.model.getLineMaxColumn(L),E=this._visibleLines.getVisibleLine(L).getVisibleRangesForRange(I,k,_);if(!!E){if(n&&Lthis._visibleLines.getEndLineNumber()?null:this._visibleLines.getVisibleLine(i).getVisibleRangesForRange(n,t,new C.DomReadingContext(this.domNode.domNode,this._textRangeRestingSpot))}visibleRangeForPosition(i){const n=this._visibleRangesForLineRange(i.lineNumber,i.column,i.column);return n?new p.HorizontalPosition(n.outsideRenderedLine,n.ranges[0].left):null}updateLineWidths(){this._updateLineWidths(!1)}_updateLineWidthsFast(){return this._updateLineWidths(!0)}_updateLineWidthsSlow(){this._updateLineWidths(!1)}_updateLineWidths(i){const n=this._visibleLines.getStartLineNumber(),t=this._visibleLines.getEndLineNumber();let l=1,h=!0;for(let m=n;m<=t;m++){const _=this._visibleLines.getVisibleLine(m);if(i&&!_.getWidthIsFast()){h=!1;continue}l=Math.max(l,_.getWidth())}return h&&n===1&&t===this._context.model.getLineCount()&&(this._maxLineWidth=0),this._ensureMaxLineWidth(l),h}_checkMonospaceFontAssumptions(){let i=-1,n=-1;const t=this._visibleLines.getStartLineNumber(),l=this._visibleLines.getEndLineNumber();for(let h=t;h<=l;h++){const m=this._visibleLines.getVisibleLine(h);if(m.needsMonospaceFontCheck()){const _=m.getWidth();_>n&&(n=_,i=h)}}if(i!==-1&&!this._visibleLines.getVisibleLine(i).monospaceAssumptionsAreValid())for(let h=t;h<=l;h++)this._visibleLines.getVisibleLine(h).onMonospaceAssumptionsInvalidated()}prepareRender(){throw new Error("Not supported")}render(){throw new Error("Not supported")}renderText(i){if(this._visibleLines.renderLines(i),this._lastRenderedData.setCurrentVisibleRange(i.visibleRange),this.domNode.setWidth(this._context.viewLayout.getScrollWidth()),this.domNode.setHeight(Math.min(this._context.viewLayout.getScrollHeight(),1e6)),this._horizontalRevealRequest){const t=this._horizontalRevealRequest;if(i.startLineNumber<=t.minLineNumber&&t.maxLineNumber<=i.endLineNumber){this._horizontalRevealRequest=null,this.onDidRender();const l=this._computeScrollLeftToReveal(t);l&&(this._isViewportWrapping||this._ensureMaxLineWidth(l.maxHorizontalOffset),this._context.model.setScrollPosition({scrollLeft:l.scrollLeft},t.scrollType))}}if(this._updateLineWidthsFast()||this._asyncUpdateLineWidths.schedule(),b.isLinux&&!this._asyncCheckMonospaceFontAssumptions.isScheduled()){const t=this._visibleLines.getStartLineNumber(),l=this._visibleLines.getEndLineNumber();for(let h=t;h<=l;h++)if(this._visibleLines.getVisibleLine(h).needsMonospaceFontCheck()){this._asyncCheckMonospaceFontAssumptions.schedule();break}}this._linesContent.setLayerHinting(this._canUseLayerHinting),this._linesContent.setContain("strict");const n=this._context.viewLayout.getCurrentScrollTop()-i.bigNumbersDelta;this._linesContent.setTop(-n),this._linesContent.setLeft(-this._context.viewLayout.getCurrentScrollLeft())}_ensureMaxLineWidth(i){const n=Math.ceil(i);this._maxLineWidth0){let E=l[0].startLineNumber,T=l[0].endLineNumber;for(let O=1,A=l.length;O_){if(!v)return-1;k=y}else if(h===5||h===6)if(h===6&&m<=y&&L<=f)k=m;else{const E=Math.max(5*this._lineHeight,_*.2),T=y-E,O=L-_;k=Math.max(O,T)}else if(h===1||h===2)if(h===2&&m<=y&&L<=f)k=m;else{const E=(y+L)/2;k=Math.max(0,E-_/2)}else k=this._computeMinimumScrolling(m,f,y,L,h===3,h===4);return k}_computeScrollLeftToReveal(i){const n=this._context.viewLayout.getCurrentViewport(),t=n.left,l=t+n.width;let h=1073741824,m=0;if(i.type==="range"){const f=this._visibleRangesForLineRange(i.lineNumber,i.startColumn,i.endColumn);if(!f)return null;for(const v of f.ranges)h=Math.min(h,v.left),m=Math.max(m,v.left+v.width)}else for(const f of i.selections){if(f.startLineNumber!==f.endLineNumber)return null;const v=this._visibleRangesForLineRange(f.startLineNumber,f.startColumn,f.endColumn);if(!v)return null;for(const y of v.ranges)h=Math.min(h,y.left),m=Math.max(m,y.left+y.width)}return h=Math.max(0,h-u.HORIZONTAL_EXTRA_PX),m+=this._revealHorizontalRightPadding,i.type==="selections"&&m-h>n.width?null:{scrollLeft:this._computeMinimumScrolling(t,l,h,m),maxHorizontalOffset:m}}_computeMinimumScrolling(i,n,t,l,h,m){i=i|0,n=n|0,t=t|0,l=l|0,h=!!h,m=!!m;const _=n-i;if(l-t<_){if(h)return t;if(m)return Math.max(0,l-_);if(tn)return Math.max(0,l-_)}else return t;return i}}e.ViewLines=u,u.HORIZONTAL_EXTRA_PX=30}),define(Q[11],J([0,1,9,2,33,6,97,27]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Themable=e.registerThemingParticipant=e.Extensions=e.getThemeTypeSelector=e.ThemeIcon=e.themeColorFromId=e.ThemeColor=e.IThemeService=void 0,e.IThemeService=b.createDecorator("themeService");var d;(function(r){function i(n){return n&&typeof n=="object"&&typeof n.id=="string"}r.isThemeColor=i})(d=e.ThemeColor||(e.ThemeColor={}));function g(r){return{id:r}}e.themeColorFromId=g;var p;(function(r){function i(m){return m&&typeof m=="object"&&typeof m.id=="string"&&(typeof m.color=="undefined"||d.isThemeColor(m.color))}r.isThemeIcon=i;const n=new RegExp(`^\\$\\((${C.CSSIcon.iconNameExpression}(?:${C.CSSIcon.iconModifierExpression})?)\\)$`);function t(m){const _=n.exec(m);if(!!_){let[,f]=_;return{id:f}}}r.fromString=t;function l(m,_){let f=m.id;const v=f.lastIndexOf("~");return v!==-1&&(f=f.substring(0,v)),_&&(f=`${f}~${_}`),{id:f}}r.modify=l;function h(m,_){var f,v;return m.id===_.id&&((f=m.color)===null||f===void 0?void 0:f.id)===((v=_.color)===null||v===void 0?void 0:v.id)}r.isEqual=h,r.asClassNameArray=C.CSSIcon.asClassNameArray,r.asClassName=C.CSSIcon.asClassName,r.asCSSSelector=C.CSSIcon.asCSSSelector})(p=e.ThemeIcon||(e.ThemeIcon={}));function c(r){switch(r){case S.ColorScheme.DARK:return"vs-dark";case S.ColorScheme.HIGH_CONTRAST:return"hc-black";default:return"vs"}}e.getThemeTypeSelector=c,e.Extensions={ThemingContribution:"base.contributions.theming"};class o{constructor(){this.themingParticipants=[],this.themingParticipants=[],this.onThemingParticipantAddedEmitter=new w.Emitter}onColorThemeChange(i){return this.themingParticipants.push(i),this.onThemingParticipantAddedEmitter.fire(i),N.toDisposable(()=>{const n=this.themingParticipants.indexOf(i);this.themingParticipants.splice(n,1)})}getThemingParticipants(){return this.themingParticipants}}let s=new o;M.Registry.add(e.Extensions.ThemingContribution,s);function a(r){return s.onColorThemeChange(r)}e.registerThemingParticipant=a;class u extends N.Disposable{constructor(i){super();this.themeService=i,this.theme=i.getColorTheme(),this._register(this.themeService.onDidColorThemeChange(n=>this.onThemeChange(n)))}onThemeChange(i){this.theme=i,this.updateStyles()}updateStyles(){}}e.Themable=u}),define(Q[583],J([0,1,7,2,8,24,362,107,53,11]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DecorationTypeOptionsProvider=e.DecorationSubTypeOptionsProvider=e.CodeEditorServiceImpl=e.GlobalStyleSheet=e.RefCountedStyleSheet=void 0;class p{constructor(t,l,h){this._parent=t,this._editorId=l,this._styleSheet=h,this._refCount=0}ref(){this._refCount++}unref(){var t;this._refCount--,this._refCount===0&&((t=this._styleSheet.parentNode)===null||t===void 0||t.removeChild(this._styleSheet),this._parent._removeEditorStyleSheets(this._editorId))}insertRule(t,l){this._styleSheet.sheet.insertRule(t,l)}removeRulesContainingSelector(t){b.removeCSSRulesContainingSelector(t,this._styleSheet)}}e.RefCountedStyleSheet=p;class c{constructor(t){this._styleSheet=t}ref(){}unref(){}insertRule(t,l){this._styleSheet.sheet.insertRule(t,l)}removeRulesContainingSelector(t){b.removeCSSRulesContainingSelector(t,this._styleSheet)}}e.GlobalStyleSheet=c;let o=class extends S.AbstractCodeEditorService{constructor(t,l){super();this._decorationOptionProviders=new Map,this._editorStyleSheets=new Map,this._globalStyleSheet=t||null,this._themeService=l}_getOrCreateGlobalStyleSheet(){return this._globalStyleSheet||(this._globalStyleSheet=new c(b.createStyleSheet())),this._globalStyleSheet}_getOrCreateStyleSheet(t){if(!t)return this._getOrCreateGlobalStyleSheet();const l=t.getContainerDomNode();if(!b.isInShadowDOM(l))return this._getOrCreateGlobalStyleSheet();const h=t.getId();if(!this._editorStyleSheets.has(h)){const m=new p(this,h,b.createStyleSheet(l));this._editorStyleSheets.set(h,m)}return this._editorStyleSheets.get(h)}_removeEditorStyleSheets(t){this._editorStyleSheets.delete(t)}registerDecorationType(t,l,h,m){let _=this._decorationOptionProviders.get(t);if(!_){const f=this._getOrCreateStyleSheet(m),v={styleSheet:f,key:t,parentTypeKey:h,options:l||Object.create(null)};h?_=new s(this._themeService,f,v):_=new a(this._themeService,f,v),this._decorationOptionProviders.set(t,_),this._onDecorationTypeRegistered.fire(t)}_.refCount++}removeDecorationType(t){const l=this._decorationOptionProviders.get(t);l&&(l.refCount--,l.refCount<=0&&(this._decorationOptionProviders.delete(t),l.dispose(),this.listCodeEditors().forEach(h=>h.removeDecorations(t))))}resolveDecorationOptions(t,l){const h=this._decorationOptionProviders.get(t);if(!h)throw new Error("Unknown decoration type key: "+t);return h.getOptions(this,l)}};o=Me([_e(1,g.IThemeService)],o),e.CodeEditorServiceImpl=o;class s{constructor(t,l,h){this._styleSheet=l,this._styleSheet.ref(),this._parentTypeKey=h.parentTypeKey,this.refCount=0,this._beforeContentRules=new r(3,h,t),this._afterContentRules=new r(4,h,t)}getOptions(t,l){const h=t.resolveDecorationOptions(this._parentTypeKey,!0);return this._beforeContentRules&&(h.beforeContentClassName=this._beforeContentRules.className),this._afterContentRules&&(h.afterContentClassName=this._afterContentRules.className),h}dispose(){this._beforeContentRules&&(this._beforeContentRules.dispose(),this._beforeContentRules=null),this._afterContentRules&&(this._afterContentRules.dispose(),this._afterContentRules=null),this._styleSheet.unref()}}e.DecorationSubTypeOptionsProvider=s;class a{constructor(t,l,h){this._disposables=new N.DisposableStore,this._styleSheet=l,this._styleSheet.ref(),this.refCount=0;const m=I=>{const k=new r(I,h,t);if(this._disposables.add(k),k.hasContent)return k.className},_=I=>{const k=new r(I,h,t);return this._disposables.add(k),k.hasContent?{className:k.className,hasLetterSpacing:k.hasLetterSpacing}:null};this.className=m(0);const f=_(1);f&&(this.inlineClassName=f.className,this.inlineClassNameAffectsLetterSpacing=f.hasLetterSpacing),this.beforeContentClassName=m(3),this.afterContentClassName=m(4),this.glyphMarginClassName=m(2);const v=h.options;this.isWholeLine=Boolean(v.isWholeLine),this.stickiness=v.rangeBehavior;const y=v.light&&v.light.overviewRulerColor||v.overviewRulerColor,L=v.dark&&v.dark.overviewRulerColor||v.overviewRulerColor;(typeof y!="undefined"||typeof L!="undefined")&&(this.overviewRuler={color:y||L,darkColor:L||y,position:v.overviewRulerLane||d.OverviewRulerLane.Center})}getOptions(t,l){return l?{inlineClassName:this.inlineClassName,beforeContentClassName:this.beforeContentClassName,afterContentClassName:this.afterContentClassName,className:this.className,glyphMarginClassName:this.glyphMarginClassName,isWholeLine:this.isWholeLine,overviewRuler:this.overviewRuler,stickiness:this.stickiness}:this}dispose(){this._disposables.dispose(),this._styleSheet.unref()}}e.DecorationTypeOptionsProvider=a;const u={color:"color:{0} !important;",opacity:"opacity:{0};",backgroundColor:"background-color:{0};",outline:"outline:{0};",outlineColor:"outline-color:{0};",outlineStyle:"outline-style:{0};",outlineWidth:"outline-width:{0};",border:"border:{0};",borderColor:"border-color:{0};",borderRadius:"border-radius:{0};",borderSpacing:"border-spacing:{0};",borderStyle:"border-style:{0};",borderWidth:"border-width:{0};",fontStyle:"font-style:{0};",fontWeight:"font-weight:{0};",fontSize:"font-size:{0};",fontFamily:"font-family:{0};",textDecoration:"text-decoration:{0};",cursor:"cursor:{0};",letterSpacing:"letter-spacing:{0};",gutterIconPath:"background:{0} center center no-repeat;",gutterIconSize:"background-size:{0};",contentText:"content:'{0}';",contentIconPath:"content:{0};",margin:"margin:{0};",padding:"padding:{0};",width:"width:{0};",height:"height:{0};"};class r{constructor(t,l,h){this._theme=h.getColorTheme(),this._ruleType=t,this._providerArgs=l,this._usesThemeColors=!1,this._hasContent=!1,this._hasLetterSpacing=!1;let m=i.getClassName(this._providerArgs.key,t);this._providerArgs.parentTypeKey&&(m=m+" "+i.getClassName(this._providerArgs.parentTypeKey,t)),this._className=m,this._unThemedSelector=i.getSelector(this._providerArgs.key,this._providerArgs.parentTypeKey,t),this._buildCSS(),this._usesThemeColors?this._themeListener=h.onDidColorThemeChange(_=>{this._theme=h.getColorTheme(),this._removeCSS(),this._buildCSS()}):this._themeListener=null}dispose(){this._hasContent&&(this._removeCSS(),this._hasContent=!1),this._themeListener&&(this._themeListener.dispose(),this._themeListener=null)}get hasContent(){return this._hasContent}get hasLetterSpacing(){return this._hasLetterSpacing}get className(){return this._className}_buildCSS(){const t=this._providerArgs.options;let l,h,m;switch(this._ruleType){case 0:l=this.getCSSTextForModelDecorationClassName(t),h=this.getCSSTextForModelDecorationClassName(t.light),m=this.getCSSTextForModelDecorationClassName(t.dark);break;case 1:l=this.getCSSTextForModelDecorationInlineClassName(t),h=this.getCSSTextForModelDecorationInlineClassName(t.light),m=this.getCSSTextForModelDecorationInlineClassName(t.dark);break;case 2:l=this.getCSSTextForModelDecorationGlyphMarginClassName(t),h=this.getCSSTextForModelDecorationGlyphMarginClassName(t.light),m=this.getCSSTextForModelDecorationGlyphMarginClassName(t.dark);break;case 3:l=this.getCSSTextForModelDecorationContentClassName(t.before),h=this.getCSSTextForModelDecorationContentClassName(t.light&&t.light.before),m=this.getCSSTextForModelDecorationContentClassName(t.dark&&t.dark.before);break;case 4:l=this.getCSSTextForModelDecorationContentClassName(t.after),h=this.getCSSTextForModelDecorationContentClassName(t.light&&t.light.after),m=this.getCSSTextForModelDecorationContentClassName(t.dark&&t.dark.after);break;default:throw new Error("Unknown rule type: "+this._ruleType)}const _=this._providerArgs.styleSheet;let f=!1;l.length>0&&(_.insertRule(`${this._unThemedSelector} {${l}}`,0),f=!0),h.length>0&&(_.insertRule(`.vs${this._unThemedSelector} {${h}}`,0),f=!0),m.length>0&&(_.insertRule(`.vs-dark${this._unThemedSelector}, .hc-black${this._unThemedSelector} {${m}}`,0),f=!0),this._hasContent=f}_removeCSS(){this._providerArgs.styleSheet.removeRulesContainingSelector(this._unThemedSelector)}getCSSTextForModelDecorationClassName(t){if(!t)return"";const l=[];return this.collectCSSText(t,["backgroundColor"],l),this.collectCSSText(t,["outline","outlineColor","outlineStyle","outlineWidth"],l),this.collectBorderSettingsCSSText(t,l),l.join("")}getCSSTextForModelDecorationInlineClassName(t){if(!t)return"";const l=[];return this.collectCSSText(t,["fontStyle","fontWeight","textDecoration","cursor","color","opacity","letterSpacing"],l),t.letterSpacing&&(this._hasLetterSpacing=!0),l.join("")}getCSSTextForModelDecorationContentClassName(t){if(!t)return"";const l=[];if(typeof t!="undefined"){if(this.collectBorderSettingsCSSText(t,l),typeof t.contentIconPath!="undefined"&&l.push(M.format(u.contentIconPath,b.asCSSUrl(w.URI.revive(t.contentIconPath)))),typeof t.contentText=="string"){const m=t.contentText.match(/^.*$/m)[0].replace(/['\\]/g,"\\$&");l.push(M.format(u.contentText,m))}this.collectCSSText(t,["fontStyle","fontWeight","fontSize","fontFamily","textDecoration","color","opacity","backgroundColor","margin","padding"],l),this.collectCSSText(t,["width","height"],l)&&l.push("display:inline-block;")}return l.join("")}getCSSTextForModelDecorationGlyphMarginClassName(t){if(!t)return"";const l=[];return typeof t.gutterIconPath!="undefined"&&(l.push(M.format(u.gutterIconPath,b.asCSSUrl(w.URI.revive(t.gutterIconPath)))),typeof t.gutterIconSize!="undefined"&&l.push(M.format(u.gutterIconSize,t.gutterIconSize))),l.join("")}collectBorderSettingsCSSText(t,l){return this.collectCSSText(t,["border","borderColor","borderRadius","borderSpacing","borderStyle","borderWidth"],l)?(l.push(M.format("box-sizing: border-box;")),!0):!1}collectCSSText(t,l,h){const m=h.length;for(let _ of l){const f=this.resolveValue(t[_]);typeof f=="string"&&h.push(M.format(u[_],f))}return h.length!==m}resolveValue(t){if(C.isThemeColor(t)){this._usesThemeColors=!0;const l=this._theme.getColor(t.id);return l?l.toString():"transparent"}return t}}class i{static getClassName(t,l){return"ced-"+t+"-"+l}static getSelector(t,l,h){let m=".monaco-editor ."+this.getClassName(t,h);return l&&(m=m+"."+this.getClassName(l,h)),h===3?m+="::before":h===4&&(m+="::after"),m}}}),define(Q[584],J([0,1,7,30,61,45,11]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EditorScrollbar=void 0;class C extends w.ViewPart{constructor(g,p,c,o){super(g);const s=this._context.configuration.options,a=s.get(87),u=s.get(61),r=s.get(30),i=s.get(90),n={listenOnDomNode:c.domNode,className:"editor-scrollable "+S.getThemeTypeSelector(g.theme.type),useShadows:!1,lazyRender:!0,vertical:a.vertical,horizontal:a.horizontal,verticalHasArrows:a.verticalHasArrows,horizontalHasArrows:a.horizontalHasArrows,verticalScrollbarSize:a.verticalScrollbarSize,verticalSliderSize:a.verticalSliderSize,horizontalScrollbarSize:a.horizontalScrollbarSize,horizontalSliderSize:a.horizontalSliderSize,handleMouseWheel:a.handleMouseWheel,alwaysConsumeMouseWheel:a.alwaysConsumeMouseWheel,arrowSize:a.arrowSize,mouseWheelScrollSensitivity:u,fastScrollSensitivity:r,scrollPredominantAxis:i,scrollByPage:a.scrollByPage};this.scrollbar=this._register(new M.SmoothScrollableElement(p.domNode,n,this._context.viewLayout.getScrollable())),w.PartFingerprints.write(this.scrollbar.getDomNode(),5),this.scrollbarDomNode=N.createFastDomNode(this.scrollbar.getDomNode()),this.scrollbarDomNode.setPosition("absolute"),this._setLayout();const t=(l,h,m)=>{const _={};if(h){const f=l.scrollTop;f&&(_.scrollTop=this._context.viewLayout.getCurrentScrollTop()+f,l.scrollTop=0)}if(m){const f=l.scrollLeft;f&&(_.scrollLeft=this._context.viewLayout.getCurrentScrollLeft()+f,l.scrollLeft=0)}this._context.model.setScrollPosition(_,1)};this._register(b.addDisposableListener(c.domNode,"scroll",l=>t(c.domNode,!0,!0))),this._register(b.addDisposableListener(p.domNode,"scroll",l=>t(p.domNode,!0,!1))),this._register(b.addDisposableListener(o.domNode,"scroll",l=>t(o.domNode,!0,!1))),this._register(b.addDisposableListener(this.scrollbarDomNode.domNode,"scroll",l=>t(this.scrollbarDomNode.domNode,!0,!1)))}dispose(){super.dispose()}_setLayout(){const g=this._context.configuration.options,p=g.get(124);this.scrollbarDomNode.setLeft(p.contentLeft),g.get(59).side==="right"?this.scrollbarDomNode.setWidth(p.contentWidth+p.minimap.minimapWidth):this.scrollbarDomNode.setWidth(p.contentWidth),this.scrollbarDomNode.setHeight(p.height)}getOverviewRulerLayoutInfo(){return this.scrollbar.getOverviewRulerLayoutInfo()}getDomNode(){return this.scrollbarDomNode}delegateVerticalScrollbarMouseDown(g){this.scrollbar.delegateVerticalScrollbarMouseDown(g)}onConfigurationChanged(g){if(g.hasChanged(87)||g.hasChanged(61)||g.hasChanged(30)){const p=this._context.configuration.options,c=p.get(87),o=p.get(61),s=p.get(30),a=p.get(90),u={handleMouseWheel:c.handleMouseWheel,mouseWheelScrollSensitivity:o,fastScrollSensitivity:s,scrollPredominantAxis:a};this.scrollbar.updateOptions(u)}return g.hasChanged(124)&&this._setLayout(),!0}onScrollChanged(g){return!0}onThemeChanged(g){return this.scrollbar.updateClassName("editor-scrollable "+S.getThemeTypeSelector(this._context.theme.type)),!0}prepareRender(g){}render(g){this.scrollbar.renderNow()}}e.EditorScrollbar=C}),define(Q[585],J([0,1,7,30,90,2,17,8,165,45,38,3,216,244,63,22,11,21,60,365,53,88,326]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Minimap=void 0;const m=140,_=2;class f{constructor(A,B,F){const D=A.options,R=D.get(122),W=D.get(124),x=W.minimap,K=D.get(38),Y=D.get(59);this.renderMinimap=x.renderMinimap,this.size=Y.size,this.minimapHeightIsEditorHeight=x.minimapHeightIsEditorHeight,this.scrollBeyondLastLine=D.get(89),this.showSlider=Y.showSlider,this.pixelRatio=R,this.typicalHalfwidthCharacterWidth=K.typicalHalfwidthCharacterWidth,this.lineHeight=D.get(53),this.minimapLeft=x.minimapLeft,this.minimapWidth=x.minimapWidth,this.minimapHeight=W.height,this.canvasInnerWidth=x.minimapCanvasInnerWidth,this.canvasInnerHeight=x.minimapCanvasInnerHeight,this.canvasOuterWidth=x.minimapCanvasOuterWidth,this.canvasOuterHeight=x.minimapCanvasOuterHeight,this.isSampling=x.minimapIsSampling,this.editorHeight=W.height,this.fontScale=x.minimapScale,this.minimapLineHeight=x.minimapLineHeight,this.minimapCharWidth=1*this.fontScale,this.charRenderer=h.once(()=>t.MinimapCharRendererFactory.create(this.fontScale,K.fontFamily)),this.backgroundColor=f._getMinimapBackground(B,F)}static _getMinimapBackground(A,B){const F=A.getColor(u.minimapBackground);return F?new o.RGBA8(F.rgba.r,F.rgba.g,F.rgba.b,F.rgba.a):B.getColor(2)}equals(A){return this.renderMinimap===A.renderMinimap&&this.size===A.size&&this.minimapHeightIsEditorHeight===A.minimapHeightIsEditorHeight&&this.scrollBeyondLastLine===A.scrollBeyondLastLine&&this.showSlider===A.showSlider&&this.pixelRatio===A.pixelRatio&&this.typicalHalfwidthCharacterWidth===A.typicalHalfwidthCharacterWidth&&this.lineHeight===A.lineHeight&&this.minimapLeft===A.minimapLeft&&this.minimapWidth===A.minimapWidth&&this.minimapHeight===A.minimapHeight&&this.canvasInnerWidth===A.canvasInnerWidth&&this.canvasInnerHeight===A.canvasInnerHeight&&this.canvasOuterWidth===A.canvasOuterWidth&&this.canvasOuterHeight===A.canvasOuterHeight&&this.isSampling===A.isSampling&&this.editorHeight===A.editorHeight&&this.fontScale===A.fontScale&&this.minimapLineHeight===A.minimapLineHeight&&this.minimapCharWidth===A.minimapCharWidth&&this.backgroundColor&&this.backgroundColor.equals(A.backgroundColor)}}class v{constructor(A,B,F,D,R,W,x,K){this.scrollTop=A,this.scrollHeight=B,this.sliderNeeded=F,this._computedSliderRatio=D,this.sliderTop=R,this.sliderHeight=W,this.startLineNumber=x,this.endLineNumber=K}getDesiredScrollTopFromDelta(A){return Math.round(this.scrollTop+A/this._computedSliderRatio)}getDesiredScrollTopFromTouchLocation(A){return Math.round((A-this.sliderHeight/2)/this._computedSliderRatio)}static create(A,B,F,D,R,W,x,K,Y,ee,se){const ne=A.pixelRatio,le=A.minimapLineHeight,X=Math.floor(A.canvasInnerHeight/le),z=A.lineHeight;if(A.minimapHeightIsEditorHeight){const ie=K*A.lineHeight+(A.scrollBeyondLastLine?R-A.lineHeight:0),oe=Math.max(1,Math.floor(R*R/ie)),ae=Math.max(0,A.minimapHeight-oe),G=ae/(ee-R),j=Y*G,te=ae>0,Z=Math.floor(A.canvasInnerHeight/A.minimapLineHeight);return new v(Y,ee,te,G,j,oe,1,Math.min(x,Z))}let P;if(W&&F!==x){const ie=F-B+1;P=Math.floor(ie*le/ne)}else{const ie=R/z;P=Math.floor(ie*le/ne)}let V;A.scrollBeyondLastLine?V=(x-1)*le/ne:V=Math.max(0,x*le/ne-P),V=Math.min(A.minimapHeight-P,V);const U=V/(ee-R),H=Y*U;let $=0;if(A.scrollBeyondLastLine&&($=R/z-1),X>=x+$){const ie=1,oe=x,ae=V>0;return new v(Y,ee,ae,U,H,P,ie,oe)}else{let ie=Math.max(1,Math.floor(B-H*ne/le));se&&se.scrollHeight===ee&&(se.scrollTop>Y&&(ie=Math.min(ie,se.startLineNumber)),se.scrollTopy.INVALID),this._renderedLines._set(A.startLineNumber,F)}linesEquals(A){if(!this.scrollEquals(A))return!1;const F=this._renderedLines._get().lines;for(let D=0,R=F.length;D1){for(let $=0,ie=x-1;$0&&this.minimapLines[F-1]>=A;)F--;let D=this.modelLineToMinimapLine(B)-1;for(;D+1B)return null}return[F+1,D+1]}decorationLineRangeToMinimapLineRange(A,B){let F=this.modelLineToMinimapLine(A),D=this.modelLineToMinimapLine(B);return A!==B&&D===F&&(D===this.minimapLines.length?F>1&&F--:D++),[F,D]}onLinesDeleted(A){const B=A.toLineNumber-A.fromLineNumber+1;let F=this.minimapLines.length,D=0;for(let R=this.minimapLines.length-1;R>=0&&!(this.minimapLines[R]=0&&!(this.minimapLines[F]0,scrollWidth:A.scrollWidth,scrollHeight:A.scrollHeight,viewportStartLineNumber:B,viewportEndLineNumber:F,viewportStartLineNumberVerticalOffset:A.getVerticalOffsetForLineNumber(B),scrollTop:A.scrollTop,scrollLeft:A.scrollLeft,viewportWidth:A.viewportWidth,viewportHeight:A.viewportHeight};this._actual.render(D)}_recreateLineSampling(){this._minimapSelections=null;const A=Boolean(this._samplingState),[B,F]=k.compute(this.options,this._context.model.getLineCount(),this._samplingState);if(this._samplingState=B,A&&this._samplingState)for(const D of F)switch(D.type){case"deleted":this._actual.onLinesDeleted(D.deleteFromLineNumber,D.deleteToLineNumber);break;case"inserted":this._actual.onLinesInserted(D.insertFromLineNumber,D.insertToLineNumber);break;case"flush":this._actual.onFlushed();break}}getLineCount(){return this._samplingState?this._samplingState.minimapLines.length:this._context.model.getLineCount()}getRealLineCount(){return this._context.model.getLineCount()}getLineContent(A){return this._samplingState?this._context.model.getLineContent(this._samplingState.minimapLines[A-1]):this._context.model.getLineContent(A)}getMinimapLinesRenderingData(A,B,F){if(this._samplingState){let D=[];for(let R=0,W=B-A+1;R{if(F.preventDefault(),this._model.options.renderMinimap!==0&&!!this._lastRenderData){if(this._model.options.size!=="proportional"){if(F.leftButton&&this._lastRenderData){const Y=b.getDomNodePagePosition(this._slider.domNode),ee=Y.top+Y.height/2;this._startSliderDragging(F.buttons,F.posx,ee,F.posy,this._lastRenderData.renderedLayout)}return}const R=this._model.options.minimapLineHeight,W=this._model.options.canvasInnerHeight/this._model.options.canvasOuterHeight*F.browserEvent.offsetY;let K=Math.floor(W/R)+this._lastRenderData.renderedLayout.startLineNumber;K=Math.min(K,this._model.getLineCount()),this._model.revealLineNumber(K)}}),this._sliderMouseMoveMonitor=new M.GlobalMouseMoveMonitor,this._sliderMouseDownListener=b.addStandardDisposableListener(this._slider.domNode,"mousedown",F=>{F.preventDefault(),F.stopPropagation(),F.leftButton&&this._lastRenderData&&this._startSliderDragging(F.buttons,F.posx,F.posy,F.posy,this._lastRenderData.renderedLayout)}),this._gestureDisposable=n.Gesture.addTarget(this._domNode.domNode),this._sliderTouchStartListener=b.addDisposableListener(this._domNode.domNode,n.EventType.Start,F=>{F.preventDefault(),F.stopPropagation(),this._lastRenderData&&(this._slider.toggleClassName("active",!0),this._gestureInProgress=!0,this.scrollDueToTouchEvent(F))},{passive:!1}),this._sliderTouchMoveListener=b.addDisposableListener(this._domNode.domNode,n.EventType.Change,F=>{F.preventDefault(),F.stopPropagation(),this._lastRenderData&&this._gestureInProgress&&this.scrollDueToTouchEvent(F)},{passive:!1}),this._sliderTouchEndListener=b.addStandardDisposableListener(this._domNode.domNode,n.EventType.End,F=>{F.preventDefault(),F.stopPropagation(),this._gestureInProgress=!1,this._slider.toggleClassName("active",!1)})}_startSliderDragging(A,B,F,D,R){this._slider.toggleClassName("active",!0);const W=(x,K)=>{const Y=Math.abs(K-B);if(S.isWindows&&Y>m){this._model.setScrollTop(R.scrollTop);return}const ee=x-F;this._model.setScrollTop(R.getDesiredScrollTopFromDelta(ee))};D!==F&&W(D,B),this._sliderMouseMoveMonitor.startMonitoring(this._slider.domNode,A,M.standardMouseMoveMerger,x=>W(x.posy,x.posx),()=>{this._slider.toggleClassName("active",!1)})}scrollDueToTouchEvent(A){const B=this._domNode.domNode.getBoundingClientRect().top,F=this._lastRenderData.renderedLayout.getDesiredScrollTopFromTouchLocation(A.pageY-B);this._model.setScrollTop(F)}dispose(){this._mouseDownListener.dispose(),this._sliderMouseMoveMonitor.dispose(),this._sliderMouseDownListener.dispose(),this._gestureDisposable.dispose(),this._sliderTouchStartListener.dispose(),this._sliderTouchMoveListener.dispose(),this._sliderTouchEndListener.dispose(),super.dispose()}_getMinimapDomNodeClassName(){return this._model.options.showSlider==="always"?"minimap slider-always":"minimap slider-mouseover"}getDomNode(){return this._domNode}_applyLayout(){this._domNode.setLeft(this._model.options.minimapLeft),this._domNode.setWidth(this._model.options.minimapWidth),this._domNode.setHeight(this._model.options.minimapHeight),this._shadow.setHeight(this._model.options.minimapHeight),this._canvas.setWidth(this._model.options.canvasOuterWidth),this._canvas.setHeight(this._model.options.canvasOuterHeight),this._canvas.domNode.width=this._model.options.canvasInnerWidth,this._canvas.domNode.height=this._model.options.canvasInnerHeight,this._decorationsCanvas.setWidth(this._model.options.canvasOuterWidth),this._decorationsCanvas.setHeight(this._model.options.canvasOuterHeight),this._decorationsCanvas.domNode.width=this._model.options.canvasInnerWidth,this._decorationsCanvas.domNode.height=this._model.options.canvasInnerHeight,this._slider.setWidth(this._model.options.minimapWidth)}_getBuffer(){return this._buffers||this._model.options.canvasInnerWidth>0&&this._model.options.canvasInnerHeight>0&&(this._buffers=new I(this._canvas.domNode.getContext("2d"),this._model.options.canvasInnerWidth,this._model.options.canvasInnerHeight,this._model.options.backgroundColor)),this._buffers?this._buffers.getBuffer():null}onDidChangeOptions(){this._lastRenderData=null,this._buffers=null,this._applyLayout(),this._domNode.setClassName(this._getMinimapDomNodeClassName())}onSelectionChanged(){return this._renderDecorations=!0,!0}onDecorationsChanged(){return this._renderDecorations=!0,!0}onFlushed(){return this._lastRenderData=null,!0}onLinesChanged(A,B){return this._lastRenderData?this._lastRenderData.onLinesChanged(A,B):!1}onLinesDeleted(A,B){return this._lastRenderData&&this._lastRenderData.onLinesDeleted(A,B),!0}onLinesInserted(A,B){return this._lastRenderData&&this._lastRenderData.onLinesInserted(A,B),!0}onScrollChanged(){return this._renderDecorations=!0,!0}onThemeChanged(){return this._selectionColor=this._theme.getColor(u.minimapSelection),this._renderDecorations=!0,!0}onTokensChanged(A){return this._lastRenderData?this._lastRenderData.onTokensChanged(A):!1}onTokensColorsChanged(){return this._lastRenderData=null,this._buffers=null,!0}onZonesChanged(){return this._lastRenderData=null,!0}render(A){if(this._model.options.renderMinimap===0){this._shadow.setClassName("minimap-shadow-hidden"),this._sliderHorizontal.setWidth(0),this._sliderHorizontal.setHeight(0);return}A.scrollLeft+A.viewportWidth>=A.scrollWidth?this._shadow.setClassName("minimap-shadow-hidden"):this._shadow.setClassName("minimap-shadow-visible");const F=v.create(this._model.options,A.viewportStartLineNumber,A.viewportEndLineNumber,A.viewportStartLineNumberVerticalOffset,A.viewportHeight,A.viewportContainsWhitespaceGaps,this._model.getLineCount(),this._model.getRealLineCount(),A.scrollTop,A.scrollHeight,this._lastRenderData?this._lastRenderData.renderedLayout:null);this._slider.setDisplay(F.sliderNeeded?"block":"none"),this._slider.setTop(F.sliderTop),this._slider.setHeight(F.sliderHeight);const D=A.scrollLeft/this._model.options.typicalHalfwidthCharacterWidth,R=Math.min(this._model.options.minimapWidth,Math.round(D*this._model.options.minimapCharWidth/this._model.options.pixelRatio));this._sliderHorizontal.setLeft(R),this._sliderHorizontal.setWidth(this._model.options.minimapWidth-R),this._sliderHorizontal.setTop(0),this._sliderHorizontal.setHeight(F.sliderHeight),this.renderDecorations(F),this._lastRenderData=this.renderLines(F)}renderDecorations(A){if(this._renderDecorations){this._renderDecorations=!1;const B=this._model.getSelections(),F=this._model.getMinimapDecorationsInViewport(A.startLineNumber,A.endLineNumber),{canvasInnerWidth:D,canvasInnerHeight:R}=this._model.options,W=this._model.options.minimapLineHeight,x=this._model.options.minimapCharWidth,K=this._model.getOptions().tabSize,Y=this._decorationsCanvas.domNode.getContext("2d");Y.clearRect(0,0,D,R);const ee=new Map;for(let se=0;sethis._model.options.canvasInnerHeight)){let ne=B.get(W);const le=!ne;if(!ne){const $=this._model.getLineContent(W);ne=[p.MINIMAP_GUTTER_WIDTH];for(let ie=1;ie<$.length+1;ie++){const oe=$.charCodeAt(ie-1),ae=oe===9?Y*ee:C.isFullWidthCharacter(oe)?2*ee:ee;ne[ie]=ne[ie-1]+ae}B.set(W,ne)}const{startColumn:X,endColumn:z,startLineNumber:P,endLineNumber:V}=F,U=P===W?ne[X-1]:p.MINIMAP_GUTTER_WIDTH,H=V>W?ne.length-1:z-1;if(H>0){const $=ne[H]-U||2;this.renderDecoration(A,D,U,se,$,x)}le&&this.renderLineHighlight(A,D,se,x)}}renderLineHighlight(A,B,F,D){A.fillStyle=B&&B.transparent(.5).toString()||"",A.fillRect(p.MINIMAP_GUTTER_WIDTH,F,A.canvas.width,D)}renderDecoration(A,B,F,D,R,W){A.fillStyle=B&&B.toString()||"",A.fillRect(F,D,R,W)}renderLines(A){const B=A.startLineNumber,F=A.endLineNumber,D=this._model.options.minimapLineHeight;if(this._lastRenderData&&this._lastRenderData.linesEquals(A)){const Z=this._lastRenderData._get();return new L(A,Z.imageData,Z.lines)}const R=this._getBuffer();if(!R)return null;let[W,x,K]=T._renderUntouchedLines(R,B,F,D,this._lastRenderData);const Y=this._model.getMinimapLinesRenderingData(B,F,K),ee=this._model.getOptions().tabSize,se=this._model.options.backgroundColor,ne=this._model.tokensColorTracker,le=ne.backgroundIsLight(),X=this._model.options.renderMinimap,z=this._model.options.charRenderer(),P=this._model.options.fontScale,V=this._model.options.minimapCharWidth,H=(X===1?2:2+1)*P,$=D>H?Math.floor((D-H)/2):0;let ie=0;const oe=[];for(let Z=0,ue=F-B+1;Z=0&&teP)return;const te=X.charCodeAt(H);if(te===9){const Z=ee-(H+$)%ee;$+=Z-1,U+=Z*R}else if(te===32)U+=R;else{const Z=C.isFullWidthCharacter(te)?2:1;for(let ue=0;ueP)return}}}}}r.registerThemingParticipant((O,A)=>{const B=O.getColor(u.minimapBackground);B&&A.addRule(`.monaco-editor .minimap > canvas { opacity: ${B.rgba.a}; will-change: opacity; }`);const F=O.getColor(u.minimapSliderBackground);F&&A.addRule(`.monaco-editor .minimap-slider .minimap-slider-horizontal { background: ${F}; }`);const D=O.getColor(u.minimapSliderHoverBackground);D&&A.addRule(`.monaco-editor .minimap-slider:hover .minimap-slider-horizontal { background: ${D}; }`);const R=O.getColor(u.minimapSliderActiveBackground);R&&A.addRule(`.monaco-editor .minimap-slider.active .minimap-slider-horizontal { background: ${R}; }`);const W=O.getColor(u.scrollbarShadow);W&&A.addRule(`.monaco-editor .minimap-shadow-visible { box-shadow: ${W} -6px 0 6px -6px inset; }`)})}),define(Q[586],J([0,1,30,45,22,11,329]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ScrollDecorationViewPart=void 0;class S extends N.ViewPart{constructor(d){super(d);this._scrollTop=0,this._width=0,this._updateWidth(),this._shouldShow=!1;const p=this._context.configuration.options.get(87);this._useShadows=p.useShadows,this._domNode=b.createFastDomNode(document.createElement("div")),this._domNode.setAttribute("role","presentation"),this._domNode.setAttribute("aria-hidden","true")}dispose(){super.dispose()}_updateShouldShow(){const d=this._useShadows&&this._scrollTop>0;return this._shouldShow!==d?(this._shouldShow=d,!0):!1}getDomNode(){return this._domNode}_updateWidth(){const g=this._context.configuration.options.get(124);g.minimap.renderMinimap===0||g.minimap.minimapWidth>0&&g.minimap.minimapLeft===0?this._width=g.width:this._width=g.width-g.minimap.minimapWidth-g.verticalScrollbarWidth}onConfigurationChanged(d){const p=this._context.configuration.options.get(87);return this._useShadows=p.useShadows,this._updateWidth(),this._updateShouldShow(),!0}onScrollChanged(d){return this._scrollTop=d.scrollTop,this._updateShouldShow()}prepareRender(d){}render(d){this._domNode.setWidth(this._width),this._domNode.setClassName(this._shouldShow?"scroll-decoration":"")}}e.ScrollDecorationViewPart=S,w.registerThemingParticipant((C,d)=>{const g=C.getColor(M.scrollbarShadow);g&&d.addRule(`.monaco-editor .scroll-decoration { box-shadow: ${g} 0 6px 6px -6px inset; }`)})}),define(Q[587],J([0,1,94,22,11,330]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SelectionsOverlay=void 0;class w{constructor(o){this.left=o.left,this.width=o.width,this.startStyle=null,this.endStyle=null}}class S{constructor(o,s){this.lineNumber=o,this.ranges=s}}function C(c){return new w(c)}function d(c){return new S(c.lineNumber,c.ranges.map(C))}class g extends b.DynamicViewOverlay{constructor(o){super();this._previousFrameVisibleRangesWithStyle=[],this._context=o;const s=this._context.configuration.options;this._lineHeight=s.get(53),this._roundedSelection=s.get(85),this._typicalHalfwidthCharacterWidth=s.get(38).typicalHalfwidthCharacterWidth,this._selections=[],this._renderResult=null,this._context.addEventHandler(this)}dispose(){this._context.removeEventHandler(this),this._renderResult=null,super.dispose()}onConfigurationChanged(o){const s=this._context.configuration.options;return this._lineHeight=s.get(53),this._roundedSelection=s.get(85),this._typicalHalfwidthCharacterWidth=s.get(38).typicalHalfwidthCharacterWidth,!0}onCursorStateChanged(o){return this._selections=o.selections.slice(0),!0}onDecorationsChanged(o){return!0}onFlushed(o){return!0}onLinesChanged(o){return!0}onLinesDeleted(o){return!0}onLinesInserted(o){return!0}onScrollChanged(o){return o.scrollTopChanged}onZonesChanged(o){return!0}_visibleRangesHaveGaps(o){for(let s=0,a=o.length;s1)return!0;return!1}_enrichVisibleRangesWithStyle(o,s,a){const u=this._typicalHalfwidthCharacterWidth/4;let r=null,i=null;if(a&&a.length>0&&s.length>0){const n=s[0].lineNumber;if(n===o.startLineNumber)for(let l=0;!r&&l=0;l--)a[l].lineNumber===t&&(i=a[l].ranges[0]);r&&!r.startStyle&&(r=null),i&&!i.startStyle&&(i=null)}for(let n=0,t=s.length;n0){const v=s[n-1].ranges[0].left,y=s[n-1].ranges[0].left+s[n-1].ranges[0].width;p(h-v)v&&(_.top=1),p(m-y)'}_actualRenderOneSelection(o,s,a,u){if(u.length!==0){const r=!!u[0].ranges[0].startStyle,i=this._lineHeight.toString(),n=(this._lineHeight-1).toString(),t=u[0].lineNumber,l=u[u.length-1].lineNumber;for(let h=0,m=u.length;h1,l)}this._previousFrameVisibleRangesWithStyle=r,this._renderResult=s.map(([i,n])=>i+n)}render(o,s){if(!this._renderResult)return"";const a=s-o;return a<0||a>=this._renderResult.length?"":this._renderResult[a]}}e.SelectionsOverlay=g,g.SELECTION_CLASS_NAME="selected-text",g.SELECTION_TOP_LEFT="top-left-radius",g.SELECTION_BOTTOM_LEFT="bottom-left-radius",g.SELECTION_TOP_RIGHT="top-right-radius",g.SELECTION_BOTTOM_RIGHT="bottom-right-radius",g.EDITOR_BACKGROUND_CLASS_NAME="monaco-editor-background",g.ROUNDED_PIECE_WIDTH=10,M.registerThemingParticipant((c,o)=>{const s=c.getColor(N.editorSelectionBackground);s&&o.addRule(`.monaco-editor .focused .selected-text { background-color: ${s}; }`);const a=c.getColor(N.editorInactiveSelection);a&&o.addRule(`.monaco-editor .selected-text { background-color: ${a}; }`);const u=c.getColor(N.editorSelectionForeground);u&&!u.isTransparent()&&o.addRule(`.monaco-editor .view-line span.inline-selected-text { color: ${u}; }`)});function p(c){return c<0?-c:c}}),define(Q[49],J([0,1,460,29,22,11]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.overviewRulerInfo=e.overviewRulerWarning=e.overviewRulerError=e.overviewRulerRangeHighlight=e.editorUnnecessaryCodeOpacity=e.editorUnnecessaryCodeBorder=e.editorGutter=e.editorOverviewRulerBackground=e.editorOverviewRulerBorder=e.editorBracketMatchBorder=e.editorBracketMatchBackground=e.editorCodeLensForeground=e.editorRuler=e.editorActiveLineNumber=e.editorLineNumbers=e.editorActiveIndentGuides=e.editorIndentGuides=e.editorWhitespaces=e.editorCursorBackground=e.editorCursorForeground=e.editorSymbolHighlightBorder=e.editorSymbolHighlight=e.editorRangeHighlightBorder=e.editorRangeHighlight=e.editorLineHighlightBorder=e.editorLineHighlight=void 0,e.editorLineHighlight=M.registerColor("editor.lineHighlightBackground",{dark:null,light:null,hc:null},b.localize(0,null)),e.editorLineHighlightBorder=M.registerColor("editor.lineHighlightBorder",{dark:"#282828",light:"#eeeeee",hc:"#f38518"},b.localize(1,null)),e.editorRangeHighlight=M.registerColor("editor.rangeHighlightBackground",{dark:"#ffffff0b",light:"#fdff0033",hc:null},b.localize(2,null),!0),e.editorRangeHighlightBorder=M.registerColor("editor.rangeHighlightBorder",{dark:null,light:null,hc:M.activeContrastBorder},b.localize(3,null),!0),e.editorSymbolHighlight=M.registerColor("editor.symbolHighlightBackground",{dark:M.editorFindMatchHighlight,light:M.editorFindMatchHighlight,hc:null},b.localize(4,null),!0),e.editorSymbolHighlightBorder=M.registerColor("editor.symbolHighlightBorder",{dark:null,light:null,hc:M.activeContrastBorder},b.localize(5,null),!0),e.editorCursorForeground=M.registerColor("editorCursor.foreground",{dark:"#AEAFAD",light:N.Color.black,hc:N.Color.white},b.localize(6,null)),e.editorCursorBackground=M.registerColor("editorCursor.background",null,b.localize(7,null)),e.editorWhitespaces=M.registerColor("editorWhitespace.foreground",{dark:"#e3e4e229",light:"#33333333",hc:"#e3e4e229"},b.localize(8,null)),e.editorIndentGuides=M.registerColor("editorIndentGuide.background",{dark:e.editorWhitespaces,light:e.editorWhitespaces,hc:e.editorWhitespaces},b.localize(9,null)),e.editorActiveIndentGuides=M.registerColor("editorIndentGuide.activeBackground",{dark:e.editorWhitespaces,light:e.editorWhitespaces,hc:e.editorWhitespaces},b.localize(10,null)),e.editorLineNumbers=M.registerColor("editorLineNumber.foreground",{dark:"#858585",light:"#237893",hc:N.Color.white},b.localize(11,null));const S=M.registerColor("editorActiveLineNumber.foreground",{dark:"#c6c6c6",light:"#0B216F",hc:M.activeContrastBorder},b.localize(12,null),!1,b.localize(13,null));e.editorActiveLineNumber=M.registerColor("editorLineNumber.activeForeground",{dark:S,light:S,hc:S},b.localize(14,null)),e.editorRuler=M.registerColor("editorRuler.foreground",{dark:"#5A5A5A",light:N.Color.lightgrey,hc:N.Color.white},b.localize(15,null)),e.editorCodeLensForeground=M.registerColor("editorCodeLens.foreground",{dark:"#999999",light:"#999999",hc:"#999999"},b.localize(16,null)),e.editorBracketMatchBackground=M.registerColor("editorBracketMatch.background",{dark:"#0064001a",light:"#0064001a",hc:"#0064001a"},b.localize(17,null)),e.editorBracketMatchBorder=M.registerColor("editorBracketMatch.border",{dark:"#888",light:"#B9B9B9",hc:M.contrastBorder},b.localize(18,null)),e.editorOverviewRulerBorder=M.registerColor("editorOverviewRuler.border",{dark:"#7f7f7f4d",light:"#7f7f7f4d",hc:"#7f7f7f4d"},b.localize(19,null)),e.editorOverviewRulerBackground=M.registerColor("editorOverviewRuler.background",null,b.localize(20,null)),e.editorGutter=M.registerColor("editorGutter.background",{dark:M.editorBackground,light:M.editorBackground,hc:M.editorBackground},b.localize(21,null)),e.editorUnnecessaryCodeBorder=M.registerColor("editorUnnecessaryCode.border",{dark:null,light:null,hc:N.Color.fromHex("#fff").transparent(.8)},b.localize(22,null)),e.editorUnnecessaryCodeOpacity=M.registerColor("editorUnnecessaryCode.opacity",{dark:N.Color.fromHex("#000a"),light:N.Color.fromHex("#0007"),hc:null},b.localize(23,null));const C=new N.Color(new N.RGBA(0,122,204,.6));e.overviewRulerRangeHighlight=M.registerColor("editorOverviewRuler.rangeHighlightForeground",{dark:C,light:C,hc:C},b.localize(24,null),!0),e.overviewRulerError=M.registerColor("editorOverviewRuler.errorForeground",{dark:new N.Color(new N.RGBA(255,18,18,.7)),light:new N.Color(new N.RGBA(255,18,18,.7)),hc:new N.Color(new N.RGBA(255,50,50,1))},b.localize(25,null)),e.overviewRulerWarning=M.registerColor("editorOverviewRuler.warningForeground",{dark:M.editorWarningForeground,light:M.editorWarningForeground,hc:M.editorWarningBorder},b.localize(26,null)),e.overviewRulerInfo=M.registerColor("editorOverviewRuler.infoForeground",{dark:M.editorInfoForeground,light:M.editorInfoForeground,hc:M.editorInfoBorder},b.localize(27,null)),w.registerThemingParticipant((d,g)=>{const p=d.getColor(M.editorBackground);p&&g.addRule(`.monaco-editor, .monaco-editor-background, .monaco-editor .inputarea.ime-input { background-color: ${p}; }`);const c=d.getColor(M.editorForeground);c&&g.addRule(`.monaco-editor, .monaco-editor .inputarea.ime-input { color: ${c}; }`);const o=d.getColor(e.editorGutter);o&&g.addRule(`.monaco-editor .margin { background-color: ${o}; }`);const s=d.getColor(e.editorRangeHighlight);s&&g.addRule(`.monaco-editor .rangeHighlight { background-color: ${s}; }`);const a=d.getColor(e.editorRangeHighlightBorder);a&&g.addRule(`.monaco-editor .rangeHighlight { border: 1px ${d.type==="hc"?"dotted":"solid"} ${a}; }`);const u=d.getColor(e.editorSymbolHighlight);u&&g.addRule(`.monaco-editor .symbolHighlight { background-color: ${u}; }`);const r=d.getColor(e.editorSymbolHighlightBorder);r&&g.addRule(`.monaco-editor .symbolHighlight { border: 1px ${d.type==="hc"?"dotted":"solid"} ${r}; }`);const i=d.getColor(e.editorWhitespaces);i&&(g.addRule(`.monaco-editor .mtkw { color: ${i} !important; }`),g.addRule(`.monaco-editor .mtkz { color: ${i} !important; }`))})}),define(Q[588],J([0,1,94,49,19,11,21,318]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CurrentLineMarginHighlightOverlay=e.CurrentLineHighlightOverlay=e.AbstractLineHighlightOverlay=void 0;let C=!0;class d extends b.DynamicViewOverlay{constructor(o){super();this._context=o;const s=this._context.configuration.options,a=s.get(124);this._lineHeight=s.get(53),this._renderLineHighlight=s.get(80),this._renderLineHighlightOnlyWhenFocus=s.get(81),this._contentLeft=a.contentLeft,this._contentWidth=a.contentWidth,this._selectionIsEmpty=!0,this._focused=!1,this._cursorLineNumbers=[1],this._selections=[new S.Selection(1,1,1,1)],this._renderData=null,this._context.addEventHandler(this)}dispose(){this._context.removeEventHandler(this),super.dispose()}_readFromSelections(){let o=!1;const s=C?this._selections.slice(0,1):this._selections,a=s.map(r=>r.positionLineNumber);a.sort((r,i)=>r-i),M.equals(this._cursorLineNumbers,a)||(this._cursorLineNumbers=a,o=!0);const u=s.every(r=>r.isEmpty());return this._selectionIsEmpty!==u&&(this._selectionIsEmpty=u,o=!0),o}onThemeChanged(o){return this._readFromSelections()}onConfigurationChanged(o){const s=this._context.configuration.options,a=s.get(124);return this._lineHeight=s.get(53),this._renderLineHighlight=s.get(80),this._renderLineHighlightOnlyWhenFocus=s.get(81),this._contentLeft=a.contentLeft,this._contentWidth=a.contentWidth,!0}onCursorStateChanged(o){return this._selections=o.selections,this._readFromSelections()}onFlushed(o){return!0}onLinesDeleted(o){return!0}onLinesInserted(o){return!0}onScrollChanged(o){return o.scrollWidthChanged||o.scrollTopChanged}onZonesChanged(o){return!0}onFocusChanged(o){return this._renderLineHighlightOnlyWhenFocus?(this._focused=o.isFocused,!0):!1}prepareRender(o){if(!this._shouldRenderThis()){this._renderData=null;return}const s=this._renderOne(o),a=o.visibleRange.startLineNumber,u=o.visibleRange.endLineNumber,r=this._cursorLineNumbers.length;let i=0;const n=[];for(let t=a;t<=u;t++){const l=t-a;for(;i=this._renderData.length?"":this._renderData[a]}}e.AbstractLineHighlightOverlay=d;class g extends d{_renderOne(o){return`
    `}_shouldRenderThis(){return(this._renderLineHighlight==="line"||this._renderLineHighlight==="all")&&this._selectionIsEmpty&&(!this._renderLineHighlightOnlyWhenFocus||this._focused)}_shouldRenderOther(){return(this._renderLineHighlight==="gutter"||this._renderLineHighlight==="all")&&(!this._renderLineHighlightOnlyWhenFocus||this._focused)}}e.CurrentLineHighlightOverlay=g;class p extends d{_renderOne(o){return`
    `}_shouldRenderMargin(){return(this._renderLineHighlight==="gutter"||this._renderLineHighlight==="all")&&(!this._renderLineHighlightOnlyWhenFocus||this._focused)}_shouldRenderThis(){return!0}_shouldRenderOther(){return(this._renderLineHighlight==="line"||this._renderLineHighlight==="all")&&this._selectionIsEmpty&&(!this._renderLineHighlightOnlyWhenFocus||this._focused)}}e.CurrentLineMarginHighlightOverlay=p,w.registerThemingParticipant((c,o)=>{C=!1;const s=c.getColor(N.editorLineHighlight);if(s&&(o.addRule(`.monaco-editor .view-overlays .current-line { background-color: ${s}; }`),o.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { background-color: ${s}; border: none; }`)),!s||s.isTransparent()||c.defines(N.editorLineHighlightBorder)){const a=c.getColor(N.editorLineHighlightBorder);a&&(C=!0,o.addRule(`.monaco-editor .view-overlays .current-line { border: 2px solid ${a}; }`),o.addRule(`.monaco-editor .margin-view-overlays .current-line-margin { border: 2px solid ${a}; }`),c.type==="hc"&&(o.addRule(".monaco-editor .view-overlays .current-line { border-width: 1px; }"),o.addRule(".monaco-editor .margin-view-overlays .current-line-margin { border-width: 1px; }")))}})}),define(Q[589],J([0,1,94,14,49,11,321]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IndentGuidesOverlay=void 0;class S extends b.DynamicViewOverlay{constructor(d){super();this._context=d,this._primaryLineNumber=0;const g=this._context.configuration.options,p=g.get(125),c=g.get(38);this._lineHeight=g.get(53),this._spaceWidth=c.spaceWidth,this._enabled=g.get(78),this._activeIndentEnabled=g.get(47),this._maxIndentLeft=p.wrappingColumn===-1?-1:p.wrappingColumn*c.typicalHalfwidthCharacterWidth,this._renderResult=null,this._context.addEventHandler(this)}dispose(){this._context.removeEventHandler(this),this._renderResult=null,super.dispose()}onConfigurationChanged(d){const g=this._context.configuration.options,p=g.get(125),c=g.get(38);return this._lineHeight=g.get(53),this._spaceWidth=c.spaceWidth,this._enabled=g.get(78),this._activeIndentEnabled=g.get(47),this._maxIndentLeft=p.wrappingColumn===-1?-1:p.wrappingColumn*c.typicalHalfwidthCharacterWidth,!0}onCursorStateChanged(d){const g=d.selections[0],p=g.isEmpty()?g.positionLineNumber:0;return this._primaryLineNumber!==p?(this._primaryLineNumber=p,!0):!1}onDecorationsChanged(d){return!0}onFlushed(d){return!0}onLinesChanged(d){return!0}onLinesDeleted(d){return!0}onLinesInserted(d){return!0}onScrollChanged(d){return d.scrollTopChanged}onZonesChanged(d){return!0}onLanguageConfigurationChanged(d){return!0}prepareRender(d){if(!this._enabled){this._renderResult=null;return}const g=d.visibleRange.startLineNumber,p=d.visibleRange.endLineNumber,{indentSize:c}=this._context.model.getTextModelOptions(),o=c*this._spaceWidth,s=d.scrollWidth,a=this._lineHeight,u=this._context.model.getLinesIndentGuides(g,p);let r=0,i=0,n=0;if(this._activeIndentEnabled&&this._primaryLineNumber){const l=this._context.model.getActiveIndentGuide(this._primaryLineNumber,g,p);r=l.startLineNumber,i=l.endLineNumber,n=l.indent}const t=[];for(let l=g;l<=p;l++){const h=r<=l&&l<=i,m=l-g,_=u[m];let f="";if(_>=1){const v=d.visibleRangeForPosition(new N.Position(l,1));let y=v?v.left:0;for(let L=1;L<=_&&(f+=`
    `,y+=o,!(y>s||this._maxIndentLeft>0&&y>this._maxIndentLeft));L++);}t[m]=f}this._renderResult=t}render(d,g){if(!this._renderResult)return"";const p=g-d;return p<0||p>=this._renderResult.length?"":this._renderResult[p]}}e.IndentGuidesOverlay=S,w.registerThemingParticipant((C,d)=>{const g=C.getColor(M.editorIndentGuides);g&&d.addRule(`.monaco-editor .lines-content .cigr { box-shadow: 1px 0 0 0 ${g} inset; }`);const p=C.getColor(M.editorActiveIndentGuides)||g;p&&d.addRule(`.monaco-editor .lines-content .cigra { box-shadow: 1px 0 0 0 ${p} inset; }`)})}),define(Q[257],J([0,1,17,94,14,49,11,322]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LineNumbersOverlay=void 0;class C extends N.DynamicViewOverlay{constructor(g){super();this._context=g,this._readConfig(),this._lastCursorModelPosition=new M.Position(1,1),this._renderResult=null,this._activeLineNumber=1,this._context.addEventHandler(this)}_readConfig(){const g=this._context.configuration.options;this._lineHeight=g.get(53);const p=g.get(54);this._renderLineNumbers=p.renderType,this._renderCustomLineNumbers=p.renderFn,this._renderFinalNewline=g.get(79);const c=g.get(124);this._lineNumbersLeft=c.lineNumbersLeft,this._lineNumbersWidth=c.lineNumbersWidth}dispose(){this._context.removeEventHandler(this),this._renderResult=null,super.dispose()}onConfigurationChanged(g){return this._readConfig(),!0}onCursorStateChanged(g){const p=g.selections[0].getPosition();this._lastCursorModelPosition=this._context.model.coordinatesConverter.convertViewPositionToModelPosition(p);let c=!1;return this._activeLineNumber!==p.lineNumber&&(this._activeLineNumber=p.lineNumber,c=!0),(this._renderLineNumbers===2||this._renderLineNumbers===3)&&(c=!0),c}onFlushed(g){return!0}onLinesChanged(g){return!0}onLinesDeleted(g){return!0}onLinesInserted(g){return!0}onScrollChanged(g){return g.scrollTopChanged}onZonesChanged(g){return!0}_getLineRenderLineNumber(g){const p=this._context.model.coordinatesConverter.convertViewPositionToModelPosition(new M.Position(g,1));if(p.column!==1)return"";const c=p.lineNumber;if(this._renderCustomLineNumbers)return this._renderCustomLineNumbers(c);if(this._renderLineNumbers===2){const o=Math.abs(this._lastCursorModelPosition.lineNumber-c);return o===0?''+c+"":String(o)}return this._renderLineNumbers===3?this._lastCursorModelPosition.lineNumber===c||c%10==0?String(c):"":String(c)}prepareRender(g){if(this._renderLineNumbers===0){this._renderResult=null;return}const p=b.isLinux?this._lineHeight%2==0?" lh-even":" lh-odd":"",c=g.visibleRange.startLineNumber,o=g.visibleRange.endLineNumber,s='
    ',a=this._context.model.getLineCount(),u=[];for(let r=c;r<=o;r++){const i=r-c;if(!this._renderFinalNewline&&r===a&&this._context.model.getLineLength(r)===0){u[i]="";continue}const n=this._getLineRenderLineNumber(r);n?r===this._activeLineNumber?u[i]='
    '+n+"
    ":u[i]=s+n+"
    ":u[i]=""}this._renderResult=u}render(g,p){if(!this._renderResult)return"";const c=p-g;return c<0||c>=this._renderResult.length?"":this._renderResult[c]}}e.LineNumbersOverlay=C,C.CLASS_NAME="line-numbers",S.registerThemingParticipant((d,g)=>{const p=d.getColor(w.editorLineNumbers);p&&g.addRule(`.monaco-editor .line-numbers { color: ${p}; }`);const c=d.getColor(w.editorActiveLineNumber);c&&g.addRule(`.monaco-editor .line-numbers.active-line-number { color: ${c}; }`)})}),define(Q[590],J([0,1,445,35,30,17,8,69,164,214,45,257,223,38,106,14,3,21,124,317]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TextAreaHandler=void 0;class t{constructor(y,L,I){this.top=y,this.left=L,this.width=I}setWidth(y){return new t(this.top,this.left,y)}}const l=N.isFirefox;class h extends p.ViewPart{constructor(y,L,I){super(y);this._primaryCursorPosition=new u.Position(1,1),this._primaryCursorVisibleRange=null,this._viewController=L,this._viewHelper=I,this._scrollLeft=0,this._scrollTop=0;const k=this._context.configuration.options,E=k.get(124);this._setAccessibilityOptions(k),this._contentLeft=E.contentLeft,this._contentWidth=E.contentWidth,this._contentHeight=E.height,this._fontInfo=k.get(38),this._lineHeight=k.get(53),this._emptySelectionClipboard=k.get(28),this._copyWithSyntaxHighlighting=k.get(18),this._visibleTextArea=null,this._selections=[new i.Selection(1,1,1,1)],this._modelSelections=[new i.Selection(1,1,1,1)],this._lastRenderPosition=null,this.textArea=M.createFastDomNode(document.createElement("textarea")),p.PartFingerprints.write(this.textArea,6),this.textArea.setClassName(`inputarea ${n.MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`),this.textArea.setAttribute("wrap","off"),this.textArea.setAttribute("autocorrect","off"),this.textArea.setAttribute("autocapitalize","off"),this.textArea.setAttribute("autocomplete","off"),this.textArea.setAttribute("spellcheck","false"),this.textArea.setAttribute("aria-label",this._getAriaLabel(k)),this.textArea.setAttribute("tabindex",String(k.get(107))),this.textArea.setAttribute("role","textbox"),this.textArea.setAttribute("aria-roledescription",b.localize(0,null)),this.textArea.setAttribute("aria-multiline","true"),this.textArea.setAttribute("aria-haspopup","false"),this.textArea.setAttribute("aria-autocomplete","both"),w.isWeb&&k.get(75)&&this.textArea.setAttribute("readonly","true"),this.textAreaCover=M.createFastDomNode(document.createElement("div")),this.textAreaCover.setPosition("absolute");const T={getLineCount:()=>this._context.model.getLineCount(),getLineMaxColumn:A=>this._context.model.getLineMaxColumn(A),getValueInRange:(A,B)=>this._context.model.getValueInRange(A,B)},O={getDataToCopy:A=>{const B=this._context.model.getPlainTextToCopy(this._modelSelections,this._emptySelectionClipboard,w.isWindows),F=this._context.model.getEOL(),D=this._emptySelectionClipboard&&this._modelSelections.length===1&&this._modelSelections[0].isEmpty(),R=Array.isArray(B)?B:null,W=Array.isArray(B)?B.join(F):B;let x,K=null;if(A&&(d.CopyOptions.forceCopyWithSyntaxHighlighting||this._copyWithSyntaxHighlighting&&W.length<65536)){const Y=this._context.model.getRichTextToCopy(this._modelSelections,this._emptySelectionClipboard);Y&&(x=Y.html,K=Y.mode)}return{isFromEmptySelection:D,multicursorText:R,text:W,html:x,mode:K}},getScreenReaderContent:A=>{if(this._accessibilitySupport===1){if(w.isMacintosh){const B=this._selections[0];if(B.isEmpty()){const F=B.getStartPosition();let D=this._getWordBeforePosition(F);if(D.length===0&&(D=this._getCharacterBeforePosition(F)),D.length>0)return new g.TextAreaState(D,D.length,D.length,F,F)}}return g.TextAreaState.EMPTY}if(N.isAndroid){const B=this._selections[0];if(B.isEmpty()){const F=B.getStartPosition(),[D,R]=this._getAndroidWordAtPosition(F);if(D.length>0)return new g.TextAreaState(D,R,R,F,F)}return g.TextAreaState.EMPTY}return g.PagedScreenReaderStrategy.fromEditorSelection(A,T,this._selections[0],this._accessibilityPageSize,this._accessibilitySupport===0)},deduceModelPosition:(A,B,F)=>this._context.model.deduceModelPositionRelativeToViewPosition(A,B,F)};this._textAreaInput=this._register(new d.TextAreaInput(O,this.textArea)),this._register(this._textAreaInput.onKeyDown(A=>{this._viewController.emitKeyDown(A)})),this._register(this._textAreaInput.onKeyUp(A=>{this._viewController.emitKeyUp(A)})),this._register(this._textAreaInput.onPaste(A=>{let B=!1,F=null,D=null;A.metadata&&(B=this._emptySelectionClipboard&&!!A.metadata.isFromEmptySelection,F=typeof A.metadata.multicursorText!="undefined"?A.metadata.multicursorText:null,D=A.metadata.mode),this._viewController.paste(A.text,B,F,D)})),this._register(this._textAreaInput.onCut(()=>{this._viewController.cut()})),this._register(this._textAreaInput.onType(A=>{A.replacePrevCharCnt||A.replaceNextCharCnt||A.positionDelta?(g._debugComposition&&console.log(` => compositionType: <<${A.text}>>, ${A.replacePrevCharCnt}, ${A.replaceNextCharCnt}, ${A.positionDelta}`),this._viewController.compositionType(A.text,A.replacePrevCharCnt,A.replaceNextCharCnt,A.positionDelta)):(g._debugComposition&&console.log(` => type: <<${A.text}>>`),this._viewController.type(A.text))})),this._register(this._textAreaInput.onSelectionChangeRequest(A=>{this._viewController.setSelection(A)})),this._register(this._textAreaInput.onCompositionStart(A=>{const B=this._selections[0].startLineNumber,F=this._selections[0].startColumn+A.revealDeltaColumns;this._context.model.revealRange("keyboard",!0,new r.Range(B,F,B,F),0,1);const D=this._viewHelper.visibleRangeForPositionRelativeToEditor(B,F);D&&(this._visibleTextArea=new t(this._context.viewLayout.getVerticalOffsetForLineNumber(B),D.left,l?0:1),this._render()),this.textArea.setClassName(`inputarea ${n.MOUSE_CURSOR_TEXT_CSS_CLASS_NAME} ime-input`),this._viewController.compositionStart(),this._context.model.onCompositionStart()})),this._register(this._textAreaInput.onCompositionUpdate(A=>{!this._visibleTextArea||(this._visibleTextArea=this._visibleTextArea.setWidth(m(A.data,this._fontInfo)),this._render())})),this._register(this._textAreaInput.onCompositionEnd(()=>{this._visibleTextArea=null,this._render(),this.textArea.setClassName(`inputarea ${n.MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`),this._viewController.compositionEnd(),this._context.model.onCompositionEnd()})),this._register(this._textAreaInput.onFocus(()=>{this._context.model.setHasFocus(!0)})),this._register(this._textAreaInput.onBlur(()=>{this._context.model.setHasFocus(!1)}))}dispose(){super.dispose()}_getAndroidWordAtPosition(y){const L='`~!@#$%^&*()-=+[{]}\\|;:",.<>/?',I=this._context.model.getLineContent(y.lineNumber),k=a.getMapForWordSeparators(L);let E=!0,T=y.column,O=!0,A=y.column,B=0;for(;B<50&&(E||O);){if(E&&T<=1&&(E=!1),E){const F=I.charCodeAt(T-2);k.get(F)!==0?E=!1:T--}if(O&&A>I.length&&(O=!1),O){const F=I.charCodeAt(A-1);k.get(F)!==0?O=!1:A++}B++}return[I.substring(T-1,A-1),y.column-T]}_getWordBeforePosition(y){const L=this._context.model.getLineContent(y.lineNumber),I=a.getMapForWordSeparators(this._context.configuration.options.get(110));let k=y.column,E=0;for(;k>1;){const T=L.charCodeAt(k-2);if(I.get(T)!==0||E>50)return L.substring(k-1,y.column-1);E++,k--}return L.substring(0,y.column-1)}_getCharacterBeforePosition(y){if(y.column>1){const I=this._context.model.getLineContent(y.lineNumber).charAt(y.column-2);if(!S.isHighSurrogate(I.charCodeAt(0)))return I}return""}_getAriaLabel(y){return y.get(2)===1?b.localize(1,null,w.isLinux?"Shift+Alt+F1":"Alt+F1"):y.get(4)}_setAccessibilityOptions(y){this._accessibilitySupport=y.get(2);const L=y.get(3);this._accessibilitySupport===2&&L===s.EditorOptions.accessibilityPageSize.defaultValue?this._accessibilityPageSize=100:this._accessibilityPageSize=L}onConfigurationChanged(y){const L=this._context.configuration.options,I=L.get(124);return this._setAccessibilityOptions(L),this._contentLeft=I.contentLeft,this._contentWidth=I.contentWidth,this._contentHeight=I.height,this._fontInfo=L.get(38),this._lineHeight=L.get(53),this._emptySelectionClipboard=L.get(28),this._copyWithSyntaxHighlighting=L.get(18),this.textArea.setAttribute("aria-label",this._getAriaLabel(L)),this.textArea.setAttribute("tabindex",String(L.get(107))),w.isWeb&&y.hasChanged(75)&&(L.get(75)?this.textArea.setAttribute("readonly","true"):this.textArea.removeAttribute("readonly")),y.hasChanged(2)&&this._textAreaInput.writeScreenReaderContent("strategy changed"),!0}onCursorStateChanged(y){return this._selections=y.selections.slice(0),this._modelSelections=y.modelSelections.slice(0),this._textAreaInput.writeScreenReaderContent("selection changed"),!0}onDecorationsChanged(y){return!0}onFlushed(y){return!0}onLinesChanged(y){return!0}onLinesDeleted(y){return!0}onLinesInserted(y){return!0}onScrollChanged(y){return this._scrollLeft=y.scrollLeft,this._scrollTop=y.scrollTop,!0}onZonesChanged(y){return!0}isFocused(){return this._textAreaInput.isFocused()}focusTextArea(){this._textAreaInput.focusTextArea()}getLastRenderData(){return this._lastRenderPosition}setAriaOptions(y){y.activeDescendant?(this.textArea.setAttribute("aria-haspopup","true"),this.textArea.setAttribute("aria-autocomplete","list"),this.textArea.setAttribute("aria-activedescendant",y.activeDescendant)):(this.textArea.setAttribute("aria-haspopup","false"),this.textArea.setAttribute("aria-autocomplete","both"),this.textArea.removeAttribute("aria-activedescendant")),y.role&&this.textArea.setAttribute("role",y.role)}prepareRender(y){this._primaryCursorPosition=new u.Position(this._selections[0].positionLineNumber,this._selections[0].positionColumn),this._primaryCursorVisibleRange=y.visibleRangeForPosition(this._primaryCursorPosition)}render(y){this._textAreaInput.writeScreenReaderContent("render"),this._render()}_render(){if(this._visibleTextArea){this._renderInsideEditor(null,this._visibleTextArea.top-this._scrollTop,this._contentLeft+this._visibleTextArea.left-this._scrollLeft,this._visibleTextArea.width,this._lineHeight);return}if(!this._primaryCursorVisibleRange){this._renderAtTopLeft();return}const y=this._contentLeft+this._primaryCursorVisibleRange.left-this._scrollLeft;if(ythis._contentLeft+this._contentWidth){this._renderAtTopLeft();return}const L=this._context.viewLayout.getVerticalOffsetForLineNumber(this._selections[0].positionLineNumber)-this._scrollTop;if(L<0||L>this._contentHeight){this._renderAtTopLeft();return}if(w.isMacintosh){this._renderInsideEditor(this._primaryCursorPosition,L,y,l?0:1,this._lineHeight);return}this._renderInsideEditor(this._primaryCursorPosition,L,y,l?0:1,l?0:1)}_renderInsideEditor(y,L,I,k,E){this._lastRenderPosition=y;const T=this.textArea,O=this.textAreaCover;C.Configuration.applyFontInfo(T,this._fontInfo),T.setTop(L),T.setLeft(I),T.setWidth(k),T.setHeight(E),O.setTop(0),O.setLeft(0),O.setWidth(0),O.setHeight(0)}_renderAtTopLeft(){this._lastRenderPosition=null;const y=this.textArea,L=this.textAreaCover;if(C.Configuration.applyFontInfo(y,this._fontInfo),y.setTop(0),y.setLeft(0),L.setTop(0),L.setLeft(0),l){y.setWidth(0),y.setHeight(0),L.setWidth(0),L.setHeight(0);return}y.setWidth(1),y.setHeight(1),L.setWidth(1),L.setHeight(1);const I=this._context.configuration.options;I.get(44)?L.setClassName("monaco-editor-background textAreaCover "+o.Margin.OUTER_CLASS_NAME):I.get(54).renderType!==0?L.setClassName("monaco-editor-background textAreaCover "+c.LineNumbersOverlay.CLASS_NAME):L.setClassName("monaco-editor-background textAreaCover")}}e.TextAreaHandler=h;function m(v,y){const I=document.createElement("canvas").getContext("2d");I.font=_(y);const k=I.measureText(v);return N.isFirefox?k.width+2:k.width}function _(v){return f("normal",v.fontWeight,v.fontSize,v.lineHeight,v.fontFamily)}function f(v,y,L,I,k){return`${v} normal ${y} ${L}px / ${I}px ${k}`}}),define(Q[591],J([0,1,30,29,45,14,18,49]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DecorationsOverviewRuler=void 0;class d{constructor(c,o){const s=c.options;this.lineHeight=s.get(53),this.pixelRatio=s.get(122),this.overviewRulerLanes=s.get(68),this.renderBorder=s.get(67);const a=o.getColor(C.editorOverviewRulerBorder);this.borderColor=a?a.toString():null,this.hideCursor=s.get(46);const u=o.getColor(C.editorCursorForeground);this.cursorColor=u?u.transparent(.7).toString():null,this.themeType=o.type;const r=s.get(59),i=r.enabled,n=r.side,t=i?o.getColor(C.editorOverviewRulerBackground)||S.TokenizationRegistry.getDefaultBackground():null;t===null||n==="left"?this.backgroundColor=null:this.backgroundColor=N.Color.Format.CSS.formatHex(t);const h=s.get(124).overviewRuler;this.top=h.top,this.right=h.right,this.domWidth=h.width,this.domHeight=h.height,this.overviewRulerLanes===0?(this.canvasWidth=0,this.canvasHeight=0):(this.canvasWidth=this.domWidth*this.pixelRatio|0,this.canvasHeight=this.domHeight*this.pixelRatio|0);const[m,_]=this._initLanes(1,this.canvasWidth,this.overviewRulerLanes);this.x=m,this.w=_}_initLanes(c,o,s){const a=o-c;if(s>=3){const u=Math.floor(a/3),r=Math.floor(a/3),i=a-u-r,n=c,t=n+u,l=n+u+i;return[[0,n,t,n,l,n,t,n],[0,u,i,u+i,r,u+i+r,i+r,u+i+r]]}else if(s===2){const u=Math.floor(a/2),r=a-u,i=c,n=i+u;return[[0,i,i,i,n,i,i,i],[0,u,u,u,r,u+r,u+r,u+r]]}else{const u=c,r=a;return[[0,u,u,u,u,u,u,u],[0,r,r,r,r,r,r,r]]}}equals(c){return this.lineHeight===c.lineHeight&&this.pixelRatio===c.pixelRatio&&this.overviewRulerLanes===c.overviewRulerLanes&&this.renderBorder===c.renderBorder&&this.borderColor===c.borderColor&&this.hideCursor===c.hideCursor&&this.cursorColor===c.cursorColor&&this.themeType===c.themeType&&this.backgroundColor===c.backgroundColor&&this.top===c.top&&this.right===c.right&&this.domWidth===c.domWidth&&this.domHeight===c.domHeight&&this.canvasWidth===c.canvasWidth&&this.canvasHeight===c.canvasHeight}}class g extends M.ViewPart{constructor(c){super(c);this._domNode=b.createFastDomNode(document.createElement("canvas")),this._domNode.setClassName("decorationsOverviewRuler"),this._domNode.setPosition("absolute"),this._domNode.setLayerHinting(!0),this._domNode.setContain("strict"),this._domNode.setAttribute("aria-hidden","true"),this._updateSettings(!1),this._tokensColorTrackerListener=S.TokenizationRegistry.onDidChange(o=>{o.changedColorMap&&this._updateSettings(!0)}),this._cursorPositions=[]}dispose(){super.dispose(),this._tokensColorTrackerListener.dispose()}_updateSettings(c){const o=new d(this._context.configuration,this._context.theme);return this._settings&&this._settings.equals(o)?!1:(this._settings=o,this._domNode.setTop(this._settings.top),this._domNode.setRight(this._settings.right),this._domNode.setWidth(this._settings.domWidth),this._domNode.setHeight(this._settings.domHeight),this._domNode.domNode.width=this._settings.canvasWidth,this._domNode.domNode.height=this._settings.canvasHeight,c&&this._render(),!0)}onConfigurationChanged(c){return this._updateSettings(!1)}onCursorStateChanged(c){this._cursorPositions=[];for(let o=0,s=c.selections.length;oo&&(x=o-t),D=x-t,R=x+t}D>E+1||A!==I?(T!==0&&l.fillRect(h[I],k,m[I],E-k),I=A,k=D,E=R):R>E&&(E=R)}l.fillRect(h[I],k,m[I],E-k)}if(!this._settings.hideCursor&&this._settings.cursorColor){const f=2*this._settings.pixelRatio|0,v=f/2|0,y=this._settings.x[7],L=this._settings.w[7];l.fillStyle=this._settings.cursorColor;let I=-100,k=-100;for(let E=0,T=this._cursorPositions.length;Eo&&(A=o-v);const B=A-v,F=B+f;B>k+1?(E!==0&&l.fillRect(y,I,L,k-I),I=B,k=F):F>k&&(k=F)}l.fillRect(y,I,L,k-I)}this._settings.renderBorder&&this._settings.borderColor&&this._settings.overviewRulerLanes>0&&(l.beginPath(),l.lineWidth=1,l.strokeStyle=this._settings.borderColor,l.moveTo(0,0),l.lineTo(0,o),l.stroke(),l.moveTo(0,0),l.lineTo(c,0),l.stroke())}}e.DecorationsOverviewRuler=g}),define(Q[592],J([0,1,30,45,49,11,328]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.Rulers=void 0;class S extends N.ViewPart{constructor(d){super(d);this.domNode=b.createFastDomNode(document.createElement("div")),this.domNode.setAttribute("role","presentation"),this.domNode.setAttribute("aria-hidden","true"),this.domNode.setClassName("view-rulers"),this._renderedRulers=[];const g=this._context.configuration.options;this._rulers=g.get(86),this._typicalHalfwidthCharacterWidth=g.get(38).typicalHalfwidthCharacterWidth}dispose(){super.dispose()}onConfigurationChanged(d){const g=this._context.configuration.options;return this._rulers=g.get(86),this._typicalHalfwidthCharacterWidth=g.get(38).typicalHalfwidthCharacterWidth,!0}onScrollChanged(d){return d.scrollHeightChanged}prepareRender(d){}_ensureRulersCount(){const d=this._renderedRulers.length,g=this._rulers.length;if(d!==g){if(d0;){const a=b.createFastDomNode(document.createElement("div"));a.setClassName("view-ruler"),a.setWidth(o),this.domNode.appendChild(a),this._renderedRulers.push(a),s--}return}let p=d-g;for(;p>0;){const c=this._renderedRulers.pop();this.domNode.removeChild(c),p--}}}render(d){this._ensureRulersCount();for(let g=0,p=this._rulers.length;g{const g=C.getColor(M.editorRuler);g&&d.addRule(`.monaco-editor .view-ruler { box-shadow: 1px 0 0 0 ${g} inset; }`)})}),define(Q[593],J([0,1,30,15,45,565,38,49,11,331]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewCursors=void 0;class g extends M.ViewPart{constructor(c){super(c);const o=this._context.configuration.options;this._readOnly=o.get(75),this._cursorBlinking=o.get(19),this._cursorStyle=o.get(21),this._cursorSmoothCaretAnimation=o.get(20),this._selectionIsEmpty=!0,this._isComposingInput=!1,this._isVisible=!1,this._primaryCursor=new w.ViewCursor(this._context),this._secondaryCursors=[],this._renderData=[],this._domNode=b.createFastDomNode(document.createElement("div")),this._domNode.setAttribute("role","presentation"),this._domNode.setAttribute("aria-hidden","true"),this._updateDomClassName(),this._domNode.appendChild(this._primaryCursor.getDomNode()),this._startCursorBlinkAnimation=new N.TimeoutTimer,this._cursorFlatBlinkInterval=new N.IntervalTimer,this._blinkingEnabled=!1,this._editorHasFocus=!1,this._updateBlinking()}dispose(){super.dispose(),this._startCursorBlinkAnimation.dispose(),this._cursorFlatBlinkInterval.dispose()}getDomNode(){return this._domNode}onCompositionStart(c){return this._isComposingInput=!0,this._updateBlinking(),!0}onCompositionEnd(c){return this._isComposingInput=!1,this._updateBlinking(),!0}onConfigurationChanged(c){const o=this._context.configuration.options;this._readOnly=o.get(75),this._cursorBlinking=o.get(19),this._cursorStyle=o.get(21),this._cursorSmoothCaretAnimation=o.get(20),this._updateBlinking(),this._updateDomClassName(),this._primaryCursor.onConfigurationChanged(c);for(let s=0,a=this._secondaryCursors.length;so.length){const s=this._secondaryCursors.length-o.length;for(let a=0;a{for(let a=0,u=c.ranges.length;a{this._isVisible?this._hide():this._show()},g.BLINK_INTERVAL):this._startCursorBlinkAnimation.setIfNotSet(()=>{this._blinkingEnabled=!0,this._updateDomClassName()},g.BLINK_INTERVAL))}_updateDomClassName(){this._domNode.setClassName(this._getClassName())}_getClassName(){let c="cursors-layer";switch(this._selectionIsEmpty||(c+=" has-selection"),this._cursorStyle){case S.TextEditorCursorStyle.Line:c+=" cursor-line-style";break;case S.TextEditorCursorStyle.Block:c+=" cursor-block-style";break;case S.TextEditorCursorStyle.Underline:c+=" cursor-underline-style";break;case S.TextEditorCursorStyle.LineThin:c+=" cursor-line-thin-style";break;case S.TextEditorCursorStyle.BlockOutline:c+=" cursor-block-outline-style";break;case S.TextEditorCursorStyle.UnderlineThin:c+=" cursor-underline-thin-style";break;default:c+=" cursor-line-style"}if(this._blinkingEnabled)switch(this._getCursorBlinking()){case 1:c+=" cursor-blink";break;case 2:c+=" cursor-smooth";break;case 3:c+=" cursor-phase";break;case 4:c+=" cursor-expand";break;case 5:c+=" cursor-solid";break;default:c+=" cursor-solid"}else c+=" cursor-solid";return this._cursorSmoothCaretAnimation&&(c+=" cursor-smooth-caret-animation"),c}_show(){this._primaryCursor.show();for(let c=0,o=this._secondaryCursors.length;c{const o=p.getColor(C.editorCursorForeground);if(o){let s=p.getColor(C.editorCursorBackground);s||(s=o.opposite()),c.addRule(`.monaco-editor .cursors-layer .cursor { background-color: ${o}; border-color: ${o}; color: ${s}; }`),p.type==="hc"&&c.addRule(`.monaco-editor .cursors-layer.has-selection .cursor { border-left: 1px solid ${s}; border-right: 1px solid ${s}; }`)}})}),define(Q[594],J([0,1,85,2,53,11,49,36,3,43,6,22]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MarkerDecorationsService=void 0;function o(u){return u.toString()}class s extends N.Disposable{constructor(r){super();this.model=r,this._markersData=new Map,this._register(N.toDisposable(()=>{this.model.deltaDecorations([...this._markersData.keys()],[]),this._markersData.clear()}))}update(r,i){const n=[...this._markersData.keys()];this._markersData.clear();const t=this.model.deltaDecorations(n,i);for(let l=0;lthis._onModelAdded(n)),this._register(r.onModelAdded(this._onModelAdded,this)),this._register(r.onModelRemoved(this._onModelRemoved,this)),this._register(this._markerService.onMarkerChanged(this._handleMarkerChange,this))}dispose(){super.dispose(),this._markerDecorations.forEach(r=>r.dispose()),this._markerDecorations.clear()}getMarker(r,i){const n=this._markerDecorations.get(o(r));return n&&n.getMarker(i)||null}_handleMarkerChange(r){r.forEach(i=>{const n=this._markerDecorations.get(o(i));n&&this._updateDecorations(n)})}_onModelAdded(r){const i=new s(r);this._markerDecorations.set(o(r.uri),i),this._updateDecorations(i)}_onModelRemoved(r){const i=this._markerDecorations.get(o(r.uri));i&&(i.dispose(),this._markerDecorations.delete(o(r.uri))),(r.uri.scheme===g.Schemas.inMemory||r.uri.scheme===g.Schemas.internal||r.uri.scheme===g.Schemas.vscode)&&this._markerService&&this._markerService.read({resource:r.uri}).map(n=>n.owner).forEach(n=>this._markerService.remove(n,[r.uri]))}_updateDecorations(r){const i=this._markerService.read({resource:r.model.uri,take:500});let n=i.map(t=>({range:this._createDecorationRange(r.model,t),options:this._createDecorationOption(t)}));r.update(i,n)&&this._onDidChangeMarker.fire(r.model)}_createDecorationRange(r,i){let n=d.Range.lift(i);if(i.severity===b.MarkerSeverity.Hint&&!this._hasMarkerTag(i,1)&&!this._hasMarkerTag(i,2)&&(n=n.setEndPosition(n.startLineNumber,n.startColumn+2)),n=r.validateRange(n),n.isEmpty()){let t=r.getWordAtPosition(n.getStartPosition());if(t)n=new d.Range(n.startLineNumber,t.startColumn,n.endLineNumber,t.endColumn);else{let l=r.getLineLastNonWhitespaceColumn(n.startLineNumber)||r.getLineMaxColumn(n.startLineNumber);l===1||(n.endColumn>=l?n=new d.Range(n.startLineNumber,l-1,n.endLineNumber,l):n=new d.Range(n.startLineNumber,n.startColumn,n.endLineNumber,n.endColumn+1))}}else if(i.endColumn===Number.MAX_VALUE&&i.startColumn===1&&n.startLineNumber===n.endLineNumber){let t=r.getLineFirstNonWhitespaceColumn(i.startLineNumber);t=0:!1}};a=Me([_e(0,C.IModelService),_e(1,b.IMarkerService)],a),e.MarkerDecorationsService=a}),define(Q[595],J([0,1,7,90,6,2,31,467,37,11,22,60,27,337]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LightBulbWidget=void 0;var s;(function(u){u.Hidden={type:0};class r{constructor(n,t,l,h){this.actions=n,this.trigger=t,this.editorPosition=l,this.widgetPosition=h,this.type=1}}u.Showing=r})(s||(s={}));let a=class At extends w.Disposable{constructor(r,i,n,t){super();this._editor=r,this._quickFixActionId=i,this._preferredFixActionId=n,this._keybindingService=t,this._onClick=this._register(new M.Emitter),this.onClick=this._onClick.event,this._state=s.Hidden,this._domNode=document.createElement("div"),this._domNode.className=o.Codicon.lightBulb.classNames,this._editor.addContentWidget(this),this._register(this._editor.onDidChangeModelContent(l=>{const h=this._editor.getModel();(this.state.type!==1||!h||this.state.editorPosition.lineNumber>=h.getLineCount())&&this.hide()})),c.Gesture.ignoreTarget(this._domNode),this._register(b.addStandardDisposableGenericMouseDownListner(this._domNode,l=>{if(this.state.type===1){this._editor.focus(),l.preventDefault();const{top:h,height:m}=b.getDomNodePagePosition(this._domNode),_=this._editor.getOption(53);let f=Math.floor(_/3);this.state.widgetPosition.position!==null&&this.state.widgetPosition.position.lineNumber{if((l.buttons&1)==1){this.hide();const h=new N.GlobalMouseMoveMonitor;h.startMonitoring(l.target,l.buttons,N.standardMouseMoveMerger,()=>{},()=>{h.dispose()})}})),this._register(this._editor.onDidChangeConfiguration(l=>{l.hasChanged(51)&&!this._editor.getOption(51).enabled&&this.hide()})),this._updateLightBulbTitleAndIcon(),this._register(this._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitleAndIcon,this))}dispose(){super.dispose(),this._editor.removeContentWidget(this)}getId(){return"LightBulbWidget"}getDomNode(){return this._domNode}getPosition(){return this._state.type===1?this._state.widgetPosition:null}update(r,i,n){if(r.validActions.length<=0)return this.hide();const t=this._editor.getOptions();if(!t.get(51).enabled)return this.hide();const l=this._editor.getModel();if(!l)return this.hide();const{lineNumber:h,column:m}=l.validatePosition(n),_=l.getOptions().tabSize,f=t.get(38),v=l.getLineContent(h),y=S.TextModel.computeIndentLevel(v,_),L=f.spaceWidth*y>22,I=E=>E>2&&this._editor.getTopForLineNumber(E)===this._editor.getTopForLineNumber(E-1);let k=h;if(!L){if(h>1&&!I(h-1))k-=1;else if(!I(h+1))k+=1;else if(m*f.spaceWidth<22)return this.hide()}this.state=new s.Showing(r,i,n,{position:{lineNumber:k,column:1},preference:At._posPref}),this._editor.layoutContentWidget(this)}hide(){this.state=s.Hidden,this._editor.layoutContentWidget(this)}get state(){return this._state}set state(r){this._state=r,this._updateLightBulbTitleAndIcon()}_updateLightBulbTitleAndIcon(){if(this.state.type===1&&this.state.actions.hasAutoFix){this._domNode.classList.remove(...o.Codicon.lightBulb.classNamesArray),this._domNode.classList.add(...o.Codicon.lightbulbAutofix.classNamesArray);const i=this._keybindingService.lookupKeybinding(this._preferredFixActionId);if(i){this.title=C.localize(0,null,i.getLabel());return}}this._domNode.classList.remove(...o.Codicon.lightbulbAutofix.classNamesArray),this._domNode.classList.add(...o.Codicon.lightBulb.classNamesArray);const r=this._keybindingService.lookupKeybinding(this._quickFixActionId);r?this.title=C.localize(1,null,r.getLabel()):this.title=C.localize(2,null)}set title(r){this._domNode.title=r}};a._posPref=[0],a=Me([_e(3,d.IKeybindingService)],a),e.LightBulbWidget=a,g.registerThemingParticipant((u,r)=>{var i;const n=(i=u.getColor(p.editorBackground))===null||i===void 0?void 0:i.transparent(.7),t=u.getColor(p.editorLightBulbForeground);t&&r.addRule(` + .monaco-editor .contentWidgets ${o.Codicon.lightBulb.cssSelector} { + color: ${t}; + background-color: ${n}; + }`);const l=u.getColor(p.editorLightBulbAutoFixForeground);l&&r.addRule(` + .monaco-editor .contentWidgets ${o.Codicon.lightbulbAutofix.cssSelector} { + color: ${l}; + background-color: ${n}; + }`)})}),define(Q[596],J([0,1,7,3,31,49,22,11,103,338]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CodeLensWidget=e.CodeLensHelper=void 0;class g{constructor(a,u,r){this.afterLineNumber=a,this.heightInPx=u,this._onHeight=r,this.suppressMouseDown=!0,this.domNode=document.createElement("div")}onComputedHeight(a){this._lastHeight===void 0?this._lastHeight=a:this._lastHeight!==a&&(this._lastHeight=a,this._onHeight())}}class p{constructor(a,u,r){this.allowEditorOverflow=!1,this.suppressMouseDown=!0,this._commands=new Map,this._isEmpty=!0,this._editor=a,this._id=`codelens.widget-${p._idPool++}`,this.updatePosition(r),this._domNode=document.createElement("span"),this._domNode.className=`codelens-decoration ${u}`}withCommands(a,u){this._commands.clear();let r=[],i=!1;for(let n=0;n{_.symbol.command&&m.push(_.symbol),i.addDecoration({range:_.symbol.range,options:M.ModelDecorationOptions.EMPTY},v=>this._decorationIds[f]=v),h?h=N.Range.plusRange(h,_.symbol.range):h=N.Range.lift(_.symbol.range)}),this._viewZone=new g(h.startLineNumber-1,t,l),this._viewZoneId=n.addZone(this._viewZone),m.length>0&&(this._createContentWidgetIfNecessary(),this._contentWidget.withCommands(m,!1))}_createContentWidgetIfNecessary(){this._contentWidget?this._editor.layoutContentWidget(this._contentWidget):(this._contentWidget=new p(this._editor,this._className,this._viewZone.afterLineNumber+1),this._editor.addContentWidget(this._contentWidget))}dispose(a,u){this._decorationIds.forEach(a.removeDecoration,a),this._decorationIds=[],u&&u.removeZone(this._viewZoneId),this._contentWidget&&(this._editor.removeContentWidget(this._contentWidget),this._contentWidget=void 0),this._isDisposed=!0}isDisposed(){return this._isDisposed}isValid(){return this._decorationIds.some((a,u)=>{const r=this._editor.getModel().getDecorationRange(a),i=this._data[u].symbol;return!!(r&&N.Range.isEmpty(i.range)===r.isEmpty())})}updateCodeLensSymbols(a,u){this._decorationIds.forEach(u.removeDecoration,u),this._decorationIds=[],this._data=a,this._data.forEach((r,i)=>{u.addDecoration({range:r.symbol.range,options:M.ModelDecorationOptions.EMPTY},n=>this._decorationIds[i]=n)})}updateHeight(a,u){this._viewZone.heightInPx=a,u.layoutZone(this._viewZoneId),this._contentWidget&&this._editor.layoutContentWidget(this._contentWidget)}computeIfNecessary(a){if(!this._viewZone.domNode.hasAttribute("monaco-visible-view-zone"))return null;for(let u=0;u{const u=s.getColor(w.editorCodeLensForeground);u&&(a.addRule(`.monaco-editor .codelens-decoration { color: ${u}; }`),a.addRule(`.monaco-editor .codelens-decoration .codicon { color: ${u}; }`));const r=s.getColor(S.editorActiveLinkForeground);r&&(a.addRule(`.monaco-editor .codelens-decoration > a:hover { color: ${r} !important; }`),a.addRule(`.monaco-editor .codelens-decoration > a:hover .codicon { color: ${r} !important; }`))})}),define(Q[597],J([0,1,35,7,90,52,29,6,2,22,11,339]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ColorPickerWidget=e.ColorPickerBody=e.ColorPickerHeader=void 0;const c=N.$;class o extends d.Disposable{constructor(l,h,m){super();this.model=h,this.domNode=c(".colorpicker-header"),N.append(l,this.domNode),this.pickedColorNode=N.append(this.domNode,c(".picked-color"));const _=N.append(this.domNode,c(".original-color"));_.style.backgroundColor=S.Color.Format.CSS.format(this.model.originalColor)||"",this.backgroundColor=m.getColorTheme().getColor(g.editorHoverBackground)||S.Color.white,this._register(p.registerThemingParticipant((f,v)=>{this.backgroundColor=f.getColor(g.editorHoverBackground)||S.Color.white})),this._register(N.addDisposableListener(this.pickedColorNode,N.EventType.CLICK,()=>this.model.selectNextColorPresentation())),this._register(N.addDisposableListener(_,N.EventType.CLICK,()=>{this.model.color=this.model.originalColor,this.model.flushColor()})),this._register(h.onDidChangeColor(this.onDidChangeColor,this)),this._register(h.onDidChangePresentation(this.onDidChangePresentation,this)),this.pickedColorNode.style.backgroundColor=S.Color.Format.CSS.format(h.color)||"",this.pickedColorNode.classList.toggle("light",h.color.rgba.a<.5?this.backgroundColor.isLighter():h.color.isLighter())}onDidChangeColor(l){this.pickedColorNode.style.backgroundColor=S.Color.Format.CSS.format(l)||"",this.pickedColorNode.classList.toggle("light",l.rgba.a<.5?this.backgroundColor.isLighter():l.isLighter()),this.onDidChangePresentation()}onDidChangePresentation(){this.pickedColorNode.textContent=this.model.presentation?this.model.presentation.label:""}}e.ColorPickerHeader=o;class s extends d.Disposable{constructor(l,h,m){super();this.model=h,this.pixelRatio=m,this.domNode=c(".colorpicker-body"),N.append(l,this.domNode),this.saturationBox=new a(this.domNode,this.model,this.pixelRatio),this._register(this.saturationBox),this._register(this.saturationBox.onDidChange(this.onDidSaturationValueChange,this)),this._register(this.saturationBox.onColorFlushed(this.flushColor,this)),this.opacityStrip=new r(this.domNode,this.model),this._register(this.opacityStrip),this._register(this.opacityStrip.onDidChange(this.onDidOpacityChange,this)),this._register(this.opacityStrip.onColorFlushed(this.flushColor,this)),this.hueStrip=new i(this.domNode,this.model),this._register(this.hueStrip),this._register(this.hueStrip.onDidChange(this.onDidHueChange,this)),this._register(this.hueStrip.onColorFlushed(this.flushColor,this))}flushColor(){this.model.flushColor()}onDidSaturationValueChange({s:l,v:h}){const m=this.model.color.hsva;this.model.color=new S.Color(new S.HSVA(m.h,l,h,m.a))}onDidOpacityChange(l){const h=this.model.color.hsva;this.model.color=new S.Color(new S.HSVA(h.h,h.s,h.v,l))}onDidHueChange(l){const h=this.model.color.hsva,m=(1-l)*360;this.model.color=new S.Color(new S.HSVA(m===360?0:m,h.s,h.v,h.a))}layout(){this.saturationBox.layout(),this.opacityStrip.layout(),this.hueStrip.layout()}}e.ColorPickerBody=s;class a extends d.Disposable{constructor(l,h,m){super();this.model=h,this.pixelRatio=m,this._onDidChange=new C.Emitter,this.onDidChange=this._onDidChange.event,this._onColorFlushed=new C.Emitter,this.onColorFlushed=this._onColorFlushed.event,this.domNode=c(".saturation-wrap"),N.append(l,this.domNode),this.canvas=document.createElement("canvas"),this.canvas.className="saturation-box",N.append(this.domNode,this.canvas),this.selection=c(".saturation-selection"),N.append(this.domNode,this.selection),this.layout(),this._register(N.addDisposableGenericMouseDownListner(this.domNode,_=>this.onMouseDown(_))),this._register(this.model.onDidChangeColor(this.onDidChangeColor,this)),this.monitor=null}onMouseDown(l){this.monitor=this._register(new M.GlobalMouseMoveMonitor);const h=N.getDomNodePagePosition(this.domNode);l.target!==this.selection&&this.onDidChangePosition(l.offsetX,l.offsetY),this.monitor.startMonitoring(l.target,l.buttons,M.standardMouseMoveMerger,_=>this.onDidChangePosition(_.posx-h.left,_.posy-h.top),()=>null);const m=N.addDisposableGenericMouseUpListner(document,()=>{this._onColorFlushed.fire(),m.dispose(),this.monitor&&(this.monitor.stopMonitoring(!0),this.monitor=null)},!0)}onDidChangePosition(l,h){const m=Math.max(0,Math.min(1,l/this.width)),_=Math.max(0,Math.min(1,1-h/this.height));this.paintSelection(m,_),this._onDidChange.fire({s:m,v:_})}layout(){this.width=this.domNode.offsetWidth,this.height=this.domNode.offsetHeight,this.canvas.width=this.width*this.pixelRatio,this.canvas.height=this.height*this.pixelRatio,this.paint();const l=this.model.color.hsva;this.paintSelection(l.s,l.v)}paint(){const l=this.model.color.hsva,h=new S.Color(new S.HSVA(l.h,1,1,1)),m=this.canvas.getContext("2d"),_=m.createLinearGradient(0,0,this.canvas.width,0);_.addColorStop(0,"rgba(255, 255, 255, 1)"),_.addColorStop(.5,"rgba(255, 255, 255, 0.5)"),_.addColorStop(1,"rgba(255, 255, 255, 0)");const f=m.createLinearGradient(0,0,0,this.canvas.height);f.addColorStop(0,"rgba(0, 0, 0, 0)"),f.addColorStop(1,"rgba(0, 0, 0, 1)"),m.rect(0,0,this.canvas.width,this.canvas.height),m.fillStyle=S.Color.Format.CSS.format(h),m.fill(),m.fillStyle=_,m.fill(),m.fillStyle=f,m.fill()}paintSelection(l,h){this.selection.style.left=`${l*this.width}px`,this.selection.style.top=`${this.height-h*this.height}px`}onDidChangeColor(){this.monitor&&this.monitor.isMonitoring()||this.paint()}}class u extends d.Disposable{constructor(l,h){super();this.model=h,this._onDidChange=new C.Emitter,this.onDidChange=this._onDidChange.event,this._onColorFlushed=new C.Emitter,this.onColorFlushed=this._onColorFlushed.event,this.domNode=N.append(l,c(".strip")),this.overlay=N.append(this.domNode,c(".overlay")),this.slider=N.append(this.domNode,c(".slider")),this.slider.style.top="0px",this._register(N.addDisposableGenericMouseDownListner(this.domNode,m=>this.onMouseDown(m))),this.layout()}layout(){this.height=this.domNode.offsetHeight-this.slider.offsetHeight;const l=this.getValue(this.model.color);this.updateSliderPosition(l)}onMouseDown(l){const h=this._register(new M.GlobalMouseMoveMonitor),m=N.getDomNodePagePosition(this.domNode);this.domNode.classList.add("grabbing"),l.target!==this.slider&&this.onDidChangeTop(l.offsetY),h.startMonitoring(l.target,l.buttons,M.standardMouseMoveMerger,f=>this.onDidChangeTop(f.posy-m.top),()=>null);const _=N.addDisposableGenericMouseUpListner(document,()=>{this._onColorFlushed.fire(),_.dispose(),h.stopMonitoring(!0),this.domNode.classList.remove("grabbing")},!0)}onDidChangeTop(l){const h=Math.max(0,Math.min(1,1-l/this.height));this.updateSliderPosition(h),this._onDidChange.fire(h)}updateSliderPosition(l){this.slider.style.top=`${(1-l)*this.height}px`}}class r extends u{constructor(l,h){super(l,h);this.domNode.classList.add("opacity-strip"),this._register(h.onDidChangeColor(this.onDidChangeColor,this)),this.onDidChangeColor(this.model.color)}onDidChangeColor(l){const{r:h,g:m,b:_}=l.rgba,f=new S.Color(new S.RGBA(h,m,_,1)),v=new S.Color(new S.RGBA(h,m,_,0));this.overlay.style.background=`linear-gradient(to bottom, ${f} 0%, ${v} 100%)`}getValue(l){return l.hsva.a}}class i extends u{constructor(l,h){super(l,h);this.domNode.classList.add("hue-strip")}getValue(l){return 1-l.hsva.h/360}}class n extends w.Widget{constructor(l,h,m,_){super();this.model=h,this.pixelRatio=m,this._register(b.onDidChangeZoomLevel(()=>this.layout()));const f=c(".colorpicker-widget");l.appendChild(f);const v=new o(f,this.model,_);this.body=new s(f,this.model,this.pixelRatio),this._register(v),this._register(this.body)}layout(){this.body.layout()}}e.ColorPickerWidget=n}),define(Q[598],J([0,1,3,53,31,22,11]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FindDecorations=void 0;class C{constructor(g){this._editor=g,this._decorations=[],this._overviewRulerApproximateDecorations=[],this._findScopeDecorationIds=[],this._rangeHighlightDecorationId=null,this._highlightedDecorationId=null,this._startPosition=this._editor.getPosition()}dispose(){this._editor.deltaDecorations(this._allDecorations(),[]),this._decorations=[],this._overviewRulerApproximateDecorations=[],this._findScopeDecorationIds=[],this._rangeHighlightDecorationId=null,this._highlightedDecorationId=null}reset(){this._decorations=[],this._overviewRulerApproximateDecorations=[],this._findScopeDecorationIds=[],this._rangeHighlightDecorationId=null,this._highlightedDecorationId=null}getCount(){return this._decorations.length}getFindScope(){return this._findScopeDecorationIds[0]?this._editor.getModel().getDecorationRange(this._findScopeDecorationIds[0]):null}getFindScopes(){if(this._findScopeDecorationIds.length){const g=this._findScopeDecorationIds.map(p=>this._editor.getModel().getDecorationRange(p)).filter(p=>!!p);if(g.length)return g}return null}getStartPosition(){return this._startPosition}setStartPosition(g){this._startPosition=g,this.setCurrentFindMatch(null)}_getDecorationIndex(g){const p=this._decorations.indexOf(g);return p>=0?p+1:1}getCurrentMatchesPosition(g){let p=this._editor.getModel().getDecorationsInRange(g);for(const c of p){const o=c.options;if(o===C._FIND_MATCH_DECORATION||o===C._CURRENT_FIND_MATCH_DECORATION)return this._getDecorationIndex(c.id)}return 0}setCurrentFindMatch(g){let p=null,c=0;if(g)for(let o=0,s=this._decorations.length;o{if(this._highlightedDecorationId!==null&&(o.changeDecorationOptions(this._highlightedDecorationId,C._FIND_MATCH_DECORATION),this._highlightedDecorationId=null),p!==null&&(this._highlightedDecorationId=p,o.changeDecorationOptions(this._highlightedDecorationId,C._CURRENT_FIND_MATCH_DECORATION)),this._rangeHighlightDecorationId!==null&&(o.removeDecoration(this._rangeHighlightDecorationId),this._rangeHighlightDecorationId=null),p!==null){let s=this._editor.getModel().getDecorationRange(p);if(s.startLineNumber!==s.endLineNumber&&s.endColumn===1){let a=s.endLineNumber-1,u=this._editor.getModel().getLineMaxColumn(a);s=new b.Range(s.startLineNumber,s.startColumn,a,u)}this._rangeHighlightDecorationId=o.addDecoration(s,C._RANGE_HIGHLIGHT_DECORATION)}}),c}set(g,p){this._editor.changeDecorations(c=>{let o=C._FIND_MATCH_DECORATION,s=[];if(g.length>1e3){o=C._FIND_MATCH_NO_OVERVIEW_DECORATION;const u=this._editor.getModel().getLineCount(),i=this._editor.getLayoutInfo().height/u,n=Math.max(2,Math.ceil(3/i));let t=g[0].range.startLineNumber,l=g[0].range.endLineNumber;for(let h=1,m=g.length;h=_.startLineNumber?_.endLineNumber>l&&(l=_.endLineNumber):(s.push({range:new b.Range(t,1,l,1),options:C._FIND_MATCH_ONLY_OVERVIEW_DECORATION}),t=_.startLineNumber,l=_.endLineNumber)}s.push({range:new b.Range(t,1,l,1),options:C._FIND_MATCH_ONLY_OVERVIEW_DECORATION})}let a=new Array(g.length);for(let u=0,r=g.length;uc.removeDecoration(u)),this._findScopeDecorationIds=[]),(p==null?void 0:p.length)&&(this._findScopeDecorationIds=p.map(u=>c.addDecoration(u,C._FIND_SCOPE_DECORATION)))})}matchBeforePosition(g){if(this._decorations.length===0)return null;for(let p=this._decorations.length-1;p>=0;p--){let c=this._decorations[p],o=this._editor.getModel().getDecorationRange(c);if(!(!o||o.endLineNumber>g.lineNumber)){if(o.endLineNumberg.column))return o}}return this._editor.getModel().getDecorationRange(this._decorations[this._decorations.length-1])}matchAfterPosition(g){if(this._decorations.length===0)return null;for(let p=0,c=this._decorations.length;pg.lineNumber)return s;if(!(s.startColumnthis.research(!1),100),this._toDispose.add(this._updateDecorationsScheduler),this._toDispose.add(this._editor.onDidChangeCursorPosition(t=>{(t.reason===3||t.reason===5||t.reason===6)&&this._decorations.setStartPosition(this._editor.getPosition())})),this._ignoreModelContentChanged=!1,this._toDispose.add(this._editor.onDidChangeModelContent(t=>{this._ignoreModelContentChanged||(t.isFlush&&this._decorations.reset(),this._decorations.setStartPosition(this._editor.getPosition()),this._updateDecorationsScheduler.schedule())})),this._toDispose.add(this._state.onFindReplaceStateChange(t=>this._onStateChanged(t))),this.research(!1,this._state.searchScope)}dispose(){this._isDisposed=!0,N.dispose(this._startSearchingTimer),this._toDispose.dispose()}_onStateChanged(i){this._isDisposed||!this._editor.hasModel()||(i.searchString||i.isReplaceRevealed||i.isRegex||i.wholeWord||i.matchCase||i.searchScope)&&(this._editor.getModel().isTooLargeForSyncing()?(this._startSearchingTimer.cancel(),this._startSearchingTimer.setIfNotSet(()=>{i.searchScope?this.research(i.moveCursor,this._state.searchScope):this.research(i.moveCursor)},a)):i.searchScope?this.research(i.moveCursor,this._state.searchScope):this.research(i.moveCursor))}static _getSearchRange(i,n){return n||i.getFullModelRange()}research(i,n){let t=null;typeof n!="undefined"?n!==null&&(Array.isArray(n)?t=n:t=[n]):t=this._decorations.getFindScopes(),t!==null&&(t=t.map(_=>{if(_.startLineNumber!==_.endLineNumber){let f=_.endLineNumber;return _.endColumn===1&&(f=f-1),new S.Range(_.startLineNumber,1,f,this._editor.getModel().getLineMaxColumn(f))}return _}));let l=this._findMatches(t,!1,e.MATCHES_LIMIT);this._decorations.set(l,t);const h=this._editor.getSelection();let m=this._decorations.getCurrentMatchesPosition(h);if(m===0&&l.length>0){const _=s.findFirstInSorted(l.map(f=>f.range),f=>S.Range.compareRangesUsingStarts(f,h)>=0);m=_>0?_-1+1:m}this._state.changeMatchInfo(m,this._decorations.getCount(),void 0),i&&this._editor.getOption(31).cursorMoveOnType&&this._moveToNextMatch(this._decorations.getStartPosition())}_hasMatches(){return this._state.matchesCount>0}_cannotFind(){if(!this._hasMatches()){let i=this._decorations.getFindScope();return i&&this._editor.revealRangeInCenterIfOutsideViewport(i,0),!0}return!1}_setCurrentFindMatch(i){let n=this._decorations.setCurrentFindMatch(i);this._state.changeMatchInfo(n,this._decorations.getCount(),i),this._editor.setSelection(i),this._editor.revealRangeInCenterIfOutsideViewport(i,0)}_prevSearchPosition(i){let n=this._state.isRegex&&(this._state.searchString.indexOf("^")>=0||this._state.searchString.indexOf("$")>=0),{lineNumber:t,column:l}=i,h=this._editor.getModel();return n||l===1?(t===1?t=h.getLineCount():t--,l=h.getLineMaxColumn(t)):l--,new w.Position(t,l)}_moveToPrevMatch(i,n=!1){if(!this._state.canNavigateBack()){const y=this._decorations.matchAfterPosition(i);y&&this._setCurrentFindMatch(y);return}if(this._decorations.getCount()=0||this._state.searchString.indexOf("$")>=0),{lineNumber:t,column:l}=i,h=this._editor.getModel();return n||l===h.getLineMaxColumn(t)?(t===h.getLineCount()?t=1:t++,l=1):l++,new w.Position(t,l)}_moveToNextMatch(i){if(!this._state.canNavigateForward()){const t=this._decorations.matchBeforePosition(i);t&&this._setCurrentFindMatch(t);return}if(this._decorations.getCount()u._getSearchRange(this._editor.getModel(),h));return this._editor.getModel().findMatches(this._state.searchString,l,this._state.isRegex,this._state.matchCase,this._state.wholeWord?this._editor.getOption(110):null,n,t)}replaceAll(){if(!!this._hasMatches()){const i=this._decorations.getFindScopes();i===null&&this._state.matchesCount>=e.MATCHES_LIMIT?this._largeReplaceAll():this._regularReplaceAll(i),this.research(!1)}}_largeReplaceAll(){const n=new d.SearchParams(this._state.searchString,this._state.isRegex,this._state.matchCase,this._state.wholeWord?this._editor.getOption(110):null).parseSearchRequest();if(!!n){let t=n.regex;if(!t.multiline){let L="mu";t.ignoreCase&&(L+="i"),t.global&&(L+="g"),t=new RegExp(t.source,L)}const l=this._editor.getModel(),h=l.getValue(1),m=l.getFullModelRange(),_=this._getReplacePattern();let f;const v=this._state.preserveCase;_.hasReplacementPatterns||v?f=h.replace(t,function(){return _.buildReplaceString(arguments,v)}):f=h.replace(t,_.buildReplaceString(null,v));let y=new M.ReplaceCommandThatPreservesSelection(m,f,this._editor.getSelection());this._executeEditorCommand("replaceAll",y)}}_regularReplaceAll(i){const n=this._getReplacePattern();let t=this._findMatches(i,n.hasReplacementPatterns||this._state.preserveCase,1073741824),l=[];for(let m=0,_=t.length;m<_;m++)l[m]=n.buildReplaceString(t[m].matches,this._state.preserveCase);let h=new p.ReplaceAllCommand(this._editor.getSelection(),t.map(m=>m.range),l);this._executeEditorCommand("replaceAll",h)}selectAllMatches(){if(!!this._hasMatches()){let i=this._decorations.getFindScopes(),t=this._findMatches(i,!1,1073741824).map(h=>new C.Selection(h.range.startLineNumber,h.range.startColumn,h.range.endLineNumber,h.range.endColumn)),l=this._editor.getSelection();for(let h=0,m=t.length;hthis._hide(),2e3)),this._isVisible=!1,this._editor=c,this._state=o,this._keybindingService=s,this._domNode=document.createElement("div"),this._domNode.className="findOptionsWidget",this._domNode.style.display="none",this._domNode.style.top="10px",this._domNode.setAttribute("role","presentation"),this._domNode.setAttribute("aria-hidden","true");const u=a.getColorTheme().getColor(C.inputActiveOptionBorder),r=a.getColorTheme().getColor(C.inputActiveOptionForeground),i=a.getColorTheme().getColor(C.inputActiveOptionBackground);this.caseSensitive=this._register(new N.CaseSensitiveCheckbox({appendTitle:this._keybindingLabelFor(S.FIND_IDS.ToggleCaseSensitiveCommand),isChecked:this._state.matchCase,inputActiveOptionBorder:u,inputActiveOptionForeground:r,inputActiveOptionBackground:i})),this._domNode.appendChild(this.caseSensitive.domNode),this._register(this.caseSensitive.onChange(()=>{this._state.change({matchCase:this.caseSensitive.checked},!1)})),this.wholeWords=this._register(new N.WholeWordsCheckbox({appendTitle:this._keybindingLabelFor(S.FIND_IDS.ToggleWholeWordCommand),isChecked:this._state.wholeWord,inputActiveOptionBorder:u,inputActiveOptionForeground:r,inputActiveOptionBackground:i})),this._domNode.appendChild(this.wholeWords.domNode),this._register(this.wholeWords.onChange(()=>{this._state.change({wholeWord:this.wholeWords.checked},!1)})),this.regex=this._register(new N.RegexCheckbox({appendTitle:this._keybindingLabelFor(S.FIND_IDS.ToggleRegexCommand),isChecked:this._state.isRegex,inputActiveOptionBorder:u,inputActiveOptionForeground:r,inputActiveOptionBackground:i})),this._domNode.appendChild(this.regex.domNode),this._register(this.regex.onChange(()=>{this._state.change({isRegex:this.regex.checked},!1)})),this._editor.addOverlayWidget(this),this._register(this._state.onFindReplaceStateChange(n=>{let t=!1;n.isRegex&&(this.regex.checked=this._state.isRegex,t=!0),n.wholeWord&&(this.wholeWords.checked=this._state.wholeWord,t=!0),n.matchCase&&(this.caseSensitive.checked=this._state.matchCase,t=!0),!this._state.isRevealed&&t&&this._revealTemporarily()})),this._register(b.addDisposableNonBubblingMouseOutListener(this._domNode,n=>this._onMouseOut())),this._register(b.addDisposableListener(this._domNode,"mouseover",n=>this._onMouseOver())),this._applyTheme(a.getColorTheme()),this._register(a.onDidColorThemeChange(this._applyTheme.bind(this)))}_keybindingLabelFor(c){let o=this._keybindingService.lookupKeybinding(c);return o?` (${o.getLabel()})`:""}dispose(){this._editor.removeOverlayWidget(this),super.dispose()}getId(){return g.ID}getDomNode(){return this._domNode}getPosition(){return{preference:0}}highlightFindOptions(){this._revealTemporarily()}_revealTemporarily(){this._show(),this._hideSoon.schedule()}_onMouseOut(){this._hideSoon.schedule()}_onMouseOver(){this._hideSoon.cancel()}_show(){this._isVisible||(this._isVisible=!0,this._domNode.style.display="block")}_hide(){!this._isVisible||(this._isVisible=!1,this._domNode.style.display="none")}_applyTheme(c){let o={inputActiveOptionBorder:c.getColor(C.inputActiveOptionBorder),inputActiveOptionForeground:c.getColor(C.inputActiveOptionForeground),inputActiveOptionBackground:c.getColor(C.inputActiveOptionBackground)};this.caseSensitive.style(o),this.wholeWords.style(o),this.regex.style(o)}}e.FindOptionsWidget=g,g.ID="editor.contrib.findOptionsWidget",d.registerThemingParticipant((p,c)=>{const o=p.getColor(C.editorWidgetBackground);o&&c.addRule(`.monaco-editor .findOptionsWidget { background-color: ${o}; }`);const s=p.getColor(C.editorWidgetForeground);s&&c.addRule(`.monaco-editor .findOptionsWidget { color: ${s}; }`);const a=p.getColor(C.widgetShadow);a&&c.addRule(`.monaco-editor .findOptionsWidget { box-shadow: 0 0 8px 2px ${a}; }`);const u=p.getColor(C.contrastBorder);u&&c.addRule(`.monaco-editor .findOptionsWidget { border: 2px solid ${u}; }`)})}),define(Q[600],J([0,1,6,2,3,142]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FindReplaceState=void 0;function S(d,g){return d===1?!0:d===2?!1:g}class C extends N.Disposable{constructor(){super();this._onFindReplaceStateChange=this._register(new b.Emitter),this.onFindReplaceStateChange=this._onFindReplaceStateChange.event,this._searchString="",this._replaceString="",this._isRevealed=!1,this._isReplaceRevealed=!1,this._isRegex=!1,this._isRegexOverride=0,this._wholeWord=!1,this._wholeWordOverride=0,this._matchCase=!1,this._matchCaseOverride=0,this._preserveCase=!1,this._preserveCaseOverride=0,this._searchScope=null,this._matchesPosition=0,this._matchesCount=0,this._currentMatch=null,this._loop=!0}get searchString(){return this._searchString}get replaceString(){return this._replaceString}get isRevealed(){return this._isRevealed}get isReplaceRevealed(){return this._isReplaceRevealed}get isRegex(){return S(this._isRegexOverride,this._isRegex)}get wholeWord(){return S(this._wholeWordOverride,this._wholeWord)}get matchCase(){return S(this._matchCaseOverride,this._matchCase)}get preserveCase(){return S(this._preserveCaseOverride,this._preserveCase)}get actualIsRegex(){return this._isRegex}get actualWholeWord(){return this._wholeWord}get actualMatchCase(){return this._matchCase}get actualPreserveCase(){return this._preserveCase}get searchScope(){return this._searchScope}get matchesPosition(){return this._matchesPosition}get matchesCount(){return this._matchesCount}get currentMatch(){return this._currentMatch}changeMatchInfo(g,p,c){let o={moveCursor:!1,updateHistory:!1,searchString:!1,replaceString:!1,isRevealed:!1,isReplaceRevealed:!1,isRegex:!1,wholeWord:!1,matchCase:!1,preserveCase:!1,searchScope:!1,matchesPosition:!1,matchesCount:!1,currentMatch:!1,loop:!1},s=!1;p===0&&(g=0),g>p&&(g=p),this._matchesPosition!==g&&(this._matchesPosition=g,o.matchesPosition=!0,s=!0),this._matchesCount!==p&&(this._matchesCount=p,o.matchesCount=!0,s=!0),typeof c!="undefined"&&(M.Range.equalsRange(this._currentMatch,c)||(this._currentMatch=c,o.currentMatch=!0,s=!0)),s&&this._onFindReplaceStateChange.fire(o)}change(g,p,c=!0){var o;let s={moveCursor:p,updateHistory:c,searchString:!1,replaceString:!1,isRevealed:!1,isReplaceRevealed:!1,isRegex:!1,wholeWord:!1,matchCase:!1,preserveCase:!1,searchScope:!1,matchesPosition:!1,matchesCount:!1,currentMatch:!1,loop:!1},a=!1;const u=this.isRegex,r=this.wholeWord,i=this.matchCase,n=this.preserveCase;typeof g.searchString!="undefined"&&this._searchString!==g.searchString&&(this._searchString=g.searchString,s.searchString=!0,a=!0),typeof g.replaceString!="undefined"&&this._replaceString!==g.replaceString&&(this._replaceString=g.replaceString,s.replaceString=!0,a=!0),typeof g.isRevealed!="undefined"&&this._isRevealed!==g.isRevealed&&(this._isRevealed=g.isRevealed,s.isRevealed=!0,a=!0),typeof g.isReplaceRevealed!="undefined"&&this._isReplaceRevealed!==g.isReplaceRevealed&&(this._isReplaceRevealed=g.isReplaceRevealed,s.isReplaceRevealed=!0,a=!0),typeof g.isRegex!="undefined"&&(this._isRegex=g.isRegex),typeof g.wholeWord!="undefined"&&(this._wholeWord=g.wholeWord),typeof g.matchCase!="undefined"&&(this._matchCase=g.matchCase),typeof g.preserveCase!="undefined"&&(this._preserveCase=g.preserveCase),typeof g.searchScope!="undefined"&&(((o=g.searchScope)===null||o===void 0?void 0:o.every(t=>{var l;return(l=this._searchScope)===null||l===void 0?void 0:l.some(h=>!M.Range.equalsRange(h,t))}))||(this._searchScope=g.searchScope,s.searchScope=!0,a=!0)),typeof g.loop!="undefined"&&this._loop!==g.loop&&(this._loop=g.loop,s.loop=!0,a=!0),this._isRegexOverride=typeof g.isRegexOverride!="undefined"?g.isRegexOverride:0,this._wholeWordOverride=typeof g.wholeWordOverride!="undefined"?g.wholeWordOverride:0,this._matchCaseOverride=typeof g.matchCaseOverride!="undefined"?g.matchCaseOverride:0,this._preserveCaseOverride=typeof g.preserveCaseOverride!="undefined"?g.preserveCaseOverride:0,u!==this.isRegex&&(a=!0,s.isRegex=!0),r!==this.wholeWord&&(a=!0,s.wholeWord=!0),i!==this.matchCase&&(a=!0,s.matchCase=!0),n!==this.preserveCase&&(a=!0,s.preserveCase=!0),a&&this._onFindReplaceStateChange.fire(s)}canNavigateBack(){return this.canNavigateInLoop()||this.matchesPosition!==1}canNavigateForward(){return this.canNavigateInLoop()||this.matchesPosition=w.MATCHES_LIMIT}}e.FindReplaceState=C}),define(Q[601],J([0,1,132,67,175,205,115,11,116,7,484,159,44,2,9,37,66,157]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AccessibilityProvider=e.OneReferenceRenderer=e.FileReferencesRenderer=e.IdentityProvider=e.StringRepresentationProvider=e.Delegate=e.DataSource=void 0;let n=class{constructor(I){this._resolverService=I}hasChildren(I){return I instanceof b.ReferencesModel||I instanceof b.FileReferences}getChildren(I){if(I instanceof b.ReferencesModel)return I.groups;if(I instanceof b.FileReferences)return I.resolve(this._resolverService).then(k=>k.children);throw new Error("bad tree")}};n=Me([_e(0,N.ITextModelService)],n),e.DataSource=n;class t{getHeight(){return 23}getTemplateId(I){return I instanceof b.FileReferences?_.id:v.id}}e.Delegate=t;let l=class{constructor(I){this._keybindingService=I}getKeyboardNavigationLabel(I){var k;if(I instanceof b.OneReference){const E=(k=I.parent.getPreview(I))===null||k===void 0?void 0:k.preview(I.range);if(E)return E.value}return o.basename(I.uri)}};l=Me([_e(0,u.IKeybindingService)],l),e.StringRepresentationProvider=l;class h{getId(I){return I instanceof b.OneReference?I.id:I.uri}}e.IdentityProvider=h;let m=class extends s.Disposable{constructor(I,k,E){super();this._uriLabel=k;const T=document.createElement("div");T.classList.add("reference-file"),this.file=this._register(new M.IconLabel(T,{supportHighlights:!0})),this.badge=new w.CountBadge(g.append(T,g.$(".count"))),this._register(d.attachBadgeStyler(this.badge,E)),I.appendChild(T)}set(I,k){let E=o.dirname(I.uri);this.file.setLabel(c.getBaseLabel(I.uri),this._uriLabel.getUriLabel(E,{relative:!0}),{title:this._uriLabel.getUriLabel(I.uri),matches:k});const T=I.children.length;this.badge.setCount(T),T>1?this.badge.setTitleFormat(p.localize(0,null,T)):this.badge.setTitleFormat(p.localize(1,null,T))}};m=Me([_e(1,S.ILabelService),_e(2,C.IThemeService)],m);let _=class Ot{constructor(I){this._instantiationService=I,this.templateId=Ot.id}renderTemplate(I){return this._instantiationService.createInstance(m,I)}renderElement(I,k,E){E.set(I.element,r.createMatches(I.filterData))}disposeTemplate(I){I.dispose()}};_.id="FileReferencesRenderer",_=Me([_e(0,a.IInstantiationService)],_),e.FileReferencesRenderer=_;class f{constructor(I){this.label=new i.HighlightedLabel(I,!1)}set(I,k){var E;const T=(E=I.parent.getPreview(I))===null||E===void 0?void 0:E.preview(I.range);if(!T||!T.value)this.label.set(`${o.basename(I.uri)}:${I.range.startLineNumber+1}:${I.range.startColumn+1}`);else{const{value:O,highlight:A}=T;k&&!r.FuzzyScore.isDefault(k)?(this.label.element.classList.toggle("referenceMatch",!1),this.label.set(O,r.createMatches(k))):(this.label.element.classList.toggle("referenceMatch",!0),this.label.set(O,[A]))}}}class v{constructor(){this.templateId=v.id}renderTemplate(I){return new f(I)}renderElement(I,k,E){E.set(I.element,I.filterData)}disposeTemplate(){}}e.OneReferenceRenderer=v,v.id="OneReferenceRenderer";class y{getWidgetAriaLabel(){return p.localize(2,null)}getAriaLabel(I){return I.ariaMessage}}e.AccessibilityProvider=y}),define(Q[258],J([0,1,53,11,49,2,108,20,88]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractEditorNavigationQuickAccessProvider=void 0;class g{constructor(c){this.options=c,this.rangeHighlightDecorationId=void 0}provide(c,o){var s;const a=new w.DisposableStore;c.canAcceptInBackground=!!((s=this.options)===null||s===void 0?void 0:s.canAcceptInBackground),c.matchOnLabel=c.matchOnDescription=c.matchOnDetail=c.sortByLabel=!1;const u=a.add(new w.MutableDisposable);return u.value=this.doProvide(c,o),a.add(this.onDidActiveTextEditorControlChange(()=>{u.value=void 0,u.value=this.doProvide(c,o)})),a}doProvide(c,o){const s=new w.DisposableStore,a=this.activeTextEditorControl;if(a&&this.canProvideWithTextEditor(a)){const u={editor:a},r=S.getCodeEditor(a);if(r){let i=C.withNullAsUndefined(a.saveViewState());s.add(r.onDidChangeCursorPosition(()=>{i=C.withNullAsUndefined(a.saveViewState())})),u.restoreViewState=()=>{i&&a===this.activeTextEditorControl&&a.restoreViewState(i)},s.add(d.once(o.onCancellationRequested)(()=>{var n;return(n=u.restoreViewState)===null||n===void 0?void 0:n.call(u)}))}s.add(w.toDisposable(()=>this.clearDecorations(a))),s.add(this.provideWithTextEditor(u,c,o))}else s.add(this.provideWithoutTextEditor(c,o));return s}canProvideWithTextEditor(c){return!0}gotoLocation({editor:c},o){c.setSelection(o.range),c.revealRangeInCenter(o.range,0),o.preserveFocus||c.focus()}getModel(c){var o;return S.isDiffEditor(c)?(o=c.getModel())===null||o===void 0?void 0:o.modified:c.getModel()}addDecorations(c,o){c.changeDecorations(s=>{const a=[];this.rangeHighlightDecorationId&&(a.push(this.rangeHighlightDecorationId.overviewRulerDecorationId),a.push(this.rangeHighlightDecorationId.rangeHighlightId),this.rangeHighlightDecorationId=void 0);const u=[{range:o,options:{className:"rangeHighlight",isWholeLine:!0}},{range:o,options:{overviewRuler:{color:N.themeColorFromId(M.overviewRulerRangeHighlight),position:b.OverviewRulerLane.Full}}}],[r,i]=s.deltaDecorations(a,u);this.rangeHighlightDecorationId={rangeHighlightId:r,overviewRulerDecorationId:i}})}clearDecorations(c){const o=this.rangeHighlightDecorationId;o&&(c.changeDecorations(s=>{s.deltaDecorations([o.overviewRulerDecorationId,o.rangeHighlightId],[])}),this.rangeHighlightDecorationId=void 0)}}e.AbstractEditorNavigationQuickAccessProvider=g}),define(Q[602],J([0,1,501,2,258,108]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractGotoLineQuickAccessProvider=void 0;class S extends M.AbstractEditorNavigationQuickAccessProvider{constructor(){super({canAcceptInBackground:!0})}provideWithoutTextEditor(d){const g=b.localize(0,null);return d.items=[{label:g}],d.ariaLabel=g,N.Disposable.None}provideWithTextEditor(d,g,p){const c=d.editor,o=new N.DisposableStore;o.add(g.onDidAccept(u=>{const[r]=g.selectedItems;if(r){if(!this.isValidLineNumber(c,r.lineNumber))return;this.gotoLocation(d,{range:this.toRange(r.lineNumber,r.column),keyMods:g.keyMods,preserveFocus:u.inBackground}),u.inBackground||g.hide()}}));const s=()=>{const u=this.parsePosition(c,g.value.trim().substr(S.PREFIX.length)),r=this.getPickLabel(c,u.lineNumber,u.column);if(g.items=[{lineNumber:u.lineNumber,column:u.column,label:r}],g.ariaLabel=r,!this.isValidLineNumber(c,u.lineNumber)){this.clearDecorations(c);return}const i=this.toRange(u.lineNumber,u.column);c.revealRangeInCenter(i,0),this.addDecorations(c,i)};s(),o.add(g.onDidChangeValue(()=>s()));const a=w.getCodeEditor(c);return a&&a.getOptions().get(54).renderType===2&&(a.updateOptions({lineNumbers:"on"}),o.add(N.toDisposable(()=>a.updateOptions({lineNumbers:"relative"})))),o}toRange(d=1,g=1){return{startLineNumber:d,startColumn:g,endLineNumber:d,endColumn:g}}parsePosition(d,g){const p=g.split(/,|:|#/).map(o=>parseInt(o,10)).filter(o=>!isNaN(o)),c=this.lineCount(d)+1;return{lineNumber:p[0]>0?p[0]:c+p[0],column:p[1]}}getPickLabel(d,g,p){if(this.isValidLineNumber(d,g))return this.isValidColumn(d,g,p)?b.localize(1,null,g,p):b.localize(2,null,g);const c=d.getPosition()||{lineNumber:1,column:1},o=this.lineCount(d);return o>1?b.localize(3,null,c.lineNumber,c.column,o):b.localize(4,null,c.lineNumber,c.column)}isValidLineNumber(d,g){return!g||typeof g!="number"?!1:g>0&&g<=this.lineCount(d)}isValidColumn(d,g,p){if(!p||typeof p!="number")return!1;const c=this.getModel(d);if(!c)return!1;const o={lineNumber:g,column:p};return c.validatePosition(o).equals(o)}lineCount(d){var g,p;return(p=(g=this.getModel(d))===null||g===void 0?void 0:g.getLineCount())!==null&&p!==void 0?p:0}}e.AbstractGotoLineQuickAccessProvider=S,S.PREFIX=":"}),define(Q[603],J([0,1,502,23,2,3,258,18,245,8,286,27]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AbstractGotoSymbolQuickAccessProvider=void 0;class o extends S.AbstractEditorNavigationQuickAccessProvider{constructor(r=Object.create(null)){super(r);this.options=r,r.canAcceptInBackground=!0}provideWithoutTextEditor(r){return this.provideLabelPick(r,b.localize(0,null)),M.Disposable.None}provideWithTextEditor(r,i,n){const t=r.editor,l=this.getModel(t);return l?C.DocumentSymbolProviderRegistry.has(l)?this.doProvideWithEditorSymbols(r,l,i,n):this.doProvideWithoutEditorSymbols(r,l,i,n):M.Disposable.None}doProvideWithoutEditorSymbols(r,i,n,t){const l=new M.DisposableStore;return this.provideLabelPick(n,b.localize(1,null)),(()=>Ie(this,void 0,void 0,function*(){!(yield this.waitForLanguageSymbolRegistry(i,l))||t.isCancellationRequested||l.add(this.doProvideWithEditorSymbols(r,i,n,t))}))(),l}provideLabelPick(r,i){r.items=[{label:i,index:0,kind:14}],r.ariaLabel=i}waitForLanguageSymbolRegistry(r,i){return Ie(this,void 0,void 0,function*(){if(C.DocumentSymbolProviderRegistry.has(r))return!0;let n;const t=new Promise(h=>n=h),l=i.add(C.DocumentSymbolProviderRegistry.onDidChange(()=>{C.DocumentSymbolProviderRegistry.has(r)&&(l.dispose(),n(!0))}));return i.add(M.toDisposable(()=>n(!1))),t})}doProvideWithEditorSymbols(r,i,n,t){const l=r.editor,h=new M.DisposableStore;h.add(n.onDidAccept(y=>{const[L]=n.selectedItems;L&&L.range&&(this.gotoLocation(r,{range:L.range.selection,keyMods:n.keyMods,preserveFocus:y.inBackground}),y.inBackground||n.hide())})),h.add(n.onDidTriggerItemButton(({item:y})=>{y&&y.range&&(this.gotoLocation(r,{range:y.range.selection,keyMods:n.keyMods,forceSideBySide:!0}),n.hide())}));const m=this.getDocumentSymbols(i,t);let _;const f=()=>Ie(this,void 0,void 0,function*(){_==null||_.dispose(!0),n.busy=!1,_=new N.CancellationTokenSource(t),n.busy=!0;try{const y=p.prepareQuery(n.value.substr(o.PREFIX.length).trim()),L=yield this.doGetSymbolPicks(m,y,void 0,_.token);if(t.isCancellationRequested)return;L.length>0?n.items=L:y.original.length>0?this.provideLabelPick(n,b.localize(2,null)):this.provideLabelPick(n,b.localize(3,null))}finally{t.isCancellationRequested||(n.busy=!1)}});h.add(n.onDidChangeValue(()=>f())),f();let v=!0;return h.add(n.onDidChangeActive(()=>{const[y]=n.activeItems;if(y&&y.range){if(v){v=!1;return}l.revealRangeInCenter(y.range.selection,0),this.addDecorations(l,y.range.decoration)}})),h}doGetSymbolPicks(r,i,n,t){return Ie(this,void 0,void 0,function*(){const l=yield r;if(t.isCancellationRequested)return[];const h=i.original.indexOf(o.SCOPE_PREFIX)===0,m=h?1:0;let _,f;i.values&&i.values.length>1?(_=p.pieceToQuery(i.values[0]),f=p.pieceToQuery(i.values.slice(1))):_=i;const v=[];for(let k=0;km){let K=!1;if(_!==i&&([F,D]=p.scoreFuzzy2(O,Object.assign(Object.assign({},i),{values:void 0}),m,A),typeof F=="number"&&(K=!0)),typeof F!="number"&&([F,D]=p.scoreFuzzy2(O,_,m,A),typeof F!="number"))continue;if(!K&&f){if(B&&f.original.length>0&&([R,W]=p.scoreFuzzy2(B,f)),typeof R!="number")continue;typeof F=="number"&&(F+=R)}}const x=E.tags&&E.tags.indexOf(1)>=0;v.push({index:k,kind:E.kind,score:F,label:O,ariaLabel:T,description:B,highlights:x?void 0:{label:D,description:W},range:{selection:w.Range.collapseToStart(E.selectionRange),decoration:E.range},strikethrough:x,buttons:(()=>{var K,Y;const ee=((K=this.options)===null||K===void 0?void 0:K.openSideBySideDirection)?(Y=this.options)===null||Y===void 0?void 0:Y.openSideBySideDirection():void 0;if(!!ee)return[{iconClass:ee==="right"?c.Codicon.splitHorizontal.classNames:c.Codicon.splitVertical.classNames,tooltip:ee==="right"?b.localize(4,null):b.localize(5,null)}]})()})}const y=v.sort((k,E)=>h?this.compareByKindAndScore(k,E):this.compareByScore(k,E));let L=[];if(h){let k,E,T=0;function I(){E&&typeof k=="number"&&T>0&&(E.label=g.format(a[k]||s,T))}for(const O of y)k!==O.kind?(I(),k=O.kind,T=1,E={type:"separator"},L.push(E)):T++,L.push(O);I()}else y.length>0&&(L=[{label:b.localize(6,null,v.length),type:"separator"},...y]);return L})}compareByScore(r,i){if(typeof r.score!="number"&&typeof i.score=="number")return 1;if(typeof r.score=="number"&&typeof i.score!="number")return-1;if(typeof r.score=="number"&&typeof i.score=="number"){if(r.score>i.score)return-1;if(r.scorei.index?1:0}compareByKindAndScore(r,i){const n=a[r.kind]||s,t=a[i.kind]||s,l=n.localeCompare(t);return l===0?this.compareByScore(r,i):l}getDocumentSymbols(r,i){return Ie(this,void 0,void 0,function*(){const n=yield d.OutlineModel.create(r,i);return i.isCancellationRequested?[]:n.asListOfDocumentSymbols()})}}e.AbstractGotoSymbolQuickAccessProvider=o,o.PREFIX="@",o.SCOPE_PREFIX=":",o.PREFIX_BY_CATEGORY=`${o.PREFIX}${o.SCOPE_PREFIX}`;const s=b.localize(7,null),a={[5]:b.localize(8,null),[11]:b.localize(9,null),[8]:b.localize(10,null),[12]:b.localize(11,null),[4]:b.localize(12,null),[22]:b.localize(13,null),[23]:b.localize(14,null),[24]:b.localize(15,null),[10]:b.localize(16,null),[2]:b.localize(17,null),[3]:b.localize(18,null),[25]:b.localize(19,null),[1]:b.localize(20,null),[6]:b.localize(21,null),[9]:b.localize(22,null),[21]:b.localize(23,null),[14]:b.localize(24,null),[0]:b.localize(25,null),[17]:b.localize(26,null),[15]:b.localize(27,null),[16]:b.localize(28,null),[18]:b.localize(29,null),[19]:b.localize(30,null),[7]:b.localize(31,null),[13]:b.localize(32,null)}}),define(Q[604],J([0,1,2,14,504,16,22,11,37,350]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RenameInputField=e.CONTEXT_RENAME_INPUT_VISIBLE=void 0,e.CONTEXT_RENAME_INPUT_VISIBLE=new w.RawContextKey("renameInputVisible",!1);let g=class{constructor(c,o,s,a,u){this._editor=c,this._acceptKeybindings=o,this._themeService=s,this._keybindingService=a,this._disposables=new b.DisposableStore,this.allowEditorOverflow=!0,this._visibleContextKey=e.CONTEXT_RENAME_INPUT_VISIBLE.bindTo(u),this._editor.addContentWidget(this),this._disposables.add(this._editor.onDidChangeConfiguration(r=>{r.hasChanged(38)&&this._updateFont()})),this._disposables.add(s.onDidColorThemeChange(this._updateStyles,this))}dispose(){this._disposables.dispose(),this._editor.removeContentWidget(this)}getId(){return"__renameInputWidget"}getDomNode(){if(!this._domNode){this._domNode=document.createElement("div"),this._domNode.className="monaco-editor rename-box",this._input=document.createElement("input"),this._input.className="rename-input",this._input.type="text",this._input.setAttribute("aria-label",M.localize(0,null)),this._domNode.appendChild(this._input),this._label=document.createElement("div"),this._label.className="rename-label",this._domNode.appendChild(this._label);const c=()=>{var o,s;const[a,u]=this._acceptKeybindings;this._keybindingService.lookupKeybinding(a),this._label.innerText=M.localize(1,null,(o=this._keybindingService.lookupKeybinding(a))===null||o===void 0?void 0:o.getLabel(),(s=this._keybindingService.lookupKeybinding(u))===null||s===void 0?void 0:s.getLabel())};c(),this._disposables.add(this._keybindingService.onDidUpdateKeybindings(c)),this._updateFont(),this._updateStyles(this._themeService.getColorTheme())}return this._domNode}_updateStyles(c){var o,s,a,u;if(!(!this._input||!this._domNode)){const r=c.getColor(S.widgetShadow);this._domNode.style.backgroundColor=String((o=c.getColor(S.editorWidgetBackground))!==null&&o!==void 0?o:""),this._domNode.style.boxShadow=r?` 0 0 8px 2px ${r}`:"",this._domNode.style.color=String((s=c.getColor(S.inputForeground))!==null&&s!==void 0?s:""),this._input.style.backgroundColor=String((a=c.getColor(S.inputBackground))!==null&&a!==void 0?a:"");const i=c.getColor(S.inputBorder);this._input.style.borderWidth=i?"1px":"0px",this._input.style.borderStyle=i?"solid":"none",this._input.style.borderColor=(u=i==null?void 0:i.toString())!==null&&u!==void 0?u:"none"}}_updateFont(){if(!(!this._input||!this._label)){const c=this._editor.getOption(38);this._input.style.fontFamily=c.fontFamily,this._input.style.fontWeight=c.fontWeight,this._input.style.fontSize=`${c.fontSize}px`,this._label.style.fontSize=`${c.fontSize*.8}px`}}getPosition(){return this._visible?{position:this._position,preference:[2,1]}:null}afterRender(c){c||this.cancelInput(!0)}acceptInput(c){this._currentAcceptInput&&this._currentAcceptInput(c)}cancelInput(c){this._currentCancelInput&&this._currentCancelInput(c)}getInput(c,o,s,a,u,r){this._domNode.classList.toggle("preview",u),this._position=new N.Position(c.startLineNumber,c.startColumn),this._input.value=o,this._input.setAttribute("selectionStart",s.toString()),this._input.setAttribute("selectionEnd",a.toString()),this._input.size=Math.max((c.endColumn-c.startColumn)*1.1,20);const i=new b.DisposableStore;return new Promise(n=>{this._currentCancelInput=t=>(this._currentAcceptInput=void 0,this._currentCancelInput=void 0,n(t),!0),this._currentAcceptInput=t=>{if(this._input.value.trim().length===0||this._input.value===o){this.cancelInput(!0);return}this._currentAcceptInput=void 0,this._currentCancelInput=void 0,n({newName:this._input.value,wantsPreview:u&&t})},r.onCancellationRequested(()=>this.cancelInput(!0)),i.add(this._editor.onDidBlurEditorWidget(()=>this.cancelInput(!1))),this._show()}).finally(()=>{i.dispose(),this._hide()})}_show(){this._editor.revealLineInCenterIfOutsideViewport(this._position.lineNumber,0),this._visible=!0,this._visibleContextKey.set(!0),this._editor.layoutContentWidget(this),setTimeout(()=>{this._input.focus(),this._input.setSelectionRange(parseInt(this._input.getAttribute("selectionStart")),parseInt(this._input.getAttribute("selectionEnd")))},100)}_hide(){this._visible=!1,this._visibleContextKey.reset(),this._editor.layoutContentWidget(this)}};g=Me([_e(2,C.IThemeService),_e(3,d.IKeybindingService),_e(4,w.IContextKeyService)],g),e.RenameInputField=g}),define(Q[259],J([0,1,512,11,22,27]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SYMBOL_ICON_VARIABLE_FOREGROUND=e.SYMBOL_ICON_UNIT_FOREGROUND=e.SYMBOL_ICON_TYPEPARAMETER_FOREGROUND=e.SYMBOL_ICON_TEXT_FOREGROUND=e.SYMBOL_ICON_STRUCT_FOREGROUND=e.SYMBOL_ICON_STRING_FOREGROUND=e.SYMBOL_ICON_SNIPPET_FOREGROUND=e.SYMBOL_ICON_REFERENCE_FOREGROUND=e.SYMBOL_ICON_PROPERTY_FOREGROUND=e.SYMBOL_ICON_PACKAGE_FOREGROUND=e.SYMBOL_ICON_OPERATOR_FOREGROUND=e.SYMBOL_ICON_OBJECT_FOREGROUND=e.SYMBOL_ICON_NUMBER_FOREGROUND=e.SYMBOL_ICON_NULL_FOREGROUND=e.SYMBOL_ICON_NAMESPACE_FOREGROUND=e.SYMBOL_ICON_MODULE_FOREGROUND=e.SYMBOL_ICON_METHOD_FOREGROUND=e.SYMBOL_ICON_KEYWORD_FOREGROUND=e.SYMBOL_ICON_KEY_FOREGROUND=e.SYMBOL_ICON_INTERFACE_FOREGROUND=e.SYMBOL_ICON_FUNCTION_FOREGROUND=e.SYMBOL_ICON_FOLDER_FOREGROUND=e.SYMBOL_ICON_FILE_FOREGROUND=e.SYMBOL_ICON_FIELD_FOREGROUND=e.SYMBOL_ICON_EVENT_FOREGROUND=e.SYMBOL_ICON_ENUMERATOR_MEMBER_FOREGROUND=e.SYMBOL_ICON_ENUMERATOR_FOREGROUND=e.SYMBOL_ICON_CONSTRUCTOR_FOREGROUND=e.SYMBOL_ICON_CONSTANT_FOREGROUND=e.SYMBOL_ICON_COLOR_FOREGROUND=e.SYMBOL_ICON_CLASS_FOREGROUND=e.SYMBOL_ICON_BOOLEAN_FOREGROUND=e.SYMBOL_ICON_ARRAY_FOREGROUND=void 0,e.SYMBOL_ICON_ARRAY_FOREGROUND=M.registerColor("symbolIcon.arrayForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(0,null)),e.SYMBOL_ICON_BOOLEAN_FOREGROUND=M.registerColor("symbolIcon.booleanForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(1,null)),e.SYMBOL_ICON_CLASS_FOREGROUND=M.registerColor("symbolIcon.classForeground",{dark:"#EE9D28",light:"#D67E00",hc:"#EE9D28"},b.localize(2,null)),e.SYMBOL_ICON_COLOR_FOREGROUND=M.registerColor("symbolIcon.colorForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(3,null)),e.SYMBOL_ICON_CONSTANT_FOREGROUND=M.registerColor("symbolIcon.constantForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(4,null)),e.SYMBOL_ICON_CONSTRUCTOR_FOREGROUND=M.registerColor("symbolIcon.constructorForeground",{dark:"#B180D7",light:"#652D90",hc:"#B180D7"},b.localize(5,null)),e.SYMBOL_ICON_ENUMERATOR_FOREGROUND=M.registerColor("symbolIcon.enumeratorForeground",{dark:"#EE9D28",light:"#D67E00",hc:"#EE9D28"},b.localize(6,null)),e.SYMBOL_ICON_ENUMERATOR_MEMBER_FOREGROUND=M.registerColor("symbolIcon.enumeratorMemberForeground",{dark:"#75BEFF",light:"#007ACC",hc:"#75BEFF"},b.localize(7,null)),e.SYMBOL_ICON_EVENT_FOREGROUND=M.registerColor("symbolIcon.eventForeground",{dark:"#EE9D28",light:"#D67E00",hc:"#EE9D28"},b.localize(8,null)),e.SYMBOL_ICON_FIELD_FOREGROUND=M.registerColor("symbolIcon.fieldForeground",{dark:"#75BEFF",light:"#007ACC",hc:"#75BEFF"},b.localize(9,null)),e.SYMBOL_ICON_FILE_FOREGROUND=M.registerColor("symbolIcon.fileForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(10,null)),e.SYMBOL_ICON_FOLDER_FOREGROUND=M.registerColor("symbolIcon.folderForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(11,null)),e.SYMBOL_ICON_FUNCTION_FOREGROUND=M.registerColor("symbolIcon.functionForeground",{dark:"#B180D7",light:"#652D90",hc:"#B180D7"},b.localize(12,null)),e.SYMBOL_ICON_INTERFACE_FOREGROUND=M.registerColor("symbolIcon.interfaceForeground",{dark:"#75BEFF",light:"#007ACC",hc:"#75BEFF"},b.localize(13,null)),e.SYMBOL_ICON_KEY_FOREGROUND=M.registerColor("symbolIcon.keyForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(14,null)),e.SYMBOL_ICON_KEYWORD_FOREGROUND=M.registerColor("symbolIcon.keywordForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(15,null)),e.SYMBOL_ICON_METHOD_FOREGROUND=M.registerColor("symbolIcon.methodForeground",{dark:"#B180D7",light:"#652D90",hc:"#B180D7"},b.localize(16,null)),e.SYMBOL_ICON_MODULE_FOREGROUND=M.registerColor("symbolIcon.moduleForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(17,null)),e.SYMBOL_ICON_NAMESPACE_FOREGROUND=M.registerColor("symbolIcon.namespaceForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(18,null)),e.SYMBOL_ICON_NULL_FOREGROUND=M.registerColor("symbolIcon.nullForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(19,null)),e.SYMBOL_ICON_NUMBER_FOREGROUND=M.registerColor("symbolIcon.numberForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(20,null)),e.SYMBOL_ICON_OBJECT_FOREGROUND=M.registerColor("symbolIcon.objectForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(21,null)),e.SYMBOL_ICON_OPERATOR_FOREGROUND=M.registerColor("symbolIcon.operatorForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(22,null)),e.SYMBOL_ICON_PACKAGE_FOREGROUND=M.registerColor("symbolIcon.packageForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(23,null)),e.SYMBOL_ICON_PROPERTY_FOREGROUND=M.registerColor("symbolIcon.propertyForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(24,null)),e.SYMBOL_ICON_REFERENCE_FOREGROUND=M.registerColor("symbolIcon.referenceForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(25,null)),e.SYMBOL_ICON_SNIPPET_FOREGROUND=M.registerColor("symbolIcon.snippetForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(26,null)),e.SYMBOL_ICON_STRING_FOREGROUND=M.registerColor("symbolIcon.stringForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(27,null)),e.SYMBOL_ICON_STRUCT_FOREGROUND=M.registerColor("symbolIcon.structForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(28,null)),e.SYMBOL_ICON_TEXT_FOREGROUND=M.registerColor("symbolIcon.textForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(29,null)),e.SYMBOL_ICON_TYPEPARAMETER_FOREGROUND=M.registerColor("symbolIcon.typeParameterForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(30,null)),e.SYMBOL_ICON_UNIT_FOREGROUND=M.registerColor("symbolIcon.unitForeground",{dark:M.foreground,light:M.foreground,hc:M.foreground},b.localize(31,null)),e.SYMBOL_ICON_VARIABLE_FOREGROUND=M.registerColor("symbolIcon.variableForeground",{dark:"#75BEFF",light:"#007ACC",hc:"#75BEFF"},b.localize(32,null)),N.registerThemingParticipant((S,C)=>{const d=S.getColor(e.SYMBOL_ICON_ARRAY_FOREGROUND);d&&C.addRule(`${w.Codicon.symbolArray.cssSelector} { color: ${d}; }`);const g=S.getColor(e.SYMBOL_ICON_BOOLEAN_FOREGROUND);g&&C.addRule(`${w.Codicon.symbolBoolean.cssSelector} { color: ${g}; }`);const p=S.getColor(e.SYMBOL_ICON_CLASS_FOREGROUND);p&&C.addRule(`${w.Codicon.symbolClass.cssSelector} { color: ${p}; }`);const c=S.getColor(e.SYMBOL_ICON_METHOD_FOREGROUND);c&&C.addRule(`${w.Codicon.symbolMethod.cssSelector} { color: ${c}; }`);const o=S.getColor(e.SYMBOL_ICON_COLOR_FOREGROUND);o&&C.addRule(`${w.Codicon.symbolColor.cssSelector} { color: ${o}; }`);const s=S.getColor(e.SYMBOL_ICON_CONSTANT_FOREGROUND);s&&C.addRule(`${w.Codicon.symbolConstant.cssSelector} { color: ${s}; }`);const a=S.getColor(e.SYMBOL_ICON_CONSTRUCTOR_FOREGROUND);a&&C.addRule(`${w.Codicon.symbolConstructor.cssSelector} { color: ${a}; }`);const u=S.getColor(e.SYMBOL_ICON_ENUMERATOR_FOREGROUND);u&&C.addRule(` + ${w.Codicon.symbolValue.cssSelector},${w.Codicon.symbolEnum.cssSelector} { color: ${u}; }`);const r=S.getColor(e.SYMBOL_ICON_ENUMERATOR_MEMBER_FOREGROUND);r&&C.addRule(`${w.Codicon.symbolEnumMember.cssSelector} { color: ${r}; }`);const i=S.getColor(e.SYMBOL_ICON_EVENT_FOREGROUND);i&&C.addRule(`${w.Codicon.symbolEvent.cssSelector} { color: ${i}; }`);const n=S.getColor(e.SYMBOL_ICON_FIELD_FOREGROUND);n&&C.addRule(`${w.Codicon.symbolField.cssSelector} { color: ${n}; }`);const t=S.getColor(e.SYMBOL_ICON_FILE_FOREGROUND);t&&C.addRule(`${w.Codicon.symbolFile.cssSelector} { color: ${t}; }`);const l=S.getColor(e.SYMBOL_ICON_FOLDER_FOREGROUND);l&&C.addRule(`${w.Codicon.symbolFolder.cssSelector} { color: ${l}; }`);const h=S.getColor(e.SYMBOL_ICON_FUNCTION_FOREGROUND);h&&C.addRule(`${w.Codicon.symbolFunction.cssSelector} { color: ${h}; }`);const m=S.getColor(e.SYMBOL_ICON_INTERFACE_FOREGROUND);m&&C.addRule(`${w.Codicon.symbolInterface.cssSelector} { color: ${m}; }`);const _=S.getColor(e.SYMBOL_ICON_KEY_FOREGROUND);_&&C.addRule(`${w.Codicon.symbolKey.cssSelector} { color: ${_}; }`);const f=S.getColor(e.SYMBOL_ICON_KEYWORD_FOREGROUND);f&&C.addRule(`${w.Codicon.symbolKeyword.cssSelector} { color: ${f}; }`);const v=S.getColor(e.SYMBOL_ICON_MODULE_FOREGROUND);v&&C.addRule(`${w.Codicon.symbolModule.cssSelector} { color: ${v}; }`);const y=S.getColor(e.SYMBOL_ICON_NAMESPACE_FOREGROUND);y&&C.addRule(`${w.Codicon.symbolNamespace.cssSelector} { color: ${y}; }`);const L=S.getColor(e.SYMBOL_ICON_NULL_FOREGROUND);L&&C.addRule(`${w.Codicon.symbolNull.cssSelector} { color: ${L}; }`);const I=S.getColor(e.SYMBOL_ICON_NUMBER_FOREGROUND);I&&C.addRule(`${w.Codicon.symbolNumber.cssSelector} { color: ${I}; }`);const k=S.getColor(e.SYMBOL_ICON_OBJECT_FOREGROUND);k&&C.addRule(`${w.Codicon.symbolObject.cssSelector} { color: ${k}; }`);const E=S.getColor(e.SYMBOL_ICON_OPERATOR_FOREGROUND);E&&C.addRule(`${w.Codicon.symbolOperator.cssSelector} { color: ${E}; }`);const T=S.getColor(e.SYMBOL_ICON_PACKAGE_FOREGROUND);T&&C.addRule(`${w.Codicon.symbolPackage.cssSelector} { color: ${T}; }`);const O=S.getColor(e.SYMBOL_ICON_PROPERTY_FOREGROUND);O&&C.addRule(`${w.Codicon.symbolProperty.cssSelector} { color: ${O}; }`);const A=S.getColor(e.SYMBOL_ICON_REFERENCE_FOREGROUND);A&&C.addRule(`${w.Codicon.symbolReference.cssSelector} { color: ${A}; }`);const B=S.getColor(e.SYMBOL_ICON_SNIPPET_FOREGROUND);B&&C.addRule(`${w.Codicon.symbolSnippet.cssSelector} { color: ${B}; }`);const F=S.getColor(e.SYMBOL_ICON_STRING_FOREGROUND);F&&C.addRule(`${w.Codicon.symbolString.cssSelector} { color: ${F}; }`);const D=S.getColor(e.SYMBOL_ICON_STRUCT_FOREGROUND);D&&C.addRule(`${w.Codicon.symbolStruct.cssSelector} { color: ${D}; }`);const R=S.getColor(e.SYMBOL_ICON_TEXT_FOREGROUND);R&&C.addRule(`${w.Codicon.symbolText.cssSelector} { color: ${R}; }`);const W=S.getColor(e.SYMBOL_ICON_TYPEPARAMETER_FOREGROUND);W&&C.addRule(`${w.Codicon.symbolTypeParameter.cssSelector} { color: ${W}; }`);const x=S.getColor(e.SYMBOL_ICON_UNIT_FOREGROUND);x&&C.addRule(`${w.Codicon.symbolUnit.cssSelector} { color: ${x}; }`);const K=S.getColor(e.SYMBOL_ICON_VARIABLE_FOREGROUND);K&&C.addRule(`${w.Codicon.symbolVariable.cssSelector} { color: ${K}; }`)})}),define(Q[605],J([0,1,7,43,583,16,11]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.StandaloneCodeEditorServiceImpl=void 0;let C=class extends M.CodeEditorServiceImpl{constructor(g,p,c){super(g,c);this.onCodeEditorAdd(()=>this._checkContextKey()),this.onCodeEditorRemove(()=>this._checkContextKey()),this._editorIsOpen=p.createKey("editorIsOpen",!1)}_checkContextKey(){let g=!1;for(const p of this.listCodeEditors())if(!p.isSimpleWidget){g=!0;break}this._editorIsOpen.set(g)}getActiveCodeEditor(){return null}openCodeEditor(g,p,c){return p?Promise.resolve(this.doOpenEditor(p,g)):Promise.resolve(null)}doOpenEditor(g,p){if(!this.findModel(g,p.resource)){if(p.resource){const s=p.resource.scheme;if(s===N.Schemas.http||s===N.Schemas.https)return b.windowOpenNoOpener(p.resource.toString()),g}return null}const o=p.options?p.options.selection:null;if(o)if(typeof o.endLineNumber=="number"&&typeof o.endColumn=="number")g.setSelection(o),g.revealRangeInCenter(o,1);else{const s={lineNumber:o.startLineNumber,column:o.startColumn};g.setPosition(s),g.revealPositionInCenter(s,1)}return g}findModel(g,p){const c=g.getModel();return c&&c.uri.toString()!==p.toString()?null:c}};C=Me([_e(1,w.IContextKeyService),_e(2,S.IThemeService)],C),e.StandaloneCodeEditorServiceImpl=C}),define(Q[606],J([0,1,49,22]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.hc_black=e.vs_dark=e.vs=void 0,e.vs={base:"vs",inherit:!1,rules:[{token:"",foreground:"000000",background:"fffffe"},{token:"invalid",foreground:"cd3131"},{token:"emphasis",fontStyle:"italic"},{token:"strong",fontStyle:"bold"},{token:"variable",foreground:"001188"},{token:"variable.predefined",foreground:"4864AA"},{token:"constant",foreground:"dd0000"},{token:"comment",foreground:"008000"},{token:"number",foreground:"098658"},{token:"number.hex",foreground:"3030c0"},{token:"regexp",foreground:"800000"},{token:"annotation",foreground:"808080"},{token:"type",foreground:"008080"},{token:"delimiter",foreground:"000000"},{token:"delimiter.html",foreground:"383838"},{token:"delimiter.xml",foreground:"0000FF"},{token:"tag",foreground:"800000"},{token:"tag.id.pug",foreground:"4F76AC"},{token:"tag.class.pug",foreground:"4F76AC"},{token:"meta.scss",foreground:"800000"},{token:"metatag",foreground:"e00000"},{token:"metatag.content.html",foreground:"FF0000"},{token:"metatag.html",foreground:"808080"},{token:"metatag.xml",foreground:"808080"},{token:"metatag.php",fontStyle:"bold"},{token:"key",foreground:"863B00"},{token:"string.key.json",foreground:"A31515"},{token:"string.value.json",foreground:"0451A5"},{token:"attribute.name",foreground:"FF0000"},{token:"attribute.value",foreground:"0451A5"},{token:"attribute.value.number",foreground:"098658"},{token:"attribute.value.unit",foreground:"098658"},{token:"attribute.value.html",foreground:"0000FF"},{token:"attribute.value.xml",foreground:"0000FF"},{token:"string",foreground:"A31515"},{token:"string.html",foreground:"0000FF"},{token:"string.sql",foreground:"FF0000"},{token:"string.yaml",foreground:"0451A5"},{token:"keyword",foreground:"0000FF"},{token:"keyword.json",foreground:"0451A5"},{token:"keyword.flow",foreground:"AF00DB"},{token:"keyword.flow.scss",foreground:"0000FF"},{token:"operator.scss",foreground:"666666"},{token:"operator.sql",foreground:"778899"},{token:"operator.swift",foreground:"666666"},{token:"predefined.sql",foreground:"C700C7"}],colors:{[N.editorBackground]:"#FFFFFE",[N.editorForeground]:"#000000",[N.editorInactiveSelection]:"#E5EBF1",[b.editorIndentGuides]:"#D3D3D3",[b.editorActiveIndentGuides]:"#939393",[N.editorSelectionHighlight]:"#ADD6FF4D"}},e.vs_dark={base:"vs-dark",inherit:!1,rules:[{token:"",foreground:"D4D4D4",background:"1E1E1E"},{token:"invalid",foreground:"f44747"},{token:"emphasis",fontStyle:"italic"},{token:"strong",fontStyle:"bold"},{token:"variable",foreground:"74B0DF"},{token:"variable.predefined",foreground:"4864AA"},{token:"variable.parameter",foreground:"9CDCFE"},{token:"constant",foreground:"569CD6"},{token:"comment",foreground:"608B4E"},{token:"number",foreground:"B5CEA8"},{token:"number.hex",foreground:"5BB498"},{token:"regexp",foreground:"B46695"},{token:"annotation",foreground:"cc6666"},{token:"type",foreground:"3DC9B0"},{token:"delimiter",foreground:"DCDCDC"},{token:"delimiter.html",foreground:"808080"},{token:"delimiter.xml",foreground:"808080"},{token:"tag",foreground:"569CD6"},{token:"tag.id.pug",foreground:"4F76AC"},{token:"tag.class.pug",foreground:"4F76AC"},{token:"meta.scss",foreground:"A79873"},{token:"meta.tag",foreground:"CE9178"},{token:"metatag",foreground:"DD6A6F"},{token:"metatag.content.html",foreground:"9CDCFE"},{token:"metatag.html",foreground:"569CD6"},{token:"metatag.xml",foreground:"569CD6"},{token:"metatag.php",fontStyle:"bold"},{token:"key",foreground:"9CDCFE"},{token:"string.key.json",foreground:"9CDCFE"},{token:"string.value.json",foreground:"CE9178"},{token:"attribute.name",foreground:"9CDCFE"},{token:"attribute.value",foreground:"CE9178"},{token:"attribute.value.number.css",foreground:"B5CEA8"},{token:"attribute.value.unit.css",foreground:"B5CEA8"},{token:"attribute.value.hex.css",foreground:"D4D4D4"},{token:"string",foreground:"CE9178"},{token:"string.sql",foreground:"FF0000"},{token:"keyword",foreground:"569CD6"},{token:"keyword.flow",foreground:"C586C0"},{token:"keyword.json",foreground:"CE9178"},{token:"keyword.flow.scss",foreground:"569CD6"},{token:"operator.scss",foreground:"909090"},{token:"operator.sql",foreground:"778899"},{token:"operator.swift",foreground:"909090"},{token:"predefined.sql",foreground:"FF00FF"}],colors:{[N.editorBackground]:"#1E1E1E",[N.editorForeground]:"#D4D4D4",[N.editorInactiveSelection]:"#3A3D41",[b.editorIndentGuides]:"#404040",[b.editorActiveIndentGuides]:"#707070",[N.editorSelectionHighlight]:"#ADD6FF26"}},e.hc_black={base:"hc-black",inherit:!1,rules:[{token:"",foreground:"FFFFFF",background:"000000"},{token:"invalid",foreground:"f44747"},{token:"emphasis",fontStyle:"italic"},{token:"strong",fontStyle:"bold"},{token:"variable",foreground:"1AEBFF"},{token:"variable.parameter",foreground:"9CDCFE"},{token:"constant",foreground:"569CD6"},{token:"comment",foreground:"608B4E"},{token:"number",foreground:"FFFFFF"},{token:"regexp",foreground:"C0C0C0"},{token:"annotation",foreground:"569CD6"},{token:"type",foreground:"3DC9B0"},{token:"delimiter",foreground:"FFFF00"},{token:"delimiter.html",foreground:"FFFF00"},{token:"tag",foreground:"569CD6"},{token:"tag.id.pug",foreground:"4F76AC"},{token:"tag.class.pug",foreground:"4F76AC"},{token:"meta",foreground:"D4D4D4"},{token:"meta.tag",foreground:"CE9178"},{token:"metatag",foreground:"569CD6"},{token:"metatag.content.html",foreground:"1AEBFF"},{token:"metatag.html",foreground:"569CD6"},{token:"metatag.xml",foreground:"569CD6"},{token:"metatag.php",fontStyle:"bold"},{token:"key",foreground:"9CDCFE"},{token:"string.key",foreground:"9CDCFE"},{token:"string.value",foreground:"CE9178"},{token:"attribute.name",foreground:"569CD6"},{token:"attribute.value",foreground:"3FF23F"},{token:"string",foreground:"CE9178"},{token:"string.sql",foreground:"FF0000"},{token:"keyword",foreground:"569CD6"},{token:"keyword.flow",foreground:"C586C0"},{token:"operator.sql",foreground:"778899"},{token:"operator.swift",foreground:"909090"},{token:"predefined.sql",foreground:"FF00FF"}],colors:{[N.editorBackground]:"#000000",[N.editorForeground]:"#FFFFFF",[b.editorIndentGuides]:"#FFFFFF",[b.editorActiveIndentGuides]:"#FFFFFF"}}}),define(Q[34],J([0,1,48,9,16,26,2,6,11,54,71,27]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MenuItemAction=e.SubmenuItemAction=e.MenuRegistry=e.IMenuService=e.MenuId=e.isIMenuItem=void 0;function o(r){return r.command!==void 0}e.isIMenuItem=o;class s{constructor(i){this.id=s._idPool++,this._debugName=i}}e.MenuId=s,s._idPool=0,s.CommandPalette=new s("CommandPalette"),s.EditorContext=new s("EditorContext"),s.EditorContextPeek=new s("EditorContextPeek"),s.MenubarEditMenu=new s("MenubarEditMenu"),s.MenubarGoMenu=new s("MenubarGoMenu"),s.MenubarSelectionMenu=new s("MenubarSelectionMenu"),e.IMenuService=N.createDecorator("menuService"),e.MenuRegistry=new class{constructor(){this._commands=new Map,this._menuItems=new Map,this._onDidChangeMenu=new C.Emitter,this.onDidChangeMenu=this._onDidChangeMenu.event,this._commandPaletteChangeEvent={has:r=>r===s.CommandPalette}}addCommand(r){return this.addCommands(g.Iterable.single(r))}addCommands(r){for(const i of r)this._commands.set(i.id,i);return this._onDidChangeMenu.fire(this._commandPaletteChangeEvent),S.toDisposable(()=>{let i=!1;for(const n of r)i=this._commands.delete(n.id)||i;i&&this._onDidChangeMenu.fire(this._commandPaletteChangeEvent)})}getCommand(r){return this._commands.get(r)}getCommands(){const r=new Map;return this._commands.forEach((i,n)=>r.set(n,i)),r}appendMenuItem(r,i){return this.appendMenuItems(g.Iterable.single({id:r,item:i}))}appendMenuItems(r){const i=new Set,n=new p.LinkedList;for(const{id:t,item:l}of r){let h=this._menuItems.get(t);h||(h=new p.LinkedList,this._menuItems.set(t,h)),n.push(h.push(l)),i.add(t)}return this._onDidChangeMenu.fire(i),S.toDisposable(()=>{if(n.size>0){for(let t of n)t();this._onDidChangeMenu.fire(i),n.clear()}})}getMenuItems(r){let i;return this._menuItems.has(r)?i=[...this._menuItems.get(r)]:i=[],r===s.CommandPalette&&this._appendImplicitItems(i),i}_appendImplicitItems(r){const i=new Set;for(const n of r)o(n)&&(i.add(n.command.id),n.alt&&i.add(n.alt.id));this._commands.forEach((n,t)=>{i.has(t)||r.push({command:n})})}};class a extends b.SubmenuAction{constructor(i,n,t,l){super(`submenuitem.${i.submenu.id}`,typeof i.title=="string"?i.title:i.title.value,[],"submenu");this.item=i,this._menuService=n,this._contextKeyService=t,this._options=l}get actions(){const i=[],n=this._menuService.createMenu(this.item.submenu,this._contextKeyService),t=n.getActions(this._options);n.dispose();for(const[,l]of t)l.length>0&&(i.push(...l),i.push(new b.Separator));return i.length&&i.pop(),i}}e.SubmenuItemAction=a;let u=class Pt{constructor(i,n,t,l,h){var m;if(this._commandService=h,this.id=i.id,this.label=typeof i.title=="string"?i.title:i.title.value,this.tooltip=(m=i.tooltip)!==null&&m!==void 0?m:"",this.enabled=!i.precondition||l.contextMatchesRules(i.precondition),this.checked=!1,i.toggled){const _=i.toggled.condition?i.toggled:{condition:i.toggled};this.checked=l.contextMatchesRules(_.condition),this.checked&&_.tooltip&&(this.tooltip=typeof _.tooltip=="string"?_.tooltip:_.tooltip.value)}this.item=i,this.alt=n?new Pt(n,void 0,t,l,h):void 0,this._options=t,d.ThemeIcon.isThemeIcon(i.icon)&&(this.class=c.CSSIcon.asClassName(i.icon))}dispose(){}run(...i){var n,t;let l=[];return((n=this._options)===null||n===void 0?void 0:n.arg)&&(l=[...l,this._options.arg]),((t=this._options)===null||t===void 0?void 0:t.shouldForwardArgs)&&(l=[...l,...i]),this._commandService.executeCommand(this.id,...l)}};u=Me([_e(3,M.IContextKeyService),_e(4,w.ICommandService)],u),e.MenuItemAction=u}),define(Q[13],J([0,1,447,24,28,14,36,67,34,26,16,86,33,87,20]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SelectAllCommand=e.RedoCommand=e.UndoCommand=e.EditorExtensionsRegistry=e.registerEditorContribution=e.registerInstantiatedEditorAction=e.registerMultiEditorAction=e.registerEditorAction=e.registerEditorCommand=e.registerModelCommand=e.registerModelAndPositionCommand=e.MultiEditorAction=e.EditorAction=e.EditorCommand=e.ProxyCommand=e.MultiCommand=e.Command=void 0;class u{constructor(A){this.id=A.id,this.precondition=A.precondition,this._kbOpts=A.kbOpts,this._menuOpts=A.menuOpts,this._description=A.description}register(){if(Array.isArray(this._menuOpts)?this._menuOpts.forEach(this._registerMenuItem,this):this._menuOpts&&this._registerMenuItem(this._menuOpts),this._kbOpts){let A=this._kbOpts.kbExpr;this.precondition&&(A?A=p.ContextKeyExpr.and(A,this.precondition):A=this.precondition),c.KeybindingsRegistry.registerCommandAndKeybindingRule({id:this.id,handler:(B,F)=>this.runCommand(B,F),weight:this._kbOpts.weight,args:this._kbOpts.args,when:A,primary:this._kbOpts.primary,secondary:this._kbOpts.secondary,win:this._kbOpts.win,linux:this._kbOpts.linux,mac:this._kbOpts.mac,description:this._description})}else g.CommandsRegistry.registerCommand({id:this.id,handler:(A,B)=>this.runCommand(A,B),description:this._description})}_registerMenuItem(A){d.MenuRegistry.appendMenuItem(A.menuId,{group:A.group,command:{id:this.id,title:A.title,icon:A.icon,precondition:this.precondition},when:A.when,order:A.order})}}e.Command=u;class r extends u{constructor(){super(...arguments);this._implementations=[]}addImplementation(A,B){return this._implementations.push([A,B]),this._implementations.sort((F,D)=>D[0]-F[0]),{dispose:()=>{for(let F=0;F{if(!!R.get(p.IContextKeyService).contextMatchesRules(a.withNullAsUndefined(this.precondition)))return this.runEditorCommand(R,D,B)})}}e.EditorCommand=n;class t extends n{constructor(A){super(t.convertOptions(A));this.label=A.label,this.alias=A.alias}static convertOptions(A){let B;Array.isArray(A.menuOpts)?B=A.menuOpts:A.menuOpts?B=[A.menuOpts]:B=[];function F(D){return D.menuId||(D.menuId=d.MenuId.EditorContext),D.title||(D.title=A.label),D.when=p.ContextKeyExpr.and(A.precondition,D.when),D}return Array.isArray(A.contextMenuOpts)?B.push(...A.contextMenuOpts.map(F)):A.contextMenuOpts&&B.push(F(A.contextMenuOpts)),A.menuOpts=B,A}runEditorCommand(A,B,F){return this.reportTelemetry(A,B),this.run(A,B,F||{})}reportTelemetry(A,B){A.get(s.ITelemetryService).publicLog2("editorActionInvoked",{name:this.label,id:this.id})}}e.EditorAction=t;class l extends t{constructor(){super(...arguments);this._implementations=[]}addImplementation(A,B){return this._implementations.push([A,B]),this._implementations.sort((F,D)=>D[0]-F[0]),{dispose:()=>{for(let F=0;Fnew Promise((K,Y)=>{try{const ee=A(x.object.textEditorModel,w.Position.lift(R),F.slice(2));K(ee)}catch(ee){Y(ee)}}).finally(()=>{x.dispose()}))})}e.registerModelAndPositionCommand=h;function m(O,A){g.CommandsRegistry.registerCommand(O,function(B,...F){const[D]=F;a.assertType(N.URI.isUri(D));const R=B.get(S.IModelService).getModel(D);return R?A(R,...F.slice(1)):B.get(C.ITextModelService).createModelReference(D).then(W=>new Promise((x,K)=>{try{const Y=A(W.object.textEditorModel,F.slice(1));x(Y)}catch(Y){K(Y)}}).finally(()=>{W.dispose()}))})}e.registerModelCommand=m;function _(O){return E.INSTANCE.registerEditorCommand(O),O}e.registerEditorCommand=_;function f(O){const A=new O;return E.INSTANCE.registerEditorAction(A),A}e.registerEditorAction=f;function v(O){return E.INSTANCE.registerEditorAction(O),O}e.registerMultiEditorAction=v;function y(O){E.INSTANCE.registerEditorAction(O)}e.registerInstantiatedEditorAction=y;function L(O,A){E.INSTANCE.registerEditorContribution(O,A)}e.registerEditorContribution=L;var I;(function(O){function A(W){return E.INSTANCE.getEditorCommand(W)}O.getEditorCommand=A;function B(){return E.INSTANCE.getEditorActions()}O.getEditorActions=B;function F(){return E.INSTANCE.getEditorContributions()}O.getEditorContributions=F;function D(W){return E.INSTANCE.getEditorContributions().filter(x=>W.indexOf(x.id)>=0)}O.getSomeEditorContributions=D;function R(){return E.INSTANCE.getDiffEditorContributions()}O.getDiffEditorContributions=R})(I=e.EditorExtensionsRegistry||(e.EditorExtensionsRegistry={}));const k={EditorCommonContributions:"editor.contributions"};class E{constructor(){this.editorContributions=[],this.diffEditorContributions=[],this.editorActions=[],this.editorCommands=Object.create(null)}registerEditorContribution(A,B){this.editorContributions.push({id:A,ctor:B})}getEditorContributions(){return this.editorContributions.slice(0)}getDiffEditorContributions(){return this.diffEditorContributions.slice(0)}registerEditorAction(A){A.register(),this.editorActions.push(A)}getEditorActions(){return this.editorActions.slice(0)}registerEditorCommand(A){A.register(),this.editorCommands[A.id]=A}getEditorCommand(A){return this.editorCommands[A]||null}}E.INSTANCE=new E,o.Registry.add(k.EditorCommonContributions,E.INSTANCE);function T(O){return O.register(),O}e.UndoCommand=T(new r({id:"undo",precondition:void 0,kbOpts:{weight:0,primary:2048|56},menuOpts:[{menuId:d.MenuId.MenubarEditMenu,group:"1_do",title:b.localize(0,null),order:1},{menuId:d.MenuId.CommandPalette,group:"",title:b.localize(1,null),order:1}]})),T(new i(e.UndoCommand,{id:"default:undo",precondition:void 0})),e.RedoCommand=T(new r({id:"redo",precondition:void 0,kbOpts:{weight:0,primary:2048|55,secondary:[2048|1024|56],mac:{primary:2048|1024|56}},menuOpts:[{menuId:d.MenuId.MenubarEditMenu,group:"1_do",title:b.localize(2,null),order:2},{menuId:d.MenuId.CommandPalette,group:"",title:b.localize(3,null),order:1}]})),T(new i(e.RedoCommand,{id:"default:redo",precondition:void 0})),e.SelectAllCommand=T(new r({id:"editor.action.selectAll",precondition:void 0,kbOpts:{weight:0,kbExpr:null,primary:2048|31},menuOpts:[{menuId:d.MenuId.MenubarSelectionMenu,group:"1_basic",title:b.localize(4,null),order:1},{menuId:d.MenuId.CommandPalette,group:"",title:b.localize(5,null),order:1}]}))}),define(Q[188],J([0,1,444,35,20,13,28,536,42,181,241,182,14,3,25,16,86]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CoreEditingCommands=e.CoreNavigationCommands=e.RevealLine_=e.EditorScroll_=e.CoreEditorCommand=void 0;const i=0;class n extends w.EditorCommand{runEditorCommand(E,T,O){const A=T._getViewModel();!A||this.runCoreEditorCommand(A,O||{})}}e.CoreEditorCommand=n;var t;(function(k){const E=function(O){if(!M.isObject(O))return!1;const A=O;return!(!M.isString(A.to)||!M.isUndefined(A.by)&&!M.isString(A.by)||!M.isUndefined(A.value)&&!M.isNumber(A.value)||!M.isUndefined(A.revealCursor)&&!M.isBoolean(A.revealCursor))};k.description={description:"Scroll editor in the given direction",args:[{name:"Editor scroll argument object",description:"Property-value pairs that can be passed through this argument:\n * 'to': A mandatory direction value.\n ```\n 'up', 'down'\n ```\n * 'by': Unit to move. Default is computed based on 'to' value.\n ```\n 'line', 'wrappedLine', 'page', 'halfPage'\n ```\n * 'value': Number of units to move. Default is '1'.\n * 'revealCursor': If 'true' reveals the cursor if it is outside view port.\n ",constraint:E,schema:{type:"object",required:["to"],properties:{to:{type:"string",enum:["up","down"]},by:{type:"string",enum:["line","wrappedLine","page","halfPage"]},value:{type:"number",default:1},revealCursor:{type:"boolean"}}}}]},k.RawDirection={Up:"up",Down:"down"},k.RawUnit={Line:"line",WrappedLine:"wrappedLine",Page:"page",HalfPage:"halfPage"};function T(O){let A;switch(O.to){case k.RawDirection.Up:A=1;break;case k.RawDirection.Down:A=2;break;default:return null}let B;switch(O.by){case k.RawUnit.Line:B=1;break;case k.RawUnit.WrappedLine:B=2;break;case k.RawUnit.Page:B=3;break;case k.RawUnit.HalfPage:B=4;break;default:B=2}const F=Math.floor(O.value||1),D=!!O.revealCursor;return{direction:A,unit:B,value:F,revealCursor:D,select:!!O.select}}k.parse=T})(t=e.EditorScroll_||(e.EditorScroll_={}));var l;(function(k){const E=function(T){if(!M.isObject(T))return!1;const O=T;return!(!M.isNumber(O.lineNumber)&&!M.isString(O.lineNumber)||!M.isUndefined(O.at)&&!M.isString(O.at))};k.description={description:"Reveal the given line at the given logical position",args:[{name:"Reveal line argument object",description:"Property-value pairs that can be passed through this argument:\n * 'lineNumber': A mandatory line number value.\n * 'at': Logical position at which line has to be revealed.\n ```\n 'top', 'center', 'bottom'\n ```\n ",constraint:E,schema:{type:"object",required:["lineNumber"],properties:{lineNumber:{type:["number","string"]},at:{type:"string",enum:["top","center","bottom"]}}}}]},k.RawAtArgument={Top:"top",Center:"center",Bottom:"bottom"}})(l=e.RevealLine_||(e.RevealLine_={}));class h{constructor(E){E.addImplementation(1e4,(T,O)=>{const A=T.get(S.ICodeEditorService).getFocusedCodeEditor();return A&&A.hasTextFocus()?this._runEditorCommand(T,A,O):!1}),E.addImplementation(1e3,(T,O)=>{const A=document.activeElement;return A&&["input","textarea"].indexOf(A.tagName.toLowerCase())>=0?(this.runDOMCommand(),!0):!1}),E.addImplementation(0,(T,O)=>{const A=T.get(S.ICodeEditorService).getActiveCodeEditor();return A?(A.focus(),this._runEditorCommand(T,A,O)):!1})}_runEditorCommand(E,T,O){const A=this.runEditorCommand(E,T,O);return A||!0}}var m;(function(k){class E extends n{constructor(z){super(z);this._inSelectionMode=z.inSelectionMode}runCoreEditorCommand(z,P){z.model.pushStackElement(),z.setCursorStates(P.source,3,[p.CursorMoveCommands.moveTo(z,z.getPrimaryCursorState(),this._inSelectionMode,P.position,P.viewPosition)]),z.revealPrimaryCursor(P.source,!0)}}k.MoveTo=w.registerEditorCommand(new E({id:"_moveTo",inSelectionMode:!1,precondition:void 0})),k.MoveToSelect=w.registerEditorCommand(new E({id:"_moveToSelect",inSelectionMode:!0,precondition:void 0}));class T extends n{runCoreEditorCommand(z,P){z.model.pushStackElement();const V=this._getColumnSelectResult(z,z.getPrimaryCursorState(),z.getCursorColumnSelectData(),P);z.setCursorStates(P.source,3,V.viewStates.map(U=>d.CursorState.fromViewState(U))),z.setCursorColumnSelectData({isReal:!0,fromViewLineNumber:V.fromLineNumber,fromViewVisualColumn:V.fromVisualColumn,toViewLineNumber:V.toLineNumber,toViewVisualColumn:V.toVisualColumn}),V.reversed?z.revealTopMostCursor(P.source):z.revealBottomMostCursor(P.source)}}k.ColumnSelect=w.registerEditorCommand(new class extends T{constructor(){super({id:"columnSelect",precondition:void 0})}_getColumnSelectResult(X,z,P,V){const U=X.model.validatePosition(V.position),H=X.coordinatesConverter.validateViewPosition(new o.Position(V.viewPosition.lineNumber,V.viewPosition.column),U);let $=V.doColumnSelect?P.fromViewLineNumber:H.lineNumber,ie=V.doColumnSelect?P.fromViewVisualColumn:V.mouseColumn-1;return C.ColumnSelection.columnSelect(X.cursorConfig,X,$,ie,H.lineNumber,V.mouseColumn-1)}}),k.CursorColumnSelectLeft=w.registerEditorCommand(new class extends T{constructor(){super({id:"cursorColumnSelectLeft",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:2048|1024|512|15,linux:{primary:0}}})}_getColumnSelectResult(X,z,P,V){return C.ColumnSelection.columnSelectLeft(X.cursorConfig,X,P)}}),k.CursorColumnSelectRight=w.registerEditorCommand(new class extends T{constructor(){super({id:"cursorColumnSelectRight",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:2048|1024|512|17,linux:{primary:0}}})}_getColumnSelectResult(X,z,P,V){return C.ColumnSelection.columnSelectRight(X.cursorConfig,X,P)}});class O extends T{constructor(z){super(z);this._isPaged=z.isPaged}_getColumnSelectResult(z,P,V,U){return C.ColumnSelection.columnSelectUp(z.cursorConfig,z,V,this._isPaged)}}k.CursorColumnSelectUp=w.registerEditorCommand(new O({isPaged:!1,id:"cursorColumnSelectUp",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:2048|1024|512|16,linux:{primary:0}}})),k.CursorColumnSelectPageUp=w.registerEditorCommand(new O({isPaged:!0,id:"cursorColumnSelectPageUp",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:2048|1024|512|11,linux:{primary:0}}}));class A extends T{constructor(z){super(z);this._isPaged=z.isPaged}_getColumnSelectResult(z,P,V,U){return C.ColumnSelection.columnSelectDown(z.cursorConfig,z,V,this._isPaged)}}k.CursorColumnSelectDown=w.registerEditorCommand(new A({isPaged:!1,id:"cursorColumnSelectDown",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:2048|1024|512|18,linux:{primary:0}}})),k.CursorColumnSelectPageDown=w.registerEditorCommand(new A({isPaged:!0,id:"cursorColumnSelectPageDown",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:2048|1024|512|12,linux:{primary:0}}}));class B extends n{constructor(){super({id:"cursorMove",precondition:void 0,description:p.CursorMove.description})}runCoreEditorCommand(z,P){const V=p.CursorMove.parse(P);!V||this._runCursorMove(z,P.source,V)}_runCursorMove(z,P,V){z.model.pushStackElement(),z.setCursorStates(P,3,B._move(z,z.getCursorStates(),V)),z.revealPrimaryCursor(P,!0)}static _move(z,P,V){const U=V.select,H=V.value;switch(V.direction){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:case 8:case 9:case 10:return p.CursorMoveCommands.simpleMove(z,P,V.direction,U,H,V.unit);case 11:case 13:case 12:case 14:return p.CursorMoveCommands.viewportMove(z,P,V.direction,U,H);default:return null}}}k.CursorMoveImpl=B,k.CursorMove=w.registerEditorCommand(new B);class F extends n{constructor(z){super(z);this._staticArgs=z.args}runCoreEditorCommand(z,P){let V=this._staticArgs;this._staticArgs.value===-1&&(V={direction:this._staticArgs.direction,unit:this._staticArgs.unit,select:this._staticArgs.select,value:z.cursorConfig.pageSize}),z.model.pushStackElement(),z.setCursorStates(P.source,3,p.CursorMoveCommands.simpleMove(z,z.getCursorStates(),V.direction,V.select,V.value,V.unit)),z.revealPrimaryCursor(P.source,!0)}}k.CursorLeft=w.registerEditorCommand(new F({args:{direction:0,unit:0,select:!1,value:1},id:"cursorLeft",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:15,mac:{primary:15,secondary:[256|32]}}})),k.CursorLeftSelect=w.registerEditorCommand(new F({args:{direction:0,unit:0,select:!0,value:1},id:"cursorLeftSelect",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:1024|15}})),k.CursorRight=w.registerEditorCommand(new F({args:{direction:1,unit:0,select:!1,value:1},id:"cursorRight",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:17,mac:{primary:17,secondary:[256|36]}}})),k.CursorRightSelect=w.registerEditorCommand(new F({args:{direction:1,unit:0,select:!0,value:1},id:"cursorRightSelect",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:1024|17}})),k.CursorUp=w.registerEditorCommand(new F({args:{direction:2,unit:2,select:!1,value:1},id:"cursorUp",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:16,mac:{primary:16,secondary:[256|46]}}})),k.CursorUpSelect=w.registerEditorCommand(new F({args:{direction:2,unit:2,select:!0,value:1},id:"cursorUpSelect",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:1024|16,secondary:[2048|1024|16],mac:{primary:1024|16},linux:{primary:1024|16}}})),k.CursorPageUp=w.registerEditorCommand(new F({args:{direction:2,unit:2,select:!1,value:-1},id:"cursorPageUp",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:11}})),k.CursorPageUpSelect=w.registerEditorCommand(new F({args:{direction:2,unit:2,select:!0,value:-1},id:"cursorPageUpSelect",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:1024|11}})),k.CursorDown=w.registerEditorCommand(new F({args:{direction:3,unit:2,select:!1,value:1},id:"cursorDown",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:18,mac:{primary:18,secondary:[256|44]}}})),k.CursorDownSelect=w.registerEditorCommand(new F({args:{direction:3,unit:2,select:!0,value:1},id:"cursorDownSelect",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:1024|18,secondary:[2048|1024|18],mac:{primary:1024|18},linux:{primary:1024|18}}})),k.CursorPageDown=w.registerEditorCommand(new F({args:{direction:3,unit:2,select:!1,value:-1},id:"cursorPageDown",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:12}})),k.CursorPageDownSelect=w.registerEditorCommand(new F({args:{direction:3,unit:2,select:!0,value:-1},id:"cursorPageDownSelect",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:1024|12}})),k.CreateCursor=w.registerEditorCommand(new class extends n{constructor(){super({id:"createCursor",precondition:void 0})}runCoreEditorCommand(X,z){let P;z.wholeLine?P=p.CursorMoveCommands.line(X,X.getPrimaryCursorState(),!1,z.position,z.viewPosition):P=p.CursorMoveCommands.moveTo(X,X.getPrimaryCursorState(),!1,z.position,z.viewPosition);const V=X.getCursorStates();if(V.length>1){const U=P.modelState?P.modelState.position:null,H=P.viewState?P.viewState.position:null;for(let $=0,ie=V.length;$H&&(U=H);const $=new s.Range(U,1,U,X.model.getLineMaxColumn(U));let ie=0;if(P.at)switch(P.at){case l.RawAtArgument.Top:ie=3;break;case l.RawAtArgument.Center:ie=1;break;case l.RawAtArgument.Bottom:ie=4;break;default:break}const oe=X.coordinatesConverter.convertModelRangeToViewRange($);X.revealRange(z.source,!1,oe,ie,0)}}),k.SelectAll=new class extends h{constructor(){super(w.SelectAllCommand)}runDOMCommand(){N.isFirefox&&(document.activeElement.focus(),document.activeElement.select()),document.execCommand("selectAll")}runEditorCommand(X,z,P){const V=z._getViewModel();!V||this.runCoreEditorCommand(V,P)}runCoreEditorCommand(X,z){X.model.pushStackElement(),X.setCursorStates("keyboard",3,[p.CursorMoveCommands.selectAll(X,X.getPrimaryCursorState())])}},k.SetSelection=w.registerEditorCommand(new class extends n{constructor(){super({id:"setSelection",precondition:void 0})}runCoreEditorCommand(X,z){X.model.pushStackElement(),X.setCursorStates(z.source,3,[d.CursorState.fromModelSelection(z.selection)])}})})(m=e.CoreNavigationCommands||(e.CoreNavigationCommands={}));const _=u.ContextKeyExpr.and(a.EditorContextKeys.textInputFocus,a.EditorContextKeys.columnSelection);function f(k,E){r.KeybindingsRegistry.registerKeybindingRule({id:k,primary:E,when:_,weight:i+1})}f(m.CursorColumnSelectLeft.id,1024|15),f(m.CursorColumnSelectRight.id,1024|17),f(m.CursorColumnSelectUp.id,1024|16),f(m.CursorColumnSelectPageUp.id,1024|11),f(m.CursorColumnSelectDown.id,1024|18),f(m.CursorColumnSelectPageDown.id,1024|12);function v(k){return k.register(),k}var y;(function(k){class E extends w.EditorCommand{runEditorCommand(O,A,B){const F=A._getViewModel();!F||this.runCoreEditingCommand(A,F,B||{})}}k.CoreEditingCommand=E,k.LineBreakInsert=w.registerEditorCommand(new class extends E{constructor(){super({id:"lineBreakInsert",precondition:a.EditorContextKeys.writable,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|45}}})}runCoreEditingCommand(T,O,A){T.pushUndoStop(),T.executeCommands(this.id,c.TypeOperations.lineBreakInsert(O.cursorConfig,O.model,O.getCursorStates().map(B=>B.modelState.selection)))}}),k.Outdent=w.registerEditorCommand(new class extends E{constructor(){super({id:"outdent",precondition:a.EditorContextKeys.writable,kbOpts:{weight:i,kbExpr:u.ContextKeyExpr.and(a.EditorContextKeys.editorTextFocus,a.EditorContextKeys.tabDoesNotMoveFocus),primary:1024|2}})}runCoreEditingCommand(T,O,A){T.pushUndoStop(),T.executeCommands(this.id,c.TypeOperations.outdent(O.cursorConfig,O.model,O.getCursorStates().map(B=>B.modelState.selection))),T.pushUndoStop()}}),k.Tab=w.registerEditorCommand(new class extends E{constructor(){super({id:"tab",precondition:a.EditorContextKeys.writable,kbOpts:{weight:i,kbExpr:u.ContextKeyExpr.and(a.EditorContextKeys.editorTextFocus,a.EditorContextKeys.tabDoesNotMoveFocus),primary:2}})}runCoreEditingCommand(T,O,A){T.pushUndoStop(),T.executeCommands(this.id,c.TypeOperations.tab(O.cursorConfig,O.model,O.getCursorStates().map(B=>B.modelState.selection))),T.pushUndoStop()}}),k.DeleteLeft=w.registerEditorCommand(new class extends E{constructor(){super({id:"deleteLeft",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:1,secondary:[1024|1],mac:{primary:1,secondary:[1024|1,256|38,256|1]}}})}runCoreEditingCommand(T,O,A){const[B,F]=g.DeleteOperations.deleteLeft(O.getPrevEditOperationType(),O.cursorConfig,O.model,O.getCursorStates().map(D=>D.modelState.selection));B&&T.pushUndoStop(),T.executeCommands(this.id,F),O.setPrevEditOperationType(2)}}),k.DeleteRight=w.registerEditorCommand(new class extends E{constructor(){super({id:"deleteRight",precondition:void 0,kbOpts:{weight:i,kbExpr:a.EditorContextKeys.textInputFocus,primary:20,mac:{primary:20,secondary:[256|34,256|20]}}})}runCoreEditingCommand(T,O,A){const[B,F]=g.DeleteOperations.deleteRight(O.getPrevEditOperationType(),O.cursorConfig,O.model,O.getCursorStates().map(D=>D.modelState.selection));B&&T.pushUndoStop(),T.executeCommands(this.id,F),O.setPrevEditOperationType(3)}}),k.Undo=new class extends h{constructor(){super(w.UndoCommand)}runDOMCommand(){document.execCommand("undo")}runEditorCommand(T,O,A){if(!(!O.hasModel()||O.getOption(75)===!0))return O.getModel().undo()}},k.Redo=new class extends h{constructor(){super(w.RedoCommand)}runDOMCommand(){document.execCommand("redo")}runEditorCommand(T,O,A){if(!(!O.hasModel()||O.getOption(75)===!0))return O.getModel().redo()}}})(y=e.CoreEditingCommands||(e.CoreEditingCommands={}));class L extends w.Command{constructor(E,T,O){super({id:E,precondition:void 0,description:O});this._handlerId=T}runCommand(E,T){const O=E.get(S.ICodeEditorService).getFocusedCodeEditor();!O||O.trigger("keyboard",this._handlerId,T)}}function I(k,E){v(new L("default:"+k,k)),v(new L(k,k,E))}I("type",{description:"Type",args:[{name:"args",schema:{type:"object",required:["text"],properties:{text:{type:"string"}}}}]}),I("replacePreviousChar"),I("compositionType"),I("compositionStart"),I("compositionEnd"),I("paste"),I("cut")}),define(Q[607],J([0,1,13,16,23,71,9,74,446]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EditorKeybindingCancellationTokenSource=void 0;const g=S.createDecorator("IEditorCancelService"),p=new N.RawContextKey("cancellableOperation",!1,d.localize(0,null));C.registerSingleton(g,class{constructor(){this._tokens=new WeakMap}add(o,s){let a=this._tokens.get(o);a||(a=o.invokeWithinContext(r=>{const i=p.bindTo(r.get(N.IContextKeyService)),n=new w.LinkedList;return{key:i,tokens:n}}),this._tokens.set(o,a));let u;return a.key.set(!0),u=a.tokens.push(s),()=>{u&&(u(),a.key.set(!a.tokens.isEmpty()),u=void 0)}}cancel(o){const s=this._tokens.get(o);if(!!s){const a=s.tokens.pop();a&&(a.cancel(),s.key.set(!s.tokens.isEmpty()))}}},!0);class c extends M.CancellationTokenSource{constructor(s,a){super(a);this.editor=s,this._unregister=s.invokeWithinContext(u=>u.get(g).add(s,this))}dispose(){this._unregister(),super.dispose()}}e.EditorKeybindingCancellationTokenSource=c,b.registerEditorCommand(new class extends b.EditorCommand{constructor(){super({id:"editor.cancelOperation",kbOpts:{weight:100,primary:9},precondition:p})}runEditorCommand(o,s){o.get(g).cancel(s)}})}),define(Q[70],J([0,1,8,3,23,2,607]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.StableEditorScrollState=e.TextModelCancellationTokenSource=e.EditorStateCancellationTokenSource=e.EditorState=void 0;class C{constructor(o,s){if(this.flags=s,(this.flags&1)!=0){const a=o.getModel();this.modelVersionId=a?b.format("{0}#{1}",a.uri.toString(),a.getVersionId()):null}else this.modelVersionId=null;(this.flags&4)!=0?this.position=o.getPosition():this.position=null,(this.flags&2)!=0?this.selection=o.getSelection():this.selection=null,(this.flags&8)!=0?(this.scrollLeft=o.getScrollLeft(),this.scrollTop=o.getScrollTop()):(this.scrollLeft=-1,this.scrollTop=-1)}_equals(o){if(!(o instanceof C))return!1;const s=o;return!(this.modelVersionId!==s.modelVersionId||this.scrollLeft!==s.scrollLeft||this.scrollTop!==s.scrollTop||!this.position&&s.position||this.position&&!s.position||this.position&&s.position&&!this.position.equals(s.position)||!this.selection&&s.selection||this.selection&&!s.selection||this.selection&&s.selection&&!this.selection.equalsRange(s.selection))}validate(o){return this._equals(new C(o,this.flags))}}e.EditorState=C;class d extends S.EditorKeybindingCancellationTokenSource{constructor(o,s,a,u){super(o,u);this.editor=o,this._listener=new w.DisposableStore,s&4&&this._listener.add(o.onDidChangeCursorPosition(r=>{(!a||!N.Range.containsPosition(a,r.position))&&this.cancel()})),s&2&&this._listener.add(o.onDidChangeCursorSelection(r=>{(!a||!N.Range.containsRange(a,r.selection))&&this.cancel()})),s&8&&this._listener.add(o.onDidScrollChange(r=>this.cancel())),s&1&&(this._listener.add(o.onDidChangeModel(r=>this.cancel())),this._listener.add(o.onDidChangeModelContent(r=>this.cancel())))}dispose(){this._listener.dispose(),super.dispose()}}e.EditorStateCancellationTokenSource=d;class g extends M.CancellationTokenSource{constructor(o,s){super(s);this._listener=o.onDidChangeContent(()=>this.cancel())}dispose(){this._listener.dispose(),super.dispose()}}e.TextModelCancellationTokenSource=g;class p{constructor(o,s,a){this._visiblePosition=o,this._visiblePositionScrollDelta=s,this._cursorPosition=a}static capture(o){let s=null,a=0;if(o.getScrollTop()!==0){const u=o.getVisibleRanges();if(u.length>0){s=u[0].getStartPosition();const r=o.getTopForPosition(s.lineNumber,s.column);a=o.getScrollTop()-r}}return new p(s,a,o.getPosition())}restore(o){if(this._visiblePosition){const s=o.getTopForPosition(this._visiblePosition.lineNumber,this._visiblePosition.column);o.setScrollTop(s+this._visiblePositionScrollDelta)}}restoreRelativeVerticalPositionOfCursor(o){const s=o.getPosition();if(!(!this._cursorPosition||!s)){const a=o.getTopForLineNumber(s.lineNumber)-o.getTopForLineNumber(this._cursorPosition.lineNumber);o.setScrollTop(o.getScrollTop()+a)}}}e.StableEditorScrollState=p}),define(Q[608],J([0,1,178,13]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MarkerDecorationsContribution=void 0;let M=class{constructor(S,C){}dispose(){}};M.ID="editor.contrib.markerDecorations",M=Me([_e(1,b.IMarkerDecorationsService)],M),e.MarkerDecorationsContribution=M,N.registerEditorContribution(M.ID,M)}),define(Q[609],J([0,1,188,14,17]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ViewController=void 0;class w{constructor(C,d,g,p){this.configuration=C,this.viewModel=d,this.userInputEvents=g,this.commandDelegate=p}paste(C,d,g,p){this.commandDelegate.paste(C,d,g,p)}type(C){this.commandDelegate.type(C)}compositionType(C,d,g,p){this.commandDelegate.compositionType(C,d,g,p)}compositionStart(){this.commandDelegate.startComposition()}compositionEnd(){this.commandDelegate.endComposition()}cut(){this.commandDelegate.cut()}setSelection(C){b.CoreNavigationCommands.SetSelection.runCoreEditorCommand(this.viewModel,{source:"keyboard",selection:C})}_validateViewColumn(C){const d=this.viewModel.getLineMinColumn(C.lineNumber);return C.column=4?this._selectAll():C.mouseDownCount===3?this._hasMulticursorModifier(C)?C.inSelectionMode?this._lastCursorLineSelectDrag(C.position):this._lastCursorLineSelect(C.position):C.inSelectionMode?this._lineSelectDrag(C.position):this._lineSelect(C.position):C.mouseDownCount===2?this._hasMulticursorModifier(C)?this._lastCursorWordSelect(C.position):C.inSelectionMode?this._wordSelectDrag(C.position):this._wordSelect(C.position):this._hasMulticursorModifier(C)?this._hasNonMulticursorModifier(C)||(C.shiftKey?this._columnSelect(C.position,C.mouseColumn,!0):C.inSelectionMode?this._lastCursorMoveToSelect(C.position):this._createCursor(C.position,!1)):C.inSelectionMode?C.altKey?this._columnSelect(C.position,C.mouseColumn,!0):p?this._columnSelect(C.position,C.mouseColumn,!0):this._moveToSelect(C.position):this.moveTo(C.position)}_usualArgs(C){return C=this._validateViewColumn(C),{source:"mouse",position:this._convertViewToModelPosition(C),viewPosition:C}}moveTo(C){b.CoreNavigationCommands.MoveTo.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_moveToSelect(C){b.CoreNavigationCommands.MoveToSelect.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_columnSelect(C,d,g){C=this._validateViewColumn(C),b.CoreNavigationCommands.ColumnSelect.runCoreEditorCommand(this.viewModel,{source:"mouse",position:this._convertViewToModelPosition(C),viewPosition:C,mouseColumn:d,doColumnSelect:g})}_createCursor(C,d){C=this._validateViewColumn(C),b.CoreNavigationCommands.CreateCursor.runCoreEditorCommand(this.viewModel,{source:"mouse",position:this._convertViewToModelPosition(C),viewPosition:C,wholeLine:d})}_lastCursorMoveToSelect(C){b.CoreNavigationCommands.LastCursorMoveToSelect.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_wordSelect(C){b.CoreNavigationCommands.WordSelect.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_wordSelectDrag(C){b.CoreNavigationCommands.WordSelectDrag.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_lastCursorWordSelect(C){b.CoreNavigationCommands.LastCursorWordSelect.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_lineSelect(C){b.CoreNavigationCommands.LineSelect.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_lineSelectDrag(C){b.CoreNavigationCommands.LineSelectDrag.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_lastCursorLineSelect(C){b.CoreNavigationCommands.LastCursorLineSelect.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_lastCursorLineSelectDrag(C){b.CoreNavigationCommands.LastCursorLineSelectDrag.runCoreEditorCommand(this.viewModel,this._usualArgs(C))}_selectAll(){b.CoreNavigationCommands.SelectAll.runCoreEditorCommand(this.viewModel,{source:"mouse"})}_convertViewToModelPosition(C){return this.viewModel.coordinatesConverter.convertViewPositionToModelPosition(C)}emitKeyDown(C){this.userInputEvents.emitKeyDown(C)}emitKeyUp(C){this.userInputEvents.emitKeyUp(C)}emitContextMenu(C){this.userInputEvents.emitContextMenu(C)}emitMouseMove(C){this.userInputEvents.emitMouseMove(C)}emitMouseLeave(C){this.userInputEvents.emitMouseLeave(C)}emitMouseUp(C){this.userInputEvents.emitMouseUp(C)}emitMouseDown(C){this.userInputEvents.emitMouseDown(C)}emitMouseDrag(C){this.userInputEvents.emitMouseDrag(C)}emitMouseDrop(C){this.userInputEvents.emitMouseDrop(C)}emitMouseDropCanceled(){this.userInputEvents.emitMouseDropCanceled()}emitMouseWheel(C){this.userInputEvents.emitMouseWheel(C)}}e.ViewController=w}),define(Q[610],J([0,1,7,35,21,30,12,581,590,609,256,564,45,390,588,391,584,172,589,257,582,392,223,393,585,394,591,395,592,586,587,593,396,14,3,110,385,387,111,11,187]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y,L,I,k,E,T,O,A,B,F,D,R,W,x,K){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.View=void 0;class Y extends W.ViewEventHandler{constructor(ne,le,X,z,P,V){super();this._selections=[new M.Selection(1,1,1,1)],this._renderAnimationFrame=null;const U=new g.ViewController(le,z,P,ne);this._context=new D.ViewContext(le,X.getColorTheme(),z),this._configPixelRatio=this._configPixelRatio=this._context.configuration.options.get(122),this._context.addEventHandler(this),this._register(X.onDidColorThemeChange(te=>{this._context.theme.update(te),this._context.model.onDidColorThemeChange(),this.render(!0,!1)})),this._viewParts=[],this._textAreaHandler=new d.TextAreaHandler(this._context,U,this._createTextAreaHandlerHelper()),this._viewParts.push(this._textAreaHandler),this._linesContent=w.createFastDomNode(document.createElement("div")),this._linesContent.setClassName("lines-content monaco-editor-background"),this._linesContent.setPosition("absolute"),this.domNode=w.createFastDomNode(document.createElement("div")),this.domNode.setClassName(this._getEditorClassName()),this.domNode.setAttribute("role","code"),this._overflowGuardContainer=w.createFastDomNode(document.createElement("div")),o.PartFingerprints.write(this._overflowGuardContainer,3),this._overflowGuardContainer.setClassName("overflow-guard"),this._scrollbar=new r.EditorScrollbar(this._context,this._linesContent,this.domNode,this._overflowGuardContainer),this._viewParts.push(this._scrollbar),this._viewLines=new l.ViewLines(this._context,this._linesContent),this._viewZones=new O.ViewZones(this._context),this._viewParts.push(this._viewZones);const H=new y.DecorationsOverviewRuler(this._context);this._viewParts.push(H);const $=new k.ScrollDecorationViewPart(this._context);this._viewParts.push($);const ie=new c.ContentViewOverlays(this._context);this._viewParts.push(ie),ie.addDynamicOverlay(new a.CurrentLineHighlightOverlay(this._context)),ie.addDynamicOverlay(new E.SelectionsOverlay(this._context)),ie.addDynamicOverlay(new n.IndentGuidesOverlay(this._context)),ie.addDynamicOverlay(new u.DecorationsOverlay(this._context));const oe=new c.MarginViewOverlays(this._context);this._viewParts.push(oe),oe.addDynamicOverlay(new a.CurrentLineMarginHighlightOverlay(this._context)),oe.addDynamicOverlay(new i.GlyphMarginOverlay(this._context)),oe.addDynamicOverlay(new _.MarginViewLineDecorationsOverlay(this._context)),oe.addDynamicOverlay(new h.LinesDecorationsOverlay(this._context)),oe.addDynamicOverlay(new t.LineNumbersOverlay(this._context));const ae=new m.Margin(this._context);ae.getDomNode().appendChild(this._viewZones.marginDomNode),ae.getDomNode().appendChild(oe.getDomNode()),this._viewParts.push(ae),this._contentWidgets=new s.ViewContentWidgets(this._context,this.domNode),this._viewParts.push(this._contentWidgets),this._viewCursors=new T.ViewCursors(this._context),this._viewParts.push(this._viewCursors),this._overlayWidgets=new v.ViewOverlayWidgets(this._context),this._viewParts.push(this._overlayWidgets);const G=new I.Rulers(this._context);this._viewParts.push(G);const j=new f.Minimap(this._context);if(this._viewParts.push(j),H){const te=this._scrollbar.getOverviewRulerLayoutInfo();te.parent.insertBefore(H.getDomNode(),te.insertBefore)}this._linesContent.appendChild(ie.getDomNode()),this._linesContent.appendChild(G.domNode),this._linesContent.appendChild(this._viewZones.domNode),this._linesContent.appendChild(this._viewLines.getDomNode()),this._linesContent.appendChild(this._contentWidgets.domNode),this._linesContent.appendChild(this._viewCursors.getDomNode()),this._overflowGuardContainer.appendChild(ae.getDomNode()),this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()),this._overflowGuardContainer.appendChild($.getDomNode()),this._overflowGuardContainer.appendChild(this._textAreaHandler.textArea),this._overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover),this._overflowGuardContainer.appendChild(this._overlayWidgets.getDomNode()),this._overflowGuardContainer.appendChild(j.getDomNode()),this.domNode.appendChild(this._overflowGuardContainer),V?V.appendChild(this._contentWidgets.overflowingContentWidgetsDomNode.domNode):this.domNode.appendChild(this._contentWidgets.overflowingContentWidgetsDomNode),this._applyLayout(),this._pointerHandler=this._register(new C.PointerHandler(this._context,U,this._createPointerHandlerHelper()))}_flushAccumulatedAndRenderNow(){this._renderNow()}_createPointerHandlerHelper(){return{viewDomNode:this.domNode.domNode,linesContentDomNode:this._linesContent.domNode,focusTextArea:()=>{this.focus()},dispatchTextAreaEvent:ne=>{this._textAreaHandler.textArea.domNode.dispatchEvent(ne)},getLastRenderData:()=>{const ne=this._viewCursors.getLastRenderData()||[],le=this._textAreaHandler.getLastRenderData();return new K.PointerHandlerLastRenderData(ne,le)},shouldSuppressMouseDownOnViewZone:ne=>this._viewZones.shouldSuppressMouseDownOnViewZone(ne),shouldSuppressMouseDownOnWidget:ne=>this._contentWidgets.shouldSuppressMouseDownOnWidget(ne),getPositionFromDOMInfo:(ne,le)=>(this._flushAccumulatedAndRenderNow(),this._viewLines.getPositionFromDOMInfo(ne,le)),visibleRangeForPosition:(ne,le)=>(this._flushAccumulatedAndRenderNow(),this._viewLines.visibleRangeForPosition(new A.Position(ne,le))),getLineWidth:ne=>(this._flushAccumulatedAndRenderNow(),this._viewLines.getLineWidth(ne))}}_createTextAreaHandlerHelper(){return{visibleRangeForPositionRelativeToEditor:(ne,le)=>(this._flushAccumulatedAndRenderNow(),this._viewLines.visibleRangeForPosition(new A.Position(ne,le)))}}_applyLayout(){const le=this._context.configuration.options.get(124);this.domNode.setWidth(le.width),this.domNode.setHeight(le.height),this._overflowGuardContainer.setWidth(le.width),this._overflowGuardContainer.setHeight(le.height),this._linesContent.setWidth(1e6),this._linesContent.setHeight(1e6)}_getEditorClassName(){const ne=this._textAreaHandler.isFocused()?" focused":"";return this._context.configuration.options.get(121)+" "+x.getThemeTypeSelector(this._context.theme.type)+ne}handleEvents(ne){super.handleEvents(ne),this._scheduleRender()}onConfigurationChanged(ne){return this._configPixelRatio=this._context.configuration.options.get(122),this.domNode.setClassName(this._getEditorClassName()),this._applyLayout(),!1}onCursorStateChanged(ne){return this._selections=ne.selections,!1}onFocusChanged(ne){return this.domNode.setClassName(this._getEditorClassName()),!1}onThemeChanged(ne){return this.domNode.setClassName(this._getEditorClassName()),!1}dispose(){this._renderAnimationFrame!==null&&(this._renderAnimationFrame.dispose(),this._renderAnimationFrame=null),this._contentWidgets.overflowingContentWidgetsDomNode.domNode.remove(),this._context.removeEventHandler(this),this._viewLines.dispose();for(const ne of this._viewParts)ne.dispose();super.dispose()}_scheduleRender(){this._renderAnimationFrame===null&&(this._renderAnimationFrame=b.runAtThisOrScheduleAtNextAnimationFrame(this._onRenderScheduled.bind(this),100))}_onRenderScheduled(){this._renderAnimationFrame=null,this._flushAccumulatedAndRenderNow()}_renderNow(){ee(()=>this._actualRender())}_getViewPartsToRender(){let ne=[],le=0;for(const X of this._viewParts)X.shouldRender()&&(ne[le++]=X);return ne}_actualRender(){if(!!b.isInDOM(this.domNode.domNode)){let ne=this._getViewPartsToRender();if(!(!this._viewLines.shouldRender()&&ne.length===0)){const le=this._context.viewLayout.getLinesViewportData();this._context.model.setViewport(le.startLineNumber,le.endLineNumber,le.centeredLineNumber);const X=new R.ViewportData(this._selections,le,this._context.viewLayout.getWhitespaceViewportData(),this._context.model);this._contentWidgets.shouldRender()&&this._contentWidgets.onBeforeRender(X),this._viewLines.shouldRender()&&(this._viewLines.renderText(X),this._viewLines.onDidRender(),ne=this._getViewPartsToRender());const z=new F.RenderingContext(this._context.viewLayout,X,this._viewLines);for(const P of ne)P.prepareRender(z);for(const P of ne)P.render(z),P.onDidRender();Math.abs(N.getPixelRatio()-this._configPixelRatio)>.001&&this._context.configuration.updatePixelRatio()}}}delegateVerticalScrollbarMouseDown(ne){this._scrollbar.delegateVerticalScrollbarMouseDown(ne)}restoreState(ne){this._context.model.setScrollPosition({scrollTop:ne.scrollTop},1),this._context.model.tokenizeViewport(),this._renderNow(),this._viewLines.updateLineWidths(),this._context.model.setScrollPosition({scrollLeft:ne.scrollLeft},1)}getOffsetForColumn(ne,le){const X=this._context.model.validateModelPosition({lineNumber:ne,column:le}),z=this._context.model.coordinatesConverter.convertModelPositionToViewPosition(X);this._flushAccumulatedAndRenderNow();const P=this._viewLines.visibleRangeForPosition(new A.Position(z.lineNumber,z.column));return P?P.left:-1}getTargetAtClientPoint(ne,le){const X=this._pointerHandler.getTargetAtClientPoint(ne,le);return X?p.ViewUserInputEvents.convertViewToModelMouseTarget(X,this._context.model.coordinatesConverter):null}createOverviewRuler(ne){return new L.OverviewRuler(this._context,ne)}change(ne){this._viewZones.changeViewZones(ne),this._scheduleRender()}render(ne,le){if(le){this._viewLines.forceShouldRender();for(const X of this._viewParts)X.forceShouldRender()}ne?this._flushAccumulatedAndRenderNow():this._scheduleRender()}focus(){this._textAreaHandler.focusTextArea()}isFocused(){return this._textAreaHandler.isFocused()}setAriaOptions(ne){this._textAreaHandler.setAriaOptions(ne)}addContentWidget(ne){this._contentWidgets.addWidget(ne.widget),this.layoutContentWidget(ne),this._scheduleRender()}layoutContentWidget(ne){let le=ne.position&&ne.position.range||null;if(le===null){const z=ne.position?ne.position.position:null;z!==null&&(le=new B.Range(z.lineNumber,z.column,z.lineNumber,z.column))}const X=ne.position?ne.position.preference:null;this._contentWidgets.setWidgetPosition(ne.widget,le,X),this._scheduleRender()}removeContentWidget(ne){this._contentWidgets.removeWidget(ne.widget),this._scheduleRender()}addOverlayWidget(ne){this._overlayWidgets.addWidget(ne.widget),this.layoutOverlayWidget(ne),this._scheduleRender()}layoutOverlayWidget(ne){const le=ne.position?ne.position.preference:null;this._overlayWidgets.setWidgetPosition(ne.widget,le)&&this._scheduleRender()}removeOverlayWidget(ne){this._overlayWidgets.removeWidget(ne.widget),this._scheduleRender()}}e.View=Y;function ee(se){try{return se()}catch(ne){S.onUnexpectedError(ne)}}}),define(Q[143],J([0,1,448,7,12,6,2,43,69,13,28,610,256,38,242,42,14,3,21,217,107,25,18,49,22,540,26,16,9,138,32,11,65,20,397,563,136,608,334]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y,L,I,k,E,T,O,A,B,F,D){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EditorModeContext=e.BooleanEventEmitter=e.CodeEditorWidget=void 0;let R=0;class W{constructor(H,$,ie,oe,ae){this.model=H,this.viewModel=$,this.view=ie,this.hasRealView=oe,this.listenersToRemove=ae}dispose(){S.dispose(this.listenersToRemove),this.model.onBeforeDetached(),this.hasRealView&&this.view.dispose(),this.viewModel.dispose()}}let x=class ct extends S.Disposable{constructor(H,$,ie,oe,ae,G,j,te,Z,ue){super();this._onDidDispose=this._register(new w.Emitter),this.onDidDispose=this._onDidDispose.event,this._onDidChangeModelContent=this._register(new w.Emitter),this.onDidChangeModelContent=this._onDidChangeModelContent.event,this._onDidChangeModelLanguage=this._register(new w.Emitter),this.onDidChangeModelLanguage=this._onDidChangeModelLanguage.event,this._onDidChangeModelLanguageConfiguration=this._register(new w.Emitter),this.onDidChangeModelLanguageConfiguration=this._onDidChangeModelLanguageConfiguration.event,this._onDidChangeModelOptions=this._register(new w.Emitter),this.onDidChangeModelOptions=this._onDidChangeModelOptions.event,this._onDidChangeModelDecorations=this._register(new w.Emitter),this.onDidChangeModelDecorations=this._onDidChangeModelDecorations.event,this._onDidChangeConfiguration=this._register(new w.Emitter),this.onDidChangeConfiguration=this._onDidChangeConfiguration.event,this._onDidChangeModel=this._register(new w.Emitter),this.onDidChangeModel=this._onDidChangeModel.event,this._onDidChangeCursorPosition=this._register(new w.Emitter),this.onDidChangeCursorPosition=this._onDidChangeCursorPosition.event,this._onDidChangeCursorSelection=this._register(new w.Emitter),this.onDidChangeCursorSelection=this._onDidChangeCursorSelection.event,this._onDidAttemptReadOnlyEdit=this._register(new w.Emitter),this.onDidAttemptReadOnlyEdit=this._onDidAttemptReadOnlyEdit.event,this._onDidLayoutChange=this._register(new w.Emitter),this.onDidLayoutChange=this._onDidLayoutChange.event,this._editorTextFocus=this._register(new K),this.onDidFocusEditorText=this._editorTextFocus.onDidChangeToTrue,this.onDidBlurEditorText=this._editorTextFocus.onDidChangeToFalse,this._editorWidgetFocus=this._register(new K),this.onDidFocusEditorWidget=this._editorWidgetFocus.onDidChangeToTrue,this.onDidBlurEditorWidget=this._editorWidgetFocus.onDidChangeToFalse,this._onWillType=this._register(new w.Emitter),this.onWillType=this._onWillType.event,this._onDidType=this._register(new w.Emitter),this.onDidType=this._onDidType.event,this._onDidCompositionStart=this._register(new w.Emitter),this.onDidCompositionStart=this._onDidCompositionStart.event,this._onDidCompositionEnd=this._register(new w.Emitter),this.onDidCompositionEnd=this._onDidCompositionEnd.event,this._onDidPaste=this._register(new w.Emitter),this.onDidPaste=this._onDidPaste.event,this._onMouseUp=this._register(new w.Emitter),this.onMouseUp=this._onMouseUp.event,this._onMouseDown=this._register(new w.Emitter),this.onMouseDown=this._onMouseDown.event,this._onMouseDrag=this._register(new w.Emitter),this.onMouseDrag=this._onMouseDrag.event,this._onMouseDrop=this._register(new w.Emitter),this.onMouseDrop=this._onMouseDrop.event,this._onMouseDropCanceled=this._register(new w.Emitter),this.onMouseDropCanceled=this._onMouseDropCanceled.event,this._onContextMenu=this._register(new w.Emitter),this.onContextMenu=this._onContextMenu.event,this._onMouseMove=this._register(new w.Emitter),this.onMouseMove=this._onMouseMove.event,this._onMouseLeave=this._register(new w.Emitter),this.onMouseLeave=this._onMouseLeave.event,this._onMouseWheel=this._register(new w.Emitter),this.onMouseWheel=this._onMouseWheel.event,this._onKeyUp=this._register(new w.Emitter),this.onKeyUp=this._onKeyUp.event,this._onKeyDown=this._register(new w.Emitter),this.onKeyDown=this._onKeyDown.event,this._onDidContentSizeChange=this._register(new w.Emitter),this.onDidContentSizeChange=this._onDidContentSizeChange.event,this._onDidScrollChange=this._register(new w.Emitter),this.onDidScrollChange=this._onDidScrollChange.event,this._onDidChangeViewZones=this._register(new w.Emitter),this.onDidChangeViewZones=this._onDidChangeViewZones.event;const he=Object.assign({},$);this._domElement=H,this._overflowWidgetsDomNode=he.overflowWidgetsDomNode,delete he.overflowWidgetsDomNode,this._id=++R,this._decorationTypeKeysToIds={},this._decorationTypeSubtypes={},this.isSimpleWidget=ie.isSimpleWidget||!1,this._telemetryData=ie.telemetryData,this._configuration=this._register(this._createConfiguration(he,ue)),this._register(this._configuration.onDidChange(ce=>{this._onDidChangeConfiguration.fire(ce);const me=this._configuration.options;if(ce.hasChanged(124)){const Ce=me.get(124);this._onDidLayoutChange.fire(Ce)}})),this._contextKeyService=this._register(j.createScoped(this._domElement)),this._notificationService=Z,this._codeEditorService=ae,this._commandService=G,this._themeService=te,this._register(new Y(this,this._contextKeyService)),this._register(new ee(this,this._contextKeyService)),this._instantiationService=oe.createChild(new k.ServiceCollection([L.IContextKeyService,this._contextKeyService])),this._modelData=null,this._contributions={},this._actions={},this._focusTracker=new se(H),this._focusTracker.onChange(()=>{this._editorWidgetFocus.setValue(this._focusTracker.hasFocus())}),this._contentWidgets={},this._overlayWidgets={};let re;Array.isArray(ie.contributions)?re=ie.contributions:re=g.EditorExtensionsRegistry.getEditorContributions();for(const ce of re)try{const me=this._instantiationService.createInstance(ce.ctor,this);this._contributions[ce.id]=me}catch(me){M.onUnexpectedError(me)}g.EditorExtensionsRegistry.getEditorActions().forEach(ce=>{const me=new t.InternalEditorAction(ce.id,ce.label,ce.alias,A.withNullAsUndefined(ce.precondition),()=>this._instantiationService.invokeFunction(Ce=>Promise.resolve(ce.runEditorCommand(Ce,this,null))),this._contextKeyService);this._actions[me.id]=me}),this._codeEditorService.addCodeEditor(this)}_createConfiguration(H,$){return new d.Configuration(this.isSimpleWidget,H,this._domElement,$)}getId(){return this.getEditorType()+":"+this._id}getEditorType(){return l.EditorType.ICodeEditor}dispose(){this._codeEditorService.removeCodeEditor(this),this._focusTracker.dispose();const H=Object.keys(this._contributions);for(let $=0,ie=H.length;$i.Range.lift($)))}getVisibleColumnFromPosition(H){if(!this._modelData)return H.column;const $=this._modelData.model.validatePosition(H),ie=this._modelData.model.getOptions().tabSize;return u.CursorColumns.visibleColumnFromColumn(this._modelData.model.getLineContent($.lineNumber),$.column,ie)+1}getPosition(){return this._modelData?this._modelData.viewModel.getPosition():null}setPosition(H){if(!!this._modelData){if(!r.Position.isIPosition(H))throw new Error("Invalid arguments");this._modelData.viewModel.setSelections("api",[{selectionStartLineNumber:H.lineNumber,selectionStartColumn:H.column,positionLineNumber:H.lineNumber,positionColumn:H.column}])}}_sendRevealRange(H,$,ie,oe){if(!!this._modelData){if(!i.Range.isIRange(H))throw new Error("Invalid arguments");const ae=this._modelData.model.validateRange(H),G=this._modelData.viewModel.coordinatesConverter.convertModelRangeToViewRange(ae);this._modelData.viewModel.revealRange("api",ie,G,$,oe)}}revealLine(H,$=0){this._revealLine(H,0,$)}revealLineInCenter(H,$=0){this._revealLine(H,1,$)}revealLineInCenterIfOutsideViewport(H,$=0){this._revealLine(H,2,$)}revealLineNearTop(H,$=0){this._revealLine(H,5,$)}_revealLine(H,$,ie){if(typeof H!="number")throw new Error("Invalid arguments");this._sendRevealRange(new i.Range(H,1,H,1),$,!1,ie)}revealPosition(H,$=0){this._revealPosition(H,0,!0,$)}revealPositionInCenter(H,$=0){this._revealPosition(H,1,!0,$)}revealPositionInCenterIfOutsideViewport(H,$=0){this._revealPosition(H,2,!0,$)}revealPositionNearTop(H,$=0){this._revealPosition(H,5,!0,$)}_revealPosition(H,$,ie,oe){if(!r.Position.isIPosition(H))throw new Error("Invalid arguments");this._sendRevealRange(new i.Range(H.lineNumber,H.column,H.lineNumber,H.column),$,ie,oe)}getSelection(){return this._modelData?this._modelData.viewModel.getSelection():null}getSelections(){return this._modelData?this._modelData.viewModel.getSelections():null}setSelection(H){const $=n.Selection.isISelection(H),ie=i.Range.isIRange(H);if(!$&&!ie)throw new Error("Invalid arguments");if($)this._setSelectionImpl(H);else if(ie){const oe={selectionStartLineNumber:H.startLineNumber,selectionStartColumn:H.startColumn,positionLineNumber:H.endLineNumber,positionColumn:H.endColumn};this._setSelectionImpl(oe)}}_setSelectionImpl(H){if(!!this._modelData){const $=new n.Selection(H.selectionStartLineNumber,H.selectionStartColumn,H.positionLineNumber,H.positionColumn);this._modelData.viewModel.setSelections("api",[$])}}revealLines(H,$,ie=0){this._revealLines(H,$,0,ie)}revealLinesInCenter(H,$,ie=0){this._revealLines(H,$,1,ie)}revealLinesInCenterIfOutsideViewport(H,$,ie=0){this._revealLines(H,$,2,ie)}revealLinesNearTop(H,$,ie=0){this._revealLines(H,$,5,ie)}_revealLines(H,$,ie,oe){if(typeof H!="number"||typeof $!="number")throw new Error("Invalid arguments");this._sendRevealRange(new i.Range(H,1,$,1),ie,!1,oe)}revealRange(H,$=0,ie=!1,oe=!0){this._revealRange(H,ie?1:0,oe,$)}revealRangeInCenter(H,$=0){this._revealRange(H,1,!0,$)}revealRangeInCenterIfOutsideViewport(H,$=0){this._revealRange(H,2,!0,$)}revealRangeNearTop(H,$=0){this._revealRange(H,5,!0,$)}revealRangeNearTopIfOutsideViewport(H,$=0){this._revealRange(H,6,!0,$)}revealRangeAtTop(H,$=0){this._revealRange(H,3,!0,$)}_revealRange(H,$,ie,oe){if(!i.Range.isIRange(H))throw new Error("Invalid arguments");this._sendRevealRange(i.Range.lift(H),$,ie,oe)}setSelections(H,$="api",ie=0){if(!!this._modelData){if(!H||H.length===0)throw new Error("Invalid arguments");for(let oe=0,ae=H.length;oe$.isSupported()),H}getAction(H){return this._actions[H]||null}trigger(H,$,ie){switch(ie=ie||{},$){case"compositionStart":this._startComposition();return;case"compositionEnd":this._endComposition(H);return;case"type":{const ae=ie;this._type(H,ae.text||"");return}case"replacePreviousChar":{const ae=ie;this._compositionType(H,ae.text||"",ae.replaceCharCnt||0,0,0);return}case"compositionType":{const ae=ie;this._compositionType(H,ae.text||"",ae.replacePrevCharCnt||0,ae.replaceNextCharCnt||0,ae.positionDelta||0);return}case"paste":{const ae=ie;this._paste(H,ae.text||"",ae.pasteOnNewLine||!1,ae.multicursorText||null,ae.mode||null);return}case"cut":this._cut(H);return}const oe=this.getAction($);if(oe){Promise.resolve(oe.run()).then(void 0,M.onUnexpectedError);return}!this._modelData||this._triggerEditorCommand(H,$,ie)||this._commandService.executeCommand($,ie)}_startComposition(){!this._modelData||(this._modelData.viewModel.startComposition(),this._onDidCompositionStart.fire())}_endComposition(H){!this._modelData||(this._modelData.viewModel.endComposition(H),this._onDidCompositionEnd.fire())}_type(H,$){!this._modelData||$.length===0||(H==="keyboard"&&this._onWillType.fire($),this._modelData.viewModel.type($,H),H==="keyboard"&&this._onDidType.fire($))}_compositionType(H,$,ie,oe,ae){!this._modelData||this._modelData.viewModel.compositionType($,ie,oe,ae,H)}_paste(H,$,ie,oe,ae){if(!(!this._modelData||$.length===0)){const G=this._modelData.viewModel.getSelection().getStartPosition();this._modelData.viewModel.paste($,ie,oe,H);const j=this._modelData.viewModel.getSelection().getStartPosition();H==="keyboard"&&this._onDidPaste.fire({range:new i.Range(G.lineNumber,G.column,j.lineNumber,j.column),mode:ae})}}_cut(H){!this._modelData||this._modelData.viewModel.cut(H)}_triggerEditorCommand(H,$,ie){const oe=g.EditorExtensionsRegistry.getEditorCommand($);return oe?(ie=ie||{},ie.source=H,this._instantiationService.invokeFunction(ae=>{Promise.resolve(oe.runEditorCommand(ae,this,ie)).then(void 0,M.onUnexpectedError)}),!0):!1}_getViewModel(){return this._modelData?this._modelData.viewModel:null}pushUndoStop(){return!this._modelData||this._configuration.options.get(75)?!1:(this._modelData.model.pushStackElement(),!0)}popUndoStop(){return!this._modelData||this._configuration.options.get(75)?!1:(this._modelData.model.popStackElement(),!0)}executeEdits(H,$,ie){if(!this._modelData||this._configuration.options.get(75))return!1;let oe;return ie?Array.isArray(ie)?oe=()=>ie:oe=ie:oe=()=>null,this._modelData.viewModel.executeEdits(H,$,oe),!0}executeCommand(H,$){!this._modelData||this._modelData.viewModel.executeCommand($,H)}executeCommands(H,$){!this._modelData||this._modelData.viewModel.executeCommands($,H)}changeDecorations(H){return this._modelData?this._modelData.model.changeDecorations(H,this._id):null}getLineDecorations(H){return this._modelData?this._modelData.model.getLineDecorations(H,this._id,s.filterValidationDecorations(this._configuration.options)):null}deltaDecorations(H,$){return this._modelData?H.length===0&&$.length===0?H:this._modelData.model.deltaDecorations(H,$,this._id):[]}removeDecorations(H){const $=this._decorationTypeKeysToIds[H];$&&this.deltaDecorations($,[]),this._decorationTypeKeysToIds.hasOwnProperty(H)&&delete this._decorationTypeKeysToIds[H],this._decorationTypeSubtypes.hasOwnProperty(H)&&delete this._decorationTypeSubtypes[H]}getLayoutInfo(){return this._configuration.options.get(124)}createOverviewRuler(H){return!this._modelData||!this._modelData.hasRealView?null:this._modelData.view.createOverviewRuler(H)}getContainerDomNode(){return this._domElement}getDomNode(){return!this._modelData||!this._modelData.hasRealView?null:this._modelData.view.domNode.domNode}delegateVerticalScrollbarMouseDown(H){!this._modelData||!this._modelData.hasRealView||this._modelData.view.delegateVerticalScrollbarMouseDown(H)}layout(H){this._configuration.observeReferenceElement(H),this.render()}focus(){!this._modelData||!this._modelData.hasRealView||this._modelData.view.focus()}hasTextFocus(){return!this._modelData||!this._modelData.hasRealView?!1:this._modelData.view.isFocused()}hasWidgetFocus(){return this._focusTracker&&this._focusTracker.hasFocus()}addContentWidget(H){const $={widget:H,position:H.getPosition()};this._contentWidgets.hasOwnProperty(H.getId())&&console.warn("Overwriting a content widget with the same id."),this._contentWidgets[H.getId()]=$,this._modelData&&this._modelData.hasRealView&&this._modelData.view.addContentWidget($)}layoutContentWidget(H){const $=H.getId();if(this._contentWidgets.hasOwnProperty($)){const ie=this._contentWidgets[$];ie.position=H.getPosition(),this._modelData&&this._modelData.hasRealView&&this._modelData.view.layoutContentWidget(ie)}}removeContentWidget(H){const $=H.getId();if(this._contentWidgets.hasOwnProperty($)){const ie=this._contentWidgets[$];delete this._contentWidgets[$],this._modelData&&this._modelData.hasRealView&&this._modelData.view.removeContentWidget(ie)}}addOverlayWidget(H){const $={widget:H,position:H.getPosition()};this._overlayWidgets.hasOwnProperty(H.getId())&&console.warn("Overwriting an overlay widget with the same id."),this._overlayWidgets[H.getId()]=$,this._modelData&&this._modelData.hasRealView&&this._modelData.view.addOverlayWidget($)}layoutOverlayWidget(H){const $=H.getId();if(this._overlayWidgets.hasOwnProperty($)){const ie=this._overlayWidgets[$];ie.position=H.getPosition(),this._modelData&&this._modelData.hasRealView&&this._modelData.view.layoutOverlayWidget(ie)}}removeOverlayWidget(H){const $=H.getId();if(this._overlayWidgets.hasOwnProperty($)){const ie=this._overlayWidgets[$];delete this._overlayWidgets[$],this._modelData&&this._modelData.hasRealView&&this._modelData.view.removeOverlayWidget(ie)}}changeViewZones(H){!this._modelData||!this._modelData.hasRealView||this._modelData.view.change(H)}getTargetAtClientPoint(H,$){return!this._modelData||!this._modelData.hasRealView?null:this._modelData.view.getTargetAtClientPoint(H,$)}getScrolledVisiblePosition(H){if(!this._modelData||!this._modelData.hasRealView)return null;const $=this._modelData.model.validatePosition(H),ie=this._configuration.options,oe=ie.get(124),ae=ct._getVerticalOffsetForPosition(this._modelData,$.lineNumber,$.column)-this.getScrollTop(),G=this._modelData.view.getOffsetForColumn($.lineNumber,$.column)+oe.glyphMarginWidth+oe.lineNumbersWidth+oe.decorationsWidth-this.getScrollLeft();return{top:ae,left:G,height:ie.get(53)}}getOffsetForColumn(H,$){return!this._modelData||!this._modelData.hasRealView?-1:this._modelData.view.getOffsetForColumn(H,$)}render(H=!1){!this._modelData||!this._modelData.hasRealView||this._modelData.view.render(!0,H)}setAriaOptions(H){!this._modelData||!this._modelData.hasRealView||this._modelData.view.setAriaOptions(H)}applyFontInfo(H){d.Configuration.applyFontInfoSlow(H,this._configuration.options.get(38))}_attachModel(H){if(!H){this._modelData=null;return}const $=[];this._domElement.setAttribute("data-mode-id",H.getLanguageIdentifier().language),this._configuration.setIsDominatedByLongLines(H.isDominatedByLongLines()),this._configuration.setMaxLineNumber(H.getLineCount()),H.onBeforeAttached();const ie=new v.ViewModel(this._id,this._configuration,H,F.DOMLineBreaksComputerFactory.create(),B.MonospaceLineBreaksComputerFactory.create(this._configuration.options),G=>N.scheduleAtNextAnimationFrame(G));$.push(H.onDidChangeDecorations(G=>this._onDidChangeModelDecorations.fire(G))),$.push(H.onDidChangeLanguage(G=>{this._domElement.setAttribute("data-mode-id",H.getLanguageIdentifier().language),this._onDidChangeModelLanguage.fire(G)})),$.push(H.onDidChangeLanguageConfiguration(G=>this._onDidChangeModelLanguageConfiguration.fire(G))),$.push(H.onDidChangeContent(G=>this._onDidChangeModelContent.fire(G))),$.push(H.onDidChangeOptions(G=>this._onDidChangeModelOptions.fire(G))),$.push(H.onWillDispose(()=>this.setModel(null))),$.push(ie.onEvent(G=>{switch(G.kind){case 0:this._onDidContentSizeChange.fire(G);break;case 1:this._editorTextFocus.setValue(G.hasFocus);break;case 2:this._onDidScrollChange.fire(G);break;case 3:this._onDidChangeViewZones.fire();break;case 4:this._onDidAttemptReadOnlyEdit.fire();break;case 5:{G.reachedMaxCursorCount&&this._notificationService.warn(b.localize(0,null,a.Cursor.MAX_CURSOR_COUNT));const j=[];for(let ue=0,he=G.selections.length;ue{this._paste("keyboard",ae,G,j,te)},type:ae=>{this._type("keyboard",ae)},compositionType:(ae,G,j,te)=>{this._compositionType("keyboard",ae,G,j,te)},startComposition:()=>{this._startComposition()},endComposition:()=>{this._endComposition("keyboard")},cut:()=>{this._cut("keyboard")}}:$={paste:(ae,G,j,te)=>{const Z={text:ae,pasteOnNewLine:G,multicursorText:j,mode:te};this._commandService.executeCommand("paste",Z)},type:ae=>{const G={text:ae};this._commandService.executeCommand("type",G)},compositionType:(ae,G,j,te)=>{if(j||te){const Z={text:ae,replacePrevCharCnt:G,replaceNextCharCnt:j,positionDelta:te};this._commandService.executeCommand("compositionType",Z)}else{const Z={text:ae,replaceCharCnt:G};this._commandService.executeCommand("replacePreviousChar",Z)}},startComposition:()=>{this._commandService.executeCommand("compositionStart",{})},endComposition:()=>{this._commandService.executeCommand("compositionEnd",{})},cut:()=>{this._commandService.executeCommand("cut",{})}};const ie=new o.ViewUserInputEvents(H.coordinatesConverter);return ie.onKeyDown=ae=>this._onKeyDown.fire(ae),ie.onKeyUp=ae=>this._onKeyUp.fire(ae),ie.onContextMenu=ae=>this._onContextMenu.fire(ae),ie.onMouseMove=ae=>this._onMouseMove.fire(ae),ie.onMouseLeave=ae=>this._onMouseLeave.fire(ae),ie.onMouseDown=ae=>this._onMouseDown.fire(ae),ie.onMouseUp=ae=>this._onMouseUp.fire(ae),ie.onMouseDrag=ae=>this._onMouseDrag.fire(ae),ie.onMouseDrop=ae=>this._onMouseDrop.fire(ae),ie.onMouseDropCanceled=ae=>this._onMouseDropCanceled.fire(ae),ie.onMouseWheel=ae=>this._onMouseWheel.fire(ae),[new c.View($,this._configuration,this._themeService,H,ie,this._overflowWidgetsDomNode),!0]}_postDetachModelCleanup(H){H&&H.removeAllDecorationsWithOwnerId(this._id)}_detachModel(){if(!this._modelData)return null;const H=this._modelData.model,$=this._modelData.hasRealView?this._modelData.view.domNode.domNode:null;return this._modelData.dispose(),this._modelData=null,this._domElement.removeAttribute("data-mode-id"),$&&this._domElement.contains($)&&this._domElement.removeChild($),H}_removeDecorationType(H){this._codeEditorService.removeDecorationType(H)}hasModel(){return this._modelData!==null}};x=Me([_e(3,I.IInstantiationService),_e(4,p.ICodeEditorService),_e(5,y.ICommandService),_e(6,L.IContextKeyService),_e(7,T.IThemeService),_e(8,E.INotificationService),_e(9,O.IAccessibilityService)],x),e.CodeEditorWidget=x;class K extends S.Disposable{constructor(){super();this._onDidChangeToTrue=this._register(new w.Emitter),this.onDidChangeToTrue=this._onDidChangeToTrue.event,this._onDidChangeToFalse=this._register(new w.Emitter),this.onDidChangeToFalse=this._onDidChangeToFalse.event,this._value=0}setValue(H){const $=H?2:1;this._value!==$&&(this._value=$,this._value===2?this._onDidChangeToTrue.fire():this._value===1&&this._onDidChangeToFalse.fire())}}e.BooleanEventEmitter=K;class Y extends S.Disposable{constructor(H,$){super();this._editor=H,$.createKey("editorId",H.getId()),this._editorSimpleInput=h.EditorContextKeys.editorSimpleInput.bindTo($),this._editorFocus=h.EditorContextKeys.focus.bindTo($),this._textInputFocus=h.EditorContextKeys.textInputFocus.bindTo($),this._editorTextFocus=h.EditorContextKeys.editorTextFocus.bindTo($),this._editorTabMovesFocus=h.EditorContextKeys.tabMovesFocus.bindTo($),this._editorReadonly=h.EditorContextKeys.readOnly.bindTo($),this._inDiffEditor=h.EditorContextKeys.inDiffEditor.bindTo($),this._editorColumnSelection=h.EditorContextKeys.columnSelection.bindTo($),this._hasMultipleSelections=h.EditorContextKeys.hasMultipleSelections.bindTo($),this._hasNonEmptySelection=h.EditorContextKeys.hasNonEmptySelection.bindTo($),this._canUndo=h.EditorContextKeys.canUndo.bindTo($),this._canRedo=h.EditorContextKeys.canRedo.bindTo($),this._register(this._editor.onDidChangeConfiguration(()=>this._updateFromConfig())),this._register(this._editor.onDidChangeCursorSelection(()=>this._updateFromSelection())),this._register(this._editor.onDidFocusEditorWidget(()=>this._updateFromFocus())),this._register(this._editor.onDidBlurEditorWidget(()=>this._updateFromFocus())),this._register(this._editor.onDidFocusEditorText(()=>this._updateFromFocus())),this._register(this._editor.onDidBlurEditorText(()=>this._updateFromFocus())),this._register(this._editor.onDidChangeModel(()=>this._updateFromModel())),this._register(this._editor.onDidChangeConfiguration(()=>this._updateFromModel())),this._updateFromConfig(),this._updateFromSelection(),this._updateFromFocus(),this._updateFromModel(),this._editorSimpleInput.set(this._editor.isSimpleWidget)}_updateFromConfig(){const H=this._editor.getOptions();this._editorTabMovesFocus.set(H.get(123)),this._editorReadonly.set(H.get(75)),this._inDiffEditor.set(H.get(49)),this._editorColumnSelection.set(H.get(15))}_updateFromSelection(){const H=this._editor.getSelections();H?(this._hasMultipleSelections.set(H.length>1),this._hasNonEmptySelection.set(H.some($=>!$.isEmpty()))):(this._hasMultipleSelections.reset(),this._hasNonEmptySelection.reset())}_updateFromFocus(){this._editorFocus.set(this._editor.hasWidgetFocus()&&!this._editor.isSimpleWidget),this._editorTextFocus.set(this._editor.hasTextFocus()&&!this._editor.isSimpleWidget),this._textInputFocus.set(this._editor.hasTextFocus())}_updateFromModel(){const H=this._editor.getModel();this._canUndo.set(Boolean(H&&H.canUndo())),this._canRedo.set(Boolean(H&&H.canRedo()))}}class ee extends S.Disposable{constructor(H,$){super();this._editor=H,this._contextKeyService=$,this._langId=h.EditorContextKeys.languageId.bindTo($),this._hasCompletionItemProvider=h.EditorContextKeys.hasCompletionItemProvider.bindTo($),this._hasCodeActionsProvider=h.EditorContextKeys.hasCodeActionsProvider.bindTo($),this._hasCodeLensProvider=h.EditorContextKeys.hasCodeLensProvider.bindTo($),this._hasDefinitionProvider=h.EditorContextKeys.hasDefinitionProvider.bindTo($),this._hasDeclarationProvider=h.EditorContextKeys.hasDeclarationProvider.bindTo($),this._hasImplementationProvider=h.EditorContextKeys.hasImplementationProvider.bindTo($),this._hasTypeDefinitionProvider=h.EditorContextKeys.hasTypeDefinitionProvider.bindTo($),this._hasHoverProvider=h.EditorContextKeys.hasHoverProvider.bindTo($),this._hasDocumentHighlightProvider=h.EditorContextKeys.hasDocumentHighlightProvider.bindTo($),this._hasDocumentSymbolProvider=h.EditorContextKeys.hasDocumentSymbolProvider.bindTo($),this._hasReferenceProvider=h.EditorContextKeys.hasReferenceProvider.bindTo($),this._hasRenameProvider=h.EditorContextKeys.hasRenameProvider.bindTo($),this._hasSignatureHelpProvider=h.EditorContextKeys.hasSignatureHelpProvider.bindTo($),this._hasInlineHintsProvider=h.EditorContextKeys.hasInlineHintsProvider.bindTo($),this._hasDocumentFormattingProvider=h.EditorContextKeys.hasDocumentFormattingProvider.bindTo($),this._hasDocumentSelectionFormattingProvider=h.EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo($),this._hasMultipleDocumentFormattingProvider=h.EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo($),this._hasMultipleDocumentSelectionFormattingProvider=h.EditorContextKeys.hasMultipleDocumentSelectionFormattingProvider.bindTo($),this._isInWalkThrough=h.EditorContextKeys.isInWalkThroughSnippet.bindTo($);const ie=()=>this._update();this._register(H.onDidChangeModel(ie)),this._register(H.onDidChangeModelLanguage(ie)),this._register(m.CompletionProviderRegistry.onDidChange(ie)),this._register(m.CodeActionProviderRegistry.onDidChange(ie)),this._register(m.CodeLensProviderRegistry.onDidChange(ie)),this._register(m.DefinitionProviderRegistry.onDidChange(ie)),this._register(m.DeclarationProviderRegistry.onDidChange(ie)),this._register(m.ImplementationProviderRegistry.onDidChange(ie)),this._register(m.TypeDefinitionProviderRegistry.onDidChange(ie)),this._register(m.HoverProviderRegistry.onDidChange(ie)),this._register(m.DocumentHighlightProviderRegistry.onDidChange(ie)),this._register(m.DocumentSymbolProviderRegistry.onDidChange(ie)),this._register(m.ReferenceProviderRegistry.onDidChange(ie)),this._register(m.RenameProviderRegistry.onDidChange(ie)),this._register(m.DocumentFormattingEditProviderRegistry.onDidChange(ie)),this._register(m.DocumentRangeFormattingEditProviderRegistry.onDidChange(ie)),this._register(m.SignatureHelpProviderRegistry.onDidChange(ie)),this._register(m.InlineHintsProviderRegistry.onDidChange(ie)),ie()}dispose(){super.dispose()}reset(){this._contextKeyService.bufferChangeEvents(()=>{this._langId.reset(),this._hasCompletionItemProvider.reset(),this._hasCodeActionsProvider.reset(),this._hasCodeLensProvider.reset(),this._hasDefinitionProvider.reset(),this._hasDeclarationProvider.reset(),this._hasImplementationProvider.reset(),this._hasTypeDefinitionProvider.reset(),this._hasHoverProvider.reset(),this._hasDocumentHighlightProvider.reset(),this._hasDocumentSymbolProvider.reset(),this._hasReferenceProvider.reset(),this._hasRenameProvider.reset(),this._hasDocumentFormattingProvider.reset(),this._hasDocumentSelectionFormattingProvider.reset(),this._hasSignatureHelpProvider.reset(),this._isInWalkThrough.reset()})}_update(){const H=this._editor.getModel();if(!H){this.reset();return}this._contextKeyService.bufferChangeEvents(()=>{this._langId.set(H.getLanguageIdentifier().language),this._hasCompletionItemProvider.set(m.CompletionProviderRegistry.has(H)),this._hasCodeActionsProvider.set(m.CodeActionProviderRegistry.has(H)),this._hasCodeLensProvider.set(m.CodeLensProviderRegistry.has(H)),this._hasDefinitionProvider.set(m.DefinitionProviderRegistry.has(H)),this._hasDeclarationProvider.set(m.DeclarationProviderRegistry.has(H)),this._hasImplementationProvider.set(m.ImplementationProviderRegistry.has(H)),this._hasTypeDefinitionProvider.set(m.TypeDefinitionProviderRegistry.has(H)),this._hasHoverProvider.set(m.HoverProviderRegistry.has(H)),this._hasDocumentHighlightProvider.set(m.DocumentHighlightProviderRegistry.has(H)),this._hasDocumentSymbolProvider.set(m.DocumentSymbolProviderRegistry.has(H)),this._hasReferenceProvider.set(m.ReferenceProviderRegistry.has(H)),this._hasRenameProvider.set(m.RenameProviderRegistry.has(H)),this._hasSignatureHelpProvider.set(m.SignatureHelpProviderRegistry.has(H)),this._hasInlineHintsProvider.set(m.InlineHintsProviderRegistry.has(H)),this._hasDocumentFormattingProvider.set(m.DocumentFormattingEditProviderRegistry.has(H)||m.DocumentRangeFormattingEditProviderRegistry.has(H)),this._hasDocumentSelectionFormattingProvider.set(m.DocumentRangeFormattingEditProviderRegistry.has(H)),this._hasMultipleDocumentFormattingProvider.set(m.DocumentFormattingEditProviderRegistry.all(H).length+m.DocumentRangeFormattingEditProviderRegistry.all(H).length>1),this._hasMultipleDocumentSelectionFormattingProvider.set(m.DocumentRangeFormattingEditProviderRegistry.all(H).length>1),this._isInWalkThrough.set(H.uri.scheme===C.Schemas.walkThroughSnippet)})}}e.EditorModeContext=ee;class se extends S.Disposable{constructor(H){super();this._onChange=this._register(new w.Emitter),this.onChange=this._onChange.event,this._hasFocus=!1,this._domFocusTracker=this._register(N.trackFocus(H)),this._register(this._domFocusTracker.onDidFocus(()=>{this._hasFocus=!0,this._onChange.fire(void 0)})),this._register(this._domFocusTracker.onDidBlur(()=>{this._hasFocus=!1,this._onChange.fire(void 0)}))}hasFocus(){return this._hasFocus}}const ne=encodeURIComponent("");function X(U){return ne+encodeURIComponent(U.toString())+le}const z=encodeURIComponent('');function V(U){return z+encodeURIComponent(U.toString())+P}T.registerThemingParticipant((U,H)=>{const $=U.getColor(f.editorErrorBorder);$&&H.addRule(`.monaco-editor .squiggly-error { border-bottom: 4px double ${$}; }`);const ie=U.getColor(f.editorErrorForeground);ie&&H.addRule(`.monaco-editor .squiggly-error { background: url("data:image/svg+xml,${X(ie)}") repeat-x bottom left; }`);const oe=U.getColor(f.editorErrorBackground);oe&&H.addRule(`.monaco-editor .squiggly-error::before { display: block; content: ''; width: 100%; height: 100%; background: ${oe}; }`);const ae=U.getColor(f.editorWarningBorder);ae&&H.addRule(`.monaco-editor .squiggly-warning { border-bottom: 4px double ${ae}; }`);const G=U.getColor(f.editorWarningForeground);G&&H.addRule(`.monaco-editor .squiggly-warning { background: url("data:image/svg+xml,${X(G)}") repeat-x bottom left; }`);const j=U.getColor(f.editorWarningBackground);j&&H.addRule(`.monaco-editor .squiggly-warning::before { display: block; content: ''; width: 100%; height: 100%; background: ${j}; }`);const te=U.getColor(f.editorInfoBorder);te&&H.addRule(`.monaco-editor .squiggly-info { border-bottom: 4px double ${te}; }`);const Z=U.getColor(f.editorInfoForeground);Z&&H.addRule(`.monaco-editor .squiggly-info { background: url("data:image/svg+xml,${X(Z)}") repeat-x bottom left; }`);const ue=U.getColor(f.editorInfoBackground);ue&&H.addRule(`.monaco-editor .squiggly-info::before { display: block; content: ''; width: 100%; height: 100%; background: ${ue}; }`);const he=U.getColor(f.editorHintBorder);he&&H.addRule(`.monaco-editor .squiggly-hint { border-bottom: 2px dotted ${he}; }`);const re=U.getColor(f.editorHintForeground);re&&H.addRule(`.monaco-editor .squiggly-hint { background: url("data:image/svg+xml,${V(re)}") no-repeat bottom left; }`);const ce=U.getColor(_.editorUnnecessaryCodeOpacity);ce&&H.addRule(`.monaco-editor.showUnused .squiggly-inline-unnecessary { opacity: ${ce.rgba.a}; }`);const me=U.getColor(_.editorUnnecessaryCodeBorder);me&&H.addRule(`.monaco-editor.showUnused .squiggly-unnecessary { border-bottom: 2px dashed ${me}; }`);const Ce=U.getColor(f.editorForeground)||"inherit";H.addRule(`.monaco-editor.showDeprecated .squiggly-inline-deprecated { text-decoration: line-through; text-decoration-color: ${Ce}}`)})}),define(Q[144],J([0,1,40,28,143,26,16,9,32,11,65]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.EmbeddedCodeEditorWidget=void 0;let c=class extends M.CodeEditorWidget{constructor(s,a,u,r,i,n,t,l,h,m){super(s,Object.assign(Object.assign({},u.getRawOptions()),{overflowWidgetsDomNode:u.getOverflowWidgetsDomNode()}),{},r,i,n,t,l,h,m);this._parentEditor=u,this._overwriteOptions=a,super.updateOptions(this._overwriteOptions),this._register(u.onDidChangeConfiguration(_=>this._onParentConfigurationChanged(_)))}getParentEditor(){return this._parentEditor}_onParentConfigurationChanged(s){super.updateOptions(this._parentEditor.getRawOptions()),super.updateOptions(this._overwriteOptions)}updateOptions(s){b.mixin(this._overwriteOptions,s,!0),super.updateOptions(this._overwriteOptions)}};c=Me([_e(3,C.IInstantiationService),_e(4,N.ICodeEditorService),_e(5,w.ICommandService),_e(6,S.IContextKeyService),_e(7,g.IThemeService),_e(8,d.INotificationService),_e(9,p.IAccessibilityService)],c),e.EmbeddedCodeEditorWidget=c}),define(Q[611],J([0,1,13,461,25,21,39,16,73,47,335]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SelectionAnchorSet=void 0,e.SelectionAnchorSet=new C.RawContextKey("selectionAnchorSet",!1);let p=class Ft{constructor(r,i){this.editor=r,this.selectionAnchorSetContextKey=e.SelectionAnchorSet.bindTo(i),this.modelChangeListener=r.onDidChangeModel(()=>this.selectionAnchorSetContextKey.reset())}static get(r){return r.getContribution(Ft.ID)}setSelectionAnchor(){if(this.editor.hasModel()){const r=this.editor.getPosition(),i=this.decorationId?[this.decorationId]:[],n=this.editor.deltaDecorations(i,[{range:w.Selection.fromPositions(r,r),options:{stickiness:1,hoverMessage:new d.MarkdownString().appendText(N.localize(0,null)),className:"selection-anchor"}}]);this.decorationId=n[0],this.selectionAnchorSetContextKey.set(!!this.decorationId),g.alert(N.localize(1,null,r.lineNumber,r.column))}}goToSelectionAnchor(){if(this.editor.hasModel()&&this.decorationId){const r=this.editor.getModel().getDecorationRange(this.decorationId);r&&this.editor.setPosition(r.getStartPosition())}}selectFromAnchorToCursor(){if(this.editor.hasModel()&&this.decorationId){const r=this.editor.getModel().getDecorationRange(this.decorationId);if(r){const i=this.editor.getPosition();this.editor.setSelection(w.Selection.fromPositions(r.getStartPosition(),i)),this.cancelSelectionAnchor()}}}cancelSelectionAnchor(){this.decorationId&&(this.editor.deltaDecorations([this.decorationId],[]),this.decorationId=void 0,this.selectionAnchorSetContextKey.set(!1))}dispose(){this.cancelSelectionAnchor(),this.modelChangeListener.dispose()}};p.ID="editor.contrib.selectionAnchorController",p=Me([_e(1,C.IContextKeyService)],p);class c extends b.EditorAction{constructor(){super({id:"editor.action.setSelectionAnchor",label:N.localize(2,null),alias:"Set Selection Anchor",precondition:void 0,kbOpts:{kbExpr:M.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|32),weight:100}})}run(r,i){return Ie(this,void 0,void 0,function*(){p.get(i).setSelectionAnchor()})}}class o extends b.EditorAction{constructor(){super({id:"editor.action.goToSelectionAnchor",label:N.localize(3,null),alias:"Go to Selection Anchor",precondition:e.SelectionAnchorSet})}run(r,i){return Ie(this,void 0,void 0,function*(){p.get(i).goToSelectionAnchor()})}}class s extends b.EditorAction{constructor(){super({id:"editor.action.selectFromAnchorToCursor",label:N.localize(4,null),alias:"Select from Anchor to Cursor",precondition:e.SelectionAnchorSet,kbOpts:{kbExpr:M.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|41),weight:100}})}run(r,i){return Ie(this,void 0,void 0,function*(){p.get(i).selectFromAnchorToCursor()})}}class a extends b.EditorAction{constructor(){super({id:"editor.action.cancelSelectionAnchor",label:N.localize(5,null),alias:"Cancel Selection Anchor",precondition:e.SelectionAnchorSet,kbOpts:{kbExpr:M.EditorContextKeys.editorTextFocus,primary:9,weight:100}})}run(r,i){return Ie(this,void 0,void 0,function*(){p.get(i).cancelSelectionAnchor()})}}b.registerEditorContribution(p.ID,p),b.registerEditorAction(c),b.registerEditorAction(o),b.registerEditorAction(s),b.registerEditorAction(a)}),define(Q[612],J([0,1,462,15,2,13,14,3,21,25,53,31,49,22,11,34,336]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.BracketMatchingController=void 0;const r=s.registerColor("editorOverviewRuler.bracketMatchForeground",{dark:"#A0A0A0",light:"#A0A0A0",hc:"#A0A0A0"},b.localize(0,null));class i extends w.EditorAction{constructor(){super({id:"editor.action.jumpToBracket",label:b.localize(1,null),alias:"Go to Bracket",precondition:void 0,kbOpts:{kbExpr:g.EditorContextKeys.editorTextFocus,primary:2048|1024|88,weight:100}})}run(m,_){let f=l.get(_);!f||f.jumpToBracket()}}class n extends w.EditorAction{constructor(){super({id:"editor.action.selectToBracket",label:b.localize(2,null),alias:"Select to Bracket",precondition:void 0,description:{description:"Select to Bracket",args:[{name:"args",schema:{type:"object",properties:{selectBrackets:{type:"boolean",default:!0}}}}]}})}run(m,_,f){const v=l.get(_);if(!!v){let y=!0;f&&f.selectBrackets===!1&&(y=!1),v.selectToBracket(y)}}}class t{constructor(m,_,f){this.position=m,this.brackets=_,this.options=f}}class l extends M.Disposable{constructor(m){super();this._editor=m,this._lastBracketsData=[],this._lastVersionId=0,this._decorations=[],this._updateBracketsSoon=this._register(new N.RunOnceScheduler(()=>this._updateBrackets(),50)),this._matchBrackets=this._editor.getOption(58),this._updateBracketsSoon.schedule(),this._register(m.onDidChangeCursorPosition(_=>{this._matchBrackets!=="never"&&this._updateBracketsSoon.schedule()})),this._register(m.onDidChangeModelContent(_=>{this._updateBracketsSoon.schedule()})),this._register(m.onDidChangeModel(_=>{this._lastBracketsData=[],this._decorations=[],this._updateBracketsSoon.schedule()})),this._register(m.onDidChangeModelLanguageConfiguration(_=>{this._lastBracketsData=[],this._updateBracketsSoon.schedule()})),this._register(m.onDidChangeConfiguration(_=>{_.hasChanged(58)&&(this._matchBrackets=this._editor.getOption(58),this._decorations=this._editor.deltaDecorations(this._decorations,[]),this._lastBracketsData=[],this._lastVersionId=0,this._updateBracketsSoon.schedule())}))}static get(m){return m.getContribution(l.ID)}jumpToBracket(){if(!!this._editor.hasModel()){const m=this._editor.getModel(),_=this._editor.getSelections().map(f=>{const v=f.getStartPosition(),y=m.matchBracket(v);let L=null;if(y)y[0].containsPosition(v)?L=y[1].getStartPosition():y[1].containsPosition(v)&&(L=y[0].getStartPosition());else{const I=m.findEnclosingBrackets(v);if(I)L=I[0].getStartPosition();else{const k=m.findNextBracket(v);k&&k.range&&(L=k.range.getStartPosition())}}return L?new d.Selection(L.lineNumber,L.column,L.lineNumber,L.column):new d.Selection(v.lineNumber,v.column,v.lineNumber,v.column)});this._editor.setSelections(_),this._editor.revealRange(_[0])}}selectToBracket(m){if(!!this._editor.hasModel()){const _=this._editor.getModel(),f=[];this._editor.getSelections().forEach(v=>{const y=v.getStartPosition();let L=_.matchBracket(y);if(!L&&(L=_.findEnclosingBrackets(y),!L)){const E=_.findNextBracket(y);E&&E.range&&(L=_.matchBracket(E.range.getStartPosition()))}let I=null,k=null;if(L){L.sort(C.Range.compareRangesUsingStarts);const[E,T]=L;I=m?E.getStartPosition():E.getEndPosition(),k=m?T.getEndPosition():T.getStartPosition()}I&&k&&f.push(new d.Selection(I.lineNumber,I.column,k.lineNumber,k.column))}),f.length>0&&(this._editor.setSelections(f),this._editor.revealRange(f[0]))}}_updateBrackets(){if(this._matchBrackets!=="never"){this._recomputeBrackets();let m=[],_=0;for(const f of this._lastBracketsData){let v=f.brackets;v&&(m[_++]={range:v[0],options:f.options},m[_++]={range:v[1],options:f.options})}this._decorations=this._editor.deltaDecorations(this._decorations,m)}}_recomputeBrackets(){if(!this._editor.hasModel()){this._lastBracketsData=[],this._lastVersionId=0;return}const m=this._editor.getSelections();if(m.length>100){this._lastBracketsData=[],this._lastVersionId=0;return}const _=this._editor.getModel(),f=_.getVersionId();let v=[];this._lastVersionId===f&&(v=this._lastBracketsData);let y=[],L=0;for(let O=0,A=m.length;O1&&y.sort(S.Position.compare);let I=[],k=0,E=0,T=v.length;for(let O=0,A=y.length;O{const _=h.getColor(o.editorBracketMatchBackground);_&&m.addRule(`.monaco-editor .bracket-match { background-color: ${_}; }`);const f=h.getColor(o.editorBracketMatchBorder);f&&m.addRule(`.monaco-editor .bracket-match { border: 1px solid ${f}; }`)}),u.MenuRegistry.appendMenuItem(u.MenuId.MenubarGoMenu,{group:"5_infile_nav",command:{id:"editor.action.jumpToBracket",title:b.localize(3,null)},order:2})}),define(Q[613],J([0,1,463,13,25,399]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0});class S extends N.EditorAction{constructor(p,c){super(c);this.left=p}run(p,c){if(!!c.hasModel()){let o=[],s=c.getSelections();for(const a of s)o.push(new w.MoveCaretCommand(a,this.left));c.pushUndoStop(),c.executeCommands(this.id,o),c.pushUndoStop()}}}class C extends S{constructor(){super(!0,{id:"editor.action.moveCarretLeftAction",label:b.localize(0,null),alias:"Move Selected Text Left",precondition:M.EditorContextKeys.writable})}}class d extends S{constructor(){super(!1,{id:"editor.action.moveCarretRightAction",label:b.localize(1,null),alias:"Move Selected Text Right",precondition:M.EditorContextKeys.writable})}}N.registerEditorAction(C),N.registerEditorAction(d)}),define(Q[614],J([0,1,464,13,92,3,25,180]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0});class d extends N.EditorAction{constructor(){super({id:"editor.action.transposeLetters",label:b.localize(0,null),alias:"Transpose Letters",precondition:S.EditorContextKeys.writable,kbOpts:{kbExpr:S.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|50},weight:100}})}run(p,c){if(!!c.hasModel()){let o=c.getModel(),s=[],a=c.getSelections();for(let u of a)if(!!u.isEmpty()){let r=u.startLineNumber,i=u.startColumn,n=o.getLineMaxColumn(r);if(!(r===1&&(i===1||i===2&&n===2))){let t=i===n?u.getPosition():C.MoveOperations.rightPosition(o,u.getPosition().lineNumber,u.getPosition().column),l=C.MoveOperations.leftPosition(o,t.lineNumber,t.column),h=C.MoveOperations.leftPosition(o,l.lineNumber,l.column),m=o.getValueInRange(w.Range.fromPositions(h,l)),_=o.getValueInRange(w.Range.fromPositions(l,t)),f=w.Range.fromPositions(h,t);s.push(new M.ReplaceCommand(f,_+m))}}s.length>0&&(c.pushUndoStop(),c.executeCommands(this.id,s),c.pushUndoStop())}}}N.registerEditorAction(d)}),define(Q[615],J([0,1,465,35,17,164,13,28,25,34,84]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.PasteAction=e.CopyAction=e.CutAction=void 0;const c="9_cutcopypaste",o=M.isNative||document.queryCommandSupported("cut"),s=M.isNative||document.queryCommandSupported("copy"),a=typeof navigator.clipboard=="undefined"||N.isFirefox?document.queryCommandSupported("paste"):!0;function u(n){return n.register(),n}e.CutAction=o?u(new S.MultiCommand({id:"editor.action.clipboardCutAction",precondition:void 0,kbOpts:M.isNative?{primary:2048|54,win:{primary:2048|54,secondary:[1024|20]},weight:100}:void 0,menuOpts:[{menuId:g.MenuId.MenubarEditMenu,group:"2_ccp",title:b.localize(0,null),order:1},{menuId:g.MenuId.EditorContext,group:c,title:b.localize(1,null),when:d.EditorContextKeys.writable,order:1},{menuId:g.MenuId.CommandPalette,group:"",title:b.localize(2,null),order:1}]})):void 0,e.CopyAction=s?u(new S.MultiCommand({id:"editor.action.clipboardCopyAction",precondition:void 0,kbOpts:M.isNative?{primary:2048|33,win:{primary:2048|33,secondary:[2048|19]},weight:100}:void 0,menuOpts:[{menuId:g.MenuId.MenubarEditMenu,group:"2_ccp",title:b.localize(3,null),order:2},{menuId:g.MenuId.EditorContext,group:c,title:b.localize(4,null),order:2},{menuId:g.MenuId.CommandPalette,group:"",title:b.localize(5,null),order:1}]})):void 0,e.PasteAction=a?u(new S.MultiCommand({id:"editor.action.clipboardPasteAction",precondition:void 0,kbOpts:M.isNative?{primary:2048|52,win:{primary:2048|52,secondary:[1024|19]},linux:{primary:2048|52,secondary:[1024|19]},weight:100}:void 0,menuOpts:[{menuId:g.MenuId.MenubarEditMenu,group:"2_ccp",title:b.localize(6,null),order:3},{menuId:g.MenuId.EditorContext,group:c,title:b.localize(7,null),when:d.EditorContextKeys.writable,order:3},{menuId:g.MenuId.CommandPalette,group:"",title:b.localize(8,null),order:1}]})):void 0;class r extends S.EditorAction{constructor(){super({id:"editor.action.clipboardCopyWithSyntaxHighlightingAction",label:b.localize(9,null),alias:"Copy With Syntax Highlighting",precondition:void 0,kbOpts:{kbExpr:d.EditorContextKeys.textInputFocus,primary:0,weight:100}})}run(t,l){!l.hasModel()||!l.getOption(28)&&l.getSelection().isEmpty()||(w.CopyOptions.forceCopyWithSyntaxHighlighting=!0,l.focus(),document.execCommand("copy"),w.CopyOptions.forceCopyWithSyntaxHighlighting=!1)}}function i(n,t){!n||(n.addImplementation(1e4,(l,h)=>{const m=l.get(C.ICodeEditorService).getFocusedCodeEditor();if(m&&m.hasTextFocus()){const _=m.getOption(28),f=m.getSelection();return f&&f.isEmpty()&&!_||document.execCommand(t),!0}return!1}),n.addImplementation(0,(l,h)=>(document.execCommand(t),!0)))}i(e.CutAction,"cut"),i(e.CopyAction,"copy"),e.PasteAction&&(e.PasteAction.addImplementation(1e4,(n,t)=>{const l=n.get(C.ICodeEditorService),h=n.get(p.IClipboardService),m=l.getFocusedCodeEditor();return m&&m.hasTextFocus()?(!document.execCommand("paste")&&M.isWeb&&(()=>Ie(void 0,void 0,void 0,function*(){const f=yield h.readText();if(f!==""){const v=w.InMemoryClipboardMetadataManager.INSTANCE.get(f);let y=!1,L=null,I=null;v&&(y=m.getOption(28)&&!!v.isFromEmptySelection,L=typeof v.multicursorText!="undefined"?v.multicursorText:null,I=v.mode),m.trigger("keyboard","paste",{text:f,pasteOnNewLine:y,multicursorText:L,mode:I})}}))(),!0):!1}),e.PasteAction.addImplementation(0,(n,t)=>(document.execCommand("paste"),!0))),s&&S.registerEditorAction(r)}),define(Q[145],J([0,1,19,23,12,2,24,70,3,21,18,36,130,59,26]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getCodeActions=e.CodeActionItem=e.fixAllCommandId=e.organizeImportsCommandId=e.sourceActionCommandId=e.refactorCommandId=e.codeActionCommandId=void 0,e.codeActionCommandId="editor.action.codeAction",e.refactorCommandId="editor.action.refactor",e.sourceActionCommandId="editor.action.sourceAction",e.organizeImportsCommandId="editor.action.organizeImports",e.fixAllCommandId="editor.action.fixAll";class u{constructor(m,_){this.action=m,this.provider=_}resolve(m){var _;return Ie(this,void 0,void 0,function*(){if(((_=this.provider)===null||_===void 0?void 0:_.resolveCodeAction)&&!this.action.edit){let f;try{f=yield this.provider.resolveCodeAction(this.action,m)}catch(v){M.onUnexpectedExternalError(v)}f&&(this.action.edit=f.edit)}return this})}}e.CodeActionItem=u;class r extends w.Disposable{constructor(m,_,f){super();this.documentation=_,this._register(f),this.allActions=b.mergeSort([...m],r.codeActionsComparator),this.validActions=this.allActions.filter(({action:v})=>!v.disabled)}static codeActionsComparator({action:m},{action:_}){return m.isPreferred&&!_.isPreferred?-1:!m.isPreferred&&_.isPreferred?1:b.isNonEmptyArray(m.diagnostics)?b.isNonEmptyArray(_.diagnostics)?m.diagnostics[0].message.localeCompare(_.diagnostics[0].message):-1:b.isNonEmptyArray(_.diagnostics)?1:0}get hasAutoFix(){return this.validActions.some(({action:m})=>!!m.kind&&o.CodeActionKind.QuickFix.contains(new o.CodeActionKind(m.kind))&&!!m.isPreferred)}}const i={actions:[],documentation:void 0};function n(h,m,_,f,v){var y;const L=_.filter||{},I={only:(y=L.include)===null||y===void 0?void 0:y.value,trigger:_.type},k=new C.TextModelCancellationTokenSource(h,v),E=t(h,L),T=new w.DisposableStore,O=E.map(B=>Ie(this,void 0,void 0,function*(){try{f.report(B);const F=yield B.provideCodeActions(h,m,I,k.token);if(F&&T.add(F),k.token.isCancellationRequested)return i;const D=((F==null?void 0:F.actions)||[]).filter(W=>W&&o.filtersAction(L,W)),R=l(B,D,L.include);return{actions:D.map(W=>new u(W,B)),documentation:R}}catch(F){if(M.isPromiseCanceledError(F))throw F;return M.onUnexpectedExternalError(F),i}})),A=p.CodeActionProviderRegistry.onDidChange(()=>{const B=p.CodeActionProviderRegistry.all(h);b.equals(B,E)||k.cancel()});return Promise.all(O).then(B=>{const F=b.flatten(B.map(R=>R.actions)),D=b.coalesce(B.map(R=>R.documentation));return new r(F,D,T)}).finally(()=>{A.dispose(),k.dispose()})}e.getCodeActions=n;function t(h,m){return p.CodeActionProviderRegistry.all(h).filter(_=>_.providedCodeActionKinds?_.providedCodeActionKinds.some(f=>o.mayIncludeActionsOfKind(m,new o.CodeActionKind(f))):!0)}function l(h,m,_){if(!!h.documentation){const f=h.documentation.map(v=>({kind:new o.CodeActionKind(v.kind),command:v.command}));if(_){let v;for(const y of f)y.kind.contains(_)&&(v?v.kind.contains(y.kind)&&(v=y):v=y);if(v)return v==null?void 0:v.command}for(const v of m)if(!!v.kind){for(const y of f)if(y.kind.contains(new o.CodeActionKind(v.kind)))return y.command}}}a.CommandsRegistry.registerCommand("_executeCodeActionProvider",function(h,m,_,f,v){return Ie(this,void 0,void 0,function*(){if(!(m instanceof S.URI))throw M.illegalArgument();const y=h.get(c.IModelService).getModel(m);if(!y)throw M.illegalArgument();const L=g.Selection.isISelection(_)?g.Selection.liftSelection(_):d.Range.isIRange(_)?y.validateRange(_):void 0;if(!L)throw M.illegalArgument();const I=typeof f=="string"?new o.CodeActionKind(f):void 0,k=yield n(y,L,{type:2,filter:{includeSourceActions:!0,include:I}},s.Progress.None,N.CancellationToken.None),E=[],T=Math.min(k.validActions.length,typeof v=="number"?v:0);for(let O=0;OO.action)}finally{setTimeout(()=>k.dispose(),100)}})})}),define(Q[616],J([0,1,7,48,12,150,2,14,18,145,130,68,37]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CodeActionKeybindingResolver=e.CodeActionMenu=void 0;class s extends N.Action{constructor(n,t){super(n.command?n.command.id:n.title,a(n.title),void 0,!n.disabled,t);this.action=n}}function a(i){return i.replace(/\r\n|\r|\n/g," ")}let u=class extends S.Disposable{constructor(n,t,l,h){super();this._editor=n,this._delegate=t,this._contextMenuService=l,this._visible=!1,this._showingActions=this._register(new S.MutableDisposable),this._keybindingResolver=new r({getKeybindings:()=>h.getKeybindings()})}get isVisible(){return this._visible}show(n,t,l,h){return Ie(this,void 0,void 0,function*(){const m=h.includeDisabledActions?t.allActions:t.validActions;if(!m.length){this._visible=!1;return}if(!this._editor.getDomNode())throw this._visible=!1,M.canceled();this._visible=!0,this._showingActions.value=t;const _=this.getMenuActions(n,m,t.documentation),f=C.Position.isIPosition(l)?this._toCoords(l):l||{x:0,y:0},v=this._keybindingResolver.getResolver();this._contextMenuService.showContextMenu({domForShadowRoot:this._editor.getDomNode(),getAnchor:()=>f,getActions:()=>_,onHide:()=>{this._visible=!1,this._editor.focus()},autoSelectFirstItem:!0,getKeyBinding:y=>y instanceof s?v(y.action):void 0})})}getMenuActions(n,t,l){var h,m;const _=L=>new s(L.action,()=>this._delegate.onSelectCodeAction(L)),f=t.map(_),v=[...l],y=this._editor.getModel();if(y&&f.length)for(const L of d.CodeActionProviderRegistry.all(y))L._getAdditionalMenuItems&&v.push(...L._getAdditionalMenuItems({trigger:n.type,only:(m=(h=n.filter)===null||h===void 0?void 0:h.include)===null||m===void 0?void 0:m.value},t.map(I=>I.action)));return v.length&&f.push(new N.Separator,...v.map(L=>_(new g.CodeActionItem({title:L.title,command:L},void 0)))),f}_toCoords(n){if(!this._editor.hasModel())return{x:0,y:0};this._editor.revealPosition(n,1),this._editor.render();const t=this._editor.getScrolledVisiblePosition(n),l=b.getDomNodePagePosition(this._editor.getDomNode()),h=l.left+t.left,m=l.top+t.top+t.height;return{x:h,y:m}}};u=Me([_e(2,c.IContextMenuService),_e(3,o.IKeybindingService)],u),e.CodeActionMenu=u;class r{constructor(n){this._keybindingProvider=n}getResolver(){const n=new w.Lazy(()=>this._keybindingProvider.getKeybindings().filter(t=>r.codeActionCommands.indexOf(t.command)>=0).filter(t=>t.resolvedKeybinding).map(t=>{let l=t.commandArgs;return t.command===g.organizeImportsCommandId?l={kind:p.CodeActionKind.SourceOrganizeImports.value}:t.command===g.fixAllCommandId&&(l={kind:p.CodeActionKind.SourceFixAll.value}),Object.assign({resolvedKeybinding:t.resolvedKeybinding},p.CodeActionCommandArgs.fromUser(l,{kind:p.CodeActionKind.None,apply:"never"}))}));return t=>{if(t.kind){const l=this.bestKeybindingForCodeAction(t,n.getValue());return l==null?void 0:l.resolvedKeybinding}}}bestKeybindingForCodeAction(n,t){if(!!n.kind){const l=new p.CodeActionKind(n.kind);return t.filter(h=>h.kind.contains(l)).filter(h=>h.preferred?n.isPreferred:!0).reduceRight((h,m)=>h?h.kind.contains(m.kind)?m:h:m,void 0)}}}e.CodeActionKeybindingResolver=r,r.codeActionCommands=[g.refactorCommandId,g.codeActionCommandId,g.sourceActionCommandId,g.organizeImportsCommandId,g.fixAllCommandId]});var tt=this&&this.__classPrivateFieldGet||function(q,e){if(!e.has(q))throw new TypeError("attempted to get private field on non-instance");return e.get(q)},_t=this&&this.__classPrivateFieldSet||function(q,e,b){if(!e.has(q))throw new TypeError("attempted to set private field on non-instance");return e.set(q,b),b};define(Q[617],J([0,1,15,12,6,2,44,3,18,16,59,145]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";var o;Object.defineProperty(e,"__esModule",{value:!0}),e.CodeActionModel=e.CodeActionsState=e.SUPPORTED_CODE_ACTIONS=void 0,e.SUPPORTED_CODE_ACTIONS=new g.RawContextKey("supportedCodeAction","");class s extends w.Disposable{constructor(n,t,l,h=250){super();this._editor=n,this._markerService=t,this._signalChange=l,this._delay=h,this._autoTriggerTimer=this._register(new b.TimeoutTimer),this._register(this._markerService.onMarkerChanged(m=>this._onMarkerChanges(m))),this._register(this._editor.onDidChangeCursorPosition(()=>this._onCursorChange()))}trigger(n){const t=this._getRangeOfSelectionUnlessWhitespaceEnclosed(n);return this._createEventAndSignalChange(n,t)}_onMarkerChanges(n){const t=this._editor.getModel();!t||n.some(l=>S.isEqual(l,t.uri))&&this._autoTriggerTimer.cancelAndSet(()=>{this.trigger({type:1})},this._delay)}_onCursorChange(){this._autoTriggerTimer.cancelAndSet(()=>{this.trigger({type:1})},this._delay)}_getRangeOfMarker(n){const t=this._editor.getModel();if(!!t)for(const l of this._markerService.read({resource:t.uri})){const h=t.validateRange(l);if(C.Range.intersectRanges(h,n))return C.Range.lift(h)}}_getRangeOfSelectionUnlessWhitespaceEnclosed(n){if(!!this._editor.hasModel()){const t=this._editor.getModel(),l=this._editor.getSelection();if(l.isEmpty()&&n.type===1){const{lineNumber:h,column:m}=l.getPosition(),_=t.getLineContent(h);if(_.length===0)return;if(m===1){if(/\s/.test(_[0]))return}else if(m===t.getLineMaxColumn(h)){if(/\s/.test(_[_.length-1]))return}else if(/\s/.test(_[m-2])&&/\s/.test(_[m-1]))return}return l}}_createEventAndSignalChange(n,t){const l=this._editor.getModel();if(!t||!l){this._signalChange(void 0);return}const h=this._getRangeOfMarker(t),m=h?h.getStartPosition():t.getStartPosition(),_={trigger:n,selection:t,position:m};return this._signalChange(_),_}}var a;(function(i){i.Empty={type:0};class n{constructor(l,h,m,_){this.trigger=l,this.rangeOrSelection=h,this.position=m,this._cancellablePromise=_,this.type=1,this.actions=_.catch(f=>{if(N.isPromiseCanceledError(f))return u;throw f})}cancel(){this._cancellablePromise.cancel()}}i.Triggered=n})(a=e.CodeActionsState||(e.CodeActionsState={}));const u={allActions:[],validActions:[],dispose:()=>{},documentation:[],hasAutoFix:!1};class r extends w.Disposable{constructor(n,t,l,h){super();this._editor=n,this._markerService=t,this._progressService=h,this._codeActionOracle=this._register(new w.MutableDisposable),this._state=a.Empty,this._onDidChangeState=this._register(new M.Emitter),this.onDidChangeState=this._onDidChangeState.event,o.set(this,!1),this._supportedCodeActions=e.SUPPORTED_CODE_ACTIONS.bindTo(l),this._register(this._editor.onDidChangeModel(()=>this._update())),this._register(this._editor.onDidChangeModelLanguage(()=>this._update())),this._register(d.CodeActionProviderRegistry.onDidChange(()=>this._update())),this._update()}dispose(){tt(this,o)||(_t(this,o,!0),super.dispose(),this.setState(a.Empty,!0))}_update(){if(!tt(this,o)){this._codeActionOracle.value=void 0,this.setState(a.Empty);const n=this._editor.getModel();if(n&&d.CodeActionProviderRegistry.has(n)&&!this._editor.getOption(75)){const t=[];for(const l of d.CodeActionProviderRegistry.all(n))Array.isArray(l.providedCodeActionKinds)&&t.push(...l.providedCodeActionKinds);this._supportedCodeActions.set(t.join(" ")),this._codeActionOracle.value=new s(this._editor,this._markerService,l=>{var h;if(!l){this.setState(a.Empty);return}const m=b.createCancelablePromise(_=>c.getCodeActions(n,l.selection,l.trigger,p.Progress.None,_));l.trigger.type===2&&((h=this._progressService)===null||h===void 0||h.showWhile(m,250)),this.setState(new a.Triggered(l.trigger,l.selection,l.position,m))},void 0),this._codeActionOracle.value.trigger({type:1})}else this._supportedCodeActions.reset()}}trigger(n){this._codeActionOracle.value&&this._codeActionOracle.value.trigger(n)}setState(n,t){n!==this._state&&(this._state.type===1&&this._state.cancel(),this._state=n,!t&&!tt(this,o)&&this._onDidChangeState.fire(n))}}e.CodeActionModel=r,o=new WeakMap}),define(Q[618],J([0,1,15,12,2,70,13,18,248,596,26,32,575,7,89,78,468,25,134]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CodeLensContribution=void 0;let t=class{constructor(h,m,_,f){this._editor=h,this._commandService=m,this._notificationService=_,this._codeLensCache=f,this._disposables=new M.DisposableStore,this._localToDispose=new M.DisposableStore,this._lenses=[],this._getCodeLensModelDelays=new n.LanguageFeatureRequestDelays(C.CodeLensProviderRegistry,250,2500),this._oldCodeLensModels=new M.DisposableStore,this._resolveCodeLensesDelays=new n.LanguageFeatureRequestDelays(C.CodeLensProviderRegistry,250,2500),this._resolveCodeLensesScheduler=new b.RunOnceScheduler(()=>this._resolveCodeLensesInViewport(),this._resolveCodeLensesDelays.min),this._disposables.add(this._editor.onDidChangeModel(()=>this._onModelChange())),this._disposables.add(this._editor.onDidChangeModelLanguage(()=>this._onModelChange())),this._disposables.add(this._editor.onDidChangeConfiguration(v=>{(v.hasChanged(38)||v.hasChanged(13)||v.hasChanged(12))&&this._updateLensStyle(),v.hasChanged(11)&&this._onModelChange()})),this._disposables.add(C.CodeLensProviderRegistry.onDidChange(this._onModelChange,this)),this._onModelChange(),this._styleClassName="_"+a.hash(this._editor.getId()).toString(16),this._styleElement=s.createStyleSheet(s.isInShadowDOM(this._editor.getContainerDomNode())?this._editor.getContainerDomNode():void 0),this._updateLensStyle()}dispose(){var h;this._localDispose(),this._disposables.dispose(),this._oldCodeLensModels.dispose(),(h=this._currentCodeLensModel)===null||h===void 0||h.dispose(),this._styleElement.remove()}_getLayoutInfo(){let h=this._editor.getOption(13),m;return!h||h<5?(h=this._editor.getOption(40)*.9|0,m=this._editor.getOption(53)):m=h*Math.max(1.3,this._editor.getOption(53)/this._editor.getOption(40))|0,{codeLensHeight:m,fontSize:h}}_updateLensStyle(){const{codeLensHeight:h,fontSize:m}=this._getLayoutInfo(),_=this._editor.getOption(12),f=this._editor.getOption(38),v=`--codelens-font-family${this._styleClassName}`;let y=` + .monaco-editor .codelens-decoration.${this._styleClassName} { line-height: ${h}px; font-size: ${m}px; padding-right: ${Math.round(m*.5)}px; font-feature-settings: ${f.fontFeatureSettings} } + .monaco-editor .codelens-decoration.${this._styleClassName} span.codicon { line-height: ${h}px; font-size: ${m}px; } + `;_&&(y+=`.monaco-editor .codelens-decoration.${this._styleClassName} { font-family: var(${v})}`),this._styleElement.textContent=y,this._editor.getContainerDomNode().style.setProperty(v,_!=null?_:"inherit"),this._editor.changeViewZones(L=>{for(let I of this._lenses)I.updateHeight(h,L)})}_localDispose(){var h,m,_;(h=this._getCodeLensModelPromise)===null||h===void 0||h.cancel(),this._getCodeLensModelPromise=void 0,(m=this._resolveCodeLensesPromise)===null||m===void 0||m.cancel(),this._resolveCodeLensesPromise=void 0,this._localToDispose.clear(),this._oldCodeLensModels.clear(),(_=this._currentCodeLensModel)===null||_===void 0||_.dispose()}_onModelChange(){this._localDispose();const h=this._editor.getModel();if(!!h&&!!this._editor.getOption(11)){const m=this._codeLensCache.get(h);if(m&&this._renderCodeLensSymbols(m),!C.CodeLensProviderRegistry.has(h)){m&&this._localToDispose.add(b.disposableTimeout(()=>{const f=this._codeLensCache.get(h);m===f&&(this._codeLensCache.delete(h),this._onModelChange())},30*1e3));return}for(const f of C.CodeLensProviderRegistry.all(h))if(typeof f.onDidChange=="function"){let v=f.onDidChange(()=>_.schedule());this._localToDispose.add(v)}const _=new b.RunOnceScheduler(()=>{var f;const v=Date.now();(f=this._getCodeLensModelPromise)===null||f===void 0||f.cancel(),this._getCodeLensModelPromise=b.createCancelablePromise(y=>d.getCodeLensModel(h,y)),this._getCodeLensModelPromise.then(y=>{this._currentCodeLensModel&&this._oldCodeLensModels.add(this._currentCodeLensModel),this._currentCodeLensModel=y,this._codeLensCache.put(h,y);const L=this._getCodeLensModelDelays.update(h,Date.now()-v);_.delay=L,this._renderCodeLensSymbols(y),this._resolveCodeLensesInViewport()},N.onUnexpectedError)},this._getCodeLensModelDelays.get(h));this._localToDispose.add(_),this._localToDispose.add(M.toDisposable(()=>this._resolveCodeLensesScheduler.cancel())),this._localToDispose.add(this._editor.onDidChangeModelContent(()=>{this._editor.changeDecorations(f=>{this._editor.changeViewZones(v=>{let y=[],L=-1;this._lenses.forEach(k=>{!k.isValid()||L===k.getLineNumber()?y.push(k):(k.update(v),L=k.getLineNumber())});let I=new g.CodeLensHelper;y.forEach(k=>{k.dispose(I,v),this._lenses.splice(this._lenses.indexOf(k),1)}),I.commit(f)})}),_.schedule()})),this._localToDispose.add(this._editor.onDidFocusEditorWidget(()=>{_.schedule()})),this._localToDispose.add(this._editor.onDidScrollChange(f=>{f.scrollTopChanged&&this._lenses.length>0&&this._resolveCodeLensesInViewportSoon()})),this._localToDispose.add(this._editor.onDidLayoutChange(()=>{this._resolveCodeLensesInViewportSoon()})),this._localToDispose.add(M.toDisposable(()=>{if(this._editor.getModel()){const f=w.StableEditorScrollState.capture(this._editor);this._editor.changeDecorations(v=>{this._editor.changeViewZones(y=>{this._disposeAllLenses(v,y)})}),f.restore(this._editor)}else this._disposeAllLenses(void 0,void 0)})),this._localToDispose.add(this._editor.onMouseDown(f=>{if(f.target.type===9){let v=f.target.element;if((v==null?void 0:v.tagName)==="SPAN"&&(v=v.parentElement),(v==null?void 0:v.tagName)==="A")for(const y of this._lenses){let L=y.getCommand(v);if(L){this._commandService.executeCommand(L.id,...L.arguments||[]).catch(I=>this._notificationService.error(I));break}}}})),_.schedule()}}_disposeAllLenses(h,m){const _=new g.CodeLensHelper;for(const f of this._lenses)f.dispose(_,m);h&&_.commit(h),this._lenses.length=0}_renderCodeLensSymbols(h){if(!!this._editor.hasModel()){let m=this._editor.getModel().getLineCount(),_=[],f;for(let L of h.lenses){let I=L.symbol.range.startLineNumber;I<1||I>m||(f&&f[f.length-1].symbol.range.startLineNumber===I?f.push(L):(f=[L],_.push(f)))}const v=w.StableEditorScrollState.capture(this._editor),y=this._getLayoutInfo();this._editor.changeDecorations(L=>{this._editor.changeViewZones(I=>{const k=new g.CodeLensHelper;let E=0,T=0;for(;T<_.length&&Ethis._resolveCodeLensesInViewportSoon())),E++,T++)}for(;Ethis._resolveCodeLensesInViewportSoon())),T++;k.commit(L)})}),v.restore(this._editor)}}_resolveCodeLensesInViewportSoon(){this._editor.getModel()&&this._resolveCodeLensesScheduler.schedule()}_resolveCodeLensesInViewport(){var h;(h=this._resolveCodeLensesPromise)===null||h===void 0||h.cancel(),this._resolveCodeLensesPromise=void 0;const m=this._editor.getModel();if(!!m){const _=[],f=[];if(this._lenses.forEach(L=>{const I=L.computeIfNecessary(m);I&&(_.push(I),f.push(L))}),_.length!==0){const v=Date.now(),y=b.createCancelablePromise(L=>{const I=_.map((k,E)=>{const T=new Array(k.length),O=k.map((A,B)=>!A.symbol.command&&typeof A.provider.resolveCodeLens=="function"?Promise.resolve(A.provider.resolveCodeLens(m,A.symbol,L)).then(F=>{T[B]=F},N.onUnexpectedExternalError):(T[B]=A.symbol,Promise.resolve(void 0)));return Promise.all(O).then(()=>{!L.isCancellationRequested&&!f[E].isDisposed()&&f[E].updateCommands(T)})});return Promise.all(I)});this._resolveCodeLensesPromise=y,this._resolveCodeLensesPromise.then(()=>{const L=this._resolveCodeLensesDelays.update(m,Date.now()-v);this._resolveCodeLensesScheduler.delay=L,this._currentCodeLensModel&&this._codeLensCache.put(m,this._currentCodeLensModel),this._oldCodeLensModels.clear(),y===this._resolveCodeLensesPromise&&(this._resolveCodeLensesPromise=void 0)},L=>{N.onUnexpectedError(L),y===this._resolveCodeLensesPromise&&(this._resolveCodeLensesPromise=void 0)})}}}getLenses(){return this._lenses}};t.ID="css.editor.codeLens",t=Me([_e(1,p.ICommandService),_e(2,c.INotificationService),_e(3,o.ICodeLensCache)],t),e.CodeLensContribution=t,S.registerEditorContribution(t.ID,t),S.registerEditorAction(class extends S.EditorAction{constructor(){super({id:"codelens.showLensesInCurrentLine",precondition:i.EditorContextKeys.hasCodeLensProvider,label:r.localize(0,null),alias:"Show CodeLens Commands For Current Line"})}run(h,m){return Ie(this,void 0,void 0,function*(){if(!!m.hasModel()){const _=h.get(u.IQuickInputService),f=h.get(p.ICommandService),v=h.get(c.INotificationService),y=m.getSelection().positionLineNumber,L=m.getContribution(t.ID),I=[];for(let E of L.getLenses())if(E.getLineNumber()===y)for(let T of E.getItems()){const{command:O}=T.symbol;O&&I.push({label:O.title,command:O})}if(I.length!==0){const k=yield _.pick(I,{canPickMany:!1});if(!!k)try{yield f.executeCommand(k.command.id,...k.command.arguments||[])}catch(E){v.error(E)}}}})}})}),define(Q[260],J([0,1,15,29,12,89,2,13,28,3,31,18,249,46]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ColorDetector=void 0;const a=500;let u=class Wt extends S.Disposable{constructor(i,n,t){super();this._editor=i,this._codeEditorService=n,this._configurationService=t,this._localToDispose=this._register(new S.DisposableStore),this._decorationsIds=[],this._colorDatas=new Map,this._colorDecoratorIds=[],this._decorationsTypes=new Set,this._register(i.onDidChangeModel(()=>{this._isEnabled=this.isEnabled(),this.onModelChanged()})),this._register(i.onDidChangeModelLanguage(()=>this.onModelChanged())),this._register(c.ColorProviderRegistry.onDidChange(()=>this.onModelChanged())),this._register(i.onDidChangeConfiguration(()=>{let l=this._isEnabled;this._isEnabled=this.isEnabled(),l!==this._isEnabled&&(this._isEnabled?this.onModelChanged():this.removeAllDecorations())})),this._timeoutTimer=null,this._computePromise=null,this._isEnabled=this.isEnabled(),this.onModelChanged()}isEnabled(){const i=this._editor.getModel();if(!i)return!1;const n=i.getLanguageIdentifier(),t=this._configurationService.getValue(n.language);if(t){const l=t.colorDecorators;if(l&&l.enable!==void 0&&!l.enable)return l.enable}return this._editor.getOption(14)}static get(i){return i.getContribution(this.ID)}dispose(){this.stop(),this.removeAllDecorations(),super.dispose()}onModelChanged(){if(this.stop(),!!this._isEnabled){const i=this._editor.getModel();!i||!c.ColorProviderRegistry.has(i)||(this._localToDispose.add(this._editor.onDidChangeModelContent(()=>{this._timeoutTimer||(this._timeoutTimer=new b.TimeoutTimer,this._timeoutTimer.cancelAndSet(()=>{this._timeoutTimer=null,this.beginCompute()},Wt.RECOMPUTE_TIME))})),this.beginCompute())}}beginCompute(){this._computePromise=b.createCancelablePromise(i=>{const n=this._editor.getModel();return n?o.getColors(n,i):Promise.resolve([])}),this._computePromise.then(i=>{this.updateDecorations(i),this.updateColorDecorators(i),this._computePromise=null},M.onUnexpectedError)}stop(){this._timeoutTimer&&(this._timeoutTimer.cancel(),this._timeoutTimer=null),this._computePromise&&(this._computePromise.cancel(),this._computePromise=null),this._localToDispose.clear()}updateDecorations(i){const n=i.map(t=>({range:{startLineNumber:t.colorInfo.range.startLineNumber,startColumn:t.colorInfo.range.startColumn,endLineNumber:t.colorInfo.range.endLineNumber,endColumn:t.colorInfo.range.endColumn},options:p.ModelDecorationOptions.EMPTY}));this._decorationsIds=this._editor.deltaDecorations(this._decorationsIds,n),this._colorDatas=new Map,this._decorationsIds.forEach((t,l)=>this._colorDatas.set(t,i[l]))}updateColorDecorators(i){let n=[],t={};for(let l=0;l{t[l]||this._codeEditorService.removeDecorationType(l)}),this._colorDecoratorIds=this._editor.deltaDecorations(this._colorDecoratorIds,n)}removeAllDecorations(){this._decorationsIds=this._editor.deltaDecorations(this._decorationsIds,[]),this._colorDecoratorIds=this._editor.deltaDecorations(this._colorDecoratorIds,[]),this._decorationsTypes.forEach(i=>{this._codeEditorService.removeDecorationType(i)})}getColorData(i){const n=this._editor.getModel();if(!n)return null;const t=n.getDecorationsInRange(g.Range.fromPositions(i,i)).filter(l=>this._colorDatas.has(l.id));return t.length===0?null:this._colorDatas.get(t[0].id)}};u.ID="editor.contrib.colorDetector",u.RECOMPUTE_TIME=1e3,u=Me([_e(1,d.ICodeEditorService),_e(2,s.IConfigurationService)],u),e.ColorDetector=u,C.registerEditorContribution(u.ID,u)}),define(Q[619],J([0,1,469,39,13,3,25,224,401,34]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0});class p extends M.EditorAction{constructor(r,i){super(i);this._type=r}run(r,i){if(!!i.hasModel()){const n=i.getModel(),t=[],l=n.getOptions(),h=i.getOption(16),m=i.getSelections().map((f,v)=>({selection:f,index:v,ignoreFirstLine:!1}));m.sort((f,v)=>w.Range.compareRangesUsingStarts(f.selection,v.selection));let _=m[0];for(let f=1;fthis._onContextMenu(m))),this._toDispose.add(this._editor.onMouseWheel(m=>{if(this._contextMenuIsBeingShownCount>0){const _=this._contextViewService.getContextViewElement(),f=m.srcElement;f.shadowRoot&&N.getShadowRoot(_)===f.shadowRoot||this._contextViewService.hideContextView()}})),this._toDispose.add(this._editor.onKeyDown(m=>{m.keyCode===58&&(m.preventDefault(),m.stopPropagation(),this.showContextMenu())}))}static get(r){return r.getContribution(Bt.ID)}_onContextMenu(r){if(!!this._editor.hasModel()){if(!this._editor.getOption(17)){this._editor.focus(),r.target.position&&!this._editor.getSelection().containsPosition(r.target.position)&&this._editor.setPosition(r.target.position);return}if(r.target.type!==12&&(r.event.preventDefault(),!(r.target.type!==6&&r.target.type!==7&&r.target.type!==1))){if(this._editor.focus(),r.target.position){let n=!1;for(const t of this._editor.getSelections())if(t.containsPosition(r.target.position)){n=!0;break}n||this._editor.setPosition(r.target.position)}let i=null;r.target.type!==1&&(i={x:r.event.posx-1,width:2,y:r.event.posy-1,height:2}),this.showContextMenu(i)}}}showContextMenu(r){if(!!this._editor.getOption(17)&&!!this._editor.hasModel()){if(!this._contextMenuService){this._editor.focus();return}const i=this._getMenuActions(this._editor.getModel(),d.MenuId.EditorContext);i.length>0&&this._doShowContextMenu(i,r)}}_getMenuActions(r,i){const n=[],t=this._menuService.createMenu(i,this._contextKeyService),l=t.getActions({arg:r.uri});t.dispose();for(let h of l){const[,m]=h;let _=0;for(const f of m)if(f instanceof d.SubmenuItemAction){const v=this._getMenuActions(r,f.item.submenu);v.length>0&&(n.push(new M.SubmenuAction(f.id,f.label,v)),_++)}else n.push(f),_++;_&&n.push(new M.Separator)}return n.length&&n.pop(),n}_doShowContextMenu(r,i=null){if(!!this._editor.hasModel()){const n=this._editor.getOption(48);if(this._editor.updateOptions({hover:{enabled:!1}}),!i){this._editor.revealPosition(this._editor.getPosition(),1),this._editor.render();const t=this._editor.getScrolledVisiblePosition(this._editor.getPosition()),l=N.getDomNodePagePosition(this._editor.getDomNode()),h=l.left+t.left,m=l.top+t.top+t.height;i={x:h,y:m}}this._contextMenuIsBeingShownCount++,this._contextMenuService.showContextMenu({domForShadowRoot:this._editor.getDomNode(),getAnchor:()=>i,getActions:()=>r,getActionViewItem:t=>{const l=this._keybindingFor(t);if(l)return new o.ActionViewItem(t,t,{label:!0,keybinding:l.getLabel(),isMenu:!0});const h=t;return typeof h.getActionViewItem=="function"?h.getActionViewItem():new o.ActionViewItem(t,t,{icon:!0,label:!0,isMenu:!0})},getKeyBinding:t=>this._keybindingFor(t),onHide:t=>{this._contextMenuIsBeingShownCount--,this._editor.focus(),this._editor.updateOptions({hover:n})}})}}_keybindingFor(r){return this._keybindingService.lookupKeybinding(r.id)}dispose(){this._contextMenuIsBeingShownCount>0&&this._contextViewService.hideContextView(),this._toDispose.dispose()}};s.ID="editor.contrib.contextmenu",s=Me([_e(1,p.IContextMenuService),_e(2,p.IContextViewService),_e(3,g.IContextKeyService),_e(4,c.IKeybindingService),_e(5,d.IMenuService)],s),e.ContextMenuController=s;class a extends S.EditorAction{constructor(){super({id:"editor.action.showContextMenu",label:b.localize(0,null),alias:"Show Editor Context Menu",precondition:void 0,kbOpts:{kbExpr:C.EditorContextKeys.textInputFocus,primary:1024|68,weight:100}})}run(r,i){s.get(i).showContextMenu()}}S.registerEditorContribution(s.ID,s),S.registerEditorAction(a)}),define(Q[621],J([0,1,471,2,13,25]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CursorRedo=e.CursorUndo=e.CursorUndoRedoController=void 0;class S{constructor(o){this.selections=o}equals(o){const s=this.selections.length,a=o.selections.length;if(s!==a)return!1;for(let u=0;u{this._undoStack=[],this._redoStack=[]})),this._register(o.onDidChangeModelContent(s=>{this._undoStack=[],this._redoStack=[]})),this._register(o.onDidChangeCursorSelection(s=>{if(!this._isCursorUndoRedo&&!!s.oldSelections&&s.oldModelVersionId===s.modelVersionId){const a=new S(s.oldSelections);this._undoStack.length>0&&this._undoStack[this._undoStack.length-1].cursorState.equals(a)||(this._undoStack.push(new C(a,o.getScrollTop(),o.getScrollLeft())),this._redoStack=[],this._undoStack.length>50&&this._undoStack.shift())}}))}static get(o){return o.getContribution(d.ID)}cursorUndo(){!this._editor.hasModel()||this._undoStack.length===0||(this._redoStack.push(new C(new S(this._editor.getSelections()),this._editor.getScrollTop(),this._editor.getScrollLeft())),this._applyState(this._undoStack.pop()))}cursorRedo(){!this._editor.hasModel()||this._redoStack.length===0||(this._undoStack.push(new C(new S(this._editor.getSelections()),this._editor.getScrollTop(),this._editor.getScrollLeft())),this._applyState(this._redoStack.pop()))}_applyState(o){this._isCursorUndoRedo=!0,this._editor.setSelections(o.cursorState.selections),this._editor.setScrollPosition({scrollTop:o.scrollTop,scrollLeft:o.scrollLeft}),this._isCursorUndoRedo=!1}}e.CursorUndoRedoController=d,d.ID="editor.contrib.cursorUndoRedoController";class g extends M.EditorAction{constructor(){super({id:"cursorUndo",label:b.localize(0,null),alias:"Cursor Undo",precondition:void 0,kbOpts:{kbExpr:w.EditorContextKeys.textInputFocus,primary:2048|51,weight:100}})}run(o,s,a){d.get(s).cursorUndo()}}e.CursorUndo=g;class p extends M.EditorAction{constructor(){super({id:"cursorRedo",label:b.localize(1,null),alias:"Cursor Redo",precondition:void 0})}run(o,s,a){d.get(s).cursorRedo()}}e.CursorRedo=p,M.registerEditorContribution(d.ID,d),M.registerEditorAction(g),M.registerEditorAction(p)}),define(Q[622],J([0,1,2,17,13,14,3,21,402,31,340]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DragAndDropController=void 0;function p(o){return N.isMacintosh?o.altKey:o.ctrlKey}class c extends b.Disposable{constructor(s){super();this._editor=s,this._register(this._editor.onMouseDown(a=>this._onEditorMouseDown(a))),this._register(this._editor.onMouseUp(a=>this._onEditorMouseUp(a))),this._register(this._editor.onMouseDrag(a=>this._onEditorMouseDrag(a))),this._register(this._editor.onMouseDrop(a=>this._onEditorMouseDrop(a))),this._register(this._editor.onMouseDropCanceled(()=>this._onEditorMouseDropCanceled())),this._register(this._editor.onKeyDown(a=>this.onEditorKeyDown(a))),this._register(this._editor.onKeyUp(a=>this.onEditorKeyUp(a))),this._register(this._editor.onDidBlurEditorWidget(()=>this.onEditorBlur())),this._register(this._editor.onDidBlurEditorText(()=>this.onEditorBlur())),this._dndDecorationIds=[],this._mouseDown=!1,this._modifierPressed=!1,this._dragSelection=null}onEditorBlur(){this._removeDecoration(),this._dragSelection=null,this._mouseDown=!1,this._modifierPressed=!1}onEditorKeyDown(s){!this._editor.getOption(27)||this._editor.getOption(15)||(p(s)&&(this._modifierPressed=!0),this._mouseDown&&p(s)&&this._editor.updateOptions({mouseStyle:"copy"}))}onEditorKeyUp(s){!this._editor.getOption(27)||this._editor.getOption(15)||(p(s)&&(this._modifierPressed=!1),this._mouseDown&&s.keyCode===c.TRIGGER_KEY_VALUE&&this._editor.updateOptions({mouseStyle:"default"}))}_onEditorMouseDown(s){this._mouseDown=!0}_onEditorMouseUp(s){this._mouseDown=!1,this._editor.updateOptions({mouseStyle:"text"})}_onEditorMouseDrag(s){let a=s.target;if(this._dragSelection===null){let r=(this._editor.getSelections()||[]).filter(i=>a.position&&i.containsPosition(a.position));if(r.length===1)this._dragSelection=r[0];else return}p(s.event)?this._editor.updateOptions({mouseStyle:"copy"}):this._editor.updateOptions({mouseStyle:"default"}),a.position&&(this._dragSelection.containsPosition(a.position)?this._removeDecoration():this.showAt(a.position))}_onEditorMouseDropCanceled(){this._editor.updateOptions({mouseStyle:"text"}),this._removeDecoration(),this._dragSelection=null,this._mouseDown=!1}_onEditorMouseDrop(s){if(s.target&&(this._hitContent(s.target)||this._hitMargin(s.target))&&s.target.position){let a=new w.Position(s.target.position.lineNumber,s.target.position.column);if(this._dragSelection===null){let u=null;if(s.event.shiftKey){let r=this._editor.getSelection();if(r){const{selectionStartLineNumber:i,selectionStartColumn:n}=r;u=[new C.Selection(i,n,a.lineNumber,a.column)]}}else u=(this._editor.getSelections()||[]).map(r=>r.containsPosition(a)?new C.Selection(a.lineNumber,a.column,a.lineNumber,a.column):r);this._editor.setSelections(u||[],"mouse",3)}else(!this._dragSelection.containsPosition(a)||(p(s.event)||this._modifierPressed)&&(this._dragSelection.getEndPosition().equals(a)||this._dragSelection.getStartPosition().equals(a)))&&(this._editor.pushUndoStop(),this._editor.executeCommand(c.ID,new d.DragAndDropCommand(this._dragSelection,a,p(s.event)||this._modifierPressed)),this._editor.pushUndoStop())}this._editor.updateOptions({mouseStyle:"text"}),this._removeDecoration(),this._dragSelection=null,this._mouseDown=!1}showAt(s){let a=[{range:new S.Range(s.lineNumber,s.column,s.lineNumber,s.column),options:c._DECORATION_OPTIONS}];this._dndDecorationIds=this._editor.deltaDecorations(this._dndDecorationIds,a),this._editor.revealPosition(s,1)}_removeDecoration(){this._dndDecorationIds=this._editor.deltaDecorations(this._dndDecorationIds,[])}_hitContent(s){return s.type===6||s.type===7}_hitMargin(s){return s.type===2||s.type===3||s.type===4}dispose(){this._removeDecoration(),this._dragSelection=null,this._mouseDown=!1,this._modifierPressed=!1,super.dispose()}}e.DragAndDropController=c,c.ID="editor.contrib.dragAndDrop",c.TRIGGER_KEY_VALUE=N.isMacintosh?6:5,c._DECORATION_OPTIONS=g.ModelDecorationOptions.register({className:"dnd-target"}),M.registerEditorContribution(c.ID,c)}),define(Q[623],J([0,1,476,13,126]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0});class w extends N.EditorAction{constructor(){super({id:"editor.action.fontZoomIn",label:b.localize(0,null),alias:"Editor Font Zoom In",precondition:void 0})}run(g,p){M.EditorZoom.setZoomLevel(M.EditorZoom.getZoomLevel()+1)}}class S extends N.EditorAction{constructor(){super({id:"editor.action.fontZoomOut",label:b.localize(1,null),alias:"Editor Font Zoom Out",precondition:void 0})}run(g,p){M.EditorZoom.setZoomLevel(M.EditorZoom.getZoomLevel()-1)}}class C extends N.EditorAction{constructor(){super({id:"editor.action.fontZoomReset",label:b.localize(2,null),alias:"Editor Font Zoom Reset",precondition:void 0})}run(g,p){M.EditorZoom.setZoomLevel(0)}}N.registerEditorAction(w),N.registerEditorAction(S),N.registerEditorAction(C)}),define(Q[261],J([0,1,47,19,23,12,24,70,108,14,3,21,18,75,36,226,477,532,9,71,26,20,54]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getOnTypeFormattingEdits=e.getDocumentFormattingEditsUntilResult=e.getDocumentRangeFormattingEditsUntilResult=e.formatDocumentWithProvider=e.formatDocumentWithSelectedProvider=e.formatDocumentRangesWithProvider=e.formatDocumentRangesWithSelectedProvider=e.FormattingConflicts=e.getRealAndSyntheticDocumentFormattersOrdered=e.alertFormattingEdits=void 0;function _(A){if(A=A.filter(R=>R.range),!!A.length){let{range:B}=A[0];for(let R=1;R0&&p.Range.areIntersectingOrTouching(Y[ee-1],ne)?Y[ee-1]=p.Range.fromPositions(Y[ee-1].getStartPosition(),ne.getEndPosition()):ee=Y.push(ne);const se=[];for(let ne of Y)try{const le=yield B.provideDocumentRangeFormattingEdits(x,ne,x.getFormattingOptions(),K.token),X=yield W.computeMoreMinimalEdits(x.uri,le);if(X&&se.push(...X),K.token.isCancellationRequested)return!0}finally{K.dispose()}if(se.length===0)return!1;if(d.isCodeEditor(F))u.FormattingEdit.execute(F,se,!0),_(se),F.revealPositionInCenterIfOutsideViewport(F.getPosition(),1);else{const[{range:ne}]=se,le=new c.Selection(ne.startLineNumber,ne.startColumn,ne.endLineNumber,ne.endColumn);x.pushEditOperations([le],se.map(X=>({text:X.text,range:p.Range.lift(X.range),forceMoveMarkers:!0})),X=>{for(const{range:z}of X)if(p.Range.areIntersectingOrTouching(z,le))return[new c.Selection(z.startLineNumber,z.startColumn,z.endLineNumber,z.endColumn)];return null})}return!0})}e.formatDocumentRangesWithProvider=L;function I(A,B,F,D,R){return Ie(this,void 0,void 0,function*(){const W=A.get(n.IInstantiationService),x=d.isCodeEditor(B)?B.getModel():B,K=f(x),Y=yield v.select(K,x,F);Y&&(D.report(Y),yield W.invokeFunction(k,Y,B,F,R))})}e.formatDocumentWithSelectedProvider=I;function k(A,B,F,D,R){return Ie(this,void 0,void 0,function*(){const W=A.get(s.IEditorWorkerService);let x,K;d.isCodeEditor(F)?(x=F.getModel(),K=new C.EditorStateCancellationTokenSource(F,1|4,void 0,R)):(x=F,K=new C.TextModelCancellationTokenSource(F,R));let Y;try{const ee=yield B.provideDocumentFormattingEdits(x,x.getFormattingOptions(),K.token);if(Y=yield W.computeMoreMinimalEdits(x.uri,ee),K.token.isCancellationRequested)return!0}finally{K.dispose()}if(!Y||Y.length===0)return!1;if(d.isCodeEditor(F))u.FormattingEdit.execute(F,Y,D!==2),D!==2&&(_(Y),F.revealPositionInCenterIfOutsideViewport(F.getPosition(),1));else{const[{range:ee}]=Y,se=new c.Selection(ee.startLineNumber,ee.startColumn,ee.endLineNumber,ee.endColumn);x.pushEditOperations([se],Y.map(ne=>({text:ne.text,range:p.Range.lift(ne.range),forceMoveMarkers:!0})),ne=>{for(const{range:le}of ne)if(p.Range.areIntersectingOrTouching(le,se))return[new c.Selection(le.startLineNumber,le.startColumn,le.endLineNumber,le.endColumn)];return null})}return!0})}e.formatDocumentWithProvider=k;function E(A,B,F,D,R){return Ie(this,void 0,void 0,function*(){const W=o.DocumentRangeFormattingEditProviderRegistry.ordered(B);for(const x of W){let K=yield Promise.resolve(x.provideDocumentRangeFormattingEdits(B,F,D,R)).catch(w.onUnexpectedExternalError);if(N.isNonEmptyArray(K))return yield A.computeMoreMinimalEdits(B.uri,K)}})}e.getDocumentRangeFormattingEditsUntilResult=E;function T(A,B,F,D){return Ie(this,void 0,void 0,function*(){const R=f(B);for(const W of R){let x=yield Promise.resolve(W.provideDocumentFormattingEdits(B,F,D)).catch(w.onUnexpectedExternalError);if(N.isNonEmptyArray(x))return yield A.computeMoreMinimalEdits(B.uri,x)}})}e.getDocumentFormattingEditsUntilResult=T;function O(A,B,F,D,R){const W=o.OnTypeFormattingEditProviderRegistry.ordered(B);return W.length===0||W[0].autoFormatTriggerCharacters.indexOf(D)<0?Promise.resolve(void 0):Promise.resolve(W[0].provideOnTypeFormattingEdits(B,F,D,R,M.CancellationToken.None)).catch(w.onUnexpectedExternalError).then(x=>A.computeMoreMinimalEdits(B.uri,x))}e.getOnTypeFormattingEdits=O,l.CommandsRegistry.registerCommand("_executeFormatRangeProvider",function(A,...B){const[F,D,R]=B;h.assertType(S.URI.isUri(F)),h.assertType(p.Range.isIRange(D));const W=A.get(a.IModelService).getModel(F);if(!W)throw w.illegalArgument("resource");return E(A.get(s.IEditorWorkerService),W,p.Range.lift(D),R,M.CancellationToken.None)}),l.CommandsRegistry.registerCommand("_executeFormatDocumentProvider",function(A,...B){const[F,D]=B;h.assertType(S.URI.isUri(F));const R=A.get(a.IModelService).getModel(F);if(!R)throw w.illegalArgument("resource");return T(A.get(s.IEditorWorkerService),R,D,M.CancellationToken.None)}),l.CommandsRegistry.registerCommand("_executeFormatOnTypeProvider",function(A,...B){const[F,D,R,W]=B;h.assertType(S.URI.isUri(F)),h.assertType(g.Position.isIPosition(D)),h.assertType(typeof R=="string");const x=A.get(a.IModelService).getModel(F);if(!x)throw w.illegalArgument("resource");return O(A.get(s.IEditorWorkerService),x,g.Position.lift(D),R,W)})}),define(Q[624],J([0,1,19,23,39,2,13,28,91,3,25,18,75,261,226,478,26,16,9,12,59]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l){"use strict";Object.defineProperty(e,"__esModule",{value:!0});let h=class{constructor(y,L){this._workerService=L,this._callOnDispose=new w.DisposableStore,this._callOnModel=new w.DisposableStore,this._editor=y,this._callOnDispose.add(y.onDidChangeConfiguration(()=>this._update())),this._callOnDispose.add(y.onDidChangeModel(()=>this._update())),this._callOnDispose.add(y.onDidChangeModelLanguage(()=>this._update())),this._callOnDispose.add(c.OnTypeFormattingEditProviderRegistry.onDidChange(this._update,this))}dispose(){this._callOnDispose.dispose(),this._callOnModel.dispose()}_update(){if(this._callOnModel.clear(),!!this._editor.getOption(43)&&!!this._editor.hasModel()){const y=this._editor.getModel(),[L]=c.OnTypeFormattingEditProviderRegistry.ordered(y);if(!(!L||!L.autoFormatTriggerCharacters)){let I=new d.CharacterSet;for(let k of L.autoFormatTriggerCharacters)I.add(k.charCodeAt(0));this._callOnModel.add(this._editor.onDidType(k=>{let E=k.charCodeAt(k.length-1);I.has(E)&&this._trigger(String.fromCharCode(E))}))}}}_trigger(y){if(!!this._editor.hasModel()&&!(this._editor.getSelections().length>1)){const L=this._editor.getModel(),I=this._editor.getPosition();let k=!1;const E=this._editor.onDidChangeModelContent(T=>{if(T.isFlush){k=!0,E.dispose();return}for(let O=0,A=T.changes.length;O{E.dispose(),!k&&b.isNonEmptyArray(T)&&(a.FormattingEdit.execute(this._editor,T,!0),s.alertFormattingEdits(T))},T=>{throw E.dispose(),T})}}};h.ID="editor.contrib.autoFormat",h=Me([_e(1,o.IEditorWorkerService)],h);let m=class{constructor(y,L){this.editor=y,this._instantiationService=L,this._callOnDispose=new w.DisposableStore,this._callOnModel=new w.DisposableStore,this._callOnDispose.add(y.onDidChangeConfiguration(()=>this._update())),this._callOnDispose.add(y.onDidChangeModel(()=>this._update())),this._callOnDispose.add(y.onDidChangeModelLanguage(()=>this._update())),this._callOnDispose.add(c.DocumentRangeFormattingEditProviderRegistry.onDidChange(this._update,this))}dispose(){this._callOnDispose.dispose(),this._callOnModel.dispose()}_update(){this._callOnModel.clear(),!!this.editor.getOption(42)&&(!this.editor.hasModel()||!c.DocumentRangeFormattingEditProviderRegistry.has(this.editor.getModel())||this._callOnModel.add(this.editor.onDidPaste(({range:y})=>this._trigger(y))))}_trigger(y){!this.editor.hasModel()||this.editor.getSelections().length>1||this._instantiationService.invokeFunction(s.formatDocumentRangesWithSelectedProvider,this.editor,y,2,l.Progress.None,N.CancellationToken.None).catch(t.onUnexpectedError)}};m.ID="editor.contrib.formatOnPaste",m=Me([_e(1,n.IInstantiationService)],m);class _ extends S.EditorAction{constructor(){super({id:"editor.action.formatDocument",label:u.localize(0,null),alias:"Format Document",precondition:i.ContextKeyExpr.and(p.EditorContextKeys.notInCompositeEditor,p.EditorContextKeys.writable,p.EditorContextKeys.hasDocumentFormattingProvider),kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:1024|512|36,linux:{primary:2048|1024|39},weight:100},contextMenuOpts:{group:"1_modification",order:1.3}})}run(y,L){return Ie(this,void 0,void 0,function*(){if(L.hasModel()){const I=y.get(n.IInstantiationService);yield y.get(l.IEditorProgressService).showWhile(I.invokeFunction(s.formatDocumentWithSelectedProvider,L,1,l.Progress.None,N.CancellationToken.None),250)}})}}class f extends S.EditorAction{constructor(){super({id:"editor.action.formatSelection",label:u.localize(1,null),alias:"Format Selection",precondition:i.ContextKeyExpr.and(p.EditorContextKeys.writable,p.EditorContextKeys.hasDocumentSelectionFormattingProvider),kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:M.KeyChord(2048|41,2048|36),weight:100},contextMenuOpts:{when:p.EditorContextKeys.hasNonEmptySelection,group:"1_modification",order:1.31}})}run(y,L){return Ie(this,void 0,void 0,function*(){if(!!L.hasModel()){const I=y.get(n.IInstantiationService),k=L.getModel(),E=L.getSelections().map(O=>O.isEmpty()?new g.Range(O.startLineNumber,1,O.startLineNumber,k.getLineMaxColumn(O.startLineNumber)):O);yield y.get(l.IEditorProgressService).showWhile(I.invokeFunction(s.formatDocumentRangesWithSelectedProvider,L,E,1,l.Progress.None,N.CancellationToken.None),250)}})}}S.registerEditorContribution(h.ID,h),S.registerEditorContribution(m.ID,m),S.registerEditorAction(_),S.registerEditorAction(f),r.CommandsRegistry.registerCommand("editor.action.format",v=>Ie(void 0,void 0,void 0,function*(){const y=v.get(C.ICodeEditorService).getFocusedCodeEditor();if(!(!y||!y.hasModel())){const L=v.get(r.ICommandService);y.getSelection().isEmpty()?yield L.executeCommand("editor.action.formatDocument"):yield L.executeCommand("editor.action.formatSelection")}}))}),define(Q[262],J([0,1,23,12,13,18]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getReferencesAtPosition=e.getTypeDefinitionsAtPosition=e.getImplementationsAtPosition=e.getDeclarationsAtPosition=e.getDefinitionsAtPosition=void 0;function S(o,s,a,u){const i=a.ordered(o).map(n=>Promise.resolve(u(n,o,s)).then(void 0,t=>{N.onUnexpectedExternalError(t)}));return Promise.all(i).then(n=>{const t=[];for(let l of n)Array.isArray(l)?t.push(...l):l&&t.push(l);return t})}function C(o,s,a){return S(o,s,w.DefinitionProviderRegistry,(u,r,i)=>u.provideDefinition(r,i,a))}e.getDefinitionsAtPosition=C;function d(o,s,a){return S(o,s,w.DeclarationProviderRegistry,(u,r,i)=>u.provideDeclaration(r,i,a))}e.getDeclarationsAtPosition=d;function g(o,s,a){return S(o,s,w.ImplementationProviderRegistry,(u,r,i)=>u.provideImplementation(r,i,a))}e.getImplementationsAtPosition=g;function p(o,s,a){return S(o,s,w.TypeDefinitionProviderRegistry,(u,r,i)=>u.provideTypeDefinition(r,i,a))}e.getTypeDefinitionsAtPosition=p;function c(o,s,a,u){return S(o,s,w.ReferenceProviderRegistry,(r,i,n)=>Ie(this,void 0,void 0,function*(){const t=yield r.provideReferences(i,n,{includeDeclaration:!0},u);if(!a||!t||t.length!==2)return t;const l=yield r.provideReferences(i,n,{includeDeclaration:!1},u);return l&&l.length===1?l:t}))}e.getReferencesAtPosition=c,M.registerModelAndPositionCommand("_executeDefinitionProvider",(o,s)=>C(o,s,b.CancellationToken.None)),M.registerModelAndPositionCommand("_executeDeclarationProvider",(o,s)=>d(o,s,b.CancellationToken.None)),M.registerModelAndPositionCommand("_executeImplementationProvider",(o,s)=>g(o,s,b.CancellationToken.None)),M.registerModelAndPositionCommand("_executeTypeDefinitionProvider",(o,s)=>p(o,s,b.CancellationToken.None)),M.registerModelAndPositionCommand("_executeReferenceProvider",(o,s)=>c(o,s,!1,b.CancellationToken.None))}),define(Q[625],J([0,1,16,9,74,86,13,28,3,2,6,487,37,32,44]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ISymbolNavigationService=e.ctxHasSymbols=void 0,e.ctxHasSymbols=new b.RawContextKey("hasSymbols",!1),e.ISymbolNavigationService=N.createDecorator("ISymbolNavigationService");let u=class{constructor(n,t,l,h){this._editorService=t,this._notificationService=l,this._keybindingService=h,this._currentModel=void 0,this._currentIdx=-1,this._ignoreEditorChange=!1,this._ctxHasSymbols=e.ctxHasSymbols.bindTo(n)}reset(){var n,t;this._ctxHasSymbols.reset(),(n=this._currentState)===null||n===void 0||n.dispose(),(t=this._currentMessage)===null||t===void 0||t.dispose(),this._currentModel=void 0,this._currentIdx=-1}put(n){const t=n.parent.parent;if(t.references.length<=1){this.reset();return}this._currentModel=t,this._currentIdx=t.references.indexOf(n),this._ctxHasSymbols.set(!0),this._showMessage();const l=new r(this._editorService),h=l.onDidChange(m=>{if(!this._ignoreEditorChange){const _=this._editorService.getActiveCodeEditor();if(!!_){const f=_.getModel(),v=_.getPosition();if(!(!f||!v)){let y=!1,L=!1;for(const I of t.references)if(a.isEqual(I.uri,f.uri))y=!0,L=L||d.Range.containsPosition(I.range,v);else if(y)break;(!y||!L)&&this.reset()}}}});this._currentState=g.combinedDisposable(l,h)}revealNext(n){if(!this._currentModel)return Promise.resolve();this._currentIdx+=1,this._currentIdx%=this._currentModel.references.length;const t=this._currentModel.references[this._currentIdx];return this._showMessage(),this._ignoreEditorChange=!0,this._editorService.openCodeEditor({resource:t.uri,options:{selection:d.Range.collapseToStart(t.range),selectionRevealType:3}},n).finally(()=>{this._ignoreEditorChange=!1})}_showMessage(){var n;(n=this._currentMessage)===null||n===void 0||n.dispose();const t=this._keybindingService.lookupKeybinding("editor.gotoNextSymbolFromResult"),l=t?c.localize(0,null,this._currentIdx+1,this._currentModel.references.length,t.getLabel()):c.localize(1,null,this._currentIdx+1,this._currentModel.references.length);this._currentMessage=this._notificationService.status(l)}};u=Me([_e(0,b.IContextKeyService),_e(1,C.ICodeEditorService),_e(2,s.INotificationService),_e(3,o.IKeybindingService)],u),M.registerSingleton(e.ISymbolNavigationService,u,!0),S.registerEditorCommand(new class extends S.EditorCommand{constructor(){super({id:"editor.gotoNextSymbolFromResult",precondition:e.ctxHasSymbols,kbOpts:{weight:100,primary:70}})}runEditorCommand(i,n){return i.get(e.ISymbolNavigationService).revealNext(n)}}),w.KeybindingsRegistry.registerCommandAndKeybindingRule({id:"editor.gotoNextSymbolFromResult.cancel",weight:100,when:e.ctxHasSymbols,primary:9,handler(i){i.get(e.ISymbolNavigationService).reset()}});let r=class{constructor(n){this._listener=new Map,this._disposables=new g.DisposableStore,this._onDidChange=new p.Emitter,this.onDidChange=this._onDidChange.event,this._disposables.add(n.onCodeEditorRemove(this._onDidRemoveEditor,this)),this._disposables.add(n.onCodeEditorAdd(this._onDidAddEditor,this)),n.listCodeEditors().forEach(this._onDidAddEditor,this)}dispose(){this._disposables.dispose(),this._onDidChange.dispose(),g.dispose(this._listener.values())}_onDidAddEditor(n){this._listener.set(n,g.combinedDisposable(n.onDidChangeCursorPosition(t=>this._onDidChange.fire({editor:n})),n.onDidChangeModelContent(t=>this._onDidChange.fire({editor:n}))))}_onDidRemoveEditor(n){var t;(t=this._listener.get(n))===null||t===void 0||t.dispose(),this._listener.delete(n)}};r=Me([_e(0,C.ICodeEditorService)],r)}),define(Q[626],J([0,1,19,23,12,13,18]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getHover=void 0;function C(g,p,c){const s=S.HoverProviderRegistry.ordered(g).map(a=>Promise.resolve(a.provideHover(g,p,c)).then(u=>u&&d(u)?u:void 0,u=>{M.onUnexpectedExternalError(u)}));return Promise.all(s).then(b.coalesce)}e.getHover=C,w.registerModelAndPositionCommand("_executeHoverProvider",(g,p)=>C(g,p,N.CancellationToken.None));function d(g){const p=typeof g.range!="undefined",c=typeof g.contents!="undefined"&&g.contents&&g.contents.length>0;return p&&c}}),define(Q[627],J([0,1,489,7,73,2,3,140,19,58,57,18,626,14]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MarkdownHoverParticipant=e.MarkdownHover=void 0;const a=N.$;class u{constructor(n,t){this.range=n,this.contents=t}equals(n){return n instanceof u?M.markedStringsEquals(this.contents,n.contents):!1}}e.MarkdownHover=u;let r=class{constructor(n,t,l,h){this._editor=n,this._hover=t,this._modeService=l,this._openerService=h}createLoadingMessage(n){return new u(n,[new M.MarkdownString().appendText(b.localize(0,null))])}computeSync(n,t){if(!this._editor.hasModel())return[];const l=this._editor.getModel(),h=n.startLineNumber,m=l.getLineMaxColumn(h),_=[];for(const f of t){const v=f.range.startLineNumber===h?f.range.startColumn:1,y=f.range.endLineNumber===h?f.range.endColumn:m,L=f.options.hoverMessage;if(!(!L||M.isEmptyMarkdownString(L))){const I=new S.Range(n.startLineNumber,v,n.startLineNumber,y);_.push(new u(I,d.asArray(L)))}}return _}computeAsync(n,t){return Ie(this,void 0,void 0,function*(){if(!this._editor.hasModel()||!n)return Promise.resolve([]);const l=this._editor.getModel();if(!c.HoverProviderRegistry.has(l))return Promise.resolve([]);const h=yield o.getHover(l,new s.Position(n.startLineNumber,n.startColumn),t),m=[];for(const _ of h)if(!M.isEmptyMarkdownString(_.contents)){const f=_.range?S.Range.lift(_.range):n;m.push(new u(f,_.contents))}return m})}renderHoverParts(n,t){const l=new w.DisposableStore;for(const h of n)for(const m of h.contents)if(!M.isEmptyMarkdownString(m)){const _=a("div.hover-row.markdown-hover"),f=N.append(_,a("div.hover-contents")),v=l.add(new C.MarkdownRenderer({editor:this._editor},this._modeService,this._openerService));l.add(v.onDidRenderAsync(()=>{f.className="hover-contents code-hover-contents",this._hover.onContentsChanged()}));const y=l.add(v.render(m));f.appendChild(y.element),t.appendChild(_)}return l}};r=Me([_e(2,p.IModeService),_e(3,g.IOpenerService)],r),e.MarkdownHoverParticipant=r}),define(Q[628],J([0,1,491,3,21,25,13,75,409,70,11,49,31,15,12]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0});let u=class Ct{constructor(t,l){this.decorationIds=[],this.editor=t,this.editorWorkerService=l}static get(t){return t.getContribution(Ct.ID)}dispose(){}run(t,l){this.currentRequest&&this.currentRequest.cancel();const h=this.editor.getSelection(),m=this.editor.getModel();if(!(!m||!h)){let _=h;if(_.startLineNumber===_.endLineNumber){const f=new g.EditorState(this.editor,1|4),v=m.uri;return this.editorWorkerService.canNavigateValueSet(v)?(this.currentRequest=s.createCancelablePromise(y=>this.editorWorkerService.navigateValueSet(v,_,l)),this.currentRequest.then(y=>{if(!(!y||!y.range||!y.value)&&!!f.validate(this.editor)){let L=N.Range.lift(y.range),I=y.range,k=y.value.length-(_.endColumn-_.startColumn);I={startLineNumber:I.startLineNumber,startColumn:I.startColumn,endLineNumber:I.endLineNumber,endColumn:I.startColumn+y.value.length},k>1&&(_=new M.Selection(_.startLineNumber,_.startColumn,_.endLineNumber,_.endColumn+k-1));const E=new d.InPlaceReplaceCommand(L,_,y.value);this.editor.pushUndoStop(),this.editor.executeCommand(t,E),this.editor.pushUndoStop(),this.decorationIds=this.editor.deltaDecorations(this.decorationIds,[{range:I,options:Ct.DECORATION}]),this.decorationRemover&&this.decorationRemover.cancel(),this.decorationRemover=s.timeout(350),this.decorationRemover.then(()=>this.decorationIds=this.editor.deltaDecorations(this.decorationIds,[])).catch(a.onUnexpectedError)}}).catch(a.onUnexpectedError)):Promise.resolve(void 0)}}}};u.ID="editor.contrib.inPlaceReplaceController",u.DECORATION=o.ModelDecorationOptions.register({className:"valueSetReplacement"}),u=Me([_e(1,C.IEditorWorkerService)],u);class r extends S.EditorAction{constructor(){super({id:"editor.action.inPlaceReplace.up",label:b.localize(0,null),alias:"Replace with Previous Value",precondition:w.EditorContextKeys.writable,kbOpts:{kbExpr:w.EditorContextKeys.editorTextFocus,primary:2048|1024|82,weight:100}})}run(t,l){const h=u.get(l);return h?h.run(this.id,!0):Promise.resolve(void 0)}}class i extends S.EditorAction{constructor(){super({id:"editor.action.inPlaceReplace.down",label:b.localize(1,null),alias:"Replace with Next Value",precondition:w.EditorContextKeys.writable,kbOpts:{kbExpr:w.EditorContextKeys.editorTextFocus,primary:2048|1024|84,weight:100}})}run(t,l){const h=u.get(l);return h?h.run(this.id,!1):Promise.resolve(void 0)}}S.registerEditorContribution(u.ID,u),S.registerEditorAction(r),S.registerEditorAction(i),p.registerThemingParticipant((n,t)=>{const l=n.getColor(c.editorBracketMatchBorder);l&&t.addRule(`.monaco-editor.vs .valueSetReplacement { outline: solid 2px ${l}; }`)})}),define(Q[629],J([0,1,492,2,8,13,179,62,3,21,25,31,41,36,229,78]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IndentationToTabsCommand=e.IndentationToSpacesCommand=e.AutoIndentOnPaste=e.AutoIndentOnPasteCommand=e.ReindentSelectedLinesAction=e.ReindentLinesAction=e.DetectIndentation=e.IndentUsingSpaces=e.IndentUsingTabs=e.ChangeIndentationSizeAction=e.IndentationToTabsAction=e.IndentationToSpacesAction=e.getReindentEditOperations=void 0;function r(E,T,O,A){if(E.getLineCount()===1&&E.getLineMaxColumn(1)===1)return[];let B=o.LanguageConfigurationRegistry.getIndentationRules(E.getLanguageIdentifier().id);if(!B)return[];for(O=Math.min(O,E.getLineCount());T<=O&&B.unIndentedLinePattern;){let le=E.getLineContent(T);if(!B.unIndentedLinePattern.test(le))break;T++}if(T>O-1)return[];const{tabSize:F,indentSize:D,insertSpaces:R}=E.getOptions(),W=(le,X)=>(X=X||1,S.ShiftCommand.shiftIndent(le,le.length+X,F,D,R)),x=(le,X)=>(X=X||1,S.ShiftCommand.unshiftIndent(le,le.length+X,F,D,R));let K=[],Y,ee=E.getLineContent(T),se=ee;if(A!=null){Y=A;let le=M.getLeadingWhitespace(ee);se=Y+ee.substring(le.length),B.decreaseIndentPattern&&B.decreaseIndentPattern.test(se)&&(Y=x(Y),se=Y+ee.substring(le.length)),ee!==se&&K.push(C.EditOperation.replaceMove(new g.Selection(T,1,T,le.length+1),c.TextModel.normalizeIndentation(Y,D,R)))}else Y=M.getLeadingWhitespace(ee);let ne=Y;B.increaseIndentPattern&&B.increaseIndentPattern.test(se)?(ne=W(ne),Y=W(Y)):B.indentNextLinePattern&&B.indentNextLinePattern.test(se)&&(ne=W(ne)),T++;for(let le=T;le<=O;le++){let X=E.getLineContent(le),z=M.getLeadingWhitespace(X),P=ne+X.substring(z.length);B.decreaseIndentPattern&&B.decreaseIndentPattern.test(P)&&(ne=x(ne),Y=x(Y)),z!==ne&&K.push(C.EditOperation.replaceMove(new g.Selection(le,1,le,z.length+1),c.TextModel.normalizeIndentation(ne,D,R))),!(B.unIndentedLinePattern&&B.unIndentedLinePattern.test(X))&&(B.increaseIndentPattern&&B.increaseIndentPattern.test(P)?(Y=W(Y),ne=Y):B.indentNextLinePattern&&B.indentNextLinePattern.test(P)?ne=W(ne):ne=Y)}return K}e.getReindentEditOperations=r;class i extends w.EditorAction{constructor(){super({id:i.ID,label:b.localize(0,null),alias:"Convert Indentation to Spaces",precondition:p.EditorContextKeys.writable})}run(T,O){let A=O.getModel();if(!!A){let B=A.getOptions(),F=O.getSelection();if(!!F){const D=new I(F,B.tabSize);O.pushUndoStop(),O.executeCommands(this.id,[D]),O.pushUndoStop(),A.updateOptions({insertSpaces:!0})}}}}e.IndentationToSpacesAction=i,i.ID="editor.action.indentationToSpaces";class n extends w.EditorAction{constructor(){super({id:n.ID,label:b.localize(1,null),alias:"Convert Indentation to Tabs",precondition:p.EditorContextKeys.writable})}run(T,O){let A=O.getModel();if(!!A){let B=A.getOptions(),F=O.getSelection();if(!!F){const D=new k(F,B.tabSize);O.pushUndoStop(),O.executeCommands(this.id,[D]),O.pushUndoStop(),A.updateOptions({insertSpaces:!1})}}}}e.IndentationToTabsAction=n,n.ID="editor.action.indentationToTabs";class t extends w.EditorAction{constructor(T,O){super(O);this.insertSpaces=T}run(T,O){const A=T.get(u.IQuickInputService),B=T.get(s.IModelService);let F=O.getModel();if(!!F){let D=B.getCreationOptions(F.getLanguageIdentifier().language,F.uri,F.isForSimpleWidget);const R=[1,2,3,4,5,6,7,8].map(x=>({id:x.toString(),label:x.toString(),description:x===D.tabSize?b.localize(2,null):void 0})),W=Math.min(F.getOptions().tabSize-1,7);setTimeout(()=>{A.pick(R,{placeHolder:b.localize(3,null),activeItem:R[W]}).then(x=>{x&&F&&!F.isDisposed()&&F.updateOptions({tabSize:parseInt(x.label,10),insertSpaces:this.insertSpaces})})},50)}}}e.ChangeIndentationSizeAction=t;class l extends t{constructor(){super(!1,{id:l.ID,label:b.localize(4,null),alias:"Indent Using Tabs",precondition:void 0})}}e.IndentUsingTabs=l,l.ID="editor.action.indentUsingTabs";class h extends t{constructor(){super(!0,{id:h.ID,label:b.localize(5,null),alias:"Indent Using Spaces",precondition:void 0})}}e.IndentUsingSpaces=h,h.ID="editor.action.indentUsingSpaces";class m extends w.EditorAction{constructor(){super({id:m.ID,label:b.localize(6,null),alias:"Detect Indentation from Content",precondition:void 0})}run(T,O){const A=T.get(s.IModelService);let B=O.getModel();if(!!B){let F=A.getCreationOptions(B.getLanguageIdentifier().language,B.uri,B.isForSimpleWidget);B.detectIndentation(F.insertSpaces,F.tabSize)}}}e.DetectIndentation=m,m.ID="editor.action.detectIndentation";class _ extends w.EditorAction{constructor(){super({id:"editor.action.reindentlines",label:b.localize(7,null),alias:"Reindent Lines",precondition:p.EditorContextKeys.writable})}run(T,O){let A=O.getModel();if(!!A){let B=r(A,1,A.getLineCount());B.length>0&&(O.pushUndoStop(),O.executeEdits(this.id,B),O.pushUndoStop())}}}e.ReindentLinesAction=_;class f extends w.EditorAction{constructor(){super({id:"editor.action.reindentselectedlines",label:b.localize(8,null),alias:"Reindent Selected Lines",precondition:p.EditorContextKeys.writable})}run(T,O){let A=O.getModel();if(!!A){let B=O.getSelections();if(B!==null){let F=[];for(let D of B){let R=D.startLineNumber,W=D.endLineNumber;if(R!==W&&D.endColumn===1&&W--,R===1){if(R===W)continue}else R--;let x=r(A,R,W);F.push(...x)}F.length>0&&(O.pushUndoStop(),O.executeEdits(this.id,F),O.pushUndoStop())}}}}e.ReindentSelectedLinesAction=f;class v{constructor(T,O){this._initialSelection=O,this._edits=[],this._selectionId=null;for(let A of T)A.range&&typeof A.text=="string"&&this._edits.push(A)}getEditOperations(T,O){for(let B of this._edits)O.addEditOperation(d.Range.lift(B.range),B.text);let A=!1;Array.isArray(this._edits)&&this._edits.length===1&&this._initialSelection.isEmpty()&&(this._edits[0].range.startColumn===this._initialSelection.endColumn&&this._edits[0].range.startLineNumber===this._initialSelection.endLineNumber?(A=!0,this._selectionId=O.trackSelection(this._initialSelection,!0)):this._edits[0].range.endColumn===this._initialSelection.startColumn&&this._edits[0].range.endLineNumber===this._initialSelection.startLineNumber&&(A=!0,this._selectionId=O.trackSelection(this._initialSelection,!1))),A||(this._selectionId=O.trackSelection(this._initialSelection))}computeCursorState(T,O){return O.getTrackedSelection(this._selectionId)}}e.AutoIndentOnPasteCommand=v;class y{constructor(T){this.callOnDispose=new N.DisposableStore,this.callOnModel=new N.DisposableStore,this.editor=T,this.callOnDispose.add(T.onDidChangeConfiguration(()=>this.update())),this.callOnDispose.add(T.onDidChangeModel(()=>this.update())),this.callOnDispose.add(T.onDidChangeModelLanguage(()=>this.update()))}update(){this.callOnModel.clear(),!(this.editor.getOption(8)<4||this.editor.getOption(42))&&(!this.editor.hasModel()||this.callOnModel.add(this.editor.onDidPaste(({range:T})=>{this.trigger(T)})))}trigger(T){let O=this.editor.getSelections();if(!(O===null||O.length>1)){const A=this.editor.getModel();if(!!A&&!!A.isCheapToTokenize(T.getStartPosition().lineNumber)){const B=this.editor.getOption(8),{tabSize:F,indentSize:D,insertSpaces:R}=A.getOptions();let W=[],x={shiftIndent:se=>S.ShiftCommand.shiftIndent(se,se.length+1,F,D,R),unshiftIndent:se=>S.ShiftCommand.unshiftIndent(se,se.length+1,F,D,R)},K=T.startLineNumber;for(;K<=T.endLineNumber;){if(this.shouldIgnoreLine(A,K)){K++;continue}break}if(!(K>T.endLineNumber)){let Y=A.getLineContent(K);if(!/\S/.test(Y.substring(0,T.startColumn-1))){let se=o.LanguageConfigurationRegistry.getGoodIndentForLine(B,A,A.getLanguageIdentifier().id,K,x);if(se!==null){let ne=M.getLeadingWhitespace(Y),le=a.getSpaceCnt(se,F),X=a.getSpaceCnt(ne,F);if(le!==X){let z=a.generateIndent(le,F,R);W.push({range:new d.Range(K,1,K,ne.length+1),text:z}),Y=z+Y.substr(ne.length)}else{let z=o.LanguageConfigurationRegistry.getIndentMetadata(A,K);if(z===0||z===8)return}}}const ee=K;for(;KA.getLineTokens(le),getLanguageIdentifier:()=>A.getLanguageIdentifier(),getLanguageIdAtPosition:(le,X)=>A.getLanguageIdAtPosition(le,X),getLineContent:le=>le===ee?Y:A.getLineContent(le)},ne=o.LanguageConfigurationRegistry.getGoodIndentForLine(B,se,A.getLanguageIdentifier().id,K+1,x);if(ne!==null){let le=a.getSpaceCnt(ne,F),X=a.getSpaceCnt(M.getLeadingWhitespace(A.getLineContent(K+1)),F);if(le!==X){let z=le-X;for(let P=K+1;P<=T.endLineNumber;P++){let V=A.getLineContent(P),U=M.getLeadingWhitespace(V),$=a.getSpaceCnt(U,F)+z,ie=a.generateIndent($,F,R);ie!==U&&W.push({range:new d.Range(P,1,P,U.length+1),text:ie})}}}}if(W.length>0){this.editor.pushUndoStop();let se=new v(W,this.editor.getSelection());this.editor.executeCommand("autoIndentOnPaste",se),this.editor.pushUndoStop()}}}}}shouldIgnoreLine(T,O){T.forceTokenization(O);let A=T.getLineFirstNonWhitespaceColumn(O);if(A===0)return!0;let B=T.getLineTokens(O);if(B.getCount()>0){let F=B.findTokenIndexAtOffset(A);if(F>=0&&B.getStandardTokenType(F)===1)return!0}return!1}dispose(){this.callOnDispose.dispose(),this.callOnModel.dispose()}}e.AutoIndentOnPaste=y,y.ID="editor.contrib.autoIndentOnPaste";function L(E,T,O,A){if(!(E.getLineCount()===1&&E.getLineMaxColumn(1)===1)){let B="";for(let D=0;Df.map(E=>Promise.resolve(k.provideInlineHints(_,E,v)).then(T=>{T&&y.push({list:T,provider:k})},T=>{N.onUnexpectedExternalError(T)}))));return yield Promise.all(I),y})}e.getInlineHints=h;let m=class{constructor(f,v,y){this._editor=f,this._codeEditorService=v,this._themeService=y,this._disposables=new w.DisposableStore,this._sessionDisposables=new w.DisposableStore,this._getInlineHintsDelays=new a.LanguageFeatureRequestDelays(d.InlineHintsProviderRegistry,250,2500),this._decorationsTypeIds=[],this._decorationIds=[],this._disposables.add(d.InlineHintsProviderRegistry.onDidChange(()=>this._update())),this._disposables.add(y.onDidColorThemeChange(()=>this._update())),this._disposables.add(f.onDidChangeModel(()=>this._update())),this._disposables.add(f.onDidChangeModelLanguage(()=>this._update())),this._disposables.add(f.onDidChangeConfiguration(L=>{L.hasChanged(120)&&this._update()})),this._update()}dispose(){this._sessionDisposables.dispose(),this._removeAllDecorations(),this._disposables.dispose()}_update(){if(this._sessionDisposables.clear(),!this._editor.getOption(120).enabled){this._removeAllDecorations();return}const f=this._editor.getModel();if(!f||!d.InlineHintsProviderRegistry.has(f)){this._removeAllDecorations();return}const v=new b.RunOnceScheduler(()=>Ie(this,void 0,void 0,function*(){const L=Date.now(),I=new c.CancellationTokenSource;this._sessionDisposables.add(w.toDisposable(()=>I.dispose(!0)));const k=this._editor.getVisibleRangesPlusViewportAboveBelow(),E=yield h(f,k,I.token),T=this._getInlineHintsDelays.update(f,Date.now()-L);v.delay=T,this._updateHintsDecorators(E)}),this._getInlineHintsDelays.get(f));this._sessionDisposables.add(v),this._sessionDisposables.add(this._editor.onDidChangeModelContent(()=>v.schedule())),this._disposables.add(this._editor.onDidScrollChange(()=>v.schedule())),v.schedule();const y=new w.DisposableStore;this._sessionDisposables.add(y);for(const L of d.InlineHintsProviderRegistry.all(f))typeof L.onDidChangeInlineHints=="function"&&y.add(L.onDidChangeInlineHints(()=>v.schedule()))}_updateHintsDecorators(f){const{fontSize:v,fontFamily:y}=this._getLayoutInfo(),L=this._themeService.getColorTheme().getColor(p.editorInlineHintBackground),I=this._themeService.getColorTheme().getColor(p.editorInlineHintForeground),k=[],E=[];for(const{list:T}of f)for(let O=0;Ov)&&(y=v*.9|0);const L=f.fontFamily;return{fontSize:y,fontFamily:L}}_removeAllDecorations(){this._decorationIds=this._editor.deltaDecorations(this._decorationIds,[]),this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType,this._codeEditorService),this._decorationsTypeIds=[]}};m.ID="editor.contrib.InlineHints",m=Me([_e(1,C.ICodeEditorService),_e(2,o.IThemeService)],m),e.InlineHintsController=m,S.registerEditorContribution(m.ID,m),r.CommandsRegistry.registerCommand("_executeInlineHintProvider",(_,...f)=>Ie(void 0,void 0,void 0,function*(){const[v,y]=f;n.assertType(i.URI.isUri(v)),n.assertType(s.Range.isIRange(y));const L=yield _.get(t.ITextModelService).createModelReference(v);try{const I=yield h(L.object.textEditorModel,[s.Range.lift(y)],c.CancellationToken.None);return g.flatten(I.map(k=>k.list)).sort((k,E)=>s.Range.compareRangesUsingStarts(k.range,E.range))}finally{L.dispose()}}))}),define(Q[631],J([0,1,493,39,188,13,92,366,182,62,14,3,21,25,410,542,411,34]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SnakeCaseAction=e.TitleCaseAction=e.LowerCaseAction=e.UpperCaseAction=e.AbstractCaseAction=e.TransposeAction=e.JoinLinesAction=e.DeleteAllRightAction=e.DeleteAllLeftAction=e.AbstractDeleteAllToBoundaryAction=e.InsertLineAfterAction=e.InsertLineBeforeAction=e.IndentLinesAction=e.DeleteLinesAction=e.TrimTrailingWhitespaceAction=e.SortLinesDescendingAction=e.SortLinesAscendingAction=e.AbstractSortLinesAction=e.DuplicateSelectionAction=void 0;class n extends w.EditorAction{constructor(le,X){super(X);this.down=le}run(le,X){if(!!X.hasModel()){const z=X.getSelections().map((U,H)=>({selection:U,index:H,ignore:!1}));z.sort((U,H)=>c.Range.compareRangesUsingStarts(U.selection,H.selection));let P=z[0];for(let U=1;Unew p.Position(H.positionLineNumber,H.positionColumn)));let V=X.getSelection();if(V!==null){let U=new C.TrimTrailingWhitespaceCommand(V,P);X.pushUndoStop(),X.executeCommands(this.id,[U]),X.pushUndoStop()}}}e.TrimTrailingWhitespaceAction=I,I.ID="editor.action.trimTrailingWhitespace";class k extends w.EditorAction{constructor(){super({id:"editor.action.deleteLines",label:b.localize(13,null),alias:"Delete Line",precondition:s.EditorContextKeys.writable,kbOpts:{kbExpr:s.EditorContextKeys.textInputFocus,primary:2048|1024|41,weight:100}})}run(le,X){if(!!X.hasModel()){let z=this._getLinesToRemove(X),P=X.getModel();if(!(P.getLineCount()===1&&P.getLineMaxColumn(1)===1)){let V=0,U=[],H=[];for(let $=0,ie=z.length;$1&&(ae-=1,j=P.getLineMaxColumn(ae)),U.push(g.EditOperation.replace(new o.Selection(ae,j,G,te),"")),H.push(new o.Selection(ae-V,oe.positionColumn,ae-V,oe.positionColumn)),V+=oe.endLineNumber-oe.startLineNumber+1}X.pushUndoStop(),X.executeEdits(this.id,U,H),X.pushUndoStop()}}}_getLinesToRemove(le){let X=le.getSelections().map(V=>{let U=V.endLineNumber;return V.startLineNumberV.startLineNumber===U.startLineNumber?V.endLineNumber-U.endLineNumber:V.startLineNumber-U.startLineNumber);let z=[],P=X[0];for(let V=1;V=X[V].startLineNumber?P.endLineNumber=X[V].endLineNumber:(z.push(P),P=X[V]);return z.push(P),z}}e.DeleteLinesAction=k;class E extends w.EditorAction{constructor(){super({id:"editor.action.indentLines",label:b.localize(14,null),alias:"Indent Line",precondition:s.EditorContextKeys.writable,kbOpts:{kbExpr:s.EditorContextKeys.editorTextFocus,primary:2048|89,weight:100}})}run(le,X){const z=X._getViewModel();!z||(X.pushUndoStop(),X.executeCommands(this.id,d.TypeOperations.indent(z.cursorConfig,X.getModel(),X.getSelections())),X.pushUndoStop())}}e.IndentLinesAction=E;class T extends w.EditorAction{constructor(){super({id:"editor.action.outdentLines",label:b.localize(15,null),alias:"Outdent Line",precondition:s.EditorContextKeys.writable,kbOpts:{kbExpr:s.EditorContextKeys.editorTextFocus,primary:2048|87,weight:100}})}run(le,X){M.CoreEditingCommands.Outdent.runEditorCommand(le,X,null)}}class O extends w.EditorAction{constructor(){super({id:"editor.action.insertLineBefore",label:b.localize(16,null),alias:"Insert Line Above",precondition:s.EditorContextKeys.writable,kbOpts:{kbExpr:s.EditorContextKeys.editorTextFocus,primary:2048|1024|3,weight:100}})}run(le,X){const z=X._getViewModel();!z||(X.pushUndoStop(),X.executeCommands(this.id,d.TypeOperations.lineInsertBefore(z.cursorConfig,X.getModel(),X.getSelections())))}}e.InsertLineBeforeAction=O;class A extends w.EditorAction{constructor(){super({id:"editor.action.insertLineAfter",label:b.localize(17,null),alias:"Insert Line Below",precondition:s.EditorContextKeys.writable,kbOpts:{kbExpr:s.EditorContextKeys.editorTextFocus,primary:2048|3,weight:100}})}run(le,X){const z=X._getViewModel();!z||(X.pushUndoStop(),X.executeCommands(this.id,d.TypeOperations.lineInsertAfter(z.cursorConfig,X.getModel(),X.getSelections())))}}e.InsertLineAfterAction=A;class B extends w.EditorAction{run(le,X){if(!!X.hasModel()){const z=X.getSelection();let P=this._getRangesToDelete(X),V=[];for(let $=0,ie=P.length-1;$g.EditOperation.replace($,""));X.pushUndoStop(),X.executeEdits(this.id,H,U),X.pushUndoStop()}}}e.AbstractDeleteAllToBoundaryAction=B;class F extends B{constructor(){super({id:"deleteAllLeft",label:b.localize(18,null),alias:"Delete All Left",precondition:s.EditorContextKeys.writable,kbOpts:{kbExpr:s.EditorContextKeys.textInputFocus,primary:0,mac:{primary:2048|1},weight:100}})}_getEndCursorState(le,X){let z=null,P=[],V=0;return X.forEach(U=>{let H;if(U.endColumn===1&&V>0){let $=U.startLineNumber-V;H=new o.Selection($,U.startColumn,$,U.startColumn)}else H=new o.Selection(U.startLineNumber,U.startColumn,U.startLineNumber,U.startColumn);V+=U.endLineNumber-U.startLineNumber,U.intersectRanges(le)?z=H:P.push(H)}),z&&P.unshift(z),P}_getRangesToDelete(le){let X=le.getSelections();if(X===null)return[];let z=X,P=le.getModel();return P===null?[]:(z.sort(c.Range.compareRangesUsingStarts),z=z.map(V=>{if(V.isEmpty())if(V.startColumn===1){let U=Math.max(1,V.startLineNumber-1),H=V.startLineNumber===1?1:P.getLineContent(U).length+1;return new c.Range(U,H,V.startLineNumber,1)}else return new c.Range(V.startLineNumber,1,V.startLineNumber,V.startColumn);else return new c.Range(V.startLineNumber,1,V.endLineNumber,V.endColumn)}),z)}}e.DeleteAllLeftAction=F;class D extends B{constructor(){super({id:"deleteAllRight",label:b.localize(19,null),alias:"Delete All Right",precondition:s.EditorContextKeys.writable,kbOpts:{kbExpr:s.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|41,secondary:[2048|20]},weight:100}})}_getEndCursorState(le,X){let z=null,P=[];for(let V=0,U=X.length,H=0;V{if(V.isEmpty()){const U=X.getLineMaxColumn(V.startLineNumber);return V.startColumn===U?new c.Range(V.startLineNumber,V.startColumn,V.startLineNumber+1,1):new c.Range(V.startLineNumber,V.startColumn,V.startLineNumber,U)}return V});return P.sort(c.Range.compareRangesUsingStarts),P}}e.DeleteAllRightAction=D;class R extends w.EditorAction{constructor(){super({id:"editor.action.joinLines",label:b.localize(20,null),alias:"Join Lines",precondition:s.EditorContextKeys.writable,kbOpts:{kbExpr:s.EditorContextKeys.editorTextFocus,primary:0,mac:{primary:256|40},weight:100}})}run(le,X){let z=X.getSelections();if(z!==null){let P=X.getSelection();if(P!==null){z.sort(c.Range.compareRangesUsingStarts);let V=[],U=z.reduce((G,j)=>G.isEmpty()?G.endLineNumber===j.startLineNumber?(P.equalsSelection(G)&&(P=j),j):j.startLineNumber>G.endLineNumber+1?(V.push(G),j):new o.Selection(G.startLineNumber,G.startColumn,j.endLineNumber,j.endColumn):j.startLineNumber>G.endLineNumber?(V.push(G),j):new o.Selection(G.startLineNumber,G.startColumn,j.endLineNumber,j.endColumn));V.push(U);let H=X.getModel();if(H!==null){let $=[],ie=[],oe=P,ae=0;for(let G=0,j=V.length;G=1){let Ee=!0;Ce===""&&(Ee=!1),Ee&&(Ce.charAt(Ce.length-1)===" "||Ce.charAt(Ce.length-1)===" ")&&(Ee=!1,Ce=Ce.replace(/[\s\uFEFF\xA0]+$/g," "));let Ae=De.substr(Re-1);Ce+=(Ee?" ":"")+Ae,Ee?he=Ae.length+1:he=Ae.length}else he=0}let be=new c.Range(Z,ue,re,ce);if(!be.isEmpty()){let Le;te.isEmpty()?($.push(g.EditOperation.replace(be,Ce)),Le=new o.Selection(be.startLineNumber-ae,Ce.length-he+1,Z-ae,Ce.length-he+1)):te.startLineNumber===te.endLineNumber?($.push(g.EditOperation.replace(be,Ce)),Le=new o.Selection(te.startLineNumber-ae,te.startColumn,te.endLineNumber-ae,te.endColumn)):($.push(g.EditOperation.replace(be,Ce)),Le=new o.Selection(te.startLineNumber-ae,te.startColumn,te.startLineNumber-ae,Ce.length-me)),c.Range.intersectRanges(be,P)!==null?oe=Le:ie.push(Le)}ae+=be.endLineNumber-be.startLineNumber}ie.unshift(oe),X.pushUndoStop(),X.executeEdits(this.id,$,ie),X.pushUndoStop()}}}}}e.JoinLinesAction=R;class W extends w.EditorAction{constructor(){super({id:"editor.action.transpose",label:b.localize(21,null),alias:"Transpose characters around the cursor",precondition:s.EditorContextKeys.writable})}run(le,X){let z=X.getSelections();if(z!==null){let P=X.getModel();if(P!==null){let V=[];for(let U=0,H=z.length;U=oe){if(ie.lineNumber===P.getLineCount())continue;let ae=new c.Range(ie.lineNumber,Math.max(1,ie.column-1),ie.lineNumber+1,1),G=P.getValueInRange(ae).split("").reverse().join("");V.push(new S.ReplaceCommand(new o.Selection(ie.lineNumber,Math.max(1,ie.column-1),ie.lineNumber+1,1),G))}else{let ae=new c.Range(ie.lineNumber,Math.max(1,ie.column-1),ie.lineNumber,ie.column+1),G=P.getValueInRange(ae).split("").reverse().join("");V.push(new S.ReplaceCommandThatPreservesSelection(ae,G,new o.Selection(ie.lineNumber,ie.column+1,ie.lineNumber,ie.column+1)))}}}X.pushUndoStop(),X.executeCommands(this.id,V),X.pushUndoStop()}}}}e.TransposeAction=W;class x extends w.EditorAction{run(le,X){const z=X.getSelections();if(z!==null){const P=X.getModel();if(P!==null){const V=X.getOption(110),U=[];for(const H of z)if(H.isEmpty()){const $=H.getStartPosition(),ie=X.getConfiguredWordAtPosition($);if(!ie)continue;const oe=new c.Range($.lineNumber,ie.startColumn,$.lineNumber,ie.endColumn),ae=P.getValueInRange(oe);U.push(g.EditOperation.replace(oe,this._modifyText(ae,V)))}else{const $=P.getValueInRange(H);U.push(g.EditOperation.replace(H,this._modifyText($,V)))}X.pushUndoStop(),X.executeEdits(this.id,U),X.pushUndoStop()}}}}e.AbstractCaseAction=x;class K extends x{constructor(){super({id:"editor.action.transformToUppercase",label:b.localize(22,null),alias:"Transform to Uppercase",precondition:s.EditorContextKeys.writable})}_modifyText(le,X){return le.toLocaleUpperCase()}}e.UpperCaseAction=K;class Y extends x{constructor(){super({id:"editor.action.transformToLowercase",label:b.localize(23,null),alias:"Transform to Lowercase",precondition:s.EditorContextKeys.writable})}_modifyText(le,X){return le.toLocaleLowerCase()}}e.LowerCaseAction=Y;class ee extends x{constructor(){super({id:"editor.action.transformToTitlecase",label:b.localize(24,null),alias:"Transform to Title Case",precondition:s.EditorContextKeys.writable})}_modifyText(le,X){const P=(`\r + `+X).split("");let V="",U=!0;for(let H=0;H=0?(U=!0,V+=$):U?(U=!1,V+=$.toLocaleUpperCase()):V+=$.toLocaleLowerCase()}return V}}e.TitleCaseAction=ee;class se extends x{constructor(){super({id:"editor.action.transformToSnakecase",label:b.localize(25,null),alias:"Transform to Snake Case",precondition:s.EditorContextKeys.writable})}_modifyText(le,X){return le.replace(/(\p{Ll})(\p{Lu})/gmu,"$1_$2").replace(/([^\b_])(\p{Lu})(\p{Ll})/gmu,"$1_$2$3").toLocaleLowerCase()}}e.SnakeCaseAction=se,w.registerEditorAction(t),w.registerEditorAction(l),w.registerEditorAction(h),w.registerEditorAction(_),w.registerEditorAction(f),w.registerEditorAction(y),w.registerEditorAction(L),w.registerEditorAction(I),w.registerEditorAction(k),w.registerEditorAction(E),w.registerEditorAction(T),w.registerEditorAction(O),w.registerEditorAction(A),w.registerEditorAction(F),w.registerEditorAction(D),w.registerEditorAction(R),w.registerEditorAction(W),w.registerEditorAction(K),w.registerEditorAction(Y),w.registerEditorAction(ee),w.registerEditorAction(se)}),define(Q[632],J([0,1,494,13,19,2,14,23,3,18,15,31,16,25,24,28,12,8,22,11,29,41]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.editorLinkedEditingBackground=e.LinkedEditingAction=e.LinkedEditingContribution=e.CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE=void 0,e.CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE=new o.RawContextKey("LinkedEditingInputVisible",!1);const m="linked-editing-decoration";let _=class vt extends w.Disposable{constructor(I,k){super();this._debounceDuration=200,this._localToDispose=this._register(new w.DisposableStore),this._editor=I,this._enabled=!1,this._visibleContextKey=e.CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE.bindTo(k),this._currentDecorations=[],this._languageWordPattern=null,this._currentWordPattern=null,this._ignoreChangeEvent=!1,this._localToDispose=this._register(new w.DisposableStore),this._rangeUpdateTriggerPromise=null,this._rangeSyncTriggerPromise=null,this._currentRequest=null,this._currentRequestPosition=null,this._currentRequestModelVersion=null,this._register(this._editor.onDidChangeModel(()=>this.reinitialize())),this._register(this._editor.onDidChangeConfiguration(E=>{(E.hasChanged(56)||E.hasChanged(76))&&this.reinitialize()})),this._register(g.LinkedEditingRangeProviderRegistry.onDidChange(()=>this.reinitialize())),this._register(this._editor.onDidChangeModelLanguage(()=>this.reinitialize())),this.reinitialize()}static get(I){return I.getContribution(vt.ID)}reinitialize(){const I=this._editor.getModel(),k=I!==null&&(this._editor.getOption(56)||this._editor.getOption(76))&&g.LinkedEditingRangeProviderRegistry.has(I);if(k!==this._enabled&&(this._enabled=k,this.clearRanges(),this._localToDispose.clear(),!(!k||I===null))){this._languageWordPattern=h.LanguageConfigurationRegistry.getWordDefinition(I.getLanguageIdentifier().id),this._localToDispose.add(I.onDidChangeLanguageConfiguration(()=>{this._languageWordPattern=h.LanguageConfigurationRegistry.getWordDefinition(I.getLanguageIdentifier().id)}));const E=new p.Delayer(this._debounceDuration),T=()=>{this._rangeUpdateTriggerPromise=E.trigger(()=>this.updateRanges(),this._debounceDuration)},O=new p.Delayer(0),A=B=>{this._rangeSyncTriggerPromise=O.trigger(()=>this._syncRanges(B))};this._localToDispose.add(this._editor.onDidChangeCursorPosition(()=>{T()})),this._localToDispose.add(this._editor.onDidChangeModelContent(B=>{if(!this._ignoreChangeEvent&&this._currentDecorations.length>0){const F=I.getDecorationRange(this._currentDecorations[0]);if(F&&B.changes.every(D=>F.intersectRanges(D.range))){A(this._currentDecorations);return}}T()})),this._localToDispose.add({dispose:()=>{E.cancel(),O.cancel()}}),this.updateRanges()}}_syncRanges(I){if(!(!this._editor.hasModel()||I!==this._currentDecorations||I.length===0)){const k=this._editor.getModel(),E=k.getDecorationRange(I[0]);if(!E||E.startLineNumber!==E.endLineNumber)return this.clearRanges();const T=k.getValueInRange(E);if(this._currentWordPattern){const A=T.match(this._currentWordPattern);if((A?A[0].length:0)!==T.length)return this.clearRanges()}let O=[];for(let A=1,B=I.length;A1){this.clearRanges();return}const E=this._editor.getModel(),T=E.getVersionId();if(this._currentRequestPosition&&this._currentRequestModelVersion===T){if(k.equals(this._currentRequestPosition))return;if(this._currentDecorations&&this._currentDecorations.length>0){const A=E.getDecorationRange(this._currentDecorations[0]);if(A&&A.containsPosition(k))return}}this._currentRequestPosition=k,this._currentRequestModelVersion=T;const O=p.createCancelablePromise(A=>Ie(this,void 0,void 0,function*(){try{const B=yield y(E,k,A);if(O!==this._currentRequest||(this._currentRequest=null,T!==E.getVersionId()))return;let F=[];(B==null?void 0:B.ranges)&&(F=B.ranges),this._currentWordPattern=(B==null?void 0:B.wordPattern)||this._languageWordPattern;let D=!1;for(let W=0,x=F.length;W({range:W,options:vt.DECORATION}));this._visibleContextKey.set(!0),this._currentDecorations=this._editor.deltaDecorations(this._currentDecorations,R)}catch(B){r.isPromiseCanceledError(B)||r.onUnexpectedError(B),(this._currentRequest===O||!this._currentRequest)&&this.clearRanges()}}));return this._currentRequest=O,O})}};_.ID="editor.contrib.linkedEditing",_.DECORATION=c.ModelDecorationOptions.register({stickiness:0,className:m}),_=Me([_e(1,o.IContextKeyService)],_),e.LinkedEditingContribution=_;class f extends N.EditorAction{constructor(){super({id:"editor.action.linkedEditing",label:b.localize(0,null),alias:"Start Linked Editing",precondition:o.ContextKeyExpr.and(s.EditorContextKeys.writable,s.EditorContextKeys.hasRenameProvider),kbOpts:{kbExpr:s.EditorContextKeys.editorTextFocus,primary:2048|1024|60,weight:100}})}runCommand(I,k){const E=I.get(u.ICodeEditorService),[T,O]=Array.isArray(k)&&k||[void 0,void 0];return a.URI.isUri(T)&&S.Position.isIPosition(O)?E.openCodeEditor({resource:T},E.getActiveCodeEditor()).then(A=>{!A||(A.setPosition(O),A.invokeWithinContext(B=>(this.reportTelemetry(B,A),this.run(B,A))))},r.onUnexpectedError):super.runCommand(I,k)}run(I,k){const E=_.get(k);return E?Promise.resolve(E.updateRanges(!0)):Promise.resolve()}}e.LinkedEditingAction=f;const v=N.EditorCommand.bindToContribution(_.get);N.registerEditorCommand(new v({id:"cancelLinkedEditingInput",precondition:e.CONTEXT_ONTYPE_RENAME_INPUT_VISIBLE,handler:L=>L.clearRanges(),kbOpts:{kbExpr:s.EditorContextKeys.editorTextFocus,weight:100+99,primary:9,secondary:[1024|9]}}));function y(L,I,k){const E=g.LinkedEditingRangeProviderRegistry.ordered(L);return p.first(E.map(T=>()=>Ie(this,void 0,void 0,function*(){try{return yield T.provideLinkedEditingRanges(L,I,k)}catch(O){r.onUnexpectedExternalError(O);return}})),T=>!!T&&M.isNonEmptyArray(T==null?void 0:T.ranges))}e.editorLinkedEditingBackground=n.registerColor("editor.linkedEditingBackground",{dark:l.Color.fromHex("#f00").transparent(.3),light:l.Color.fromHex("#f00").transparent(.3),hc:l.Color.fromHex("#f00").transparent(.3)},b.localize(1,null)),t.registerThemingParticipant((L,I)=>{const k=L.getColor(e.editorLinkedEditingBackground);k&&I.addRule(`.monaco-editor .${m} { background: ${k}; border-left-color: ${k}; }`)}),N.registerModelAndPositionCommand("_executeLinkedEditingProvider",(L,I)=>y(L,I,C.CancellationToken.None)),N.registerEditorContribution(_.ID,_),N.registerEditorAction(f)}),define(Q[633],J([0,1,495,15,23,12,73,2,17,13,31,18,227,546,32,58,22,11,24,43,44,346]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.LinkDetector=void 0;function h(y,L){const I=y.url&&/^command:/i.test(y.url.toString()),k=y.tooltip?y.tooltip:I?b.localize(0,null):b.localize(1,null),E=L?d.isMacintosh?b.localize(2,null):b.localize(3,null):d.isMacintosh?b.localize(4,null):b.localize(5,null);if(y.url){let T="";if(/^command:/i.test(y.url.toString())){const A=y.url.toString().match(/^command:([^?#]+)/);if(A){const B=A[1];T=` "${b.localize(6,null,B)}"`}}return new S.MarkdownString("",!0).appendMarkdown(`[${k}](${y.url.toString(!0)}${T}) (${E})`)}else return new S.MarkdownString().appendText(`${k} (${E})`)}const m={general:p.ModelDecorationOptions.register({stickiness:1,collapseOnReplaceEdit:!0,inlineClassName:"detected-link"}),active:p.ModelDecorationOptions.register({stickiness:1,collapseOnReplaceEdit:!0,inlineClassName:"detected-link-active"})};class _{constructor(L,I){this.link=L,this.decorationId=I}static decoration(L,I){return{range:L.range,options:_._getOptions(L,I,!1)}}static _getOptions(L,I,k){const E=Object.assign({},k?m.active:m.general);return E.hoverMessage=h(L,I),E}activate(L,I){L.changeDecorationOptions(this.decorationId,_._getOptions(this.link,I,!0))}deactivate(L,I){L.changeDecorationOptions(this.decorationId,_._getOptions(this.link,I,!1))}}let f=class bt{constructor(L,I,k){this.listenersToRemove=new C.DisposableStore,this.editor=L,this.openerService=I,this.notificationService=k;let E=new o.ClickLinkGesture(L);this.listenersToRemove.add(E),this.listenersToRemove.add(E.onMouseMoveOrRelevantKeyDown(([T,O])=>{this._onEditorMouseMove(T,O)})),this.listenersToRemove.add(E.onExecute(T=>{this.onEditorMouseUp(T)})),this.listenersToRemove.add(E.onCancel(T=>{this.cleanUpActiveLinkDecoration()})),this.enabled=L.getOption(57),this.listenersToRemove.add(L.onDidChangeConfiguration(T=>{const O=L.getOption(57);this.enabled!==O&&(this.enabled=O,this.updateDecorations([]),this.stop(),this.beginCompute())})),this.listenersToRemove.add(L.onDidChangeModelContent(T=>this.onChange())),this.listenersToRemove.add(L.onDidChangeModel(T=>this.onModelChanged())),this.listenersToRemove.add(L.onDidChangeModelLanguage(T=>this.onModelModeChanged())),this.listenersToRemove.add(c.LinkProviderRegistry.onDidChange(T=>this.onModelModeChanged())),this.timeout=new N.TimeoutTimer,this.computePromise=null,this.activeLinksList=null,this.currentOccurrences={},this.activeLinkDecorationId=null,this.beginCompute()}static get(L){return L.getContribution(bt.ID)}onModelChanged(){this.currentOccurrences={},this.activeLinkDecorationId=null,this.stop(),this.beginCompute()}onModelModeChanged(){this.stop(),this.beginCompute()}onChange(){this.timeout.setIfNotSet(()=>this.beginCompute(),bt.RECOMPUTE_TIME)}beginCompute(){return Ie(this,void 0,void 0,function*(){if(!(!this.editor.hasModel()||!this.enabled)){const L=this.editor.getModel();if(!!c.LinkProviderRegistry.has(L)){this.activeLinksList&&(this.activeLinksList.dispose(),this.activeLinksList=null),this.computePromise=N.createCancelablePromise(I=>s.getLinks(L,I));try{this.activeLinksList=yield this.computePromise,this.updateDecorations(this.activeLinksList.links)}catch(I){w.onUnexpectedError(I)}finally{this.computePromise=null}}}})}updateDecorations(L){const I=this.editor.getOption(64)==="altKey";let k=[],E=Object.keys(this.currentOccurrences);for(let A=0,B=E.length;A{E.activate(T,k),this.activeLinkDecorationId=E.decorationId})}else this.cleanUpActiveLinkDecoration()}cleanUpActiveLinkDecoration(){const L=this.editor.getOption(64)==="altKey";if(this.activeLinkDecorationId){const I=this.currentOccurrences[this.activeLinkDecorationId];I&&this.editor.changeDecorations(k=>{I.deactivate(k,L)}),this.activeLinkDecorationId=null}}onEditorMouseUp(L){if(!!this.isEnabled(L)){const I=this.getLinkOccurrence(L.target.position);!I||this.openLinkOccurrence(I,L.hasSideBySideModifier,!0)}}openLinkOccurrence(L,I,k=!1){if(!!this.openerService){const{link:E}=L;E.resolve(M.CancellationToken.None).then(T=>{if(typeof T=="string"&&this.editor.hasModel()){const O=this.editor.getModel().uri;if(O.scheme===t.Schemas.file&&T.startsWith(`${t.Schemas.file}:`)){const A=n.URI.parse(T);if(A.scheme===t.Schemas.file){const B=l.originalFSPath(A);let F=null;B.startsWith("/./")?F=`.${B.substr(1)}`:B.startsWith("//./")&&(F=`.${B.substr(2)}`),F&&(T=l.joinPath(O,F))}}}return this.openerService.open(T,{openToSide:I,fromUserGesture:k,allowContributedOpeners:!0})},T=>{const O=T instanceof Error?T.message:T;O==="invalid"?this.notificationService.warn(b.localize(7,null,E.url.toString())):O==="missing"?this.notificationService.warn(b.localize(8,null)):w.onUnexpectedError(T)})}}getLinkOccurrence(L){if(!this.editor.hasModel()||!L)return null;const I=this.editor.getModel().getDecorationsInRange({startLineNumber:L.lineNumber,startColumn:L.column,endLineNumber:L.lineNumber,endColumn:L.column},0,!0);for(const k of I){const E=this.currentOccurrences[k.id];if(E)return E}return null}isEnabled(L,I){return Boolean(L.target.type===6&&(L.hasTriggerModifier||I&&I.keyCodeIsTriggerKey))}stop(){var L;this.timeout.cancel(),this.activeLinksList&&((L=this.activeLinksList)===null||L===void 0||L.dispose(),this.activeLinksList=null),this.computePromise&&(this.computePromise.cancel(),this.computePromise=null)}dispose(){this.listenersToRemove.dispose(),this.stop(),this.timeout.dispose()}};f.ID="editor.linkDetector",f.RECOMPUTE_TIME=1e3,f=Me([_e(1,u.IOpenerService),_e(2,a.INotificationService)],f),e.LinkDetector=f;class v extends g.EditorAction{constructor(){super({id:"editor.action.openLink",label:b.localize(9,null),alias:"Open Link",precondition:void 0})}run(L,I){let k=f.get(I);if(!!k&&!!I.hasModel()){let E=I.getSelections();for(let T of E){let O=k.getLinkOccurrence(T.getEndPosition());O&&k.openLinkOccurrence(O,!1)}}}}g.registerEditorContribution(f.ID,f),g.registerEditorAction(v),i.registerThemingParticipant((y,L)=>{const I=y.getColor(r.editorActiveLinkForeground);I&&L.addRule(`.monaco-editor .detected-link-active { color: ${I} !important; }`)})}),define(Q[146],J([0,1,496,15,2,47,3,13,16,11,22,97,347]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MessageController=void 0;let o=class St{constructor(r,i){this._messageWidget=new M.MutableDisposable,this._messageListeners=new M.DisposableStore,this._editor=r,this._visible=St.MESSAGE_VISIBLE.bindTo(i),this._editorListener=this._editor.onDidAttemptReadOnlyEdit(()=>this._onDidAttemptReadOnlyEdit())}static get(r){return r.getContribution(St.ID)}dispose(){this._editorListener.dispose(),this._messageListeners.dispose(),this._messageWidget.dispose(),this._visible.reset()}showMessage(r,i){w.alert(r),this._visible.set(!0),this._messageWidget.clear(),this._messageListeners.clear(),this._messageWidget.value=new a(this._editor,i,r),this._messageListeners.add(this._editor.onDidBlurEditorText(()=>this.closeMessage())),this._messageListeners.add(this._editor.onDidChangeCursorPosition(()=>this.closeMessage())),this._messageListeners.add(this._editor.onDidDispose(()=>this.closeMessage())),this._messageListeners.add(this._editor.onDidChangeModel(()=>this.closeMessage())),this._messageListeners.add(new N.TimeoutTimer(()=>this.closeMessage(),3e3));let n;this._messageListeners.add(this._editor.onMouseMove(t=>{!t.target.position||(n?n.containsPosition(t.target.position)||this.closeMessage():n=new S.Range(i.lineNumber-3,1,t.target.position.lineNumber+3,1))}))}closeMessage(){this._visible.reset(),this._messageListeners.clear(),this._messageWidget.value&&this._messageListeners.add(a.fadeOut(this._messageWidget.value))}_onDidAttemptReadOnlyEdit(){this._editor.hasModel()&&this.showMessage(b.localize(1,null),this._editor.getPosition())}};o.ID="editor.contrib.messageController",o.MESSAGE_VISIBLE=new d.RawContextKey("messageVisible",!1,b.localize(0,null)),o=Me([_e(1,d.IContextKeyService)],o),e.MessageController=o;const s=C.EditorCommand.bindToContribution(o.get);C.registerEditorCommand(new s({id:"leaveEditorMessage",precondition:o.MESSAGE_VISIBLE,handler:u=>u.closeMessage(),kbOpts:{weight:100+30,primary:9}}));class a{constructor(r,{lineNumber:i,column:n},t){this.allowEditorOverflow=!0,this.suppressMouseDown=!1,this._editor=r,this._editor.revealLinesInCenterIfOutsideViewport(i,i,0),this._position={lineNumber:i,column:n-1},this._domNode=document.createElement("div"),this._domNode.classList.add("monaco-editor-overlaymessage");const l=document.createElement("div");l.classList.add("anchor","top"),this._domNode.appendChild(l);const h=document.createElement("div");h.classList.add("message"),h.textContent=t,this._domNode.appendChild(h);const m=document.createElement("div");m.classList.add("anchor","below"),this._domNode.appendChild(m),this._editor.addContentWidget(this),this._domNode.classList.add("fadeIn")}static fadeOut(r){let i;const n=()=>{r.dispose(),clearTimeout(i),r.getDomNode().removeEventListener("animationend",n)};return i=setTimeout(n,110),r.getDomNode().addEventListener("animationend",n),r.getDomNode().classList.add("fadeOut"),{dispose:n}}dispose(){this._editor.removeContentWidget(this)}getId(){return"messageoverlay"}getDomNode(){return this._domNode}getPosition(){return{position:this._position,preference:[1,2]}}afterRender(r){this._domNode.classList.toggle("below",r===2)}}C.registerEditorContribution(o.ID,o),g.registerThemingParticipant((u,r)=>{const i=u.getColor(p.inputValidationInfoBorder);if(i){let l=u.type===c.ColorScheme.HIGH_CONTRAST?2:1;r.addRule(`.monaco-editor .monaco-editor-overlaymessage .anchor.below { border-top-color: ${i}; }`),r.addRule(`.monaco-editor .monaco-editor-overlaymessage .anchor.top { border-bottom-color: ${i}; }`),r.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { border: ${l}px solid ${i}; }`)}const n=u.getColor(p.inputValidationInfoBackground);n&&r.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { background-color: ${n}; }`);const t=u.getColor(p.inputValidationInfoForeground);t&&r.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { color: ${t}; }`)})});var _t=this&&this.__classPrivateFieldSet||function(q,e,b){if(!e.has(q))throw new TypeError("attempted to set private field on non-instance");return e.set(q,b),b},tt=this&&this.__classPrivateFieldGet||function(q,e){if(!e.has(q))throw new TypeError("attempted to get private field on non-instance");return e.get(q)};define(Q[634],J([0,1,12,150,2,146,9,616,595]),function(q,e,b,N,M,w,S,C,d){"use strict";var g;Object.defineProperty(e,"__esModule",{value:!0}),e.CodeActionUi=void 0;let p=class extends M.Disposable{constructor(o,s,a,u,r){super();this._editor=o,this.delegate=u,this._activeCodeActions=this._register(new M.MutableDisposable),g.set(this,!1),this._codeActionWidget=new N.Lazy(()=>this._register(r.createInstance(C.CodeActionMenu,this._editor,{onSelectCodeAction:i=>Ie(this,void 0,void 0,function*(){this.delegate.applyCodeAction(i,!0)})}))),this._lightBulbWidget=new N.Lazy(()=>{const i=this._register(r.createInstance(d.LightBulbWidget,this._editor,s,a));return this._register(i.onClick(n=>this.showCodeActionList(n.trigger,n.actions,n,{includeDisabledActions:!1}))),i})}dispose(){_t(this,g,!0),super.dispose()}update(o){var s,a,u;return Ie(this,void 0,void 0,function*(){if(o.type!==1){(s=this._lightBulbWidget.rawValue)===null||s===void 0||s.hide();return}let r;try{r=yield o.actions}catch(i){b.onUnexpectedError(i);return}if(!tt(this,g))if(this._lightBulbWidget.getValue().update(r,o.trigger,o.position),o.trigger.type===2){if((a=o.trigger.filter)===null||a===void 0?void 0:a.include){const n=this.tryGetValidActionToApply(o.trigger,r);if(n){try{yield this.delegate.applyCodeAction(n,!1)}finally{r.dispose()}return}if(o.trigger.context){const t=this.getInvalidActionThatWouldHaveBeenApplied(o.trigger,r);if(t&&t.action.disabled){w.MessageController.get(this._editor).showMessage(t.action.disabled,o.trigger.context.position),r.dispose();return}}}const i=!!((u=o.trigger.filter)===null||u===void 0?void 0:u.include);if(o.trigger.context&&(!r.allActions.length||!i&&!r.validActions.length)){w.MessageController.get(this._editor).showMessage(o.trigger.context.notAvailableMessage,o.trigger.context.position),this._activeCodeActions.value=r,r.dispose();return}this._activeCodeActions.value=r,this._codeActionWidget.getValue().show(o.trigger,r,o.position,{includeDisabledActions:i})}else this._codeActionWidget.getValue().isVisible?r.dispose():this._activeCodeActions.value=r})}getInvalidActionThatWouldHaveBeenApplied(o,s){if(!!s.allActions.length&&(o.autoApply==="first"&&s.validActions.length===0||o.autoApply==="ifSingle"&&s.allActions.length===1))return s.allActions.find(({action:a})=>a.disabled)}tryGetValidActionToApply(o,s){if(!!s.validActions.length&&(o.autoApply==="first"&&s.validActions.length>0||o.autoApply==="ifSingle"&&s.validActions.length===1))return s.validActions[0]}showCodeActionList(o,s,a,u){return Ie(this,void 0,void 0,function*(){this._codeActionWidget.getValue().show(o,s,a,u)})}};g=new WeakMap,p=Me([_e(4,S.IInstantiationService)],p),e.CodeActionUi=p}),define(Q[263],J([0,1,23,150,2,8,13,133,25,145,634,146,466,26,16,9,85,32,59,87,617,130]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.AutoFixAction=e.FixAllAction=e.OrganizeImportsAction=e.SourceAction=e.RefactorAction=e.CodeActionCommand=e.QuickFixAction=e.applyCodeAction=e.QuickFixController=void 0;function m(F){return a.ContextKeyExpr.regex(l.SUPPORTED_CODE_ACTIONS.keys()[0],new RegExp("(\\s|^)"+w.escapeRegExpCharacters(F.value)+"\\b"))}const _={type:"object",defaultSnippets:[{body:{kind:""}}],properties:{kind:{type:"string",description:o.localize(0,null)},apply:{type:"string",description:o.localize(1,null),default:"ifSingle",enum:["first","ifSingle","never"],enumDescriptions:[o.localize(2,null),o.localize(3,null),o.localize(4,null)]},preferred:{type:"boolean",default:!1,description:o.localize(5,null)}}};let f=class xt extends M.Disposable{constructor(D,R,W,x,K){super();this._instantiationService=K,this._editor=D,this._model=this._register(new l.CodeActionModel(this._editor,R,W,x)),this._register(this._model.onDidChangeState(Y=>this.update(Y))),this._ui=new N.Lazy(()=>this._register(new p.CodeActionUi(D,I.Id,B.Id,{applyCodeAction:(Y,ee)=>Ie(this,void 0,void 0,function*(){try{yield this._applyCodeAction(Y)}finally{ee&&this._trigger({type:1,filter:{}})}})},this._instantiationService)))}static get(D){return D.getContribution(xt.ID)}update(D){this._ui.getValue().update(D)}showCodeActions(D,R,W){return this._ui.getValue().showCodeActionList(D,R,W,{includeDisabledActions:!1})}manualTriggerAtCurrentPosition(D,R,W){if(!!this._editor.hasModel()){c.MessageController.get(this._editor).closeMessage();const x=this._editor.getPosition();this._trigger({type:2,filter:R,autoApply:W,context:{notAvailableMessage:D,position:x}})}}_trigger(D){return this._model.trigger(D)}_applyCodeAction(D){return this._instantiationService.invokeFunction(v,D,this._editor)}};f.ID="editor.contrib.quickFixController",f=Me([_e(1,r.IMarkerService),_e(2,a.IContextKeyService),_e(3,n.IEditorProgressService),_e(4,u.IInstantiationService)],f),e.QuickFixController=f;function v(F,D,R){return Ie(this,void 0,void 0,function*(){const W=F.get(C.IBulkEditService),x=F.get(s.ICommandService),K=F.get(t.ITelemetryService),Y=F.get(i.INotificationService);if(K.publicLog2("codeAction.applyCodeAction",{codeActionTitle:D.action.title,codeActionKind:D.action.kind,codeActionIsPreferred:!!D.action.isPreferred}),yield D.resolve(b.CancellationToken.None),D.action.edit&&(yield W.apply(C.ResourceEdit.convert(D.action.edit),{editor:R,label:D.action.title})),D.action.command)try{yield x.executeCommand(D.action.command.id,...D.action.command.arguments||[])}catch(ee){const se=y(ee);Y.error(typeof se=="string"?se:o.localize(6,null))}})}e.applyCodeAction=v;function y(F){return typeof F=="string"?F:F instanceof Error&&typeof F.message=="string"?F.message:void 0}function L(F,D,R,W){if(F.hasModel()){const x=f.get(F);x&&x.manualTriggerAtCurrentPosition(D,R,W)}}class I extends S.EditorAction{constructor(){super({id:I.Id,label:o.localize(7,null),alias:"Quick Fix...",precondition:a.ContextKeyExpr.and(d.EditorContextKeys.writable,d.EditorContextKeys.hasCodeActionsProvider),kbOpts:{kbExpr:d.EditorContextKeys.editorTextFocus,primary:2048|84,weight:100}})}run(D,R){return L(R,o.localize(8,null),void 0,void 0)}}e.QuickFixAction=I,I.Id="editor.action.quickFix";class k extends S.EditorCommand{constructor(){super({id:g.codeActionCommandId,precondition:a.ContextKeyExpr.and(d.EditorContextKeys.writable,d.EditorContextKeys.hasCodeActionsProvider),description:{description:"Trigger a code action",args:[{name:"args",schema:_}]}})}runEditorCommand(D,R,W){const x=h.CodeActionCommandArgs.fromUser(W,{kind:h.CodeActionKind.Empty,apply:"ifSingle"});return L(R,typeof(W==null?void 0:W.kind)=="string"?x.preferred?o.localize(9,null,W.kind):o.localize(10,null,W.kind):x.preferred?o.localize(11,null):o.localize(12,null),{include:x.kind,includeSourceActions:!0,onlyIncludePreferredActions:x.preferred},x.apply)}}e.CodeActionCommand=k;class E extends S.EditorAction{constructor(){super({id:g.refactorCommandId,label:o.localize(13,null),alias:"Refactor...",precondition:a.ContextKeyExpr.and(d.EditorContextKeys.writable,d.EditorContextKeys.hasCodeActionsProvider),kbOpts:{kbExpr:d.EditorContextKeys.editorTextFocus,primary:2048|1024|48,mac:{primary:256|1024|48},weight:100},contextMenuOpts:{group:"1_modification",order:2,when:a.ContextKeyExpr.and(d.EditorContextKeys.writable,m(h.CodeActionKind.Refactor))},description:{description:"Refactor...",args:[{name:"args",schema:_}]}})}run(D,R,W){const x=h.CodeActionCommandArgs.fromUser(W,{kind:h.CodeActionKind.Refactor,apply:"never"});return L(R,typeof(W==null?void 0:W.kind)=="string"?x.preferred?o.localize(14,null,W.kind):o.localize(15,null,W.kind):x.preferred?o.localize(16,null):o.localize(17,null),{include:h.CodeActionKind.Refactor.contains(x.kind)?x.kind:h.CodeActionKind.None,onlyIncludePreferredActions:x.preferred},x.apply)}}e.RefactorAction=E;class T extends S.EditorAction{constructor(){super({id:g.sourceActionCommandId,label:o.localize(18,null),alias:"Source Action...",precondition:a.ContextKeyExpr.and(d.EditorContextKeys.writable,d.EditorContextKeys.hasCodeActionsProvider),contextMenuOpts:{group:"1_modification",order:2.1,when:a.ContextKeyExpr.and(d.EditorContextKeys.writable,m(h.CodeActionKind.Source))},description:{description:"Source Action...",args:[{name:"args",schema:_}]}})}run(D,R,W){const x=h.CodeActionCommandArgs.fromUser(W,{kind:h.CodeActionKind.Source,apply:"never"});return L(R,typeof(W==null?void 0:W.kind)=="string"?x.preferred?o.localize(19,null,W.kind):o.localize(20,null,W.kind):x.preferred?o.localize(21,null):o.localize(22,null),{include:h.CodeActionKind.Source.contains(x.kind)?x.kind:h.CodeActionKind.None,includeSourceActions:!0,onlyIncludePreferredActions:x.preferred},x.apply)}}e.SourceAction=T;class O extends S.EditorAction{constructor(){super({id:g.organizeImportsCommandId,label:o.localize(23,null),alias:"Organize Imports",precondition:a.ContextKeyExpr.and(d.EditorContextKeys.writable,m(h.CodeActionKind.SourceOrganizeImports)),kbOpts:{kbExpr:d.EditorContextKeys.editorTextFocus,primary:1024|512|45,weight:100}})}run(D,R){return L(R,o.localize(24,null),{include:h.CodeActionKind.SourceOrganizeImports,includeSourceActions:!0},"ifSingle")}}e.OrganizeImportsAction=O;class A extends S.EditorAction{constructor(){super({id:g.fixAllCommandId,label:o.localize(25,null),alias:"Fix All",precondition:a.ContextKeyExpr.and(d.EditorContextKeys.writable,m(h.CodeActionKind.SourceFixAll))})}run(D,R){return L(R,o.localize(26,null),{include:h.CodeActionKind.SourceFixAll,includeSourceActions:!0},"ifSingle")}}e.FixAllAction=A;class B extends S.EditorAction{constructor(){super({id:B.Id,label:o.localize(27,null),alias:"Auto Fix...",precondition:a.ContextKeyExpr.and(d.EditorContextKeys.writable,m(h.CodeActionKind.QuickFix)),kbOpts:{kbExpr:d.EditorContextKeys.editorTextFocus,primary:512|1024|84,mac:{primary:2048|512|84},weight:100}})}run(D,R){return L(R,o.localize(28,null),{include:h.CodeActionKind.QuickFix,onlyIncludePreferredActions:!0},"ifSingle")}}e.AutoFixAction=B,B.Id="editor.action.autoFix"}),define(Q[635],J([0,1,13,263]),function(q,e,b,N){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),b.registerEditorContribution(N.QuickFixController.ID,N.QuickFixController),b.registerEditorAction(N.QuickFixAction),b.registerEditorAction(N.RefactorAction),b.registerEditorAction(N.SourceAction),b.registerEditorAction(N.OrganizeImportsAction),b.registerEditorAction(N.AutoFixAction),b.registerEditorAction(N.FixAllAction),b.registerEditorCommand(new N.CodeActionCommand)}),define(Q[636],J([0,1,503,12,16,59,13,25,604,18,14,47,3,146,70,32,133,24,28,23,2,15,77,9,33,95,137,20]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y,L){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RenameAction=e.rename=void 0;class I{constructor(B,F){this.model=B,this.position=F,this._providerRenameIdx=0,this._providers=g.RenameProviderRegistry.ordered(B)}hasProvider(){return this._providers.length>0}resolveRenameLocation(B){return Ie(this,void 0,void 0,function*(){const F=[];for(this._providerRenameIdx=0;this._providerRenameIdx0?F.join(` +`):void 0}:{range:o.Range.fromPositions(this.position),text:"",rejectReason:F.length>0?F.join(` +`):void 0}})}provideRenameEdits(B,F){return Ie(this,void 0,void 0,function*(){return this._provideRenameEdits(B,this._providerRenameIdx,[],F)})}_provideRenameEdits(B,F,D,R){return Ie(this,void 0,void 0,function*(){const W=this._providers[F];if(!W)return{edits:[],rejectReason:D.join(` +`)};const x=yield W.provideRenameEdits(this.model,this.position,B,R);if(x){if(x.rejectReason)return this._provideRenameEdits(B,F+1,D.concat(x.rejectReason),R)}else return this._provideRenameEdits(B,F+1,D.concat(b.localize(0,null)),R);return x})}}function k(A,B,F){return Ie(this,void 0,void 0,function*(){const D=new I(A,B),R=yield D.resolveRenameLocation(t.CancellationToken.None);return(R==null?void 0:R.rejectReason)?{edits:[],rejectReason:R.rejectReason}:D.provideRenameEdits(F,t.CancellationToken.None)})}e.rename=k;let E=class Vt{constructor(B,F,D,R,W,x,K){this.editor=B,this._instaService=F,this._notificationService=D,this._bulkEditService=R,this._progressService=W,this._logService=x,this._configService=K,this._dispoableStore=new l.DisposableStore,this._cts=new t.CancellationTokenSource,this._renameInputField=this._dispoableStore.add(new h.IdleValue(()=>this._dispoableStore.add(this._instaService.createInstance(d.RenameInputField,this.editor,["acceptRenameInput","acceptRenameInputWithPreview"]))))}static get(B){return B.getContribution(Vt.ID)}dispose(){this._dispoableStore.dispose(),this._cts.dispose(!0)}run(){return Ie(this,void 0,void 0,function*(){if(this._cts.dispose(!0),!!this.editor.hasModel()){const B=this.editor.getPosition(),F=new I(this.editor.getModel(),B);if(!!F.hasProvider()){this._cts=new a.EditorStateCancellationTokenSource(this.editor,4|1);let D;try{const se=F.resolveRenameLocation(this._cts.token);this._progressService.showWhile(se,250),D=yield se}catch(se){s.MessageController.get(this.editor).showMessage(se||b.localize(1,null),B);return}if(!!D){if(D.rejectReason){s.MessageController.get(this.editor).showMessage(D.rejectReason,B);return}if(!this._cts.token.isCancellationRequested){this._cts.dispose(),this._cts=new a.EditorStateCancellationTokenSource(this.editor,4|1,D.range);let R=this.editor.getSelection(),W=0,x=D.text.length;!o.Range.isEmpty(R)&&!o.Range.spansMultipleLines(R)&&o.Range.containsRange(D.range,R)&&(W=Math.max(0,R.startColumn-D.range.startColumn),x=Math.min(D.range.endColumn,R.endColumn)-D.range.startColumn);const K=this._bulkEditService.hasPreviewHandler()&&this._configService.getValue(this.editor.getModel().uri,"editor.rename.enablePreview"),Y=yield this._renameInputField.value.getInput(D.range,D.text,W,x,K,this._cts.token);if(typeof Y=="boolean"){Y&&this.editor.focus();return}this.editor.focus();const ee=h.raceCancellation(F.provideRenameEdits(Y.newName,this._cts.token),this._cts.token).then(se=>Ie(this,void 0,void 0,function*(){if(!(!se||!this.editor.hasModel())){if(se.rejectReason){this._notificationService.info(se.rejectReason);return}this._bulkEditService.apply(r.ResourceEdit.convert(se),{editor:this.editor,showPreview:Y.wantsPreview,label:b.localize(2,null,D==null?void 0:D.text),quotableLabel:b.localize(3,null,D==null?void 0:D.text)}).then(ne=>{ne.ariaSummary&&c.alert(b.localize(4,null,D.text,Y.newName,ne.ariaSummary))}).catch(ne=>{this._notificationService.error(b.localize(5,null)),this._logService.error(ne)})}}),se=>{this._notificationService.error(b.localize(6,null)),this._logService.error(se)});return this._progressService.showWhile(ee,250),ee}}}}})}acceptRenameInput(B){this._renameInputField.value.acceptInput(B)}cancelRenameInput(){this._renameInputField.value.cancelInput(!0)}};E.ID="editor.contrib.renameController",E=Me([_e(1,_.IInstantiationService),_e(2,u.INotificationService),_e(3,r.IBulkEditService),_e(4,w.IEditorProgressService),_e(5,m.ILogService),_e(6,y.ITextResourceConfigurationService)],E);class T extends S.EditorAction{constructor(){super({id:"editor.action.rename",label:b.localize(7,null),alias:"Rename Symbol",precondition:M.ContextKeyExpr.and(C.EditorContextKeys.writable,C.EditorContextKeys.hasRenameProvider),kbOpts:{kbExpr:C.EditorContextKeys.editorTextFocus,primary:60,weight:100},contextMenuOpts:{group:"1_modification",order:1.1}})}runCommand(B,F){const D=B.get(n.ICodeEditorService),[R,W]=Array.isArray(F)&&F||[void 0,void 0];return i.URI.isUri(R)&&p.Position.isIPosition(W)?D.openCodeEditor({resource:R},D.getActiveCodeEditor()).then(x=>{!x||(x.setPosition(W),x.invokeWithinContext(K=>(this.reportTelemetry(K,x),this.run(K,x))))},N.onUnexpectedError):super.runCommand(B,F)}run(B,F){const D=E.get(F);return D?D.run():Promise.resolve()}}e.RenameAction=T,S.registerEditorContribution(E.ID,E),S.registerEditorAction(T);const O=S.EditorCommand.bindToContribution(E.get);S.registerEditorCommand(new O({id:"acceptRenameInput",precondition:d.CONTEXT_RENAME_INPUT_VISIBLE,handler:A=>A.acceptRenameInput(!1),kbOpts:{weight:100+99,kbExpr:C.EditorContextKeys.focus,primary:3}})),S.registerEditorCommand(new O({id:"acceptRenameInputWithPreview",precondition:M.ContextKeyExpr.and(d.CONTEXT_RENAME_INPUT_VISIBLE,M.ContextKeyExpr.has("config.editor.rename.enablePreview")),handler:A=>A.acceptRenameInput(!0),kbOpts:{weight:100+99,kbExpr:C.EditorContextKeys.focus,primary:1024+3}})),S.registerEditorCommand(new O({id:"cancelRenameInput",precondition:d.CONTEXT_RENAME_INPUT_VISIBLE,handler:A=>A.cancelRenameInput(),kbOpts:{weight:100+99,kbExpr:C.EditorContextKeys.focus,primary:9,secondary:[1024|9]}})),S.registerModelAndPositionCommand("_executeDocumentRenameProvider",function(A,B,...F){const[D]=F;return L.assertType(typeof D=="string"),k(A,B,D)}),f.Registry.as(v.Extensions.Configuration).registerConfiguration({id:"editor",properties:{"editor.rename.enablePreview":{scope:5,description:b.localize(8,null),default:!0,type:"boolean"}}})}),define(Q[637],J([0,1,19,23,13,14,3,21,25,18,505,34,412,230,26,12]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.provideSelectionRanges=void 0;class r{constructor(_,f){this.index=_,this.ranges=f}mov(_){let f=this.index+(_?1:-1);if(f<0||f>=this.ranges.length)return this;const v=new r(f,this.ranges);return v.ranges[f].equalsRange(this.ranges[this.index])?v.mov(_):v}}class i{constructor(_){this._editor=_,this._ignoreSelection=!1}static get(_){return _.getContribution(i.ID)}dispose(){var _;(_=this._selectionListener)===null||_===void 0||_.dispose()}run(_){return Ie(this,void 0,void 0,function*(){if(!!this._editor.hasModel()){const f=this._editor.getSelections(),v=this._editor.getModel();if(!!g.SelectionRangeRegistry.has(v)&&(this._state||(yield h(v,f.map(L=>L.getPosition()),this._editor.getOption(97),N.CancellationToken.None).then(L=>{var I;if(!(!b.isNonEmptyArray(L)||L.length!==f.length)&&!(!this._editor.hasModel()||!b.equals(this._editor.getSelections(),f,(k,E)=>k.equalsSelection(E)))){for(let k=0;kE.containsPosition(f[k].getStartPosition())&&E.containsPosition(f[k].getEndPosition())),L[k].unshift(f[k]);this._state=L.map(k=>new r(0,k)),(I=this._selectionListener)===null||I===void 0||I.dispose(),this._selectionListener=this._editor.onDidChangeCursorPosition(()=>{var k;this._ignoreSelection||((k=this._selectionListener)===null||k===void 0||k.dispose(),this._state=void 0)})}})),!!this._state)){this._state=this._state.map(L=>L.mov(_));const y=this._state.map(L=>C.Selection.fromPositions(L.ranges[L.index].getStartPosition(),L.ranges[L.index].getEndPosition()));this._ignoreSelection=!0;try{this._editor.setSelections(y)}finally{this._ignoreSelection=!1}}}})}}i.ID="editor.contrib.smartSelectController";class n extends M.EditorAction{constructor(_,f){super(f);this._forward=_}run(_,f){return Ie(this,void 0,void 0,function*(){let v=i.get(f);v&&(yield v.run(this._forward))})}}class t extends n{constructor(){super(!0,{id:"editor.action.smartSelect.expand",label:p.localize(0,null),alias:"Expand Selection",precondition:void 0,kbOpts:{kbExpr:d.EditorContextKeys.editorTextFocus,primary:1024|512|17,mac:{primary:2048|256|1024|17,secondary:[256|1024|17]},weight:100},menuOpts:{menuId:c.MenuId.MenubarSelectionMenu,group:"1_basic",title:p.localize(1,null),order:2}})}}a.CommandsRegistry.registerCommandAlias("editor.action.smartSelect.grow","editor.action.smartSelect.expand");class l extends n{constructor(){super(!1,{id:"editor.action.smartSelect.shrink",label:p.localize(2,null),alias:"Shrink Selection",precondition:void 0,kbOpts:{kbExpr:d.EditorContextKeys.editorTextFocus,primary:1024|512|15,mac:{primary:2048|256|1024|15,secondary:[256|1024|15]},weight:100},menuOpts:{menuId:c.MenuId.MenubarSelectionMenu,group:"1_basic",title:p.localize(3,null),order:3}})}}M.registerEditorContribution(i.ID,i),M.registerEditorAction(t),M.registerEditorAction(l),g.SelectionRangeRegistry.register("*",new o.WordSelectionRangeProvider);function h(m,_,f,v){return Ie(this,void 0,void 0,function*(){const y=g.SelectionRangeRegistry.all(m);y.length===1&&y.unshift(new s.BracketSelectionRangeProvider);let L=[],I=[];for(const k of y)L.push(Promise.resolve(k.provideSelectionRanges(m,_,v)).then(E=>{if(b.isNonEmptyArray(E)&&E.length===_.length)for(let T=0;T<_.length;T++){I[T]||(I[T]=[]);for(const O of E[T])S.Range.isIRange(O.range)&&S.Range.containsPosition(O.range,_[T])&&I[T].push(S.Range.lift(O.range))}},u.onUnexpectedExternalError));return yield Promise.all(L),I.map(k=>{if(k.length===0)return[];k.sort((A,B)=>w.Position.isBefore(A.getStartPosition(),B.getStartPosition())?1:w.Position.isBefore(B.getStartPosition(),A.getStartPosition())||w.Position.isBefore(A.getEndPosition(),B.getEndPosition())?-1:w.Position.isBefore(B.getEndPosition(),A.getEndPosition())?1:0);let E=[],T;for(const A of k)(!T||S.Range.containsRange(A,T)&&!S.Range.equalsRange(A,T))&&(E.push(A),T=A);if(!f.selectLeadingAndTrailingWhitespace)return E;let O=[E[0]];for(let A=1;A{this._resolveCache=void 0,this._isResolved=!1});this._resolveCache=Promise.resolve(this.provider.resolveCompletionItem(this.completion,T)).then(A=>{Object.assign(this.completion,A),this._isResolved=!0,O.dispose()},A=>{b.isPromiseCanceledError(A)&&(this._resolveCache=void 0,this._isResolved=!1)})}return this._resolveCache})}}e.CompletionItem=i;class n{constructor(T=2,O=new Set,A=new Set){this.snippetSortOrder=T,this.kindFilter=O,this.providerFilter=A}}e.CompletionOptions=n,n.default=new n;let t;function l(){return t}e.getSnippetSuggestSupport=l;class h{constructor(T,O,A,B){this.items=T,this.needsClipboard=O,this.durations=A,this.disposable=B}}e.CompletionItemModel=h;function m(E,T,O=n.default,A={triggerKind:0},B=S.CancellationToken.None){return Ie(this,void 0,void 0,function*(){const F=new o.StopWatch(!0);T=T.clone();const D=E.getWordAtPosition(T),R=D?new C.Range(T.lineNumber,D.startColumn,T.lineNumber,D.endColumn):C.Range.fromPositions(T),W={replace:R,insert:R.setEndPosition(T.lineNumber,T.column)},x=[],K=new g.DisposableStore,Y=[];let ee=!1;const se=(le,X,z)=>{var P,V;if(!!X){for(let U of X.suggestions)O.kindFilter.has(U.kind)||(U.range||(U.range=W),U.sortText||(U.sortText=typeof U.label=="string"?U.label:U.label.name),!ee&&U.insertTextRules&&U.insertTextRules&4&&(ee=c.SnippetParser.guessNeedsClipboard(U.insertText)),x.push(new i(T,U,X,le)));g.isDisposable(X)&&K.add(X),Y.push({providerName:(P=le._debugDisplayName)!==null&&P!==void 0?P:"unkown_provider",elapsedProvider:(V=X.duration)!==null&&V!==void 0?V:-1,elapsedOverall:z.elapsed()})}},ne=(()=>Ie(this,void 0,void 0,function*(){if(!(!t||O.kindFilter.has(27))&&!(O.providerFilter.size>0&&!O.providerFilter.has(t))){const le=new o.StopWatch(!0),X=yield t.provideCompletionItems(E,T,A,B);se(t,X,le)}}))();for(let le of N.CompletionProviderRegistry.orderedGroups(E)){let X=x.length;if(yield Promise.all(le.map(z=>Ie(this,void 0,void 0,function*(){if(!(O.providerFilter.size>0&&!O.providerFilter.has(z)))try{const P=new o.StopWatch(!0),V=yield z.provideCompletionItems(E,T,A,B);se(z,V,P)}catch(P){b.onUnexpectedExternalError(P)}}))),X!==x.length||B.isCancellationRequested)break}return yield ne,B.isCancellationRequested?(K.dispose(),Promise.reject(b.canceled())):new h(x.sort(L(O.snippetSortOrder)),ee,{entries:Y,elapsed:F.elapsed()},K)})}e.provideSuggestionItems=m;function _(E,T){if(E.sortTextLow&&T.sortTextLow){if(E.sortTextLowT.sortTextLow)return 1}return E.completion.labelT.completion.label?1:E.completion.kind-T.completion.kind}function f(E,T){if(E.completion.kind!==T.completion.kind){if(E.completion.kind===27)return-1;if(T.completion.kind===27)return 1}return _(E,T)}function v(E,T){if(E.completion.kind!==T.completion.kind){if(E.completion.kind===27)return 1;if(T.completion.kind===27)return-1}return _(E,T)}const y=new Map;y.set(0,f),y.set(2,v),y.set(1,_);function L(E){return y.get(E)}e.getSuggestionComparator=L,s.CommandsRegistry.registerCommand("_executeCompletionItemProvider",(E,...T)=>Ie(void 0,void 0,void 0,function*(){const[O,A,B,F]=T;a.assertType(u.URI.isUri(O)),a.assertType(M.Position.isIPosition(A)),a.assertType(typeof B=="string"||!B),a.assertType(typeof F=="number"||!F);const D=yield E.get(r.ITextModelService).createModelReference(O);try{const R={incomplete:!1,suggestions:[]},W=[],x=yield m(D.object.textEditorModel,M.Position.lift(A),void 0,{triggerCharacter:B,triggerKind:B?1:0});for(const K of x.items)W.length<(F!=null?F:0)&&W.push(K.resolve(S.CancellationToken.None)),R.incomplete=R.incomplete||K.container.incomplete,R.suggestions.push(K.completion);try{return yield Promise.all(W),R}finally{setTimeout(()=>x.disposable.dispose(),100)}}finally{D.dispose()}}));const I=new class{constructor(){this.onlyOnceSuggestions=[]}provideCompletionItems(){let T={suggestions:this.onlyOnceSuggestions.slice(0)};return this.onlyOnceSuggestions.length=0,T}};N.CompletionProviderRegistry.register("*",I);function k(E,T){setTimeout(()=>{I.onlyOnceSuggestions.push(...T),E.getContribution("editor.contrib.suggestController").triggerSuggest(new Set().add(I))},0)}e.showSimpleSuggestions=k}),define(Q[264],J([0,1,513,47,13,186]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ToggleTabFocusModeAction=void 0;class S extends M.EditorAction{constructor(){super({id:S.ID,label:b.localize(0,null),alias:"Toggle Tab Key Moves Focus",precondition:void 0,kbOpts:{kbExpr:null,primary:2048|43,mac:{primary:256|1024|43},weight:100}})}run(d,g){const c=!w.TabFocus.getTabFocusMode();w.TabFocus.setTabFocusMode(c),c?N.alert(b.localize(1,null)):N.alert(b.localize(2,null))}}e.ToggleTabFocusModeAction=S,S.ID="editor.action.toggleTabFocusMode",M.registerEditorAction(S)}),define(Q[638],J([0,1,514,13,81]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0});class w extends N.EditorAction{constructor(){super({id:"editor.action.forceRetokenize",label:b.localize(0,null),alias:"Developer: Force Retokenize",precondition:void 0})}run(C,d){if(!!d.hasModel()){const g=d.getModel();g.resetTokenization();const p=new M.StopWatch(!0);g.forceTokenization(g.getLineCount()),p.stop(),console.log(`tokenization took ${p.elapsed()}`)}}}N.registerEditorAction(w)}),define(Q[639],J([0,1,515,2,13,28,184]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0});const C="ignoreUnusualLineTerminators";function d(c,o,s){c.setModelProperty(o.uri,C,s)}function g(c,o){return c.getModelProperty(o.uri,C)}let p=class extends N.Disposable{constructor(o,s,a){super();this._editor=o,this._dialogService=s,this._codeEditorService=a,this._config=this._editor.getOption(108),this._register(this._editor.onDidChangeConfiguration(u=>{u.hasChanged(108)&&(this._config=this._editor.getOption(108),this._checkForUnusualLineTerminators())})),this._register(this._editor.onDidChangeModel(()=>{this._checkForUnusualLineTerminators()})),this._register(this._editor.onDidChangeModelContent(u=>{u.isUndoing||this._checkForUnusualLineTerminators()}))}_checkForUnusualLineTerminators(){return Ie(this,void 0,void 0,function*(){if(this._config!=="off"&&!!this._editor.hasModel()){const o=this._editor.getModel();if(!!o.mightContainUnusualLineTerminators()&&g(this._codeEditorService,o)!==!0&&!this._editor.getOption(75)){if(this._config==="auto"){o.removeUnusualLineTerminators(this._editor.getSelections());return}if(!(yield this._dialogService.confirm({title:b.localize(0,null),message:b.localize(1,null),detail:b.localize(2,null),primaryButton:b.localize(3,null),secondaryButton:b.localize(4,null)})).confirmed){d(this._codeEditorService,o,!0);return}o.removeUnusualLineTerminators(this._editor.getSelections())}}})}};p.ID="editor.contrib.unusualLineTerminatorsDetector",p=Me([_e(1,S.IDialogService),_e(2,w.ICodeEditorService)],p),M.registerEditorContribution(p.ID,p)}),define(Q[640],J([0,1,516,19,15,23,12,2,13,3,25,53,31,18,16,22,11,47]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getOccurrencesAtPosition=void 0;const n=u.registerColor("editor.wordHighlightBackground",{dark:"#575757B8",light:"#57575740",hc:null},b.localize(0,null),!0),t=u.registerColor("editor.wordHighlightStrongBackground",{dark:"#004972B8",light:"#0e639c40",hc:null},b.localize(1,null),!0),l=u.registerColor("editor.wordHighlightBorder",{light:null,dark:null,hc:u.activeContrastBorder},b.localize(2,null)),h=u.registerColor("editor.wordHighlightStrongBorder",{light:null,dark:null,hc:u.activeContrastBorder},b.localize(3,null)),m=u.registerColor("editorOverviewRuler.wordHighlightForeground",{dark:"#A0A0A0CC",light:"#A0A0A0CC",hc:"#A0A0A0CC"},b.localize(4,null),!0),_=u.registerColor("editorOverviewRuler.wordHighlightStrongForeground",{dark:"#C0A0C0CC",light:"#C0A0C0CC",hc:"#C0A0C0CC"},b.localize(5,null),!0),f=new a.RawContextKey("hasWordHighlights",!1);function v(D,R,W){const x=s.DocumentHighlightProviderRegistry.ordered(D);return M.first(x.map(K=>()=>Promise.resolve(K.provideDocumentHighlights(D,R,W)).then(void 0,S.onUnexpectedExternalError)),N.isNonEmptyArray)}e.getOccurrencesAtPosition=v;class y{constructor(R,W,x){this._wordRange=this._getCurrentWordRange(R,W),this.result=M.createCancelablePromise(K=>this._compute(R,W,x,K))}_getCurrentWordRange(R,W){const x=R.getWordAtPosition(W.getPosition());return x?new g.Range(W.startLineNumber,x.startColumn,W.startLineNumber,x.endColumn):null}isValid(R,W,x){const K=W.startLineNumber,Y=W.startColumn,ee=W.endColumn,se=this._getCurrentWordRange(R,W);let ne=Boolean(this._wordRange&&this._wordRange.equalsRange(se));for(let le=0,X=x.length;!ne&&le=ee&&(ne=!0)}return ne}cancel(){this.result.cancel()}}class L extends y{_compute(R,W,x,K){return v(R,W.getPosition(),K).then(Y=>Y||[])}}class I extends y{constructor(R,W,x){super(R,W,x);this._selectionIsEmpty=W.isEmpty()}_compute(R,W,x,K){return M.timeout(250,K).then(()=>{if(!W.isEmpty())return[];const Y=R.getWordAtPosition(W.getPosition());return!Y||Y.word.length>1e3?[]:R.findMatches(Y.word,!0,!1,!0,x,!1).map(se=>({range:se.range,kind:s.DocumentHighlightKind.Text}))})}isValid(R,W,x){const K=W.isEmpty();return this._selectionIsEmpty!==K?!1:super.isValid(R,W,x)}}function k(D,R,W){return s.DocumentHighlightProviderRegistry.has(D)?new L(D,R,W):new I(D,R,W)}d.registerModelAndPositionCommand("_executeDocumentHighlights",(D,R)=>v(D,R,w.CancellationToken.None));class E{constructor(R,W){this.toUnhook=new C.DisposableStore,this.workerRequestTokenId=0,this.workerRequestCompleted=!1,this.workerRequestValue=[],this.lastCursorPositionChangeTime=0,this.renderDecorationsTimer=-1,this.editor=R,this._hasWordHighlights=f.bindTo(W),this._ignorePositionChangeEvent=!1,this.occurrencesHighlight=this.editor.getOption(66),this.model=this.editor.getModel(),this.toUnhook.add(R.onDidChangeCursorPosition(x=>{this._ignorePositionChangeEvent||!this.occurrencesHighlight||this._onPositionChanged(x)})),this.toUnhook.add(R.onDidChangeModelContent(x=>{this._stopAll()})),this.toUnhook.add(R.onDidChangeConfiguration(x=>{let K=this.editor.getOption(66);this.occurrencesHighlight!==K&&(this.occurrencesHighlight=K,this._stopAll())})),this._decorationIds=[],this.workerRequestTokenId=0,this.workerRequest=null,this.workerRequestCompleted=!1,this.lastCursorPositionChangeTime=0,this.renderDecorationsTimer=-1}hasDecorations(){return this._decorationIds.length>0}restore(){!this.occurrencesHighlight||this._run()}_getSortedHighlights(){return N.coalesce(this._decorationIds.map(R=>this.model.getDecorationRange(R)).sort(g.Range.compareRangesUsingStarts))}moveNext(){let R=this._getSortedHighlights(),x=(R.findIndex(Y=>Y.containsPosition(this.editor.getPosition()))+1)%R.length,K=R[x];try{this._ignorePositionChangeEvent=!0,this.editor.setPosition(K.getStartPosition()),this.editor.revealRangeInCenterIfOutsideViewport(K);const Y=this._getWord();if(Y){const ee=this.editor.getModel().getLineContent(K.startLineNumber);i.alert(`${ee}, ${x+1} of ${R.length} for '${Y.word}'`)}}finally{this._ignorePositionChangeEvent=!1}}moveBack(){let R=this._getSortedHighlights(),x=(R.findIndex(Y=>Y.containsPosition(this.editor.getPosition()))-1+R.length)%R.length,K=R[x];try{this._ignorePositionChangeEvent=!0,this.editor.setPosition(K.getStartPosition()),this.editor.revealRangeInCenterIfOutsideViewport(K);const Y=this._getWord();if(Y){const ee=this.editor.getModel().getLineContent(K.startLineNumber);i.alert(`${ee}, ${x+1} of ${R.length} for '${Y.word}'`)}}finally{this._ignorePositionChangeEvent=!1}}_removeDecorations(){this._decorationIds.length>0&&(this._decorationIds=this.editor.deltaDecorations(this._decorationIds,[]),this._hasWordHighlights.set(!1))}_stopAll(){this._removeDecorations(),this.renderDecorationsTimer!==-1&&(clearTimeout(this.renderDecorationsTimer),this.renderDecorationsTimer=-1),this.workerRequest!==null&&(this.workerRequest.cancel(),this.workerRequest=null),this.workerRequestCompleted||(this.workerRequestTokenId++,this.workerRequestCompleted=!0)}_onPositionChanged(R){if(!this.occurrencesHighlight){this._stopAll();return}if(R.reason!==3){this._stopAll();return}this._run()}_getWord(){let R=this.editor.getSelection(),W=R.startLineNumber,x=R.startColumn;return this.model.getWordAtPosition({lineNumber:W,column:x})}_run(){let R=this.editor.getSelection();if(R.startLineNumber!==R.endLineNumber){this._stopAll();return}let W=R.startColumn,x=R.endColumn;const K=this._getWord();if(!K||K.startColumn>W||K.endColumn{ee===this.workerRequestTokenId&&(this.workerRequestCompleted=!0,this.workerRequestValue=se||[],this._beginRenderDecorations())},S.onUnexpectedError)}}_beginRenderDecorations(){let R=new Date().getTime(),W=this.lastCursorPositionChangeTime+250;R>=W?(this.renderDecorationsTimer=-1,this.renderDecorations()):this.renderDecorationsTimer=setTimeout(()=>{this.renderDecorations()},W-R)}renderDecorations(){this.renderDecorationsTimer=-1;let R=[];for(const W of this.workerRequestValue)W.range&&R.push({range:W.range,options:E._getDecorationOptions(W.kind)});this._decorationIds=this.editor.deltaDecorations(this._decorationIds,R),this._hasWordHighlights.set(this.hasDecorations())}static _getDecorationOptions(R){return R===s.DocumentHighlightKind.Write?this._WRITE_OPTIONS:R===s.DocumentHighlightKind.Text?this._TEXT_OPTIONS:this._REGULAR_OPTIONS}dispose(){this._stopAll(),this.toUnhook.dispose()}}E._WRITE_OPTIONS=o.ModelDecorationOptions.register({stickiness:1,className:"wordHighlightStrong",overviewRuler:{color:r.themeColorFromId(_),position:c.OverviewRulerLane.Center}}),E._TEXT_OPTIONS=o.ModelDecorationOptions.register({stickiness:1,className:"selectionHighlight",overviewRuler:{color:r.themeColorFromId(u.overviewRulerSelectionHighlightForeground),position:c.OverviewRulerLane.Center}}),E._REGULAR_OPTIONS=o.ModelDecorationOptions.register({stickiness:1,className:"wordHighlight",overviewRuler:{color:r.themeColorFromId(m),position:c.OverviewRulerLane.Center}});let T=class zt extends C.Disposable{constructor(R,W){super();this.wordHighlighter=null;const x=()=>{R.hasModel()&&(this.wordHighlighter=new E(R,W))};this._register(R.onDidChangeModel(K=>{this.wordHighlighter&&(this.wordHighlighter.dispose(),this.wordHighlighter=null),x()})),x()}static get(R){return R.getContribution(zt.ID)}saveViewState(){return!!(this.wordHighlighter&&this.wordHighlighter.hasDecorations())}moveNext(){this.wordHighlighter&&this.wordHighlighter.moveNext()}moveBack(){this.wordHighlighter&&this.wordHighlighter.moveBack()}restoreViewState(R){this.wordHighlighter&&R&&this.wordHighlighter.restore()}dispose(){this.wordHighlighter&&(this.wordHighlighter.dispose(),this.wordHighlighter=null),super.dispose()}};T.ID="editor.contrib.wordHighlighter",T=Me([_e(1,a.IContextKeyService)],T);class O extends d.EditorAction{constructor(R,W){super(W);this._isNext=R}run(R,W){const x=T.get(W);!x||(this._isNext?x.moveNext():x.moveBack())}}class A extends O{constructor(){super(!0,{id:"editor.action.wordHighlight.next",label:b.localize(6,null),alias:"Go to Next Symbol Highlight",precondition:f,kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:65,weight:100}})}}class B extends O{constructor(){super(!1,{id:"editor.action.wordHighlight.prev",label:b.localize(7,null),alias:"Go to Previous Symbol Highlight",precondition:f,kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:1024|65,weight:100}})}}class F extends d.EditorAction{constructor(){super({id:"editor.action.wordHighlight.trigger",label:b.localize(8,null),alias:"Trigger Symbol Highlight",precondition:f.toNegated(),kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:0,weight:100}})}run(R,W,x){const K=T.get(W);!K||K.restoreViewState(!0)}}d.registerEditorContribution(T.ID,T),d.registerEditorAction(A),d.registerEditorAction(B),d.registerEditorAction(F),r.registerThemingParticipant((D,R)=>{const W=D.getColor(u.editorSelectionHighlight);W&&(R.addRule(`.monaco-editor .focused .selectionHighlight { background-color: ${W}; }`),R.addRule(`.monaco-editor .selectionHighlight { background-color: ${W.transparent(.5)}; }`));const x=D.getColor(n);x&&R.addRule(`.monaco-editor .wordHighlight { background-color: ${x}; }`);const K=D.getColor(t);K&&R.addRule(`.monaco-editor .wordHighlightStrong { background-color: ${K}; }`);const Y=D.getColor(u.editorSelectionHighlightBorder);Y&&R.addRule(`.monaco-editor .selectionHighlight { border: 1px ${D.type==="hc"?"dotted":"solid"} ${Y}; box-sizing: border-box; }`);const ee=D.getColor(l);ee&&R.addRule(`.monaco-editor .wordHighlight { border: 1px ${D.type==="hc"?"dashed":"solid"} ${ee}; box-sizing: border-box; }`);const se=D.getColor(h);se&&R.addRule(`.monaco-editor .wordHighlightStrong { border: 1px ${D.type==="hc"?"dashed":"solid"} ${se}; box-sizing: border-box; }`)})}),define(Q[265],J([0,1,517,13,92,42,136,106,14,3,21,25,65,16,38,41,250]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DeleteInsideWord=e.DeleteWordRight=e.DeleteWordEndRight=e.DeleteWordStartRight=e.DeleteWordLeft=e.DeleteWordEndLeft=e.DeleteWordStartLeft=e.DeleteWordRightCommand=e.DeleteWordLeftCommand=e.DeleteWordCommand=e.CursorWordAccessibilityRightSelect=e.CursorWordAccessibilityRight=e.CursorWordRightSelect=e.CursorWordEndRightSelect=e.CursorWordStartRightSelect=e.CursorWordRight=e.CursorWordEndRight=e.CursorWordStartRight=e.CursorWordAccessibilityLeftSelect=e.CursorWordAccessibilityLeft=e.CursorWordLeftSelect=e.CursorWordEndLeftSelect=e.CursorWordStartLeftSelect=e.CursorWordLeft=e.CursorWordEndLeft=e.CursorWordStartLeft=e.WordRightCommand=e.WordLeftCommand=e.MoveWordCommand=void 0;class i extends N.EditorCommand{constructor(z){super(z);this._inSelectionMode=z.inSelectionMode,this._wordNavigationType=z.wordNavigationType}runEditorCommand(z,P,V){if(!!P.hasModel()){const U=C.getMapForWordSeparators(P.getOption(110)),H=P.getModel(),ie=P.getSelections().map(oe=>{const ae=new d.Position(oe.positionLineNumber,oe.positionColumn),G=this._move(U,H,ae,this._wordNavigationType);return this._moveTo(oe,G,this._inSelectionMode)});if(H.pushStackElement(),P._getViewModel().setCursorStates("moveWordCommand",3,ie.map(oe=>w.CursorState.fromModelSelection(oe))),ie.length===1){const oe=new d.Position(ie[0].positionLineNumber,ie[0].positionColumn);P.revealPosition(oe,0)}}}_moveTo(z,P,V){return V?new p.Selection(z.selectionStartLineNumber,z.selectionStartColumn,P.lineNumber,P.column):new p.Selection(P.lineNumber,P.column,P.lineNumber,P.column)}}e.MoveWordCommand=i;class n extends i{_move(z,P,V,U){return S.WordOperations.moveWordLeft(z,P,V,U)}}e.WordLeftCommand=n;class t extends i{_move(z,P,V,U){return S.WordOperations.moveWordRight(z,P,V,U)}}e.WordRightCommand=t;class l extends n{constructor(){super({inSelectionMode:!1,wordNavigationType:0,id:"cursorWordStartLeft",precondition:void 0})}}e.CursorWordStartLeft=l;class h extends n{constructor(){super({inSelectionMode:!1,wordNavigationType:2,id:"cursorWordEndLeft",precondition:void 0})}}e.CursorWordEndLeft=h;class m extends n{constructor(){var z;super({inSelectionMode:!1,wordNavigationType:1,id:"cursorWordLeft",precondition:void 0,kbOpts:{kbExpr:s.ContextKeyExpr.and(c.EditorContextKeys.textInputFocus,(z=s.ContextKeyExpr.and(o.CONTEXT_ACCESSIBILITY_MODE_ENABLED,r.IsWindowsContext))===null||z===void 0?void 0:z.negate()),primary:2048|15,mac:{primary:512|15},weight:100}})}}e.CursorWordLeft=m;class _ extends n{constructor(){super({inSelectionMode:!0,wordNavigationType:0,id:"cursorWordStartLeftSelect",precondition:void 0})}}e.CursorWordStartLeftSelect=_;class f extends n{constructor(){super({inSelectionMode:!0,wordNavigationType:2,id:"cursorWordEndLeftSelect",precondition:void 0})}}e.CursorWordEndLeftSelect=f;class v extends n{constructor(){var z;super({inSelectionMode:!0,wordNavigationType:1,id:"cursorWordLeftSelect",precondition:void 0,kbOpts:{kbExpr:s.ContextKeyExpr.and(c.EditorContextKeys.textInputFocus,(z=s.ContextKeyExpr.and(o.CONTEXT_ACCESSIBILITY_MODE_ENABLED,r.IsWindowsContext))===null||z===void 0?void 0:z.negate()),primary:2048|1024|15,mac:{primary:512|1024|15},weight:100}})}}e.CursorWordLeftSelect=v;class y extends n{constructor(){super({inSelectionMode:!1,wordNavigationType:3,id:"cursorWordAccessibilityLeft",precondition:void 0})}_move(z,P,V,U){return super._move(C.getMapForWordSeparators(a.EditorOptions.wordSeparators.defaultValue),P,V,U)}}e.CursorWordAccessibilityLeft=y;class L extends n{constructor(){super({inSelectionMode:!0,wordNavigationType:3,id:"cursorWordAccessibilityLeftSelect",precondition:void 0})}_move(z,P,V,U){return super._move(C.getMapForWordSeparators(a.EditorOptions.wordSeparators.defaultValue),P,V,U)}}e.CursorWordAccessibilityLeftSelect=L;class I extends t{constructor(){super({inSelectionMode:!1,wordNavigationType:0,id:"cursorWordStartRight",precondition:void 0})}}e.CursorWordStartRight=I;class k extends t{constructor(){var z;super({inSelectionMode:!1,wordNavigationType:2,id:"cursorWordEndRight",precondition:void 0,kbOpts:{kbExpr:s.ContextKeyExpr.and(c.EditorContextKeys.textInputFocus,(z=s.ContextKeyExpr.and(o.CONTEXT_ACCESSIBILITY_MODE_ENABLED,r.IsWindowsContext))===null||z===void 0?void 0:z.negate()),primary:2048|17,mac:{primary:512|17},weight:100}})}}e.CursorWordEndRight=k;class E extends t{constructor(){super({inSelectionMode:!1,wordNavigationType:2,id:"cursorWordRight",precondition:void 0})}}e.CursorWordRight=E;class T extends t{constructor(){super({inSelectionMode:!0,wordNavigationType:0,id:"cursorWordStartRightSelect",precondition:void 0})}}e.CursorWordStartRightSelect=T;class O extends t{constructor(){var z;super({inSelectionMode:!0,wordNavigationType:2,id:"cursorWordEndRightSelect",precondition:void 0,kbOpts:{kbExpr:s.ContextKeyExpr.and(c.EditorContextKeys.textInputFocus,(z=s.ContextKeyExpr.and(o.CONTEXT_ACCESSIBILITY_MODE_ENABLED,r.IsWindowsContext))===null||z===void 0?void 0:z.negate()),primary:2048|1024|17,mac:{primary:512|1024|17},weight:100}})}}e.CursorWordEndRightSelect=O;class A extends t{constructor(){super({inSelectionMode:!0,wordNavigationType:2,id:"cursorWordRightSelect",precondition:void 0})}}e.CursorWordRightSelect=A;class B extends t{constructor(){super({inSelectionMode:!1,wordNavigationType:3,id:"cursorWordAccessibilityRight",precondition:void 0})}_move(z,P,V,U){return super._move(C.getMapForWordSeparators(a.EditorOptions.wordSeparators.defaultValue),P,V,U)}}e.CursorWordAccessibilityRight=B;class F extends t{constructor(){super({inSelectionMode:!0,wordNavigationType:3,id:"cursorWordAccessibilityRightSelect",precondition:void 0})}_move(z,P,V,U){return super._move(C.getMapForWordSeparators(a.EditorOptions.wordSeparators.defaultValue),P,V,U)}}e.CursorWordAccessibilityRightSelect=F;class D extends N.EditorCommand{constructor(z){super(z);this._whitespaceHeuristics=z.whitespaceHeuristics,this._wordNavigationType=z.wordNavigationType}runEditorCommand(z,P,V){if(!!P.hasModel()){const U=C.getMapForWordSeparators(P.getOption(110)),H=P.getModel(),$=P.getSelections(),ie=P.getOption(5),oe=P.getOption(7),ae=u.LanguageConfigurationRegistry.getAutoClosingPairs(H.getLanguageIdentifier().id),G=$.map(j=>{const te=this._delete({wordSeparators:U,model:H,selection:j,whitespaceHeuristics:this._whitespaceHeuristics,autoClosingBrackets:ie,autoClosingQuotes:oe,autoClosingPairs:ae},this._wordNavigationType);return new M.ReplaceCommand(te,"")});P.pushUndoStop(),P.executeCommands(this.id,G),P.pushUndoStop()}}}e.DeleteWordCommand=D;class R extends D{_delete(z,P){let V=S.WordOperations.deleteWordLeft(z,P);return V||new g.Range(1,1,1,1)}}e.DeleteWordLeftCommand=R;class W extends D{_delete(z,P){let V=S.WordOperations.deleteWordRight(z,P);if(V)return V;const U=z.model.getLineCount(),H=z.model.getLineMaxColumn(U);return new g.Range(U,H,U,H)}}e.DeleteWordRightCommand=W;class x extends R{constructor(){super({whitespaceHeuristics:!1,wordNavigationType:0,id:"deleteWordStartLeft",precondition:c.EditorContextKeys.writable})}}e.DeleteWordStartLeft=x;class K extends R{constructor(){super({whitespaceHeuristics:!1,wordNavigationType:2,id:"deleteWordEndLeft",precondition:c.EditorContextKeys.writable})}}e.DeleteWordEndLeft=K;class Y extends R{constructor(){super({whitespaceHeuristics:!0,wordNavigationType:0,id:"deleteWordLeft",precondition:c.EditorContextKeys.writable,kbOpts:{kbExpr:c.EditorContextKeys.textInputFocus,primary:2048|1,mac:{primary:512|1},weight:100}})}}e.DeleteWordLeft=Y;class ee extends W{constructor(){super({whitespaceHeuristics:!1,wordNavigationType:0,id:"deleteWordStartRight",precondition:c.EditorContextKeys.writable})}}e.DeleteWordStartRight=ee;class se extends W{constructor(){super({whitespaceHeuristics:!1,wordNavigationType:2,id:"deleteWordEndRight",precondition:c.EditorContextKeys.writable})}}e.DeleteWordEndRight=se;class ne extends W{constructor(){super({whitespaceHeuristics:!0,wordNavigationType:2,id:"deleteWordRight",precondition:c.EditorContextKeys.writable,kbOpts:{kbExpr:c.EditorContextKeys.textInputFocus,primary:2048|20,mac:{primary:512|20},weight:100}})}}e.DeleteWordRight=ne;class le extends N.EditorAction{constructor(){super({id:"deleteInsideWord",precondition:c.EditorContextKeys.writable,label:b.localize(0,null),alias:"Delete Word"})}run(z,P,V){if(!!P.hasModel()){const U=C.getMapForWordSeparators(P.getOption(110)),H=P.getModel(),ie=P.getSelections().map(oe=>{const ae=S.WordOperations.deleteInsideWord(U,H,oe);return new M.ReplaceCommand(ae,"")});P.pushUndoStop(),P.executeCommands(this.id,ie),P.pushUndoStop()}}}e.DeleteInsideWord=le,N.registerEditorCommand(new l),N.registerEditorCommand(new h),N.registerEditorCommand(new m),N.registerEditorCommand(new _),N.registerEditorCommand(new f),N.registerEditorCommand(new v),N.registerEditorCommand(new I),N.registerEditorCommand(new k),N.registerEditorCommand(new E),N.registerEditorCommand(new T),N.registerEditorCommand(new O),N.registerEditorCommand(new A),N.registerEditorCommand(new y),N.registerEditorCommand(new L),N.registerEditorCommand(new B),N.registerEditorCommand(new F),N.registerEditorCommand(new x),N.registerEditorCommand(new K),N.registerEditorCommand(new Y),N.registerEditorCommand(new ee),N.registerEditorCommand(new se),N.registerEditorCommand(new ne),N.registerEditorAction(le)}),define(Q[641],J([0,1,13,136,3,25,265,26]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.CursorWordPartRightSelect=e.CursorWordPartRight=e.WordPartRightCommand=e.CursorWordPartLeftSelect=e.CursorWordPartLeft=e.WordPartLeftCommand=e.DeleteWordPartRight=e.DeleteWordPartLeft=void 0;class d extends S.DeleteWordCommand{constructor(){super({whitespaceHeuristics:!0,wordNavigationType:0,id:"deleteWordPartLeft",precondition:w.EditorContextKeys.writable,kbOpts:{kbExpr:w.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|512|1},weight:100}})}_delete(i,n){let t=N.WordPartOperations.deleteWordPartLeft(i);return t||new M.Range(1,1,1,1)}}e.DeleteWordPartLeft=d;class g extends S.DeleteWordCommand{constructor(){super({whitespaceHeuristics:!0,wordNavigationType:2,id:"deleteWordPartRight",precondition:w.EditorContextKeys.writable,kbOpts:{kbExpr:w.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|512|20},weight:100}})}_delete(i,n){let t=N.WordPartOperations.deleteWordPartRight(i);if(t)return t;const l=i.model.getLineCount(),h=i.model.getLineMaxColumn(l);return new M.Range(l,h,l,h)}}e.DeleteWordPartRight=g;class p extends S.MoveWordCommand{_move(i,n,t,l){return N.WordPartOperations.moveWordPartLeft(i,n,t)}}e.WordPartLeftCommand=p;class c extends p{constructor(){super({inSelectionMode:!1,wordNavigationType:0,id:"cursorWordPartLeft",precondition:void 0,kbOpts:{kbExpr:w.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|512|15},weight:100}})}}e.CursorWordPartLeft=c,C.CommandsRegistry.registerCommandAlias("cursorWordPartStartLeft","cursorWordPartLeft");class o extends p{constructor(){super({inSelectionMode:!0,wordNavigationType:0,id:"cursorWordPartLeftSelect",precondition:void 0,kbOpts:{kbExpr:w.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|512|1024|15},weight:100}})}}e.CursorWordPartLeftSelect=o,C.CommandsRegistry.registerCommandAlias("cursorWordPartStartLeftSelect","cursorWordPartLeftSelect");class s extends S.MoveWordCommand{_move(i,n,t,l){return N.WordPartOperations.moveWordPartRight(i,n,t)}}e.WordPartRightCommand=s;class a extends s{constructor(){super({inSelectionMode:!1,wordNavigationType:2,id:"cursorWordPartRight",precondition:void 0,kbOpts:{kbExpr:w.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|512|17},weight:100}})}}e.CursorWordPartRight=a;class u extends s{constructor(){super({inSelectionMode:!0,wordNavigationType:2,id:"cursorWordPartRightSelect",precondition:void 0,kbOpts:{kbExpr:w.EditorContextKeys.textInputFocus,primary:0,mac:{primary:256|512|1024|17},weight:100}})}}e.CursorWordPartRightSelect=u,b.registerEditorCommand(new d),b.registerEditorCommand(new g),b.registerEditorCommand(new c),b.registerEditorCommand(new o),b.registerEditorCommand(new a),b.registerEditorCommand(new u)}),define(Q[642],J([0,1,7,30,156,47,52,2,17,8,24,13,25,264,16,9,37,58,22,11,64,354]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l){"use strict";Object.defineProperty(e,"__esModule",{value:!0});const h=new a.RawContextKey("accessibilityHelpWidgetVisible",!1);let m=class Ht extends C.Disposable{constructor(I,k){super();this._editor=I,this._widget=this._register(k.createInstance(f,this._editor))}static get(I){return I.getContribution(Ht.ID)}show(){this._widget.show()}hide(){this._widget.hide()}};m.ID="editor.contrib.accessibilityHelpController",m=Me([_e(1,u.IInstantiationService)],m);function _(L,I){return!L||L.length===0?l.AccessibilityHelpNLS.noSelection:L.length===1?I?g.format(l.AccessibilityHelpNLS.singleSelectionRange,L[0].positionLineNumber,L[0].positionColumn,I):g.format(l.AccessibilityHelpNLS.singleSelection,L[0].positionLineNumber,L[0].positionColumn):I?g.format(l.AccessibilityHelpNLS.multiSelectionRange,L.length,I):L.length>0?g.format(l.AccessibilityHelpNLS.multiSelection,L.length):""}let f=class ht extends S.Widget{constructor(I,k,E,T){super();this._contextKeyService=k,this._keybindingService=E,this._openerService=T,this._editor=I,this._isVisibleKey=h.bindTo(this._contextKeyService),this._domNode=N.createFastDomNode(document.createElement("div")),this._domNode.setClassName("accessibilityHelpWidget"),this._domNode.setDisplay("none"),this._domNode.setAttribute("role","dialog"),this._domNode.setAttribute("aria-hidden","true"),this._contentDomNode=N.createFastDomNode(document.createElement("div")),this._contentDomNode.setAttribute("role","document"),this._domNode.appendChild(this._contentDomNode),this._isVisible=!1,this._register(this._editor.onDidLayoutChange(()=>{this._isVisible&&this._layout()})),this._register(b.addStandardDisposableListener(this._contentDomNode.domNode,"keydown",O=>{if(!!this._isVisible&&(O.equals(2048|35)&&(w.alert(l.AccessibilityHelpNLS.emergencyConfOn),this._editor.updateOptions({accessibilitySupport:"on"}),b.clearNode(this._contentDomNode.domNode),this._buildContent(),this._contentDomNode.domNode.focus(),O.preventDefault(),O.stopPropagation()),O.equals(2048|38))){w.alert(l.AccessibilityHelpNLS.openingDocs);let A=this._editor.getRawOptions().accessibilityHelpUrl;typeof A=="undefined"&&(A="https://go.microsoft.com/fwlink/?linkid=852450"),this._openerService.open(p.URI.parse(A)),O.preventDefault(),O.stopPropagation()}})),this.onblur(this._contentDomNode.domNode,()=>{this.hide()}),this._editor.addOverlayWidget(this)}dispose(){this._editor.removeOverlayWidget(this),super.dispose()}getId(){return ht.ID}getDomNode(){return this._domNode.domNode}getPosition(){return{preference:null}}show(){this._isVisible||(this._isVisible=!0,this._isVisibleKey.set(!0),this._layout(),this._domNode.setDisplay("block"),this._domNode.setAttribute("aria-hidden","false"),this._contentDomNode.domNode.tabIndex=0,this._buildContent(),this._contentDomNode.domNode.focus())}_descriptionForCommand(I,k,E){let T=this._keybindingService.lookupKeybinding(I);return T?g.format(k,T.getAriaLabel()):g.format(E,I)}_buildContent(){const I=this._editor.getOptions(),k=this._editor.getSelections();let E=0;if(k){const B=this._editor.getModel();B&&k.forEach(F=>{E+=B.getValueLengthInRange(F)})}let T=_(k,E);I.get(49)?I.get(75)?T+=l.AccessibilityHelpNLS.readonlyDiffEditor:T+=l.AccessibilityHelpNLS.editableDiffEditor:I.get(75)?T+=l.AccessibilityHelpNLS.readonlyEditor:T+=l.AccessibilityHelpNLS.editableEditor;const O=d.isMacintosh?l.AccessibilityHelpNLS.changeConfigToOnMac:l.AccessibilityHelpNLS.changeConfigToOnWinLinux;switch(I.get(2)){case 0:T+=` + + - `+O;break;case 2:T+=` + + - `+l.AccessibilityHelpNLS.auto_on;break;case 1:T+=` + + - `+l.AccessibilityHelpNLS.auto_off,T+=" "+O;break}I.get(123)?T+=` + + - `+this._descriptionForCommand(s.ToggleTabFocusModeAction.ID,l.AccessibilityHelpNLS.tabFocusModeOnMsg,l.AccessibilityHelpNLS.tabFocusModeOnMsgNoKb):T+=` + + - `+this._descriptionForCommand(s.ToggleTabFocusModeAction.ID,l.AccessibilityHelpNLS.tabFocusModeOffMsg,l.AccessibilityHelpNLS.tabFocusModeOffMsgNoKb);const A=d.isMacintosh?l.AccessibilityHelpNLS.openDocMac:l.AccessibilityHelpNLS.openDocWinLinux;T+=` + + - `+A,T+=` + +`+l.AccessibilityHelpNLS.outroMsg,this._contentDomNode.domNode.appendChild(M.renderFormattedText(T)),this._contentDomNode.domNode.setAttribute("aria-label",T)}hide(){!this._isVisible||(this._isVisible=!1,this._isVisibleKey.reset(),this._domNode.setDisplay("none"),this._domNode.setAttribute("aria-hidden","true"),this._contentDomNode.domNode.tabIndex=-1,b.clearNode(this._contentDomNode.domNode),this._editor.focus())}_layout(){let I=this._editor.getLayoutInfo(),k=Math.max(5,Math.min(ht.WIDTH,I.width-40)),E=Math.max(5,Math.min(ht.HEIGHT,I.height-40));this._domNode.setWidth(k),this._domNode.setHeight(E);let T=Math.round((I.height-E)/2);this._domNode.setTop(T);let O=Math.round((I.width-k)/2);this._domNode.setLeft(O)}};f.ID="editor.contrib.accessibilityHelpWidget",f.WIDTH=500,f.HEIGHT=300,f=Me([_e(1,a.IContextKeyService),_e(2,r.IKeybindingService),_e(3,i.IOpenerService)],f);class v extends c.EditorAction{constructor(){super({id:"editor.action.showAccessibilityHelp",label:l.AccessibilityHelpNLS.showAccessibilityHelpAction,alias:"Show Accessibility Help",precondition:void 0,kbOpts:{primary:512|59,weight:100,linux:{primary:512|1024|59,secondary:[512|59]}}})}run(I,k){let E=m.get(k);E&&E.show()}}c.registerEditorContribution(m.ID,m),c.registerEditorAction(v);const y=c.EditorCommand.bindToContribution(m.get);c.registerEditorCommand(new y({id:"closeAccessibilityHelp",precondition:h,handler:L=>L.hide(),kbOpts:{weight:100+100,kbExpr:o.EditorContextKeys.focus,primary:9,secondary:[1024|9]}})),t.registerThemingParticipant((L,I)=>{const k=L.getColor(n.editorWidgetBackground);k&&I.addRule(`.monaco-editor .accessibilityHelpWidget { background-color: ${k}; }`);const E=L.getColor(n.editorWidgetForeground);E&&I.addRule(`.monaco-editor .accessibilityHelpWidget { color: ${E}; }`);const T=L.getColor(n.widgetShadow);T&&I.addRule(`.monaco-editor .accessibilityHelpWidget { box-shadow: 0 2px 8px ${T}; }`);const O=L.getColor(n.contrastBorder);O&&I.addRule(`.monaco-editor .accessibilityHelpWidget { border: 2px solid ${O}; }`)})}),define(Q[643],J([0,1,35,7,2,13,355]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.IPadShowKeyboard=void 0;class S extends M.Disposable{constructor(g){super();this.editor=g,this.widget=null,b.isIPad&&(this._register(g.onDidChangeConfiguration(()=>this.update())),this.update())}update(){const g=!this.editor.getOption(75);!this.widget&&g?this.widget=new C(this.editor):this.widget&&!g&&(this.widget.dispose(),this.widget=null)}dispose(){super.dispose(),this.widget&&(this.widget.dispose(),this.widget=null)}}e.IPadShowKeyboard=S,S.ID="editor.contrib.iPadShowKeyboard";class C extends M.Disposable{constructor(g){super();this.editor=g,this._domNode=document.createElement("textarea"),this._domNode.className="iPadShowKeyboard",this._register(N.addDisposableListener(this._domNode,"touchstart",p=>{this.editor.focus()})),this._register(N.addDisposableListener(this._domNode,"focus",p=>{this.editor.focus()})),this.editor.addOverlayWidget(this)}dispose(){this.editor.removeOverlayWidget(this),super.dispose()}getId(){return C.ID}getDomNode(){return this._domNode}getPosition(){return{preference:1}}}C.ID="editor.contrib.ShowKeyboardWidget",w.registerEditorContribution(S.ID,S)}),define(Q[644],J([0,1,7,29,2,13,18,76,57,114,22,11,64,97,356]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0});let a=class Ut extends M.Disposable{constructor(l,h,m){super();this._editor=l,this._modeService=m,this._widget=null,this._register(this._editor.onDidChangeModel(_=>this.stop())),this._register(this._editor.onDidChangeModelLanguage(_=>this.stop())),this._register(S.TokenizationRegistry.onDidChange(_=>this.stop())),this._register(this._editor.onKeyUp(_=>_.keyCode===9&&this.stop()))}static get(l){return l.getContribution(Ut.ID)}dispose(){this.stop(),super.dispose()}launch(){this._widget||!this._editor.hasModel()||(this._widget=new n(this._editor,this._modeService))}stop(){this._widget&&(this._widget.dispose(),this._widget=null)}};a.ID="editor.contrib.inspectTokens",a=Me([_e(1,g.IStandaloneThemeService),_e(2,d.IModeService)],a);class u extends w.EditorAction{constructor(){super({id:"editor.action.inspectTokens",label:o.InspectTokensNLS.inspectTokensAction,alias:"Developer: Inspect Tokens",precondition:void 0})}run(l,h){let m=a.get(h);m&&m.launch()}}function r(t){let l="";for(let h=0,m=t.length;hC.NULL_STATE,tokenize:(h,m,_,f)=>C.nullTokenize(t.language,h,_,f),tokenize2:(h,m,_,f)=>C.nullTokenize2(t.id,h,_,f)}}class n extends M.Disposable{constructor(l,h){super();this.allowEditorOverflow=!0,this._editor=l,this._modeService=h,this._model=this._editor.getModel(),this._domNode=document.createElement("div"),this._domNode.className="tokens-inspect-widget",this._tokenizationSupport=i(this._model.getLanguageIdentifier()),this._compute(this._editor.getPosition()),this._register(this._editor.onDidChangeCursorPosition(m=>this._compute(this._editor.getPosition()))),this._editor.addContentWidget(this)}dispose(){this._editor.removeContentWidget(this),super.dispose()}getId(){return n._ID}_compute(l){let h=this._getTokensAtLine(l.lineNumber),m=0;for(let L=h.tokens1.length-1;L>=0;L--){let I=h.tokens1[L];if(l.column-1>=I.offset){m=L;break}}let _=0;for(let L=h.tokens2.length>>>1;L>=0;L--)if(l.column-1>=h.tokens2[L<<1]){_=L;break}let f=this._model.getLineContent(l.lineNumber),v="";if(m{const h=t.getColor(p.editorHoverBorder);if(h){let f=t.type===s.ColorScheme.HIGH_CONTRAST?2:1;l.addRule(`.monaco-editor .tokens-inspect-widget { border: ${f}px solid ${h}; }`),l.addRule(`.monaco-editor .tokens-inspect-widget .tokens-inspect-separator { background-color: ${h}; }`)}const m=t.getColor(p.editorHoverBackground);m&&l.addRule(`.monaco-editor .tokens-inspect-widget { background-color: ${m}; }`);const _=t.getColor(p.editorHoverForeground);_&&l.addRule(`.monaco-editor .tokens-inspect-widget { color: ${_}; }`)})}),define(Q[645],J([0,1,33,96,64,28,578,20,9,37,26,87,32,13,25,78]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.GotoLineAction=e.StandaloneCommandsQuickAccessProvider=void 0;let r=class extends S.AbstractEditorCommandsQuickAccessProvider{constructor(t,l,h,m,_,f){super({showAlias:!1},t,h,m,_,f);this.codeEditorService=l}get activeTextEditorControl(){return C.withNullAsUndefined(this.codeEditorService.getFocusedCodeEditor())}getCommandPicks(){return Ie(this,void 0,void 0,function*(){return this.getCodeEditorCommandPicks()})}};r=Me([_e(0,d.IInstantiationService),_e(1,w.ICodeEditorService),_e(2,g.IKeybindingService),_e(3,p.ICommandService),_e(4,c.ITelemetryService),_e(5,o.INotificationService)],r),e.StandaloneCommandsQuickAccessProvider=r,b.Registry.as(N.Extensions.Quickaccess).registerQuickAccessProvider({ctor:r,prefix:r.PREFIX,helpEntries:[{description:M.QuickCommandNLS.quickCommandHelp,needsEditor:!0}]});class i extends s.EditorAction{constructor(){super({id:"editor.action.quickCommand",label:M.QuickCommandNLS.quickCommandActionLabel,alias:"Command Palette",precondition:void 0,kbOpts:{kbExpr:a.EditorContextKeys.focus,primary:59,weight:100},contextMenuOpts:{group:"z_commands",order:1}})}run(t){t.get(u.IQuickInputService).quickAccess.show(r.PREFIX)}}e.GotoLineAction=i,s.registerEditorAction(i)}),define(Q[646],J([0,1,602,33,96,28,20,64,6,13,25,78]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.GotoLineAction=e.StandaloneGotoLineQuickAccessProvider=void 0;let o=class extends b.AbstractGotoLineQuickAccessProvider{constructor(u){super();this.editorService=u,this.onDidActiveTextEditorControlChange=d.Event.None}get activeTextEditorControl(){return S.withNullAsUndefined(this.editorService.getFocusedCodeEditor())}};o=Me([_e(0,w.ICodeEditorService)],o),e.StandaloneGotoLineQuickAccessProvider=o,N.Registry.as(M.Extensions.Quickaccess).registerQuickAccessProvider({ctor:o,prefix:o.PREFIX,helpEntries:[{description:C.GoToLineNLS.gotoLineActionLabel,needsEditor:!0}]});class s extends g.EditorAction{constructor(){super({id:"editor.action.gotoLine",label:C.GoToLineNLS.gotoLineActionLabel,alias:"Go to Line/Column...",precondition:void 0,kbOpts:{kbExpr:p.EditorContextKeys.focus,primary:2048|37,mac:{primary:256|37},weight:100}})}run(u){u.get(c.IQuickInputService).quickAccess.show(o.PREFIX)}}e.GotoLineAction=s,g.registerEditorAction(s)}),define(Q[647],J([0,1,603,33,96,28,20,64,6,13,25,78,123,259]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.GotoLineAction=e.StandaloneGotoSymbolQuickAccessProvider=void 0;let o=class extends b.AbstractGotoSymbolQuickAccessProvider{constructor(u){super();this.editorService=u,this.onDidActiveTextEditorControlChange=d.Event.None}get activeTextEditorControl(){return S.withNullAsUndefined(this.editorService.getFocusedCodeEditor())}};o=Me([_e(0,w.ICodeEditorService)],o),e.StandaloneGotoSymbolQuickAccessProvider=o,N.Registry.as(M.Extensions.Quickaccess).registerQuickAccessProvider({ctor:o,prefix:b.AbstractGotoSymbolQuickAccessProvider.PREFIX,helpEntries:[{description:C.QuickOutlineNLS.quickOutlineActionLabel,prefix:b.AbstractGotoSymbolQuickAccessProvider.PREFIX,needsEditor:!0},{description:C.QuickOutlineNLS.quickOutlineByCategoryActionLabel,prefix:b.AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY,needsEditor:!0}]});class s extends g.EditorAction{constructor(){super({id:"editor.action.quickOutline",label:C.QuickOutlineNLS.quickOutlineActionLabel,alias:"Go to Symbol...",precondition:p.EditorContextKeys.hasDocumentSymbolProvider,kbOpts:{kbExpr:p.EditorContextKeys.focus,primary:2048|1024|45,weight:100},contextMenuOpts:{group:"navigation",order:3}})}run(u){u.get(c.IQuickInputService).quickAccess.show(b.AbstractGotoSymbolQuickAccessProvider.PREFIX)}}e.GotoLineAction=s,g.registerEditorAction(s)}),define(Q[648],J([0,1,13,114,64]),function(q,e,b,N,M){"use strict";Object.defineProperty(e,"__esModule",{value:!0});class w extends b.EditorAction{constructor(){super({id:"editor.action.toggleHighContrast",label:M.ToggleHighContrastNLS.toggleHighContrast,alias:"Toggle High Contrast Theme",precondition:void 0});this._originalThemeName=null}run(C,d){const g=C.get(N.IStandaloneThemeService);this._originalThemeName?(g.setTheme(this._originalThemeName),this._originalThemeName=null):(this._originalThemeName=g.getColorTheme().themeName,g.setTheme("hc-black"))}}b.registerEditorAction(w)}),define(Q[189],J([0,1,7,55,48,2,518,34,68,37,32,11,112,431,17,359]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createActionViewItem=e.SubmenuEntryActionViewItem=e.MenuEntryActionViewItem=e.createAndFillInActionBarActions=void 0;function u(h,m,_,f,v,y){const L=h.getActions(m);return i(L,_,!1,f,v,y),r(L)}e.createAndFillInActionBarActions=u;function r(h){const m=new w.DisposableStore;for(const[,_]of h)for(const f of _)m.add(f);return m}function i(h,m,_,f=L=>L==="navigation",v=Number.MAX_SAFE_INTEGER,y=()=>!1){let L,I;Array.isArray(m)?(L=m,I=m):(L=m.primary,I=m.secondary);const k=new Set;for(const[E,T]of h){let O;f(E)?O=L:(O=I,O.length>0&&O.push(new M.Separator));for(let A of T){_&&(A=A instanceof C.MenuItemAction&&A.alt?A.alt:A);const B=O.push(A);A instanceof M.SubmenuAction&&k.add({group:E,action:A,index:B-1})}}for(const{group:E,action:T,index:O}of k){const A=f(E)?L:I,B=T.actions;(B.length<=1||A.length+B.length-2<=v)&&y(T,E,A.length)&&A.splice(O,1,...B)}if(L!==I&&L.length>v){const E=L.splice(v,L.length-v);I.unshift(...E,new M.Separator)}}let n=class extends o.ActionViewItem{constructor(m,_,f){super(void 0,m,{icon:!!(m.class||m.item.icon),label:!m.class&&!m.item.icon});this._action=m,this._keybindingService=_,this._notificationService=f,this._wantsAltCommand=!1,this._itemClassDispose=this._register(new w.MutableDisposable),this._altKey=b.ModifierKeyEmitter.getInstance()}get _commandAction(){return this._wantsAltCommand&&this._action.alt||this._action}onClick(m){m.preventDefault(),m.stopPropagation(),this.actionRunner.run(this._commandAction,this._context).catch(_=>this._notificationService.error(_))}render(m){super.render(m),m.classList.add("menu-entry"),this._updateItemClass(this._action.item);let _=!1,f=this._altKey.keyStatus.altKey||(a.isWindows||a.isLinux)&&this._altKey.keyStatus.shiftKey;const v=()=>{const y=_&&f;y!==this._wantsAltCommand&&(this._wantsAltCommand=y,this.updateLabel(),this.updateTooltip(),this.updateClass())};this._action.alt&&this._register(this._altKey.event(y=>{f=y.altKey||(a.isWindows||a.isLinux)&&y.shiftKey,v()})),this._register(N.domEvent(m,"mouseleave")(y=>{_=!1,v()})),this._register(N.domEvent(m,"mouseenter")(y=>{_=!0,v()}))}updateLabel(){this.options.label&&this.label&&(this.label.textContent=this._commandAction.label)}updateTooltip(){if(this.label){const m=this._keybindingService.lookupKeybinding(this._commandAction.id),_=m&&m.getLabel(),f=this._commandAction.tooltip||this._commandAction.label;this.label.title=_?S.localize(0,null,f,_):f}}updateClass(){this.options.icon&&(this._commandAction!==this._action?this._action.alt&&this._updateItemClass(this._action.alt.item):this._action.alt&&this._updateItemClass(this._action.item))}_updateItemClass(m){var _;this._itemClassDispose.value=void 0;const{element:f,label:v}=this;if(!(!f||!v)){const y=this._commandAction.checked&&((_=m.toggled)===null||_===void 0?void 0:_.icon)?m.toggled.icon:m.icon;if(!!y)if(c.ThemeIcon.isThemeIcon(y)){const L=c.ThemeIcon.asClassName(y);v.classList.add(...L.split(" ")),this._itemClassDispose.value=w.toDisposable(()=>{v.classList.remove(...L.split(" "))})}else y.light&&v.style.setProperty("--menu-entry-icon-light",b.asCSSUrl(y.light)),y.dark&&v.style.setProperty("--menu-entry-icon-dark",b.asCSSUrl(y.dark)),v.classList.add("icon"),this._itemClassDispose.value=w.toDisposable(()=>{v.classList.remove("icon"),v.style.removeProperty("--menu-entry-icon-light"),v.style.removeProperty("--menu-entry-icon-dark")})}}};n=Me([_e(1,g.IKeybindingService),_e(2,p.INotificationService)],n),e.MenuEntryActionViewItem=n;let t=class extends s.DropdownMenuActionViewItem{constructor(m,_){super(m,{getActions:()=>m.actions},_,{menuAsChild:!0,classNames:c.ThemeIcon.isThemeIcon(m.item.icon)?c.ThemeIcon.asClassName(m.item.icon):void 0})}render(m){if(super.render(m),this.element){m.classList.add("menu-entry");const{icon:_}=this._action.item;_&&!c.ThemeIcon.isThemeIcon(_)&&(this.element.classList.add("icon"),_.light&&this.element.style.setProperty("--menu-entry-icon-light",b.asCSSUrl(_.light)),_.dark&&this.element.style.setProperty("--menu-entry-icon-dark",b.asCSSUrl(_.dark)))}}};t=Me([_e(1,d.IContextMenuService)],t),e.SubmenuEntryActionViewItem=t;function l(h,m){return m instanceof C.MenuItemAction?h.createInstance(n,m):m instanceof C.SubmenuItemAction?h.createInstance(t,m):void 0}e.createActionViewItem=l}),define(Q[118],J([0,1,7,83,48,29,6,40,28,144,543,500,16,9,74,13,22,27,189,349]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.peekViewEditorMatchHighlightBorder=e.peekViewEditorMatchHighlight=e.peekViewResultsMatchHighlight=e.peekViewEditorGutterBackground=e.peekViewEditorBackground=e.peekViewResultsSelectionForeground=e.peekViewResultsSelectionBackground=e.peekViewResultsFileForeground=e.peekViewResultsMatchForeground=e.peekViewResultsBackground=e.peekViewBorder=e.peekViewTitleInfoForeground=e.peekViewTitleForeground=e.peekViewTitleBackground=e.PeekViewWidget=e.getOuterEditor=e.PeekContext=e.IPeekViewService=void 0,e.IPeekViewService=s.createDecorator("IPeekViewService"),a.registerSingleton(e.IPeekViewService,class{constructor(){this._widgets=new Map}addExclusiveWidget(f,v){const y=this._widgets.get(f);y&&(y.listener.dispose(),y.widget.dispose());const L=()=>{const I=this._widgets.get(f);I&&I.widget===v&&(I.listener.dispose(),this._widgets.delete(f))};this._widgets.set(f,{widget:v,listener:v.onDidClose(L)})}});var t;(function(f){f.inPeekEditor=new o.RawContextKey("inReferenceSearchEditor",!0),f.notInPeekEditor=f.inPeekEditor.toNegated()})(t=e.PeekContext||(e.PeekContext={}));let l=class{constructor(v,y){v instanceof g.EmbeddedCodeEditorWidget&&t.inPeekEditor.bindTo(y)}dispose(){}};l.ID="editor.contrib.referenceController",l=Me([_e(1,o.IContextKeyService)],l),u.registerEditorContribution(l.ID,l);function h(f){let v=f.get(d.ICodeEditorService).getFocusedCodeEditor();return v instanceof g.EmbeddedCodeEditorWidget?v.getParentEditor():v}e.getOuterEditor=h;const m={headerBackgroundColor:w.Color.white,primaryHeadingColor:w.Color.fromHex("#333333"),secondaryHeadingColor:w.Color.fromHex("#6c6c6cb3")};let _=class extends p.ZoneWidget{constructor(v,y,L){super(v,y);this.instantiationService=L,this._onDidClose=new S.Emitter,this.onDidClose=this._onDidClose.event,C.mixin(this.options,m,!1)}dispose(){this.disposed||(this.disposed=!0,super.dispose(),this._onDidClose.fire(this))}style(v){let y=this.options;v.headerBackgroundColor&&(y.headerBackgroundColor=v.headerBackgroundColor),v.primaryHeadingColor&&(y.primaryHeadingColor=v.primaryHeadingColor),v.secondaryHeadingColor&&(y.secondaryHeadingColor=v.secondaryHeadingColor),super.style(v)}_applyStyles(){super._applyStyles();let v=this.options;this._headElement&&v.headerBackgroundColor&&(this._headElement.style.backgroundColor=v.headerBackgroundColor.toString()),this._primaryHeading&&v.primaryHeadingColor&&(this._primaryHeading.style.color=v.primaryHeadingColor.toString()),this._secondaryHeading&&v.secondaryHeadingColor&&(this._secondaryHeading.style.color=v.secondaryHeadingColor.toString()),this._bodyElement&&v.frameColor&&(this._bodyElement.style.borderColor=v.frameColor.toString())}_fillContainer(v){this.setCssClass("peekview-widget"),this._headElement=b.$(".head"),this._bodyElement=b.$(".body"),this._fillHead(this._headElement),this._fillBody(this._bodyElement),v.appendChild(this._headElement),v.appendChild(this._bodyElement)}_fillHead(v,y){const L=b.$(".peekview-title");b.append(this._headElement,L),b.addStandardDisposableListener(L,"click",E=>this._onTitleClick(E)),this._fillTitleIcon(L),this._primaryHeading=b.$("span.filename"),this._secondaryHeading=b.$("span.dirname"),this._metaHeading=b.$("span.meta"),b.append(L,this._primaryHeading,this._secondaryHeading,this._metaHeading);const I=b.$(".peekview-actions");b.append(this._headElement,I);const k=this._getActionBarOptions();this._actionbarWidget=new N.ActionBar(I,k),this._disposables.add(this._actionbarWidget),y||this._actionbarWidget.push(new M.Action("peekview.close",c.localize(0,null),i.Codicon.close.classNames,!0,()=>(this.dispose(),Promise.resolve())),{label:!1,icon:!0})}_fillTitleIcon(v){}_getActionBarOptions(){return{actionViewItemProvider:n.createActionViewItem.bind(void 0,this.instantiationService),orientation:0}}_onTitleClick(v){}setTitle(v,y){this._primaryHeading&&this._secondaryHeading&&(this._primaryHeading.innerText=v,this._primaryHeading.setAttribute("aria-label",v),y?this._secondaryHeading.innerText=y:b.clearNode(this._secondaryHeading))}setMetaTitle(v){this._metaHeading&&(v?(this._metaHeading.innerText=v,b.show(this._metaHeading)):b.hide(this._metaHeading))}_doLayout(v,y){if(!this._isShowing&&v<0){this.dispose();return}const L=Math.ceil(this.editor.getOption(53)*1.2),I=Math.round(v-(L+2));this._doLayoutHead(L,y),this._doLayoutBody(I,y)}_doLayoutHead(v,y){this._headElement&&(this._headElement.style.height=`${v}px`,this._headElement.style.lineHeight=this._headElement.style.height)}_doLayoutBody(v,y){this._bodyElement&&(this._bodyElement.style.height=`${v}px`)}};_=Me([_e(2,s.IInstantiationService)],_),e.PeekViewWidget=_,e.peekViewTitleBackground=r.registerColor("peekViewTitle.background",{dark:"#1E1E1E",light:"#FFFFFF",hc:"#0C141F"},c.localize(1,null)),e.peekViewTitleForeground=r.registerColor("peekViewTitleLabel.foreground",{dark:"#FFFFFF",light:"#333333",hc:"#FFFFFF"},c.localize(2,null)),e.peekViewTitleInfoForeground=r.registerColor("peekViewTitleDescription.foreground",{dark:"#ccccccb3",light:"#616161e6",hc:"#FFFFFF99"},c.localize(3,null)),e.peekViewBorder=r.registerColor("peekView.border",{dark:"#007acc",light:"#007acc",hc:r.contrastBorder},c.localize(4,null)),e.peekViewResultsBackground=r.registerColor("peekViewResult.background",{dark:"#252526",light:"#F3F3F3",hc:w.Color.black},c.localize(5,null)),e.peekViewResultsMatchForeground=r.registerColor("peekViewResult.lineForeground",{dark:"#bbbbbb",light:"#646465",hc:w.Color.white},c.localize(6,null)),e.peekViewResultsFileForeground=r.registerColor("peekViewResult.fileForeground",{dark:w.Color.white,light:"#1E1E1E",hc:w.Color.white},c.localize(7,null)),e.peekViewResultsSelectionBackground=r.registerColor("peekViewResult.selectionBackground",{dark:"#3399ff33",light:"#3399ff33",hc:null},c.localize(8,null)),e.peekViewResultsSelectionForeground=r.registerColor("peekViewResult.selectionForeground",{dark:w.Color.white,light:"#6C6C6C",hc:w.Color.white},c.localize(9,null)),e.peekViewEditorBackground=r.registerColor("peekViewEditor.background",{dark:"#001F33",light:"#F2F8FC",hc:w.Color.black},c.localize(10,null)),e.peekViewEditorGutterBackground=r.registerColor("peekViewEditorGutter.background",{dark:e.peekViewEditorBackground,light:e.peekViewEditorBackground,hc:e.peekViewEditorBackground},c.localize(11,null)),e.peekViewResultsMatchHighlight=r.registerColor("peekViewResult.matchHighlightBackground",{dark:"#ea5c004d",light:"#ea5c004d",hc:null},c.localize(12,null)),e.peekViewEditorMatchHighlight=r.registerColor("peekViewEditor.matchHighlightBackground",{dark:"#ff8f0099",light:"#f5d802de",hc:null},c.localize(13,null)),e.peekViewEditorMatchHighlightBorder=r.registerColor("peekViewEditor.matchHighlightBorder",{dark:null,light:null,hc:r.activeContrastBorder},c.localize(14,null))}),define(Q[649],J([0,1,7,83,2,117,511,189,34,16,9]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SuggestWidgetStatus=void 0;class c extends C.MenuEntryActionViewItem{updateLabel(){const a=this._keybindingService.lookupKeybinding(this._action.id);if(!a)return super.updateLabel();this.label&&(this.label.textContent=S.localize(0,null,this._action.label,c.symbolPrintEnter(a)))}static symbolPrintEnter(a){var u;return(u=a.getLabel())===null||u===void 0?void 0:u.replace(/\benter\b/gi,"\u23CE")}}let o=class{constructor(a,u,r,i){this._menuService=r,this._contextKeyService=i,this._menuDisposables=new M.DisposableStore,this.element=b.append(a,b.$(".suggest-status-bar"));const n=t=>t instanceof d.MenuItemAction?u.createInstance(c,t):void 0;this._leftActions=new N.ActionBar(this.element,{actionViewItemProvider:n}),this._rightActions=new N.ActionBar(this.element,{actionViewItemProvider:n}),this._leftActions.domNode.classList.add("left"),this._rightActions.domNode.classList.add("right")}dispose(){this._menuDisposables.dispose(),this.element.remove()}show(){const a=this._menuService.createMenu(w.suggestWidgetStatusbarMenu,this._contextKeyService),u=()=>{const r=[],i=[];for(let[n,t]of a.getActions())n==="left"?r.push(...t):i.push(...t);this._leftActions.clear(),this._leftActions.push(r),this._rightActions.clear(),this._rightActions.push(i)};this._menuDisposables.add(a.onDidChange(()=>u())),this._menuDisposables.add(a)}hide(){this._menuDisposables.clear()}};o=Me([_e(1,p.IInstantiationService),_e(2,d.IMenuService),_e(3,g.IContextKeyService)],o),e.SuggestWidgetStatus=o}),define(Q[650],J([0,1,15,6,2,34,26,16]),function(q,e,b,N,M,w,S,C){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MenuService=void 0;let d=class{constructor(c){this._commandService=c}createMenu(c,o,s=!1){return new g(c,s,this._commandService,o,this)}};d=Me([_e(0,S.ICommandService)],d),e.MenuService=d;let g=class et{constructor(c,o,s,a,u){this._id=c,this._fireEventsForSubmenuChanges=o,this._commandService=s,this._contextKeyService=a,this._menuService=u,this._dispoables=new M.DisposableStore,this._onDidChange=new N.Emitter,this.onDidChange=this._onDidChange.event,this._menuGroups=[],this._contextKeys=new Set,this._build();const r=new b.RunOnceScheduler(()=>this._build(),50);this._dispoables.add(r),this._dispoables.add(w.MenuRegistry.onDidChangeMenu(n=>{n.has(c)&&r.schedule()}));const i=new b.RunOnceScheduler(()=>this._onDidChange.fire(this),50);this._dispoables.add(i),this._dispoables.add(a.onDidChangeContext(n=>{n.affectsSome(this._contextKeys)&&i.schedule()}))}dispose(){this._dispoables.dispose(),this._onDidChange.dispose()}_build(){this._menuGroups.length=0,this._contextKeys.clear();const c=w.MenuRegistry.getMenuItems(this._id);let o;c.sort(et._compareMenuItems);for(let s of c){const a=s.group||"";(!o||o[0]!==a)&&(o=[a,[]],this._menuGroups.push(o)),o[1].push(s),this._collectContextKeys(s)}this._onDidChange.fire(this)}_collectContextKeys(c){if(et._fillInKbExprKeys(c.when,this._contextKeys),w.isIMenuItem(c)){if(c.command.precondition&&et._fillInKbExprKeys(c.command.precondition,this._contextKeys),c.command.toggled){const o=c.command.toggled.condition||c.command.toggled;et._fillInKbExprKeys(o,this._contextKeys)}}else this._fireEventsForSubmenuChanges&&w.MenuRegistry.getMenuItems(c.submenu).forEach(this._collectContextKeys,this)}getActions(c){const o=[];for(let s of this._menuGroups){const[a,u]=s,r=[];for(const i of u)if(this._contextKeyService.contextMatchesRules(i.when)){const n=w.isIMenuItem(i)?new w.MenuItemAction(i.command,i.alt,c,this._contextKeyService,this._commandService):new w.SubmenuItemAction(i,this._menuService,this._contextKeyService,c);r.push(n)}r.length>0&&o.push([a,r])}return o}static _fillInKbExprKeys(c,o){if(c)for(let s of c.keys())o.add(s)}static _compareMenuItems(c,o){let s=c.group,a=o.group;if(s!==a){if(s){if(!a)return-1}else return 1;if(s==="navigation")return-1;if(a==="navigation")return 1;let i=s.localeCompare(a);if(i!==0)return i}let u=c.order||0,r=o.order||0;return ur?1:et._compareTitles(w.isIMenuItem(c)?c.command.title:c.title,w.isIMenuItem(o)?o.command.title:o.title)}static _compareTitles(c,o){const s=typeof c=="string"?c:c.original,a=typeof o=="string"?o:o.original;return s.localeCompare(a)}};g=Me([_e(2,S.ICommandService),_e(3,C.IContextKeyService),_e(4,w.IMenuService)],g)}),define(Q[651],J([0,1,579,68,87,32,11,37,2,7]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ContextMenuService=void 0;let p=class extends d.Disposable{constructor(o,s,a,u,r){super();this.contextMenuHandler=new b.ContextMenuHandler(a,o,s,u,r)}configure(o){this.contextMenuHandler.configure(o)}showContextMenu(o){this.contextMenuHandler.showContextMenu(o),g.ModifierKeyEmitter.getInstance().resetKeyStatus()}};p=Me([_e(0,M.ITelemetryService),_e(1,w.INotificationService),_e(2,N.IContextViewService),_e(3,C.IKeybindingService),_e(4,S.IThemeService)],p),e.ContextMenuService=p}),define(Q[147],J([0,1,7,312,105,6,2,523,46,95,16,9,37,33,116,11,250,234,428,427,65,315]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.WorkbenchCompressibleAsyncDataTree=e.WorkbenchAsyncDataTree=e.WorkbenchDataTree=e.WorkbenchCompressibleObjectTree=e.WorkbenchObjectTree=e.WorkbenchTable=e.WorkbenchPagedList=e.WorkbenchList=e.didBindWorkbenchListAutomaticKeyboardNavigation=e.WorkbenchListAutomaticKeyboardNavigation=e.WorkbenchListAutomaticKeyboardNavigationKey=e.WorkbenchListSupportsKeyboardNavigation=e.WorkbenchListMultiSelection=e.WorkbenchListDoubleSelection=e.WorkbenchListHasSelectionOrFocus=e.WorkbenchListFocusContextKey=e.WorkbenchListSupportsMultiSelectContextKey=e.ListService=e.IListService=void 0,e.IListService=c.createDecorator("listService");let m=class{constructor(oe){this._themeService=oe,this.disposables=new S.DisposableStore,this.lists=[],this._lastFocusedWidget=void 0,this._hasCreatedStyleController=!1}get lastFocusedList(){return this._lastFocusedWidget}register(oe,ae){if(!this._hasCreatedStyleController){this._hasCreatedStyleController=!0;const j=new M.DefaultStyleController(b.createStyleSheet(),"");this.disposables.add(a.attachListStyler(j,this._themeService))}if(this.lists.some(j=>j.widget===oe))throw new Error("Cannot register the same widget multiple times");const G={widget:oe,extraContextKeys:ae};return this.lists.push(G),oe.getHTMLElement()===document.activeElement&&(this._lastFocusedWidget=oe),S.combinedDisposable(oe.onDidFocus(()=>this._lastFocusedWidget=oe),S.toDisposable(()=>this.lists.splice(this.lists.indexOf(G),1)),oe.onDidDispose(()=>{this.lists=this.lists.filter(j=>j!==G),this._lastFocusedWidget===oe&&(this._lastFocusedWidget=void 0)}))}dispose(){this.disposables.dispose()}};m=Me([_e(0,u.IThemeService)],m),e.ListService=m;const _=new p.RawContextKey("listFocus",!0);e.WorkbenchListSupportsMultiSelectContextKey=new p.RawContextKey("listSupportsMultiselect",!0),e.WorkbenchListFocusContextKey=p.ContextKeyExpr.and(_,p.ContextKeyExpr.not(r.InputFocusedContextKey)),e.WorkbenchListHasSelectionOrFocus=new p.RawContextKey("listHasSelectionOrFocus",!1),e.WorkbenchListDoubleSelection=new p.RawContextKey("listDoubleSelection",!1),e.WorkbenchListMultiSelection=new p.RawContextKey("listMultiSelection",!1),e.WorkbenchListSupportsKeyboardNavigation=new p.RawContextKey("listSupportsKeyboardNavigation",!0),e.WorkbenchListAutomaticKeyboardNavigationKey="listAutomaticKeyboardNavigation",e.WorkbenchListAutomaticKeyboardNavigation=new p.RawContextKey(e.WorkbenchListAutomaticKeyboardNavigationKey,!0),e.didBindWorkbenchListAutomaticKeyboardNavigation=!1;function f(ie,oe){const ae=ie.createScoped(oe.getHTMLElement());return _.bindTo(ae),ae}const v="workbench.list.multiSelectModifier",y="workbench.list.openMode",L="workbench.list.horizontalScrolling",I="workbench.list.keyboardNavigation",k="workbench.list.automaticKeyboardNavigation",E="workbench.tree.indent",T="workbench.tree.renderIndentGuides",O="workbench.list.smoothScrolling",A="workbench.tree.expandMode";function B(ie){return ie.getValue(v)==="alt"}class F extends S.Disposable{constructor(oe){super();this.configurationService=oe,this.useAltAsMultipleSelectionModifier=B(oe),this.registerListeners()}registerListeners(){this._register(this.configurationService.onDidChangeConfiguration(oe=>{oe.affectsConfiguration(v)&&(this.useAltAsMultipleSelectionModifier=B(this.configurationService))}))}isSelectionSingleChangeEvent(oe){return this.useAltAsMultipleSelectionModifier?oe.browserEvent.altKey:M.isSelectionSingleChangeEvent(oe)}isSelectionRangeChangeEvent(oe){return M.isSelectionRangeChangeEvent(oe)}}function D(ie,oe,ae){const G=new S.DisposableStore,j=Object.assign({},ie);if(ie.multipleSelectionSupport!==!1&&!ie.multipleSelectionController){const te=new F(oe);j.multipleSelectionController=te,G.add(te)}return j.keyboardNavigationDelegate={mightProducePrintableCharacter(te){return ae.mightProducePrintableCharacter(te)}},j.smoothScrolling=oe.getValue(O),[j,G]}let R=class extends M.List{constructor(oe,ae,G,j,te,Z,ue,he,re,ce){const me=typeof te.horizontalScrolling!="undefined"?te.horizontalScrolling:re.getValue(L),[Ce,be]=D(te,re,ce);super(oe,ae,G,j,Object.assign(Object.assign(Object.assign({keyboardSupport:!1},a.computeStyles(he.getColorTheme(),a.defaultListStyles)),Ce),{horizontalScrolling:me}));this.disposables.add(be),this.contextKeyService=f(Z,this),this.themeService=he,e.WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService).set(te.multipleSelectionSupport!==!1),this.listHasSelectionOrFocus=e.WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService),this.listDoubleSelection=e.WorkbenchListDoubleSelection.bindTo(this.contextKeyService),this.listMultiSelection=e.WorkbenchListMultiSelection.bindTo(this.contextKeyService),this.horizontalScrolling=te.horizontalScrolling,this._useAltAsMultipleSelectionModifier=B(re),this.disposables.add(this.contextKeyService),this.disposables.add(ue.register(this)),te.overrideStyles&&this.updateStyles(te.overrideStyles),this.disposables.add(this.onDidChangeSelection(()=>{const De=this.getSelection(),Re=this.getFocus();this.contextKeyService.bufferChangeEvents(()=>{this.listHasSelectionOrFocus.set(De.length>0||Re.length>0),this.listMultiSelection.set(De.length>1),this.listDoubleSelection.set(De.length===2)})})),this.disposables.add(this.onDidChangeFocus(()=>{const De=this.getSelection(),Re=this.getFocus();this.listHasSelectionOrFocus.set(De.length>0||Re.length>0)})),this.disposables.add(re.onDidChangeConfiguration(De=>{De.affectsConfiguration(v)&&(this._useAltAsMultipleSelectionModifier=B(re));let Re={};if(De.affectsConfiguration(L)&&this.horizontalScrolling===void 0){const Ee=re.getValue(L);Re=Object.assign(Object.assign({},Re),{horizontalScrolling:Ee})}if(De.affectsConfiguration(O)){const Ee=re.getValue(O);Re=Object.assign(Object.assign({},Re),{smoothScrolling:Ee})}Object.keys(Re).length>0&&this.updateOptions(Re)})),this.navigator=new Y(this,Object.assign({configurationService:re},te)),this.disposables.add(this.navigator)}updateOptions(oe){super.updateOptions(oe),oe.overrideStyles&&this.updateStyles(oe.overrideStyles)}updateStyles(oe){var ae;(ae=this._styler)===null||ae===void 0||ae.dispose(),this._styler=a.attachListStyler(this,this.themeService,oe)}dispose(){var oe;(oe=this._styler)===null||oe===void 0||oe.dispose(),super.dispose()}};R=Me([_e(5,p.IContextKeyService),_e(6,e.IListService),_e(7,u.IThemeService),_e(8,d.IConfigurationService),_e(9,o.IKeybindingService)],R),e.WorkbenchList=R;let W=class extends N.PagedList{constructor(oe,ae,G,j,te,Z,ue,he,re,ce){const me=typeof te.horizontalScrolling!="undefined"?te.horizontalScrolling:re.getValue(L),[Ce,be]=D(te,re,ce);super(oe,ae,G,j,Object.assign(Object.assign(Object.assign({keyboardSupport:!1},a.computeStyles(he.getColorTheme(),a.defaultListStyles)),Ce),{horizontalScrolling:me}));this.disposables=new S.DisposableStore,this.disposables.add(be),this.contextKeyService=f(Z,this),this.themeService=he,this.horizontalScrolling=te.horizontalScrolling,e.WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService).set(te.multipleSelectionSupport!==!1),this._useAltAsMultipleSelectionModifier=B(re),this.disposables.add(this.contextKeyService),this.disposables.add(ue.register(this)),te.overrideStyles&&this.updateStyles(te.overrideStyles),te.overrideStyles&&this.disposables.add(a.attachListStyler(this,he,te.overrideStyles)),this.disposables.add(re.onDidChangeConfiguration(De=>{De.affectsConfiguration(v)&&(this._useAltAsMultipleSelectionModifier=B(re));let Re={};if(De.affectsConfiguration(L)&&this.horizontalScrolling===void 0){const Ee=re.getValue(L);Re=Object.assign(Object.assign({},Re),{horizontalScrolling:Ee})}if(De.affectsConfiguration(O)){const Ee=re.getValue(O);Re=Object.assign(Object.assign({},Re),{smoothScrolling:Ee})}Object.keys(Re).length>0&&this.updateOptions(Re)})),this.navigator=new Y(this,Object.assign({configurationService:re},te)),this.disposables.add(this.navigator)}updateOptions(oe){super.updateOptions(oe),oe.overrideStyles&&this.updateStyles(oe.overrideStyles)}updateStyles(oe){var ae;(ae=this._styler)===null||ae===void 0||ae.dispose(),this._styler=a.attachListStyler(this,this.themeService,oe)}dispose(){var oe;(oe=this._styler)===null||oe===void 0||oe.dispose(),this.disposables.dispose(),super.dispose()}};W=Me([_e(5,p.IContextKeyService),_e(6,e.IListService),_e(7,u.IThemeService),_e(8,d.IConfigurationService),_e(9,o.IKeybindingService)],W),e.WorkbenchPagedList=W;let x=class extends h.Table{constructor(oe,ae,G,j,te,Z,ue,he,re,ce,me){const Ce=typeof Z.horizontalScrolling!="undefined"?Z.horizontalScrolling:ce.getValue(L),[be,Le]=D(Z,ce,me);super(oe,ae,G,j,te,Object.assign(Object.assign(Object.assign({keyboardSupport:!1},a.computeStyles(re.getColorTheme(),a.defaultListStyles)),be),{horizontalScrolling:Ce}));this.disposables=new S.DisposableStore,this.disposables.add(Le),this.contextKeyService=f(ue,this),this.themeService=re,e.WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService).set(Z.multipleSelectionSupport!==!1),this.listHasSelectionOrFocus=e.WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService),this.listDoubleSelection=e.WorkbenchListDoubleSelection.bindTo(this.contextKeyService),this.listMultiSelection=e.WorkbenchListMultiSelection.bindTo(this.contextKeyService),this.horizontalScrolling=Z.horizontalScrolling,this._useAltAsMultipleSelectionModifier=B(ce),this.disposables.add(this.contextKeyService),this.disposables.add(he.register(this)),Z.overrideStyles&&this.updateStyles(Z.overrideStyles),this.disposables.add(this.onDidChangeSelection(()=>{const Re=this.getSelection(),Ee=this.getFocus();this.contextKeyService.bufferChangeEvents(()=>{this.listHasSelectionOrFocus.set(Re.length>0||Ee.length>0),this.listMultiSelection.set(Re.length>1),this.listDoubleSelection.set(Re.length===2)})})),this.disposables.add(this.onDidChangeFocus(()=>{const Re=this.getSelection(),Ee=this.getFocus();this.listHasSelectionOrFocus.set(Re.length>0||Ee.length>0)})),this.disposables.add(ce.onDidChangeConfiguration(Re=>{Re.affectsConfiguration(v)&&(this._useAltAsMultipleSelectionModifier=B(ce));let Ee={};if(Re.affectsConfiguration(L)&&this.horizontalScrolling===void 0){const Ae=ce.getValue(L);Ee=Object.assign(Object.assign({},Ee),{horizontalScrolling:Ae})}if(Re.affectsConfiguration(O)){const Ae=ce.getValue(O);Ee=Object.assign(Object.assign({},Ee),{smoothScrolling:Ae})}Object.keys(Ee).length>0&&this.updateOptions(Ee)})),this.navigator=new ee(this,Object.assign({configurationService:ce},Z)),this.disposables.add(this.navigator)}updateOptions(oe){super.updateOptions(oe),oe.overrideStyles&&this.updateStyles(oe.overrideStyles)}updateStyles(oe){var ae;(ae=this._styler)===null||ae===void 0||ae.dispose(),this._styler=a.attachListStyler(this,this.themeService,oe)}dispose(){var oe;(oe=this._styler)===null||oe===void 0||oe.dispose(),this.disposables.dispose(),super.dispose()}};x=Me([_e(6,p.IContextKeyService),_e(7,e.IListService),_e(8,u.IThemeService),_e(9,d.IConfigurationService),_e(10,o.IKeybindingService)],x),e.WorkbenchTable=x;class K extends S.Disposable{constructor(oe,ae){var G,j;super();this.widget=oe,this._onDidOpen=this._register(new w.Emitter),this.onDidOpen=this._onDidOpen.event,this.openOnFocus=(G=ae==null?void 0:ae.openOnFocus)!==null&&G!==void 0?G:!1,this._register(w.Event.filter(this.widget.onDidChangeSelection,te=>te.browserEvent instanceof KeyboardEvent)(te=>this.onSelectionFromKeyboard(te))),this._register(this.widget.onPointer(te=>this.onPointer(te.element,te.browserEvent))),this._register(this.widget.onMouseDblClick(te=>this.onMouseDblClick(te.element,te.browserEvent))),this.openOnFocus&&this._register(w.Event.filter(this.widget.onDidChangeFocus,te=>te.browserEvent instanceof KeyboardEvent)(te=>this.onFocusFromKeyboard(te))),typeof(ae==null?void 0:ae.openOnSingleClick)!="boolean"&&(ae==null?void 0:ae.configurationService)?(this.openOnSingleClick=(ae==null?void 0:ae.configurationService.getValue(y))!=="doubleClick",this._register(ae==null?void 0:ae.configurationService.onDidChangeConfiguration(()=>{this.openOnSingleClick=(ae==null?void 0:ae.configurationService.getValue(y))!=="doubleClick"}))):this.openOnSingleClick=(j=ae==null?void 0:ae.openOnSingleClick)!==null&&j!==void 0?j:!0}onFocusFromKeyboard(oe){const ae=this.widget.getFocus();this.widget.setSelection(ae,oe.browserEvent);const G=oe.browserEvent,j=typeof G.preserveFocus=="boolean"?G.preserveFocus:!0,te=typeof G.pinned=="boolean"?G.pinned:!j,Z=!1;this._open(this.getSelectedElement(),j,te,Z,oe.browserEvent)}onSelectionFromKeyboard(oe){if(oe.elements.length===1){const ae=oe.browserEvent,G=typeof ae.preserveFocus=="boolean"?ae.preserveFocus:!0,j=typeof ae.pinned=="boolean"?ae.pinned:!G,te=!1;this._open(this.getSelectedElement(),G,j,te,oe.browserEvent)}}onPointer(oe,ae){if(!!this.openOnSingleClick&&ae.detail!==2){const j=ae.button===1,te=!0,Z=j,ue=ae.ctrlKey||ae.metaKey||ae.altKey;this._open(oe,te,Z,ue,ae)}}onMouseDblClick(oe,ae){if(!!ae){const G=!1,j=!0,te=ae.ctrlKey||ae.metaKey||ae.altKey;this._open(oe,G,j,te,ae)}}_open(oe,ae,G,j,te){!oe||this._onDidOpen.fire({editorOptions:{preserveFocus:ae,pinned:G,revealIfVisible:!0},sideBySide:j,element:oe,browserEvent:te})}}class Y extends K{constructor(oe,ae){super(oe,ae);this.widget=oe}getSelectedElement(){return this.widget.getSelectedElements()[0]}}class ee extends K{constructor(oe,ae){super(oe,ae);this.widget=oe}getSelectedElement(){return this.widget.getSelectedElements()[0]}}class se extends K{constructor(oe,ae){super(oe,ae);this.widget=oe}getSelectedElement(){var oe;return(oe=this.widget.getSelection()[0])!==null&&oe!==void 0?oe:void 0}}function ne(ie,oe){let ae=!1;return G=>{if(ae)return ae=!1,!1;const j=oe.softDispatch(G,ie);return j&&j.enterChord?(ae=!0,!1):(ae=!1,!0)}}let le=class extends i.ObjectTree{constructor(oe,ae,G,j,te,Z,ue,he,re,ce,me){const{options:Ce,getAutomaticKeyboardNavigation:be,disposable:Le}=U(ae,te,Z,re,ce,me);super(oe,ae,G,j,Ce);this.disposables.add(Le),this.internals=new H(this,te,be,te.overrideStyles,Z,ue,he,re,me),this.disposables.add(this.internals)}};le=Me([_e(5,p.IContextKeyService),_e(6,e.IListService),_e(7,u.IThemeService),_e(8,d.IConfigurationService),_e(9,o.IKeybindingService),_e(10,l.IAccessibilityService)],le),e.WorkbenchObjectTree=le;let X=class extends i.CompressibleObjectTree{constructor(oe,ae,G,j,te,Z,ue,he,re,ce,me){const{options:Ce,getAutomaticKeyboardNavigation:be,disposable:Le}=U(ae,te,Z,re,ce,me);super(oe,ae,G,j,Ce);this.disposables.add(Le),this.internals=new H(this,te,be,te.overrideStyles,Z,ue,he,re,me),this.disposables.add(this.internals)}updateOptions(oe={}){super.updateOptions(oe),oe.overrideStyles&&this.internals.updateStyleOverrides(oe.overrideStyles)}};X=Me([_e(5,p.IContextKeyService),_e(6,e.IListService),_e(7,u.IThemeService),_e(8,d.IConfigurationService),_e(9,o.IKeybindingService),_e(10,l.IAccessibilityService)],X),e.WorkbenchCompressibleObjectTree=X;let z=class extends t.DataTree{constructor(oe,ae,G,j,te,Z,ue,he,re,ce,me,Ce){const{options:be,getAutomaticKeyboardNavigation:Le,disposable:De}=U(ae,Z,ue,ce,me,Ce);super(oe,ae,G,j,te,be);this.disposables.add(De),this.internals=new H(this,Z,Le,Z.overrideStyles,ue,he,re,ce,Ce),this.disposables.add(this.internals)}updateOptions(oe={}){super.updateOptions(oe),oe.overrideStyles&&this.internals.updateStyleOverrides(oe.overrideStyles)}};z=Me([_e(6,p.IContextKeyService),_e(7,e.IListService),_e(8,u.IThemeService),_e(9,d.IConfigurationService),_e(10,o.IKeybindingService),_e(11,l.IAccessibilityService)],z),e.WorkbenchDataTree=z;let P=class extends n.AsyncDataTree{constructor(oe,ae,G,j,te,Z,ue,he,re,ce,me,Ce){const{options:be,getAutomaticKeyboardNavigation:Le,disposable:De}=U(ae,Z,ue,ce,me,Ce);super(oe,ae,G,j,te,be);this.disposables.add(De),this.internals=new H(this,Z,Le,Z.overrideStyles,ue,he,re,ce,Ce),this.disposables.add(this.internals)}get onDidOpen(){return this.internals.onDidOpen}updateOptions(oe={}){super.updateOptions(oe),oe.overrideStyles&&this.internals.updateStyleOverrides(oe.overrideStyles)}};P=Me([_e(6,p.IContextKeyService),_e(7,e.IListService),_e(8,u.IThemeService),_e(9,d.IConfigurationService),_e(10,o.IKeybindingService),_e(11,l.IAccessibilityService)],P),e.WorkbenchAsyncDataTree=P;let V=class extends n.CompressibleAsyncDataTree{constructor(oe,ae,G,j,te,Z,ue,he,re,ce,me,Ce,be){const{options:Le,getAutomaticKeyboardNavigation:De,disposable:Re}=U(ae,ue,he,me,Ce,be);super(oe,ae,G,j,te,Z,Le);this.disposables.add(Re),this.internals=new H(this,ue,De,ue.overrideStyles,he,re,ce,me,be),this.disposables.add(this.internals)}};V=Me([_e(7,p.IContextKeyService),_e(8,e.IListService),_e(9,u.IThemeService),_e(10,d.IConfigurationService),_e(11,o.IKeybindingService),_e(12,l.IAccessibilityService)],V),e.WorkbenchCompressibleAsyncDataTree=V;function U(ie,oe,ae,G,j,te){var Z;e.WorkbenchListSupportsKeyboardNavigation.bindTo(ae),e.didBindWorkbenchListAutomaticKeyboardNavigation||(e.WorkbenchListAutomaticKeyboardNavigation.bindTo(ae),e.didBindWorkbenchListAutomaticKeyboardNavigation=!0);const ue=()=>{let Le=ae.getContextKeyValue(e.WorkbenchListAutomaticKeyboardNavigationKey);return Le&&(Le=G.getValue(k)),Le},he=te.isScreenReaderOptimized(),re=oe.simpleKeyboardNavigation||he?"simple":G.getValue(I),ce=oe.horizontalScrolling!==void 0?oe.horizontalScrolling:G.getValue(L),[me,Ce]=D(oe,G,j),be=oe.additionalScrollHeight;return{getAutomaticKeyboardNavigation:ue,disposable:Ce,options:Object.assign(Object.assign({keyboardSupport:!1},me),{indent:G.getValue(E),renderIndentGuides:G.getValue(T),smoothScrolling:G.getValue(O),automaticKeyboardNavigation:ue(),simpleKeyboardNavigation:re==="simple",filterOnType:re==="filter",horizontalScrolling:ce,keyboardNavigationEventFilter:ne(ie,j),additionalScrollHeight:be,hideTwistiesOfChildlessElements:oe.hideTwistiesOfChildlessElements,expandOnlyOnTwistieClick:(Z=oe.expandOnlyOnTwistieClick)!==null&&Z!==void 0?Z:G.getValue(A)==="doubleClick"})}}let H=class{constructor(oe,ae,G,j,te,Z,ue,he,re){this.tree=oe,this.themeService=ue,this.disposables=[],this.contextKeyService=f(te,oe),e.WorkbenchListSupportsMultiSelectContextKey.bindTo(this.contextKeyService).set(ae.multipleSelectionSupport!==!1),this.hasSelectionOrFocus=e.WorkbenchListHasSelectionOrFocus.bindTo(this.contextKeyService),this.hasDoubleSelection=e.WorkbenchListDoubleSelection.bindTo(this.contextKeyService),this.hasMultiSelection=e.WorkbenchListMultiSelection.bindTo(this.contextKeyService),this._useAltAsMultipleSelectionModifier=B(he);const me=new Set;me.add(e.WorkbenchListAutomaticKeyboardNavigationKey);const Ce=()=>{const Le=re.isScreenReaderOptimized()?"simple":he.getValue(I);oe.updateOptions({simpleKeyboardNavigation:Le==="simple",filterOnType:Le==="filter"})};this.updateStyleOverrides(j),this.disposables.push(this.contextKeyService,Z.register(oe),oe.onDidChangeSelection(()=>{const be=oe.getSelection(),Le=oe.getFocus();this.contextKeyService.bufferChangeEvents(()=>{this.hasSelectionOrFocus.set(be.length>0||Le.length>0),this.hasMultiSelection.set(be.length>1),this.hasDoubleSelection.set(be.length===2)})}),oe.onDidChangeFocus(()=>{const be=oe.getSelection(),Le=oe.getFocus();this.hasSelectionOrFocus.set(be.length>0||Le.length>0)}),he.onDidChangeConfiguration(be=>{let Le={};if(be.affectsConfiguration(v)&&(this._useAltAsMultipleSelectionModifier=B(he)),be.affectsConfiguration(E)){const De=he.getValue(E);Le=Object.assign(Object.assign({},Le),{indent:De})}if(be.affectsConfiguration(T)){const De=he.getValue(T);Le=Object.assign(Object.assign({},Le),{renderIndentGuides:De})}if(be.affectsConfiguration(O)){const De=he.getValue(O);Le=Object.assign(Object.assign({},Le),{smoothScrolling:De})}if(be.affectsConfiguration(I)&&Ce(),be.affectsConfiguration(k)&&(Le=Object.assign(Object.assign({},Le),{automaticKeyboardNavigation:G()})),be.affectsConfiguration(L)&&ae.horizontalScrolling===void 0){const De=he.getValue(L);Le=Object.assign(Object.assign({},Le),{horizontalScrolling:De})}be.affectsConfiguration(A)&&ae.expandOnlyOnTwistieClick===void 0&&(Le=Object.assign(Object.assign({},Le),{expandOnlyOnTwistieClick:he.getValue(A)==="doubleClick"})),Object.keys(Le).length>0&&oe.updateOptions(Le)}),this.contextKeyService.onDidChangeContext(be=>{be.affectsSome(me)&&oe.updateOptions({automaticKeyboardNavigation:G()})}),re.onDidChangeScreenReaderOptimized(()=>Ce())),this.navigator=new se(oe,Object.assign({configurationService:he},ae)),this.disposables.push(this.navigator)}get onDidOpen(){return this.navigator.onDidOpen}updateStyleOverrides(oe){S.dispose(this.styler),this.styler=oe?a.attachListStyler(this.tree,this.themeService,oe):S.Disposable.None}dispose(){this.disposables=S.dispose(this.disposables),S.dispose(this.styler),this.styler=void 0}};H=Me([_e(4,p.IContextKeyService),_e(5,e.IListService),_e(6,u.IThemeService),_e(7,d.IConfigurationService),_e(8,l.IAccessibilityService)],H),s.Registry.as(g.Extensions.Configuration).registerConfiguration({id:"workbench",order:7,title:C.localize(0,null),type:"object",properties:{[v]:{type:"string",enum:["ctrlCmd","alt"],enumDescriptions:[C.localize(1,null),C.localize(2,null)],default:"ctrlCmd",description:C.localize(3,null)},[y]:{type:"string",enum:["singleClick","doubleClick"],default:"singleClick",description:C.localize(4,null)},[L]:{type:"boolean",default:!1,description:C.localize(5,null)},[E]:{type:"number",default:8,minimum:0,maximum:40,description:C.localize(6,null)},[T]:{type:"string",enum:["none","onHover","always"],default:"onHover",description:C.localize(7,null)},[O]:{type:"boolean",default:!1,description:C.localize(8,null)},[I]:{type:"string",enum:["simple","highlight","filter"],enumDescriptions:[C.localize(9,null),C.localize(10,null),C.localize(11,null)],default:"highlight",description:C.localize(12,null)},[k]:{type:"boolean",default:!0,markdownDescription:C.localize(13,null)},[A]:{type:"string",enum:["singleClick","doubleClick"],default:"singleClick",description:C.localize(14,null)}}})}),define(Q[652],J([0,1,139,9,11,22,23,116,16,65,443,147,574]),function(q,e,b,N,M,w,S,C,d,g,p,c,o){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.QuickInputService=void 0;let s=class extends M.Themable{constructor(u,r,i,n,t){super(i);this.instantiationService=u,this.contextKeyService=r,this.accessibilityService=n,this.layoutService=t,this.contexts=new Map}get controller(){return this._controller||(this._controller=this._register(this.createController())),this._controller}get quickAccess(){return this._quickAccess||(this._quickAccess=this._register(this.instantiationService.createInstance(o.QuickAccessController))),this._quickAccess}createController(u=this.layoutService,r){var i,n;const t={idPrefix:"quickInput_",container:u.container,ignoreFocusOut:()=>!1,isScreenReaderOptimized:()=>this.accessibilityService.isScreenReaderOptimized(),backKeybindingLabel:()=>{},setContextKey:h=>this.setContextKey(h),returnFocus:()=>u.focus(),createList:(h,m,_,f,v)=>this.instantiationService.createInstance(c.WorkbenchList,h,m,_,f,v),styles:this.computeStyles()},l=this._register(new p.QuickInputController(Object.assign(Object.assign({},t),r)));return l.layout(u.dimension,(n=(i=u.offset)===null||i===void 0?void 0:i.top)!==null&&n!==void 0?n:0),this._register(u.onDidLayout(h=>{var m,_;return l.layout(h,(_=(m=u.offset)===null||m===void 0?void 0:m.top)!==null&&_!==void 0?_:0)})),this._register(l.onShow(()=>this.resetContextKeys())),this._register(l.onHide(()=>this.resetContextKeys())),l}setContextKey(u){let r;u&&(r=this.contexts.get(u),r||(r=new d.RawContextKey(u,!1).bindTo(this.contextKeyService),this.contexts.set(u,r))),!(r&&r.get())&&(this.resetContextKeys(),r&&r.set(!0))}resetContextKeys(){this.contexts.forEach(u=>{u.get()&&u.reset()})}pick(u,r={},i=S.CancellationToken.None){return this.controller.pick(u,r,i)}createQuickPick(){return this.controller.createQuickPick()}updateStyles(){this.controller.applyStyles(this.computeStyles())}computeStyles(){return{widget:Object.assign({},C.computeStyles(this.theme,{quickInputBackground:w.quickInputBackground,quickInputForeground:w.quickInputForeground,quickInputTitleBackground:w.quickInputTitleBackground,contrastBorder:w.contrastBorder,widgetShadow:w.widgetShadow})),inputBox:C.computeStyles(this.theme,{inputForeground:w.inputForeground,inputBackground:w.inputBackground,inputBorder:w.inputBorder,inputValidationInfoBackground:w.inputValidationInfoBackground,inputValidationInfoForeground:w.inputValidationInfoForeground,inputValidationInfoBorder:w.inputValidationInfoBorder,inputValidationWarningBackground:w.inputValidationWarningBackground,inputValidationWarningForeground:w.inputValidationWarningForeground,inputValidationWarningBorder:w.inputValidationWarningBorder,inputValidationErrorBackground:w.inputValidationErrorBackground,inputValidationErrorForeground:w.inputValidationErrorForeground,inputValidationErrorBorder:w.inputValidationErrorBorder}),countBadge:C.computeStyles(this.theme,{badgeBackground:w.badgeBackground,badgeForeground:w.badgeForeground,badgeBorder:w.contrastBorder}),button:C.computeStyles(this.theme,{buttonForeground:w.buttonForeground,buttonBackground:w.buttonBackground,buttonHoverBackground:w.buttonHoverBackground,buttonBorder:w.contrastBorder}),progressBar:C.computeStyles(this.theme,{progressBarBackground:w.progressBarBackground}),list:C.computeStyles(this.theme,{listBackground:w.quickInputBackground,listInactiveFocusForeground:w.listFocusForeground,listInactiveFocusBackground:w.quickInputListFocusBackground,listFocusOutline:w.activeContrastBorder,listInactiveFocusOutline:w.activeContrastBorder,pickerGroupBorder:w.pickerGroupBorder,pickerGroupForeground:w.pickerGroupForeground})}}};s=Me([_e(0,N.IInstantiationService),_e(1,d.IContextKeyService),_e(2,M.IThemeService),_e(3,g.IAccessibilityService),_e(4,b.ILayoutService)],s),e.QuickInputService=s}),define(Q[653],J([0,1,13,11,23,9,16,65,139,28,652,88,357]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.QuickInputEditorWidget=e.QuickInputEditorContribution=e.StandaloneQuickInputServiceImpl=e.EditorScopedQuickInputServiceImpl=void 0;let o=class extends p.QuickInputService{constructor(i,n,t,l,h,m){super(n,t,l,h,m);this.host=void 0;const _=a.get(i);this.host={_serviceBrand:void 0,get container(){return _.widget.getDomNode()},get dimension(){return i.getLayoutInfo()},get onDidLayout(){return i.onDidLayoutChange},focus:()=>i.focus()}}createController(){return super.createController(this.host)}};o=Me([_e(1,w.IInstantiationService),_e(2,S.IContextKeyService),_e(3,N.IThemeService),_e(4,C.IAccessibilityService),_e(5,d.ILayoutService)],o),e.EditorScopedQuickInputServiceImpl=o;let s=class{constructor(i,n){this.instantiationService=i,this.codeEditorService=n,this.mapEditorToService=new Map}get activeService(){const i=this.codeEditorService.getFocusedCodeEditor();if(!i)throw new Error("Quick input service needs a focused editor to work.");let n=this.mapEditorToService.get(i);if(!n){const t=n=this.instantiationService.createInstance(o,i);this.mapEditorToService.set(i,n),c.once(i.onDidDispose)(()=>{t.dispose(),this.mapEditorToService.delete(i)})}return n}get quickAccess(){return this.activeService.quickAccess}pick(i,n={},t=M.CancellationToken.None){return this.activeService.pick(i,n,t)}createQuickPick(){return this.activeService.createQuickPick()}};s=Me([_e(0,w.IInstantiationService),_e(1,g.ICodeEditorService)],s),e.StandaloneQuickInputServiceImpl=s;class a{constructor(i){this.editor=i,this.widget=new u(this.editor)}static get(i){return i.getContribution(a.ID)}dispose(){this.widget.dispose()}}e.QuickInputEditorContribution=a,a.ID="editor.controller.quickInput";class u{constructor(i){this.codeEditor=i,this.domNode=document.createElement("div"),this.codeEditor.addOverlayWidget(this)}getId(){return u.ID}getDomNode(){return this.domNode}getPosition(){return{preference:2}}dispose(){this.codeEditor.removeOverlayWidget(this)}}e.QuickInputEditorWidget=u,u.ID="editor.contrib.quickInputWidget",b.registerEditorContribution(a.ID,a)}),define(Q[654],J([0,1,82,11,22,27]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SeverityIcon=void 0;var S;(function(C){function d(g){switch(g){case b.default.Ignore:return"severity-ignore "+w.Codicon.info.classNames;case b.default.Info:return w.Codicon.info.classNames;case b.default.Warning:return w.Codicon.warning.classNames;case b.default.Error:return w.Codicon.error.classNames;default:return""}}C.className=d})(S=e.SeverityIcon||(e.SeverityIcon={})),N.registerThemingParticipant((C,d)=>{const g=C.getColor(M.problemsErrorIconForeground);if(g){const o=w.Codicon.error.cssSelector;d.addRule(` + .monaco-editor .zone-widget ${o}, + .markers-panel .marker-icon${o}, + .extensions-viewlet > .extensions ${o} { + color: ${g}; + } + `)}const p=C.getColor(M.problemsWarningIconForeground);if(p){const o=w.Codicon.warning.cssSelector;d.addRule(` + .monaco-editor .zone-widget ${o}, + .markers-panel .marker-icon${o}, + .extensions-viewlet > .extensions ${o}, + .extension-editor ${o} { + color: ${p}; + } + `)}const c=C.getColor(M.problemsInfoIconForeground);if(c){const o=w.Codicon.info.cssSelector;d.addRule(` + .monaco-editor .zone-widget ${o}, + .markers-panel .marker-icon${o}, + .extensions-viewlet > .extensions ${o}, + .extension-editor ${o} { + color: ${c}; + } + `)}})}),define(Q[655],J([0,1,480,7,2,85,3,22,11,29,61,159,19,6,118,44,654,58,34,16,189,9,8,115,343]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.editorMarkerNavigationBackground=e.editorMarkerNavigationInfo=e.editorMarkerNavigationWarning=e.editorMarkerNavigationError=e.MarkerNavigationWidget=void 0;class f{constructor(E,T,O,A,B){this._openerService=A,this._labelService=B,this._lines=0,this._longestLineLength=0,this._relatedDiagnostics=new WeakMap,this._disposables=new M.DisposableStore,this._editor=T;const F=document.createElement("div");F.className="descriptioncontainer",this._messageBlock=document.createElement("div"),this._messageBlock.classList.add("message"),this._messageBlock.setAttribute("aria-live","assertive"),this._messageBlock.setAttribute("role","alert"),F.appendChild(this._messageBlock),this._relatedBlock=document.createElement("div"),F.appendChild(this._relatedBlock),this._disposables.add(N.addStandardDisposableListener(this._relatedBlock,"click",D=>{D.preventDefault();const R=this._relatedDiagnostics.get(D.target);R&&O(R)})),this._scrollable=new p.ScrollableElement(F,{horizontal:1,vertical:1,useShadows:!1,horizontalScrollbarSize:3,verticalScrollbarSize:3}),E.appendChild(this._scrollable.getDomNode()),this._disposables.add(this._scrollable.onScroll(D=>{F.style.left=`-${D.scrollLeft}px`,F.style.top=`-${D.scrollTop}px`})),this._disposables.add(this._scrollable)}dispose(){M.dispose(this._disposables)}update(E){const{source:T,message:O,relatedInformation:A,code:B}=E;let F=((T==null?void 0:T.length)||0)+"()".length;B&&(typeof B=="string"?F+=B.length:F+=B.value.length);const D=m.splitLines(O);this._lines=D.length,this._longestLineLength=0;for(const Y of D)this._longestLineLength=Math.max(Y.length+F,this._longestLineLength);N.clearNode(this._messageBlock),this._messageBlock.setAttribute("aria-label",this.getAriaLabel(E)),this._editor.applyFontInfo(this._messageBlock);let R=this._messageBlock;for(const Y of D)R=document.createElement("div"),R.innerText=Y,Y===""&&(R.style.height=this._messageBlock.style.lineHeight),this._messageBlock.appendChild(R);if(T||B){const Y=document.createElement("span");if(Y.classList.add("details"),R.appendChild(Y),T){const ee=document.createElement("span");ee.innerText=T,ee.classList.add("source"),Y.appendChild(ee)}if(B)if(typeof B=="string"){const ee=document.createElement("span");ee.innerText=`(${B})`,ee.classList.add("code"),Y.appendChild(ee)}else{this._codeLink=N.$("a.code-link"),this._codeLink.setAttribute("href",`${B.target.toString()}`),this._codeLink.onclick=se=>{this._openerService.open(B.target),se.preventDefault(),se.stopPropagation()};const ee=N.append(this._codeLink,N.$("span"));ee.innerText=B.value,Y.appendChild(this._codeLink)}}if(N.clearNode(this._relatedBlock),this._editor.applyFontInfo(this._relatedBlock),o.isNonEmptyArray(A)){const Y=this._relatedBlock.appendChild(document.createElement("div"));Y.style.paddingTop=`${Math.floor(this._editor.getOption(53)*.66)}px`,this._lines+=1;for(const ee of A){let se=document.createElement("div"),ne=document.createElement("a");ne.classList.add("filename"),ne.innerText=`${c.getBaseLabel(ee.resource)}(${ee.startLineNumber}, ${ee.startColumn}): `,ne.title=this._labelService.getUriLabel(ee.resource),this._relatedDiagnostics.set(ne,ee);let le=document.createElement("span");le.innerText=ee.message,se.appendChild(ne),se.appendChild(le),this._lines+=1,Y.appendChild(se)}}const W=this._editor.getOption(38),x=Math.ceil(W.typicalFullwidthCharacterWidth*this._longestLineLength*.75),K=W.lineHeight*this._lines;this._scrollable.setScrollDimensions({scrollWidth:x,scrollHeight:K})}layout(E,T){this._scrollable.getDomNode().style.height=`${E}px`,this._scrollable.getDomNode().style.width=`${T}px`,this._scrollable.setScrollDimensions({width:T,height:E})}getHeightInLines(){return Math.min(17,this._lines)}getAriaLabel(E){let T="";switch(E.severity){case w.MarkerSeverity.Error:T=b.localize(0,null);break;case w.MarkerSeverity.Warning:T=b.localize(1,null);break;case w.MarkerSeverity.Info:T=b.localize(2,null);break;case w.MarkerSeverity.Hint:T=b.localize(3,null);break}let O=b.localize(4,null,T,E.startLineNumber+":"+E.startColumn);const A=this._editor.getModel();return A&&E.startLineNumber<=A.getLineCount()&&E.startLineNumber>=1&&(O=`${A.getLineContent(E.startLineNumber)}, ${O}`),O}}let v=class Kt extends a.PeekViewWidget{constructor(E,T,O,A,B,F,D){super(E,{showArrow:!0,showFrame:!0,isAccessible:!0},B);this._themeService=T,this._openerService=O,this._menuService=A,this._contextKeyService=F,this._labelService=D,this._callOnDispose=new M.DisposableStore,this._onDidSelectRelatedInformation=new s.Emitter,this.onDidSelectRelatedInformation=this._onDidSelectRelatedInformation.event,this._severity=w.MarkerSeverity.Warning,this._backgroundColor=g.Color.white,this._applyTheme(T.getColorTheme()),this._callOnDispose.add(T.onDidColorThemeChange(this._applyTheme.bind(this))),this.create()}_applyTheme(E){this._backgroundColor=E.getColor(e.editorMarkerNavigationBackground);let T=e.editorMarkerNavigationError;this._severity===w.MarkerSeverity.Warning?T=e.editorMarkerNavigationWarning:this._severity===w.MarkerSeverity.Info&&(T=e.editorMarkerNavigationInfo);const O=E.getColor(T);this.style({arrowColor:O,frameColor:O,headerBackgroundColor:this._backgroundColor,primaryHeadingColor:E.getColor(a.peekViewTitleForeground),secondaryHeadingColor:E.getColor(a.peekViewTitleInfoForeground)})}_applyStyles(){this._parentContainer&&(this._parentContainer.style.backgroundColor=this._backgroundColor?this._backgroundColor.toString():""),super._applyStyles()}dispose(){this._callOnDispose.dispose(),super.dispose()}_fillHead(E){super._fillHead(E),this._disposables.add(this._actionbarWidget.actionRunner.onBeforeRun(A=>this.editor.focus()));const T=[],O=this._menuService.createMenu(Kt.TitleMenu,this._contextKeyService);l.createAndFillInActionBarActions(O,void 0,T),this._actionbarWidget.push(T,{label:!1,icon:!0,index:0}),O.dispose()}_fillTitleIcon(E){this._icon=N.append(E,N.$(""))}_fillBody(E){this._parentContainer=E,E.classList.add("marker-widget"),this._parentContainer.tabIndex=0,this._parentContainer.setAttribute("role","tooltip"),this._container=document.createElement("div"),E.appendChild(this._container),this._message=new f(this._container,this.editor,T=>this._onDidSelectRelatedInformation.fire(T),this._openerService,this._labelService),this._disposables.add(this._message)}show(){throw new Error("call showAtMarker")}showAtMarker(E,T,O){this._container.classList.remove("stale"),this._message.update(E),this._severity=E.severity,this._applyTheme(this._themeService.getColorTheme());let A=S.Range.lift(E);const B=this.editor.getPosition();let F=B&&A.containsPosition(B)?B:A.getStartPosition();super.show(F,this.computeRequiredHeight());const D=this.editor.getModel();if(D){const R=O>1?b.localize(5,null,T,O):b.localize(6,null,T,O);this.setTitle(u.basename(D.uri),R)}this._icon.className=`codicon ${r.SeverityIcon.className(w.MarkerSeverity.toSeverity(this._severity))}`,this.editor.revealPositionNearTop(F,0),this.editor.focus()}updateMarker(E){this._container.classList.remove("stale"),this._message.update(E)}showStale(){this._container.classList.add("stale"),this._relayout()}_doLayoutBody(E,T){super._doLayoutBody(E,T),this._heightInPixel=E,this._message.layout(E,T),this._container.style.height=`${E}px`}_onWidth(E){this._message.layout(this._heightInPixel,E)}_relayout(){super._relayout(this.computeRequiredHeight())}computeRequiredHeight(){return 3+this._message.getHeightInLines()}};v.TitleMenu=new n.MenuId("gotoErrorTitleMenu"),v=Me([_e(1,d.IThemeService),_e(2,i.IOpenerService),_e(3,n.IMenuService),_e(4,h.IInstantiationService),_e(5,t.IContextKeyService),_e(6,_.ILabelService)],v),e.MarkerNavigationWidget=v;let y=C.oneOf(C.editorErrorForeground,C.editorErrorBorder),L=C.oneOf(C.editorWarningForeground,C.editorWarningBorder),I=C.oneOf(C.editorInfoForeground,C.editorInfoBorder);e.editorMarkerNavigationError=C.registerColor("editorMarkerNavigationError.background",{dark:y,light:y,hc:y},b.localize(7,null)),e.editorMarkerNavigationWarning=C.registerColor("editorMarkerNavigationWarning.background",{dark:L,light:L,hc:L},b.localize(8,null)),e.editorMarkerNavigationInfo=C.registerColor("editorMarkerNavigationInfo.background",{dark:I,light:I,hc:I},b.localize(9,null)),e.editorMarkerNavigationBackground=C.registerColor("editorMarkerNavigation.background",{dark:"#2D2D30",light:g.Color.white,hc:"#0C141F"},b.localize(10,null)),d.registerThemingParticipant((k,E)=>{const T=k.getColor(C.textLinkForeground);T&&(E.addRule(`.monaco-editor .marker-widget a { color: ${T}; }`),E.addRule(`.monaco-editor .marker-widget a.code-link span:hover { color: ${T}; }`))})}),define(Q[80],J([0,1,33,11,6,528,185,15,27]),function(q,e,b,N,M,w,S,C,d){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.widgetClose=e.iconsSchemaId=e.getIconRegistry=e.registerIcon=e.Extensions=void 0,e.Extensions={IconContribution:"base.contributions.icons"};class g{constructor(){this._onDidChange=new M.Emitter,this.onDidChange=this._onDidChange.event,this.iconSchema={definitions:{icons:{type:"object",properties:{fontId:{type:"string",description:w.localize(0,null)},fontCharacter:{type:"string",description:w.localize(1,null)}},additionalProperties:!1,defaultSnippets:[{body:{fontCharacter:"\\\\e030"}}]}},type:"object",properties:{}},this.iconReferenceSchema={type:"string",pattern:`^${d.CSSIcon.iconNameExpression}$`,enum:[],enumDescriptions:[]},this.iconsById={},this.iconFontsById={}}registerIcon(i,n,t,l){const h=this.iconsById[i];if(h){if(t&&!h.description){h.description=t,this.iconSchema.properties[i].markdownDescription=`${t} $(${i})`;const f=this.iconReferenceSchema.enum.indexOf(i);f!==-1&&(this.iconReferenceSchema.enumDescriptions[f]=t),this._onDidChange.fire()}return h}let m={id:i,description:t,defaults:n,deprecationMessage:l};this.iconsById[i]=m;let _={$ref:"#/definitions/icons"};return l&&(_.deprecationMessage=l),t&&(_.markdownDescription=`${t}: $(${i})`),this.iconSchema.properties[i]=_,this.iconReferenceSchema.enum.push(i),this.iconReferenceSchema.enumDescriptions.push(t||""),this._onDidChange.fire(),{id:i}}getIcons(){return Object.keys(this.iconsById).map(i=>this.iconsById[i])}getIcon(i){return this.iconsById[i]}getIconSchema(){return this.iconSchema}getIconFont(i){return this.iconFontsById[i]}toString(){const i=(h,m)=>h.id.localeCompare(m.id),n=h=>{for(;N.ThemeIcon.isThemeIcon(h.defaults);)h=this.iconsById[h.defaults.id];return`codicon codicon-${h?h.id:""}`};let t=[];t.push("| preview | identifier | default codicon ID | description"),t.push("| ----------- | --------------------------------- | --------------------------------- | --------------------------------- |");const l=Object.keys(this.iconsById).map(h=>this.iconsById[h]);for(const h of l.filter(m=>!!m.description).sort(i))t.push(`||${h.id}|${N.ThemeIcon.isThemeIcon(h.defaults)?h.defaults.id:h.id}|${h.description||""}|`);t.push("| preview | identifier "),t.push("| ----------- | --------------------------------- |");for(const h of l.filter(m=>!N.ThemeIcon.isThemeIcon(m.defaults)).sort(i))t.push(`||${h.id}|`);return t.join(` +`)}}const p=new g;b.Registry.add(e.Extensions.IconContribution,p);function c(r,i,n,t){return p.registerIcon(r,i,n,t)}e.registerIcon=c;function o(){return p}e.getIconRegistry=o;function s(){for(const r of d.iconRegistry.all)p.registerIcon(r.id,r.definition,r.description);d.iconRegistry.onDidRegister(r=>p.registerIcon(r.id,r.definition,r.description))}s(),e.iconsSchemaId="vscode://schemas/icons";let a=b.Registry.as(S.Extensions.JSONContribution);a.registerSchema(e.iconsSchemaId,p.getIconSchema());const u=new C.RunOnceScheduler(()=>a.notifySchemaChanged(e.iconsSchemaId),200);p.onDidChange(()=>{u.isScheduled()||u.schedule()}),e.widgetClose=c("widget-close",d.Codicon.close,w.localize(2,null))}),define(Q[656],J([0,1,450,7,30,83,61,48,2,69,13,28,38,113,14,49,129,63,16,22,11,27,80,333]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m){"use strict";var _;Object.defineProperty(e,"__esModule",{value:!0}),e.DiffReview=void 0;const f=3;class v{constructor(F,D,R,W){this.originalLineStart=F,this.originalLineEnd=D,this.modifiedLineStart=R,this.modifiedLineEnd=W}getType(){return this.originalLineStart===0?1:this.modifiedLineStart===0?2:0}}class y{constructor(F){this.entries=F}}const L=m.registerIcon("diff-review-insert",h.Codicon.add,b.localize(0,null)),I=m.registerIcon("diff-review-remove",h.Codicon.remove,b.localize(1,null)),k=m.registerIcon("diff-review-close",h.Codicon.close,b.localize(2,null));class E extends d.Disposable{constructor(F){super();this._width=0,this._diffEditor=F,this._isVisible=!1,this.shadow=M.createFastDomNode(document.createElement("div")),this.shadow.setClassName("diff-review-shadow"),this.actionBarContainer=M.createFastDomNode(document.createElement("div")),this.actionBarContainer.setClassName("diff-review-actions"),this._actionBar=this._register(new w.ActionBar(this.actionBarContainer.domNode)),this._actionBar.push(new C.Action("diffreview.close",b.localize(3,null),"close-diff-review "+l.ThemeIcon.asClassName(k),!0,()=>(this.hide(),Promise.resolve(null))),{label:!1,icon:!0}),this.domNode=M.createFastDomNode(document.createElement("div")),this.domNode.setClassName("diff-review monaco-editor-background"),this._content=M.createFastDomNode(document.createElement("div")),this._content.setClassName("diff-review-content"),this._content.setAttribute("role","code"),this.scrollbar=this._register(new S.DomScrollableElement(this._content.domNode,{})),this.domNode.domNode.appendChild(this.scrollbar.getDomNode()),this._register(F.onDidUpdateDiff(()=>{!this._isVisible||(this._diffs=this._compute(),this._render())})),this._register(F.getModifiedEditor().onDidChangeCursorPosition(()=>{!this._isVisible||this._render()})),this._register(N.addStandardDisposableListener(this.domNode.domNode,"click",D=>{D.preventDefault();let R=N.findParentWithClass(D.target,"diff-review-row");R&&this._goToRow(R)})),this._register(N.addStandardDisposableListener(this.domNode.domNode,"keydown",D=>{(D.equals(18)||D.equals(2048|18)||D.equals(512|18))&&(D.preventDefault(),this._goToRow(this._getNextRow())),(D.equals(16)||D.equals(2048|16)||D.equals(512|16))&&(D.preventDefault(),this._goToRow(this._getPrevRow())),(D.equals(9)||D.equals(2048|9)||D.equals(512|9)||D.equals(1024|9))&&(D.preventDefault(),this.hide()),(D.equals(10)||D.equals(3))&&(D.preventDefault(),this.accept())})),this._diffs=[],this._currentDiff=null}prev(){let F=0;if(this._isVisible||(this._diffs=this._compute()),this._isVisible){let R=-1;for(let W=0,x=this._diffs.length;W0){const te=F[se-1];te.originalEndLineNumber===0?oe=te.originalStartLineNumber+1:oe=te.originalEndLineNumber+1,te.modifiedEndLineNumber===0?ae=te.modifiedStartLineNumber+1:ae=te.modifiedEndLineNumber+1}let G=$-f+1,j=ie-f+1;if(Goe){const te=oe-G;G=G+te,j=j+te}if(j>ae){const te=ae-j;G=G+te,j=j+te}U[H++]=new v($,G,ie,j)}W[x++]=new y(U)}let K=W[0].entries,Y=[],ee=0;for(let se=1,ne=W.length;sele)&&(le=he),re!==0&&(X===0||rez)&&(z=ce)}let P=document.createElement("div");P.className="diff-review-row";let V=document.createElement("div");V.className="diff-review-cell diff-review-summary";const U=le-ne+1,H=z-X+1;V.appendChild(document.createTextNode(`${Y+1}/${this._diffs.length}: @@ -${ne},${U} +${X},${H} @@`)),P.setAttribute("data-line",String(X));const $=j=>j===0?b.localize(4,null):j===1?b.localize(5,null):b.localize(6,null,j),ie=$(U),oe=$(H);P.setAttribute("aria-label",b.localize(7,null,Y+1,this._diffs.length,ne,ie,X,oe)),P.appendChild(V),P.setAttribute("role","listitem"),se.appendChild(P);const ae=D.get(53);let G=X;for(let j=0,te=ee.length;j>>0,ee=new Uint32Array(2);ee[0]=x.length,ee[1]=Y;const se=new s.LineTokens(ee,x),ne=i.ViewLineRenderingData.isBasicASCII(x,F.mightContainNonBasicASCII()),le=i.ViewLineRenderingData.containsRTL(x,ne,F.mightContainRTL());return r.renderViewLine2(new r.RenderLineInput(K.isMonospace&&!D.get(26),K.canUseHalfwidthRightwardsArrow,x,!1,ne,le,0,se,[],R,0,K.spaceWidth,K.middotWidth,K.wsmiddotWidth,D.get(100),D.get(83),D.get(77),D.get(39)!==o.EditorFontLigatures.OFF,null)).html}}e.DiffReview=E,E._ttPolicy=(_=window.trustedTypes)===null||_===void 0?void 0:_.createPolicy("diffReview",{createHTML:B=>B}),l.registerThemingParticipant((B,F)=>{const D=B.getColor(u.editorLineNumbers);D&&F.addRule(`.monaco-diff-editor .diff-review-line-number { color: ${D}; }`);const R=B.getColor(t.scrollbarShadow);R&&F.addRule(`.monaco-diff-editor .diff-review-shadow { box-shadow: ${R} 0 -6px 6px -6px inset; }`)});class T extends p.EditorAction{constructor(){super({id:"editor.action.diffReview.next",label:b.localize(13,null),alias:"Go to Next Difference",precondition:n.ContextKeyExpr.has("isInDiffEditor"),kbOpts:{kbExpr:null,primary:65,weight:100}})}run(F,D){const R=A(F);R&&R.diffReviewNext()}}class O extends p.EditorAction{constructor(){super({id:"editor.action.diffReview.prev",label:b.localize(14,null),alias:"Go to Previous Difference",precondition:n.ContextKeyExpr.has("isInDiffEditor"),kbOpts:{kbExpr:null,primary:1024|65,weight:100}})}run(F,D){const R=A(F);R&&R.diffReviewPrev()}}function A(B){const F=B.get(c.ICodeEditorService),D=F.listDiffEditors(),R=F.getActiveCodeEditor();if(!R)return null;for(let W=0,x=D.length;W!this._zonesMap[String(ce.id)])}clean(re){this._zones.length>0&&re.changeViewZones(ce=>{for(const me of this._zones)ce.removeZone(me)}),this._zones=[],this._zonesMap={},this._decorations=re.deltaDecorations(this._decorations,[])}apply(re,ce,me,Ce){const be=Ce?p.StableEditorScrollState.capture(re):null;re.changeViewZones(Le=>{for(const De of this._zones)Le.removeZone(De);for(const De of this._inlineDiffMargins)De.dispose();this._zones=[],this._zonesMap={},this._inlineDiffMargins=[];for(let De=0,Re=me.zones.length;Dehe});let X=class Ge extends d.Disposable{constructor(re,ce,me,Ce,be,Le,De,Re,Ee,Ae,Se,we){super();this._editorProgressService=we,this._onDidDispose=this._register(new C.Emitter),this.onDidDispose=this._onDidDispose.event,this._onDidUpdateDiff=this._register(new C.Emitter),this.onDidUpdateDiff=this._onDidUpdateDiff.event,this._onDidContentSizeChange=this._register(new C.Emitter),this._lastOriginalWarning=null,this._lastModifiedWarning=null,this._editorWorkerService=be,this._codeEditorService=Re,this._contextKeyService=this._register(Le.createScoped(re)),this._instantiationService=De.createChild(new y.ServiceCollection([f.IContextKeyService,this._contextKeyService])),this._contextKeyService.createKey("isInDiffEditor",!0),this._themeService=Ee,this._notificationService=Ae,this._id=++ee,this._state=0,this._updatingDiffProgress=null,this._domElement=re,ce=ce||{},this._renderSideBySide=!0,typeof ce.renderSideBySide!="undefined"&&(this._renderSideBySide=ce.renderSideBySide),this._maxComputationTime=5e3,typeof ce.maxComputationTime!="undefined"&&(this._maxComputationTime=ce.maxComputationTime),this._ignoreTrimWhitespace=!0,typeof ce.ignoreTrimWhitespace!="undefined"&&(this._ignoreTrimWhitespace=ce.ignoreTrimWhitespace),this._renderIndicators=!0,typeof ce.renderIndicators!="undefined"&&(this._renderIndicators=ce.renderIndicators),this._originalIsEditable=a.boolean(ce.originalEditable,!1),this._diffCodeLens=a.boolean(ce.diffCodeLens,!1),this._diffWordWrap=G(ce.diffWordWrap,"inherit"),typeof ce.isInEmbeddedEditor!="undefined"?this._contextKeyService.createKey("isInEmbeddedDiffEditor",ce.isInEmbeddedEditor):this._contextKeyService.createKey("isInEmbeddedDiffEditor",!1),this._renderOverviewRuler=!0,typeof ce.renderOverviewRuler!="undefined"&&(this._renderOverviewRuler=Boolean(ce.renderOverviewRuler)),this._updateDecorationsRunner=this._register(new S.RunOnceScheduler(()=>this._updateDecorations(),0)),this._containerDomElement=document.createElement("div"),this._containerDomElement.className=Ge._getClassName(this._themeService.getColorTheme(),this._renderSideBySide),this._containerDomElement.style.position="relative",this._containerDomElement.style.height="100%",this._domElement.appendChild(this._containerDomElement),this._overviewViewportDomElement=M.createFastDomNode(document.createElement("div")),this._overviewViewportDomElement.setClassName("diffViewport"),this._overviewViewportDomElement.setPosition("absolute"),this._overviewDomElement=document.createElement("div"),this._overviewDomElement.className="diffOverview",this._overviewDomElement.style.position="absolute",this._overviewDomElement.appendChild(this._overviewViewportDomElement.domNode),this._register(N.addStandardDisposableListener(this._overviewDomElement,"mousedown",fe=>{this._modifiedEditor.delegateVerticalScrollbarMouseDown(fe)})),this._renderOverviewRuler&&this._containerDomElement.appendChild(this._overviewDomElement),this._originalDomNode=document.createElement("div"),this._originalDomNode.className="editor original",this._originalDomNode.style.position="absolute",this._originalDomNode.style.height="100%",this._containerDomElement.appendChild(this._originalDomNode),this._modifiedDomNode=document.createElement("div"),this._modifiedDomNode.className="editor modified",this._modifiedDomNode.style.position="absolute",this._modifiedDomNode.style.height="100%",this._containerDomElement.appendChild(this._modifiedDomNode),this._beginUpdateDecorationsTimeout=-1,this._currentlyChangingViewZones=!1,this._diffComputationToken=0,this._originalEditorState=new Y(Se,Ce),this._modifiedEditorState=new Y(Se,Ce),this._isVisible=!0,this._isHandlingScrollEvent=!1,this._elementSizeObserver=this._register(new D.ElementSizeObserver(this._containerDomElement,ce.dimension,()=>this._onDidContainerSizeChanged())),ce.automaticLayout&&this._elementSizeObserver.startObserving(),this._diffComputationResult=null,this._originalEditor=this._createLeftHandSideEditor(ce,me.originalEditor||{}),this._modifiedEditor=this._createRightHandSideEditor(ce,me.modifiedEditor||{}),this._originalOverviewRuler=null,this._modifiedOverviewRuler=null,this._reviewPane=new s.DiffReview(this),this._containerDomElement.appendChild(this._reviewPane.domNode.domNode),this._containerDomElement.appendChild(this._reviewPane.shadow.domNode),this._containerDomElement.appendChild(this._reviewPane.actionBarContainer.domNode),this._enableSplitViewResizing=!0,typeof ce.enableSplitViewResizing!="undefined"&&(this._enableSplitViewResizing=ce.enableSplitViewResizing),this._renderSideBySide?this._setStrategy(new $(this._createDataSource(),this._enableSplitViewResizing)):this._setStrategy(new oe(this._createDataSource(),this._enableSplitViewResizing)),this._register(Ee.onDidColorThemeChange(fe=>{this._strategy&&this._strategy.applyColors(fe)&&this._updateDecorationsRunner.schedule(),this._containerDomElement.className=Ge._getClassName(this._themeService.getColorTheme(),this._renderSideBySide)}));const ye=A.EditorExtensionsRegistry.getDiffEditorContributions();for(const fe of ye)try{this._register(De.createInstance(fe.ctor,this))}catch(de){B.onUnexpectedError(de)}this._codeEditorService.addDiffEditor(this)}_setState(re){this._state!==re&&(this._state=re,this._updatingDiffProgress&&(this._updatingDiffProgress.done(),this._updatingDiffProgress=null),this._state===1&&(this._updatingDiffProgress=this._editorProgressService.show(!0,1e3)))}diffReviewNext(){this._reviewPane.next()}diffReviewPrev(){this._reviewPane.prev()}static _getClassName(re,ce){let me="monaco-diff-editor monaco-editor-background ";return ce&&(me+="side-by-side "),me+=k.getThemeTypeSelector(re.type),me}_recreateOverviewRulers(){!this._renderOverviewRuler||(this._originalOverviewRuler&&(this._overviewDomElement.removeChild(this._originalOverviewRuler.getDomNode()),this._originalOverviewRuler.dispose()),this._originalEditor.hasModel()&&(this._originalOverviewRuler=this._originalEditor.createOverviewRuler("original diffOverviewRuler"),this._overviewDomElement.appendChild(this._originalOverviewRuler.getDomNode())),this._modifiedOverviewRuler&&(this._overviewDomElement.removeChild(this._modifiedOverviewRuler.getDomNode()),this._modifiedOverviewRuler.dispose()),this._modifiedEditor.hasModel()&&(this._modifiedOverviewRuler=this._modifiedEditor.createOverviewRuler("modified diffOverviewRuler"),this._overviewDomElement.appendChild(this._modifiedOverviewRuler.getDomNode())),this._layoutOverviewRulers())}_createLeftHandSideEditor(re,ce){const me=this._createInnerEditor(this._instantiationService,this._originalDomNode,this._adjustOptionsForLeftHandSide(re),ce);this._register(me.onDidScrollChange(be=>{this._isHandlingScrollEvent||!be.scrollTopChanged&&!be.scrollLeftChanged&&!be.scrollHeightChanged||(this._isHandlingScrollEvent=!0,this._modifiedEditor.setScrollPosition({scrollLeft:be.scrollLeft,scrollTop:be.scrollTop}),this._isHandlingScrollEvent=!1,this._layoutOverviewViewport())})),this._register(me.onDidChangeViewZones(()=>{this._onViewZonesChanged()})),this._register(me.onDidChangeConfiguration(be=>{!me.getModel()||(be.hasChanged(38)&&this._updateDecorationsRunner.schedule(),be.hasChanged(125)&&(this._updateDecorationsRunner.cancel(),this._updateDecorations()))})),this._register(me.onDidChangeModelContent(()=>{this._isVisible&&this._beginUpdateDecorationsSoon()}));const Ce=this._contextKeyService.createKey("isInDiffLeftEditor",me.hasWidgetFocus());return this._register(me.onDidFocusEditorWidget(()=>Ce.set(!0))),this._register(me.onDidBlurEditorWidget(()=>Ce.set(!1))),this._register(me.onDidContentSizeChange(be=>{const Le=this._originalEditor.getContentWidth()+this._modifiedEditor.getContentWidth()+Ge.ONE_OVERVIEW_WIDTH,De=Math.max(this._modifiedEditor.getContentHeight(),this._originalEditor.getContentHeight());this._onDidContentSizeChange.fire({contentHeight:De,contentWidth:Le,contentHeightChanged:be.contentHeightChanged,contentWidthChanged:be.contentWidthChanged})})),me}_createRightHandSideEditor(re,ce){const me=this._createInnerEditor(this._instantiationService,this._modifiedDomNode,this._adjustOptionsForRightHandSide(re),ce);this._register(me.onDidScrollChange(be=>{this._isHandlingScrollEvent||!be.scrollTopChanged&&!be.scrollLeftChanged&&!be.scrollHeightChanged||(this._isHandlingScrollEvent=!0,this._originalEditor.setScrollPosition({scrollLeft:be.scrollLeft,scrollTop:be.scrollTop}),this._isHandlingScrollEvent=!1,this._layoutOverviewViewport())})),this._register(me.onDidChangeViewZones(()=>{this._onViewZonesChanged()})),this._register(me.onDidChangeConfiguration(be=>{!me.getModel()||(be.hasChanged(38)&&this._updateDecorationsRunner.schedule(),be.hasChanged(125)&&(this._updateDecorationsRunner.cancel(),this._updateDecorations()))})),this._register(me.onDidChangeModelContent(()=>{this._isVisible&&this._beginUpdateDecorationsSoon()})),this._register(me.onDidChangeModelOptions(be=>{be.tabSize&&this._updateDecorationsRunner.schedule()}));const Ce=this._contextKeyService.createKey("isInDiffRightEditor",me.hasWidgetFocus());return this._register(me.onDidFocusEditorWidget(()=>Ce.set(!0))),this._register(me.onDidBlurEditorWidget(()=>Ce.set(!1))),this._register(me.onDidContentSizeChange(be=>{const Le=this._originalEditor.getContentWidth()+this._modifiedEditor.getContentWidth()+Ge.ONE_OVERVIEW_WIDTH,De=Math.max(this._modifiedEditor.getContentHeight(),this._originalEditor.getContentHeight());this._onDidContentSizeChange.fire({contentHeight:De,contentWidth:Le,contentHeightChanged:be.contentHeightChanged,contentWidthChanged:be.contentWidthChanged})})),me}_createInnerEditor(re,ce,me,Ce){return re.createInstance(o.CodeEditorWidget,ce,me,Ce)}dispose(){this._codeEditorService.removeDiffEditor(this),this._beginUpdateDecorationsTimeout!==-1&&(window.clearTimeout(this._beginUpdateDecorationsTimeout),this._beginUpdateDecorationsTimeout=-1),this._cleanViewZonesAndDecorations(),this._originalOverviewRuler&&(this._overviewDomElement.removeChild(this._originalOverviewRuler.getDomNode()),this._originalOverviewRuler.dispose()),this._modifiedOverviewRuler&&(this._overviewDomElement.removeChild(this._modifiedOverviewRuler.getDomNode()),this._modifiedOverviewRuler.dispose()),this._overviewDomElement.removeChild(this._overviewViewportDomElement.domNode),this._renderOverviewRuler&&this._containerDomElement.removeChild(this._overviewDomElement),this._containerDomElement.removeChild(this._originalDomNode),this._originalEditor.dispose(),this._containerDomElement.removeChild(this._modifiedDomNode),this._modifiedEditor.dispose(),this._strategy.dispose(),this._containerDomElement.removeChild(this._reviewPane.domNode.domNode),this._containerDomElement.removeChild(this._reviewPane.shadow.domNode),this._containerDomElement.removeChild(this._reviewPane.actionBarContainer.domNode),this._reviewPane.dispose(),this._domElement.removeChild(this._containerDomElement),this._onDidDispose.fire(),super.dispose()}getId(){return this.getEditorType()+":"+this._id}getEditorType(){return i.EditorType.IDiffEditor}getLineChanges(){return this._diffComputationResult?this._diffComputationResult.changes:null}getOriginalEditor(){return this._originalEditor}getModifiedEditor(){return this._modifiedEditor}updateOptions(re){let ce=!1;typeof re.renderSideBySide!="undefined"&&this._renderSideBySide!==re.renderSideBySide&&(this._renderSideBySide=re.renderSideBySide,ce=!0),typeof re.maxComputationTime!="undefined"&&(this._maxComputationTime=re.maxComputationTime,this._isVisible&&this._beginUpdateDecorationsSoon());let me=!1;typeof re.ignoreTrimWhitespace!="undefined"&&this._ignoreTrimWhitespace!==re.ignoreTrimWhitespace&&(this._ignoreTrimWhitespace=re.ignoreTrimWhitespace,me=!0),typeof re.renderIndicators!="undefined"&&this._renderIndicators!==re.renderIndicators&&(this._renderIndicators=re.renderIndicators,me=!0),me&&this._beginUpdateDecorations(),this._originalIsEditable=a.boolean(re.originalEditable,this._originalIsEditable),this._diffCodeLens=a.boolean(re.diffCodeLens,this._diffCodeLens),this._diffWordWrap=G(re.diffWordWrap,this._diffWordWrap),this._modifiedEditor.updateOptions(this._adjustOptionsForRightHandSide(re)),this._originalEditor.updateOptions(this._adjustOptionsForLeftHandSide(re)),typeof re.enableSplitViewResizing!="undefined"&&(this._enableSplitViewResizing=re.enableSplitViewResizing),this._strategy.setEnableSplitViewResizing(this._enableSplitViewResizing),ce&&(this._renderSideBySide?this._setStrategy(new $(this._createDataSource(),this._enableSplitViewResizing)):this._setStrategy(new oe(this._createDataSource(),this._enableSplitViewResizing)),this._containerDomElement.className=Ge._getClassName(this._themeService.getColorTheme(),this._renderSideBySide)),typeof re.renderOverviewRuler!="undefined"&&this._renderOverviewRuler!==re.renderOverviewRuler&&(this._renderOverviewRuler=re.renderOverviewRuler,this._renderOverviewRuler?this._containerDomElement.appendChild(this._overviewDomElement):this._containerDomElement.removeChild(this._overviewDomElement))}getModel(){return{original:this._originalEditor.getModel(),modified:this._modifiedEditor.getModel()}}setModel(re){if(re&&(!re.original||!re.modified))throw new Error(re.original?"DiffEditorWidget.setModel: Modified model is null":"DiffEditorWidget.setModel: Original model is null");this._cleanViewZonesAndDecorations(),this._originalEditor.setModel(re?re.original:null),this._modifiedEditor.setModel(re?re.modified:null),this._updateDecorationsRunner.cancel(),re&&(this._originalEditor.setScrollTop(0),this._modifiedEditor.setScrollTop(0)),this._diffComputationResult=null,this._diffComputationToken++,this._setState(0),re&&(this._recreateOverviewRulers(),this._beginUpdateDecorations()),this._layoutOverviewViewport()}getDomNode(){return this._domElement}getVisibleColumnFromPosition(re){return this._modifiedEditor.getVisibleColumnFromPosition(re)}getPosition(){return this._modifiedEditor.getPosition()}setPosition(re){this._modifiedEditor.setPosition(re)}revealLine(re,ce=0){this._modifiedEditor.revealLine(re,ce)}revealLineInCenter(re,ce=0){this._modifiedEditor.revealLineInCenter(re,ce)}revealLineInCenterIfOutsideViewport(re,ce=0){this._modifiedEditor.revealLineInCenterIfOutsideViewport(re,ce)}revealLineNearTop(re,ce=0){this._modifiedEditor.revealLineNearTop(re,ce)}revealPosition(re,ce=0){this._modifiedEditor.revealPosition(re,ce)}revealPositionInCenter(re,ce=0){this._modifiedEditor.revealPositionInCenter(re,ce)}revealPositionInCenterIfOutsideViewport(re,ce=0){this._modifiedEditor.revealPositionInCenterIfOutsideViewport(re,ce)}revealPositionNearTop(re,ce=0){this._modifiedEditor.revealPositionNearTop(re,ce)}getSelection(){return this._modifiedEditor.getSelection()}getSelections(){return this._modifiedEditor.getSelections()}setSelection(re){this._modifiedEditor.setSelection(re)}setSelections(re){this._modifiedEditor.setSelections(re)}revealLines(re,ce,me=0){this._modifiedEditor.revealLines(re,ce,me)}revealLinesInCenter(re,ce,me=0){this._modifiedEditor.revealLinesInCenter(re,ce,me)}revealLinesInCenterIfOutsideViewport(re,ce,me=0){this._modifiedEditor.revealLinesInCenterIfOutsideViewport(re,ce,me)}revealLinesNearTop(re,ce,me=0){this._modifiedEditor.revealLinesNearTop(re,ce,me)}revealRange(re,ce=0,me=!1,Ce=!0){this._modifiedEditor.revealRange(re,ce,me,Ce)}revealRangeInCenter(re,ce=0){this._modifiedEditor.revealRangeInCenter(re,ce)}revealRangeInCenterIfOutsideViewport(re,ce=0){this._modifiedEditor.revealRangeInCenterIfOutsideViewport(re,ce)}revealRangeNearTop(re,ce=0){this._modifiedEditor.revealRangeNearTop(re,ce)}revealRangeNearTopIfOutsideViewport(re,ce=0){this._modifiedEditor.revealRangeNearTopIfOutsideViewport(re,ce)}revealRangeAtTop(re,ce=0){this._modifiedEditor.revealRangeAtTop(re,ce)}getSupportedActions(){return this._modifiedEditor.getSupportedActions()}saveViewState(){const re=this._originalEditor.saveViewState(),ce=this._modifiedEditor.saveViewState();return{original:re,modified:ce}}restoreViewState(re){if(re&&re.original&&re.modified){const ce=re;this._originalEditor.restoreViewState(ce.original),this._modifiedEditor.restoreViewState(ce.modified)}}layout(re){this._elementSizeObserver.observe(re)}focus(){this._modifiedEditor.focus()}hasTextFocus(){return this._originalEditor.hasTextFocus()||this._modifiedEditor.hasTextFocus()}trigger(re,ce,me){this._modifiedEditor.trigger(re,ce,me)}changeDecorations(re){return this._modifiedEditor.changeDecorations(re)}_onDidContainerSizeChanged(){this._doLayout()}_getReviewHeight(){return this._reviewPane.isVisible()?this._elementSizeObserver.getHeight():0}_layoutOverviewRulers(){if(!!this._renderOverviewRuler&&!(!this._originalOverviewRuler||!this._modifiedOverviewRuler)){const re=this._elementSizeObserver.getHeight(),ce=this._getReviewHeight(),me=Ge.ENTIRE_DIFF_OVERVIEW_WIDTH-2*Ge.ONE_OVERVIEW_WIDTH;this._modifiedEditor.getLayoutInfo()&&(this._originalOverviewRuler.setLayout({top:0,width:Ge.ONE_OVERVIEW_WIDTH,right:me+Ge.ONE_OVERVIEW_WIDTH,height:re-ce}),this._modifiedOverviewRuler.setLayout({top:0,right:0,width:Ge.ONE_OVERVIEW_WIDTH,height:re-ce}))}}_onViewZonesChanged(){this._currentlyChangingViewZones||this._updateDecorationsRunner.schedule()}_beginUpdateDecorationsSoon(){this._beginUpdateDecorationsTimeout!==-1&&(window.clearTimeout(this._beginUpdateDecorationsTimeout),this._beginUpdateDecorationsTimeout=-1),this._beginUpdateDecorationsTimeout=window.setTimeout(()=>this._beginUpdateDecorations(),Ge.UPDATE_DIFF_DECORATIONS_DELAY)}static _equals(re,ce){return!re&&!ce?!0:!re||!ce?!1:re.toString()===ce.toString()}_beginUpdateDecorations(){this._beginUpdateDecorationsTimeout=-1;const re=this._originalEditor.getModel(),ce=this._modifiedEditor.getModel();if(!(!re||!ce)){this._diffComputationToken++;const me=this._diffComputationToken;if(this._setState(1),!this._editorWorkerService.canComputeDiff(re.uri,ce.uri)){(!Ge._equals(re.uri,this._lastOriginalWarning)||!Ge._equals(ce.uri,this._lastModifiedWarning))&&(this._lastOriginalWarning=re.uri,this._lastModifiedWarning=ce.uri,this._notificationService.warn(b.localize(2,null)));return}this._editorWorkerService.computeDiff(re.uri,ce.uri,this._ignoreTrimWhitespace,this._maxComputationTime).then(Ce=>{me===this._diffComputationToken&&re===this._originalEditor.getModel()&&ce===this._modifiedEditor.getModel()&&(this._setState(2),this._diffComputationResult=Ce,this._updateDecorationsRunner.schedule(),this._onDidUpdateDiff.fire())},Ce=>{me===this._diffComputationToken&&re===this._originalEditor.getModel()&&ce===this._modifiedEditor.getModel()&&(this._setState(2),this._diffComputationResult=null,this._updateDecorationsRunner.schedule())})}}_cleanViewZonesAndDecorations(){this._originalEditorState.clean(this._originalEditor),this._modifiedEditorState.clean(this._modifiedEditor)}_updateDecorations(){if(!(!this._originalEditor.getModel()||!this._modifiedEditor.getModel())){const re=this._diffComputationResult?this._diffComputationResult.changes:[],ce=this._originalEditorState.getForeignViewZones(this._originalEditor.getWhitespaces()),me=this._modifiedEditorState.getForeignViewZones(this._modifiedEditor.getWhitespaces()),Ce=this._strategy.getEditorsDiffDecorations(re,this._ignoreTrimWhitespace,this._renderIndicators,ce,me);try{this._currentlyChangingViewZones=!0,this._originalEditorState.apply(this._originalEditor,this._originalOverviewRuler,Ce.original,!1),this._modifiedEditorState.apply(this._modifiedEditor,this._modifiedOverviewRuler,Ce.modified,!0)}finally{this._currentlyChangingViewZones=!1}}}_adjustOptionsForSubEditor(re){const ce=Object.assign({},re);return ce.inDiffEditor=!0,ce.automaticLayout=!1,ce.scrollbar=Object.assign({},ce.scrollbar||{}),ce.scrollbar.vertical="visible",ce.folding=!1,ce.codeLens=this._diffCodeLens,ce.fixedOverflowWidgets=!0,ce.minimap=Object.assign({},ce.minimap||{}),ce.minimap.enabled=!1,ce}_adjustOptionsForLeftHandSide(re){const ce=this._adjustOptionsForSubEditor(re);return this._renderSideBySide?ce.wordWrapOverride1=this._diffWordWrap:ce.wordWrapOverride1="off",re.originalAriaLabel&&(ce.ariaLabel=re.originalAriaLabel),ce.readOnly=!this._originalIsEditable,ce.extraEditorClassName="original-in-monaco-diff-editor",Object.assign(Object.assign({},ce),{dimension:{height:0,width:0}})}_adjustOptionsForRightHandSide(re){const ce=this._adjustOptionsForSubEditor(re);return re.modifiedAriaLabel&&(ce.ariaLabel=re.modifiedAriaLabel),ce.wordWrapOverride1=this._diffWordWrap,ce.revealHorizontalRightPadding=a.EditorOptions.revealHorizontalRightPadding.defaultValue+Ge.ENTIRE_DIFF_OVERVIEW_WIDTH,ce.scrollbar.verticalHasArrows=!1,ce.extraEditorClassName="modified-in-monaco-diff-editor",Object.assign(Object.assign({},ce),{dimension:{height:0,width:0}})}doLayout(){this._elementSizeObserver.observe(),this._doLayout()}_doLayout(){const re=this._elementSizeObserver.getWidth(),ce=this._elementSizeObserver.getHeight(),me=this._getReviewHeight(),Ce=this._strategy.layout();this._originalDomNode.style.width=Ce+"px",this._originalDomNode.style.left="0px",this._modifiedDomNode.style.width=re-Ce+"px",this._modifiedDomNode.style.left=Ce+"px",this._overviewDomElement.style.top="0px",this._overviewDomElement.style.height=ce-me+"px",this._overviewDomElement.style.width=Ge.ENTIRE_DIFF_OVERVIEW_WIDTH+"px",this._overviewDomElement.style.left=re-Ge.ENTIRE_DIFF_OVERVIEW_WIDTH+"px",this._overviewViewportDomElement.setWidth(Ge.ENTIRE_DIFF_OVERVIEW_WIDTH),this._overviewViewportDomElement.setHeight(30),this._originalEditor.layout({width:Ce,height:ce-me}),this._modifiedEditor.layout({width:re-Ce-(this._renderOverviewRuler?Ge.ENTIRE_DIFF_OVERVIEW_WIDTH:0),height:ce-me}),(this._originalOverviewRuler||this._modifiedOverviewRuler)&&this._layoutOverviewRulers(),this._reviewPane.layout(ce-me,re,me),this._layoutOverviewViewport()}_layoutOverviewViewport(){const re=this._computeOverviewViewport();re?(this._overviewViewportDomElement.setTop(re.top),this._overviewViewportDomElement.setHeight(re.height)):(this._overviewViewportDomElement.setTop(0),this._overviewViewportDomElement.setHeight(0))}_computeOverviewViewport(){const re=this._modifiedEditor.getLayoutInfo();if(!re)return null;const ce=this._modifiedEditor.getScrollTop(),me=this._modifiedEditor.getScrollHeight(),Ce=Math.max(0,re.height),be=Math.max(0,Ce-2*0),Le=me>0?be/me:0,De=Math.max(0,Math.floor(re.height*Le)),Re=Math.floor(ce*Le);return{height:De,top:Re}}_createDataSource(){return{getWidth:()=>this._elementSizeObserver.getWidth(),getHeight:()=>this._elementSizeObserver.getHeight()-this._getReviewHeight(),getOptions:()=>({renderOverviewRuler:this._renderOverviewRuler}),getContainerDomNode:()=>this._containerDomElement,relayoutEditors:()=>{this._doLayout()},getOriginalEditor:()=>this._originalEditor,getModifiedEditor:()=>this._modifiedEditor}}_setStrategy(re){this._strategy&&this._strategy.dispose(),this._strategy=re,re.applyColors(this._themeService.getColorTheme()),this._diffComputationResult&&this._updateDecorations(),this._doLayout()}_getLineChangeAtOrBeforeLineNumber(re,ce){const me=this._diffComputationResult?this._diffComputationResult.changes:[];if(me.length===0||re=Re?Ce=Le+1:(Ce=Le,be=Le)}return me[Ce]}_getEquivalentLineForOriginalLineNumber(re){const ce=this._getLineChangeAtOrBeforeLineNumber(re,Re=>Re.originalStartLineNumber);if(!ce)return re;const me=ce.originalStartLineNumber+(ce.originalEndLineNumber>0?-1:0),Ce=ce.modifiedStartLineNumber+(ce.modifiedEndLineNumber>0?-1:0),be=ce.originalEndLineNumber>0?ce.originalEndLineNumber-ce.originalStartLineNumber+1:0,Le=ce.modifiedEndLineNumber>0?ce.modifiedEndLineNumber-ce.modifiedStartLineNumber+1:0,De=re-me;return De<=be?Ce+Math.min(De,Le):Ce+Le-be+De}_getEquivalentLineForModifiedLineNumber(re){const ce=this._getLineChangeAtOrBeforeLineNumber(re,Re=>Re.modifiedStartLineNumber);if(!ce)return re;const me=ce.originalStartLineNumber+(ce.originalEndLineNumber>0?-1:0),Ce=ce.modifiedStartLineNumber+(ce.modifiedEndLineNumber>0?-1:0),be=ce.originalEndLineNumber>0?ce.originalEndLineNumber-ce.originalStartLineNumber+1:0,Le=ce.modifiedEndLineNumber>0?ce.modifiedEndLineNumber-ce.modifiedStartLineNumber+1:0,De=re-Ce;return De<=Le?me+Math.min(De,be):me+be-Le+De}getDiffLineInformationForOriginal(re){return this._diffComputationResult?{equivalentLineNumber:this._getEquivalentLineForOriginalLineNumber(re)}:null}getDiffLineInformationForModified(re){return this._diffComputationResult?{equivalentLineNumber:this._getEquivalentLineForModifiedLineNumber(re)}:null}};X.ONE_OVERVIEW_WIDTH=15,X.ENTIRE_DIFF_OVERVIEW_WIDTH=30,X.UPDATE_DIFF_DECORATIONS_DELAY=200,X=Me([_e(3,O.IClipboardService),_e(4,t.IEditorWorkerService),_e(5,f.IContextKeyService),_e(6,v.IInstantiationService),_e(7,c.ICodeEditorService),_e(8,k.IThemeService),_e(9,L.INotificationService),_e(10,E.IContextMenuService),_e(11,F.IEditorProgressService)],X),e.DiffEditorWidget=X;class z extends d.Disposable{constructor(re){super();this._dataSource=re,this._insertColor=null,this._removeColor=null}applyColors(re){const ce=(re.getColor(I.diffInserted)||I.defaultInsertColor).transparent(2),me=(re.getColor(I.diffRemoved)||I.defaultRemoveColor).transparent(2),Ce=!ce.equals(this._insertColor)||!me.equals(this._removeColor);return this._insertColor=ce,this._removeColor=me,Ce}getEditorsDiffDecorations(re,ce,me,Ce,be){be=be.sort((Ee,Ae)=>Ee.afterLineNumber-Ae.afterLineNumber),Ce=Ce.sort((Ee,Ae)=>Ee.afterLineNumber-Ae.afterLineNumber);const Le=this._getViewZones(re,Ce,be,me),De=this._getOriginalEditorDecorations(re,ce,me),Re=this._getModifiedEditorDecorations(re,ce,me);return{original:{decorations:De.decorations,overviewZones:De.overviewZones,zones:Le.original},modified:{decorations:Re.decorations,overviewZones:Re.overviewZones,zones:Le.modified}}}}class P{constructor(re){this._source=re,this._index=-1,this.current=null,this.advance()}advance(){this._index++,this._indexOe.afterLineNumber-Fe.afterLineNumber,pe=(Oe,Fe)=>{if(Fe.domNode===null&&Oe.length>0){const Pe=Oe[Oe.length-1];if(Pe.afterLineNumber===Fe.afterLineNumber&&Pe.domNode===null){Pe.heightInLines+=Fe.heightInLines;return}}Oe.push(Fe)},ve=new P(this._modifiedForeignVZ),ke=new P(this._originalForeignVZ);let Ne=1,Te=1;for(let Oe=0,Fe=this._lineChanges.length;Oe<=Fe;Oe++){const Pe=Oe0?-1:0),ye=Pe.modifiedStartLineNumber+(Pe.modifiedEndLineNumber>0?-1:0),Se=Pe.originalEndLineNumber>0?V._getViewLineCount(this._originalEditor,Pe.originalStartLineNumber,Pe.originalEndLineNumber):0,Ae=Pe.modifiedEndLineNumber>0?V._getViewLineCount(this._modifiedEditor,Pe.modifiedStartLineNumber,Pe.modifiedEndLineNumber):0,fe=Math.max(Pe.originalStartLineNumber,Pe.originalEndLineNumber),de=Math.max(Pe.modifiedStartLineNumber,Pe.modifiedEndLineNumber)):(we+=1e7+Se,ye+=1e7+Ae,fe=we,de=ye);let xe=[],We=[];if(be){let Be;Pe?Pe.originalEndLineNumber>0?Be=Pe.originalStartLineNumber-Ne:Be=Pe.modifiedStartLineNumber-Te:Be=Le.getLineCount()-Ne;for(let He=0;Heje&&We.push({afterLineNumber:Ue,heightInLines:Ye-je,domNode:null,marginDomNode:null})}Pe&&(Ne=(Pe.originalEndLineNumber>0?Pe.originalEndLineNumber:Pe.originalStartLineNumber)+1,Te=(Pe.modifiedEndLineNumber>0?Pe.modifiedEndLineNumber:Pe.modifiedStartLineNumber)+1)}for(;ve.current&&ve.current.afterLineNumber<=de;){let Be;ve.current.afterLineNumber<=ye?Be=we-ye+ve.current.afterLineNumber:Be=fe;let He=null;Pe&&Pe.modifiedStartLineNumber<=ve.current.afterLineNumber&&ve.current.afterLineNumber<=Pe.modifiedEndLineNumber&&(He=this._createOriginalMarginDomNodeForModifiedForeignViewZoneInAddedRegion()),xe.push({afterLineNumber:Be,heightInLines:ve.current.height/ce,domNode:null,marginDomNode:He}),ve.advance()}for(;ke.current&&ke.current.afterLineNumber<=fe;){let Be;ke.current.afterLineNumber<=we?Be=ye-we+ke.current.afterLineNumber:Be=de,We.push({afterLineNumber:Be,heightInLines:ke.current.height/re,domNode:null}),ke.advance()}if(Pe!==null&&j(Pe)){const Be=this._produceOriginalFromDiff(Pe,Se,Ae);Be&&xe.push(Be)}if(Pe!==null&&te(Pe)){const Be=this._produceModifiedFromDiff(Pe,Se,Ae);Be&&We.push(Be)}let ze=0,Ke=0;for(xe=xe.sort(ge),We=We.sort(ge);ze=He.heightInLines?(Be.heightInLines-=He.heightInLines,Ke++):(He.heightInLines-=Be.heightInLines,ze++)}for(;ze(ce.domNode||(ce.domNode=Z()),ce))}}function U(he,re,ce,me,Ce){return{range:new u.Range(he,re,ce,me),options:Ce}}const H={charDelete:n.ModelDecorationOptions.register({className:"char-delete"}),charDeleteWholeLine:n.ModelDecorationOptions.register({className:"char-delete",isWholeLine:!0}),charInsert:n.ModelDecorationOptions.register({className:"char-insert"}),charInsertWholeLine:n.ModelDecorationOptions.register({className:"char-insert",isWholeLine:!0}),lineInsert:n.ModelDecorationOptions.register({className:"line-insert",marginClassName:"line-insert",isWholeLine:!0}),lineInsertWithSign:n.ModelDecorationOptions.register({className:"line-insert",linesDecorationsClassName:"insert-sign "+k.ThemeIcon.asClassName(se),marginClassName:"line-insert",isWholeLine:!0}),lineDelete:n.ModelDecorationOptions.register({className:"line-delete",marginClassName:"line-delete",isWholeLine:!0}),lineDeleteWithSign:n.ModelDecorationOptions.register({className:"line-delete",linesDecorationsClassName:"delete-sign "+k.ThemeIcon.asClassName(ne),marginClassName:"line-delete",isWholeLine:!0}),lineDeleteMargin:n.ModelDecorationOptions.register({marginClassName:"line-delete"})};class $ extends z{constructor(re,ce){super(re);this._disableSash=ce===!1,this._sashRatio=null,this._sashPosition=null,this._startSashPosition=null,this._sash=this._register(new w.Sash(this._dataSource.getContainerDomNode(),this,{orientation:0})),this._disableSash&&(this._sash.state=0),this._sash.onDidStart(()=>this._onSashDragStart()),this._sash.onDidChange(me=>this._onSashDrag(me)),this._sash.onDidEnd(()=>this._onSashDragEnd()),this._sash.onDidReset(()=>this._onSashReset())}setEnableSplitViewResizing(re){const ce=re===!1;this._disableSash!==ce&&(this._disableSash=ce,this._sash.state=this._disableSash?0:3)}layout(re=this._sashRatio){const me=this._dataSource.getWidth()-(this._dataSource.getOptions().renderOverviewRuler?X.ENTIRE_DIFF_OVERVIEW_WIDTH:0);let Ce=Math.floor((re||.5)*me);const be=Math.floor(.5*me);return Ce=this._disableSash?be:Ce||be,me>$.MINIMUM_EDITOR_WIDTH*2?(Ce<$.MINIMUM_EDITOR_WIDTH&&(Ce=$.MINIMUM_EDITOR_WIDTH),Ce>me-$.MINIMUM_EDITOR_WIDTH&&(Ce=me-$.MINIMUM_EDITOR_WIDTH)):Ce=be,this._sashPosition!==Ce&&(this._sashPosition=Ce,this._sash.layout()),this._sashPosition}_onSashDragStart(){this._startSashPosition=this._sashPosition}_onSashDrag(re){const me=this._dataSource.getWidth()-(this._dataSource.getOptions().renderOverviewRuler?X.ENTIRE_DIFF_OVERVIEW_WIDTH:0),Ce=this.layout((this._startSashPosition+(re.currentX-re.startX))/me);this._sashRatio=Ce/me,this._dataSource.relayoutEditors()}_onSashDragEnd(){this._sash.layout()}_onSashReset(){this._sashRatio=.5,this._dataSource.relayoutEditors(),this._sash.layout()}getVerticalSashTop(re){return 0}getVerticalSashLeft(re){return this._sashPosition}getVerticalSashHeight(re){return this._dataSource.getHeight()}_getViewZones(re,ce,me){const Ce=this._dataSource.getOriginalEditor(),be=this._dataSource.getModifiedEditor();return new ie(re,ce,me,Ce,be).getViewZones()}_getOriginalEditorDecorations(re,ce,me){const Ce=this._dataSource.getOriginalEditor(),be=String(this._removeColor),Le={decorations:[],overviewZones:[]},De=Ce.getModel(),Re=Ce._getViewModel();for(const Ee of re)if(te(Ee)){Le.decorations.push({range:new u.Range(Ee.originalStartLineNumber,1,Ee.originalEndLineNumber,1073741824),options:me?H.lineDeleteWithSign:H.lineDelete}),(!j(Ee)||!Ee.charChanges)&&Le.decorations.push(U(Ee.originalStartLineNumber,1,Ee.originalEndLineNumber,1073741824,H.charDeleteWholeLine));const Ae=ue(De,Re,Ee.originalStartLineNumber,Ee.originalEndLineNumber);if(Le.overviewZones.push(new l.OverviewRulerZone(Ae.startLineNumber,Ae.endLineNumber,be)),Ee.charChanges){for(const Se of Ee.charChanges)if(te(Se))if(ce)for(let we=Se.originalStartLineNumber;we<=Se.originalEndLineNumber;we++){let ye,fe;we===Se.originalStartLineNumber?ye=Se.originalStartColumn:ye=De.getLineFirstNonWhitespaceColumn(we),we===Se.originalEndLineNumber?fe=Se.originalEndColumn:fe=De.getLineLastNonWhitespaceColumn(we),Le.decorations.push(U(we,ye,we,fe,H.charDelete))}else Le.decorations.push(U(Se.originalStartLineNumber,Se.originalStartColumn,Se.originalEndLineNumber,Se.originalEndColumn,H.charDelete))}}return Le}_getModifiedEditorDecorations(re,ce,me){const Ce=this._dataSource.getModifiedEditor(),be=String(this._insertColor),Le={decorations:[],overviewZones:[]},De=Ce.getModel(),Re=Ce._getViewModel();for(const Ee of re)if(j(Ee)){Le.decorations.push({range:new u.Range(Ee.modifiedStartLineNumber,1,Ee.modifiedEndLineNumber,1073741824),options:me?H.lineInsertWithSign:H.lineInsert}),(!te(Ee)||!Ee.charChanges)&&Le.decorations.push(U(Ee.modifiedStartLineNumber,1,Ee.modifiedEndLineNumber,1073741824,H.charInsertWholeLine));const Ae=ue(De,Re,Ee.modifiedStartLineNumber,Ee.modifiedEndLineNumber);if(Le.overviewZones.push(new l.OverviewRulerZone(Ae.startLineNumber,Ae.endLineNumber,be)),Ee.charChanges){for(const Se of Ee.charChanges)if(j(Se))if(ce)for(let we=Se.modifiedStartLineNumber;we<=Se.modifiedEndLineNumber;we++){let ye,fe;we===Se.modifiedStartLineNumber?ye=Se.modifiedStartColumn:ye=De.getLineFirstNonWhitespaceColumn(we),we===Se.modifiedEndLineNumber?fe=Se.modifiedEndColumn:fe=De.getLineLastNonWhitespaceColumn(we),Le.decorations.push(U(we,ye,we,fe,H.charInsert))}else Le.decorations.push(U(Se.modifiedStartLineNumber,Se.modifiedStartColumn,Se.modifiedEndLineNumber,Se.modifiedEndColumn,H.charInsert))}}return Le}}$.MINIMUM_EDITOR_WIDTH=100;class ie extends V{constructor(re,ce,me,Ce,be){super(re,ce,me,Ce,be)}_createOriginalMarginDomNodeForModifiedForeignViewZoneInAddedRegion(){return null}_produceOriginalFromDiff(re,ce,me){return me>ce?{afterLineNumber:Math.max(re.originalStartLineNumber,re.originalEndLineNumber),heightInLines:me-ce,domNode:null}:null}_produceModifiedFromDiff(re,ce,me){return ce>me?{afterLineNumber:Math.max(re.modifiedStartLineNumber,re.modifiedEndLineNumber),heightInLines:ce-me,domNode:null}:null}}class oe extends z{constructor(re,ce){super(re);this._decorationsLeft=re.getOriginalEditor().getLayoutInfo().decorationsLeft,this._register(re.getOriginalEditor().onDidLayoutChange(me=>{this._decorationsLeft!==me.decorationsLeft&&(this._decorationsLeft=me.decorationsLeft,re.relayoutEditors())}))}setEnableSplitViewResizing(re){}_getViewZones(re,ce,me,Ce){const be=this._dataSource.getOriginalEditor(),Le=this._dataSource.getModifiedEditor();return new ae(re,ce,me,be,Le,Ce).getViewZones()}_getOriginalEditorDecorations(re,ce,me){const Ce=String(this._removeColor),be={decorations:[],overviewZones:[]},Le=this._dataSource.getOriginalEditor(),De=Le.getModel(),Re=Le._getViewModel();for(const Ee of re)if(te(Ee)){be.decorations.push({range:new u.Range(Ee.originalStartLineNumber,1,Ee.originalEndLineNumber,1073741824),options:H.lineDeleteMargin});const Ae=ue(De,Re,Ee.originalStartLineNumber,Ee.originalEndLineNumber);be.overviewZones.push(new l.OverviewRulerZone(Ae.startLineNumber,Ae.endLineNumber,Ce))}return be}_getModifiedEditorDecorations(re,ce,me){const Ce=this._dataSource.getModifiedEditor(),be=String(this._insertColor),Le={decorations:[],overviewZones:[]},De=Ce.getModel(),Re=Ce._getViewModel();for(const Ee of re)if(j(Ee)){Le.decorations.push({range:new u.Range(Ee.modifiedStartLineNumber,1,Ee.modifiedEndLineNumber,1073741824),options:me?H.lineInsertWithSign:H.lineInsert});const Ae=ue(De,Re,Ee.modifiedStartLineNumber,Ee.modifiedEndLineNumber);if(Le.overviewZones.push(new l.OverviewRulerZone(Ae.startLineNumber,Ae.endLineNumber,be)),Ee.charChanges){for(const Se of Ee.charChanges)if(j(Se))if(ce)for(let we=Se.modifiedStartLineNumber;we<=Se.modifiedEndLineNumber;we++){let ye,fe;we===Se.modifiedStartLineNumber?ye=Se.modifiedStartColumn:ye=De.getLineFirstNonWhitespaceColumn(we),we===Se.modifiedEndLineNumber?fe=Se.modifiedEndColumn:fe=De.getLineLastNonWhitespaceColumn(we),Le.decorations.push(U(we,ye,we,fe,H.charInsert))}else Le.decorations.push(U(Se.modifiedStartLineNumber,Se.modifiedStartColumn,Se.modifiedEndLineNumber,Se.modifiedEndColumn,H.charInsert))}else Le.decorations.push(U(Ee.modifiedStartLineNumber,1,Ee.modifiedEndLineNumber,1073741824,H.charInsertWholeLine))}return Le}layout(){return Math.max(5,this._decorationsLeft)}}class ae extends V{constructor(re,ce,me,Ce,be,Le){super(re,ce,me,Ce,be);this._originalModel=Ce.getModel(),this._renderIndicators=Le,this._pendingLineChange=[],this._pendingViewZones=[],this._lineBreaksComputer=this._modifiedEditor._getViewModel().createLineBreaksComputer()}getViewZones(){const re=super.getViewZones();return this._finalize(re),re}_createOriginalMarginDomNodeForModifiedForeignViewZoneInAddedRegion(){const re=document.createElement("div");return re.className="inline-added-margin-view-zone",re}_produceOriginalFromDiff(re,ce,me){const Ce=document.createElement("div");return Ce.className="inline-added-margin-view-zone",{afterLineNumber:Math.max(re.originalStartLineNumber,re.originalEndLineNumber),heightInLines:me,domNode:document.createElement("div"),marginDomNode:Ce}}_produceModifiedFromDiff(re,ce,me){const Ce=document.createElement("div");Ce.className=`view-lines line-delete ${W.MOUSE_CURSOR_TEXT_CSS_CLASS_NAME}`;const be=document.createElement("div");be.className="inline-deleted-margin-view-zone";const Le={shouldNotShrink:!0,afterLineNumber:re.modifiedEndLineNumber===0?re.modifiedStartLineNumber:re.modifiedStartLineNumber-1,heightInLines:ce,minWidthInPx:0,domNode:Ce,marginDomNode:be,diff:{originalStartLineNumber:re.originalStartLineNumber,originalEndLineNumber:re.originalEndLineNumber,modifiedStartLineNumber:re.modifiedStartLineNumber,modifiedEndLineNumber:re.modifiedEndLineNumber,originalModel:this._originalModel,viewLineCounts:null}};for(let De=re.originalStartLineNumber;De<=re.originalEndLineNumber;De++)this._lineBreaksComputer.addRequest(this._originalModel.getLineContent(De),null);return this._pendingLineChange.push(re),this._pendingViewZones.push(Le),Le}_finalize(re){const ce=this._modifiedEditor.getOptions(),me=this._modifiedEditor.getModel().getOptions().tabSize,Ce=ce.get(38),be=ce.get(26),Le=Ce.typicalHalfwidthCharacterWidth,De=ce.get(88),Re=this._originalModel.mightContainNonBasicASCII(),Ee=this._originalModel.mightContainRTL(),Ae=ce.get(53),we=ce.get(124).decorationsWidth,ye=ce.get(100),fe=ce.get(83),de=ce.get(77),ge=ce.get(39),pe=this._lineBreaksComputer.finalize();let ve=0;for(let ke=0;ke0,We=r.createStringBuilder(1e4);let ze=0,Ke=0,Be=null;for(let Ue=Ne.originalStartLineNumber;Ue<=Ne.originalEndLineNumber;Ue++){const Ye=Ue-Ne.originalStartLineNumber,je=this._originalModel.getLineTokens(Ue),Xe=je.getLineContent(),$e=pe[ve++],Ze=h.LineDecoration.filter(Pe,Ue,1,Xe.length+1);if($e){let Qe=0;for(const Je of $e.breakOffsets){const nt=je.sliceAndInflate(Qe,Je,0),si=Xe.substring(Qe,Je);ze=Math.max(ze,this._renderOriginalLine(Ke++,si,nt,h.LineDecoration.extractWrapped(Ze,Qe,Je),xe,Re,Ee,Ce,be,Ae,we,ye,fe,de,ge,me,We,Fe)),Qe=Je}for(Be||(Be=[]);Be.lengthke.afterLineNumber-Ne.afterLineNumber)}_renderOriginalLine(re,ce,me,Ce,be,Le,De,Re,Ee,Ae,Se,we,ye,fe,de,ge,pe,ve){pe.appendASCIIString('
    ');const ke=_.ViewLineRenderingData.isBasicASCII(ce,Le),Ne=_.ViewLineRenderingData.containsRTL(ce,ke,De),Te=m.renderViewLine(new m.RenderLineInput(Re.isMonospace&&!Ee,Re.canUseHalfwidthRightwardsArrow,ce,!1,ke,Ne,0,me,Ce,ge,0,Re.spaceWidth,Re.middotWidth,Re.wsmiddotWidth,we,ye,fe,de!==a.EditorFontLigatures.OFF,null),pe);if(pe.appendASCIIString("
    "),this._renderIndicators){const Fe=document.createElement("div");Fe.className=`delete-sign ${k.ThemeIcon.asClassName(ne)}`,Fe.setAttribute("style",`position:absolute;top:${re*Ae}px;width:${Se}px;height:${Ae}px;right:0;`),ve.appendChild(Fe)}const Oe=Te.characterMapping.getAbsoluteOffsets();return Oe.length>0?Oe[Oe.length-1]:0}}function G(he,re){return a.stringSet(he,re,["off","on","inherit"])}function j(he){return he.modifiedEndLineNumber>0}function te(he){return he.originalEndLineNumber>0}function Z(){const he=document.createElement("div");return he.className="diagonal-fill",he}function ue(he,re,ce,me){const Ce=he.getLineCount();return ce=Math.min(Ce,Math.max(1,ce)),me=Math.min(Ce,Math.max(1,me)),re.coordinatesConverter.convertModelRangeToViewRange(new u.Range(ce,he.getLineMinColumn(ce),me,he.getLineMaxColumn(me)))}k.registerThemingParticipant((he,re)=>{const ce=he.getColor(I.diffInserted);ce&&(re.addRule(`.monaco-editor .line-insert, .monaco-editor .char-insert { background-color: ${ce}; }`),re.addRule(`.monaco-diff-editor .line-insert, .monaco-diff-editor .char-insert { background-color: ${ce}; }`),re.addRule(`.monaco-editor .inline-added-margin-view-zone { background-color: ${ce}; }`));const me=he.getColor(I.diffRemoved);me&&(re.addRule(`.monaco-editor .line-delete, .monaco-editor .char-delete { background-color: ${me}; }`),re.addRule(`.monaco-diff-editor .line-delete, .monaco-diff-editor .char-delete { background-color: ${me}; }`),re.addRule(`.monaco-editor .inline-deleted-margin-view-zone { background-color: ${me}; }`));const Ce=he.getColor(I.diffInsertedOutline);Ce&&re.addRule(`.monaco-editor .line-insert, .monaco-editor .char-insert { border: 1px ${he.type==="hc"?"dashed":"solid"} ${Ce}; }`);const be=he.getColor(I.diffRemovedOutline);be&&re.addRule(`.monaco-editor .line-delete, .monaco-editor .char-delete { border: 1px ${he.type==="hc"?"dashed":"solid"} ${be}; }`);const Le=he.getColor(I.scrollbarShadow);Le&&re.addRule(`.monaco-diff-editor.side-by-side .editor.modified { box-shadow: -6px 0 5px -5px ${Le}; }`);const De=he.getColor(I.diffBorder);De&&re.addRule(`.monaco-diff-editor.side-by-side .editor.modified { border-left: 1px solid ${De}; }`);const Re=he.getColor(I.scrollbarSliderBackground);Re&&re.addRule(` + .monaco-diff-editor .diffViewport { + background: ${Re}; + } + `);const Ee=he.getColor(I.scrollbarSliderHoverBackground);Ee&&re.addRule(` + .monaco-diff-editor .diffViewport:hover { + background: ${Ee}; + } + `);const Ae=he.getColor(I.scrollbarSliderActiveBackground);Ae&&re.addRule(` + .monaco-diff-editor .diffViewport:active { + background: ${Ae}; + } + `);const Se=he.getColor(I.diffDiagonalFill);re.addRule(` + .monaco-editor .diagonal-fill { + background-image: linear-gradient( + -45deg, + ${Se} 12.5%, + #0000 12.5%, #0000 50%, + ${Se} 50%, ${Se} 62.5%, + #0000 62.5%, #0000 100% + ); + background-size: 8px 8px; + } + `)})}),define(Q[657],J([0,1,473,7,47,160,104,52,15,12,2,17,8,3,142,22,11,571,27,80,341]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SimpleButton=e.FindWidget=e.FindWidgetViewZone=e.findNextMatchIcon=e.findPreviousMatchIcon=e.findReplaceAllIcon=e.findReplaceIcon=void 0;const l=t.registerIcon("find-selection",n.Codicon.selection,b.localize(0,null)),h=t.registerIcon("find-collapsed",n.Codicon.chevronRight,b.localize(1,null)),m=t.registerIcon("find-expanded",n.Codicon.chevronDown,b.localize(2,null));e.findReplaceIcon=t.registerIcon("find-replace",n.Codicon.replace,b.localize(3,null)),e.findReplaceAllIcon=t.registerIcon("find-replace-all",n.Codicon.replaceAll,b.localize(4,null)),e.findPreviousMatchIcon=t.registerIcon("find-previous-match",n.Codicon.arrowUp,b.localize(5,null)),e.findNextMatchIcon=t.registerIcon("find-next-match",n.Codicon.arrowDown,b.localize(6,null));const _=b.localize(7,null),f=b.localize(8,null),v=b.localize(9,null),y=b.localize(10,null),L=b.localize(11,null),I=b.localize(12,null),k=b.localize(13,null),E=b.localize(14,null),T=b.localize(15,null),O=b.localize(16,null),A=b.localize(17,null),B=b.localize(18,null,a.MATCHES_LIMIT),F=b.localize(19,null),D=b.localize(20,null),R=419,x=275-54;let K=69;const Y=33,ee="ctrlEnterReplaceAll.windows.donotask",se=c.isMacintosh?256:2048;class ne{constructor(U){this.afterLineNumber=U,this.heightInPx=Y,this.suppressMouseDown=!1,this.domNode=document.createElement("div"),this.domNode.className="dock-find-viewzone"}}e.FindWidgetViewZone=ne;function le(V,U,H){const $=!!U.match(/\n/);if(H&&$&&H.selectionStart>0){V.stopPropagation();return}}function X(V,U,H){const $=!!U.match(/\n/);if(H&&$&&H.selectionEndthis._updateHistoryDelayer.cancel())),this._register(this._state.onFindReplaceStateChange(Z=>this._onStateChanged(Z))),this._buildDomNode(),this._updateButtons(),this._tryUpdateWidgetWidth(),this._findInput.inputBox.layout(),this._register(this._codeEditor.onDidChangeConfiguration(Z=>{if(Z.hasChanged(75)&&(this._codeEditor.getOption(75)&&this._state.change({isReplaceRevealed:!1},!1),this._updateButtons()),Z.hasChanged(124)&&this._tryUpdateWidgetWidth(),Z.hasChanged(2)&&this.updateAccessibilitySupport(),Z.hasChanged(31)){const ue=this._codeEditor.getOption(31).addExtraSpaceOnTop;ue&&!this._viewZone&&(this._viewZone=new ne(0),this._showViewZone()),!ue&&this._viewZone&&this._removeViewZone()}})),this.updateAccessibilitySupport(),this._register(this._codeEditor.onDidChangeCursorSelection(()=>{this._isVisible&&this._updateToggleSelectionFindButton()})),this._register(this._codeEditor.onDidFocusEditorWidget(()=>Ie(this,void 0,void 0,function*(){if(this._isVisible){let Z=yield this._controller.getGlobalBufferTerm();Z&&Z!==this._state.searchString&&(this._state.change({searchString:Z},!1),this._findInput.select())}}))),this._findInputFocused=a.CONTEXT_FIND_INPUT_FOCUSED.bindTo(ae),this._findFocusTracker=this._register(N.trackFocus(this._findInput.inputBox.inputElement)),this._register(this._findFocusTracker.onDidFocus(()=>{this._findInputFocused.set(!0),this._updateSearchScope()})),this._register(this._findFocusTracker.onDidBlur(()=>{this._findInputFocused.set(!1)})),this._replaceInputFocused=a.CONTEXT_REPLACE_INPUT_FOCUSED.bindTo(ae),this._replaceFocusTracker=this._register(N.trackFocus(this._replaceInput.inputBox.inputElement)),this._register(this._replaceFocusTracker.onDidFocus(()=>{this._replaceInputFocused.set(!0),this._updateSearchScope()})),this._register(this._replaceFocusTracker.onDidBlur(()=>{this._replaceInputFocused.set(!1)})),this._codeEditor.addOverlayWidget(this),this._codeEditor.getOption(31).addExtraSpaceOnTop&&(this._viewZone=new ne(0)),this._applyTheme(G.getColorTheme()),this._register(G.onDidColorThemeChange(this._applyTheme.bind(this))),this._register(this._codeEditor.onDidChangeModel(()=>{!this._isVisible||(this._viewZoneId=void 0)})),this._register(this._codeEditor.onDidScrollChange(Z=>{if(Z.scrollTopChanged){this._layoutViewZone();return}setTimeout(()=>{this._layoutViewZone()},0)}))}getId(){return z.ID}getDomNode(){return this._domNode}getPosition(){return this._isVisible?{preference:0}:null}_onStateChanged(U){if(U.searchString){try{this._ignoreChangeEvent=!0,this._findInput.setValue(this._state.searchString)}finally{this._ignoreChangeEvent=!1}this._updateButtons()}if(U.replaceString&&(this._replaceInput.inputBox.value=this._state.replaceString),U.isRevealed&&(this._state.isRevealed?this._reveal():this._hide(!0)),U.isReplaceRevealed&&(this._state.isReplaceRevealed?!this._codeEditor.getOption(75)&&!this._isReplaceVisible&&(this._isReplaceVisible=!0,this._replaceInput.width=N.getTotalWidth(this._findInput.domNode),this._updateButtons(),this._replaceInput.inputBox.layout()):this._isReplaceVisible&&(this._isReplaceVisible=!1,this._updateButtons())),(U.isRevealed||U.isReplaceRevealed)&&(this._state.isRevealed||this._state.isReplaceRevealed)&&this._tryUpdateHeight()&&this._showViewZone(),U.isRegex&&this._findInput.setRegex(this._state.isRegex),U.wholeWord&&this._findInput.setWholeWords(this._state.wholeWord),U.matchCase&&this._findInput.setCaseSensitive(this._state.matchCase),U.preserveCase&&this._replaceInput.setPreserveCase(this._state.preserveCase),U.searchScope&&(this._state.searchScope?this._toggleSelectionFind.checked=!0:this._toggleSelectionFind.checked=!1,this._updateToggleSelectionFindButton()),U.searchString||U.matchesCount||U.matchesPosition){let H=this._state.searchString.length>0&&this._state.matchesCount===0;this._domNode.classList.toggle("no-results",H),this._updateMatchesCount(),this._updateButtons()}(U.searchString||U.currentMatch)&&this._layoutViewZone(),U.updateHistory&&this._delayedUpdateHistory(),U.loop&&this._updateButtons()}_delayedUpdateHistory(){this._updateHistoryDelayer.trigger(this._updateHistory.bind(this)).then(void 0,g.onUnexpectedError)}_updateHistory(){this._state.searchString&&this._findInput.inputBox.addToHistory(),this._state.replaceString&&this._replaceInput.inputBox.addToHistory()}_updateMatchesCount(){this._matchesCount.style.minWidth=K+"px",this._state.matchesCount>=a.MATCHES_LIMIT?this._matchesCount.title=B:this._matchesCount.title="",this._matchesCount.firstChild&&this._matchesCount.removeChild(this._matchesCount.firstChild);let U;if(this._state.matchesCount>0){let H=String(this._state.matchesCount);this._state.matchesCount>=a.MATCHES_LIMIT&&(H+="+");let $=String(this._state.matchesPosition);$==="0"&&($="?"),U=o.format(F,$,H)}else U=D;this._matchesCount.appendChild(document.createTextNode(U)),M.alert(this._getAriaLabel(U,this._state.currentMatch,this._state.searchString)),K=Math.max(K,this._matchesCount.clientWidth)}_getAriaLabel(U,H,$){if(U===D)return $===""?b.localize(21,null,U):b.localize(22,null,U,$);if(H){const ie=b.localize(23,null,U,$,H.startLineNumber+":"+H.startColumn),oe=this._codeEditor.getModel();return oe&&H.startLineNumber<=oe.getLineCount()&&H.startLineNumber>=1?`${oe.getLineContent(H.startLineNumber)}, ${ie}`:ie}return b.localize(24,null,U,$)}_updateToggleSelectionFindButton(){let U=this._codeEditor.getSelection(),H=U?U.startLineNumber!==U.endLineNumber||U.startColumn!==U.endColumn:!1,$=this._toggleSelectionFind.checked;this._isVisible&&($||H)?this._toggleSelectionFind.enable():this._toggleSelectionFind.disable()}_updateButtons(){this._findInput.setEnabled(this._isVisible),this._replaceInput.setEnabled(this._isVisible&&this._isReplaceVisible),this._updateToggleSelectionFindButton(),this._closeBtn.setEnabled(this._isVisible);let U=this._state.searchString.length>0,H=!!this._state.matchesCount;this._prevBtn.setEnabled(this._isVisible&&U&&H&&this._state.canNavigateBack()),this._nextBtn.setEnabled(this._isVisible&&U&&H&&this._state.canNavigateForward()),this._replaceBtn.setEnabled(this._isVisible&&this._isReplaceVisible&&U),this._replaceAllBtn.setEnabled(this._isVisible&&this._isReplaceVisible&&U),this._domNode.classList.toggle("replaceToggled",this._isReplaceVisible),this._toggleReplaceBtn.setExpanded(this._isReplaceVisible);let $=!this._codeEditor.getOption(75);this._toggleReplaceBtn.setEnabled(this._isVisible&&$)}_reveal(){if(this._revealTimeouts.forEach(U=>{clearTimeout(U)}),this._revealTimeouts=[],!this._isVisible){this._isVisible=!0;const U=this._codeEditor.getSelection();switch(this._codeEditor.getOption(31).autoFindInSelection){case"always":this._toggleSelectionFind.checked=!0;break;case"never":this._toggleSelectionFind.checked=!1;break;case"multiline":const $=!!U&&U.startLineNumber!==U.endLineNumber;this._toggleSelectionFind.checked=$;break;default:break}this._tryUpdateWidgetWidth(),this._updateButtons(),this._revealTimeouts.push(setTimeout(()=>{this._domNode.classList.add("visible"),this._domNode.setAttribute("aria-hidden","false")},0)),this._revealTimeouts.push(setTimeout(()=>{this._findInput.validate()},200)),this._codeEditor.layoutOverlayWidget(this);let H=!0;if(this._codeEditor.getOption(31).seedSearchStringFromSelection&&U){const $=this._codeEditor.getDomNode();if($){const ie=N.getDomNodePagePosition($),oe=this._codeEditor.getScrolledVisiblePosition(U.getStartPosition()),ae=ie.left+(oe?oe.left:0),G=oe?oe.top:0;if(this._viewZone&&GU.startLineNumber&&(H=!1);const j=N.getTopLeftOffset(this._domNode).left;ae>j&&(H=!1);const te=this._codeEditor.getScrolledVisiblePosition(U.getEndPosition());ie.left+(te?te.left:0)>j&&(H=!1)}}}this._showViewZone(H)}}_hide(U){this._revealTimeouts.forEach(H=>{clearTimeout(H)}),this._revealTimeouts=[],this._isVisible&&(this._isVisible=!1,this._updateButtons(),this._domNode.classList.remove("visible"),this._domNode.setAttribute("aria-hidden","true"),this._findInput.clearMessage(),U&&this._codeEditor.focus(),this._codeEditor.layoutOverlayWidget(this),this._removeViewZone())}_layoutViewZone(U){if(!this._codeEditor.getOption(31).addExtraSpaceOnTop){this._removeViewZone();return}if(!!this._isVisible){const $=this._viewZone;this._viewZoneId!==void 0||!$||this._codeEditor.changeViewZones(ie=>{$.heightInPx=this._getHeight(),this._viewZoneId=ie.addZone($),this._codeEditor.setScrollTop(U||this._codeEditor.getScrollTop()+$.heightInPx)})}}_showViewZone(U=!0){if(!!this._isVisible&&!!this._codeEditor.getOption(31).addExtraSpaceOnTop){this._viewZone===void 0&&(this._viewZone=new ne(0));const $=this._viewZone;this._codeEditor.changeViewZones(ie=>{if(this._viewZoneId!==void 0){const oe=this._getHeight();if(oe===$.heightInPx)return;let ae=oe-$.heightInPx;$.heightInPx=oe,ie.layoutZone(this._viewZoneId),U&&this._codeEditor.setScrollTop(this._codeEditor.getScrollTop()+ae);return}else{let oe=this._getHeight();if(oe-=this._codeEditor.getOption(69).top,oe<=0)return;$.heightInPx=oe,this._viewZoneId=ie.addZone($),U&&this._codeEditor.setScrollTop(this._codeEditor.getScrollTop()+oe)}})}}_removeViewZone(){this._codeEditor.changeViewZones(U=>{this._viewZoneId!==void 0&&(U.removeZone(this._viewZoneId),this._viewZoneId=void 0,this._viewZone&&(this._codeEditor.setScrollTop(this._codeEditor.getScrollTop()-this._viewZone.heightInPx),this._viewZone=void 0))})}_applyTheme(U){let H={inputActiveOptionBorder:U.getColor(u.inputActiveOptionBorder),inputActiveOptionBackground:U.getColor(u.inputActiveOptionBackground),inputActiveOptionForeground:U.getColor(u.inputActiveOptionForeground),inputBackground:U.getColor(u.inputBackground),inputForeground:U.getColor(u.inputForeground),inputBorder:U.getColor(u.inputBorder),inputValidationInfoBackground:U.getColor(u.inputValidationInfoBackground),inputValidationInfoForeground:U.getColor(u.inputValidationInfoForeground),inputValidationInfoBorder:U.getColor(u.inputValidationInfoBorder),inputValidationWarningBackground:U.getColor(u.inputValidationWarningBackground),inputValidationWarningForeground:U.getColor(u.inputValidationWarningForeground),inputValidationWarningBorder:U.getColor(u.inputValidationWarningBorder),inputValidationErrorBackground:U.getColor(u.inputValidationErrorBackground),inputValidationErrorForeground:U.getColor(u.inputValidationErrorForeground),inputValidationErrorBorder:U.getColor(u.inputValidationErrorBorder)};this._findInput.style(H),this._replaceInput.style(H),this._toggleSelectionFind.style(H)}_tryUpdateWidgetWidth(){if(!!this._isVisible&&!!N.isInDOM(this._domNode)){const U=this._codeEditor.getLayoutInfo();if(U.contentWidth<=0){this._domNode.classList.add("hiddenEditor");return}else this._domNode.classList.contains("hiddenEditor")&&this._domNode.classList.remove("hiddenEditor");const $=U.width,ie=U.minimap.minimapWidth;let oe=!1,ae=!1,G=!1;if(this._resized&&N.getTotalWidth(this._domNode)>R){this._domNode.style.maxWidth=`${$-28-ie-15}px`,this._replaceInput.width=N.getTotalWidth(this._findInput.domNode);return}if(R+28+ie>=$&&(ae=!0),R+28+ie-K>=$&&(G=!0),R+28+ie-K>=$+50&&(oe=!0),this._domNode.classList.toggle("collapsed-find-widget",oe),this._domNode.classList.toggle("narrow-find-widget",G),this._domNode.classList.toggle("reduced-find-widget",ae),!G&&!oe&&(this._domNode.style.maxWidth=`${$-28-ie-15}px`),this._resized){this._findInput.inputBox.layout();let j=this._findInput.inputBox.element.clientWidth;j>0&&(this._replaceInput.width=j)}else this._isReplaceVisible&&(this._replaceInput.width=N.getTotalWidth(this._findInput.domNode))}}_getHeight(){let U=0;return U+=4,U+=this._findInput.inputBox.height+2,this._isReplaceVisible&&(U+=4,U+=this._replaceInput.inputBox.height+2),U+=4,U}_tryUpdateHeight(){const U=this._getHeight();return this._cachedHeight!==null&&this._cachedHeight===U?!1:(this._cachedHeight=U,this._domNode.style.height=`${U}px`,!0)}focusFindInput(){this._findInput.select(),this._findInput.focus()}focusReplaceInput(){this._replaceInput.select(),this._replaceInput.focus()}highlightFindOptions(){this._findInput.highlightFindOptions()}_updateSearchScope(){if(!!this._codeEditor.hasModel()&&this._toggleSelectionFind.checked){let U=this._codeEditor.getSelections();U.map(H=>{H.endColumn===1&&H.endLineNumber>H.startLineNumber&&(H=H.setEndPosition(H.endLineNumber-1,this._codeEditor.getModel().getLineMaxColumn(H.endLineNumber-1)));const $=this._state.currentMatch;return H.startLineNumber!==H.endLineNumber&&!s.Range.equalsRange(H,$)?H:null}).filter(H=>!!H),U.length&&this._state.change({searchScope:U},!0)}}_onFindInputMouseDown(U){U.middleButton&&U.stopPropagation()}_onFindInputKeyDown(U){if(U.equals(se|3)){this._findInput.inputBox.insertAtCursor(` +`),U.preventDefault();return}if(U.equals(2)){this._isReplaceVisible?this._replaceInput.focus():this._findInput.focusOnCaseSensitive(),U.preventDefault();return}if(U.equals(2048|18)){this._codeEditor.focus(),U.preventDefault();return}if(U.equals(16))return le(U,this._findInput.getValue(),this._findInput.domNode.querySelector("textarea"));if(U.equals(18))return X(U,this._findInput.getValue(),this._findInput.domNode.querySelector("textarea"))}_onReplaceInputKeyDown(U){if(U.equals(se|3)){c.isWindows&&c.isNative&&!this._ctrlEnterReplaceAllWarningPrompted&&(this._notificationService.info(b.localize(25,null)),this._ctrlEnterReplaceAllWarningPrompted=!0,this._storageService.store(ee,!0,0,0)),this._replaceInput.inputBox.insertAtCursor(` +`),U.preventDefault();return}if(U.equals(2)){this._findInput.focusOnCaseSensitive(),U.preventDefault();return}if(U.equals(1024|2)){this._findInput.focus(),U.preventDefault();return}if(U.equals(2048|18)){this._codeEditor.focus(),U.preventDefault();return}if(U.equals(16))return le(U,this._replaceInput.inputBox.value,this._replaceInput.inputBox.element.querySelector("textarea"));if(U.equals(18))return X(U,this._replaceInput.inputBox.value,this._replaceInput.inputBox.element.querySelector("textarea"))}getVerticalSashLeft(U){return 0}_keybindingLabelFor(U){let H=this._keybindingService.lookupKeybinding(U);return H?` (${H.getLabel()})`:""}_buildDomNode(){const U=!0,H=!0;this._findInput=this._register(new i.ContextScopedFindInput(null,this._contextViewProvider,{width:x,label:_,placeholder:f,appendCaseSensitiveLabel:this._keybindingLabelFor(a.FIND_IDS.ToggleCaseSensitiveCommand),appendWholeWordsLabel:this._keybindingLabelFor(a.FIND_IDS.ToggleWholeWordCommand),appendRegexLabel:this._keybindingLabelFor(a.FIND_IDS.ToggleRegexCommand),validation:j=>{if(j.length===0||!this._findInput.getRegex())return null;try{return new RegExp(j,"gu"),null}catch(te){return{content:te.message}}},flexibleHeight:U,flexibleWidth:H,flexibleMaxHeight:118},this._contextKeyService,!0)),this._findInput.setRegex(!!this._state.isRegex),this._findInput.setCaseSensitive(!!this._state.matchCase),this._findInput.setWholeWords(!!this._state.wholeWord),this._register(this._findInput.onKeyDown(j=>this._onFindInputKeyDown(j))),this._register(this._findInput.inputBox.onDidChange(()=>{this._ignoreChangeEvent||this._state.change({searchString:this._findInput.getValue()},!0)})),this._register(this._findInput.onDidOptionChange(()=>{this._state.change({isRegex:this._findInput.getRegex(),wholeWord:this._findInput.getWholeWords(),matchCase:this._findInput.getCaseSensitive()},!0)})),this._register(this._findInput.onCaseSensitiveKeyDown(j=>{j.equals(1024|2)&&this._isReplaceVisible&&(this._replaceInput.focus(),j.preventDefault())})),this._register(this._findInput.onRegexKeyDown(j=>{j.equals(2)&&this._isReplaceVisible&&(this._replaceInput.focusOnPreserve(),j.preventDefault())})),this._register(this._findInput.inputBox.onDidHeightChange(j=>{this._tryUpdateHeight()&&this._showViewZone()})),c.isLinux&&this._register(this._findInput.onMouseDown(j=>this._onFindInputMouseDown(j))),this._matchesCount=document.createElement("div"),this._matchesCount.className="matchesCount",this._updateMatchesCount(),this._prevBtn=this._register(new P({label:v+this._keybindingLabelFor(a.FIND_IDS.PreviousMatchFindAction),icon:e.findPreviousMatchIcon,onTrigger:()=>{this._codeEditor.getAction(a.FIND_IDS.PreviousMatchFindAction).run().then(void 0,g.onUnexpectedError)}})),this._nextBtn=this._register(new P({label:y+this._keybindingLabelFor(a.FIND_IDS.NextMatchFindAction),icon:e.findNextMatchIcon,onTrigger:()=>{this._codeEditor.getAction(a.FIND_IDS.NextMatchFindAction).run().then(void 0,g.onUnexpectedError)}}));let $=document.createElement("div");$.className="find-part",$.appendChild(this._findInput.domNode);const ie=document.createElement("div");ie.className="find-actions",$.appendChild(ie),ie.appendChild(this._matchesCount),ie.appendChild(this._prevBtn.domNode),ie.appendChild(this._nextBtn.domNode),this._toggleSelectionFind=this._register(new w.Checkbox({icon:l,title:L+this._keybindingLabelFor(a.FIND_IDS.ToggleSearchScopeCommand),isChecked:!1})),this._register(this._toggleSelectionFind.onChange(()=>{if(this._toggleSelectionFind.checked){if(this._codeEditor.hasModel()){let j=this._codeEditor.getSelections();j.map(te=>(te.endColumn===1&&te.endLineNumber>te.startLineNumber&&(te=te.setEndPosition(te.endLineNumber-1,this._codeEditor.getModel().getLineMaxColumn(te.endLineNumber-1))),te.isEmpty()?null:te)).filter(te=>!!te),j.length&&this._state.change({searchScope:j},!0)}}else this._state.change({searchScope:null},!0)})),ie.appendChild(this._toggleSelectionFind.domNode),this._closeBtn=this._register(new P({label:I+this._keybindingLabelFor(a.FIND_IDS.CloseFindWidgetCommand),icon:t.widgetClose,onTrigger:()=>{this._state.change({isRevealed:!1,searchScope:null},!1)},onKeyDown:j=>{j.equals(2)&&this._isReplaceVisible&&(this._replaceBtn.isEnabled()?this._replaceBtn.focus():this._codeEditor.focus(),j.preventDefault())}})),ie.appendChild(this._closeBtn.domNode),this._replaceInput=this._register(new i.ContextScopedReplaceInput(null,void 0,{label:k,placeholder:E,appendPreserveCaseLabel:this._keybindingLabelFor(a.FIND_IDS.TogglePreserveCaseCommand),history:[],flexibleHeight:U,flexibleWidth:H,flexibleMaxHeight:118},this._contextKeyService,!0)),this._replaceInput.setPreserveCase(!!this._state.preserveCase),this._register(this._replaceInput.onKeyDown(j=>this._onReplaceInputKeyDown(j))),this._register(this._replaceInput.inputBox.onDidChange(()=>{this._state.change({replaceString:this._replaceInput.inputBox.value},!1)})),this._register(this._replaceInput.inputBox.onDidHeightChange(j=>{this._isReplaceVisible&&this._tryUpdateHeight()&&this._showViewZone()})),this._register(this._replaceInput.onDidOptionChange(()=>{this._state.change({preserveCase:this._replaceInput.getPreserveCase()},!0)})),this._register(this._replaceInput.onPreserveCaseKeyDown(j=>{j.equals(2)&&(this._prevBtn.isEnabled()?this._prevBtn.focus():this._nextBtn.isEnabled()?this._nextBtn.focus():this._toggleSelectionFind.enabled?this._toggleSelectionFind.focus():this._closeBtn.isEnabled()&&this._closeBtn.focus(),j.preventDefault())})),this._replaceBtn=this._register(new P({label:T+this._keybindingLabelFor(a.FIND_IDS.ReplaceOneAction),icon:e.findReplaceIcon,onTrigger:()=>{this._controller.replace()},onKeyDown:j=>{j.equals(1024|2)&&(this._closeBtn.focus(),j.preventDefault())}})),this._replaceAllBtn=this._register(new P({label:O+this._keybindingLabelFor(a.FIND_IDS.ReplaceAllAction),icon:e.findReplaceAllIcon,onTrigger:()=>{this._controller.replaceAll()}}));let oe=document.createElement("div");oe.className="replace-part",oe.appendChild(this._replaceInput.domNode);const ae=document.createElement("div");ae.className="replace-actions",oe.appendChild(ae),ae.appendChild(this._replaceBtn.domNode),ae.appendChild(this._replaceAllBtn.domNode),this._toggleReplaceBtn=this._register(new P({label:A,className:"codicon toggle left",onTrigger:()=>{this._state.change({isReplaceRevealed:!this._isReplaceVisible},!1),this._isReplaceVisible&&(this._replaceInput.width=N.getTotalWidth(this._findInput.domNode),this._replaceInput.inputBox.layout()),this._showViewZone()}})),this._toggleReplaceBtn.setExpanded(this._isReplaceVisible),this._domNode=document.createElement("div"),this._domNode.className="editor-widget find-widget",this._domNode.setAttribute("aria-hidden","true"),this._domNode.style.width=`${R}px`,this._domNode.appendChild(this._toggleReplaceBtn.domNode),this._domNode.appendChild($),this._domNode.appendChild(oe),this._resizeSash=new S.Sash(this._domNode,this,{orientation:0,size:2}),this._resized=!1;let G=R;this._register(this._resizeSash.onDidStart(()=>{G=N.getTotalWidth(this._domNode)})),this._register(this._resizeSash.onDidChange(j=>{this._resized=!0;let te=G+j.startX-j.currentX;if(!(teZ||(this._domNode.style.width=`${te}px`,this._isReplaceVisible&&(this._replaceInput.width=N.getTotalWidth(this._findInput.domNode)),this._findInput.inputBox.layout(),this._tryUpdateHeight())}})),this._register(this._resizeSash.onDidReset(()=>{const j=N.getTotalWidth(this._domNode);if(!(j{this._opts.onTrigger(),$.preventDefault()}),this.onkeydown(this._domNode,$=>{if($.equals(10)||$.equals(3)){this._opts.onTrigger(),$.preventDefault();return}this._opts.onKeyDown&&this._opts.onKeyDown($)})}get domNode(){return this._domNode}isEnabled(){return this._domNode.tabIndex>=0}focus(){this._domNode.focus()}setEnabled(U){this._domNode.classList.toggle("disabled",!U),this._domNode.setAttribute("aria-disabled",String(!U)),this._domNode.tabIndex=U?0:-1}setExpanded(U){this._domNode.setAttribute("aria-expanded",String(!!U)),U?(this._domNode.classList.remove(...r.ThemeIcon.asClassNameArray(h)),this._domNode.classList.add(...r.ThemeIcon.asClassNameArray(m))):(this._domNode.classList.remove(...r.ThemeIcon.asClassNameArray(m)),this._domNode.classList.add(...r.ThemeIcon.asClassNameArray(h)))}}e.SimpleButton=P,r.registerThemingParticipant((V,U)=>{const H=(re,ce)=>{ce&&U.addRule(`.monaco-editor ${re} { background-color: ${ce}; }`)};H(".findMatch",V.getColor(u.editorFindMatchHighlight)),H(".currentFindMatch",V.getColor(u.editorFindMatch)),H(".findScope",V.getColor(u.editorFindRangeHighlight));const $=V.getColor(u.editorWidgetBackground);H(".find-widget",$);const ie=V.getColor(u.widgetShadow);ie&&U.addRule(`.monaco-editor .find-widget { box-shadow: 0 0 8px 2px ${ie}; }`);const oe=V.getColor(u.editorFindMatchHighlightBorder);oe&&U.addRule(`.monaco-editor .findMatch { border: 1px ${V.type==="hc"?"dotted":"solid"} ${oe}; box-sizing: border-box; }`);const ae=V.getColor(u.editorFindMatchBorder);ae&&U.addRule(`.monaco-editor .currentFindMatch { border: 2px solid ${ae}; padding: 1px; box-sizing: border-box; }`);const G=V.getColor(u.editorFindRangeHighlightBorder);G&&U.addRule(`.monaco-editor .findScope { border: 1px ${V.type==="hc"?"dashed":"solid"} ${G}; }`);const j=V.getColor(u.contrastBorder);j&&U.addRule(`.monaco-editor .find-widget { border: 1px solid ${j}; }`);const te=V.getColor(u.editorWidgetForeground);te&&U.addRule(`.monaco-editor .find-widget { color: ${te}; }`);const Z=V.getColor(u.errorForeground);Z&&U.addRule(`.monaco-editor .find-widget.no-results .matchesCount { color: ${Z}; }`);const ue=V.getColor(u.editorWidgetResizeBorder);if(ue)U.addRule(`.monaco-editor .find-widget .monaco-sash { background-color: ${ue}; }`);else{const re=V.getColor(u.editorWidgetBorder);re&&U.addRule(`.monaco-editor .find-widget .monaco-sash { background-color: ${re}; }`)}const he=V.getColor(u.focusBorder);he&&U.addRule(`.monaco-editor .find-widget .monaco-inputbox.synthetic-focus { outline-color: ${he}; }`)})}),define(Q[267],J([0,1,472,15,2,8,13,25,142,599,600,657,34,84,16,68,37,79,11,32,28]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.StartFindReplaceAction=e.PreviousSelectionMatchFindAction=e.NextSelectionMatchFindAction=e.SelectionMatchFindAction=e.PreviousMatchFindAction2=e.PreviousMatchFindAction=e.NextMatchFindAction2=e.NextMatchFindAction=e.MatchFindAction=e.StartFindWithSelectionAction=e.StartFindAction=e.FindController=e.CommonFindController=e.getSelectionSearchString=void 0;const h=524288;function m(F,D="single"){if(!F.hasModel())return null;const R=F.getSelection();if(D==="single"&&R.startLineNumber===R.endLineNumber||D==="multiple"){if(R.isEmpty()){const W=F.getConfiguredWordAtPosition(R.getStartPosition());if(W)return W.word}else if(F.getModel().getValueLengthInRange(R)this._onStateChanged(K))),this._model=null,this._register(this._editor.onDidChangeModel(()=>{let K=this._editor.getModel()&&this._state.isRevealed;this.disposeModel(),this._state.change({searchScope:null,matchCase:this._storageService.getBoolean("editor.matchCase",1,!1),wholeWord:this._storageService.getBoolean("editor.wholeWord",1,!1),isRegex:this._storageService.getBoolean("editor.isRegex",1,!1),preserveCase:this._storageService.getBoolean("editor.preserveCase",1,!1)},!1),K&&this._start({forceRevealReplace:!1,seedSearchStringFromSelection:"none",seedSearchStringFromGlobalClipboard:!1,shouldFocus:0,shouldAnimate:!1,updateSearchScope:!1,loop:this._editor.getOption(31).loop})}))}get editor(){return this._editor}static get(D){return D.getContribution(qt.ID)}dispose(){this.disposeModel(),super.dispose()}disposeModel(){this._model&&(this._model.dispose(),this._model=null)}_onStateChanged(D){this.saveQueryState(D),D.isRevealed&&(this._state.isRevealed?this._findWidgetVisible.set(!0):(this._findWidgetVisible.reset(),this.disposeModel())),D.searchString&&this.setGlobalBufferTerm(this._state.searchString)}saveQueryState(D){D.isRegex&&this._storageService.store("editor.isRegex",this._state.actualIsRegex,1,0),D.wholeWord&&this._storageService.store("editor.wholeWord",this._state.actualWholeWord,1,0),D.matchCase&&this._storageService.store("editor.matchCase",this._state.actualMatchCase,1,0),D.preserveCase&&this._storageService.store("editor.preserveCase",this._state.actualPreserveCase,1,0)}loadQueryState(){this._state.change({matchCase:this._storageService.getBoolean("editor.matchCase",1,this._state.matchCase),wholeWord:this._storageService.getBoolean("editor.wholeWord",1,this._state.wholeWord),isRegex:this._storageService.getBoolean("editor.isRegex",1,this._state.isRegex),preserveCase:this._storageService.getBoolean("editor.preserveCase",1,this._state.preserveCase)},!1)}isFindInputFocused(){return!!d.CONTEXT_FIND_INPUT_FOCUSED.getValue(this._contextKeyService)}getState(){return this._state}closeFindWidget(){this._state.change({isRevealed:!1,searchScope:null},!1),this._editor.focus()}toggleCaseSensitive(){this._state.change({matchCase:!this._state.matchCase},!1),this._state.isRevealed||this.highlightFindOptions()}toggleWholeWords(){this._state.change({wholeWord:!this._state.wholeWord},!1),this._state.isRevealed||this.highlightFindOptions()}toggleRegex(){this._state.change({isRegex:!this._state.isRegex},!1),this._state.isRevealed||this.highlightFindOptions()}togglePreserveCase(){this._state.change({preserveCase:!this._state.preserveCase},!1),this._state.isRevealed||this.highlightFindOptions()}toggleSearchScope(){if(this._state.searchScope)this._state.change({searchScope:null},!0);else if(this._editor.hasModel()){let D=this._editor.getSelections();D.map(R=>(R.endColumn===1&&R.endLineNumber>R.startLineNumber&&(R=R.setEndPosition(R.endLineNumber-1,this._editor.getModel().getLineMaxColumn(R.endLineNumber-1))),R.isEmpty()?null:R)).filter(R=>!!R),D.length&&this._state.change({searchScope:D},!0)}}setSearchString(D){this._state.isRegex&&(D=w.escapeRegExpCharacters(D)),this._state.change({searchString:D},!1)}highlightFindOptions(D=!1){}_start(D){return Ie(this,void 0,void 0,function*(){if(this.disposeModel(),!!this._editor.hasModel()){let R={isRevealed:!0};if(D.seedSearchStringFromSelection==="single"){let W=m(this._editor,D.seedSearchStringFromSelection);W&&(this._state.isRegex?R.searchString=w.escapeRegExpCharacters(W):R.searchString=W)}else if(D.seedSearchStringFromSelection==="multiple"&&!D.updateSearchScope){let W=m(this._editor,D.seedSearchStringFromSelection);W&&(R.searchString=W)}if(!R.searchString&&D.seedSearchStringFromGlobalClipboard){let W=yield this.getGlobalBufferTerm();if(!this._editor.hasModel())return;W&&(R.searchString=W)}if(D.forceRevealReplace?R.isReplaceRevealed=!0:this._findWidgetVisible.get()||(R.isReplaceRevealed=!1),D.updateSearchScope){let W=this._editor.getSelections();W.some(x=>!x.isEmpty())&&(R.searchScope=W)}R.loop=D.loop,this._state.change(R,!1),this._model||(this._model=new d.FindModelBoundToEditorModel(this._editor,this._state))}})}start(D){return this._start(D)}moveToNextMatch(){return this._model?(this._model.moveToNextMatch(),!0):!1}moveToPrevMatch(){return this._model?(this._model.moveToPrevMatch(),!0):!1}replace(){return this._model?(this._model.replace(),!0):!1}replaceAll(){return this._model?(this._model.replaceAll(),!0):!1}selectAllMatches(){return this._model?(this._model.selectAllMatches(),this._editor.focus(),!0):!1}getGlobalBufferTerm(){return Ie(this,void 0,void 0,function*(){return this._editor.getOption(31).globalFindClipboard&&this._editor.hasModel()&&!this._editor.getModel().isTooLargeForSyncing()?this._clipboardService.readFindText():""})}setGlobalBufferTerm(D){this._editor.getOption(31).globalFindClipboard&&this._editor.hasModel()&&!this._editor.getModel().isTooLargeForSyncing()&&this._clipboardService.writeFindText(D)}};_.ID="editor.contrib.findController",_=Me([_e(1,a.IContextKeyService),_e(2,i.IStorageService),_e(3,s.IClipboardService)],_),e.CommonFindController=_;let f=class extends _{constructor(D,R,W,x,K,Y,ee,se){super(D,W,ee,se);this._contextViewService=R,this._keybindingService=x,this._themeService=K,this._notificationService=Y,this._widget=null,this._findOptionsWidget=null}_start(D){const R=Object.create(null,{_start:{get:()=>super._start}});return Ie(this,void 0,void 0,function*(){this._widget||this._createFindWidget();const W=this._editor.getSelection();let x=!1;switch(this._editor.getOption(31).autoFindInSelection){case"always":x=!0;break;case"never":x=!1;break;case"multiline":x=!!W&&W.startLineNumber!==W.endLineNumber;break;default:break}D.updateSearchScope=x,yield R._start.call(this,D),this._widget&&(D.shouldFocus===2?this._widget.focusReplaceInput():D.shouldFocus===1&&this._widget.focusFindInput())})}highlightFindOptions(D=!1){this._widget||this._createFindWidget(),this._state.isRevealed&&!D?this._widget.highlightFindOptions():this._findOptionsWidget.highlightFindOptions()}_createFindWidget(){this._widget=this._register(new c.FindWidget(this._editor,this,this._state,this._contextViewService,this._keybindingService,this._contextKeyService,this._themeService,this._storageService,this._notificationService)),this._findOptionsWidget=this._register(new g.FindOptionsWidget(this._editor,this._state,this._keybindingService,this._themeService))}};f=Me([_e(1,u.IContextViewService),_e(2,a.IContextKeyService),_e(3,r.IKeybindingService),_e(4,n.IThemeService),_e(5,t.INotificationService),_e(6,i.IStorageService),_e(7,s.IClipboardService)],f),e.FindController=f,e.StartFindAction=S.registerMultiEditorAction(new S.MultiEditorAction({id:d.FIND_IDS.StartFindAction,label:b.localize(0,null),alias:"Find",precondition:a.ContextKeyExpr.or(C.EditorContextKeys.focus,a.ContextKeyExpr.has("editorIsOpen")),kbOpts:{kbExpr:null,primary:2048|36,weight:100},menuOpts:{menuId:o.MenuId.MenubarEditMenu,group:"3_find",title:b.localize(1,null),order:1}})),e.StartFindAction.addImplementation(0,(F,D)=>{const R=F.get(l.ICodeEditorService),W=R.getFocusedCodeEditor()||R.getActiveCodeEditor();if(!W)return!1;const x=_.get(W);return x?x.start({forceRevealReplace:!1,seedSearchStringFromSelection:W.getOption(31).seedSearchStringFromSelection?"single":"none",seedSearchStringFromGlobalClipboard:W.getOption(31).globalFindClipboard,shouldFocus:1,shouldAnimate:!0,updateSearchScope:!1,loop:W.getOption(31).loop}):!1});class v extends S.EditorAction{constructor(){super({id:d.FIND_IDS.StartFindWithSelection,label:b.localize(2,null),alias:"Find With Selection",precondition:void 0,kbOpts:{kbExpr:null,primary:0,mac:{primary:2048|35},weight:100}})}run(D,R){return Ie(this,void 0,void 0,function*(){let W=_.get(R);W&&(yield W.start({forceRevealReplace:!1,seedSearchStringFromSelection:"multiple",seedSearchStringFromGlobalClipboard:!1,shouldFocus:0,shouldAnimate:!0,updateSearchScope:!1,loop:R.getOption(31).loop}),W.setGlobalBufferTerm(W.getState().searchString))})}}e.StartFindWithSelectionAction=v;class y extends S.EditorAction{run(D,R){return Ie(this,void 0,void 0,function*(){let W=_.get(R);W&&!this._run(W)&&(yield W.start({forceRevealReplace:!1,seedSearchStringFromSelection:W.getState().searchString.length===0&&R.getOption(31).seedSearchStringFromSelection?"single":"none",seedSearchStringFromGlobalClipboard:!0,shouldFocus:0,shouldAnimate:!0,updateSearchScope:!1,loop:R.getOption(31).loop}),this._run(W))})}}e.MatchFindAction=y;class L extends y{constructor(){super({id:d.FIND_IDS.NextMatchFindAction,label:b.localize(3,null),alias:"Find Next",precondition:void 0,kbOpts:{kbExpr:C.EditorContextKeys.focus,primary:61,mac:{primary:2048|37,secondary:[61]},weight:100}})}_run(D){return D.moveToNextMatch()?(D.editor.pushUndoStop(),!0):!1}}e.NextMatchFindAction=L;class I extends y{constructor(){super({id:d.FIND_IDS.NextMatchFindAction,label:b.localize(4,null),alias:"Find Next",precondition:void 0,kbOpts:{kbExpr:a.ContextKeyExpr.and(C.EditorContextKeys.focus,d.CONTEXT_FIND_INPUT_FOCUSED),primary:3,weight:100}})}_run(D){return D.moveToNextMatch()?(D.editor.pushUndoStop(),!0):!1}}e.NextMatchFindAction2=I;class k extends y{constructor(){super({id:d.FIND_IDS.PreviousMatchFindAction,label:b.localize(5,null),alias:"Find Previous",precondition:void 0,kbOpts:{kbExpr:C.EditorContextKeys.focus,primary:1024|61,mac:{primary:2048|1024|37,secondary:[1024|61]},weight:100}})}_run(D){return D.moveToPrevMatch()}}e.PreviousMatchFindAction=k;class E extends y{constructor(){super({id:d.FIND_IDS.PreviousMatchFindAction,label:b.localize(6,null),alias:"Find Previous",precondition:void 0,kbOpts:{kbExpr:a.ContextKeyExpr.and(C.EditorContextKeys.focus,d.CONTEXT_FIND_INPUT_FOCUSED),primary:1024|3,weight:100}})}_run(D){return D.moveToPrevMatch()}}e.PreviousMatchFindAction2=E;class T extends S.EditorAction{run(D,R){return Ie(this,void 0,void 0,function*(){let W=_.get(R);if(!!W){let x=m(R);x&&W.setSearchString(x),this._run(W)||(yield W.start({forceRevealReplace:!1,seedSearchStringFromSelection:R.getOption(31).seedSearchStringFromSelection?"single":"none",seedSearchStringFromGlobalClipboard:!1,shouldFocus:0,shouldAnimate:!0,updateSearchScope:!1,loop:R.getOption(31).loop}),this._run(W))}})}}e.SelectionMatchFindAction=T;class O extends T{constructor(){super({id:d.FIND_IDS.NextSelectionMatchFindAction,label:b.localize(7,null),alias:"Find Next Selection",precondition:void 0,kbOpts:{kbExpr:C.EditorContextKeys.focus,primary:2048|61,weight:100}})}_run(D){return D.moveToNextMatch()}}e.NextSelectionMatchFindAction=O;class A extends T{constructor(){super({id:d.FIND_IDS.PreviousSelectionMatchFindAction,label:b.localize(8,null),alias:"Find Previous Selection",precondition:void 0,kbOpts:{kbExpr:C.EditorContextKeys.focus,primary:2048|1024|61,weight:100}})}_run(D){return D.moveToPrevMatch()}}e.PreviousSelectionMatchFindAction=A,e.StartFindReplaceAction=S.registerMultiEditorAction(new S.MultiEditorAction({id:d.FIND_IDS.StartFindReplaceAction,label:b.localize(9,null),alias:"Replace",precondition:a.ContextKeyExpr.or(C.EditorContextKeys.focus,a.ContextKeyExpr.has("editorIsOpen")),kbOpts:{kbExpr:null,primary:2048|38,mac:{primary:2048|512|36},weight:100},menuOpts:{menuId:o.MenuId.MenubarEditMenu,group:"3_find",title:b.localize(10,null),order:2}})),e.StartFindReplaceAction.addImplementation(0,(F,D)=>{const R=F.get(l.ICodeEditorService),W=R.getFocusedCodeEditor()||R.getActiveCodeEditor();if(!W||!W.hasModel()||W.getOption(75))return!1;const x=_.get(W);if(!x)return!1;const K=W.getSelection(),Y=x.isFindInputFocused(),ee=!K.isEmpty()&&K.startLineNumber===K.endLineNumber&&W.getOption(31).seedSearchStringFromSelection&&!Y,se=Y||ee?2:1;return x.start({forceRevealReplace:!0,seedSearchStringFromSelection:ee?"single":"none",seedSearchStringFromGlobalClipboard:W.getOption(31).seedSearchStringFromSelection,shouldFocus:se,shouldAnimate:!0,updateSearchScope:!1,loop:W.getOption(31).loop})}),S.registerEditorContribution(_.ID,f),S.registerEditorAction(v),S.registerEditorAction(L),S.registerEditorAction(I),S.registerEditorAction(k),S.registerEditorAction(E),S.registerEditorAction(O),S.registerEditorAction(A);const B=S.EditorCommand.bindToContribution(_.get);S.registerEditorCommand(new B({id:d.FIND_IDS.CloseFindWidgetCommand,precondition:d.CONTEXT_FIND_WIDGET_VISIBLE,handler:F=>F.closeFindWidget(),kbOpts:{weight:100+5,kbExpr:a.ContextKeyExpr.and(C.EditorContextKeys.focus,a.ContextKeyExpr.not("isComposing")),primary:9,secondary:[1024|9]}})),S.registerEditorCommand(new B({id:d.FIND_IDS.ToggleCaseSensitiveCommand,precondition:void 0,handler:F=>F.toggleCaseSensitive(),kbOpts:{weight:100+5,kbExpr:C.EditorContextKeys.focus,primary:d.ToggleCaseSensitiveKeybinding.primary,mac:d.ToggleCaseSensitiveKeybinding.mac,win:d.ToggleCaseSensitiveKeybinding.win,linux:d.ToggleCaseSensitiveKeybinding.linux}})),S.registerEditorCommand(new B({id:d.FIND_IDS.ToggleWholeWordCommand,precondition:void 0,handler:F=>F.toggleWholeWords(),kbOpts:{weight:100+5,kbExpr:C.EditorContextKeys.focus,primary:d.ToggleWholeWordKeybinding.primary,mac:d.ToggleWholeWordKeybinding.mac,win:d.ToggleWholeWordKeybinding.win,linux:d.ToggleWholeWordKeybinding.linux}})),S.registerEditorCommand(new B({id:d.FIND_IDS.ToggleRegexCommand,precondition:void 0,handler:F=>F.toggleRegex(),kbOpts:{weight:100+5,kbExpr:C.EditorContextKeys.focus,primary:d.ToggleRegexKeybinding.primary,mac:d.ToggleRegexKeybinding.mac,win:d.ToggleRegexKeybinding.win,linux:d.ToggleRegexKeybinding.linux}})),S.registerEditorCommand(new B({id:d.FIND_IDS.ToggleSearchScopeCommand,precondition:void 0,handler:F=>F.toggleSearchScope(),kbOpts:{weight:100+5,kbExpr:C.EditorContextKeys.focus,primary:d.ToggleSearchScopeKeybinding.primary,mac:d.ToggleSearchScopeKeybinding.mac,win:d.ToggleSearchScopeKeybinding.win,linux:d.ToggleSearchScopeKeybinding.linux}})),S.registerEditorCommand(new B({id:d.FIND_IDS.TogglePreserveCaseCommand,precondition:void 0,handler:F=>F.togglePreserveCase(),kbOpts:{weight:100+5,kbExpr:C.EditorContextKeys.focus,primary:d.TogglePreserveCaseKeybinding.primary,mac:d.TogglePreserveCaseKeybinding.mac,win:d.TogglePreserveCaseKeybinding.win,linux:d.TogglePreserveCaseKeybinding.linux}})),S.registerEditorCommand(new B({id:d.FIND_IDS.ReplaceOneAction,precondition:d.CONTEXT_FIND_WIDGET_VISIBLE,handler:F=>F.replace(),kbOpts:{weight:100+5,kbExpr:C.EditorContextKeys.focus,primary:2048|1024|22}})),S.registerEditorCommand(new B({id:d.FIND_IDS.ReplaceOneAction,precondition:d.CONTEXT_FIND_WIDGET_VISIBLE,handler:F=>F.replace(),kbOpts:{weight:100+5,kbExpr:a.ContextKeyExpr.and(C.EditorContextKeys.focus,d.CONTEXT_REPLACE_INPUT_FOCUSED),primary:3}})),S.registerEditorCommand(new B({id:d.FIND_IDS.ReplaceAllAction,precondition:d.CONTEXT_FIND_WIDGET_VISIBLE,handler:F=>F.replaceAll(),kbOpts:{weight:100+5,kbExpr:C.EditorContextKeys.focus,primary:2048|512|3}})),S.registerEditorCommand(new B({id:d.FIND_IDS.ReplaceAllAction,precondition:d.CONTEXT_FIND_WIDGET_VISIBLE,handler:F=>F.replaceAll(),kbOpts:{weight:100+5,kbExpr:a.ContextKeyExpr.and(C.EditorContextKeys.focus,d.CONTEXT_REPLACE_INPUT_FOCUSED),primary:void 0,mac:{primary:2048|3}}})),S.registerEditorCommand(new B({id:d.FIND_IDS.SelectAllMatchesAction,precondition:d.CONTEXT_FIND_WIDGET_VISIBLE,handler:F=>F.selectAllMatches(),kbOpts:{weight:100+5,kbExpr:C.EditorContextKeys.focus,primary:512|3}}))}),define(Q[658],J([0,1,31,27,475,80,11]),function(q,e,b,N,M,w,S){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.FoldingDecorationProvider=e.foldingCollapsedIcon=e.foldingExpandedIcon=void 0,e.foldingExpandedIcon=w.registerIcon("folding-expanded",N.Codicon.chevronDown,M.localize(0,null)),e.foldingCollapsedIcon=w.registerIcon("folding-collapsed",N.Codicon.chevronRight,M.localize(1,null));class C{constructor(g){this.editor=g,this.autoHideFoldingControls=!0,this.showFoldingHighlights=!0}getDecorationOption(g,p){return p?C.HIDDEN_RANGE_DECORATION:g?this.showFoldingHighlights?C.COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION:C.COLLAPSED_VISUAL_DECORATION:this.autoHideFoldingControls?C.EXPANDED_AUTO_HIDE_VISUAL_DECORATION:C.EXPANDED_VISUAL_DECORATION}deltaDecorations(g,p){return this.editor.deltaDecorations(g,p)}changeDecorations(g){return this.editor.changeDecorations(g)}}e.FoldingDecorationProvider=C,C.COLLAPSED_VISUAL_DECORATION=b.ModelDecorationOptions.register({stickiness:1,afterContentClassName:"inline-folded",isWholeLine:!0,firstLineDecorationClassName:S.ThemeIcon.asClassName(e.foldingCollapsedIcon)}),C.COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION=b.ModelDecorationOptions.register({stickiness:1,afterContentClassName:"inline-folded",className:"folded-background",isWholeLine:!0,firstLineDecorationClassName:S.ThemeIcon.asClassName(e.foldingCollapsedIcon)}),C.EXPANDED_AUTO_HIDE_VISUAL_DECORATION=b.ModelDecorationOptions.register({stickiness:1,isWholeLine:!0,firstLineDecorationClassName:S.ThemeIcon.asClassName(e.foldingExpandedIcon)}),C.EXPANDED_VISUAL_DECORATION=b.ModelDecorationOptions.register({stickiness:1,isWholeLine:!0,firstLineDecorationClassName:"alwaysShowFoldIcons "+S.ThemeIcon.asClassName(e.foldingExpandedIcon)}),C.HIDDEN_RANGE_DECORATION=b.ModelDecorationOptions.register({stickiness:1})}),define(Q[659],J([0,1,474,20,8,15,39,2,13,405,658,25,406,41,541,18,225,407,12,16,11,22,342]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.editorFoldForeground=e.foldBackgroundBackground=e.FoldingController=void 0;const m=new t.RawContextKey("foldingEnabled",!1);let _=class $t extends C.Disposable{constructor(W,x){super();this.contextKeyService=x,this.localToDispose=this._register(new C.DisposableStore),this.editor=W;const K=this.editor.getOptions();this._isEnabled=K.get(33),this._useFoldingProviders=K.get(34)!=="indentation",this._unfoldOnClickAfterEndOfLine=K.get(36),this._restoringViewState=!1,this.foldingModel=null,this.hiddenRangeModel=null,this.rangeProvider=null,this.foldingRegionPromise=null,this.foldingStateMemento=null,this.foldingModelPromise=null,this.updateScheduler=null,this.cursorChangedScheduler=null,this.mouseDownInfo=null,this.foldingDecorationProvider=new p.FoldingDecorationProvider(W),this.foldingDecorationProvider.autoHideFoldingControls=K.get(94)==="mouseover",this.foldingDecorationProvider.showFoldingHighlights=K.get(35),this.foldingEnabled=m.bindTo(this.contextKeyService),this.foldingEnabled.set(this._isEnabled),this._register(this.editor.onDidChangeModel(()=>this.onModelChanged())),this._register(this.editor.onDidChangeConfiguration(Y=>{if(Y.hasChanged(33)&&(this._isEnabled=this.editor.getOptions().get(33),this.foldingEnabled.set(this._isEnabled),this.onModelChanged()),Y.hasChanged(94)||Y.hasChanged(35)){const ee=this.editor.getOptions();this.foldingDecorationProvider.autoHideFoldingControls=ee.get(94)==="mouseover",this.foldingDecorationProvider.showFoldingHighlights=ee.get(35),this.onModelContentChanged()}Y.hasChanged(34)&&(this._useFoldingProviders=this.editor.getOptions().get(34)!=="indentation",this.onFoldingStrategyChanged()),Y.hasChanged(36)&&(this._unfoldOnClickAfterEndOfLine=this.editor.getOptions().get(36))})),this.onModelChanged()}static get(W){return W.getContribution($t.ID)}saveViewState(){let W=this.editor.getModel();if(!W||!this._isEnabled||W.isTooLargeForTokenization())return{};if(this.foldingModel){let x=this.foldingModel.isInitialized?this.foldingModel.getMemento():this.hiddenRangeModel.getMemento(),K=this.rangeProvider?this.rangeProvider.id:void 0;return{collapsedRegions:x,lineCount:W.getLineCount(),provider:K}}}restoreViewState(W){let x=this.editor.getModel();if(!(!x||!this._isEnabled||x.isTooLargeForTokenization()||!this.hiddenRangeModel)&&!(!W||!W.collapsedRegions||W.lineCount!==x.getLineCount())){(W.provider===r.ID_SYNTAX_PROVIDER||W.provider===i.ID_INIT_PROVIDER)&&(this.foldingStateMemento=W);const K=W.collapsedRegions;if(this.hiddenRangeModel.applyMemento(K)){const Y=this.getFoldingModel();Y&&Y.then(ee=>{if(ee){this._restoringViewState=!0;try{ee.applyMemento(K)}finally{this._restoringViewState=!1}}}).then(void 0,n.onUnexpectedError)}}}onModelChanged(){this.localToDispose.clear();let W=this.editor.getModel();!this._isEnabled||!W||W.isTooLargeForTokenization()||(this.foldingModel=new g.FoldingModel(W,this.foldingDecorationProvider),this.localToDispose.add(this.foldingModel),this.hiddenRangeModel=new o.HiddenRangeModel(this.foldingModel),this.localToDispose.add(this.hiddenRangeModel),this.localToDispose.add(this.hiddenRangeModel.onDidChange(x=>this.onHiddenRangesChanges(x))),this.updateScheduler=new w.Delayer(200),this.cursorChangedScheduler=new w.RunOnceScheduler(()=>this.revealCursor(),200),this.localToDispose.add(this.cursorChangedScheduler),this.localToDispose.add(u.FoldingRangeProviderRegistry.onDidChange(()=>this.onFoldingStrategyChanged())),this.localToDispose.add(this.editor.onDidChangeModelLanguageConfiguration(()=>this.onFoldingStrategyChanged())),this.localToDispose.add(this.editor.onDidChangeModelContent(()=>this.onModelContentChanged())),this.localToDispose.add(this.editor.onDidChangeCursorPosition(()=>this.onCursorPositionChanged())),this.localToDispose.add(this.editor.onMouseDown(x=>this.onEditorMouseDown(x))),this.localToDispose.add(this.editor.onMouseUp(x=>this.onEditorMouseUp(x))),this.localToDispose.add({dispose:()=>{this.foldingRegionPromise&&(this.foldingRegionPromise.cancel(),this.foldingRegionPromise=null),this.updateScheduler&&this.updateScheduler.cancel(),this.updateScheduler=null,this.foldingModel=null,this.foldingModelPromise=null,this.hiddenRangeModel=null,this.cursorChangedScheduler=null,this.foldingStateMemento=null,this.rangeProvider&&this.rangeProvider.dispose(),this.rangeProvider=null}}),this.onModelContentChanged())}onFoldingStrategyChanged(){this.rangeProvider&&this.rangeProvider.dispose(),this.rangeProvider=null,this.onModelContentChanged()}getRangeProvider(W){if(this.rangeProvider)return this.rangeProvider;if(this.rangeProvider=new a.IndentRangeProvider(W),this._useFoldingProviders&&this.foldingModel){let x=u.FoldingRangeProviderRegistry.ordered(this.foldingModel.textModel);if(x.length===0&&this.foldingStateMemento&&this.foldingStateMemento.collapsedRegions)return this.rangeProvider=new i.InitializingRangeProvider(W,this.foldingStateMemento.collapsedRegions,()=>{this.foldingStateMemento=null,this.onFoldingStrategyChanged()},3e4);x.length>0&&(this.rangeProvider=new r.SyntaxRangeProvider(W,x,()=>this.onModelContentChanged()))}return this.foldingStateMemento=null,this.rangeProvider}getFoldingModel(){return this.foldingModelPromise}onModelContentChanged(){this.updateScheduler&&(this.foldingRegionPromise&&(this.foldingRegionPromise.cancel(),this.foldingRegionPromise=null),this.foldingModelPromise=this.updateScheduler.trigger(()=>{const W=this.foldingModel;if(!W)return null;let x=this.foldingRegionPromise=w.createCancelablePromise(K=>this.getRangeProvider(W.textModel).compute(K));return x.then(K=>{if(K&&x===this.foldingRegionPromise){let Y=this.editor.getSelections(),ee=Y?Y.map(se=>se.startLineNumber):[];W.update(K,ee)}return W})}).then(void 0,W=>(n.onUnexpectedError(W),null)))}onHiddenRangesChanges(W){if(this.hiddenRangeModel&&W.length&&!this._restoringViewState){let x=this.editor.getSelections();x&&this.hiddenRangeModel.adjustSelections(x)&&this.editor.setSelections(x)}this.editor.setHiddenAreas(W)}onCursorPositionChanged(){this.hiddenRangeModel&&this.hiddenRangeModel.hasRanges()&&this.cursorChangedScheduler.schedule()}revealCursor(){const W=this.getFoldingModel();!W||W.then(x=>{if(x){let K=this.editor.getSelections();if(K&&K.length>0){let Y=[];for(let ee of K){let se=ee.selectionStartLineNumber;this.hiddenRangeModel&&this.hiddenRangeModel.isHidden(se)&&Y.push(...x.getAllRegionsAtLine(se,ne=>ne.isCollapsed&&se>ne.startLineNumber))}Y.length&&(x.toggleCollapseState(Y),this.reveal(K[0].getPosition()))}}}).then(void 0,n.onUnexpectedError)}onEditorMouseDown(W){if(this.mouseDownInfo=null,!(!this.hiddenRangeModel||!W.target||!W.target.range)&&!(!W.event.leftButton&&!W.event.middleButton)){const x=W.target.range;let K=!1;switch(W.target.type){case 4:const Y=W.target.detail,ee=W.target.element.offsetLeft;if(Y.offsetX-ee<5)return;K=!0;break;case 7:{if(this._unfoldOnClickAfterEndOfLine&&this.hiddenRangeModel.hasRanges()&&!W.target.detail.isAfterLines)break;return}case 6:{if(this.hiddenRangeModel.hasRanges()){let ne=this.editor.getModel();if(ne&&x.startColumn===ne.getLineMaxColumn(x.startLineNumber))break}return}default:return}this.mouseDownInfo={lineNumber:x.startLineNumber,iconClicked:K}}}onEditorMouseUp(W){const x=this.getFoldingModel();if(!(!x||!this.mouseDownInfo||!W.target)){let K=this.mouseDownInfo.lineNumber,Y=this.mouseDownInfo.iconClicked,ee=W.target.range;if(!(!ee||ee.startLineNumber!==K)){if(Y){if(W.target.type!==4)return}else{let se=this.editor.getModel();if(!se||ee.startColumn!==se.getLineMaxColumn(K))return}x.then(se=>{if(se){let ne=se.getRegionAtLine(K);if(ne&&ne.startLineNumber===K){let le=ne.isCollapsed;if(Y||le){let X=[],z=W.event.middleButton||W.event.shiftKey;if(z)for(const P of se.getRegionsInside(ne))P.isCollapsed===le&&X.push(P);(le||!z||X.length===0)&&X.push(ne),se.toggleCollapseState(X),this.reveal({lineNumber:K,column:1})}}}}).then(void 0,n.onUnexpectedError)}}}reveal(W){this.editor.revealPositionInCenterIfOutsideViewport(W,0)}};_.ID="editor.contrib.folding",_=Me([_e(1,t.IContextKeyService)],_),e.FoldingController=_;class f extends d.EditorAction{runEditorCommand(W,x,K){let Y=_.get(x);if(!!Y){let ee=Y.getFoldingModel();if(ee)return this.reportTelemetry(W,x),ee.then(se=>{if(se){this.invoke(Y,se,x,K);const ne=x.getSelection();ne&&Y.reveal(ne.getStartPosition())}})}}getSelectedLines(W){let x=W.getSelections();return x?x.map(K=>K.startLineNumber):[]}getLineNumbers(W,x){return W&&W.selectionLines?W.selectionLines.map(K=>K+1):this.getSelectedLines(x)}run(W,x){}}function v(R){if(!N.isUndefined(R)){if(!N.isObject(R))return!1;const W=R;if(!N.isUndefined(W.levels)&&!N.isNumber(W.levels)||!N.isUndefined(W.direction)&&!N.isString(W.direction)||!N.isUndefined(W.selectionLines)&&(!N.isArray(W.selectionLines)||!W.selectionLines.every(N.isNumber)))return!1}return!0}class y extends f{constructor(){super({id:"editor.unfold",label:b.localize(0,null),alias:"Unfold",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:2048|1024|89,mac:{primary:2048|512|89},weight:100},description:{description:"Unfold the content in the editor",args:[{name:"Unfold editor argument",description:`Property-value pairs that can be passed through this argument: + * 'levels': Number of levels to unfold. If not set, defaults to 1. + * 'direction': If 'up', unfold given number of levels up otherwise unfolds down. + * 'selectionLines': The start lines (0-based) of the editor selections to apply the unfold action to. If not set, the active selection(s) will be used. + `,constraint:v,schema:{type:"object",properties:{levels:{type:"number",default:1},direction:{type:"string",enum:["up","down"],default:"down"},selectionLines:{type:"array",items:{type:"number"}}}}}]}})}invoke(W,x,K,Y){let ee=Y&&Y.levels||1,se=this.getLineNumbers(Y,K);Y&&Y.direction==="up"?g.setCollapseStateLevelsUp(x,!1,ee,se):g.setCollapseStateLevelsDown(x,!1,ee,se)}}class L extends f{constructor(){super({id:"editor.unfoldRecursively",label:b.localize(1,null),alias:"Unfold Recursively",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|89),weight:100}})}invoke(W,x,K,Y){g.setCollapseStateLevelsDown(x,!1,Number.MAX_VALUE,this.getSelectedLines(K))}}class I extends f{constructor(){super({id:"editor.fold",label:b.localize(2,null),alias:"Fold",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:2048|1024|87,mac:{primary:2048|512|87},weight:100},description:{description:"Fold the content in the editor",args:[{name:"Fold editor argument",description:`Property-value pairs that can be passed through this argument: + * 'levels': Number of levels to fold. + * 'direction': If 'up', folds given number of levels up otherwise folds down. + * 'selectionLines': The start lines (0-based) of the editor selections to apply the fold action to. If not set, the active selection(s) will be used. + If no levels or direction is set, folds the region at the locations or if already collapsed, the first uncollapsed parent instead. + `,constraint:v,schema:{type:"object",properties:{levels:{type:"number"},direction:{type:"string",enum:["up","down"]},selectionLines:{type:"array",items:{type:"number"}}}}}]}})}invoke(W,x,K,Y){let ee=this.getLineNumbers(Y,K);const se=Y&&Y.levels,ne=Y&&Y.direction;typeof se!="number"&&typeof ne!="string"?g.setCollapseStateUp(x,!0,ee):ne==="up"?g.setCollapseStateLevelsUp(x,!0,se||1,ee):g.setCollapseStateLevelsDown(x,!0,se||1,ee)}}class k extends f{constructor(){super({id:"editor.toggleFold",label:b.localize(3,null),alias:"Toggle Fold",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|42),weight:100}})}invoke(W,x,K){let Y=this.getSelectedLines(K);g.toggleCollapseState(x,1,Y)}}class E extends f{constructor(){super({id:"editor.foldRecursively",label:b.localize(4,null),alias:"Fold Recursively",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|87),weight:100}})}invoke(W,x,K){let Y=this.getSelectedLines(K);g.setCollapseStateLevelsDown(x,!0,Number.MAX_VALUE,Y)}}class T extends f{constructor(){super({id:"editor.foldAllBlockComments",label:b.localize(5,null),alias:"Fold All Block Comments",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|85),weight:100}})}invoke(W,x,K){if(x.regions.hasTypes())g.setCollapseStateForType(x,u.FoldingRangeKind.Comment.value,!0);else{const Y=K.getModel();if(!Y)return;let ee=s.LanguageConfigurationRegistry.getComments(Y.getLanguageIdentifier().id);if(ee&&ee.blockCommentStartToken){let se=new RegExp("^\\s*"+M.escapeRegExpCharacters(ee.blockCommentStartToken));g.setCollapseStateForMatchingLines(x,se,!0)}}}}class O extends f{constructor(){super({id:"editor.foldAllMarkerRegions",label:b.localize(6,null),alias:"Fold All Regions",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|29),weight:100}})}invoke(W,x,K){if(x.regions.hasTypes())g.setCollapseStateForType(x,u.FoldingRangeKind.Region.value,!0);else{const Y=K.getModel();if(!Y)return;let ee=s.LanguageConfigurationRegistry.getFoldingRules(Y.getLanguageIdentifier().id);if(ee&&ee.markers&&ee.markers.start){let se=new RegExp(ee.markers.start);g.setCollapseStateForMatchingLines(x,se,!0)}}}}class A extends f{constructor(){super({id:"editor.unfoldAllMarkerRegions",label:b.localize(7,null),alias:"Unfold All Regions",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|30),weight:100}})}invoke(W,x,K){if(x.regions.hasTypes())g.setCollapseStateForType(x,u.FoldingRangeKind.Region.value,!1);else{const Y=K.getModel();if(!Y)return;let ee=s.LanguageConfigurationRegistry.getFoldingRules(Y.getLanguageIdentifier().id);if(ee&&ee.markers&&ee.markers.start){let se=new RegExp(ee.markers.start);g.setCollapseStateForMatchingLines(x,se,!1)}}}}class B extends f{constructor(){super({id:"editor.foldAll",label:b.localize(8,null),alias:"Fold All",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|21),weight:100}})}invoke(W,x,K){g.setCollapseStateLevelsDown(x,!0)}}class F extends f{constructor(){super({id:"editor.unfoldAll",label:b.localize(9,null),alias:"Unfold All",precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|40),weight:100}})}invoke(W,x,K){g.setCollapseStateLevelsDown(x,!1)}}class D extends f{getFoldingLevel(){return parseInt(this.id.substr(D.ID_PREFIX.length))}invoke(W,x,K){g.setCollapseStateAtLevel(x,this.getFoldingLevel(),!0,this.getSelectedLines(K))}}D.ID_PREFIX="editor.foldLevel",D.ID=R=>D.ID_PREFIX+R,d.registerEditorContribution(_.ID,_),d.registerEditorAction(y),d.registerEditorAction(L),d.registerEditorAction(I),d.registerEditorAction(E),d.registerEditorAction(B),d.registerEditorAction(F),d.registerEditorAction(T),d.registerEditorAction(O),d.registerEditorAction(A),d.registerEditorAction(k);for(let R=1;R<=7;R++)d.registerInstantiatedEditorAction(new D({id:D.ID(R),label:b.localize(10,null,R),alias:`Fold Level ${R}`,precondition:m,kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:S.KeyChord(2048|41,2048|21+R),weight:100}}));e.foldBackgroundBackground=h.registerColor("editor.foldBackground",{light:h.transparent(h.editorSelectionBackground,.3),dark:h.transparent(h.editorSelectionBackground,.3),hc:null},b.localize(11,null),!0),e.editorFoldForeground=h.registerColor("editorGutter.foldingControlForeground",{dark:h.iconForeground,light:h.iconForeground,hc:h.iconForeground},b.localize(12,null)),l.registerThemingParticipant((R,W)=>{const x=R.getColor(e.foldBackgroundBackground);x&&W.addRule(`.monaco-editor .folded-background { background-color: ${x}; }`);const K=R.getColor(e.editorFoldForeground);K&&W.addRule(` + .monaco-editor .cldr${l.ThemeIcon.asCSSSelector(p.foldingExpandedIcon)}, + .monaco-editor .cldr${l.ThemeIcon.asCSSSelector(p.foldingCollapsedIcon)} { + color: ${K} !important; + } + `)})}),define(Q[268],J([0,1,479,2,16,14,3,13,25,655,28,34,27,9,557,80]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.NextMarkerAction=e.MarkerController=void 0;let r=class gt{constructor(v,y,L,I,k){this._markerNavigationService=y,this._contextKeyService=L,this._editorService=I,this._instantiationService=k,this._sessionDispoables=new N.DisposableStore,this._editor=v,this._widgetVisible=m.bindTo(this._contextKeyService)}static get(v){return v.getContribution(gt.ID)}dispose(){this._cleanUp(),this._sessionDispoables.dispose()}_cleanUp(){this._widgetVisible.reset(),this._sessionDispoables.clear(),this._widget=void 0,this._model=void 0}_getOrCreateModel(v){if(this._model&&this._model.matches(v))return this._model;let y=!1;return this._model&&(y=!0,this._cleanUp()),this._model=this._markerNavigationService.getMarkerList(v),y&&this._model.move(!0,this._editor.getModel(),this._editor.getPosition()),this._widget=this._instantiationService.createInstance(g.MarkerNavigationWidget,this._editor),this._widget.onDidClose(()=>this.close(),this,this._sessionDispoables),this._widgetVisible.set(!0),this._sessionDispoables.add(this._model),this._sessionDispoables.add(this._widget),this._sessionDispoables.add(this._editor.onDidChangeCursorPosition(L=>{var I,k,E;(!((I=this._model)===null||I===void 0?void 0:I.selected)||!S.Range.containsPosition((k=this._model)===null||k===void 0?void 0:k.selected.marker,L.position))&&((E=this._model)===null||E===void 0||E.resetIndex())})),this._sessionDispoables.add(this._model.onDidChange(()=>{if(!(!this._widget||!this._widget.position||!this._model)){const L=this._model.find(this._editor.getModel().uri,this._widget.position);L?this._widget.updateMarker(L.marker):this._widget.showStale()}})),this._sessionDispoables.add(this._widget.onDidSelectRelatedInformation(L=>{this._editorService.openCodeEditor({resource:L.resource,options:{pinned:!0,revealIfOpened:!0,selection:S.Range.lift(L).collapseToStart()}},this._editor),this.close(!1)})),this._sessionDispoables.add(this._editor.onDidChangeModel(()=>this._cleanUp())),this._model}close(v=!0){this._cleanUp(),v&&this._editor.focus()}showAtMarker(v){if(this._editor.hasModel()){const y=this._getOrCreateModel(this._editor.getModel().uri);y.resetIndex(),y.move(!0,this._editor.getModel(),new w.Position(v.startLineNumber,v.startColumn)),y.selected&&this._widget.showAtMarker(y.selected.marker,y.selected.index,y.selected.total)}}nagivate(v,y){return Ie(this,void 0,void 0,function*(){if(this._editor.hasModel()){const L=this._getOrCreateModel(y?void 0:this._editor.getModel().uri);if(L.move(v,this._editor.getModel(),this._editor.getPosition()),!L.selected)return;if(L.selected.marker.resource.toString()!==this._editor.getModel().uri.toString()){this._cleanUp();const I=yield this._editorService.openCodeEditor({resource:L.selected.marker.resource,options:{pinned:!1,revealIfOpened:!0,selectionRevealType:2,selection:L.selected.marker}},this._editor);I&&(gt.get(I).close(),gt.get(I).nagivate(v,y))}else this._widget.showAtMarker(L.selected.marker,L.selected.index,L.selected.total)}})}};r.ID="editor.contrib.markerController",r=Me([_e(1,a.IMarkerNavigationService),_e(2,M.IContextKeyService),_e(3,p.ICodeEditorService),_e(4,s.IInstantiationService)],r),e.MarkerController=r;class i extends C.EditorAction{constructor(v,y,L){super(L);this._next=v,this._multiFile=y}run(v,y){return Ie(this,void 0,void 0,function*(){y.hasModel()&&r.get(y).nagivate(this._next,this._multiFile)})}}class n extends i{constructor(){super(!0,!1,{id:n.ID,label:n.LABEL,alias:"Go to Next Problem (Error, Warning, Info)",precondition:void 0,kbOpts:{kbExpr:d.EditorContextKeys.focus,primary:512|66,weight:100},menuOpts:{menuId:g.MarkerNavigationWidget.TitleMenu,title:n.LABEL,icon:u.registerIcon("marker-navigation-next",o.Codicon.chevronDown,b.localize(1,null)),group:"navigation",order:1}})}}e.NextMarkerAction=n,n.ID="editor.action.marker.next",n.LABEL=b.localize(0,null);class t extends i{constructor(){super(!1,!1,{id:t.ID,label:t.LABEL,alias:"Go to Previous Problem (Error, Warning, Info)",precondition:void 0,kbOpts:{kbExpr:d.EditorContextKeys.focus,primary:1024|512|66,weight:100},menuOpts:{menuId:g.MarkerNavigationWidget.TitleMenu,title:n.LABEL,icon:u.registerIcon("marker-navigation-previous",o.Codicon.chevronUp,b.localize(3,null)),group:"navigation",order:2}})}}t.ID="editor.action.marker.prev",t.LABEL=b.localize(2,null);class l extends i{constructor(){super(!0,!0,{id:"editor.action.marker.nextInFiles",label:b.localize(4,null),alias:"Go to Next Problem in Files (Error, Warning, Info)",precondition:void 0,kbOpts:{kbExpr:d.EditorContextKeys.focus,primary:66,weight:100},menuOpts:{menuId:c.MenuId.MenubarGoMenu,title:b.localize(5,null),group:"6_problem_nav",order:1}})}}class h extends i{constructor(){super(!1,!0,{id:"editor.action.marker.prevInFiles",label:b.localize(6,null),alias:"Go to Previous Problem in Files (Error, Warning, Info)",precondition:void 0,kbOpts:{kbExpr:d.EditorContextKeys.focus,primary:1024|66,weight:100},menuOpts:{menuId:c.MenuId.MenubarGoMenu,title:b.localize(7,null),group:"6_problem_nav",order:2}})}}C.registerEditorContribution(r.ID,r),C.registerEditorAction(n),C.registerEditorAction(t),C.registerEditorAction(l),C.registerEditorAction(h);const m=new M.RawContextKey("markersNavigationVisible",!1),_=C.EditorCommand.bindToContribution(r.get);C.registerEditorCommand(new _({id:"closeMarkersNavigation",precondition:m,handler:f=>f.close(),kbOpts:{weight:100+50,kbExpr:d.EditorContextKeys.focus,primary:9,secondary:[1024|9]}}))}),define(Q[660],J([0,1,490,7,2,3,19,85,44,178,12,58,268,37,15,145,263,130,59,209]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.MarkerHoverParticipant=e.MarkerHover=void 0;const l=N.$;class h{constructor(v,y){this.range=v,this.marker=y}equals(v){return v instanceof h?C.IMarkerData.makeKey(this.marker)===C.IMarkerData.makeKey(v.marker):!1}}e.MarkerHover=h;const m={type:2,filter:{include:i.CodeActionKind.QuickFix}};let _=class{constructor(v,y,L,I,k){this._editor=v,this._hover=y,this._markerDecorationsService=L,this._keybindingService=I,this._openerService=k,this.recentMarkerCodeActionsInfo=void 0}computeSync(v,y){if(!this._editor.hasModel())return[];const L=this._editor.getModel(),I=v.startLineNumber,k=L.getLineMaxColumn(I),E=[];for(const T of y){const O=T.range.startLineNumber===I?T.range.startColumn:1,A=T.range.endLineNumber===I?T.range.endColumn:k,B=this._markerDecorationsService.getMarker(L.uri,T);if(!!B){const F=new w.Range(v.startLineNumber,O,v.startLineNumber,A);E.push(new h(F,B))}}return E}renderHoverParts(v,y){if(!v.length)return M.Disposable.None;const L=new M.DisposableStore;v.forEach(k=>y.appendChild(this.renderMarkerHover(k,L)));const I=v.length===1?v[0]:v.sort((k,E)=>C.MarkerSeverity.compare(k.marker.severity,E.marker.severity))[0];return y.appendChild(this.renderMarkerStatusbar(I,L)),L}renderMarkerHover(v,y){const L=l("div.hover-row"),I=N.append(L,l("div.marker.hover-contents")),{source:k,message:E,code:T,relatedInformation:O}=v.marker;this._editor.applyFontInfo(I);const A=N.append(I,l("span"));if(A.style.whiteSpace="pre-wrap",A.innerText=E,k||T)if(T&&typeof T!="string"){const B=l("span");if(k){const W=N.append(B,l("span"));W.innerText=k}const F=N.append(B,l("a.code-link"));F.setAttribute("href",T.target.toString()),y.add(N.addDisposableListener(F,"click",W=>{this._openerService.open(T.target),W.preventDefault(),W.stopPropagation()}));const D=N.append(F,l("span"));D.innerText=T.value;const R=N.append(I,B);R.style.opacity="0.6",R.style.paddingLeft="6px"}else{const B=N.append(I,l("span"));B.style.opacity="0.6",B.style.paddingLeft="6px",B.innerText=k&&T?`${k}(${T})`:k||`(${T})`}if(S.isNonEmptyArray(O))for(const{message:B,resource:F,startLineNumber:D,startColumn:R}of O){const W=N.append(I,l("div"));W.style.marginTop="8px";const x=N.append(W,l("a"));x.innerText=`${d.basename(F)}(${D}, ${R}): `,x.style.cursor="pointer",y.add(N.addDisposableListener(x,"click",Y=>{Y.stopPropagation(),Y.preventDefault(),this._openerService&&this._openerService.open(F,{fromUserGesture:!0,editorOptions:{selection:{startLineNumber:D,startColumn:R}}}).catch(p.onUnexpectedError)}));const K=N.append(W,l("span"));K.innerText=B,this._editor.applyFontInfo(K)}return L}renderMarkerStatusbar(v,y){const L=l("div.hover-row.status-bar"),I=N.append(L,l("div.actions"));if((v.marker.severity===C.MarkerSeverity.Error||v.marker.severity===C.MarkerSeverity.Warning||v.marker.severity===C.MarkerSeverity.Info)&&y.add(this.renderAction(I,{label:b.localize(0,null),commandId:o.NextMarkerAction.ID,run:()=>{this._hover.hide(),o.MarkerController.get(this._editor).showAtMarker(v.marker),this._editor.focus()}})),!this._editor.getOption(75)){const k=N.append(I,l("div"));this.recentMarkerCodeActionsInfo&&(C.IMarkerData.makeKey(this.recentMarkerCodeActionsInfo.marker)===C.IMarkerData.makeKey(v.marker)?this.recentMarkerCodeActionsInfo.hasCodeActions||(k.textContent=b.localize(1,null)):this.recentMarkerCodeActionsInfo=void 0);const E=this.recentMarkerCodeActionsInfo&&!this.recentMarkerCodeActionsInfo.hasCodeActions?M.Disposable.None:y.add(a.disposableTimeout(()=>k.textContent=b.localize(2,null),200));k.textContent||(k.textContent=String.fromCharCode(160));const T=this.getCodeActions(v.marker);y.add(M.toDisposable(()=>T.cancel())),T.then(O=>{if(E.dispose(),this.recentMarkerCodeActionsInfo={marker:v.marker,hasCodeActions:O.validActions.length>0},!this.recentMarkerCodeActionsInfo.hasCodeActions){O.dispose(),k.textContent=b.localize(3,null);return}k.style.display="none";let A=!1;y.add(M.toDisposable(()=>{A||O.dispose()})),y.add(this.renderAction(I,{label:b.localize(4,null),commandId:r.QuickFixAction.Id,run:B=>{A=!0;const F=r.QuickFixController.get(this._editor),D=N.getDomNodePagePosition(B);this._hover.hide(),F.showCodeActions(m,O,{x:D.left+6,y:D.top+D.height+6})}}))})}return L}renderAction(v,y){const L=this._keybindingService.lookupKeybinding(y.commandId),I=L?L.getLabel():null;return t.renderHoverAction(v,y,I)}getCodeActions(v){return a.createCancelablePromise(y=>u.getCodeActions(this._editor.getModel(),new w.Range(v.startLineNumber,v.startColumn,v.endLineNumber,v.endColumn),m,n.Progress.None,y))}};_=Me([_e(2,g.IMarkerDecorationsService),_e(3,s.IKeybindingService),_e(4,c.IOpenerService)],_),e.MarkerHoverParticipant=_}),define(Q[661],J([0,1,7,23,29,2,14,3,31,18,249,260,400,597,228,11,19,22,52,209,660,627]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ModesContentHoverWidget=void 0;class m{constructor(I,k,E){this.range=I,this.color=k,this.provider=E}equals(I){return!1}}class _{constructor(I,k){this.owner=I,this.data=k}}class f{constructor(I,k,E){this._markerHoverParticipant=k,this._markdownHoverParticipant=E,this._editor=I,this._result=[],this._range=null}setRange(I){this._range=I,this._result=[]}clearResult(){this._result=[]}computeAsync(I){return Ie(this,void 0,void 0,function*(){return!this._editor.hasModel()||!this._range?Promise.resolve([]):(yield this._markdownHoverParticipant.computeAsync(this._range,I)).map(E=>new _(this._markdownHoverParticipant,E))})}computeSync(){if(!this._editor.hasModel()||!this._range)return[];const I=this._editor.getModel(),k=this._range,E=k.startLineNumber;if(E>this._editor.getModel().getLineCount())return[];const T=I.getLineMaxColumn(E),O=this._editor.getLineDecorations(E).filter(R=>{if(R.options.isWholeLine)return!0;const W=R.range.startLineNumber===E?R.range.startColumn:1,x=R.range.endLineNumber===E?R.range.endColumn:T;return!(W>k.startColumn||k.endColumn>x)});let A=[];const B=c.ColorDetector.get(this._editor);for(const R of O){const W=B.getColorData(R.range.getStartPosition());if(W){const{color:x,range:K}=W.colorInfo;A.push(new _(null,new m(C.Range.lift(K),x,W.provider)));break}}const F=this._markdownHoverParticipant.computeSync(this._range,O);A=A.concat(F.map(R=>new _(this._markdownHoverParticipant,R)));const D=this._markerHoverParticipant.computeSync(this._range,O);return A=A.concat(D.map(R=>new _(this._markerHoverParticipant,R))),r.coalesce(A)}onResult(I,k){k?this._result=I.concat(this._result):this._result=this._result.concat(I)}getResult(){return this._result.slice(0)}getResultWithLoadingMessage(){if(this._range){const I=new _(this._markdownHoverParticipant,this._markdownHoverParticipant.createLoadingMessage(this._range));return this._result.slice(0).concat([I])}return this._result.slice(0)}}class v extends n.Widget{constructor(I,k,E,T){super();this._hoverVisibleKey=k,this._themeService=T,this.allowEditorOverflow=!0,this._markerHoverParticipant=E.createInstance(l.MarkerHoverParticipant,I,this),this._markdownHoverParticipant=E.createInstance(h.MarkdownHoverParticipant,I,this),this._hover=this._register(new t.HoverWidget),this._id=v.ID,this._editor=I,this._isVisible=!1,this._stoleFocus=!1,this._renderDisposable=null,this.onkeydown(this._hover.containerDomNode,O=>{O.equals(9)&&this.hide()}),this._register(this._editor.onDidChangeConfiguration(O=>{O.hasChanged(38)&&this._updateFont()})),this._editor.onDidLayoutChange(()=>this.layout()),this.layout(),this._editor.addContentWidget(this),this._showAtPosition=null,this._showAtRange=null,this._stoleFocus=!1,this._messages=[],this._lastRange=null,this._computer=new f(this._editor,this._markerHoverParticipant,this._markdownHoverParticipant),this._highlightDecorations=[],this._isChangingDecorations=!1,this._shouldFocus=!1,this._colorPicker=null,this._hoverOperation=new a.HoverOperation(this._computer,O=>this._withResult(O,!0),null,O=>this._withResult(O,!1),this._editor.getOption(48).delay),this._register(b.addStandardDisposableListener(this.getDomNode(),b.EventType.FOCUS,()=>{this._colorPicker&&this.getDomNode().classList.add("colorpicker-hover")})),this._register(b.addStandardDisposableListener(this.getDomNode(),b.EventType.BLUR,()=>{this.getDomNode().classList.remove("colorpicker-hover")})),this._register(I.onDidChangeConfiguration(()=>{this._hoverOperation.setHoverTime(this._editor.getOption(48).delay)})),this._register(g.TokenizationRegistry.onDidChange(()=>{this._isVisible&&this._lastRange&&this._messages.length>0&&(this._messages=this._messages.map(O=>{var A,B;if(O.data instanceof m&&!!((A=this._lastRange)===null||A===void 0?void 0:A.intersectRanges(O.data.range))&&((B=this._colorPicker)===null||B===void 0?void 0:B.model.color)){const F=this._colorPicker.model.color,D={red:F.rgba.r/255,green:F.rgba.g/255,blue:F.rgba.b/255,alpha:F.rgba.a};return new _(O.owner,new m(O.data.range,D,O.data.provider))}else return O}),this._hover.contentsDomNode.textContent="",this._renderMessages(this._lastRange,this._messages))}))}dispose(){this._hoverOperation.cancel(),this._editor.removeContentWidget(this),super.dispose()}getId(){return this._id}getDomNode(){return this._hover.containerDomNode}showAt(I,k,E){this._showAtPosition=I,this._showAtRange=k,this._hoverVisibleKey.set(!0),this._isVisible=!0,this._hover.containerDomNode.classList.toggle("hidden",!this._isVisible),this._editor.layoutContentWidget(this),this._editor.render(),this._stoleFocus=E,E&&this._hover.containerDomNode.focus()}getPosition(){return this._isVisible?{position:this._showAtPosition,range:this._showAtRange,preference:[1,2]}:null}_updateFont(){Array.prototype.slice.call(this._hover.contentsDomNode.getElementsByClassName("code")).forEach(k=>this._editor.applyFontInfo(k))}_updateContents(I){this._hover.contentsDomNode.textContent="",this._hover.contentsDomNode.appendChild(I),this._updateFont(),this._editor.layoutContentWidget(this),this._hover.onContentsChanged()}layout(){const I=Math.max(this._editor.getLayoutInfo().height/4,250),{fontSize:k,lineHeight:E}=this._editor.getOption(38);this._hover.contentsDomNode.style.fontSize=`${k}px`,this._hover.contentsDomNode.style.lineHeight=`${E}px`,this._hover.contentsDomNode.style.maxHeight=`${I}px`,this._hover.contentsDomNode.style.maxWidth=`${Math.max(this._editor.getLayoutInfo().width*.66,500)}px`}onModelDecorationsChanged(){this._isChangingDecorations||this._isVisible&&(this._hoverOperation.cancel(),this._computer.clearResult(),this._colorPicker||this._hoverOperation.start(0))}startShowingAt(I,k,E){if(!(this._lastRange&&this._lastRange.equalsRange(I))){if(this._hoverOperation.cancel(),this._isVisible)if(!this._showAtPosition||this._showAtPosition.lineNumber!==I.startLineNumber)this.hide();else{let T=[];for(let O=0,A=this._messages.length;O=I.endColumn&&T.push(B)}if(T.length>0){if(y(T,this._messages))return;this._renderMessages(I,T)}else this.hide()}this._lastRange=I,this._computer.setRange(I),this._shouldFocus=E,this._hoverOperation.start(k)}}hide(){this._lastRange=null,this._hoverOperation.cancel(),this._isVisible&&(setTimeout(()=>{this._isVisible||this._hoverVisibleKey.set(!1)},0),this._isVisible=!1,this._hover.containerDomNode.classList.toggle("hidden",!this._isVisible),this._editor.layoutContentWidget(this),this._stoleFocus&&this._editor.focus()),this._isChangingDecorations=!0,this._highlightDecorations=this._editor.deltaDecorations(this._highlightDecorations,[]),this._isChangingDecorations=!1,this._renderDisposable&&(this._renderDisposable.dispose(),this._renderDisposable=null),this._colorPicker=null}isColorPickerVisible(){return!!this._colorPicker}onContentsChanged(){this._hover.onContentsChanged()}_withResult(I,k){this._messages=I,this._lastRange&&this._messages.length>0?this._renderMessages(this._lastRange,this._messages):k&&this.hide()}_renderMessages(I,k){this._renderDisposable&&(this._renderDisposable.dispose(),this._renderDisposable=null),this._colorPicker=null;let E=1073741824,T=k[0].data.range?C.Range.lift(k[0].data.range):null,O=document.createDocumentFragment(),A=!1;const B=new w.DisposableStore,F=[],D=[];k.forEach(R=>{const W=R.data;if(!!W.range)if(E=Math.min(E,W.range.startColumn),T=T?C.Range.plusRange(T,W.range):C.Range.lift(W.range),W instanceof m){A=!0;const{red:x,green:K,blue:Y,alpha:ee}=W.color,se=new M.RGBA(Math.round(x*255),Math.round(K*255),Math.round(Y*255),ee),ne=new M.Color(se);if(!this._editor.hasModel())return;const le=this._editor.getModel();let X=new C.Range(W.range.startLineNumber,W.range.startColumn,W.range.endLineNumber,W.range.endColumn),z={range:W.range,color:W.color};const P=new o.ColorPickerModel(ne,[],0),V=new s.ColorPickerWidget(O,P,this._editor.getOption(122),this._themeService);p.getColorPresentations(le,z,W.provider,N.CancellationToken.None).then(U=>{if(P.colorPresentations=U||[],!!this._editor.hasModel()){const H=this._editor.getModel().getValueInRange(W.range);P.guessColorPresentation(ne,H);const $=()=>{let G,j;if(P.presentation.textEdit){G=[P.presentation.textEdit],j=new C.Range(P.presentation.textEdit.range.startLineNumber,P.presentation.textEdit.range.startColumn,P.presentation.textEdit.range.endLineNumber,P.presentation.textEdit.range.endColumn);const te=this._editor.getModel()._setTrackedRange(null,j,3);this._editor.pushUndoStop(),this._editor.executeEdits("colorpicker",G),j=this._editor.getModel()._getTrackedRange(te)||j}else G=[{identifier:null,range:X,text:P.presentation.label,forceMoveMarkers:!1}],j=X.setEndPosition(X.endLineNumber,X.startColumn+P.presentation.label.length),this._editor.pushUndoStop(),this._editor.executeEdits("colorpicker",G);P.presentation.additionalTextEdits&&(G=[...P.presentation.additionalTextEdits],this._editor.executeEdits("colorpicker",G),this.hide()),this._editor.pushUndoStop(),X=j},ie=G=>p.getColorPresentations(le,{range:X,color:{red:G.rgba.r/255,green:G.rgba.g/255,blue:G.rgba.b/255,alpha:G.rgba.a}},W.provider,N.CancellationToken.None).then(j=>{P.colorPresentations=j||[]}),oe=P.onColorFlushed(G=>{ie(G).then($)}),ae=P.onDidChangeColor(ie);this._colorPicker=V,this.showAt(X.getStartPosition(),X,this._shouldFocus),this._updateContents(O),this._colorPicker.layout(),this._renderDisposable=w.combinedDisposable(oe,ae,V,B)}})}else W instanceof l.MarkerHover?F.push(W):W instanceof h.MarkdownHover&&D.push(W)}),D.length>0&&B.add(this._markdownHoverParticipant.renderHoverParts(D,O)),F.length&&B.add(this._markerHoverParticipant.renderHoverParts(F,O)),this._renderDisposable=B,!A&&O.hasChildNodes()&&(this.showAt(new S.Position(I.startLineNumber,E),T,this._shouldFocus),this._updateContents(O)),this._isChangingDecorations=!0,this._highlightDecorations=this._editor.deltaDecorations(this._highlightDecorations,T?[{range:T,options:v._DECORATION_OPTIONS}]:[]),this._isChangingDecorations=!1}}e.ModesContentHoverWidget=v,v.ID="editor.contrib.modesContentHoverWidget",v._DECORATION_OPTIONS=d.ModelDecorationOptions.register({className:"hoverHighlight"});function y(L,I){if(L.length!==I.length)return!1;for(let k=0;k{const k=L.getColor(i.textLinkForeground);k&&I.addRule(`.monaco-hover .hover-contents a.code-link span:hover { color: ${k}; }`)})}),define(Q[662],J([0,1,497,15,39,2,13,241,3,21,25,53,31,18,267,34,22,11,16]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SelectionHighlighter=e.CompatChangeAll=e.SelectHighlightsAction=e.MoveSelectionToPreviousFindMatchAction=e.MoveSelectionToNextFindMatchAction=e.AddSelectionToPreviousFindMatchAction=e.AddSelectionToNextFindMatchAction=e.MultiCursorSelectionControllerAction=e.MultiCursorSelectionController=e.MultiCursorSession=e.MultiCursorSessionResult=e.InsertCursorBelow=e.InsertCursorAbove=void 0;class t extends S.EditorAction{constructor(){super({id:"editor.action.insertCursorAbove",label:b.localize(0,null),alias:"Add Cursor Above",precondition:void 0,kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:2048|512|16,linux:{primary:1024|512|16,secondary:[2048|1024|16]},weight:100},menuOpts:{menuId:u.MenuId.MenubarSelectionMenu,group:"3_multi",title:b.localize(1,null),order:2}})}run(x,K,Y){if(!!K.hasModel()){const ee=Y&&Y.logicalLine===!0,se=K._getViewModel();se.cursorConfig.readOnly||(se.pushStackElement(),se.setCursorStates(Y.source,3,C.CursorMoveCommands.addCursorUp(se,se.getCursorStates(),ee)),se.revealTopMostCursor(Y.source))}}}e.InsertCursorAbove=t;class l extends S.EditorAction{constructor(){super({id:"editor.action.insertCursorBelow",label:b.localize(2,null),alias:"Add Cursor Below",precondition:void 0,kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:2048|512|18,linux:{primary:1024|512|18,secondary:[2048|1024|18]},weight:100},menuOpts:{menuId:u.MenuId.MenubarSelectionMenu,group:"3_multi",title:b.localize(3,null),order:3}})}run(x,K,Y){if(!!K.hasModel()){const ee=Y&&Y.logicalLine===!0,se=K._getViewModel();se.cursorConfig.readOnly||(se.pushStackElement(),se.setCursorStates(Y.source,3,C.CursorMoveCommands.addCursorDown(se,se.getCursorStates(),ee)),se.revealBottomMostCursor(Y.source))}}}e.InsertCursorBelow=l;class h extends S.EditorAction{constructor(){super({id:"editor.action.insertCursorAtEndOfEachLineSelected",label:b.localize(4,null),alias:"Add Cursors to Line Ends",precondition:void 0,kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:1024|512|39,weight:100},menuOpts:{menuId:u.MenuId.MenubarSelectionMenu,group:"3_multi",title:b.localize(5,null),order:4}})}getCursorsForSelection(x,K,Y){if(!x.isEmpty()){for(let ee=x.startLineNumber;ee1&&Y.push(new g.Selection(x.endLineNumber,x.endColumn,x.endLineNumber,x.endColumn))}}run(x,K){if(!!K.hasModel()){const Y=K.getModel(),ee=K.getSelections();let se=[];ee.forEach(ne=>this.getCursorsForSelection(ne,Y,se)),se.length>0&&K.setSelections(se)}}}class m extends S.EditorAction{constructor(){super({id:"editor.action.addCursorsToBottom",label:b.localize(6,null),alias:"Add Cursors To Bottom",precondition:void 0})}run(x,K){if(!!K.hasModel()){const Y=K.getSelections(),ee=K.getModel().getLineCount();let se=[];for(let ne=Y[0].startLineNumber;ne<=ee;ne++)se.push(new g.Selection(ne,Y[0].startColumn,ne,Y[0].endColumn));se.length>0&&K.setSelections(se)}}}class _ extends S.EditorAction{constructor(){super({id:"editor.action.addCursorsToTop",label:b.localize(7,null),alias:"Add Cursors To Top",precondition:void 0})}run(x,K){if(!!K.hasModel()){const Y=K.getSelections();let ee=[];for(let se=Y[0].startLineNumber;se>=1;se--)ee.push(new g.Selection(se,Y[0].startColumn,se,Y[0].endColumn));ee.length>0&&K.setSelections(ee)}}}class f{constructor(x,K,Y){this.selections=x,this.revealRange=K,this.revealScrollType=Y}}e.MultiCursorSessionResult=f;class v{constructor(x,K,Y,ee,se,ne,le){this._editor=x,this.findController=K,this.isDisconnectedFromFindController=Y,this.searchText=ee,this.wholeWord=se,this.matchCase=ne,this.currentMatch=le}static create(x,K){if(!x.hasModel())return null;const Y=K.getState();if(!x.hasTextFocus()&&Y.isRevealed&&Y.searchString.length>0)return new v(x,K,!1,Y.searchString,Y.wholeWord,Y.matchCase,null);let ee=!1,se,ne;const le=x.getSelections();le.length===1&&le[0].isEmpty()?(ee=!0,se=!0,ne=!0):(se=Y.wholeWord,ne=Y.matchCase);const X=x.getSelection();let z,P=null;if(X.isEmpty()){const V=x.getConfiguredWordAtPosition(X.getStartPosition());if(!V)return null;z=V.word,P=new g.Selection(X.startLineNumber,V.startColumn,X.startLineNumber,V.endColumn)}else z=x.getModel().getValueInRange(X).replace(/\r\n/g,` +`);return new v(x,K,ee,z,se,ne,P)}addSelectionToNextFindMatch(){if(!this._editor.hasModel())return null;const x=this._getNextMatch();if(!x)return null;const K=this._editor.getSelections();return new f(K.concat(x),x,0)}moveSelectionToNextFindMatch(){if(!this._editor.hasModel())return null;const x=this._getNextMatch();if(!x)return null;const K=this._editor.getSelections();return new f(K.slice(0,K.length-1).concat(x),x,0)}_getNextMatch(){if(!this._editor.hasModel())return null;if(this.currentMatch){const ee=this.currentMatch;return this.currentMatch=null,ee}this.findController.highlightFindOptions();const x=this._editor.getSelections(),K=x[x.length-1],Y=this._editor.getModel().findNextMatch(this.searchText,K.getEndPosition(),!1,this.matchCase,this.wholeWord?this._editor.getOption(110):null,!1);return Y?new g.Selection(Y.range.startLineNumber,Y.range.startColumn,Y.range.endLineNumber,Y.range.endColumn):null}addSelectionToPreviousFindMatch(){if(!this._editor.hasModel())return null;const x=this._getPreviousMatch();if(!x)return null;const K=this._editor.getSelections();return new f(K.concat(x),x,0)}moveSelectionToPreviousFindMatch(){if(!this._editor.hasModel())return null;const x=this._getPreviousMatch();if(!x)return null;const K=this._editor.getSelections();return new f(K.slice(0,K.length-1).concat(x),x,0)}_getPreviousMatch(){if(!this._editor.hasModel())return null;if(this.currentMatch){const ee=this.currentMatch;return this.currentMatch=null,ee}this.findController.highlightFindOptions();const x=this._editor.getSelections(),K=x[x.length-1],Y=this._editor.getModel().findPreviousMatch(this.searchText,K.getStartPosition(),!1,this.matchCase,this.wholeWord?this._editor.getOption(110):null,!1);return Y?new g.Selection(Y.range.startLineNumber,Y.range.startColumn,Y.range.endLineNumber,Y.range.endColumn):null}selectAll(){return this._editor.hasModel()?(this.findController.highlightFindOptions(),this._editor.getModel().findMatches(this.searchText,!0,!1,this.matchCase,this.wholeWord?this._editor.getOption(110):null,!1,1073741824)):[]}}e.MultiCursorSession=v;class y extends w.Disposable{constructor(x){super();this._sessionDispose=this._register(new w.DisposableStore),this._editor=x,this._ignoreSelectionChange=!1,this._session=null}static get(x){return x.getContribution(y.ID)}dispose(){this._endSession(),super.dispose()}_beginSessionIfNeeded(x){if(!this._session){const K=v.create(this._editor,x);if(!K)return;this._session=K;const Y={searchString:this._session.searchText};this._session.isDisconnectedFromFindController&&(Y.wholeWordOverride=1,Y.matchCaseOverride=1,Y.isRegexOverride=2),x.getState().change(Y,!1),this._sessionDispose.add(this._editor.onDidChangeCursorSelection(ee=>{this._ignoreSelectionChange||this._endSession()})),this._sessionDispose.add(this._editor.onDidBlurEditorText(()=>{this._endSession()})),this._sessionDispose.add(x.getState().onFindReplaceStateChange(ee=>{(ee.matchCase||ee.wholeWord)&&this._endSession()}))}}_endSession(){if(this._sessionDispose.clear(),this._session&&this._session.isDisconnectedFromFindController){const x={wholeWordOverride:0,matchCaseOverride:0,isRegexOverride:0};this._session.findController.getState().change(x,!1)}this._session=null}_setSelections(x){this._ignoreSelectionChange=!0,this._editor.setSelections(x),this._ignoreSelectionChange=!1}_expandEmptyToWord(x,K){if(!K.isEmpty())return K;const Y=this._editor.getConfiguredWordAtPosition(K.getStartPosition());return Y?new g.Selection(K.startLineNumber,Y.startColumn,K.startLineNumber,Y.endColumn):K}_applySessionResult(x){!x||(this._setSelections(x.selections),x.revealRange&&this._editor.revealRangeInCenterIfOutsideViewport(x.revealRange,x.revealScrollType))}getSession(x){return this._session}addSelectionToNextFindMatch(x){if(!!this._editor.hasModel()){if(!this._session){const K=this._editor.getSelections();if(K.length>1){const ee=x.getState().matchCase;if(!D(this._editor.getModel(),K,ee)){const ne=this._editor.getModel();let le=[];for(let X=0,z=K.length;X0&&Y.isRegex)K=this._editor.getModel().findMatches(Y.searchString,!0,Y.isRegex,Y.matchCase,Y.wholeWord?this._editor.getOption(110):null,!1,1073741824);else{if(this._beginSessionIfNeeded(x),!this._session)return;K=this._session.selectAll()}if(Y.searchScope){const ee=Y.searchScope;let se=[];K.forEach(ne=>{ee.forEach(le=>{ne.range.endLineNumber<=le.endLineNumber&&ne.range.startLineNumber>=le.startLineNumber&&se.push(ne)})}),K=se}if(K.length>0){const ee=this._editor.getSelection();for(let se=0,ne=K.length;senew g.Selection(se.range.startLineNumber,se.range.startColumn,se.range.endLineNumber,se.range.endColumn)))}}}}e.MultiCursorSelectionController=y,y.ID="editor.contrib.multiCursorController";class L extends S.EditorAction{run(x,K){const Y=y.get(K);if(!!Y){const ee=a.CommonFindController.get(K);!ee||this._run(Y,ee)}}}e.MultiCursorSelectionControllerAction=L;class I extends L{constructor(){super({id:"editor.action.addSelectionToNextFindMatch",label:b.localize(8,null),alias:"Add Selection To Next Find Match",precondition:void 0,kbOpts:{kbExpr:p.EditorContextKeys.focus,primary:2048|34,weight:100},menuOpts:{menuId:u.MenuId.MenubarSelectionMenu,group:"3_multi",title:b.localize(9,null),order:5}})}_run(x,K){x.addSelectionToNextFindMatch(K)}}e.AddSelectionToNextFindMatchAction=I;class k extends L{constructor(){super({id:"editor.action.addSelectionToPreviousFindMatch",label:b.localize(10,null),alias:"Add Selection To Previous Find Match",precondition:void 0,menuOpts:{menuId:u.MenuId.MenubarSelectionMenu,group:"3_multi",title:b.localize(11,null),order:6}})}_run(x,K){x.addSelectionToPreviousFindMatch(K)}}e.AddSelectionToPreviousFindMatchAction=k;class E extends L{constructor(){super({id:"editor.action.moveSelectionToNextFindMatch",label:b.localize(12,null),alias:"Move Last Selection To Next Find Match",precondition:void 0,kbOpts:{kbExpr:p.EditorContextKeys.focus,primary:M.KeyChord(2048|41,2048|34),weight:100}})}_run(x,K){x.moveSelectionToNextFindMatch(K)}}e.MoveSelectionToNextFindMatchAction=E;class T extends L{constructor(){super({id:"editor.action.moveSelectionToPreviousFindMatch",label:b.localize(13,null),alias:"Move Last Selection To Previous Find Match",precondition:void 0})}_run(x,K){x.moveSelectionToPreviousFindMatch(K)}}e.MoveSelectionToPreviousFindMatchAction=T;class O extends L{constructor(){super({id:"editor.action.selectHighlights",label:b.localize(14,null),alias:"Select All Occurrences of Find Match",precondition:void 0,kbOpts:{kbExpr:p.EditorContextKeys.focus,primary:2048|1024|42,weight:100},menuOpts:{menuId:u.MenuId.MenubarSelectionMenu,group:"3_multi",title:b.localize(15,null),order:7}})}_run(x,K){x.selectAll(K)}}e.SelectHighlightsAction=O;class A extends L{constructor(){super({id:"editor.action.changeAll",label:b.localize(16,null),alias:"Change All Occurrences",precondition:n.ContextKeyExpr.and(p.EditorContextKeys.writable,p.EditorContextKeys.editorTextFocus),kbOpts:{kbExpr:p.EditorContextKeys.editorTextFocus,primary:2048|60,weight:100},contextMenuOpts:{group:"1_modification",order:1.2}})}_run(x,K){x.selectAll(K)}}e.CompatChangeAll=A;class B{constructor(x,K,Y,ee){this.searchText=x,this.matchCase=K,this.wordSeparators=Y,this.modelVersionId=ee}static softEquals(x,K){return!x&&!K?!0:!x||!K?!1:x.searchText===K.searchText&&x.matchCase===K.matchCase&&x.wordSeparators===K.wordSeparators&&x.modelVersionId===K.modelVersionId}}class F extends w.Disposable{constructor(x){super();this.editor=x,this._isEnabled=x.getOption(92),this.decorations=[],this.updateSoon=this._register(new N.RunOnceScheduler(()=>this._update(),300)),this.state=null,this._register(x.onDidChangeConfiguration(K=>{this._isEnabled=x.getOption(92)})),this._register(x.onDidChangeCursorSelection(K=>{!this._isEnabled||(K.selection.isEmpty()?K.reason===3?(this.state&&this._setState(null),this.updateSoon.schedule()):this._setState(null):this._update())})),this._register(x.onDidChangeModel(K=>{this._setState(null)})),this._register(x.onDidChangeModelContent(K=>{this._isEnabled&&this.updateSoon.schedule()})),this._register(a.CommonFindController.get(x).getState().onFindReplaceStateChange(K=>{this._update()}))}_update(){this._setState(F._createState(this._isEnabled,this.editor))}static _createState(x,K){if(!x||!K.hasModel())return null;const Y=K.getSelection();if(Y.startLineNumber!==Y.endLineNumber)return null;const ee=y.get(K);if(!ee)return null;const se=a.CommonFindController.get(K);if(!se)return null;let ne=ee.getSession(se);if(!ne){const z=K.getSelections();if(z.length>1){const V=se.getState().matchCase;if(!D(K.getModel(),z,V))return null}ne=v.create(K,se)}if(!ne||ne.currentMatch||/^[ \t]+$/.test(ne.searchText)||ne.searchText.length>200)return null;const le=se.getState(),X=le.matchCase;if(le.isRevealed){let z=le.searchString;X||(z=z.toLowerCase());let P=ne.searchText;if(X||(P=P.toLowerCase()),z===P&&ne.matchCase===le.matchCase&&ne.wholeWord===le.wholeWord&&!le.isRegex)return null}return new B(ne.searchText,ne.matchCase,ne.wholeWord?K.getOption(110):null,K.getModel().getVersionId())}_setState(x){if(B.softEquals(this.state,x)){this.state=x;return}if(this.state=x,!this.state){this.decorations=this.editor.deltaDecorations(this.decorations,[]);return}if(!!this.editor.hasModel()){const K=this.editor.getModel();if(!K.isTooLargeForTokenization()){const Y=s.DocumentHighlightProviderRegistry.has(K)&&this.editor.getOption(66);let ee=K.findMatches(this.state.searchText,!0,!1,this.state.matchCase,this.state.wordSeparators,!1).map(X=>X.range);ee.sort(d.Range.compareRangesUsingStarts);let se=this.editor.getSelections();se.sort(d.Range.compareRangesUsingStarts);let ne=[];for(let X=0,z=0,P=ee.length,V=se.length;X=V)ne.push(U),X++;else{const H=d.Range.compareRangesUsingStarts(U,se[z]);H<0?((se[z].isEmpty()||!d.Range.areIntersecting(U,se[z]))&&ne.push(U),X++):(H>0||X++,z++)}}const le=ne.map(X=>({range:X,options:Y?F._SELECTION_HIGHLIGHT:F._SELECTION_HIGHLIGHT_OVERVIEW}));this.decorations=this.editor.deltaDecorations(this.decorations,le)}}}dispose(){this._setState(null),super.dispose()}}e.SelectionHighlighter=F,F.ID="editor.contrib.selectionHighlighter",F._SELECTION_HIGHLIGHT_OVERVIEW=o.ModelDecorationOptions.register({stickiness:1,className:"selectionHighlight",overviewRuler:{color:i.themeColorFromId(r.overviewRulerSelectionHighlightForeground),position:c.OverviewRulerLane.Center}}),F._SELECTION_HIGHLIGHT=o.ModelDecorationOptions.register({stickiness:1,className:"selectionHighlight"});function D(W,x,K){const Y=R(W,x[0],!K);for(let ee=1,se=x.length;ee{T?(this.show(),this.render(T)):this.hide()}))}createParamaterHintDOMNodes(){const L=m(".editor-widget.parameter-hints-widget"),I=b.append(L,m(".phwrapper"));I.tabIndex=-1;const k=b.append(I,m(".controls")),E=b.append(k,m(".button"+u.ThemeIcon.asCSSSelector(f))),T=b.append(k,m(".overloads")),O=b.append(k,m(".button"+u.ThemeIcon.asCSSSelector(_))),A=N.stop(N.domEvent(E,"click"));this._register(A(this.previous,this));const B=N.stop(N.domEvent(O,"click"));this._register(B(this.next,this));const F=m(".body"),D=new w.DomScrollableElement(F,{});this._register(D),I.appendChild(D.getDomNode());const R=b.append(F,m(".signature")),W=b.append(F,m(".docs"));L.style.userSelect="text",this.domNodes={element:L,signature:R,overloads:T,docs:W,scrollbar:D},this.editor.addContentWidget(this),this.hide(),this._register(this.editor.onDidChangeCursorSelection(K=>{this.visible&&this.editor.layoutContentWidget(this)}));const x=()=>{if(!!this.domNodes){const K=this.editor.getOption(38);this.domNodes.element.style.fontSize=`${K.fontSize}px`}};x(),this._register(S.Event.chain(this.editor.onDidChangeConfiguration.bind(this.editor)).filter(K=>K.hasChanged(38)).on(x,null)),this._register(this.editor.onDidLayoutChange(K=>this.updateMaxHeight())),this.updateMaxHeight()}show(){this.visible||(this.domNodes||this.createParamaterHintDOMNodes(),this.keyVisible.set(!0),this.visible=!0,setTimeout(()=>{this.domNodes&&this.domNodes.element.classList.add("visible")},100),this.editor.layoutContentWidget(this))}hide(){this.renderDisposeables.clear(),!!this.visible&&(this.keyVisible.reset(),this.visible=!1,this.announcedLabel=null,this.domNodes&&this.domNodes.element.classList.remove("visible"),this.editor.layoutContentWidget(this))}getPosition(){return this.visible?{position:this.editor.getPosition(),preference:[1,2]}:null}render(L){var I;if(this.renderDisposeables.clear(),!!this.domNodes){const k=L.signatures.length>1;this.domNodes.element.classList.toggle("multiple",k),this.keyMultipleSignatures.set(k),this.domNodes.signature.innerText="",this.domNodes.docs.innerText="";const E=L.signatures[L.activeSignature];if(!!E){const T=b.append(this.domNodes.signature,m(".code")),O=this.editor.getOption(38);T.style.fontSize=`${O.fontSize}px`,T.style.fontFamily=O.fontFamily;const A=E.parameters.length>0,B=(I=E.activeParameter)!==null&&I!==void 0?I:L.activeParameter;if(A)this.renderParameters(T,E,B);else{const R=b.append(T,m("span"));R.textContent=E.label}const F=E.parameters[B];if(F==null?void 0:F.documentation){const R=m("span.documentation");if(typeof F.documentation=="string")R.textContent=F.documentation;else{const W=this.renderMarkdownDocs(F.documentation);R.appendChild(W.element)}b.append(this.domNodes.docs,m("p",{},R))}if(E.documentation!==void 0)if(typeof E.documentation=="string")b.append(this.domNodes.docs,m("p",{},E.documentation));else{const R=this.renderMarkdownDocs(E.documentation);b.append(this.domNodes.docs,R.element)}const D=this.hasDocs(E,F);if(this.domNodes.signature.classList.toggle("has-docs",D),this.domNodes.docs.classList.toggle("empty",!D),this.domNodes.overloads.textContent=String(L.activeSignature+1).padStart(L.signatures.length.toString().length,"0")+"/"+L.signatures.length,F){const R=this.getParameterLabel(E,B);this.announcedLabel!==R&&(M.alert(c.localize(2,null,R)),this.announcedLabel=R)}this.editor.layoutContentWidget(this),this.domNodes.scrollbar.scanDomNode()}}}renderMarkdownDocs(L){const I=this.renderDisposeables.add(this.markdownRenderer.render(L,{asyncRenderCallback:()=>{var k;(k=this.domNodes)===null||k===void 0||k.scrollbar.scanDomNode()}}));return I.element.classList.add("markdown-docs"),I}hasDocs(L,I){return!!(I&&typeof I.documentation=="string"&&t.assertIsDefined(I.documentation).length>0||I&&typeof I.documentation=="object"&&t.assertIsDefined(I.documentation).value.length>0||L.documentation&&typeof L.documentation=="string"&&t.assertIsDefined(L.documentation).length>0||L.documentation&&typeof L.documentation=="object"&&t.assertIsDefined(L.documentation.value).length>0)}renderParameters(L,I,k){const[E,T]=this.getParameterLabelOffsets(I,k),O=document.createElement("span");O.textContent=I.label.substring(0,E);const A=document.createElement("span");A.textContent=I.label.substring(E,T),A.className="parameter active";const B=document.createElement("span");B.textContent=I.label.substring(T),b.append(L,O,A,B)}getParameterLabel(L,I){const k=L.parameters[I];return Array.isArray(k.label)?L.label.substring(k.label[0],k.label[1]):k.label}getParameterLabelOffsets(L,I){const k=L.parameters[I];if(k){if(Array.isArray(k.label))return k.label;if(k.label.length){const E=new RegExp(`(\\W|^)${i.escapeRegExpCharacters(k.label)}(?=\\W|$)`,"g");E.test(L.label);const T=E.lastIndex-k.label.length;return T>=0?[T,E.lastIndex]:[0,0]}else return[0,0]}else return[0,0]}next(){this.editor.focus(),this.model.next()}previous(){this.editor.focus(),this.model.previous()}cancel(){this.model.cancel()}getDomNode(){return this.domNodes||this.createParamaterHintDOMNodes(),this.domNodes.element}getId(){return jt.ID}trigger(L){this.model.trigger(L,0)}updateMaxHeight(){if(!!this.domNodes){const I=`${Math.max(this.editor.getLayoutInfo().height/4,250)}px`;this.domNodes.element.style.maxHeight=I;const k=this.domNodes.element.getElementsByClassName("phwrapper");k.length&&(k[0].style.maxHeight=I)}}};v.ID="editor.widget.parameterHintsWidget",v=Me([_e(1,o.IContextKeyService),_e(2,s.IOpenerService),_e(3,d.IModeService)],v),e.ParameterHintsWidget=v,u.registerThemingParticipant((y,L)=>{const I=y.getColor(a.editorHoverBorder);if(I){const A=y.type===l.ColorScheme.HIGH_CONTRAST?2:1;L.addRule(`.monaco-editor .parameter-hints-widget { border: ${A}px solid ${I}; }`),L.addRule(`.monaco-editor .parameter-hints-widget.multiple .body { border-left: 1px solid ${I.transparent(.5)}; }`),L.addRule(`.monaco-editor .parameter-hints-widget .signature.has-docs { border-bottom: 1px solid ${I.transparent(.5)}; }`)}const k=y.getColor(a.editorHoverBackground);k&&L.addRule(`.monaco-editor .parameter-hints-widget { background-color: ${k}; }`);const E=y.getColor(a.textLinkForeground);E&&L.addRule(`.monaco-editor .parameter-hints-widget a { color: ${E}; }`);const T=y.getColor(a.editorHoverForeground);T&&L.addRule(`.monaco-editor .parameter-hints-widget { color: ${T}; }`);const O=y.getColor(a.textCodeBlockBackground);O&&L.addRule(`.monaco-editor .parameter-hints-widget code { background-color: ${O}; }`)})}),define(Q[664],J([0,1,498,2,9,25,16,13,663,183,18]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TriggerParameterHintsAction=void 0;let c=class Gt extends N.Disposable{constructor(r,i){super();this.editor=r,this.widget=this._register(i.createInstance(d.ParameterHintsWidget,this.editor))}static get(r){return r.getContribution(Gt.ID)}cancel(){this.widget.cancel()}previous(){this.widget.previous()}next(){this.widget.next()}trigger(r){this.widget.trigger(r)}};c.ID="editor.controller.parameterHints",c=Me([_e(1,M.IInstantiationService)],c);class o extends C.EditorAction{constructor(){super({id:"editor.action.triggerParameterHints",label:b.localize(0,null),alias:"Trigger Parameter Hints",precondition:w.EditorContextKeys.hasSignatureHelpProvider,kbOpts:{kbExpr:w.EditorContextKeys.editorTextFocus,primary:2048|1024|10,weight:100}})}run(r,i){const n=c.get(i);n&&n.trigger({triggerKind:p.SignatureHelpTriggerKind.Invoke})}}e.TriggerParameterHintsAction=o,C.registerEditorContribution(c.ID,c),C.registerEditorAction(o);const s=100+75,a=C.EditorCommand.bindToContribution(c.get);C.registerEditorCommand(new a({id:"closeParameterHints",precondition:g.Context.Visible,handler:u=>u.cancel(),kbOpts:{weight:s,kbExpr:w.EditorContextKeys.focus,primary:9,secondary:[1024|9]}})),C.registerEditorCommand(new a({id:"showPrevParameterHint",precondition:S.ContextKeyExpr.and(g.Context.Visible,g.Context.MultipleSignatures),handler:u=>u.previous(),kbOpts:{weight:s,kbExpr:w.EditorContextKeys.focus,primary:16,secondary:[512|16],mac:{primary:16,secondary:[512|16,256|46]}}})),C.registerEditorCommand(new a({id:"showNextParameterHint",precondition:S.ContextKeyExpr.and(g.Context.Visible,g.Context.MultipleSignatures),handler:u=>u.next(),kbOpts:{weight:s,kbExpr:w.EditorContextKeys.focus,primary:18,secondary:[512|18],mac:{primary:18,secondary:[512|18,256|44]}}}))}),define(Q[665],J([0,1,510,66,2,7,11,57,18,175,562,36,24,237,19,254,27,6,80]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n){"use strict";var t;Object.defineProperty(e,"__esModule",{value:!0}),e.ItemRenderer=e.suggestMoreInfoIcon=e.getAriaId=void 0;function l(_){return`suggest-aria-id:${_}`}e.getAriaId=l,e.suggestMoreInfoIcon=n.registerIcon("suggest-more-info",r.Codicon.chevronRight,b.localize(0,null));const h=new(t=class ft{extract(f,v){if(f.textLabel.match(ft._regexStrict))return v[0]=f.textLabel,!0;if(f.completion.detail&&f.completion.detail.match(ft._regexStrict))return v[0]=f.completion.detail,!0;if(typeof f.completion.documentation=="string"){const y=ft._regexRelaxed.exec(f.completion.documentation);if(y&&(y.index===0||y.index+y[0].length===f.completion.documentation.length))return v[0]=y[0],!0}return!1}},t._regexRelaxed=/(#([\da-fA-F]{3}){1,2}|(rgb|hsl)a\(\s*(\d{1,3}%?\s*,\s*){3}(1|0?\.\d+)\)|(rgb|hsl)\(\s*\d{1,3}%?(\s*,\s*\d{1,3}%?){2}\s*\))/,t._regexStrict=new RegExp(`^${t._regexRelaxed.source}$`,"i"),t);let m=class{constructor(f,v,y,L){this._editor=f,this._modelService=v,this._modeService=y,this._themeService=L,this._onDidToggleDetails=new i.Emitter,this.onDidToggleDetails=this._onDidToggleDetails.event,this.templateId="suggestion"}dispose(){this._onDidToggleDetails.dispose()}renderTemplate(f){const v=Object.create(null);v.disposables=new M.DisposableStore,v.root=f,v.root.classList.add("show-file-icons"),v.icon=w.append(f,w.$(".icon")),v.colorspan=w.append(v.icon,w.$("span.colorspan"));const y=w.append(f,w.$(".contents")),L=w.append(y,w.$(".main"));v.iconContainer=w.append(L,w.$(".icon-label.codicon")),v.left=w.append(L,w.$("span.left")),v.right=w.append(L,w.$("span.right")),v.iconLabel=new g.IconLabel(v.left,{supportHighlights:!0,supportIcons:!0}),v.disposables.add(v.iconLabel),v.parametersLabel=w.append(v.left,w.$("span.signature-label")),v.qualifierLabel=w.append(v.left,w.$("span.qualifier-label")),v.detailsLabel=w.append(v.right,w.$("span.details-label")),v.readMore=w.append(v.right,w.$("span.readMore"+S.ThemeIcon.asCSSSelector(e.suggestMoreInfoIcon))),v.readMore.title=b.localize(1,null);const I=()=>{const k=this._editor.getOptions(),E=k.get(38),T=E.fontFamily,O=E.fontFeatureSettings,A=k.get(102)||E.fontSize,B=k.get(103)||E.lineHeight,F=E.fontWeight,D=`${A}px`,R=`${B}px`;v.root.style.fontSize=D,v.root.style.fontWeight=F,L.style.fontFamily=T,L.style.fontFeatureSettings=O,L.style.lineHeight=R,v.icon.style.height=R,v.icon.style.width=R,v.readMore.style.height=R,v.readMore.style.width=R};return I(),v.disposables.add(this._editor.onDidChangeConfiguration(k=>{(k.hasChanged(38)||k.hasChanged(102)||k.hasChanged(103))&&I()})),v}renderElement(f,v,y){var L,I,k;const{completion:E}=f,T=typeof E.label=="string"?E.label:E.label.name;y.root.id=l(v),y.colorspan.style.backgroundColor="";const O={labelEscapeNewLines:!0,matches:N.createMatches(f.score)};let A=[];if(E.kind===19&&h.extract(f,A))y.icon.className="icon customcolor",y.iconContainer.className="icon hide",y.colorspan.style.backgroundColor=A[0];else if(E.kind===20&&this._themeService.getFileIconTheme().hasFileIcons){y.icon.className="icon hide",y.iconContainer.className="icon hide";const B=p.getIconClasses(this._modelService,this._modeService,o.URI.from({scheme:"fake",path:T}),s.FileKind.FILE),F=p.getIconClasses(this._modelService,this._modeService,o.URI.from({scheme:"fake",path:E.detail}),s.FileKind.FILE);O.extraClasses=B.length>F.length?B:F}else E.kind===23&&this._themeService.getFileIconTheme().hasFolderIcons?(y.icon.className="icon hide",y.iconContainer.className="icon hide",O.extraClasses=a.flatten([p.getIconClasses(this._modelService,this._modeService,o.URI.from({scheme:"fake",path:T}),s.FileKind.FOLDER),p.getIconClasses(this._modelService,this._modeService,o.URI.from({scheme:"fake",path:E.detail}),s.FileKind.FOLDER)])):(y.icon.className="icon hide",y.iconContainer.className="",y.iconContainer.classList.add("suggest-icon",...d.completionKindToCssClass(E.kind).split(" ")));E.tags&&E.tags.indexOf(1)>=0&&(O.extraClasses=(O.extraClasses||[]).concat(["deprecated"]),O.matches=[]),y.iconLabel.setLabel(T,void 0,O),typeof E.label=="string"?(y.parametersLabel.textContent="",y.qualifierLabel.textContent="",y.detailsLabel.textContent=(E.detail||"").replace(/\n.*$/m,""),y.root.classList.add("string-label"),y.root.title=""):(y.parametersLabel.textContent=(E.label.parameters||"").replace(/\n.*$/m,""),y.qualifierLabel.textContent=(E.label.qualifier||"").replace(/\n.*$/m,""),y.detailsLabel.textContent=(E.label.type||"").replace(/\n.*$/m,""),y.root.classList.remove("string-label"),y.root.title=`${T}${(L=E.label.parameters)!==null&&L!==void 0?L:""} ${(I=E.label.qualifier)!==null&&I!==void 0?I:""} ${(k=E.label.type)!==null&&k!==void 0?k:""}`),this._editor.getOption(101).showInlineDetails?w.show(y.detailsLabel):w.hide(y.detailsLabel),u.canExpandCompletionItem(f)?(y.right.classList.add("can-expand-details"),w.show(y.readMore),y.readMore.onmousedown=B=>{B.stopPropagation(),B.preventDefault()},y.readMore.onclick=B=>{B.stopPropagation(),B.preventDefault(),this._onDidToggleDetails.fire()}):(y.right.classList.remove("can-expand-details"),w.hide(y.readMore),y.readMore.onmousedown=null,y.readMore.onclick=null)}disposeTemplate(f){f.disposables.dispose()}};m=Me([_e(1,c.IModelService),_e(2,C.IModeService),_e(3,S.IThemeService)],m),e.ItemRenderer=m}),define(Q[666],J([0,1,508,8,7,6,12,2,105,16,117,116,11,22,79,15,9,254,649,665,231,144,100,352,123,259]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SuggestContentWidget=e.SuggestWidget=e.editorSuggestWidgetHighlightForeground=e.editorSuggestWidgetSelectedBackground=e.editorSuggestWidgetForeground=e.editorSuggestWidgetBorder=e.editorSuggestWidgetBackground=void 0,e.editorSuggestWidgetBackground=s.registerColor("editorSuggestWidget.background",{dark:s.editorWidgetBackground,light:s.editorWidgetBackground,hc:s.editorWidgetBackground},b.localize(0,null)),e.editorSuggestWidgetBorder=s.registerColor("editorSuggestWidget.border",{dark:s.editorWidgetBorder,light:s.editorWidgetBorder,hc:s.editorWidgetBorder},b.localize(1,null)),e.editorSuggestWidgetForeground=s.registerColor("editorSuggestWidget.foreground",{dark:s.editorForeground,light:s.editorForeground,hc:s.editorForeground},b.localize(2,null)),e.editorSuggestWidgetSelectedBackground=s.registerColor("editorSuggestWidget.selectedBackground",{dark:s.quickInputListFocusBackground,light:s.quickInputListFocusBackground,hc:s.quickInputListFocusBackground},b.localize(3,null)),e.editorSuggestWidgetHighlightForeground=s.registerColor("editorSuggestWidget.highlightForeground",{dark:s.listHighlightForeground,light:s.listHighlightForeground,hc:s.listHighlightForeground},b.localize(4,null));class _{constructor(L,I){this._service=L,this._key=`suggestWidget.size/${I.getEditorType()}/${I instanceof h.EmbeddedCodeEditorWidget}`}restore(){var L;const I=(L=this._service.get(this._key,0))!==null&&L!==void 0?L:"";try{const k=JSON.parse(I);if(M.Dimension.is(k))return M.Dimension.lift(k)}catch(k){}}store(L){this._service.store(this._key,JSON.stringify(L),0,1)}reset(){this._service.remove(this._key,0)}}let f=class wt{constructor(L,I,k,E,T){this.editor=L,this._storageService=I,this._state=0,this._isAuto=!1,this._ignoreFocusEvents=!1,this._explainMode=!1,this._showTimeout=new u.TimeoutTimer,this._disposables=new C.DisposableStore,this._onDidSelect=new w.Emitter,this._onDidFocus=new w.Emitter,this._onDidHide=new w.Emitter,this._onDidShow=new w.Emitter,this.onDidSelect=this._onDidSelect.event,this.onDidFocus=this._onDidFocus.event,this.onDidHide=this._onDidHide.event,this.onDidShow=this._onDidShow.event,this._onDetailsKeydown=new w.Emitter,this.onDetailsKeyDown=this._onDetailsKeydown.event,this.element=new l.ResizableHTMLElement,this.element.domNode.classList.add("editor-widget","suggest-widget"),this._contentWidget=new v(this,L),this._persistedSize=new _(I,L);class O{constructor(x,K,Y=!1,ee=!1){this.persistedSize=x,this.currentSize=K,this.persistHeight=Y,this.persistWidth=ee}}let A;this._disposables.add(this.element.onDidWillResize(()=>{this._contentWidget.lockPreference(),A=new O(this._persistedSize.restore(),this.element.size)})),this._disposables.add(this.element.onDidResize(W=>{var x,K,Y,ee;if(this._resize(W.dimension.width,W.dimension.height),A&&(A.persistHeight=A.persistHeight||!!W.north||!!W.south,A.persistWidth=A.persistWidth||!!W.east||!!W.west),!!W.done){if(A){const{itemHeight:se,defaultSize:ne}=this.getLayoutInfo(),le=Math.round(se/2);let{width:X,height:z}=this.element.size;(!A.persistHeight||Math.abs(A.currentSize.height-z)<=le)&&(z=(K=(x=A.persistedSize)===null||x===void 0?void 0:x.height)!==null&&K!==void 0?K:ne.height),(!A.persistWidth||Math.abs(A.currentSize.width-X)<=le)&&(X=(ee=(Y=A.persistedSize)===null||Y===void 0?void 0:Y.width)!==null&&ee!==void 0?ee:ne.width),this._persistedSize.store(new M.Dimension(X,z))}this._contentWidget.unlockPreference(),A=void 0}})),this._messageElement=M.append(this.element.domNode,M.$(".message")),this._listElement=M.append(this.element.domNode,M.$(".tree"));const B=T.createInstance(i.SuggestDetailsWidget,this.editor);B.onDidClose(this.toggleDetails,this,this._disposables),this._details=new i.SuggestDetailsOverlay(B,this.editor);const F=()=>this.element.domNode.classList.toggle("no-icons",!this.editor.getOption(101).showIcons);F();const D=T.createInstance(t.ItemRenderer,this.editor);this._disposables.add(D),this._disposables.add(D.onDidToggleDetails(()=>this.toggleDetails())),this._list=new d.List("SuggestWidget",this._listElement,{getHeight:W=>this.getLayoutInfo().itemHeight,getTemplateId:W=>"suggestion"},[D],{alwaysConsumeMouseWheel:!0,useShadows:!1,mouseSupport:!1,accessibilityProvider:{getRole:()=>"option",getAriaLabel:W=>{const x=typeof W.completion.label=="string"?W.completion.label:W.completion.label.name;if(W.isResolved&&this._isDetailsVisible()){const{documentation:K,detail:Y}=W.completion,ee=N.format("{0}{1}",Y||"",K?typeof K=="string"?K:K.value:"");return b.localize(7,null,x,ee)}else return x},getWidgetAriaLabel:()=>b.localize(8,null),getWidgetRole:()=>"listbox"}}),this._status=T.createInstance(n.SuggestWidgetStatus,this.element.domNode);const R=()=>this.element.domNode.classList.toggle("with-status-bar",this.editor.getOption(101).showStatusBar);R(),this._disposables.add(c.attachListStyler(this._list,E,{listInactiveFocusBackground:e.editorSuggestWidgetSelectedBackground,listInactiveFocusOutline:s.activeContrastBorder})),this._disposables.add(E.onDidColorThemeChange(W=>this._onThemeChange(W))),this._onThemeChange(E.getColorTheme()),this._disposables.add(this._list.onMouseDown(W=>this._onListMouseDownOrTap(W))),this._disposables.add(this._list.onTap(W=>this._onListMouseDownOrTap(W))),this._disposables.add(this._list.onDidChangeSelection(W=>this._onListSelection(W))),this._disposables.add(this._list.onDidChangeFocus(W=>this._onListFocus(W))),this._disposables.add(this.editor.onDidChangeCursorSelection(()=>this._onCursorSelectionChanged())),this._disposables.add(this.editor.onDidChangeConfiguration(W=>{W.hasChanged(101)&&(R(),F())})),this._ctxSuggestWidgetVisible=p.Context.Visible.bindTo(k),this._ctxSuggestWidgetDetailsVisible=p.Context.DetailsVisible.bindTo(k),this._ctxSuggestWidgetMultipleSuggestions=p.Context.MultipleSuggestions.bindTo(k),this._disposables.add(M.addStandardDisposableListener(this._details.widget.domNode,"keydown",W=>{this._onDetailsKeydown.fire(W)})),this._disposables.add(this.editor.onMouseDown(W=>this._onEditorMouseDown(W)))}dispose(){var L;this._details.widget.dispose(),this._details.dispose(),this._list.dispose(),this._status.dispose(),this._disposables.dispose(),(L=this._loadingTimeout)===null||L===void 0||L.dispose(),this._showTimeout.dispose(),this._contentWidget.dispose(),this.element.dispose()}_onEditorMouseDown(L){this._details.widget.domNode.contains(L.target.element)?this._details.widget.domNode.focus():this.element.domNode.contains(L.target.element)&&this.editor.focus()}_onCursorSelectionChanged(){this._state!==0&&this._contentWidget.layout()}_onListMouseDownOrTap(L){typeof L.element=="undefined"||typeof L.index=="undefined"||(L.browserEvent.preventDefault(),L.browserEvent.stopPropagation(),this._select(L.element,L.index))}_onListSelection(L){L.elements.length&&this._select(L.elements[0],L.indexes[0])}_select(L,I){const k=this._completionModel;k&&(this._onDidSelect.fire({item:L,index:I,model:k}),this.editor.focus())}_onThemeChange(L){const I=L.getColor(e.editorSuggestWidgetBackground);I&&(this.element.domNode.style.backgroundColor=I.toString(),this._messageElement.style.backgroundColor=I.toString(),this._details.widget.domNode.style.backgroundColor=I.toString());const k=L.getColor(e.editorSuggestWidgetBorder);k&&(this.element.domNode.style.borderColor=k.toString(),this._messageElement.style.borderColor=k.toString(),this._status.element.style.borderTopColor=k.toString(),this._details.widget.domNode.style.borderColor=k.toString(),this._detailsBorderColor=k.toString());const E=L.getColor(s.focusBorder);E&&(this._detailsFocusBorderColor=E.toString()),this._details.widget.borderWidth=L.type==="hc"?2:1}_onListFocus(L){var I;if(!this._ignoreFocusEvents){if(!L.elements.length){this._currentSuggestionDetails&&(this._currentSuggestionDetails.cancel(),this._currentSuggestionDetails=void 0,this._focusedItem=void 0),this.editor.setAriaOptions({activeDescendant:void 0});return}if(!!this._completionModel){const k=L.elements[0],E=L.indexes[0];k!==this._focusedItem&&((I=this._currentSuggestionDetails)===null||I===void 0||I.cancel(),this._currentSuggestionDetails=void 0,this._focusedItem=k,this._list.reveal(E),this._currentSuggestionDetails=u.createCancelablePromise(T=>Ie(this,void 0,void 0,function*(){const O=u.disposableTimeout(()=>{this._isDetailsVisible()&&this.showDetails(!0)},250);T.onCancellationRequested(()=>O.dispose());const A=yield k.resolve(T);return O.dispose(),A})),this._currentSuggestionDetails.then(()=>{E>=this._list.length||k!==this._list.element(E)||(this._ignoreFocusEvents=!0,this._list.splice(E,1,[k]),this._list.setFocus([E]),this._ignoreFocusEvents=!1,this._isDetailsVisible()?this.showDetails(!1):this.element.domNode.classList.remove("docs-side"),this.editor.setAriaOptions({activeDescendant:t.getAriaId(E)}))}).catch(S.onUnexpectedError)),this._onDidFocus.fire({item:k,index:E,model:this._completionModel})}}}_setState(L){if(this._state!==L)switch(this._state=L,this.element.domNode.classList.toggle("frozen",L===4),this.element.domNode.classList.remove("message"),L){case 0:M.hide(this._messageElement,this._listElement,this._status.element),this._details.hide(!0),this._status.hide(),this._contentWidget.hide(),this._ctxSuggestWidgetVisible.reset(),this._ctxSuggestWidgetMultipleSuggestions.reset(),this.element.domNode.classList.remove("visible"),this._list.splice(0,this._list.length),this._focusedItem=void 0,this._cappedHeight=void 0,this._explainMode=!1;break;case 1:this.element.domNode.classList.add("message"),this._messageElement.textContent=wt.LOADING_MESSAGE,M.hide(this._listElement,this._status.element),M.show(this._messageElement),this._details.hide(),this._show(),this._focusedItem=void 0;break;case 2:this.element.domNode.classList.add("message"),this._messageElement.textContent=wt.NO_SUGGESTIONS_MESSAGE,M.hide(this._listElement,this._status.element),M.show(this._messageElement),this._details.hide(),this._show(),this._focusedItem=void 0;break;case 3:M.hide(this._messageElement),M.show(this._listElement,this._status.element),this._show();break;case 4:M.hide(this._messageElement),M.show(this._listElement,this._status.element),this._show();break;case 5:M.hide(this._messageElement),M.show(this._listElement,this._status.element),this._details.show(),this._show();break}}_show(){this._status.show(),this._contentWidget.show(),this._layout(this._persistedSize.restore()),this._ctxSuggestWidgetVisible.set(!0),this._showTimeout.cancelAndSet(()=>{this.element.domNode.classList.add("visible"),this._onDidShow.fire(this)},100)}showTriggered(L,I){this._state===0&&(this._contentWidget.setPosition(this.editor.getPosition()),this._isAuto=!!L,this._isAuto||(this._loadingTimeout=u.disposableTimeout(()=>this._setState(1),I)))}showSuggestions(L,I,k,E){var T,O;if(this._contentWidget.setPosition(this.editor.getPosition()),(T=this._loadingTimeout)===null||T===void 0||T.dispose(),(O=this._currentSuggestionDetails)===null||O===void 0||O.cancel(),this._currentSuggestionDetails=void 0,this._completionModel!==L&&(this._completionModel=L),k&&this._state!==2&&this._state!==0){this._setState(4);return}const A=this._completionModel.items.length,B=A===0;if(this._ctxSuggestWidgetMultipleSuggestions.set(A>1),B){this._setState(E?0:2),this._completionModel=void 0;return}this._focusedItem=void 0,this._list.splice(0,this._list.length,this._completionModel.items),this._setState(k?4:3),this._list.reveal(I,0),this._list.setFocus([I]),this._layout(this.element.size),this._detailsBorderColor&&(this._details.widget.domNode.style.borderColor=this._detailsBorderColor)}selectNextPage(){switch(this._state){case 0:return!1;case 5:return this._details.widget.pageDown(),!0;case 1:return!this._isAuto;default:return this._list.focusNextPage(),!0}}selectNext(){switch(this._state){case 0:return!1;case 1:return!this._isAuto;default:return this._list.focusNext(1,!0),!0}}selectLast(){switch(this._state){case 0:return!1;case 5:return this._details.widget.scrollBottom(),!0;case 1:return!this._isAuto;default:return this._list.focusLast(),!0}}selectPreviousPage(){switch(this._state){case 0:return!1;case 5:return this._details.widget.pageUp(),!0;case 1:return!this._isAuto;default:return this._list.focusPreviousPage(),!0}}selectPrevious(){switch(this._state){case 0:return!1;case 1:return!this._isAuto;default:return this._list.focusPrevious(1,!0),!1}}selectFirst(){switch(this._state){case 0:return!1;case 5:return this._details.widget.scrollTop(),!0;case 1:return!this._isAuto;default:return this._list.focusFirst(),!0}}getFocusedItem(){if(this._state!==0&&this._state!==2&&this._state!==1&&this._completionModel)return{item:this._list.getFocusedElements()[0],index:this._list.getFocus()[0],model:this._completionModel}}toggleDetailsFocus(){this._state===5?(this._setState(3),this._detailsBorderColor&&(this._details.widget.domNode.style.borderColor=this._detailsBorderColor)):this._state===3&&this._isDetailsVisible()&&(this._setState(5),this._detailsFocusBorderColor&&(this._details.widget.domNode.style.borderColor=this._detailsFocusBorderColor))}toggleDetails(){this._isDetailsVisible()?(this._ctxSuggestWidgetDetailsVisible.set(!1),this._setDetailsVisible(!1),this._details.hide(),this.element.domNode.classList.remove("shows-details")):i.canExpandCompletionItem(this._list.getFocusedElements()[0])&&(this._state===3||this._state===5||this._state===4)&&(this._ctxSuggestWidgetDetailsVisible.set(!0),this._setDetailsVisible(!0),this.showDetails(!1))}showDetails(L){this._details.show(),L?this._details.widget.renderLoading():this._details.widget.renderItem(this._list.getFocusedElements()[0],this._explainMode),this._positionDetails(),this.editor.focus(),this.element.domNode.classList.add("shows-details")}toggleExplainMode(){this._list.getFocusedElements()[0]&&this._isDetailsVisible()&&(this._explainMode=!this._explainMode,this.showDetails(!1))}resetPersistedSize(){this._persistedSize.reset()}hideWidget(){var L;(L=this._loadingTimeout)===null||L===void 0||L.dispose(),this._setState(0),this._onDidHide.fire(this);const I=this._persistedSize.restore(),k=Math.ceil(this.getLayoutInfo().itemHeight*4.3);I&&I.heightF&&(B=F);const D=this._completionModel?this._completionModel.stats.pLabelLen*O.typicalHalfwidthCharacterWidth:B,R=O.statusBarHeight+this._list.contentHeight+O.borderHeight,W=O.itemHeight+O.statusBarHeight,x=M.getDomNodePagePosition(this.editor.getDomNode()),K=this.editor.getScrolledVisiblePosition(this.editor.getPosition()),Y=x.top+K.top+K.height,ee=Math.min(T.height-Y-O.verticalPadding,R),se=Math.min(x.top+K.top-O.verticalPadding,R);let ne=Math.min(Math.max(se,ee)+O.borderHeight,R);A===((I=this._cappedHeight)===null||I===void 0?void 0:I.capped)&&(A=this._cappedHeight.wanted),Ane&&(A=ne),A>ee?(this._contentWidget.setPreference(1),this.element.enableSashes(!0,!0,!1,!1),ne=se):(this._contentWidget.setPreference(2),this.element.enableSashes(!1,!0,!0,!1),ne=ee),this.element.preferredSize=new M.Dimension(D,O.defaultSize.height),this.element.maxSize=new M.Dimension(F,ne),this.element.minSize=new M.Dimension(220,W),this._cappedHeight=A===R?{wanted:(E=(k=this._cappedHeight)===null||k===void 0?void 0:k.wanted)!==null&&E!==void 0?E:L.height,capped:A}:void 0}this._resize(B,A)}}_resize(L,I){const{width:k,height:E}=this.element.maxSize;L=Math.min(k,L),I=Math.min(E,I);const{statusBarHeight:T}=this.getLayoutInfo();this._list.layout(I-T,L),this._listElement.style.height=`${I-T}px`,this.element.layout(I,L),this._contentWidget.layout(),this._positionDetails()}_positionDetails(){this._isDetailsVisible()&&this._details.placeAtAnchor(this.element.domNode)}getLayoutInfo(){const L=this.editor.getOption(38),I=m.clamp(this.editor.getOption(103)||L.lineHeight,8,1e3),k=!this.editor.getOption(101).showStatusBar||this._state===2||this._state===1?0:I,E=this._details.widget.borderWidth,T=2*E;return{itemHeight:I,statusBarHeight:k,borderWidth:E,borderHeight:T,typicalHalfwidthCharacterWidth:L.typicalHalfwidthCharacterWidth,verticalPadding:22,horizontalPadding:14,defaultSize:new M.Dimension(430,k+12*I+T)}}_isDetailsVisible(){return this._storageService.getBoolean("expandSuggestionDocs",0,!1)}_setDetailsVisible(L){this._storageService.store("expandSuggestionDocs",L,0,0)}};f.LOADING_MESSAGE=b.localize(5,null),f.NO_SUGGESTIONS_MESSAGE=b.localize(6,null),f=Me([_e(1,a.IStorageService),_e(2,g.IContextKeyService),_e(3,o.IThemeService),_e(4,r.IInstantiationService)],f),e.SuggestWidget=f;class v{constructor(L,I){this._widget=L,this._editor=I,this.allowEditorOverflow=!0,this.suppressMouseDown=!1,this._preferenceLocked=!1,this._added=!1,this._hidden=!1}dispose(){this._added&&(this._added=!1,this._editor.removeContentWidget(this))}getId(){return"editor.widget.suggestWidget"}getDomNode(){return this._widget.element.domNode}show(){this._hidden=!1,this._added||(this._added=!0,this._editor.addContentWidget(this))}hide(){this._hidden||(this._hidden=!0,this.layout())}layout(){this._editor.layoutContentWidget(this)}getPosition(){return this._hidden||!this._position||!this._preference?null:{position:this._position,preference:[this._preference]}}beforeRender(){const{height:L,width:I}=this._widget.element.size,{borderWidth:k,horizontalPadding:E}=this._widget.getLayoutInfo();return new M.Dimension(I+2*k+E,L+2*k)}afterRender(L){this._widget._afterRender(L)}setPreference(L){this._preferenceLocked||(this._preference=L)}lockPreference(){this._preferenceLocked=!0}unlockPreference(){this._preferenceLocked=!1}setPosition(L){this._position=L}}e.SuggestContentWidget=v,o.registerThemingParticipant((y,L)=>{const I=y.getColor(e.editorSuggestWidgetHighlightForeground);I&&L.addRule(`.monaco-editor .suggest-widget .monaco-list .monaco-list-row .monaco-highlighted-label .highlight { color: ${I}; }`);const k=y.getColor(e.editorSuggestWidgetForeground);k&&L.addRule(`.monaco-editor .suggest-widget, .monaco-editor .suggest-details { color: ${k}; }`);const E=y.getColor(s.textLinkForeground);E&&L.addRule(`.monaco-editor .suggest-details a { color: ${E}; }`);const T=y.getColor(s.textCodeBlockBackground);T&&L.addRule(`.monaco-editor .suggest-details code { background-color: ${T}; }`)})}),define(Q[667],J([0,1,11,80,7,6]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.getIconsStyleSheet=void 0;function S(){const C=new w.Emitter,d=N.getIconRegistry();return d.onDidChange(()=>C.fire()),{onDidChange:C.event,getCSS(){const g={},p=o=>{let s=o.defaults;for(;b.ThemeIcon.isThemeIcon(s);){const u=d.getIcon(s.id);if(!u)return;s=u.defaults}const a=s.fontId;if(a){const u=d.getIconFont(a);if(u)return g[a]=u,`.codicon-${o.id}:before { content: '${s.fontCharacter}'; font-family: ${M.asCSSPropertyValue(a)}; }`}return`.codicon-${o.id}:before { content: '${s.fontCharacter}'; }`},c=[];for(let o of d.getIcons()){const s=p(o);s&&c.push(s)}for(let o in g){const a=g[o].definition.src.map(u=>`${M.asCSSUrl(u.location)} format('${u.format}')`).join(", ");c.push(`@font-face { src: ${a}; font-family: ${M.asCSSPropertyValue(o)}; }`)}return c.join(` +`)}}}e.getIconsStyleSheet=S}),define(Q[668],J([0,1,7,29,6,18,381,606,33,22,11,2,97,667]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.StandaloneThemeServiceImpl=void 0;const a="vs",u="vs-dark",r="hc-black",i=d.Registry.as(g.Extensions.ColorContribution),n=d.Registry.as(p.Extensions.ThemingContribution);class t{constructor(v,y){this.semanticHighlighting=!1,this.themeData=y;let L=y.base;v.length>0?(l(v)?this.id=v:this.id=L+" "+v,this.themeName=v):(this.id=L,this.themeName=L),this.colors=null,this.defaultColors=Object.create(null),this._tokenTheme=null}get base(){return this.themeData.base}notifyBaseUpdated(){this.themeData.inherit&&(this.colors=null,this._tokenTheme=null)}getColors(){if(!this.colors){const v=new Map;for(let y in this.themeData.colors)v.set(y,N.Color.fromHex(this.themeData.colors[y]));if(this.themeData.inherit){let y=h(this.themeData.base);for(let L in y.colors)v.has(L)||v.set(L,N.Color.fromHex(y.colors[L]))}this.colors=v}return this.colors}getColor(v,y){const L=this.getColors().get(v);if(L)return L;if(y!==!1)return this.getDefault(v)}getDefault(v){let y=this.defaultColors[v];return y||(y=i.resolveDefaultColor(v,this),this.defaultColors[v]=y,y)}defines(v){return Object.prototype.hasOwnProperty.call(this.getColors(),v)}get type(){switch(this.base){case a:return o.ColorScheme.LIGHT;case r:return o.ColorScheme.HIGH_CONTRAST;default:return o.ColorScheme.DARK}}get tokenTheme(){if(!this._tokenTheme){let v=[],y=[];if(this.themeData.inherit){let L=h(this.themeData.base);v=L.rules,L.encodedTokensColors&&(y=L.encodedTokensColors)}v=v.concat(this.themeData.rules),this.themeData.encodedTokensColors&&(y=this.themeData.encodedTokensColors),this._tokenTheme=S.TokenTheme.createFromRawTokenTheme(v,y)}return this._tokenTheme}getTokenStyleMetadata(v,y,L){const k=this.tokenTheme._match([v].concat(y).join(".")).metadata,E=w.TokenMetadata.getForeground(k),T=w.TokenMetadata.getFontStyle(k);return{foreground:E,italic:Boolean(T&1),bold:Boolean(T&2),underline:Boolean(T&4)}}}function l(f){return f===a||f===u||f===r}function h(f){switch(f){case a:return C.vs;case u:return C.vs_dark;case r:return C.hc_black}}function m(f){let v=h(f);return new t(f,v)}class _ extends c.Disposable{constructor(){super();this._onColorThemeChange=this._register(new M.Emitter),this.onDidColorThemeChange=this._onColorThemeChange.event,this._environment=Object.create(null),this._autoDetectHighContrast=!0,this._knownThemes=new Map,this._knownThemes.set(a,m(a)),this._knownThemes.set(u,m(u)),this._knownThemes.set(r,m(r));const v=s.getIconsStyleSheet();this._codiconCSS=v.getCSS(),this._themeCSS="",this._allCSS=`${this._codiconCSS} +${this._themeCSS}`,this._globalStyleElement=null,this._styleElements=[],this._colorMapOverride=null,this.setTheme(a),v.onDidChange(()=>{this._codiconCSS=v.getCSS(),this._updateCSS()}),window.matchMedia("(forced-colors: active)").addEventListener("change",()=>{this._updateActualTheme()})}registerEditorContainer(v){return b.isInShadowDOM(v)?this._registerShadowDomContainer(v):this._registerRegularEditorContainer()}_registerRegularEditorContainer(){return this._globalStyleElement||(this._globalStyleElement=b.createStyleSheet(),this._globalStyleElement.className="monaco-colors",this._globalStyleElement.textContent=this._allCSS,this._styleElements.push(this._globalStyleElement)),c.Disposable.None}_registerShadowDomContainer(v){const y=b.createStyleSheet(v);return y.className="monaco-colors",y.textContent=this._allCSS,this._styleElements.push(y),{dispose:()=>{for(let L=0;L{L.base===v&&L.notifyBaseUpdated()}),this._theme.themeName===v&&this.setTheme(v)}getColorTheme(){return this._theme}setColorMapOverride(v){this._colorMapOverride=v,this._updateThemeOrColorMap()}setTheme(v){let y;this._knownThemes.has(v)?y=this._knownThemes.get(v):y=this._knownThemes.get(a),this._desiredTheme=y,this._updateActualTheme()}_updateActualTheme(){const v=this._autoDetectHighContrast&&window.matchMedia("(forced-colors: active)").matches?this._knownThemes.get(r):this._desiredTheme;this._theme!==v&&(this._theme=v,this._updateThemeOrColorMap())}setAutoDetectHighContrast(v){this._autoDetectHighContrast=v,this._updateActualTheme()}_updateThemeOrColorMap(){let v=[],y={},L={addRule:k=>{y[k]||(v.push(k),y[k]=!0)}};n.getThemingParticipants().forEach(k=>k(this._theme,L,this._environment));const I=this._colorMapOverride||this._theme.tokenTheme.getColorMap();L.addRule(S.generateTokensCSSForColorMap(I)),this._themeCSS=v.join(` +`),this._updateCSS(),w.TokenizationRegistry.setColorMap(I),this._onColorThemeChange.fire(this._theme)}_updateCSS(){this._allCSS=`${this._codiconCSS} +${this._themeCSS}`,this._styleElements.forEach(v=>v.textContent=this._allCSS)}getFileIconTheme(){return{hasFileIcons:!1,hasFolderIcons:!1,hidesExplorerArrows:!1}}}e.StandaloneThemeServiceImpl=_}),define(Q[148],J([0,1,9]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.UndoRedoSource=e.UndoRedoGroup=e.ResourceEditStackSnapshot=e.IUndoRedoService=void 0,e.IUndoRedoService=b.createDecorator("undoRedoService");class N{constructor(C,d){this.resource=C,this.elements=d}}e.ResourceEditStackSnapshot=N;class M{constructor(){this.id=M._ID++,this.order=1}nextOrder(){return this.id===0?0:this.order++}}e.UndoRedoGroup=M,M._ID=0,M.None=new M;class w{constructor(){this.id=w._ID++,this.order=1}nextOrder(){return this.id===0?0:this.order++}}e.UndoRedoSource=w,w._ID=0,w.None=new w}),define(Q[269],J([0,1,6,2,17,12,38,31,18,141,137,46,15,23,11,77,148,89,236,43,253,247]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ModelSemanticColoring=e.isSemanticColoringEnabled=e.SEMANTIC_HIGHLIGHTING_SETTING_ID=e.ModelServiceImpl=void 0;function m(B){return B.toString()}function _(B){const F=new i.StringSHA1,D=B.createSnapshot();let R;for(;R=D.read();)F.update(R);return F.digest()}class f{constructor(F,D,R){this._modelEventListeners=new N.DisposableStore,this.model=F,this._languageSelection=null,this._languageSelectionListener=null,this._modelEventListeners.add(F.onWillDispose(()=>D(F))),this._modelEventListeners.add(F.onDidChangeLanguage(W=>R(F,W)))}_disposeLanguageSelection(){this._languageSelectionListener&&(this._languageSelectionListener.dispose(),this._languageSelectionListener=null),this._languageSelection&&(this._languageSelection.dispose(),this._languageSelection=null)}dispose(){this._modelEventListeners.dispose(),this._disposeLanguageSelection()}setLanguage(F){this._disposeLanguageSelection(),this._languageSelection=F,this._languageSelectionListener=this._languageSelection.onDidChange(()=>this.model.setMode(F.languageIdentifier)),this.model.setMode(F.languageIdentifier)}}const v=M.isLinux||M.isMacintosh?1:2;class y{constructor(F,D,R,W,x,K,Y,ee){this.uri=F,this.initialUndoRedoSnapshot=D,this.time=R,this.sharesUndoRedoStack=W,this.heapSize=x,this.sha1=K,this.versionId=Y,this.alternativeVersionId=ee}}function L(B){return B.scheme===t.Schemas.file||B.scheme===t.Schemas.vscodeRemote||B.scheme===t.Schemas.userData||B.scheme==="fake-fs"}let I=class st extends N.Disposable{constructor(F,D,R,W,x){super();this._configurationService=F,this._resourcePropertiesService=D,this._themeService=R,this._logService=W,this._undoRedoService=x,this._onModelAdded=this._register(new b.Emitter),this.onModelAdded=this._onModelAdded.event,this._onModelRemoved=this._register(new b.Emitter),this.onModelRemoved=this._onModelRemoved.event,this._onModelModeChanged=this._register(new b.Emitter),this.onModelModeChanged=this._onModelModeChanged.event,this._modelCreationOptionsByLanguageAndResource=Object.create(null),this._models={},this._disposedModels=new Map,this._disposedModelsHeapSize=0,this._semanticStyling=this._register(new T(this._themeService,this._logService)),this._register(this._configurationService.onDidChangeConfiguration(()=>this._updateModelOptions())),this._updateModelOptions(),this._register(new E(this,this._themeService,this._configurationService,this._semanticStyling))}static _readModelOptions(F,D){let R=S.EDITOR_MODEL_DEFAULTS.tabSize;if(F.editor&&typeof F.editor.tabSize!="undefined"){const le=parseInt(F.editor.tabSize,10);isNaN(le)||(R=le),R<1&&(R=1)}let W=R;if(F.editor&&typeof F.editor.indentSize!="undefined"&&F.editor.indentSize!=="tabSize"){const le=parseInt(F.editor.indentSize,10);isNaN(le)||(W=le),W<1&&(W=1)}let x=S.EDITOR_MODEL_DEFAULTS.insertSpaces;F.editor&&typeof F.editor.insertSpaces!="undefined"&&(x=F.editor.insertSpaces==="false"?!1:Boolean(F.editor.insertSpaces));let K=v;const Y=F.eol;Y===`\r +`?K=2:Y===` +`&&(K=1);let ee=S.EDITOR_MODEL_DEFAULTS.trimAutoWhitespace;F.editor&&typeof F.editor.trimAutoWhitespace!="undefined"&&(ee=F.editor.trimAutoWhitespace==="false"?!1:Boolean(F.editor.trimAutoWhitespace));let se=S.EDITOR_MODEL_DEFAULTS.detectIndentation;F.editor&&typeof F.editor.detectIndentation!="undefined"&&(se=F.editor.detectIndentation==="false"?!1:Boolean(F.editor.detectIndentation));let ne=S.EDITOR_MODEL_DEFAULTS.largeFileOptimizations;return F.editor&&typeof F.editor.largeFileOptimizations!="undefined"&&(ne=F.editor.largeFileOptimizations==="false"?!1:Boolean(F.editor.largeFileOptimizations)),{isForSimpleWidget:D,tabSize:R,indentSize:W,insertSpaces:x,detectIndentation:se,defaultEOL:K,trimAutoWhitespace:ee,largeFileOptimizations:ne}}_getEOL(F,D){if(F)return this._resourcePropertiesService.getEOL(F,D);const R=this._configurationService.getValue("files.eol",{overrideIdentifier:D});return R&&R!=="auto"?R:M.OS===3||M.OS===2?` +`:`\r +`}_shouldRestoreUndoStack(){const F=this._configurationService.getValue("files.restoreUndoStack");return typeof F=="boolean"?F:!0}getCreationOptions(F,D,R){let W=this._modelCreationOptionsByLanguageAndResource[F+D];if(!W){const x=this._configurationService.getValue("editor",{overrideIdentifier:F,resource:D}),K=this._getEOL(D,F);W=st._readModelOptions({editor:x,eol:K},R),this._modelCreationOptionsByLanguageAndResource[F+D]=W}return W}_updateModelOptions(){const F=this._modelCreationOptionsByLanguageAndResource;this._modelCreationOptionsByLanguageAndResource=Object.create(null);const D=Object.keys(this._models);for(let R=0,W=D.length;RF){const D=[];for(this._disposedModels.forEach(R=>{R.sharesUndoRedoStack||D.push(R)}),D.sort((R,W)=>R.time-W.time);D.length>0&&this._disposedModelsHeapSize>F;){const R=D.shift();this._removeDisposedModel(R.uri),R.initialUndoRedoSnapshot!==null&&this._undoRedoService.restoreSnapshot(R.initialUndoRedoSnapshot)}}}_createModelData(F,D,R,W){const x=this.getCreationOptions(D.language,R,W),K=new C.TextModel(F,x,D,R,this._undoRedoService);if(R&&this._disposedModels.has(m(R))){const se=this._removeDisposedModel(R),ne=this._undoRedoService.getElements(R),le=_(K)===se.sha1;if(le||se.sharesUndoRedoStack){for(const X of ne.past)n.isEditStackElement(X)&&X.matchesResource(R)&&X.setModel(K);for(const X of ne.future)n.isEditStackElement(X)&&X.matchesResource(R)&&X.setModel(K);this._undoRedoService.setElementsValidFlag(R,!0,X=>n.isEditStackElement(X)&&X.matchesResource(R)),le&&(K._overwriteVersionId(se.versionId),K._overwriteAlternativeVersionId(se.alternativeVersionId),K._overwriteInitialUndoRedoSnapshot(se.initialUndoRedoSnapshot))}else se.initialUndoRedoSnapshot!==null&&this._undoRedoService.restoreSnapshot(se.initialUndoRedoSnapshot)}const Y=m(K.uri);if(this._models[Y])throw new Error("ModelService: Cannot add model because it already exists!");const ee=new f(K,se=>this._onWillDispose(se),(se,ne)=>this._onDidChangeLanguage(se,ne));return this._models[Y]=ee,ee}createModel(F,D,R,W=!1){let x;return D?(x=this._createModelData(F,D.languageIdentifier,R,W),this.setMode(x.model,D)):x=this._createModelData(F,g.PLAINTEXT_LANGUAGE_IDENTIFIER,R,W),this._onModelAdded.fire(x.model),x.model}setMode(F,D){if(!!D){const R=this._models[m(F.uri)];!R||R.setLanguage(D)}}getModels(){const F=[],D=Object.keys(this._models);for(let R=0,W=D.length;R0||ee.future.length>0){for(const se of ee.past)n.isEditStackElement(se)&&se.matchesResource(F.uri)&&(x=!0,K+=se.heapSize(F.uri),se.setModel(F.uri));for(const se of ee.future)n.isEditStackElement(se)&&se.matchesResource(F.uri)&&(x=!0,K+=se.heapSize(F.uri),se.setModel(F.uri))}}const Y=st.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK;if(x)if(!W&&K>Y){const ee=R.model.getInitialUndoRedoSnapshot();ee!==null&&this._undoRedoService.restoreSnapshot(ee)}else this._ensureDisposedModelsHeapSize(Y-K),this._undoRedoService.setElementsValidFlag(F.uri,!1,ee=>n.isEditStackElement(ee)&&ee.matchesResource(F.uri)),this._insertDisposedModel(new y(F.uri,R.model.getInitialUndoRedoSnapshot(),Date.now(),W,K,_(F),F.getVersionId(),F.getAlternativeVersionId()));else if(!W){const ee=R.model.getInitialUndoRedoSnapshot();ee!==null&&this._undoRedoService.restoreSnapshot(ee)}delete this._models[D],R.dispose(),delete this._modelCreationOptionsByLanguageAndResource[F.getLanguageIdentifier().language+F.uri],this._onModelRemoved.fire(F)}_onDidChangeLanguage(F,D){const R=D.oldLanguage,W=F.getLanguageIdentifier().language,x=this.getCreationOptions(R,F.uri,F.isForSimpleWidget),K=this.getCreationOptions(W,F.uri,F.isForSimpleWidget);st._setModelOptionsForModel(F,K,x),this._onModelModeChanged.fire({model:F,oldModeId:R})}};I.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK=20*1024*1024,I=Me([_e(0,c.IConfigurationService),_e(1,p.ITextResourcePropertiesService),_e(2,a.IThemeService),_e(3,u.ILogService),_e(4,r.IUndoRedoService)],I),e.ModelServiceImpl=I,e.SEMANTIC_HIGHLIGHTING_SETTING_ID="editor.semanticHighlighting";function k(B,F,D){var R;const W=(R=D.getValue(e.SEMANTIC_HIGHLIGHTING_SETTING_ID,{overrideIdentifier:B.getLanguageIdentifier().language,resource:B.uri}))===null||R===void 0?void 0:R.enabled;return typeof W=="boolean"?W:F.getColorTheme().semanticHighlighting}e.isSemanticColoringEnabled=k;class E extends N.Disposable{constructor(F,D,R,W){super();this._watchers=Object.create(null),this._semanticStyling=W;const x=ee=>{this._watchers[ee.uri.toString()]=new A(ee,D,this._semanticStyling)},K=(ee,se)=>{se.dispose(),delete this._watchers[ee.uri.toString()]},Y=()=>{for(let ee of F.getModels()){const se=this._watchers[ee.uri.toString()];k(ee,D,R)?se||x(ee):se&&K(ee,se)}};this._register(F.onModelAdded(ee=>{k(ee,D,R)&&x(ee)})),this._register(F.onModelRemoved(ee=>{const se=this._watchers[ee.uri.toString()];se&&K(ee,se)})),this._register(R.onDidChangeConfiguration(ee=>{ee.affectsConfiguration(e.SEMANTIC_HIGHLIGHTING_SETTING_ID)&&Y()})),this._register(D.onDidColorThemeChange(Y))}}class T extends N.Disposable{constructor(F,D){super();this._themeService=F,this._logService=D,this._caches=new WeakMap,this._register(this._themeService.onDidColorThemeChange(()=>{this._caches=new WeakMap}))}get(F){return this._caches.has(F)||this._caches.set(F,new l.SemanticTokensProviderStyling(F.getLegend(),this._themeService,this._logService)),this._caches.get(F)}}class O{constructor(F,D,R){this._provider=F,this.resultId=D,this.data=R}dispose(){this._provider.releaseDocumentSemanticTokens(this.resultId)}}class A extends N.Disposable{constructor(F,D,R){super();this._isDisposed=!1,this._model=F,this._semanticStyling=R,this._fetchDocumentSemanticTokens=this._register(new o.RunOnceScheduler(()=>this._fetchDocumentSemanticTokensNow(),A.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY)),this._currentDocumentResponse=null,this._currentDocumentRequestCancellationTokenSource=null,this._documentProvidersChangeListeners=[],this._register(this._model.onDidChangeContent(()=>{this._fetchDocumentSemanticTokens.isScheduled()||this._fetchDocumentSemanticTokens.schedule()}));const W=()=>{N.dispose(this._documentProvidersChangeListeners),this._documentProvidersChangeListeners=[];for(const x of d.DocumentSemanticTokensProviderRegistry.all(F))typeof x.onDidChange=="function"&&this._documentProvidersChangeListeners.push(x.onDidChange(()=>this._fetchDocumentSemanticTokens.schedule(0)))};W(),this._register(d.DocumentSemanticTokensProviderRegistry.onDidChange(()=>{W(),this._fetchDocumentSemanticTokens.schedule()})),this._register(D.onDidColorThemeChange(x=>{this._setDocumentSemanticTokens(null,null,null,[]),this._fetchDocumentSemanticTokens.schedule()})),this._fetchDocumentSemanticTokens.schedule(0)}dispose(){this._currentDocumentResponse&&(this._currentDocumentResponse.dispose(),this._currentDocumentResponse=null),this._currentDocumentRequestCancellationTokenSource&&(this._currentDocumentRequestCancellationTokenSource.cancel(),this._currentDocumentRequestCancellationTokenSource=null),this._setDocumentSemanticTokens(null,null,null,[]),this._isDisposed=!0,super.dispose()}_fetchDocumentSemanticTokensNow(){if(!this._currentDocumentRequestCancellationTokenSource){const F=new s.CancellationTokenSource,D=this._currentDocumentResponse&&this._currentDocumentResponse.resultId||null,R=h.getDocumentSemanticTokens(this._model,D,F.token);if(!R){this._currentDocumentResponse&&this._model.setSemanticTokens(null,!1);return}const{provider:W,request:x}=R;this._currentDocumentRequestCancellationTokenSource=F;const K=[],Y=this._model.onDidChangeContent(se=>{K.push(se)}),ee=this._semanticStyling.get(W);x.then(se=>{this._currentDocumentRequestCancellationTokenSource=null,Y.dispose(),this._setDocumentSemanticTokens(W,se||null,ee,K)},se=>{se&&(w.isPromiseCanceledError(se)||typeof se.message=="string"&&se.message.indexOf("busy")!==-1)||w.onUnexpectedError(se),this._currentDocumentRequestCancellationTokenSource=null,Y.dispose(),K.length>0&&(this._fetchDocumentSemanticTokens.isScheduled()||this._fetchDocumentSemanticTokens.schedule())})}}static _copy(F,D,R,W,x){for(let K=0;K{W.length>0&&!this._fetchDocumentSemanticTokens.isScheduled()&&this._fetchDocumentSemanticTokens.schedule()};if(this._currentDocumentResponse&&(this._currentDocumentResponse.dispose(),this._currentDocumentResponse=null),this._isDisposed){F&&D&&F.releaseDocumentSemanticTokens(D.resultId);return}if(!F||!R){this._model.setSemanticTokens(null,!1);return}if(!D){this._model.setSemanticTokens(null,!0),K();return}if(h.isSemanticTokensEdits(D)){if(!x){this._model.setSemanticTokens(null,!0);return}if(D.edits.length===0)D={resultId:D.resultId,data:x.data};else{let Y=0;for(const X of D.edits)Y+=(X.data?X.data.length:0)-X.deleteCount;const ee=x.data,se=new Uint32Array(ee.length+Y);let ne=ee.length,le=se.length;for(let X=D.edits.length-1;X>=0;X--){const z=D.edits[X],P=ne-(z.start+z.deleteCount);P>0&&(A._copy(ee,ne-P,se,le-P,P),le-=P),z.data&&(A._copy(z.data,0,se,le-z.data.length,z.data.length),le-=z.data.length),ne=z.start}ne>0&&A._copy(ee,0,se,0,ne),D={resultId:D.resultId,data:se}}}if(h.isSemanticTokens(D)){this._currentDocumentResponse=new O(F,D.resultId,D.data);const Y=l.toMultilineTokens2(D,R,this._model.getLanguageIdentifier());if(W.length>0)for(const ee of W)for(const se of Y)for(const ne of ee.changes)se.applyEdit(ne.range,ne.text);this._model.setSemanticTokens(Y,!0)}else this._model.setSemanticTokens(null,!0);K()}}e.ModelSemanticColoring=A,A.FETCH_DOCUMENT_SEMANTIC_TOKENS_DELAY=300}),define(Q[669],J([0,1,7,29,6,2,43,44,144,3,31,67,601,485,9,115,147,22,11,118,132,210,148,37,345]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ReferenceWidget=e.LayoutData=void 0;class f{constructor(k,E){this._editor=k,this._model=E,this._decorations=new Map,this._decorationIgnoreSet=new Set,this._callOnDispose=new w.DisposableStore,this._callOnModelChange=new w.DisposableStore,this._callOnDispose.add(this._editor.onDidChangeModel(()=>this._onModelChanged())),this._onModelChanged()}dispose(){this._callOnModelChange.dispose(),this._callOnDispose.dispose(),this.removeDecorations()}_onModelChanged(){this._callOnModelChange.clear();const k=this._editor.getModel();if(!!k){for(let E of this._model.references)if(E.uri.toString()===k.uri.toString()){this._addDecorations(E.parent);return}}}_addDecorations(k){if(!!this._editor.hasModel()){this._callOnModelChange.add(this._editor.getModel().onDidChangeDecorations(()=>this._onDecorationChanged()));const E=[],T=[];for(let A=0,B=k.children.length;A{A.equals(9)&&(this._keybindingService.dispatchEvent(A,A.target),A.stopPropagation())},!0)),this._tree=this._instantiationService.createInstance(y,"ReferencesWidget",this._treeContainer,new o.Delegate,[this._instantiationService.createInstance(o.FileReferencesRenderer),this._instantiationService.createInstance(o.OneReferenceRenderer)],this._instantiationService.createInstance(o.DataSource),T),this._splitView.addView({onDidChange:M.Event.None,element:this._previewContainer,minimumSize:200,maximumSize:Number.MAX_VALUE,layout:A=>{this._preview.layout({height:this._dim.height,width:A})}},h.Sizing.Distribute),this._splitView.addView({onDidChange:M.Event.None,element:this._treeContainer,minimumSize:100,maximumSize:Number.MAX_VALUE,layout:A=>{this._treeContainer.style.height=`${this._dim.height}px`,this._treeContainer.style.width=`${A}px`,this._tree.layout(this._dim.height,A)}},h.Sizing.Distribute),this._disposables.add(this._splitView.onDidSashChange(()=>{this._dim.width&&(this.layoutData.ratio=this._splitView.getViewSize(0)/this._dim.width)},void 0));let O=(A,B)=>{A instanceof l.OneReference&&(B==="show"&&this._revealReference(A,!1),this._onDidSelectReference.fire({element:A,kind:B,source:"tree"}))};this._tree.onDidOpen(A=>{A.sideBySide?O(A.element,"side"):A.editorOptions.pinned?O(A.element,"goto"):O(A.element,"show")}),b.hide(this._treeContainer)}_onWidth(k){this._dim&&this._doLayoutBody(this._dim.height,k)}_doLayoutBody(k,E){super._doLayoutBody(k,E),this._dim=new b.Dimension(E,k),this.layoutData.heightInLines=this._viewZone?this._viewZone.heightInLines:this.layoutData.heightInLines,this._splitView.layout(E),this._splitView.resizeView(0,E*this.layoutData.ratio)}setSelection(k){return this._revealReference(k,!0).then(()=>{!this._model||(this._tree.setSelection([k]),this._tree.setFocus([k]))})}setModel(k){return this._disposeOnNewModel.clear(),this._model=k,this._model?this._onNewModel():Promise.resolve()}_onNewModel(){return this._model?this._model.isEmpty?(this.setTitle(""),this._messageContainer.innerText=s.localize(1,null),b.show(this._messageContainer),Promise.resolve(void 0)):(b.hide(this._messageContainer),this._decorationsManager=new f(this._preview,this._model),this._disposeOnNewModel.add(this._decorationsManager),this._disposeOnNewModel.add(this._model.onDidChangeReferenceRange(k=>this._tree.rerender(k))),this._disposeOnNewModel.add(this._preview.onMouseDown(k=>{const{event:E,target:T}=k;if(E.detail===2){const O=this._getFocusedReference();!O||this._onDidSelectReference.fire({element:{uri:O.uri,range:T.range},kind:E.ctrlKey||E.metaKey||E.altKey?"side":"open",source:"editor"})}})),this.container.classList.add("results-loaded"),b.show(this._treeContainer),b.show(this._previewContainer),this._splitView.layout(this._dim.width),this.focusOnReferenceTree(),this._tree.setInput(this._model.groups.length===1?this._model.groups[0]:this._model)):Promise.resolve(void 0)}_getFocusedReference(){const[k]=this._tree.getFocus();if(k instanceof l.OneReference)return k;if(k instanceof l.FileReferences&&k.children.length>0)return k.children[0]}revealReference(k){return Ie(this,void 0,void 0,function*(){yield this._revealReference(k,!1),this._onDidSelectReference.fire({element:k,kind:"goto",source:"tree"})})}_revealReference(k,E){return Ie(this,void 0,void 0,function*(){if(this._revealedReference!==k){this._revealedReference=k,k.uri.scheme!==S.Schemas.inMemory?this.setTitle(C.basenameOrAuthority(k.uri),this._uriLabel.getUriLabel(C.dirname(k.uri))):this.setTitle(s.localize(2,null));const T=this._textModelResolverService.createModelReference(k.uri);this._tree.getInput()===k.parent?this._tree.reveal(k):(E&&this._tree.reveal(k.parent),yield this._tree.expand(k.parent),this._tree.reveal(k));const O=yield T;if(!this._model){O.dispose();return}w.dispose(this._previewModelReference);const A=O.object;if(A){const B=this._preview.getModel()===A.textEditorModel?0:1,F=g.Range.lift(k.range).collapseToStart();this._previewModelReference=O,this._preview.setModel(A.textEditorModel),this._preview.setSelection(F),this._preview.revealRangeInCenter(F,B)}else this._preview.setModel(this._previewNotAvailableMessage),O.dispose()}})}};L=Me([_e(3,n.IThemeService),_e(4,c.ITextModelService),_e(5,a.IInstantiationService),_e(6,t.IPeekViewService),_e(7,u.ILabelService),_e(8,m.IUndoRedoService),_e(9,_.IKeybindingService)],L),e.ReferenceWidget=L,n.registerThemingParticipant((I,k)=>{const E=I.getColor(t.peekViewResultsMatchHighlight);E&&k.addRule(`.monaco-editor .reference-zone-widget .ref-tree .referenceMatch .highlight { background-color: ${E}; }`);const T=I.getColor(t.peekViewEditorMatchHighlight);T&&k.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { background-color: ${T}; }`);const O=I.getColor(t.peekViewEditorMatchHighlightBorder);O&&k.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { border: 2px solid ${O}; box-sizing: border-box; }`);const A=I.getColor(i.activeContrastBorder);A&&k.addRule(`.monaco-editor .reference-zone-widget .ref-tree .referenceMatch .highlight { border: 1px dotted ${A}; box-sizing: border-box; }`);const B=I.getColor(t.peekViewResultsBackground);B&&k.addRule(`.monaco-editor .reference-zone-widget .ref-tree { background-color: ${B}; }`);const F=I.getColor(t.peekViewResultsMatchForeground);F&&k.addRule(`.monaco-editor .reference-zone-widget .ref-tree { color: ${F}; }`);const D=I.getColor(t.peekViewResultsFileForeground);D&&k.addRule(`.monaco-editor .reference-zone-widget .ref-tree .reference-file { color: ${D}; }`);const R=I.getColor(t.peekViewResultsSelectionBackground);R&&k.addRule(`.monaco-editor .reference-zone-widget .ref-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { background-color: ${R}; }`);const W=I.getColor(t.peekViewResultsSelectionForeground);W&&k.addRule(`.monaco-editor .reference-zone-widget .ref-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { color: ${W} !important; }`);const x=I.getColor(t.peekViewEditorBackground);x&&k.addRule(`.monaco-editor .reference-zone-widget .preview .monaco-editor .monaco-editor-background,.monaco-editor .reference-zone-widget .preview .monaco-editor .inputarea.ime-input { background-color: ${x};}`);const K=I.getColor(t.peekViewEditorGutterBackground);K&&k.addRule(`.monaco-editor .reference-zone-widget .preview .monaco-editor .margin { background-color: ${K};}`)})}),define(Q[270],J([0,1,483,12,2,28,9,16,46,79,132,669,3,14,32,15,118,147,86,39,26]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ReferencesController=e.ctxReferenceSearchVisible=void 0,e.ctxReferenceSearchVisible=new C.RawContextKey("referenceSearchVisible",!1);let h=class yt{constructor(f,v,y,L,I,k,E,T){this._defaultTreeKeyboardSupport=f,this._editor=v,this._editorService=L,this._notificationService=I,this._instantiationService=k,this._storageService=E,this._configurationService=T,this._disposables=new M.DisposableStore,this._requestIdPool=0,this._ignoreModelChangeEvent=!1,this._referenceSearchVisible=e.ctxReferenceSearchVisible.bindTo(y)}static get(f){return f.getContribution(yt.ID)}dispose(){var f,v;this._referenceSearchVisible.reset(),this._disposables.dispose(),(f=this._widget)===null||f===void 0||f.dispose(),(v=this._model)===null||v===void 0||v.dispose(),this._widget=void 0,this._model=void 0}toggleWidget(f,v,y){let L;if(this._widget&&(L=this._widget.position),this.closeWidget(),!(!!L&&f.containsPosition(L))){this._peekMode=y,this._referenceSearchVisible.set(!0),this._disposables.add(this._editor.onDidChangeModelLanguage(()=>{this.closeWidget()})),this._disposables.add(this._editor.onDidChangeModel(()=>{this._ignoreModelChangeEvent||this.closeWidget()}));const I="peekViewLayout",k=c.LayoutData.fromJSON(this._storageService.get(I,0,"{}"));this._widget=this._instantiationService.createInstance(c.ReferenceWidget,this._editor,this._defaultTreeKeyboardSupport,k),this._widget.setTitle(b.localize(0,null)),this._widget.show(f),this._disposables.add(this._widget.onDidClose(()=>{v.cancel(),this._widget&&(this._storageService.store(I,JSON.stringify(this._widget.layoutData),0,1),this._widget=void 0),this.closeWidget()})),this._disposables.add(this._widget.onDidSelectReference(T=>{let{element:O,kind:A}=T;if(!!O)switch(A){case"open":(T.source!=="editor"||!this._configurationService.getValue("editor.stablePeek"))&&this.openReference(O,!1,!1);break;case"side":this.openReference(O,!0,!1);break;case"goto":y?this._gotoReference(O):this.openReference(O,!1,!0);break}}));const E=++this._requestIdPool;v.then(T=>{var O;if(E!==this._requestIdPool||!this._widget){T.dispose();return}return(O=this._model)===null||O===void 0||O.dispose(),this._model=T,this._widget.setModel(this._model).then(()=>{if(this._widget&&this._model&&this._editor.hasModel()){this._model.isEmpty?this._widget.setMetaTitle(""):this._widget.setMetaTitle(b.localize(1,null,this._model.title,this._model.references.length));let A=this._editor.getModel().uri,B=new s.Position(f.startLineNumber,f.startColumn),F=this._model.nearestReference(A,B);if(F)return this._widget.setSelection(F).then(()=>{this._widget&&this._editor.getOption(71)==="editor"&&this._widget.focusOnPreviewEditor()})}})},T=>{this._notificationService.error(T)})}}changeFocusBetweenPreviewAndReferences(){!this._widget||(this._widget.isPreviewEditorFocused()?this._widget.focusOnReferenceTree():this._widget.focusOnPreviewEditor())}goToNextOrPreviousReference(f){return Ie(this,void 0,void 0,function*(){if(!(!this._editor.hasModel()||!this._model||!this._widget)){const v=this._widget.position;if(!!v){const y=this._model.nearestReference(this._editor.getModel().uri,v);if(!!y){const L=this._model.nextOrPreviousReference(y,f),I=this._editor.hasTextFocus(),k=this._widget.isPreviewEditorFocused();yield this._widget.setSelection(L),yield this._gotoReference(L),I?this._editor.focus():this._widget&&k&&this._widget.focusOnPreviewEditor()}}}})}revealReference(f){return Ie(this,void 0,void 0,function*(){!this._editor.hasModel()||!this._model||!this._widget||(yield this._widget.revealReference(f))})}closeWidget(f=!0){var v,y;(v=this._widget)===null||v===void 0||v.dispose(),(y=this._model)===null||y===void 0||y.dispose(),this._referenceSearchVisible.reset(),this._disposables.clear(),this._widget=void 0,this._model=void 0,f&&this._editor.focus(),this._requestIdPool+=1}_gotoReference(f){this._widget&&this._widget.hide(),this._ignoreModelChangeEvent=!0;const v=o.Range.lift(f.range).collapseToStart();return this._editorService.openCodeEditor({resource:f.uri,options:{selection:v}},this._editor).then(y=>{var L;if(this._ignoreModelChangeEvent=!1,!y||!this._widget){this.closeWidget();return}if(this._editor===y)this._widget.show(v),this._widget.focusOnReferenceTree();else{const I=yt.get(y),k=this._model.clone();this.closeWidget(),y.focus(),I.toggleWidget(v,u.createCancelablePromise(E=>Promise.resolve(k)),(L=this._peekMode)!==null&&L!==void 0?L:!1)}},y=>{this._ignoreModelChangeEvent=!1,N.onUnexpectedError(y)})}openReference(f,v,y){v||this.closeWidget();const{uri:L,range:I}=f;this._editorService.openCodeEditor({resource:L,options:{selection:I,pinned:y}},this._editor,v)}};h.ID="editor.contrib.referencesController",h=Me([_e(2,C.IContextKeyService),_e(3,w.ICodeEditorService),_e(4,a.INotificationService),_e(5,S.IInstantiationService),_e(6,g.IStorageService),_e(7,d.IConfigurationService)],h),e.ReferencesController=h;function m(_,f){const v=r.getOuterEditor(_);if(!!v){let y=h.get(v);y&&f(y)}}n.KeybindingsRegistry.registerCommandAndKeybindingRule({id:"togglePeekWidgetFocus",weight:100,primary:t.KeyChord(2048|41,60),when:C.ContextKeyExpr.or(e.ctxReferenceSearchVisible,r.PeekContext.inPeekEditor),handler(_){m(_,f=>{f.changeFocusBetweenPreviewAndReferences()})}}),n.KeybindingsRegistry.registerCommandAndKeybindingRule({id:"goToNextReference",weight:100-10,primary:62,secondary:[70],when:C.ContextKeyExpr.or(e.ctxReferenceSearchVisible,r.PeekContext.inPeekEditor),handler(_){m(_,f=>{f.goToNextOrPreviousReference(!0)})}}),n.KeybindingsRegistry.registerCommandAndKeybindingRule({id:"goToPreviousReference",weight:100-10,primary:1024|62,secondary:[1024|70],when:C.ContextKeyExpr.or(e.ctxReferenceSearchVisible,r.PeekContext.inPeekEditor),handler(_){m(_,f=>{f.goToNextOrPreviousReference(!1)})}}),l.CommandsRegistry.registerCommandAlias("goToNextReferenceFromEmbeddedEditor","goToNextReference"),l.CommandsRegistry.registerCommandAlias("goToPreviousReferenceFromEmbeddedEditor","goToPreviousReference"),l.CommandsRegistry.registerCommandAlias("closeReferenceSearchEditor","closeReferenceSearch"),l.CommandsRegistry.registerCommand("closeReferenceSearch",_=>m(_,f=>f.closeWidget())),n.KeybindingsRegistry.registerKeybindingRule({id:"closeReferenceSearch",weight:100-101,primary:9,secondary:[1024|9],when:C.ContextKeyExpr.and(r.PeekContext.inPeekEditor,C.ContextKeyExpr.not("config.editor.stablePeek"))}),n.KeybindingsRegistry.registerKeybindingRule({id:"closeReferenceSearch",weight:200+50,primary:9,secondary:[1024|9],when:C.ContextKeyExpr.and(e.ctxReferenceSearchVisible,C.ContextKeyExpr.not("config.editor.stablePeek"))}),n.KeybindingsRegistry.registerCommandAndKeybindingRule({id:"revealReference",weight:200,primary:3,mac:{primary:3,secondary:[2048|18]},when:C.ContextKeyExpr.and(e.ctxReferenceSearchVisible,i.WorkbenchListFocusContextKey),handler(_){var f;const y=(f=_.get(i.IListService).lastFocusedList)===null||f===void 0?void 0:f.getFocus();Array.isArray(y)&&y[0]instanceof p.OneReference&&m(_,L=>L.revealReference(y[0]))}}),n.KeybindingsRegistry.registerCommandAndKeybindingRule({id:"openReferenceToSide",weight:100,primary:2048|3,mac:{primary:256|3},when:C.ContextKeyExpr.and(e.ctxReferenceSearchVisible,i.WorkbenchListFocusContextKey),handler(_){var f;const y=(f=_.get(i.IListService).lastFocusedList)===null||f===void 0?void 0:f.getFocus();Array.isArray(y)&&y[0]instanceof p.OneReference&&m(_,L=>L.openReference(y[0],!0,!0))}}),l.CommandsRegistry.registerCommand("openReference",_=>{var f;const y=(f=_.get(i.IListService).lastFocusedList)===null||f===void 0?void 0:f.getFocus();Array.isArray(y)&&y[0]instanceof p.OneReference&&m(_,L=>L.openReference(y[0],!1,!0))})}),define(Q[271],J([0,1,47,15,39,17,108,13,28,14,3,25,18,146,118,270,132,481,34,16,32,59,262,26,70,625,35,24,9,20,144]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y,L,I,k,E){"use strict";var T,O,A,B,F,D,R,W;Object.defineProperty(e,"__esModule",{value:!0}),e.DefinitionAction=void 0,n.MenuRegistry.appendMenuItem(n.MenuId.EditorContext,{submenu:n.MenuId.EditorContextPeek,title:i.localize(0,null),group:"navigation",order:100});class x extends C.EditorAction{constructor(P,V){super(V);this._configuration=P}run(P,V){if(!V.hasModel())return Promise.resolve(void 0);const U=P.get(l.INotificationService),H=P.get(d.ICodeEditorService),$=P.get(h.IEditorProgressService),ie=P.get(v.ISymbolNavigationService),oe=V.getModel(),ae=V.getPosition(),G=new f.EditorStateCancellationTokenSource(V,1|4),j=N.raceCancellation(this._getLocationModel(oe,ae,G.token),G.token).then(te=>Ie(this,void 0,void 0,function*(){if(!(!te||G.token.isCancellationRequested)){b.alert(te.ariaMessage);let Z;if(te.referenceAt(oe.uri,ae)){const he=this._getAlternativeCommand(V);he!==this.id&&(Z=V.getAction(he))}const ue=te.references.length;if(ue===0){if(!this._configuration.muteMessage){const he=oe.getWordAtPosition(ae);s.MessageController.get(V).showMessage(this._getNoResultFoundMessage(he),ae)}}else if(ue===1&&Z)Z.run();else return this._onResult(H,ie,V,te)}}),te=>{U.error(te)}).finally(()=>{G.dispose()});return $.showWhile(j,250),j}_onResult(P,V,U,H){return Ie(this,void 0,void 0,function*(){const $=this._getGoToPreference(U);if(!(U instanceof E.EmbeddedCodeEditorWidget)&&(this._configuration.openInPeek||$==="peek"&&H.references.length>1))this._openInPeek(U,H);else{const ie=H.firstReference(),oe=H.references.length>1&&$==="gotoAndPeek",ae=yield this._openReference(U,P,ie,this._configuration.openToSide,!oe);oe&&ae?this._openInPeek(ae,H):H.dispose(),$==="goto"&&V.put(ie)}})}_openReference(P,V,U,H,$){return Ie(this,void 0,void 0,function*(){let ie;if(o.isLocationLink(U)&&(ie=U.targetSelectionRange),ie||(ie=U.range),!!ie){const oe=yield V.openCodeEditor({resource:U.uri,options:{selection:p.Range.collapseToStart(ie),selectionRevealType:3}},P,H);if(!!oe){if($){const ae=oe.getModel(),G=oe.deltaDecorations([],[{range:ie,options:{className:"symbolHighlight"}}]);setTimeout(()=>{oe.getModel()===ae&&oe.deltaDecorations(G,[])},350)}return oe}}})}_openInPeek(P,V){let U=u.ReferencesController.get(P);U&&P.hasModel()?U.toggleWidget(P.getSelection(),N.createCancelablePromise(H=>Promise.resolve(V)),this._configuration.openInPeek):V.dispose()}}class K extends x{_getLocationModel(P,V,U){return Ie(this,void 0,void 0,function*(){return new r.ReferencesModel(yield m.getDefinitionsAtPosition(P,V,U),i.localize(1,null))})}_getNoResultFoundMessage(P){return P&&P.word?i.localize(2,null,P.word):i.localize(3,null)}_getAlternativeCommand(P){return P.getOption(45).alternativeDefinitionCommand}_getGoToPreference(P){return P.getOption(45).multipleDefinitions}}e.DefinitionAction=K;const Y=w.isWeb&&!y.isStandalone?2048|70:70;C.registerEditorAction((T=class Lt extends K{constructor(){super({openToSide:!1,openInPeek:!1,muteMessage:!1},{id:Lt.id,label:i.localize(4,null),alias:"Go to Definition",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasDefinitionProvider,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:Y,weight:100},contextMenuOpts:{group:"navigation",order:1.1},menuOpts:{menuId:n.MenuId.MenubarGoMenu,group:"4_symbol_nav",order:2,title:i.localize(5,null)}});_.CommandsRegistry.registerCommandAlias("editor.action.goToDeclaration",Lt.id)}},T.id="editor.action.revealDefinition",T)),C.registerEditorAction((O=class Et extends K{constructor(){super({openToSide:!0,openInPeek:!1,muteMessage:!1},{id:Et.id,label:i.localize(6,null),alias:"Open Definition to the Side",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasDefinitionProvider,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:M.KeyChord(2048|41,Y),weight:100}});_.CommandsRegistry.registerCommandAlias("editor.action.openDeclarationToTheSide",Et.id)}},O.id="editor.action.revealDefinitionAside",O)),C.registerEditorAction((A=class Dt extends K{constructor(){super({openToSide:!1,openInPeek:!0,muteMessage:!1},{id:Dt.id,label:i.localize(7,null),alias:"Peek Definition",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasDefinitionProvider,a.PeekContext.notInPeekEditor,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:512|70,linux:{primary:2048|1024|68},weight:100},contextMenuOpts:{menuId:n.MenuId.EditorContextPeek,group:"peek",order:2}});_.CommandsRegistry.registerCommandAlias("editor.action.previewDeclaration",Dt.id)}},A.id="editor.action.peekDefinition",A));class ee extends x{_getLocationModel(P,V,U){return Ie(this,void 0,void 0,function*(){return new r.ReferencesModel(yield m.getDeclarationsAtPosition(P,V,U),i.localize(8,null))})}_getNoResultFoundMessage(P){return P&&P.word?i.localize(9,null,P.word):i.localize(10,null)}_getAlternativeCommand(P){return P.getOption(45).alternativeDeclarationCommand}_getGoToPreference(P){return P.getOption(45).multipleDeclarations}}C.registerEditorAction((B=class Yt extends ee{constructor(){super({openToSide:!1,openInPeek:!1,muteMessage:!1},{id:Yt.id,label:i.localize(11,null),alias:"Go to Declaration",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasDeclarationProvider,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),contextMenuOpts:{group:"navigation",order:1.3},menuOpts:{menuId:n.MenuId.MenubarGoMenu,group:"4_symbol_nav",order:3,title:i.localize(12,null)}})}_getNoResultFoundMessage(P){return P&&P.word?i.localize(13,null,P.word):i.localize(14,null)}},B.id="editor.action.revealDeclaration",B)),C.registerEditorAction(class extends ee{constructor(){super({openToSide:!1,openInPeek:!0,muteMessage:!1},{id:"editor.action.peekDeclaration",label:i.localize(15,null),alias:"Peek Declaration",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasDeclarationProvider,a.PeekContext.notInPeekEditor,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),contextMenuOpts:{menuId:n.MenuId.EditorContextPeek,group:"peek",order:3}})}});class se extends x{_getLocationModel(P,V,U){return Ie(this,void 0,void 0,function*(){return new r.ReferencesModel(yield m.getTypeDefinitionsAtPosition(P,V,U),i.localize(16,null))})}_getNoResultFoundMessage(P){return P&&P.word?i.localize(17,null,P.word):i.localize(18,null)}_getAlternativeCommand(P){return P.getOption(45).alternativeTypeDefinitionCommand}_getGoToPreference(P){return P.getOption(45).multipleTypeDefinitions}}C.registerEditorAction((F=class Zt extends se{constructor(){super({openToSide:!1,openInPeek:!1,muteMessage:!1},{id:Zt.ID,label:i.localize(19,null),alias:"Go to Type Definition",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasTypeDefinitionProvider,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:0,weight:100},contextMenuOpts:{group:"navigation",order:1.4},menuOpts:{menuId:n.MenuId.MenubarGoMenu,group:"4_symbol_nav",order:3,title:i.localize(20,null)}})}},F.ID="editor.action.goToTypeDefinition",F)),C.registerEditorAction((D=class Xt extends se{constructor(){super({openToSide:!1,openInPeek:!0,muteMessage:!1},{id:Xt.ID,label:i.localize(21,null),alias:"Peek Type Definition",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasTypeDefinitionProvider,a.PeekContext.notInPeekEditor,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),contextMenuOpts:{menuId:n.MenuId.EditorContextPeek,group:"peek",order:4}})}},D.ID="editor.action.peekTypeDefinition",D));class ne extends x{_getLocationModel(P,V,U){return Ie(this,void 0,void 0,function*(){return new r.ReferencesModel(yield m.getImplementationsAtPosition(P,V,U),i.localize(22,null))})}_getNoResultFoundMessage(P){return P&&P.word?i.localize(23,null,P.word):i.localize(24,null)}_getAlternativeCommand(P){return P.getOption(45).alternativeImplementationCommand}_getGoToPreference(P){return P.getOption(45).multipleImplementations}}C.registerEditorAction((R=class Qt extends ne{constructor(){super({openToSide:!1,openInPeek:!1,muteMessage:!1},{id:Qt.ID,label:i.localize(25,null),alias:"Go to Implementations",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasImplementationProvider,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:2048|70,weight:100},menuOpts:{menuId:n.MenuId.MenubarGoMenu,group:"4_symbol_nav",order:4,title:i.localize(26,null)},contextMenuOpts:{group:"navigation",order:1.45}})}},R.ID="editor.action.goToImplementation",R)),C.registerEditorAction((W=class Jt extends ne{constructor(){super({openToSide:!1,openInPeek:!0,muteMessage:!1},{id:Jt.ID,label:i.localize(27,null),alias:"Peek Implementations",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasImplementationProvider,a.PeekContext.notInPeekEditor,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:2048|1024|70,weight:100},contextMenuOpts:{menuId:n.MenuId.EditorContextPeek,group:"peek",order:5}})}},W.ID="editor.action.peekImplementation",W));class le extends x{_getNoResultFoundMessage(P){return P?i.localize(28,null,P.word):i.localize(29,null)}_getAlternativeCommand(P){return P.getOption(45).alternativeReferenceCommand}_getGoToPreference(P){return P.getOption(45).multipleReferences}}C.registerEditorAction(class extends le{constructor(){super({openToSide:!1,openInPeek:!1,muteMessage:!1},{id:"editor.action.goToReferences",label:i.localize(30,null),alias:"Go to References",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasReferenceProvider,a.PeekContext.notInPeekEditor,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),kbOpts:{kbExpr:c.EditorContextKeys.editorTextFocus,primary:1024|70,weight:100},contextMenuOpts:{group:"navigation",order:1.45},menuOpts:{menuId:n.MenuId.MenubarGoMenu,group:"4_symbol_nav",order:5,title:i.localize(31,null)}})}_getLocationModel(P,V,U){return Ie(this,void 0,void 0,function*(){return new r.ReferencesModel(yield m.getReferencesAtPosition(P,V,!0,U),i.localize(32,null))})}}),C.registerEditorAction(class extends le{constructor(){super({openToSide:!1,openInPeek:!0,muteMessage:!1},{id:"editor.action.referenceSearch.trigger",label:i.localize(33,null),alias:"Peek References",precondition:t.ContextKeyExpr.and(c.EditorContextKeys.hasReferenceProvider,a.PeekContext.notInPeekEditor,c.EditorContextKeys.isInWalkThroughSnippet.toNegated()),contextMenuOpts:{menuId:n.MenuId.EditorContextPeek,group:"peek",order:6}})}_getLocationModel(P,V,U){return Ie(this,void 0,void 0,function*(){return new r.ReferencesModel(yield m.getReferencesAtPosition(P,V,!1,U),i.localize(34,null))})}});class X extends x{constructor(P,V,U){super(P,{id:"editor.action.goToLocation",label:i.localize(35,null),alias:"Go To Any Symbol",precondition:t.ContextKeyExpr.and(a.PeekContext.notInPeekEditor,c.EditorContextKeys.isInWalkThroughSnippet.toNegated())});this._references=V,this._gotoMultipleBehaviour=U}_getLocationModel(P,V,U){return Ie(this,void 0,void 0,function*(){return new r.ReferencesModel(this._references,i.localize(36,null))})}_getNoResultFoundMessage(P){return P&&i.localize(37,null,P.word)||""}_getGoToPreference(P){var V;return(V=this._gotoMultipleBehaviour)!==null&&V!==void 0?V:P.getOption(45).multipleReferences}_getAlternativeCommand(){return""}}_.CommandsRegistry.registerCommand({id:"editor.action.goToLocations",description:{description:"Go to locations from a position in a file",args:[{name:"uri",description:"The text document in which to start",constraint:L.URI},{name:"position",description:"The position at which to start",constraint:g.Position.isIPosition},{name:"locations",description:"An array of locations.",constraint:Array},{name:"multiple",description:"Define what to do when having multiple results, either `peek`, `gotoAndPeek`, or `goto"},{name:"noResultsMessage",description:"Human readable message that shows when locations is empty."}]},handler:(z,P,V,U,H,$,ie)=>Ie(void 0,void 0,void 0,function*(){k.assertType(L.URI.isUri(P)),k.assertType(g.Position.isIPosition(V)),k.assertType(Array.isArray(U)),k.assertType(typeof H=="undefined"||typeof H=="string"),k.assertType(typeof ie=="undefined"||typeof ie=="boolean");const oe=z.get(d.ICodeEditorService),ae=yield oe.openCodeEditor({resource:P},oe.getFocusedCodeEditor());if(S.isCodeEditor(ae))return ae.setPosition(V),ae.revealPositionInCenterIfOutsideViewport(V,0),ae.invokeWithinContext(G=>{const j=new class extends X{_getNoResultFoundMessage(te){return $||super._getNoResultFoundMessage(te)}}({muteMessage:!Boolean($),openInPeek:Boolean(ie),openToSide:!1},U,H);G.get(I.IInstantiationService).invokeFunction(j.run.bind(j),ae)})})}),_.CommandsRegistry.registerCommand({id:"editor.action.peekLocations",description:{description:"Peek locations from a position in a file",args:[{name:"uri",description:"The text document in which to start",constraint:L.URI},{name:"position",description:"The position at which to start",constraint:g.Position.isIPosition},{name:"locations",description:"An array of locations.",constraint:Array},{name:"multiple",description:"Define what to do when having multiple results, either `peek`, `gotoAndPeek`, or `goto"}]},handler:(z,P,V,U,H)=>Ie(void 0,void 0,void 0,function*(){z.get(_.ICommandService).executeCommand("editor.action.goToLocations",P,V,U,H,void 0,!0)})}),_.CommandsRegistry.registerCommand({id:"editor.action.findReferences",handler:(z,P,V)=>{k.assertType(L.URI.isUri(P)),k.assertType(g.Position.isIPosition(V));const U=z.get(d.ICodeEditorService);return U.openCodeEditor({resource:P},U.getFocusedCodeEditor()).then(H=>{if(!(!S.isCodeEditor(H)||!H.hasModel())){const $=u.ReferencesController.get(H);if(!!$){const ie=N.createCancelablePromise(ae=>m.getReferencesAtPosition(H.getModel(),g.Position.lift(V),!1,ae).then(G=>new r.ReferencesModel(G,i.localize(38,null)))),oe=new p.Range(V.lineNumber,V.column,V.lineNumber,V.column);return Promise.resolve($.toggleWidget(oe,ie,!1))}}})}}),_.CommandsRegistry.registerCommandAlias("editor.action.showReferences","editor.action.peekLocations")}),define(Q[272],J([0,1,482,15,12,73,57,3,18,13,262,2,67,11,22,70,271,227,14,20,118,16,344]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.GotoDefinitionAtPositionEditorContribution=void 0;let m=class rt{constructor(f,v,y){this.textModelResolverService=v,this.modeService=y,this.toUnhook=new c.DisposableStore,this.toUnhookForKeyboard=new c.DisposableStore,this.linkDecorations=[],this.currentWordAtPosition=null,this.previousPromise=null,this.editor=f;let L=new i.ClickLinkGesture(f);this.toUnhook.add(L),this.toUnhook.add(L.onMouseMoveOrRelevantKeyDown(([I,k])=>{this.startFindDefinitionFromMouse(I,t.withNullAsUndefined(k))})),this.toUnhook.add(L.onExecute(I=>{this.isEnabled(I)&&this.gotoDefinition(I.target.position,I.hasSideBySideModifier).then(()=>{this.removeLinkDecorations()},k=>{this.removeLinkDecorations(),M.onUnexpectedError(k)})})),this.toUnhook.add(L.onCancel(()=>{this.removeLinkDecorations(),this.currentWordAtPosition=null}))}static get(f){return f.getContribution(rt.ID)}startFindDefinitionFromCursor(f){return this.startFindDefinition(f).then(()=>{this.toUnhookForKeyboard.add(this.editor.onDidChangeCursorPosition(()=>{this.currentWordAtPosition=null,this.removeLinkDecorations(),this.toUnhookForKeyboard.clear()})),this.toUnhookForKeyboard.add(this.editor.onKeyDown(v=>{v&&(this.currentWordAtPosition=null,this.removeLinkDecorations(),this.toUnhookForKeyboard.clear())}))})}startFindDefinitionFromMouse(f,v){if(!(f.target.type===9&&this.linkDecorations.length>0)){if(!this.editor.hasModel()||!this.isEnabled(f,v)){this.currentWordAtPosition=null,this.removeLinkDecorations();return}const y=f.target.position;this.startFindDefinition(y)}}startFindDefinition(f){var v;this.toUnhookForKeyboard.clear();const y=f?(v=this.editor.getModel())===null||v===void 0?void 0:v.getWordAtPosition(f):null;if(!y)return this.currentWordAtPosition=null,this.removeLinkDecorations(),Promise.resolve(0);if(this.currentWordAtPosition&&this.currentWordAtPosition.startColumn===y.startColumn&&this.currentWordAtPosition.endColumn===y.endColumn&&this.currentWordAtPosition.word===y.word)return Promise.resolve(0);this.currentWordAtPosition=y;let L=new u.EditorState(this.editor,4|1|2|8);return this.previousPromise&&(this.previousPromise.cancel(),this.previousPromise=null),this.previousPromise=N.createCancelablePromise(I=>this.findDefinition(f,I)),this.previousPromise.then(I=>{if(!I||!I.length||!L.validate(this.editor)){this.removeLinkDecorations();return}if(I.length>1)this.addDecoration(new C.Range(f.lineNumber,y.startColumn,f.lineNumber,y.endColumn),new w.MarkdownString().appendText(b.localize(0,null,I.length)));else{let k=I[0];if(!k.uri)return;this.textModelResolverService.createModelReference(k.uri).then(E=>{if(!E.object||!E.object.textEditorModel){E.dispose();return}const{object:{textEditorModel:T}}=E,{startLineNumber:O}=k.range;if(O<1||O>T.getLineCount()){E.dispose();return}const A=this.getPreviewValue(T,O,k);let B;k.originSelectionRange?B=C.Range.lift(k.originSelectionRange):B=new C.Range(f.lineNumber,y.startColumn,f.lineNumber,y.endColumn);const F=this.modeService.getModeIdByFilepathOrFirstLine(T.uri);this.addDecoration(B,new w.MarkdownString().appendCodeblock(F||"",A)),E.dispose()})}}).then(void 0,M.onUnexpectedError)}getPreviewValue(f,v,y){let L=y.targetSelectionRange?y.range:this.getPreviewRangeBasedOnBrackets(f,v);return L.endLineNumber-L.startLineNumber>=rt.MAX_SOURCE_PREVIEW_LINES&&(L=this.getPreviewRangeBasedOnIndentation(f,v)),this.stripIndentationFromPreviewRange(f,v,L)}stripIndentationFromPreviewRange(f,v,y){let I=f.getLineFirstNonWhitespaceColumn(v);for(let E=v+1;Ey)return new C.Range(v,1,y+1,1);k=f.findNextBracket(new n.Position(T,O))}return new C.Range(v,1,y+1,1)}addDecoration(f,v){const y={range:f,options:{inlineClassName:"goto-definition-link",hoverMessage:v}};this.linkDecorations=this.editor.deltaDecorations(this.linkDecorations,[y])}removeLinkDecorations(){this.linkDecorations.length>0&&(this.linkDecorations=this.editor.deltaDecorations(this.linkDecorations,[]))}isEnabled(f,v){return this.editor.hasModel()&&f.isNoneOrSingleMouseDown&&f.target.type===6&&(f.hasTriggerModifier||(v?v.keyCodeIsTriggerKey:!1))&&d.DefinitionProviderRegistry.has(this.editor.getModel())}findDefinition(f,v){const y=this.editor.getModel();return y?p.getDefinitionsAtPosition(y,f,v):Promise.resolve(null)}gotoDefinition(f,v){return this.editor.setPosition(f),this.editor.invokeWithinContext(y=>{const L=!v&&this.editor.getOption(72)&&!this.isInPeekEditor(y);return new r.DefinitionAction({openToSide:v,openInPeek:L,muteMessage:!0},{alias:"",label:"",id:"",precondition:void 0}).run(y,this.editor)})}isInPeekEditor(f){const v=f.get(h.IContextKeyService);return l.PeekContext.inPeekEditor.getValue(v)}dispose(){this.toUnhook.dispose()}};m.ID="editor.contrib.gotodefinitionatposition",m.MAX_SOURCE_PREVIEW_LINES=8,m=Me([_e(1,o.ITextModelService),_e(2,S.IModeService)],m),e.GotoDefinitionAtPositionEditorContribution=m,g.registerEditorContribution(m.ID,m),s.registerThemingParticipant((_,f)=>{const v=_.getColor(a.editorActiveLinkForeground);v&&f.addRule(`.monaco-editor .goto-definition-link { color: ${v} !important; }`)})}),define(Q[273],J([0,1,488,39,2,13,3,25,57,661,560,58,22,11,272,16,9]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ModesHoverController=void 0;let i=class ei{constructor(h,m,_,f,v,y){this._editor=h,this._instantiationService=m,this._openerService=_,this._modeService=f,this._themeService=v,this._toUnhook=new M.DisposableStore,this._isMouseDown=!1,this._hoverClicked=!1,this._contentWidget=null,this._glyphWidget=null,this._hookEvents(),this._didChangeConfigurationHandler=this._editor.onDidChangeConfiguration(L=>{L.hasChanged(48)&&(this._unhookEvents(),this._hookEvents())}),this._hoverVisibleKey=C.EditorContextKeys.hoverVisible.bindTo(y)}static get(h){return h.getContribution(ei.ID)}_hookEvents(){const h=()=>this._hideWidgets(),m=this._editor.getOption(48);this._isHoverEnabled=m.enabled,this._isHoverSticky=m.sticky,this._isHoverEnabled?(this._toUnhook.add(this._editor.onMouseDown(_=>this._onEditorMouseDown(_))),this._toUnhook.add(this._editor.onMouseUp(_=>this._onEditorMouseUp(_))),this._toUnhook.add(this._editor.onMouseMove(_=>this._onEditorMouseMove(_))),this._toUnhook.add(this._editor.onKeyDown(_=>this._onKeyDown(_))),this._toUnhook.add(this._editor.onDidChangeModelDecorations(()=>this._onModelDecorationsChanged()))):(this._toUnhook.add(this._editor.onMouseMove(_=>this._onEditorMouseMove(_))),this._toUnhook.add(this._editor.onKeyDown(_=>this._onKeyDown(_)))),this._toUnhook.add(this._editor.onMouseLeave(h)),this._toUnhook.add(this._editor.onDidChangeModel(h)),this._toUnhook.add(this._editor.onDidScrollChange(_=>this._onEditorScrollChanged(_)))}_unhookEvents(){this._toUnhook.clear()}_onModelDecorationsChanged(){var h,m;(h=this._contentWidget)===null||h===void 0||h.onModelDecorationsChanged(),(m=this._glyphWidget)===null||m===void 0||m.onModelDecorationsChanged()}_onEditorScrollChanged(h){(h.scrollTopChanged||h.scrollLeftChanged)&&this._hideWidgets()}_onEditorMouseDown(h){this._isMouseDown=!0;const m=h.target.type;if(m===9&&h.target.detail===g.ModesContentHoverWidget.ID){this._hoverClicked=!0;return}m===12&&h.target.detail===p.ModesGlyphHoverWidget.ID||(m!==12&&h.target.detail!==p.ModesGlyphHoverWidget.ID&&(this._hoverClicked=!1),this._hideWidgets())}_onEditorMouseUp(h){this._isMouseDown=!1}_onEditorMouseMove(h){var m,_,f,v,y,L;let I=h.target.type;if(!(this._isMouseDown&&this._hoverClicked)&&!(this._isHoverSticky&&I===9&&h.target.detail===g.ModesContentHoverWidget.ID)&&!(this._isHoverSticky&&!((_=(m=h.event.browserEvent.view)===null||m===void 0?void 0:m.getSelection())===null||_===void 0?void 0:_.isCollapsed))&&!(!this._isHoverSticky&&I===9&&h.target.detail===g.ModesContentHoverWidget.ID&&((f=this._contentWidget)===null||f===void 0?void 0:f.isColorPickerVisible()))&&!(this._isHoverSticky&&I===12&&h.target.detail===p.ModesGlyphHoverWidget.ID)){if(I===7){const k=this._editor.getOption(38).typicalHalfwidthCharacterWidth/2,E=h.target.detail;E&&!E.isAfterLines&&typeof E.horizontalDistanceToText=="number"&&E.horizontalDistanceToTextT.startsWith("ced-colorBox"))&&h.target.range.endColumn-h.target.range.startColumn==1?new S.Range(h.target.range.startLineNumber,h.target.range.startColumn+1,h.target.range.endLineNumber,h.target.range.endColumn+1):h.target.range;this._contentWidget||(this._contentWidget=new g.ModesContentHoverWidget(this._editor,this._hoverVisibleKey,this._instantiationService,this._themeService)),this._contentWidget.startShowingAt(E,0,!1)}}else I===2?((L=this._contentWidget)===null||L===void 0||L.hide(),this._isHoverEnabled&&h.target.position&&(this._glyphWidget||(this._glyphWidget=new p.ModesGlyphHoverWidget(this._editor,this._modeService,this._openerService)),this._glyphWidget.startShowingAt(h.target.position.lineNumber))):this._hideWidgets()}}_onKeyDown(h){h.keyCode!==5&&h.keyCode!==6&&h.keyCode!==57&&h.keyCode!==4&&this._hideWidgets()}_hideWidgets(){var h,m,_;this._isMouseDown&&this._hoverClicked&&((h=this._contentWidget)===null||h===void 0?void 0:h.isColorPickerVisible())||(this._hoverClicked=!1,(m=this._glyphWidget)===null||m===void 0||m.hide(),(_=this._contentWidget)===null||_===void 0||_.hide())}isColorPickerVisible(){var h;return((h=this._contentWidget)===null||h===void 0?void 0:h.isColorPickerVisible())||!1}showContentHover(h,m,_){this._contentWidget||(this._contentWidget=new g.ModesContentHoverWidget(this._editor,this._hoverVisibleKey,this._instantiationService,this._themeService)),this._contentWidget.startShowingAt(h,m,_)}dispose(){var h,m;this._unhookEvents(),this._toUnhook.dispose(),this._didChangeConfigurationHandler.dispose(),(h=this._glyphWidget)===null||h===void 0||h.dispose(),(m=this._contentWidget)===null||m===void 0||m.dispose()}};i.ID="editor.contrib.hover",i=Me([_e(1,r.IInstantiationService),_e(2,c.IOpenerService),_e(3,d.IModeService),_e(4,s.IThemeService),_e(5,u.IContextKeyService)],i),e.ModesHoverController=i;class n extends w.EditorAction{constructor(){super({id:"editor.action.showHover",label:b.localize(0,null),alias:"Show Hover",precondition:void 0,kbOpts:{kbExpr:C.EditorContextKeys.editorTextFocus,primary:N.KeyChord(2048|41,2048|39),weight:100}})}run(h,m){if(!!m.hasModel()){let _=i.get(m);if(!!_){const f=m.getPosition(),v=new S.Range(f.lineNumber,f.column,f.lineNumber,f.column),y=m.getOption(2)===2;_.showContentHover(v,1,y)}}}}class t extends w.EditorAction{constructor(){super({id:"editor.action.showDefinitionPreviewHover",label:b.localize(1,null),alias:"Show Definition Preview Hover",precondition:void 0})}run(h,m){let _=i.get(m);if(!!_){const f=m.getPosition();if(!!f){const v=new S.Range(f.lineNumber,f.column,f.lineNumber,f.column),L=a.GotoDefinitionAtPositionEditorContribution.get(m).startFindDefinitionFromCursor(f);L?L.then(()=>{_.showContentHover(v,1,!0)}):_.showContentHover(v,1,!0)}}}}w.registerEditorContribution(i.ID,i),w.registerEditorAction(n),w.registerEditorAction(t),s.registerThemingParticipant((l,h)=>{const m=l.getColor(o.editorHoverHighlight);m&&h.addRule(`.monaco-editor .hoverHighlight { background-color: ${m}; }`);const _=l.getColor(o.editorHoverBackground);_&&h.addRule(`.monaco-editor .monaco-hover { background-color: ${_}; }`);const f=l.getColor(o.editorHoverBorder);f&&(h.addRule(`.monaco-editor .monaco-hover { border: 1px solid ${f}; }`),h.addRule(`.monaco-editor .monaco-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${f.transparent(.5)}; }`),h.addRule(`.monaco-editor .monaco-hover hr { border-top: 1px solid ${f.transparent(.5)}; }`),h.addRule(`.monaco-editor .monaco-hover hr { border-bottom: 0px solid ${f.transparent(.5)}; }`));const v=l.getColor(o.textLinkForeground);v&&h.addRule(`.monaco-editor .monaco-hover a { color: ${v}; }`);const y=l.getColor(o.editorHoverForeground);y&&h.addRule(`.monaco-editor .monaco-hover { color: ${y}; }`);const L=l.getColor(o.editorHoverStatusBarBackground);L&&h.addRule(`.monaco-editor .monaco-hover .hover-row .actions { background-color: ${L}; }`);const I=l.getColor(o.textCodeBlockBackground);I&&h.addRule(`.monaco-editor .monaco-hover code { background-color: ${I}; }`)})}),define(Q[670],J([0,1,2,13,273,3,260]),function(q,e,b,N,M,w){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.ColorContribution=void 0;class S extends b.Disposable{constructor(d){super();this._editor=d,this._register(d.onMouseDown(g=>this.onMouseDown(g)))}dispose(){super.dispose()}onMouseDown(d){var g;if(d.target.type===6&&!![...((g=d.target.element)===null||g===void 0?void 0:g.classList.values())||[]].find(s=>s.startsWith("ced-colorBox"))&&!!d.target.range){const o=this._editor.getContribution(M.ModesHoverController.ID);if(!o.isColorPickerVisible()){const s=new w.Range(d.target.range.startLineNumber,d.target.range.startColumn+1,d.target.range.endLineNumber,d.target.range.endColumn+1);o.showContentHover(s,0,!1)}}}}e.ColorContribution=S,S.ID="editor.contrib.colorContribution",N.registerEditorContribution(S.ID,S)}),define(Q[671],J([0,1,15,2,13,18,36,253,11,46,269,247]),function(q,e,b,N,M,w,S,C,d,g,p,c){"use strict";Object.defineProperty(e,"__esModule",{value:!0});let o=class extends N.Disposable{constructor(a,u,r,i){super();this._modelService=u,this._themeService=r,this._configurationService=i,this._editor=a,this._tokenizeViewport=new b.RunOnceScheduler(()=>this._tokenizeViewportNow(),100),this._outstandingRequests=[],this._register(this._editor.onDidScrollChange(()=>{this._tokenizeViewport.schedule()})),this._register(this._editor.onDidChangeModel(()=>{this._cancelAll(),this._tokenizeViewport.schedule()})),this._register(this._editor.onDidChangeModelContent(n=>{this._cancelAll(),this._tokenizeViewport.schedule()})),this._register(w.DocumentRangeSemanticTokensProviderRegistry.onDidChange(()=>{this._cancelAll(),this._tokenizeViewport.schedule()})),this._register(this._configurationService.onDidChangeConfiguration(n=>{n.affectsConfiguration(p.SEMANTIC_HIGHLIGHTING_SETTING_ID)&&(this._cancelAll(),this._tokenizeViewport.schedule())})),this._register(this._themeService.onDidColorThemeChange(()=>{this._cancelAll(),this._tokenizeViewport.schedule()}))}_cancelAll(){for(const a of this._outstandingRequests)a.cancel();this._outstandingRequests=[]}_removeOutstandingRequest(a){for(let u=0,r=this._outstandingRequests.length;uthis._requestRange(a,n,u,r)))}}}_requestRange(a,u,r,i){const n=a.getVersionId(),t=b.createCancelablePromise(l=>Promise.resolve(r.provideDocumentRangeSemanticTokens(a,u,l)));return t.then(l=>{!l||a.isDisposed()||a.getVersionId()!==n||a.setPartialSemanticTokens(u,C.toMultilineTokens2(l,i,a.getLanguageIdentifier()))}).then(()=>this._removeOutstandingRequest(t),()=>this._removeOutstandingRequest(t)),t}};o.ID="editor.contrib.viewportSemanticTokens",o=Me([_e(1,S.IModelService),_e(2,d.IThemeService),_e(3,g.IConfigurationService)],o),M.registerEditorContribution(o.ID,o)}),define(Q[672],J([0,1,13,28,270,46,16,9,32,79]),function(q,e,b,N,M,w,S,C,d,g){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.StandaloneReferencesController=void 0;let p=class extends M.ReferencesController{constructor(o,s,a,u,r,i,n){super(!0,o,s,a,u,r,i,n)}};p=Me([_e(1,S.IContextKeyService),_e(2,N.ICodeEditorService),_e(3,d.INotificationService),_e(4,C.IInstantiationService),_e(5,g.IStorageService),_e(6,w.IConfigurationService)],p),e.StandaloneReferencesController=p,b.registerEditorContribution(M.ReferencesController.ID,p)}),define(Q[673],J([0,1,529,148,12,74,184,82,43,32,2]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.UndoRedoService=void 0;const c=!1;function o(_){return _.scheme===d.Schemas.file?_.fsPath:_.path}let s=0;class a{constructor(f,v,y,L,I,k,E){this.id=++s,this.type=0,this.actual=f,this.label=f.label,this.confirmBeforeUndo=f.confirmBeforeUndo||!1,this.resourceLabel=v,this.strResource=y,this.resourceLabels=[this.resourceLabel],this.strResources=[this.strResource],this.groupId=L,this.groupOrder=I,this.sourceId=k,this.sourceOrder=E,this.isValid=!0}setValid(f){this.isValid=f}toString(){return`[id:${this.id}] [group:${this.groupId}] [${this.isValid?" VALID":"INVALID"}] ${this.actual.constructor.name} - ${this.actual}`}}class u{constructor(f,v){this.resourceLabel=f,this.reason=v}}class r{constructor(){this.elements=new Map}createMessage(){const f=[],v=[];for(const[,L]of this.elements)(L.reason===0?f:v).push(L.resourceLabel);let y=[];return f.length>0&&y.push(b.localize(0,null,f.join(", "))),v.length>0&&y.push(b.localize(1,null,v.join(", "))),y.join(` +`)}get size(){return this.elements.size}has(f){return this.elements.has(f)}set(f,v){this.elements.set(f,v)}delete(f){return this.elements.delete(f)}}class i{constructor(f,v,y,L,I,k,E){this.id=++s,this.type=1,this.actual=f,this.label=f.label,this.confirmBeforeUndo=f.confirmBeforeUndo||!1,this.resourceLabels=v,this.strResources=y,this.groupId=L,this.groupOrder=I,this.sourceId=k,this.sourceOrder=E,this.removedResources=null,this.invalidatedResources=null}canSplit(){return typeof this.actual.split=="function"}removeResource(f,v,y){this.removedResources||(this.removedResources=new r),this.removedResources.has(v)||this.removedResources.set(v,new u(f,y))}setValid(f,v,y){y?this.invalidatedResources&&(this.invalidatedResources.delete(v),this.invalidatedResources.size===0&&(this.invalidatedResources=null)):(this.invalidatedResources||(this.invalidatedResources=new r),this.invalidatedResources.has(v)||this.invalidatedResources.set(v,new u(f,0)))}toString(){return`[id:${this.id}] [group:${this.groupId}] [${this.invalidatedResources?"INVALID":" VALID"}] ${this.actual.constructor.name} - ${this.actual}`}}class n{constructor(f,v){this.resourceLabel=f,this.strResource=v,this._past=[],this._future=[],this.locked=!1,this.versionId=1}dispose(){for(const f of this._past)f.type===1&&f.removeResource(this.resourceLabel,this.strResource,0);for(const f of this._future)f.type===1&&f.removeResource(this.resourceLabel,this.strResource,0);this.versionId++}toString(){let f=[];f.push(`* ${this.strResource}:`);for(let v=0;v=0;v--)f.push(` * [REDO] ${this._future[v]}`);return f.join(` +`)}flushAllElements(){this._past=[],this._future=[],this.versionId++}_setElementValidFlag(f,v){f.type===1?f.setValid(this.resourceLabel,this.strResource,v):f.setValid(v)}setElementsValidFlag(f,v){for(const y of this._past)v(y.actual)&&this._setElementValidFlag(y,f);for(const y of this._future)v(y.actual)&&this._setElementValidFlag(y,f)}pushElement(f){for(const v of this._future)v.type===1&&v.removeResource(this.resourceLabel,this.strResource,1);this._future=[],this._past.push(f),this.versionId++}createSnapshot(f){const v=[];for(let y=0,L=this._past.length;y=0;y--)v.push(this._future[y].id);return new N.ResourceEditStackSnapshot(f,v)}restoreSnapshot(f){const v=f.elements.length;let y=!0,L=0,I=-1;for(let E=0,T=this._past.length;E=v||O.id!==f.elements[L])&&(y=!1,I=0),!y&&O.type===1&&O.removeResource(this.resourceLabel,this.strResource,0)}let k=-1;for(let E=this._future.length-1;E>=0;E--,L++){const T=this._future[E];y&&(L>=v||T.id!==f.elements[L])&&(y=!1,k=E),!y&&T.type===1&&T.removeResource(this.resourceLabel,this.strResource,0)}I!==-1&&(this._past=this._past.slice(0,I)),k!==-1&&(this._future=this._future.slice(k+1)),this.versionId++}getElements(){const f=[],v=[];for(const y of this._past)f.push(y.actual);for(const y of this._future)v.push(y.actual);return{past:f,future:v}}getClosestPastElement(){return this._past.length===0?null:this._past[this._past.length-1]}getSecondClosestPastElement(){return this._past.length<2?null:this._past[this._past.length-2]}getClosestFutureElement(){return this._future.length===0?null:this._future[this._future.length-1]}hasPastElements(){return this._past.length>0}hasFutureElements(){return this._future.length>0}splitPastWorkspaceElement(f,v){for(let y=this._past.length-1;y>=0;y--)if(this._past[y]===f){v.has(this.strResource)?this._past[y]=v.get(this.strResource):this._past.splice(y,1);break}this.versionId++}splitFutureWorkspaceElement(f,v){for(let y=this._future.length-1;y>=0;y--)if(this._future[y]===f){v.has(this.strResource)?this._future[y]=v.get(this.strResource):this._future.splice(y,1);break}this.versionId++}moveBackward(f){this._past.pop(),this._future.push(f),this.versionId++}moveForward(f){this._future.pop(),this._past.push(f),this.versionId++}}class t{constructor(f){this.editStacks=f,this._versionIds=[];for(let v=0,y=this.editStacks.length;vv.sourceOrder)&&(v=k,y=L)}return[v,y]}canUndo(f){if(f instanceof N.UndoRedoSource){const[,y]=this._findClosestUndoElementWithSource(f.id);return!!y}const v=this.getUriComparisonKey(f);return this._editStacks.has(v)?this._editStacks.get(v).hasPastElements():!1}_onError(f,v){M.onUnexpectedError(f);for(const y of v.strResources)this.removeElements(y);this._notificationService.error(f)}_acquireLocks(f){for(const v of f.editStacks)if(v.locked)throw new Error("Cannot acquire edit stack lock");for(const v of f.editStacks)v.locked=!0;return()=>{for(const v of f.editStacks)v.locked=!1}}_safeInvokeWithLocks(f,v,y,L,I){const k=this._acquireLocks(y);let E;try{E=v()}catch(T){return k(),L.dispose(),this._onError(T,f)}return E?E.then(()=>(k(),L.dispose(),I()),T=>(k(),L.dispose(),this._onError(T,f))):(k(),L.dispose(),I())}_invokeWorkspacePrepare(f){return Ie(this,void 0,void 0,function*(){if(typeof f.actual.prepareUndoRedo=="undefined")return p.Disposable.None;const v=f.actual.prepareUndoRedo();return typeof v=="undefined"?p.Disposable.None:v})}_invokeResourcePrepare(f,v){if(f.actual.type!==1||typeof f.actual.prepareUndoRedo=="undefined")return v(p.Disposable.None);const y=f.actual.prepareUndoRedo();return y?p.isDisposable(y)?v(y):y.then(L=>v(L)):v(p.Disposable.None)}_getAffectedEditStacks(f){const v=[];for(const y of f.strResources)v.push(this._editStacks.get(y)||l);return new t(v)}_tryToSplitAndUndo(f,v,y,L){if(v.canSplit())return this._splitPastWorkspaceElement(v,y),this._notificationService.info(L),new m(this._undo(f,0,!0));for(const I of v.strResources)this.removeElements(I);return this._notificationService.info(L),new m}_checkWorkspaceUndo(f,v,y,L){if(v.removedResources)return this._tryToSplitAndUndo(f,v,v.removedResources,b.localize(2,null,v.label,v.removedResources.createMessage()));if(L&&v.invalidatedResources)return this._tryToSplitAndUndo(f,v,v.invalidatedResources,b.localize(3,null,v.label,v.invalidatedResources.createMessage()));const I=[];for(const E of y.editStacks)E.getClosestPastElement()!==v&&I.push(E.resourceLabel);if(I.length>0)return this._tryToSplitAndUndo(f,v,null,b.localize(4,null,v.label,I.join(", ")));const k=[];for(const E of y.editStacks)E.locked&&k.push(E.resourceLabel);return k.length>0?this._tryToSplitAndUndo(f,v,null,b.localize(5,null,v.label,k.join(", "))):y.isValid()?null:this._tryToSplitAndUndo(f,v,null,b.localize(6,null,v.label))}_workspaceUndo(f,v,y){const L=this._getAffectedEditStacks(v),I=this._checkWorkspaceUndo(f,v,L,!1);return I?I.returnValue:this._confirmAndExecuteWorkspaceUndo(f,v,L,y)}_isPartOfUndoGroup(f){if(!f.groupId)return!1;for(const[,v]of this._editStacks){const y=v.getClosestPastElement();if(!!y){if(y===f){const L=v.getSecondClosestPastElement();if(L&&L.groupId===f.groupId)return!0}if(y.groupId===f.groupId)return!0}}return!1}_confirmAndExecuteWorkspaceUndo(f,v,y,L){return Ie(this,void 0,void 0,function*(){if(v.canSplit()&&!this._isPartOfUndoGroup(v)){const E=yield this._dialogService.show(C.default.Info,b.localize(7,null,v.label),[b.localize(8,null,y.editStacks.length),b.localize(9,null),b.localize(10,null)],{cancelId:2});if(E.choice===2)return;if(E.choice===1)return this._splitPastWorkspaceElement(v,null),this._undo(f,0,!0);const T=this._checkWorkspaceUndo(f,v,y,!1);if(T)return T.returnValue;L=!0}let I;try{I=yield this._invokeWorkspacePrepare(v)}catch(E){return this._onError(E,v)}const k=this._checkWorkspaceUndo(f,v,y,!0);if(k)return I.dispose(),k.returnValue;for(const E of y.editStacks)E.moveBackward(v);return this._safeInvokeWithLocks(v,()=>v.actual.undo(),y,I,()=>this._continueUndoInGroup(v.groupId,L))})}_resourceUndo(f,v,y){if(!v.isValid){f.flushAllElements();return}if(f.locked){const L=b.localize(11,null,v.label);this._notificationService.info(L);return}return this._invokeResourcePrepare(v,L=>(f.moveBackward(v),this._safeInvokeWithLocks(v,()=>v.actual.undo(),new t([f]),L,()=>this._continueUndoInGroup(v.groupId,y))))}_findClosestUndoElementInGroup(f){if(!f)return[null,null];let v=null,y=null;for(const[L,I]of this._editStacks){const k=I.getClosestPastElement();!k||k.groupId===f&&(!v||k.groupOrder>v.groupOrder)&&(v=k,y=L)}return[v,y]}_continueUndoInGroup(f,v){if(!!f){const[,y]=this._findClosestUndoElementInGroup(f);if(y)return this._undo(y,0,v)}}undo(f){if(f instanceof N.UndoRedoSource){const[,v]=this._findClosestUndoElementWithSource(f.id);return v?this._undo(v,f.id,!1):void 0}return typeof f=="string"?this._undo(f,0,!1):this._undo(this.getUriComparisonKey(f),0,!1)}_undo(f,v=0,y){if(!!this._editStacks.has(f)){const L=this._editStacks.get(f),I=L.getClosestPastElement();if(!!I){if(I.groupId){const[E,T]=this._findClosestUndoElementInGroup(I.groupId);if(I!==E&&T)return this._undo(T,v,y)}if((I.sourceId!==v||I.confirmBeforeUndo)&&!y)return this._confirmAndContinueUndo(f,v,I);try{return I.type===1?this._workspaceUndo(f,I,y):this._resourceUndo(L,I,y)}finally{c&&this._print("undo")}}}}_confirmAndContinueUndo(f,v,y){return Ie(this,void 0,void 0,function*(){if((yield this._dialogService.show(C.default.Info,b.localize(12,null,y.label),[b.localize(13,null),b.localize(14,null)],{cancelId:1})).choice!==1)return this._undo(f,v,!0)})}_findClosestRedoElementWithSource(f){if(!f)return[null,null];let v=null,y=null;for(const[L,I]of this._editStacks){const k=I.getClosestFutureElement();!k||k.sourceId===f&&(!v||k.sourceOrder0)return this._tryToSplitAndRedo(f,v,null,b.localize(17,null,v.label,I.join(", ")));const k=[];for(const E of y.editStacks)E.locked&&k.push(E.resourceLabel);return k.length>0?this._tryToSplitAndRedo(f,v,null,b.localize(18,null,v.label,k.join(", "))):y.isValid()?null:this._tryToSplitAndRedo(f,v,null,b.localize(19,null,v.label))}_workspaceRedo(f,v){const y=this._getAffectedEditStacks(v),L=this._checkWorkspaceRedo(f,v,y,!1);return L?L.returnValue:this._executeWorkspaceRedo(f,v,y)}_executeWorkspaceRedo(f,v,y){return Ie(this,void 0,void 0,function*(){let L;try{L=yield this._invokeWorkspacePrepare(v)}catch(k){return this._onError(k,v)}const I=this._checkWorkspaceRedo(f,v,y,!0);if(I)return L.dispose(),I.returnValue;for(const k of y.editStacks)k.moveForward(v);return this._safeInvokeWithLocks(v,()=>v.actual.redo(),y,L,()=>this._continueRedoInGroup(v.groupId))})}_resourceRedo(f,v){if(!v.isValid){f.flushAllElements();return}if(f.locked){const y=b.localize(20,null,v.label);this._notificationService.info(y);return}return this._invokeResourcePrepare(v,y=>(f.moveForward(v),this._safeInvokeWithLocks(v,()=>v.actual.redo(),new t([f]),y,()=>this._continueRedoInGroup(v.groupId))))}_findClosestRedoElementInGroup(f){if(!f)return[null,null];let v=null,y=null;for(const[L,I]of this._editStacks){const k=I.getClosestFutureElement();!k||k.groupId===f&&(!v||k.groupOrderthis.findModel(U,P),U=>this.findModel(U.getOriginalEditor(),P)||this.findModel(U.getModifiedEditor(),P))),V?Promise.resolve(new C.ImmortalReference(new k(V))):Promise.reject(new Error("Model not found"))}findModel(P,V){let U=this.modelService.getModel(V);return U&&U.uri.toString()!==V.toString()?null:U}};T=Me([_e(0,i.IModelService)],T),e.SimpleEditorModelResolverService=T;class O{show(){return O.NULL_PROGRESS_RUNNER}showWhile(P,V){return Ie(this,void 0,void 0,function*(){yield P})}}e.SimpleEditorProgressService=O,O.NULL_PROGRESS_RUNNER={done:()=>{},total:()=>{},worked:()=>{}};class A{confirm(P){return this.doConfirm(P).then(V=>({confirmed:V,checkboxChecked:!1}))}doConfirm(P){let V=P.message;return P.detail&&(V=V+` + +`+P.detail),Promise.resolve(window.confirm(V))}show(P,V,U,H){return Promise.resolve({choice:0})}}e.SimpleDialogService=A;class B{info(P){return this.notify({severity:g.default.Info,message:P})}warn(P){return this.notify({severity:g.default.Warning,message:P})}error(P){return this.notify({severity:g.default.Error,message:P})}notify(P){switch(P.severity){case g.default.Error:console.error(P.message);break;case g.default.Warning:console.warn(P.message);break;default:console.log(P.message);break}return B.NO_OP}status(P,V){return C.Disposable.None}}e.SimpleNotificationService=B,B.NO_OP=new y.NoOpNotification;class F{constructor(P){this._onWillExecuteCommand=new w.Emitter,this._onDidExecuteCommand=new w.Emitter,this._instantiationService=P}executeCommand(P,...V){const U=n.CommandsRegistry.getCommand(P);if(!U)return Promise.reject(new Error(`command '${P}' not found`));try{this._onWillExecuteCommand.fire({commandId:P,args:V});const H=this._instantiationService.invokeFunction.apply(this._instantiationService,[U.handler,...V]);return this._onDidExecuteCommand.fire({commandId:P,args:V}),Promise.resolve(H)}catch(H){return Promise.reject(H)}}}e.StandaloneCommandService=F;class D extends h.AbstractKeybindingService{constructor(P,V,U,H,$,ie){super(P,V,U,H,$);this._cachedResolver=null,this._dynamicKeybindings=[],this._register(N.addDisposableListener(ie,N.EventType.KEY_DOWN,oe=>{const ae=new M.StandardKeyboardEvent(oe);this._dispatch(ae,ae.target)&&(ae.preventDefault(),ae.stopPropagation())})),this._register(N.addDisposableListener(window,N.EventType.KEY_UP,oe=>{const ae=new M.StandardKeyboardEvent(oe);this._singleModifierDispatch(ae,ae.target)&&ae.preventDefault()}))}addDynamicKeybinding(P,V,U,H){const $=S.createKeybinding(V,d.OS),ie=new C.DisposableStore;return $&&(this._dynamicKeybindings.push({keybinding:$,command:P,when:H,weight1:1e3,weight2:0,extensionId:null,isBuiltinExtension:!1}),ie.add(C.toDisposable(()=>{for(let oe=0;oethis._log(U))}return this._cachedResolver}_documentHasFocus(){return document.hasFocus()}_toNormalizedKeybindingItems(P,V){let U=[],H=0;for(const $ of P){const ie=$.when||void 0,oe=$.keybinding;if(!oe)U[H++]=new f.ResolvedKeybindingItem(void 0,$.command,$.commandArgs,ie,V,null,!1);else{const ae=this.resolveKeybinding(oe);for(const G of ae)U[H++]=new f.ResolvedKeybindingItem(G,$.command,$.commandArgs,ie,V,null,!1)}}return U}resolveKeybinding(P){return[new v.USLayoutResolvedKeybinding(P,d.OS)]}resolveKeyboardEvent(P){let V=new S.SimpleKeybinding(P.ctrlKey,P.shiftKey,P.altKey,P.metaKey,P.keyCode).toChord();return new v.USLayoutResolvedKeybinding(V,d.OS)}}e.StandaloneKeybindingService=D;function R(z){return z&&typeof z=="object"&&(!z.overrideIdentifier||typeof z.overrideIdentifier=="string")&&(!z.resource||z.resource instanceof p.URI)}class W{constructor(){this._onDidChangeConfiguration=new w.Emitter,this.onDidChangeConfiguration=this._onDidChangeConfiguration.event,this._configuration=new l.Configuration(new l.DefaultConfigurationModel,new l.ConfigurationModel)}getValue(P,V){const U=typeof P=="string"?P:void 0,H=R(P)?P:R(V)?V:{};return this._configuration.getValue(U,H,void 0)}updateValues(P){const V={data:this._configuration.toData()};let U=[];for(const H of P){const[$,ie]=H;this.getValue($)!==ie&&(this._configuration.updateValue($,ie),U.push($))}if(U.length>0){const H=new l.ConfigurationChangeEvent({keys:U,overrides:[]},V,this._configuration);H.source=7,H.sourceConfig=null,this._onDidChangeConfiguration.fire(H)}return Promise.resolve()}}e.SimpleConfigurationService=W;class x{constructor(P){this.configurationService=P,this._onDidChangeConfiguration=new w.Emitter,this.configurationService.onDidChangeConfiguration(V=>{this._onDidChangeConfiguration.fire({affectedKeys:V.affectedKeys,affectsConfiguration:(U,H)=>V.affectsConfiguration(H)})})}getValue(P,V,U){const $=(u.Position.isIPosition(V)?V:null)?typeof U=="string"?U:void 0:typeof V=="string"?V:void 0;return typeof $=="undefined"?this.configurationService.getValue():this.configurationService.getValue($)}}e.SimpleResourceConfigurationService=x;let K=class{constructor(P){this.configurationService=P}getEOL(P,V){const U=this.configurationService.getValue("files.eol",{overrideIdentifier:V,resource:P});return U&&U!=="auto"?U:d.isLinux||d.isMacintosh?` +`:`\r +`}};K=Me([_e(0,t.IConfigurationService)],K),e.SimpleResourcePropertiesService=K;class Y{publicLog(P,V){return Promise.resolve(void 0)}publicLog2(P,V){return this.publicLog(P,V)}}e.StandaloneTelemetryService=Y;class ee{constructor(){const P=p.URI.from({scheme:ee.SCHEME,authority:"model",path:"/"});this.workspace={id:"4064f6ec-cb38-4ad0-af64-ee6467e63c82",folders:[new L.WorkspaceFolder({uri:P,name:"",index:0})]}}getWorkspace(){return this.workspace}}e.SimpleWorkspaceContextService=ee,ee.SCHEME="inmemory";function se(z,P,V){if(!!P&&z instanceof W){let U=[];Object.keys(P).forEach(H=>{s.isEditorConfigurationKey(H)&&U.push([`editor.${H}`,P[H]]),V&&s.isDiffEditorConfigurationKey(H)&&U.push([`diffEditor.${H}`,P[H]])}),U.length>0&&z.updateValues(U)}}e.updateConfigurationService=se;class ne{constructor(P){this._modelService=P}hasPreviewHandler(){return!1}apply(P,V){return Ie(this,void 0,void 0,function*(){const U=new Map;for(let ie of P){if(!(ie instanceof o.ResourceTextEdit))throw new Error("bad edit - only text edits are supported");const oe=this._modelService.getModel(ie.resource);if(!oe)throw new Error("bad edit - model not found");if(typeof ie.versionId=="number"&&oe.getVersionId()!==ie.versionId)throw new Error("bad state - model changed in the meantime");let ae=U.get(oe);ae||(ae=[],U.set(oe,ae)),ae.push(a.EditOperation.replaceMove(r.Range.lift(ie.textEdit.range),ie.textEdit.text))}let H=0,$=0;for(const[ie,oe]of U)ie.pushStackElement(),ie.pushEditOperations([],oe,()=>[]),ie.pushStackElement(),$+=1,H+=oe.length;return{ariaSummary:b.format(I.SimpleServicesNLS.bulkEditServiceSummary,H,$)}})}}e.SimpleBulkEditService=ne;class le{getUriLabel(P,V){return P.scheme==="file"?P.fsPath:P.path}}e.SimpleUriLabelService=le;class X{constructor(P,V){this._codeEditorService=P,this._container=V,this.onDidLayout=w.Event.None}get dimension(){return this._dimension||(this._dimension=N.getClientArea(window.document.body)),this._dimension}get container(){return this._container}focus(){var P;(P=this._codeEditorService.getFocusedCodeEditor())===null||P===void 0||P.focus()}}e.SimpleLayoutService=X}),define(Q[674],J([0,1,47,2,28,143,266,217,75,191,114,34,26,46,16,68,9,37,32,11,65,64,84,59,36,57]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createTextModel=e.StandaloneDiffEditor=e.StandaloneEditor=e.StandaloneCodeEditor=void 0;let y=0,L=!1;function I(){L||(L=!0,b.setARIAContainer(document.body))}let k=class extends w.CodeEditorWidget{constructor(F,D,R,W,x,K,Y,ee,se,ne){const le=Object.assign({},D);le.ariaLabel=le.ariaLabel||h.StandaloneCodeEditorNLS.editorViewAccessibleLabel,le.ariaLabel=le.ariaLabel+";"+h.StandaloneCodeEditorNLS.accessibilityHelpMessage,super(F,le,{},R,W,x,K,ee,se,ne),Y instanceof g.StandaloneKeybindingService?this._standaloneKeybindingService=Y:this._standaloneKeybindingService=null,I()}addCommand(F,D,R){if(!this._standaloneKeybindingService)return console.warn("Cannot add command because the editor is configured with an unrecognized KeybindingService"),null;let W="DYNAMIC_"+ ++y,x=a.ContextKeyExpr.deserialize(R);return this._standaloneKeybindingService.addDynamicKeybinding(W,F,D,x),W}createContextKey(F,D){return this._contextKeyService.createKey(F,D)}addAction(F){if(typeof F.id!="string"||typeof F.label!="string"||typeof F.run!="function")throw new Error("Invalid action descriptor, `id`, `label` and `run` are required properties!");if(!this._standaloneKeybindingService)return console.warn("Cannot add keybinding because the editor is configured with an unrecognized KeybindingService"),N.Disposable.None;const D=F.id,R=F.label,W=a.ContextKeyExpr.and(a.ContextKeyExpr.equals("editorId",this.getId()),a.ContextKeyExpr.deserialize(F.precondition)),x=F.keybindings,K=a.ContextKeyExpr.and(W,a.ContextKeyExpr.deserialize(F.keybindingContext)),Y=F.contextMenuGroupId||null,ee=F.contextMenuOrder||0,se=(z,...P)=>Promise.resolve(F.run(this,...P)),ne=new N.DisposableStore,le=this.getId()+":"+D;if(ne.add(o.CommandsRegistry.registerCommand(le,se)),Y){let z={command:{id:le,title:R},when:W,group:Y,order:ee};ne.add(c.MenuRegistry.appendMenuItem(c.MenuId.EditorContext,z))}if(Array.isArray(x))for(const z of x)ne.add(this._standaloneKeybindingService.addDynamicKeybinding(le,z,se,K));let X=new C.InternalEditorAction(le,R,R,W,se,this._contextKeyService);return this._actions[D]=X,ne.add(N.toDisposable(()=>{delete this._actions[D]})),ne}};k=Me([_e(2,r.IInstantiationService),_e(3,M.ICodeEditorService),_e(4,o.ICommandService),_e(5,a.IContextKeyService),_e(6,i.IKeybindingService),_e(7,t.IThemeService),_e(8,n.INotificationService),_e(9,l.IAccessibilityService)],k),e.StandaloneCodeEditor=k;let E=class extends k{constructor(F,D,R,W,x,K,Y,ee,se,ne,le,X,z,P,V){const U=Object.assign({},D);g.updateConfigurationService(X,U,!1);const H=ne.registerEditorContainer(F);typeof U.theme=="string"&&ne.setTheme(U.theme),typeof U.autoDetectHighContrast!="undefined"&&ne.setAutoDetectHighContrast(Boolean(U.autoDetectHighContrast));let $=U.model;delete U.model,super(F,U,W,x,K,Y,ee,ne,le,z),this._contextViewService=se,this._configurationService=X,this._standaloneThemeService=ne,this._register(R),this._register(H);let ie;if(typeof $=="undefined"?(ie=O(P,V,U.value||"",U.language||"text/plain",void 0),this._ownsModel=!0):(ie=$,this._ownsModel=!1),this._attachModel(ie),ie){let oe={oldModelUrl:null,newModelUrl:ie.uri};this._onDidChangeModel.fire(oe)}}dispose(){super.dispose()}updateOptions(F){g.updateConfigurationService(this._configurationService,F,!1),typeof F.theme=="string"&&this._standaloneThemeService.setTheme(F.theme),typeof F.autoDetectHighContrast!="undefined"&&this._standaloneThemeService.setAutoDetectHighContrast(Boolean(F.autoDetectHighContrast)),super.updateOptions(F)}_attachModel(F){super._attachModel(F),this._modelData&&this._contextViewService.setContainer(this._modelData.view.domNode.domNode)}_postDetachModelCleanup(F){super._postDetachModelCleanup(F),F&&this._ownsModel&&(F.dispose(),this._ownsModel=!1)}};E=Me([_e(3,r.IInstantiationService),_e(4,M.ICodeEditorService),_e(5,o.ICommandService),_e(6,a.IContextKeyService),_e(7,i.IKeybindingService),_e(8,u.IContextViewService),_e(9,p.IStandaloneThemeService),_e(10,n.INotificationService),_e(11,s.IConfigurationService),_e(12,l.IAccessibilityService),_e(13,f.IModelService),_e(14,v.IModeService)],E),e.StandaloneEditor=E;let T=class extends S.DiffEditorWidget{constructor(F,D,R,W,x,K,Y,ee,se,ne,le,X,z,P,V){const U=Object.assign({},D);g.updateConfigurationService(X,U,!0);const H=ne.registerEditorContainer(F);typeof U.theme=="string"&&ne.setTheme(U.theme),typeof U.autoDetectHighContrast!="undefined"&&ne.setAutoDetectHighContrast(Boolean(U.autoDetectHighContrast)),super(F,U,{},V,ee,x,W,se,ne,le,z,P),this._contextViewService=Y,this._configurationService=X,this._standaloneThemeService=ne,this._register(R),this._register(H),this._contextViewService.setContainer(this._containerDomElement)}dispose(){super.dispose()}updateOptions(F){g.updateConfigurationService(this._configurationService,F,!0),typeof F.theme=="string"&&this._standaloneThemeService.setTheme(F.theme),typeof F.autoDetectHighContrast!="undefined"&&this._standaloneThemeService.setAutoDetectHighContrast(Boolean(F.autoDetectHighContrast)),super.updateOptions(F)}_createInnerEditor(F,D,R){return F.createInstance(k,D,R)}getOriginalEditor(){return super.getOriginalEditor()}getModifiedEditor(){return super.getModifiedEditor()}addCommand(F,D,R){return this.getModifiedEditor().addCommand(F,D,R)}createContextKey(F,D){return this.getModifiedEditor().createContextKey(F,D)}addAction(F){return this.getModifiedEditor().addAction(F)}};T=Me([_e(3,r.IInstantiationService),_e(4,a.IContextKeyService),_e(5,i.IKeybindingService),_e(6,u.IContextViewService),_e(7,d.IEditorWorkerService),_e(8,M.ICodeEditorService),_e(9,p.IStandaloneThemeService),_e(10,n.INotificationService),_e(11,s.IConfigurationService),_e(12,u.IContextMenuService),_e(13,_.IEditorProgressService),_e(14,m.IClipboardService)],T),e.StandaloneDiffEditor=T;function O(B,F,D,R,W){if(D=D||"",!R){const x=D.indexOf(` +`);let K=D;return x!==-1&&(K=D.substring(0,x)),A(B,D,F.createByFilepathOrFirstLine(W||null,K),W)}return A(B,D,F.create(R),W)}e.createTextModel=O;function A(B,F,D,R){return B.createModel(F,D,R)}}),define(Q[274],J([0,1,2,133,28,75,252,57,567,36,269,137,191,605,668,114,34,26,46,570,16,651,68,555,184,9,550,138,37,115,147,77,558,85,32,59,79,87,11,190,650,178,594,65,139,74,568,84,530,148,673,653,78]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y,L,I,k,E,T,O,A,B,F,D,R,W,x,K,Y,ee,se,ne,le,X,z,P,V,U,H,$){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.DynamicStandaloneServices=e.StaticServices=void 0;var ie;(function(ae){const G=new L.ServiceCollection;class j{constructor(ce,me){this._serviceId=ce,this._factory=me,this._value=null}get id(){return this._serviceId}get(ce){if(!this._value){if(ce&&(this._value=ce[this._serviceId.toString()]),this._value||(this._value=this._factory(ce)),!this._value)throw new Error("Service "+this._serviceId+" is missing!");G.set(this._serviceId,this._value)}return this._value}}ae.LazyStaticService=j;let te=[];function Z(re,ce){let me=new j(re,ce);return te.push(me),me}function ue(re){let ce=new L.ServiceCollection;for(const[Ce,be]of le.getSingletonServiceDescriptors())ce.set(Ce,be);for(let Ce in re)re.hasOwnProperty(Ce)&&ce.set(v.createDecorator(Ce),re[Ce]);te.forEach(Ce=>ce.set(Ce.id,Ce.get(re)));let me=new y.InstantiationService(ce,!0);return ce.set(v.IInstantiationService,me),[ce,me]}ae.init=ue,ae.instantiationService=Z(v.IInstantiationService,()=>new y.InstantiationService(G,!0));const he=new o.SimpleConfigurationService;ae.configurationService=Z(n.IConfigurationService,()=>he),ae.resourceConfigurationService=Z(c.ITextResourceConfigurationService,()=>new o.SimpleResourceConfigurationService(he)),ae.resourcePropertiesService=Z(c.ITextResourcePropertiesService,()=>new o.SimpleResourcePropertiesService(he)),ae.contextService=Z(x.IWorkspaceContextService,()=>new o.SimpleWorkspaceContextService),ae.labelService=Z(k.ILabelService,()=>new o.SimpleUriLabelService),ae.telemetryService=Z(R.ITelemetryService,()=>new o.StandaloneTelemetryService),ae.dialogService=Z(f.IDialogService,()=>new o.SimpleDialogService),ae.notificationService=Z(B.INotificationService,()=>new o.SimpleNotificationService),ae.markerService=Z(A.IMarkerService,()=>new O.MarkerService),ae.modeService=Z(C.IModeService,re=>new d.ModeServiceImpl),ae.standaloneThemeService=Z(u.IStandaloneThemeService,()=>new a.StandaloneThemeServiceImpl),ae.logService=Z(T.ILogService,()=>new T.LogService(new T.ConsoleLogger)),ae.undoRedoService=Z(V.IUndoRedoService,re=>new U.UndoRedoService(ae.dialogService.get(re),ae.notificationService.get(re))),ae.modelService=Z(g.IModelService,re=>new p.ModelServiceImpl(ae.configurationService.get(re),ae.resourcePropertiesService.get(re),ae.standaloneThemeService.get(re),ae.logService.get(re),ae.undoRedoService.get(re))),ae.markerDecorationsService=Z(Y.IMarkerDecorationsService,re=>new ee.MarkerDecorationsService(ae.modelService.get(re),ae.markerService.get(re))),ae.contextKeyService=Z(l.IContextKeyService,re=>new t.ContextKeyService(ae.configurationService.get(re))),ae.codeEditorService=Z(M.ICodeEditorService,re=>new s.StandaloneCodeEditorServiceImpl(null,ae.contextKeyService.get(re),ae.standaloneThemeService.get(re))),ae.editorProgressService=Z(F.IEditorProgressService,()=>new o.SimpleEditorProgressService),ae.storageService=Z(D.IStorageService,()=>new D.InMemoryStorageService),ae.editorWorkerService=Z(w.IEditorWorkerService,re=>new S.EditorWorkerServiceImpl(ae.modelService.get(re),ae.resourceConfigurationService.get(re),ae.logService.get(re)))})(ie=e.StaticServices||(e.StaticServices={}));class oe extends b.Disposable{constructor(G,j){super();const[te,Z]=ie.init(j);this._serviceCollection=te,this._instantiationService=Z;const ue=this.get(n.IConfigurationService),he=this.get(B.INotificationService),re=this.get(R.ITelemetryService),ce=this.get(W.IThemeService),me=this.get(T.ILogService),Ce=this.get(l.IContextKeyService);let be=(Ae,Se)=>{let we=null;return j&&(we=j[Ae.toString()]),we||(we=Se()),this._serviceCollection.set(Ae,we),we};be(se.IAccessibilityService,()=>new X.AccessibilityService(Ce,ue)),be(E.IListService,()=>new E.ListService(ce));let Le=be(i.ICommandService,()=>new o.StandaloneCommandService(this._instantiationService)),De=be(I.IKeybindingService,()=>this._register(new o.StandaloneKeybindingService(Ce,Le,re,he,me,G))),Re=be(ne.ILayoutService,()=>new o.SimpleLayoutService(ie.codeEditorService.get(M.ICodeEditorService),G));be($.IQuickInputService,()=>new H.StandaloneQuickInputServiceImpl(Z,ie.codeEditorService.get(M.ICodeEditorService)));let Ee=be(m.IContextViewService,()=>this._register(new _.ContextViewService(Re)));be(z.IClipboardService,()=>new P.BrowserClipboardService),be(m.IContextMenuService,()=>{const Ae=new h.ContextMenuService(re,he,Ee,De,ce);return Ae.configure({blockMouse:!1}),this._register(Ae)}),be(r.IMenuService,()=>new K.MenuService(Le)),be(N.IBulkEditService,()=>new o.SimpleBulkEditService(ie.modelService.get(g.IModelService)))}get(G){let j=this._serviceCollection.get(G);if(!j)throw new Error("Missing service "+G);return j}set(G,j){this._serviceCollection.set(G,j)}has(G){return this._serviceCollection.has(G)}}e.DynamicStandaloneServices=oe}),define(Q[675],J([0,1,28,559,215,38,163,107,53,18,76,75,57,67,556,169,544,191,674,274,114,26,46,16,68,9,37,32,58,65,69,59,84,8,36,358]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y,L,I,k,E,T,O,A,B){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.createMonacoEditorAPI=e.registerCommand=e.remeasureFonts=e.setTheme=e.defineTheme=e.tokenize=e.colorizeModelLine=e.colorize=e.colorizeElement=e.createWebWorker=e.onDidChangeModelLanguage=e.onWillDisposeModel=e.onDidCreateModel=e.getModels=e.getModel=e.onDidChangeMarkers=e.getModelMarkers=e.setModelMarkers=e.setModelLanguage=e.createModel=e.createDiffNavigator=e.createDiffEditor=e.onDidCreateEditor=e.create=void 0;function F(he,re,ce){let me=new t.DynamicStandaloneServices(he,re),Ce=null;me.has(s.ITextModelService)||(Ce=new i.SimpleEditorModelResolverService(t.StaticServices.modelService.get()),me.set(s.ITextModelService,Ce)),me.has(I.IOpenerService)||me.set(I.IOpenerService,new N.OpenerService(me.get(b.ICodeEditorService),me.get(h.ICommandService)));let be=ce(me);return Ce&&Ce.setEditor(be),be}function D(he,re,ce){return F(he,ce||{},me=>new n.StandaloneEditor(he,re,me,me.get(v.IInstantiationService),me.get(b.ICodeEditorService),me.get(h.ICommandService),me.get(_.IContextKeyService),me.get(y.IKeybindingService),me.get(f.IContextViewService),me.get(l.IStandaloneThemeService),me.get(L.INotificationService),me.get(m.IConfigurationService),me.get(k.IAccessibilityService),me.get(B.IModelService),me.get(o.IModeService)))}e.create=D;function R(he){return t.StaticServices.codeEditorService.get().onCodeEditorAdd(re=>{he(re)})}e.onDidCreateEditor=R;function W(he,re,ce){return F(he,ce||{},me=>new n.StandaloneDiffEditor(he,re,me,me.get(v.IInstantiationService),me.get(_.IContextKeyService),me.get(y.IKeybindingService),me.get(f.IContextViewService),me.get(c.IEditorWorkerService),me.get(b.ICodeEditorService),me.get(l.IStandaloneThemeService),me.get(L.INotificationService),me.get(m.IConfigurationService),me.get(f.IContextMenuService),me.get(T.IEditorProgressService),me.get(O.IClipboardService)))}e.createDiffEditor=W;function x(he,re){return new M.DiffNavigator(he,re)}e.createDiffNavigator=x;function K(he,re,ce){return n.createTextModel(t.StaticServices.modelService.get(),t.StaticServices.modeService.get(),he,re,ce)}e.createModel=K;function Y(he,re){t.StaticServices.modelService.get().setMode(he,t.StaticServices.modeService.get().create(re))}e.setModelLanguage=Y;function ee(he,re,ce){he&&t.StaticServices.markerService.get().changeOne(re,he.uri,ce)}e.setModelMarkers=ee;function se(he){return t.StaticServices.markerService.get().read(he)}e.getModelMarkers=se;function ne(he){return t.StaticServices.markerService.get().onMarkerChanged(he)}e.onDidChangeMarkers=ne;function le(he){return t.StaticServices.modelService.get().getModel(he)}e.getModel=le;function X(){return t.StaticServices.modelService.get().getModels()}e.getModels=X;function z(he){return t.StaticServices.modelService.get().onModelAdded(he)}e.onDidCreateModel=z;function P(he){return t.StaticServices.modelService.get().onModelRemoved(he)}e.onWillDisposeModel=P;function V(he){return t.StaticServices.modelService.get().onModelModeChanged(re=>{he({model:re.model,oldLanguage:re.oldModeId})})}e.onDidChangeModelLanguage=V;function U(he){return a.createWebWorker(t.StaticServices.modelService.get(),he)}e.createWebWorker=U;function H(he,re){const ce=t.StaticServices.standaloneThemeService.get();return ce.registerEditorContainer(he),r.Colorizer.colorizeElement(ce,t.StaticServices.modeService.get(),he,re)}e.colorizeElement=H;function $(he,re,ce){return t.StaticServices.standaloneThemeService.get().registerEditorContainer(document.body),r.Colorizer.colorize(t.StaticServices.modeService.get(),he,re,ce)}e.colorize=$;function ie(he,re,ce=4){return t.StaticServices.standaloneThemeService.get().registerEditorContainer(document.body),r.Colorizer.colorizeModelLine(he,re,ce)}e.colorizeModelLine=ie;function oe(he){let re=g.TokenizationRegistry.get(he);return re||{getInitialState:()=>p.NULL_STATE,tokenize:(ce,me,Ce,be)=>p.nullTokenize(he,ce,Ce,be)}}function ae(he,re){t.StaticServices.modeService.get().triggerMode(re);let me=oe(re),Ce=A.splitLines(he),be=[],Le=me.getInitialState();for(let De=0,Re=Ce.length;De{H.getId()===P&&(U.dispose(),V())});return U}e.onLanguage=u;function r(P,V){let U=g.StaticServices.modeService.get().getLanguageIdentifier(P);if(!U)throw new Error(`Cannot set configuration for unknown language ${P}`);return S.LanguageConfigurationRegistry.register(U,V,100)}e.setLanguageConfiguration=r;class i{constructor(V,U){this._languageIdentifier=V,this._actual=U}getInitialState(){return this._actual.getInitialState()}tokenize(V,U,H,$){if(typeof this._actual.tokenize=="function")return n.adaptTokenize(this._languageIdentifier.language,this._actual,V,H,$);throw new Error("Not supported!")}tokenize2(V,U,H){let $=this._actual.tokenizeEncoded(V,H);return new M.TokenizationResult2($.tokens,$.endState)}}e.EncodedTokenizationSupport2Adapter=i;class n{constructor(V,U,H){this._standaloneThemeService=V,this._languageIdentifier=U,this._actual=H}getInitialState(){return this._actual.getInitialState()}static _toClassicTokens(V,U,H){let $=[],ie=0;for(let oe=0,ae=V.length;oe0&&ie[oe-1]===ue)){let he=Z.startIndex;j===0?he=0:het($)?new i(U,$):new n(g.StaticServices.standaloneThemeService.get(),U,$);return l(V)?w.TokenizationRegistry.registerPromise(P,V.then($=>H($))):w.TokenizationRegistry.register(P,H(V))}e.setTokensProvider=m;function _(P,V){const U=H=>c.createTokenizationSupport(g.StaticServices.modeService.get(),g.StaticServices.standaloneThemeService.get(),P,p.compile(P,H));return l(V)?w.TokenizationRegistry.registerPromise(P,V.then(H=>U(H))):w.TokenizationRegistry.register(P,U(V))}e.setMonarchTokensProvider=_;function f(P,V){return w.ReferenceProviderRegistry.register(P,V)}e.registerReferenceProvider=f;function v(P,V){return w.RenameProviderRegistry.register(P,V)}e.registerRenameProvider=v;function y(P,V){return w.SignatureHelpProviderRegistry.register(P,V)}e.registerSignatureHelpProvider=y;function L(P,V){return w.HoverProviderRegistry.register(P,{provideHover:(U,H,$)=>{let ie=U.getWordAtPosition(H);return Promise.resolve(V.provideHover(U,H,$)).then(oe=>{if(!!oe)return!oe.range&&ie&&(oe.range=new N.Range(H.lineNumber,ie.startColumn,H.lineNumber,ie.endColumn)),oe.range||(oe.range=new N.Range(H.lineNumber,H.column,H.lineNumber,H.column)),oe})}})}e.registerHoverProvider=L;function I(P,V){return w.DocumentSymbolProviderRegistry.register(P,V)}e.registerDocumentSymbolProvider=I;function k(P,V){return w.DocumentHighlightProviderRegistry.register(P,V)}e.registerDocumentHighlightProvider=k;function E(P,V){return w.LinkedEditingRangeProviderRegistry.register(P,V)}e.registerLinkedEditingRangeProvider=E;function T(P,V){return w.DefinitionProviderRegistry.register(P,V)}e.registerDefinitionProvider=T;function O(P,V){return w.ImplementationProviderRegistry.register(P,V)}e.registerImplementationProvider=O;function A(P,V){return w.TypeDefinitionProviderRegistry.register(P,V)}e.registerTypeDefinitionProvider=A;function B(P,V){return w.CodeLensProviderRegistry.register(P,V)}e.registerCodeLensProvider=B;function F(P,V){return w.CodeActionProviderRegistry.register(P,{provideCodeActions:(U,H,$,ie)=>{let oe=g.StaticServices.markerService.get().read({resource:U.uri}).filter(ae=>N.Range.areIntersectingOrTouching(ae,H));return V.provideCodeActions(U,H,{markers:oe,only:$.only},ie)}})}e.registerCodeActionProvider=F;function D(P,V){return w.DocumentFormattingEditProviderRegistry.register(P,V)}e.registerDocumentFormattingEditProvider=D;function R(P,V){return w.DocumentRangeFormattingEditProviderRegistry.register(P,V)}e.registerDocumentRangeFormattingEditProvider=R;function W(P,V){return w.OnTypeFormattingEditProviderRegistry.register(P,V)}e.registerOnTypeFormattingEditProvider=W;function x(P,V){return w.LinkProviderRegistry.register(P,V)}e.registerLinkProvider=x;function K(P,V){return w.CompletionProviderRegistry.register(P,V)}e.registerCompletionItemProvider=K;function Y(P,V){return w.ColorProviderRegistry.register(P,V)}e.registerColorProvider=Y;function ee(P,V){return w.FoldingRangeProviderRegistry.register(P,V)}e.registerFoldingRangeProvider=ee;function se(P,V){return w.DeclarationProviderRegistry.register(P,V)}e.registerDeclarationProvider=se;function ne(P,V){return w.SelectionRangeRegistry.register(P,V)}e.registerSelectionRangeProvider=ne;function le(P,V){return w.DocumentSemanticTokensProviderRegistry.register(P,V)}e.registerDocumentSemanticTokensProvider=le;function X(P,V){return w.DocumentRangeSemanticTokensProviderRegistry.register(P,V)}e.registerDocumentRangeSemanticTokensProvider=X;function z(){return{register:o,getLanguages:s,onLanguage:u,getEncodedLanguageId:a,setLanguageConfiguration:r,setColorMap:h,setTokensProvider:m,setMonarchTokensProvider:_,registerReferenceProvider:f,registerRenameProvider:v,registerCompletionItemProvider:K,registerSignatureHelpProvider:y,registerHoverProvider:L,registerDocumentSymbolProvider:I,registerDocumentHighlightProvider:k,registerLinkedEditingRangeProvider:E,registerDefinitionProvider:T,registerImplementationProvider:O,registerTypeDefinitionProvider:A,registerCodeLensProvider:B,registerCodeActionProvider:F,registerDocumentFormattingEditProvider:D,registerDocumentRangeFormattingEditProvider:R,registerOnTypeFormattingEditProvider:W,registerLinkProvider:x,registerColorProvider:Y,registerFoldingRangeProvider:ee,registerDeclarationProvider:se,registerSelectionRangeProvider:ne,registerDocumentSemanticTokensProvider:le,registerDocumentRangeSemanticTokensProvider:X,DocumentHighlightKind:d.DocumentHighlightKind,CompletionItemKind:d.CompletionItemKind,CompletionItemTag:d.CompletionItemTag,CompletionItemInsertTextRule:d.CompletionItemInsertTextRule,SymbolKind:d.SymbolKind,SymbolTag:d.SymbolTag,IndentAction:d.IndentAction,CompletionTriggerKind:d.CompletionTriggerKind,SignatureHelpTriggerKind:d.SignatureHelpTriggerKind,InlineHintKind:d.InlineHintKind,FoldingRangeKind:w.FoldingRangeKind}}e.createMonacoLanguagesAPI=z}),define(Q[677],J([0,1,38,220,675,676,17,261]),function(q,e,b,N,M,w,S,C){"use strict";var d;Object.defineProperty(e,"__esModule",{value:!0}),e.languages=e.editor=e.Token=e.Uri=e.MarkerTag=e.MarkerSeverity=e.SelectionDirection=e.Selection=e.Range=e.Position=e.KeyMod=e.KeyCode=e.Emitter=e.CancellationTokenSource=void 0,b.EditorOptions.wrappingIndent.defaultValue=0,b.EditorOptions.glyphMargin.defaultValue=!1,b.EditorOptions.autoIndent.defaultValue=3,b.EditorOptions.overviewRulerLanes.defaultValue=2,C.FormattingConflicts.setFormatterSelector((p,c,o)=>Promise.resolve(p[0]));const g=N.createMonacoBaseAPI();g.editor=M.createMonacoEditorAPI(),g.languages=w.createMonacoLanguagesAPI(),e.CancellationTokenSource=g.CancellationTokenSource,e.Emitter=g.Emitter,e.KeyCode=g.KeyCode,e.KeyMod=g.KeyMod,e.Position=g.Position,e.Range=g.Range,e.Selection=g.Selection,e.SelectionDirection=g.SelectionDirection,e.MarkerSeverity=g.MarkerSeverity,e.MarkerTag=g.MarkerTag,e.Uri=g.Uri,e.Token=g.Token,e.editor=g.editor,e.languages=g.languages,(((d=S.globals.MonacoEnvironment)===null||d===void 0?void 0:d.globalAPI)||typeof define=="function"&&define.amd)&&(self.monaco=g),typeof self.require!="undefined"&&typeof self.require.config=="function"&&self.require.config({ignoreDuplicateModules:["vscode-languageserver-types","vscode-languageserver-types/main","vscode-languageserver-textdocument","vscode-languageserver-textdocument/main","vscode-nls","vscode-nls/vscode-nls","jsonc-parser","jsonc-parser/main","vscode-uri","vscode-uri/index","vs/basic-languages/typescript/typescript"]})}),define(Q[678],J([0,1,24]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.toWorkspaceIdentifier=e.isSingleFolderWorkspaceIdentifier=e.WORKSPACE_EXTENSION=void 0,e.WORKSPACE_EXTENSION="code-workspace";function N(w){const S=w;return typeof(S==null?void 0:S.id)=="string"&&b.URI.isUri(S.uri)}e.isSingleFolderWorkspaceIdentifier=N;function M(w){if(w.configuration)return{id:w.id,configPath:w.configuration};if(w.folders.length===1)return{id:w.id,uri:w.folders[0].uri}}e.toWorkspaceIdentifier=M}),define(Q[679],J([0,1,506,72,44,131,41,8,678,159,292]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.RandomBasedVariableResolver=e.WorkspaceBasedVariableResolver=e.TimeBasedVariableResolver=e.CommentBasedVariableResolver=e.ClipboardBasedVariableResolver=e.ModelBasedVariableResolver=e.SelectionBasedVariableResolver=e.CompositeSnippetVariableResolver=void 0;class c{constructor(l){this._delegates=l}resolve(l){for(const h of this._delegates){let m=h.resolve(l);if(m!==void 0)return m}}}e.CompositeSnippetVariableResolver=c;class o{constructor(l,h,m,_){this._model=l,this._selection=h,this._selectionIdx=m,this._overtypingCapturer=_}resolve(l){const{name:h}=l;if(h==="SELECTION"||h==="TM_SELECTED_TEXT"){let m=this._model.getValueInRange(this._selection)||void 0,_=this._selection.startLineNumber!==this._selection.endLineNumber;if(!m&&this._overtypingCapturer){const f=this._overtypingCapturer.getLastOvertypedInfo(this._selectionIdx);f&&(m=f.value,_=f.multiline)}if(m&&_&&l.snippet){const f=this._model.getLineContent(this._selection.startLineNumber),v=C.getLeadingWhitespace(f,0,this._selection.startColumn-1);let y=v;l.snippet.walk(I=>I===l?!1:(I instanceof w.Text&&(y=C.getLeadingWhitespace(C.splitLines(I.value).pop())),!0));const L=C.commonPrefixLength(y,v);m=m.replace(/(\r\n|\r|\n)(.*)/g,(I,k,E)=>`${k}${y.substr(L)}${E}`)}return m}else{if(h==="TM_CURRENT_LINE")return this._model.getLineContent(this._selection.positionLineNumber);if(h==="TM_CURRENT_WORD"){const m=this._model.getWordAtPosition({lineNumber:this._selection.positionLineNumber,column:this._selection.positionColumn});return m&&m.word||void 0}else{if(h==="TM_LINE_INDEX")return String(this._selection.positionLineNumber-1);if(h==="TM_LINE_NUMBER")return String(this._selection.positionLineNumber)}}}}e.SelectionBasedVariableResolver=o;class s{constructor(l,h){this._labelService=l,this._model=h}resolve(l){const{name:h}=l;if(h==="TM_FILENAME")return N.basename(this._model.uri.fsPath);if(h==="TM_FILENAME_BASE"){const m=N.basename(this._model.uri.fsPath),_=m.lastIndexOf(".");return _<=0?m:m.slice(0,_)}else{if(h==="TM_DIRECTORY"&&this._labelService)return N.dirname(this._model.uri.fsPath)==="."?"":this._labelService.getUriLabel(M.dirname(this._model.uri));if(h==="TM_FILEPATH"&&this._labelService)return this._labelService.getUriLabel(this._model.uri);if(h==="RELATIVE_FILEPATH"&&this._labelService)return this._labelService.getUriLabel(this._model.uri,{relative:!0,noPrefix:!0})}}}e.ModelBasedVariableResolver=s;class a{constructor(l,h,m,_){this._readClipboardText=l,this._selectionIdx=h,this._selectionCount=m,this._spread=_}resolve(l){if(l.name==="CLIPBOARD"){const h=this._readClipboardText();if(!!h){if(this._spread){const m=h.split(/\r\n|\n|\r/).filter(_=>!C.isFalsyOrWhitespace(_));if(m.length===this._selectionCount)return m[this._selectionIdx]}return h}}}}e.ClipboardBasedVariableResolver=a;class u{constructor(l,h){this._model=l,this._selection=h}resolve(l){const{name:h}=l,m=this._model.getLanguageIdAtPosition(this._selection.selectionStartLineNumber,this._selection.selectionStartColumn),_=S.LanguageConfigurationRegistry.getComments(m);if(!!_){if(h==="LINE_COMMENT")return _.lineCommentToken||void 0;if(h==="BLOCK_COMMENT_START")return _.blockCommentStartToken||void 0;if(h==="BLOCK_COMMENT_END")return _.blockCommentEndToken||void 0}}}e.CommentBasedVariableResolver=u;class r{resolve(l){const{name:h}=l;if(h==="CURRENT_YEAR")return String(new Date().getFullYear());if(h==="CURRENT_YEAR_SHORT")return String(new Date().getFullYear()).slice(-2);if(h==="CURRENT_MONTH")return String(new Date().getMonth().valueOf()+1).padStart(2,"0");if(h==="CURRENT_DATE")return String(new Date().getDate().valueOf()).padStart(2,"0");if(h==="CURRENT_HOUR")return String(new Date().getHours().valueOf()).padStart(2,"0");if(h==="CURRENT_MINUTE")return String(new Date().getMinutes().valueOf()).padStart(2,"0");if(h==="CURRENT_SECOND")return String(new Date().getSeconds().valueOf()).padStart(2,"0");if(h==="CURRENT_DAY_NAME")return r.dayNames[new Date().getDay()];if(h==="CURRENT_DAY_NAME_SHORT")return r.dayNamesShort[new Date().getDay()];if(h==="CURRENT_MONTH_NAME")return r.monthNames[new Date().getMonth()];if(h==="CURRENT_MONTH_NAME_SHORT")return r.monthNamesShort[new Date().getMonth()];if(h==="CURRENT_SECONDS_UNIX")return String(Math.floor(Date.now()/1e3))}}e.TimeBasedVariableResolver=r,r.dayNames=[b.localize(0,null),b.localize(1,null),b.localize(2,null),b.localize(3,null),b.localize(4,null),b.localize(5,null),b.localize(6,null)],r.dayNamesShort=[b.localize(7,null),b.localize(8,null),b.localize(9,null),b.localize(10,null),b.localize(11,null),b.localize(12,null),b.localize(13,null)],r.monthNames=[b.localize(14,null),b.localize(15,null),b.localize(16,null),b.localize(17,null),b.localize(18,null),b.localize(19,null),b.localize(20,null),b.localize(21,null),b.localize(22,null),b.localize(23,null),b.localize(24,null),b.localize(25,null)],r.monthNamesShort=[b.localize(26,null),b.localize(27,null),b.localize(28,null),b.localize(29,null),b.localize(30,null),b.localize(31,null),b.localize(32,null),b.localize(33,null),b.localize(34,null),b.localize(35,null),b.localize(36,null),b.localize(37,null)];class i{constructor(l){this._workspaceService=l}resolve(l){if(!!this._workspaceService){const h=d.toWorkspaceIdentifier(this._workspaceService.getWorkspace());if(!!h){if(l.name==="WORKSPACE_NAME")return this._resolveWorkspaceName(h);if(l.name==="WORKSPACE_FOLDER")return this._resoveWorkspacePath(h)}}}_resolveWorkspaceName(l){if(d.isSingleFolderWorkspaceIdentifier(l))return N.basename(l.uri.path);let h=N.basename(l.configPath.path);return h.endsWith(d.WORKSPACE_EXTENSION)&&(h=h.substr(0,h.length-d.WORKSPACE_EXTENSION.length-1)),h}_resoveWorkspacePath(l){if(d.isSingleFolderWorkspaceIdentifier(l))return g.normalizeDriveLetter(l.uri.fsPath);let h=N.basename(l.configPath.path),m=l.configPath.fsPath;return m.endsWith(h)&&(m=m.substr(0,m.length-h.length-1)),m?g.normalizeDriveLetter(m):"/"}}e.WorkspaceBasedVariableResolver=i;class n{resolve(l){const{name:h}=l;if(h==="RANDOM")return Math.random().toString().slice(-6);if(h==="RANDOM_HEX")return Math.random().toString(16).slice(-6);if(h==="UUID")return p.generateUuid()}}e.RandomBasedVariableResolver=n}),define(Q[680],J([0,1,19,2,8,62,3,21,31,190,9,131,679,11,22,115,351]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SnippetSession=e.OneSnippet=void 0,s.registerThemingParticipant((t,l)=>{function h(m){const _=t.getColor(m);return _?_.toString():"transparent"}l.addRule(`.monaco-editor .snippet-placeholder { background-color: ${h(a.snippetTabstopHighlightBackground)}; outline-color: ${h(a.snippetTabstopHighlightBorder)}; }`),l.addRule(`.monaco-editor .finish-snippet-placeholder { background-color: ${h(a.snippetFinalTabstopHighlightBackground)}; outline-color: ${h(a.snippetFinalTabstopHighlightBorder)}; }`)});class r{constructor(l,h,m,_){this._editor=l,this._snippet=h,this._offset=m,this._snippetLineLeadingWhitespace=_,this._nestingLevel=1,this._placeholderGroups=b.groupBy(h.placeholders,c.Placeholder.compareByIndex),this._placeholderGroupsIdx=-1}dispose(){this._placeholderDecorations&&this._editor.deltaDecorations([...this._placeholderDecorations.values()],[]),this._placeholderGroups.length=0}_initDecorations(){if(!this._placeholderDecorations){this._placeholderDecorations=new Map;const l=this._editor.getModel();this._editor.changeDecorations(h=>{for(const m of this._snippet.placeholders){const _=this._snippet.offset(m),f=this._snippet.fullLen(m),v=S.Range.fromPositions(l.getPositionAt(this._offset+_),l.getPositionAt(this._offset+_+f)),y=m.isFinalTabstop?r._decor.inactiveFinal:r._decor.inactive,L=h.addDecoration(v,y);this._placeholderDecorations.set(m,L)}})}}move(l){if(!this._editor.hasModel())return[];if(this._initDecorations(),this._placeholderGroupsIdx>=0){let _=[];for(const f of this._placeholderGroups[this._placeholderGroupsIdx])if(f.transform){const v=this._placeholderDecorations.get(f),y=this._editor.getModel().getDecorationRange(v),L=this._editor.getModel().getValueInRange(y),I=f.transform.resolve(L).split(/\r\n|\r|\n/);for(let k=1;k0&&this._editor.executeEdits("snippet.placeholderTransform",_)}let h=!1;l===!0&&this._placeholderGroupsIdx0&&(this._placeholderGroupsIdx-=1,h=!0);const m=this._editor.getModel().changeDecorations(_=>{const f=new Set,v=[];for(const y of this._placeholderGroups[this._placeholderGroupsIdx]){const L=this._placeholderDecorations.get(y),I=this._editor.getModel().getDecorationRange(L);v.push(new C.Selection(I.startLineNumber,I.startColumn,I.endLineNumber,I.endColumn)),h=h&&this._hasPlaceholderBeenCollapsed(y),_.changeDecorationOptions(L,y.isFinalTabstop?r._decor.activeFinal:r._decor.active),f.add(y);for(const k of this._snippet.enclosingPlaceholders(y)){const E=this._placeholderDecorations.get(k);_.changeDecorationOptions(E,k.isFinalTabstop?r._decor.activeFinal:r._decor.active),f.add(k)}}for(const[y,L]of this._placeholderDecorations)f.has(y)||_.changeDecorationOptions(L,y.isFinalTabstop?r._decor.inactiveFinal:r._decor.inactive);return v});return h?this.move(l):m!=null?m:[]}_hasPlaceholderBeenCollapsed(l){let h=l;for(;h;){if(h instanceof c.Placeholder){const m=this._placeholderDecorations.get(h);if(this._editor.getModel().getDecorationRange(m).isEmpty()&&h.toString().length>0)return!0}h=h.parent}return!1}get isAtFirstPlaceholder(){return this._placeholderGroupsIdx<=0||this._placeholderGroups.length===0}get isAtLastPlaceholder(){return this._placeholderGroupsIdx===this._placeholderGroups.length-1}get hasPlaceholder(){return this._snippet.placeholders.length>0}computePossibleSelections(){const l=new Map;for(const h of this._placeholderGroups){let m;for(const _ of h){if(_.isFinalTabstop)break;m||(m=[],l.set(_.index,m));const f=this._placeholderDecorations.get(_),v=this._editor.getModel().getDecorationRange(f);if(!v){l.delete(_.index);break}m.push(v)}}return l}get choice(){return this._placeholderGroups[this._placeholderGroupsIdx][0].choice}merge(l){const h=this._editor.getModel();this._nestingLevel*=10,this._editor.changeDecorations(m=>{for(const _ of this._placeholderGroups[this._placeholderGroupsIdx]){const f=l.shift();console.assert(!f._placeholderDecorations);const v=f._snippet.placeholderInfo.last.index;for(const L of f._snippet.placeholderInfo.all)L.isFinalTabstop?L.index=_.index+(v+1)/this._nestingLevel:L.index=_.index+L.index/this._nestingLevel;this._snippet.replace(_,f._snippet.children);const y=this._placeholderDecorations.get(_);m.removeDecoration(y),this._placeholderDecorations.delete(_);for(const L of f._snippet.placeholders){const I=f._snippet.offset(L),k=f._snippet.fullLen(L),E=S.Range.fromPositions(h.getPositionAt(f._offset+I),h.getPositionAt(f._offset+I+k)),T=m.addDecoration(E,r._decor.inactive);this._placeholderDecorations.set(L,T)}}this._placeholderGroups=b.groupBy(this._snippet.placeholders,c.Placeholder.compareByIndex)})}}e.OneSnippet=r,r._decor={active:d.ModelDecorationOptions.register({stickiness:0,className:"snippet-placeholder"}),inactive:d.ModelDecorationOptions.register({stickiness:1,className:"snippet-placeholder"}),activeFinal:d.ModelDecorationOptions.register({stickiness:1,className:"finish-snippet-placeholder"}),inactiveFinal:d.ModelDecorationOptions.register({stickiness:1,className:"finish-snippet-placeholder"})};const i={overwriteBefore:0,overwriteAfter:0,adjustWhitespace:!0,clipboardText:void 0,overtypingCapturer:void 0};class n{constructor(l,h,m=i){this._templateMerges=[],this._snippets=[],this._editor=l,this._template=h,this._options=m}static adjustWhitespace(l,h,m,_,f){const v=l.getLineContent(h.lineNumber),y=M.getLeadingWhitespace(v,0,h.column-1);let L;return m.walk(I=>{if(!(I instanceof c.Text)||I.parent instanceof c.Choice)return!0;const k=I.value.split(/\r\n|\r|\n/);if(_){const T=m.offset(I);if(T===0)k[0]=l.normalizeIndentation(k[0]);else{L=L!=null?L:m.toString();let O=L.charCodeAt(T-1);(O===10||O===13)&&(k[0]=l.normalizeIndentation(y+k[0]))}for(let O=1;Ox.get(g.IWorkspaceContextService,p.optional)),O=l.invokeWithinContext(x=>new o.ModelBasedVariableResolver(x.get(u.ILabelService,p.optional),E)),A=()=>y;let B=0,F=E.getValueInRange(n.adjustSelection(E,l.getSelection(),m,0)),D=E.getValueInRange(n.adjustSelection(E,l.getSelection(),0,_)),R=E.getLineFirstNonWhitespaceColumn(l.getSelection().positionLineNumber);const W=l.getSelections().map((x,K)=>({selection:x,idx:K})).sort((x,K)=>S.Range.compareRangesUsingStarts(x.selection,K.selection));for(const{selection:x,idx:K}of W){let Y=n.adjustSelection(E,x,m,0),ee=n.adjustSelection(E,x,0,_);F!==E.getValueInRange(Y)&&(Y=x),D!==E.getValueInRange(ee)&&(ee=x);const se=x.setStartPosition(Y.startLineNumber,Y.startColumn).setEndPosition(ee.endLineNumber,ee.endColumn),ne=new c.SnippetParser().parse(h,!0,f),le=se.getStartPosition(),X=n.adjustWhitespace(E,le,ne,v||K>0&&R!==E.getLineFirstNonWhitespaceColumn(x.positionLineNumber),!0);ne.resolveVariables(new o.CompositeSnippetVariableResolver([O,new o.ClipboardBasedVariableResolver(A,K,W.length,l.getOption(65)==="spread"),new o.SelectionBasedVariableResolver(E,x,K,L),new o.CommentBasedVariableResolver(E,x),new o.TimeBasedVariableResolver,new o.WorkspaceBasedVariableResolver(T),new o.RandomBasedVariableResolver]));const z=E.getOffsetAt(le)+B;B+=ne.toString().length-E.getValueLengthInRange(se),I[K]=w.EditOperation.replace(se,ne.toString()),I[K].identifier={major:K,minor:0},k[K]=new r(l,ne,z,X)}return{edits:I,snippets:k}}dispose(){N.dispose(this._snippets)}_logInfo(){return`template="${this._template}", merged_templates="${this._templateMerges.join(" -> ")}"`}insert(){if(!!this._editor.hasModel()){const{edits:l,snippets:h}=n.createEditsAndSnippets(this._editor,this._template,this._options.overwriteBefore,this._options.overwriteAfter,!1,this._options.adjustWhitespace,this._options.clipboardText,this._options.overtypingCapturer);this._snippets=h,this._editor.executeEdits("snippet",l,m=>this._snippets[0].hasPlaceholder?this._move(!0):m.filter(_=>!!_.identifier).map(_=>C.Selection.fromPositions(_.range.getEndPosition()))),this._editor.revealRange(this._editor.getSelections()[0])}}merge(l,h=i){if(!!this._editor.hasModel()){this._templateMerges.push([this._snippets[0]._nestingLevel,this._snippets[0]._placeholderGroupsIdx,l]);const{edits:m,snippets:_}=n.createEditsAndSnippets(this._editor,l,h.overwriteBefore,h.overwriteAfter,!0,h.adjustWhitespace,h.clipboardText,h.overtypingCapturer);this._editor.executeEdits("snippet",m,f=>{for(const v of this._snippets)v.merge(_);return console.assert(_.length===0),this._snippets[0].hasPlaceholder?this._move(void 0):f.filter(v=>!!v.identifier).map(v=>C.Selection.fromPositions(v.range.getEndPosition()))})}}next(){const l=this._move(!0);this._editor.setSelections(l),this._editor.revealPositionInCenterIfOutsideViewport(l[0].getPosition())}prev(){const l=this._move(!1);this._editor.setSelections(l),this._editor.revealPositionInCenterIfOutsideViewport(l[0].getPosition())}_move(l){const h=[];for(const m of this._snippets){const _=m.move(l);h.push(..._)}return h}get isAtFirstPlaceholder(){return this._snippets[0].isAtFirstPlaceholder}get isAtLastPlaceholder(){return this._snippets[0].isAtLastPlaceholder}get hasPlaceholder(){return this._snippets[0].hasPlaceholder}get choice(){return this._snippets[0].choice}isSelectionWithinPlaceholders(){if(!this.hasPlaceholder)return!1;const l=this._editor.getSelections();if(l.length{f.push(..._.get(v))})}l.sort(S.Range.compareRangesUsingStarts);for(let[m,_]of h){if(_.length!==l.length){h.delete(m);continue}_.sort(S.Range.compareRangesUsingStarts);for(let f=0;f<_.length;f++)if(!_[f].containsRange(l[f])){h.delete(m);continue}}return h.size>0}}e.SnippetSession=n}),define(Q[192],J([0,1,2,13,3,21,25,117,16,77,680]),function(q,e,b,N,M,w,S,C,d,g,p){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SnippetController2=void 0;const c={overwriteBefore:0,overwriteAfter:0,undoStopBefore:!0,undoStopAfter:!0,adjustWhitespace:!0,clipboardText:void 0,overtypingCapturer:void 0};let o=class ot{constructor(u,r,i){this._editor=u,this._logService=r,this._snippetListener=new b.DisposableStore,this._modelVersionId=-1,this._inSnippet=ot.InSnippetMode.bindTo(i),this._hasNextTabstop=ot.HasNextTabstop.bindTo(i),this._hasPrevTabstop=ot.HasPrevTabstop.bindTo(i)}static get(u){return u.getContribution(ot.ID)}dispose(){var u;this._inSnippet.reset(),this._hasPrevTabstop.reset(),this._hasNextTabstop.reset(),(u=this._session)===null||u===void 0||u.dispose(),this._snippetListener.dispose()}insert(u,r){try{this._doInsert(u,typeof r=="undefined"?c:Object.assign(Object.assign({},c),r))}catch(i){this.cancel(),this._logService.error(i),this._logService.error("snippet_error"),this._logService.error("insert_template=",u),this._logService.error("existing_template=",this._session?this._session._logInfo():"")}}_doInsert(u,r){!this._editor.hasModel()||(this._snippetListener.clear(),r.undoStopBefore&&this._editor.getModel().pushStackElement(),this._session?this._session.merge(u,r):(this._modelVersionId=this._editor.getModel().getAlternativeVersionId(),this._session=new p.SnippetSession(this._editor,u,r),this._session.insert()),r.undoStopAfter&&this._editor.getModel().pushStackElement(),this._updateState(),this._snippetListener.add(this._editor.onDidChangeModelContent(i=>i.isFlush&&this.cancel())),this._snippetListener.add(this._editor.onDidChangeModel(()=>this.cancel())),this._snippetListener.add(this._editor.onDidChangeCursorSelection(()=>this._updateState())))}_updateState(){if(!(!this._session||!this._editor.hasModel())){if(this._modelVersionId===this._editor.getModel().getAlternativeVersionId())return this.cancel();if(!this._session.hasPlaceholder)return this.cancel();if(this._session.isAtLastPlaceholder||!this._session.isSelectionWithinPlaceholders())return this.cancel();this._inSnippet.set(!0),this._hasPrevTabstop.set(!this._session.isAtFirstPlaceholder),this._hasNextTabstop.set(!this._session.isAtLastPlaceholder),this._handleChoice()}}_handleChoice(){if(!this._session||!this._editor.hasModel()){this._currentChoice=void 0;return}const{choice:u}=this._session;if(!u){this._currentChoice=void 0;return}if(this._currentChoice!==u){this._currentChoice=u,this._editor.setSelections(this._editor.getSelections().map(i=>w.Selection.fromPositions(i.getStartPosition())));const[r]=u.options;C.showSimpleSuggestions(this._editor,u.options.map((i,n)=>({kind:13,label:i.value,insertText:i.value,sortText:"a".repeat(n+1),range:M.Range.fromPositions(this._editor.getPosition(),this._editor.getPosition().delta(0,r.value.length))})))}}finish(){for(;this._inSnippet.get();)this.next()}cancel(u=!1){var r;this._inSnippet.reset(),this._hasPrevTabstop.reset(),this._hasNextTabstop.reset(),this._snippetListener.clear(),(r=this._session)===null||r===void 0||r.dispose(),this._session=void 0,this._modelVersionId=-1,u&&this._editor.setSelections([this._editor.getSelection()])}prev(){this._session&&this._session.prev(),this._updateState()}next(){this._session&&this._session.next(),this._updateState()}isInSnippet(){return Boolean(this._inSnippet.get())}};o.ID="snippetController2",o.InSnippetMode=new d.RawContextKey("inSnippetMode",!1),o.HasNextTabstop=new d.RawContextKey("hasNextTabstop",!1),o.HasPrevTabstop=new d.RawContextKey("hasPrevTabstop",!1),o=Me([_e(1,g.ILogService),_e(2,d.IContextKeyService)],o),e.SnippetController2=o,N.registerEditorContribution(o.ID,o);const s=N.EditorCommand.bindToContribution(o.get);N.registerEditorCommand(new s({id:"jumpToNextSnippetPlaceholder",precondition:d.ContextKeyExpr.and(o.InSnippetMode,o.HasNextTabstop),handler:a=>a.next(),kbOpts:{weight:100+30,kbExpr:S.EditorContextKeys.editorTextFocus,primary:2}})),N.registerEditorCommand(new s({id:"jumpToPrevSnippetPlaceholder",precondition:d.ContextKeyExpr.and(o.InSnippetMode,o.HasPrevTabstop),handler:a=>a.prev(),kbOpts:{weight:100+30,kbExpr:S.EditorContextKeys.editorTextFocus,primary:1024|2}})),N.registerEditorCommand(new s({id:"leaveSnippet",precondition:o.InSnippetMode,handler:a=>a.cancel(!0),kbOpts:{weight:100+30,kbExpr:S.EditorContextKeys.editorTextFocus,primary:9,secondary:[1024|9]}})),N.registerEditorCommand(new s({id:"acceptSnippet",precondition:o.InSnippetMode,handler:a=>a.finish()}))}),define(Q[681],J([0,1,15,12,6,2,21,18,413,117,192,23,75,416,8,84,87,77]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.SuggestModel=e.LineContext=void 0;class n{constructor(h,m,_,f){this.leadingLineContent=h.getLineContent(m.lineNumber).substr(0,m.column-1),this.leadingWord=h.getWordUntilPosition(m),this.lineNumber=m.lineNumber,this.column=m.column,this.auto=_,this.shy=f}static shouldAutoTrigger(h){if(!h.hasModel())return!1;const m=h.getModel(),_=h.getPosition();m.tokenizeIfCheap(_.lineNumber);const f=m.getWordAtPosition(_);return!(!f||f.endColumn!==_.column||!isNaN(Number(f.word)))}}e.LineContext=n;let t=class ti{constructor(h,m,_,f,v){this._editor=h,this._editorWorkerService=m,this._clipboardService=_,this._telemetryService=f,this._logService=v,this._toDispose=new w.DisposableStore,this._quickSuggestDelay=10,this._triggerCharacterListener=new w.DisposableStore,this._triggerQuickSuggest=new b.TimeoutTimer,this._state=0,this._completionDisposables=new w.DisposableStore,this._onDidCancel=new M.Emitter,this._onDidTrigger=new M.Emitter,this._onDidSuggest=new M.Emitter,this.onDidCancel=this._onDidCancel.event,this.onDidTrigger=this._onDidTrigger.event,this.onDidSuggest=this._onDidSuggest.event,this._telemetryGate=0,this._currentSelection=this._editor.getSelection()||new S.Selection(1,1,1,1),this._toDispose.add(this._editor.onDidChangeModel(()=>{this._updateTriggerCharacters(),this.cancel()})),this._toDispose.add(this._editor.onDidChangeModelLanguage(()=>{this._updateTriggerCharacters(),this.cancel()})),this._toDispose.add(this._editor.onDidChangeConfiguration(()=>{this._updateTriggerCharacters(),this._updateQuickSuggest()})),this._toDispose.add(C.CompletionProviderRegistry.onDidChange(()=>{this._updateTriggerCharacters(),this._updateActiveSuggestSession()})),this._toDispose.add(this._editor.onDidChangeCursorSelection(L=>{this._onCursorChange(L)}));let y=!1;this._toDispose.add(this._editor.onDidCompositionStart(()=>{y=!0})),this._toDispose.add(this._editor.onDidCompositionEnd(()=>{y=!1,this._refilterCompletionItems()})),this._toDispose.add(this._editor.onDidChangeModelContent(()=>{y||this._refilterCompletionItems()})),this._updateTriggerCharacters(),this._updateQuickSuggest()}dispose(){w.dispose(this._triggerCharacterListener),w.dispose([this._onDidCancel,this._onDidSuggest,this._onDidTrigger,this._triggerQuickSuggest]),this._toDispose.dispose(),this._completionDisposables.dispose(),this.cancel()}_updateQuickSuggest(){this._quickSuggestDelay=this._editor.getOption(74),(isNaN(this._quickSuggestDelay)||!this._quickSuggestDelay&&this._quickSuggestDelay!==0||this._quickSuggestDelay<0)&&(this._quickSuggestDelay=10)}_updateTriggerCharacters(){if(this._triggerCharacterListener.clear(),!(this._editor.getOption(75)||!this._editor.hasModel()||!this._editor.getOption(104))){const h=new Map;for(const _ of C.CompletionProviderRegistry.all(this._editor.getModel()))for(const f of _.triggerCharacters||[]){let v=h.get(f);v||(v=new Set,v.add(g.getSnippetSuggestSupport()),h.set(f,v)),v.add(_)}const m=_=>{if(!_){const y=this._editor.getPosition();_=this._editor.getModel().getLineContent(y.lineNumber).substr(0,y.column-1)}let f="";a.isLowSurrogate(_.charCodeAt(_.length-1))?a.isHighSurrogate(_.charCodeAt(_.length-2))&&(f=_.substr(_.length-2)):f=_.charAt(_.length-1);const v=h.get(f);if(v){const y=this._completionModel?{items:this._completionModel.adopt(v),clipboardText:this._completionModel.clipboardText}:void 0;this.trigger({auto:!0,shy:!1,triggerCharacter:f},Boolean(this._completionModel),v,y)}};this._triggerCharacterListener.add(this._editor.onDidType(m)),this._triggerCharacterListener.add(this._editor.onDidCompositionEnd(m))}}get state(){return this._state}cancel(h=!1){var m;this._state!==0&&(this._triggerQuickSuggest.cancel(),(m=this._requestToken)===null||m===void 0||m.cancel(),this._requestToken=void 0,this._state=0,this._completionModel=void 0,this._context=void 0,this._onDidCancel.fire({retrigger:h}))}clear(){this._completionDisposables.clear()}_updateActiveSuggestSession(){this._state!==0&&(!this._editor.hasModel()||!C.CompletionProviderRegistry.has(this._editor.getModel())?this.cancel():this.trigger({auto:this._state===2,shy:!1},!0))}_onCursorChange(h){if(!!this._editor.hasModel()){const m=this._editor.getModel(),_=this._currentSelection;if(this._currentSelection=this._editor.getSelection(),!h.selection.isEmpty()||h.reason!==0&&h.reason!==3||h.source!=="keyboard"&&h.source!=="deleteLeft"){this.cancel();return}if(!!C.CompletionProviderRegistry.has(m))if(this._state===0&&h.reason===0){if(this._editor.getOption(73)===!1||!_.containsRange(this._currentSelection)&&!_.getEndPosition().isBeforeOrEqual(this._currentSelection.getPosition())||this._editor.getOption(101).snippetsPreventQuickSuggestions&&p.SnippetController2.get(this._editor).isInSnippet())return;this.cancel(),this._triggerQuickSuggest.cancelAndSet(()=>{if(this._state===0&&!!n.shouldAutoTrigger(this._editor)&&!!this._editor.hasModel()){const f=this._editor.getModel(),v=this._editor.getPosition(),y=this._editor.getOption(73);if(y!==!1){if(y!==!0){f.tokenizeIfCheap(v.lineNumber);const L=f.getLineTokens(v.lineNumber),I=L.getStandardTokenType(L.findTokenIndexAtOffset(Math.max(v.column-1-1,0)));if(!(y.other&&I===0||y.comments&&I===1||y.strings&&I===2))return}this.trigger({auto:!0,shy:!1})}}},this._quickSuggestDelay)}else this._state!==0&&h.reason===3&&this._refilterCompletionItems()}}_refilterCompletionItems(){Promise.resolve().then(()=>{if(this._state!==0&&!!this._editor.hasModel()){const h=this._editor.getModel(),m=this._editor.getPosition(),_=new n(h,m,this._state===2,!1);this._onNewContext(_)}})}trigger(h,m=!1,_,f){var v;if(!!this._editor.hasModel()){const y=this._editor.getModel(),L=h.auto,I=new n(y,this._editor.getPosition(),L,h.shy);this.cancel(m),this._state=L?2:1,this._onDidTrigger.fire({auto:L,shy:h.shy,position:this._editor.getPosition()}),this._context=I;let k={triggerKind:(v=h.triggerKind)!==null&&v!==void 0?v:0};h.triggerCharacter&&(k={triggerKind:1,triggerCharacter:h.triggerCharacter}),this._requestToken=new c.CancellationTokenSource;const E=this._editor.getOption(96);let T=1;switch(E){case"top":T=0;break;case"bottom":T=2;break}const O=ti._createItemKindFilter(this._editor),A=s.WordDistance.create(this._editorWorkerService,this._editor),B=g.provideSuggestionItems(y,this._editor.getPosition(),new g.CompletionOptions(T,O,_),k,this._requestToken.token);Promise.all([B,A]).then(([F,D])=>Ie(this,void 0,void 0,function*(){var R;if((R=this._requestToken)===null||R===void 0||R.dispose(),!!this._editor.hasModel()){let W=f==null?void 0:f.clipboardText;if(!W&&F.needsClipboard&&(W=yield this._clipboardService.readText()),this._state!==0){const x=this._editor.getModel();let K=F.items;if(f){const ee=g.getSuggestionComparator(T);K=K.concat(f.items).sort(ee)}const Y=new n(x,this._editor.getPosition(),L,h.shy);this._completionModel=new d.CompletionModel(K,this._context.column,{leadingLineContent:Y.leadingLineContent,characterCountDelta:Y.column-this._context.column},D,this._editor.getOption(101),this._editor.getOption(96),W),this._completionDisposables.add(F.disposable),this._onNewContext(Y),this._reportDurationsTelemetry(F.durations)}}})).catch(N.onUnexpectedError)}}_reportDurationsTelemetry(h){this._telemetryGate++%230==0&&setTimeout(()=>{this._telemetryService.publicLog2("suggest.durations.json",{data:JSON.stringify(h)}),this._logService.debug("suggest.durations.json",h)})}static _createItemKindFilter(h){const m=new Set;h.getOption(96)==="none"&&m.add(27);const f=h.getOption(101);return f.showMethods||m.add(0),f.showFunctions||m.add(1),f.showConstructors||m.add(2),f.showFields||m.add(3),f.showVariables||m.add(4),f.showClasses||m.add(5),f.showStructs||m.add(6),f.showInterfaces||m.add(7),f.showModules||m.add(8),f.showProperties||m.add(9),f.showEvents||m.add(10),f.showOperators||m.add(11),f.showUnits||m.add(12),f.showValues||m.add(13),f.showConstants||m.add(14),f.showEnums||m.add(15),f.showEnumMembers||m.add(16),f.showKeywords||m.add(17),f.showWords||m.add(18),f.showColors||m.add(19),f.showFiles||m.add(20),f.showReferences||m.add(21),f.showColors||m.add(22),f.showFolders||m.add(23),f.showTypeParameters||m.add(24),f.showSnippets||m.add(27),f.showUsers||m.add(25),f.showIssues||m.add(26),m}_onNewContext(h){if(!!this._context){if(h.lineNumber!==this._context.lineNumber){this.cancel();return}if(a.getLeadingWhitespace(h.leadingLineContent)!==a.getLeadingWhitespace(this._context.leadingLineContent)){this.cancel();return}if(h.columnthis._context.leadingWord.startColumn){const m=new Set(C.CompletionProviderRegistry.all(this._editor.getModel()));for(let f of this._completionModel.allProvider)m.delete(f);const _=this._completionModel.adopt(new Set);this.trigger({auto:this._context.auto,shy:!1},!0,m,{items:_,clipboardText:this._completionModel.clipboardText});return}if(h.column>this._context.column&&this._completionModel.incomplete.size>0&&h.leadingWord.word.length!==0){const{incomplete:m}=this._completionModel,_=this._completionModel.adopt(m);this.trigger({auto:this._state===2,shy:!1,triggerKind:2},!0,m,{items:_,clipboardText:this._completionModel.clipboardText})}else{let m=this._completionModel.lineContext,_=!1;if(this._completionModel.lineContext={leadingLineContent:h.leadingLineContent,characterCountDelta:h.column-this._context.column},this._completionModel.items.length===0){if(n.shouldAutoTrigger(this._editor)&&this._context.leadingWord.endColumn0,_&&h.leadingWord.word.length===0){this.cancel();return}}this._onDidSuggest.fire({completionModel:this._completionModel,auto:this._context.auto,shy:this._context.shy,isFrozen:_})}}}}};t=Me([_e(1,o.IEditorWorkerService),_e(2,u.IClipboardService),_e(3,r.ITelemetryService),_e(4,i.ILogService)],t),e.SuggestModel=t}),define(Q[682],J([0,1,47,19,12,39,2,70,13,62,3,25,192,131,576,507,26,16,9,86,117,548,681,666,549,6,15,20,414,415,14,17,34,23,77,81]),function(q,e,b,N,M,w,S,C,d,g,p,c,o,s,a,u,r,i,n,t,l,h,m,_,f,v,y,L,I,k,E,T,O,A,B,F){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.TriggerSuggestAction=e.SuggestController=void 0;let D=!1;class R{constructor(se,ne){if(this._model=se,this._position=ne,se.getLineMaxColumn(ne.lineNumber)!==ne.column){const X=se.getOffsetAt(ne),z=se.getPositionAt(X+1);this._marker=se.deltaDecorations([],[{range:p.Range.fromPositions(ne,z),options:{stickiness:1}}])}}dispose(){this._marker&&!this._model.isDisposed()&&this._model.deltaDecorations(this._marker,[])}delta(se){if(this._model.isDisposed()||this._position.lineNumber!==se.lineNumber)return 0;if(this._marker){const ne=this._model.getDecorationRange(this._marker[0]);return this._model.getOffsetAt(ne.getStartPosition())-this._model.getOffsetAt(se)}else return this._model.getLineMaxColumn(se.lineNumber)-se.column}}let W=class ii{constructor(se,ne,le,X,z,P){this._memoryService=ne,this._commandService=le,this._contextKeyService=X,this._instantiationService=z,this._logService=P,this._lineSuffix=new S.MutableDisposable,this._toDispose=new S.DisposableStore,this.editor=se,this.model=z.createInstance(m.SuggestModel,this.editor);const V=l.Context.InsertMode.bindTo(X);V.set(se.getOption(101).insertMode),this.model.onDidTrigger(()=>V.set(se.getOption(101).insertMode)),this.widget=this._toDispose.add(new y.IdleValue(()=>{const $=this._instantiationService.createInstance(_.SuggestWidget,this.editor);this._toDispose.add($),this._toDispose.add($.onDidSelect(j=>this._insertSuggestion(j,0),this));const ie=new I.CommitCharacterController(this.editor,$,j=>this._insertSuggestion(j,2));this._toDispose.add(ie),this._toDispose.add(this.model.onDidSuggest(j=>{j.completionModel.items.length===0&&ie.reset()}));const oe=l.Context.MakesTextEdit.bindTo(this._contextKeyService),ae=l.Context.HasInsertAndReplaceRange.bindTo(this._contextKeyService),G=l.Context.CanResolve.bindTo(this._contextKeyService);return this._toDispose.add(S.toDisposable(()=>{oe.reset(),ae.reset(),G.reset()})),this._toDispose.add($.onDidFocus(({item:j})=>{const te=this.editor.getPosition(),Z=j.editStart.column,ue=te.column;let he=!0;this.editor.getOption(1)==="smart"&&this.model.state===2&&!j.completion.command&&!j.completion.additionalTextEdits&&!(j.completion.insertTextRules&4)&&ue-Z===j.completion.insertText.length&&(he=this.editor.getModel().getValueInRange({startLineNumber:te.lineNumber,startColumn:Z,endLineNumber:te.lineNumber,endColumn:ue})!==j.completion.insertText),oe.set(he),ae.set(!E.Position.equals(j.editInsertEnd,j.editReplaceEnd)),G.set(Boolean(j.provider.resolveCompletionItem)||Boolean(j.completion.documentation)||j.completion.detail!==j.completion.label)})),this._toDispose.add($.onDetailsKeyDown(j=>{if(j.toKeybinding().equals(new w.SimpleKeybinding(!0,!1,!1,!1,33))||T.isMacintosh&&j.toKeybinding().equals(new w.SimpleKeybinding(!1,!1,!1,!0,33))){j.stopPropagation();return}j.toKeybinding().isModifierKey()||this.editor.focus()})),$})),this._overtypingCapturer=this._toDispose.add(new y.IdleValue(()=>this._toDispose.add(new k.OvertypingCapturer(this.editor,this.model)))),this._alternatives=this._toDispose.add(new y.IdleValue(()=>this._toDispose.add(new h.SuggestAlternatives(this.editor,this._contextKeyService)))),this._toDispose.add(z.createInstance(f.WordContextKey,se)),this._toDispose.add(this.model.onDidTrigger($=>{this.widget.value.showTriggered($.auto,$.shy?250:50),this._lineSuffix.value=new R(this.editor.getModel(),$.position)})),this._toDispose.add(this.model.onDidSuggest($=>{if(!$.shy){let ie=this._memoryService.select(this.editor.getModel(),this.editor.getPosition(),$.completionModel.items);this.widget.value.showSuggestions($.completionModel,ie,$.isFrozen,$.auto)}})),this._toDispose.add(this.model.onDidCancel($=>{$.retrigger||this.widget.value.hideWidget()})),this._toDispose.add(this.editor.onDidBlurEditorWidget(()=>{D||(this.model.cancel(),this.model.clear())}));let U=l.Context.AcceptSuggestionsOnEnter.bindTo(X),H=()=>{const $=this.editor.getOption(1);U.set($==="on"||$==="smart")};this._toDispose.add(this.editor.onDidChangeConfiguration(()=>H())),H()}static get(se){return se.getContribution(ii.ID)}dispose(){this._alternatives.dispose(),this._toDispose.dispose(),this.widget.dispose(),this.model.dispose(),this._lineSuffix.dispose()}_insertSuggestion(se,ne){if(!se||!se.item){this._alternatives.value.reset(),this.model.cancel(),this.model.clear();return}if(!!this.editor.hasModel()){const le=this.editor.getModel(),X=le.getAlternativeVersionId(),{item:z}=se,P=[],V=new A.CancellationTokenSource;ne&1||this.editor.pushUndoStop();const U=this.getOverwriteInfo(z,Boolean(ne&8));if(this._memoryService.memorize(le,this.editor.getPosition(),z),Array.isArray(z.completion.additionalTextEdits)){const $=C.StableEditorScrollState.capture(this.editor);this.editor.executeEdits("suggestController.additionalTextEdits.sync",z.completion.additionalTextEdits.map(ie=>g.EditOperation.replace(p.Range.lift(ie.range),ie.text))),$.restoreRelativeVerticalPositionOfCursor(this.editor)}else if(!z.isResolved){const $=new F.StopWatch(!0);let ie;const oe=le.onDidChangeContent(te=>{if(te.isFlush){V.cancel(),oe.dispose();return}for(let Z of te.changes){const ue=p.Range.getEndPosition(Z.range);(!ie||E.Position.isBefore(ue,ie))&&(ie=ue)}});let ae=ne;ne|=2;let G=!1,j=this.editor.onWillType(()=>{j.dispose(),G=!0,ae&2||this.editor.pushUndoStop()});P.push(z.resolve(V.token).then(()=>{if(!z.completion.additionalTextEdits||V.token.isCancellationRequested||ie&&z.completion.additionalTextEdits.some(Z=>E.Position.isBefore(ie,p.Range.getStartPosition(Z.range))))return!1;G&&this.editor.pushUndoStop();const te=C.StableEditorScrollState.capture(this.editor);return this.editor.executeEdits("suggestController.additionalTextEdits.async",z.completion.additionalTextEdits.map(Z=>g.EditOperation.replace(p.Range.lift(Z.range),Z.text))),te.restoreRelativeVerticalPositionOfCursor(this.editor),(G||!(ae&2))&&this.editor.pushUndoStop(),!0}).then(te=>{this._logService.trace("[suggest] async resolving of edits DONE (ms, applied?)",$.elapsed(),te),oe.dispose(),j.dispose()}))}let{insertText:H}=z.completion;z.completion.insertTextRules&4||(H=s.SnippetParser.escape(H)),o.SnippetController2.get(this.editor).insert(H,{overwriteBefore:U.overwriteBefore,overwriteAfter:U.overwriteAfter,undoStopBefore:!1,undoStopAfter:!1,adjustWhitespace:!(z.completion.insertTextRules&1),clipboardText:se.model.clipboardText,overtypingCapturer:this._overtypingCapturer.value}),ne&2||this.editor.pushUndoStop(),z.completion.command?z.completion.command.id===x.id?this.model.trigger({auto:!0,shy:!1},!0):(P.push(this._commandService.executeCommand(z.completion.command.id,...z.completion.command.arguments?[...z.completion.command.arguments]:[]).catch(M.onUnexpectedError)),this.model.cancel()):this.model.cancel(),ne&4&&this._alternatives.value.set(se,$=>{for(V.cancel();le.canUndo();){X!==le.getAlternativeVersionId()&&le.undo(),this._insertSuggestion($,1|2|(ne&8?8:0));break}}),this._alertCompletionItem(z),Promise.all(P).finally(()=>{this.model.clear(),V.dispose()})}}getOverwriteInfo(se,ne){L.assertType(this.editor.hasModel());let le=this.editor.getOption(101).insertMode==="replace";ne&&(le=!le);const X=se.position.column-se.editStart.column,z=(le?se.editReplaceEnd.column:se.editInsertEnd.column)-se.position.column,P=this.editor.getPosition().column-se.position.column,V=this._lineSuffix.value?this._lineSuffix.value.delta(this.editor.getPosition()):0;return{overwriteBefore:X+P,overwriteAfter:z+V}}_alertCompletionItem({completion:se}){const ne=typeof se.label=="string"?se.label:se.label.name;if(N.isNonEmptyArray(se.additionalTextEdits)){let le=u.localize(0,null,ne,se.additionalTextEdits.length);b.alert(le)}}triggerSuggest(se){this.editor.hasModel()&&(this.model.trigger({auto:!1,shy:!1},!1,se),this.editor.revealLine(this.editor.getPosition().lineNumber,0),this.editor.focus())}triggerSuggestAndAcceptBest(se){if(!!this.editor.hasModel()){const ne=this.editor.getPosition(),le=()=>{ne.equals(this.editor.getPosition())&&this._commandService.executeCommand(se.fallback)},X=z=>{if(z.completion.insertTextRules&4||z.completion.additionalTextEdits)return!0;const P=this.editor.getPosition(),V=z.editStart.column,U=P.column;return U-V!==z.completion.insertText.length?!0:this.editor.getModel().getValueInRange({startLineNumber:P.lineNumber,startColumn:V,endLineNumber:P.lineNumber,endColumn:U})!==z.completion.insertText};v.Event.once(this.model.onDidTrigger)(z=>{let P=[];v.Event.any(this.model.onDidTrigger,this.model.onDidCancel)(()=>{S.dispose(P),le()},void 0,P),this.model.onDidSuggest(({completionModel:V})=>{if(S.dispose(P),V.items.length===0){le();return}const U=this._memoryService.select(this.editor.getModel(),this.editor.getPosition(),V.items),H=V.items[U];if(!X(H)){le();return}this.editor.pushUndoStop(),this._insertSuggestion({index:U,item:H,model:V},4|1|2)},void 0,P)}),this.model.trigger({auto:!1,shy:!0}),this.editor.revealLine(ne.lineNumber,0),this.editor.focus()}}acceptSelectedSuggestion(se,ne){const le=this.widget.value.getFocusedItem();let X=0;se&&(X|=4),ne&&(X|=8),this._insertSuggestion(le,X)}acceptNextSuggestion(){this._alternatives.value.next()}acceptPrevSuggestion(){this._alternatives.value.prev()}cancelSuggestWidget(){this.model.cancel(),this.model.clear(),this.widget.value.hideWidget()}selectNextSuggestion(){this.widget.value.selectNext()}selectNextPageSuggestion(){this.widget.value.selectNextPage()}selectLastSuggestion(){this.widget.value.selectLast()}selectPrevSuggestion(){this.widget.value.selectPrevious()}selectPrevPageSuggestion(){this.widget.value.selectPreviousPage()}selectFirstSuggestion(){this.widget.value.selectFirst()}toggleSuggestionDetails(){this.widget.value.toggleDetails()}toggleExplainMode(){this.widget.value.toggleExplainMode()}toggleSuggestionFocus(){this.widget.value.toggleDetailsFocus()}resetWidgetSize(){this.widget.value.resetPersistedSize()}};W.ID="editor.contrib.suggestController",W=Me([_e(1,a.ISuggestMemoryService),_e(2,r.ICommandService),_e(3,i.IContextKeyService),_e(4,n.IInstantiationService),_e(5,B.ILogService)],W),e.SuggestController=W;class x extends d.EditorAction{constructor(){super({id:x.id,label:u.localize(1,null),alias:"Trigger Suggest",precondition:i.ContextKeyExpr.and(c.EditorContextKeys.writable,c.EditorContextKeys.hasCompletionItemProvider),kbOpts:{kbExpr:c.EditorContextKeys.textInputFocus,primary:2048|10,secondary:[2048|39],mac:{primary:256|10,secondary:[512|9,2048|39]},weight:100}})}run(se,ne){const le=W.get(ne);!le||le.triggerSuggest()}}e.TriggerSuggestAction=x,x.id="editor.action.triggerSuggest",d.registerEditorContribution(W.ID,W),d.registerEditorAction(x);const K=100+90,Y=d.EditorCommand.bindToContribution(W.get);d.registerEditorCommand(new Y({id:"acceptSelectedSuggestion",precondition:l.Context.Visible,handler(ee){ee.acceptSelectedSuggestion(!0,!1)}})),t.KeybindingsRegistry.registerKeybindingRule({id:"acceptSelectedSuggestion",when:i.ContextKeyExpr.and(l.Context.Visible,c.EditorContextKeys.textInputFocus),primary:2,weight:K}),t.KeybindingsRegistry.registerKeybindingRule({id:"acceptSelectedSuggestion",when:i.ContextKeyExpr.and(l.Context.Visible,c.EditorContextKeys.textInputFocus,l.Context.AcceptSuggestionsOnEnter,l.Context.MakesTextEdit),primary:3,weight:K}),O.MenuRegistry.appendMenuItem(l.suggestWidgetStatusbarMenu,{command:{id:"acceptSelectedSuggestion",title:u.localize(2,null)},group:"left",order:1,when:l.Context.HasInsertAndReplaceRange.toNegated()}),O.MenuRegistry.appendMenuItem(l.suggestWidgetStatusbarMenu,{command:{id:"acceptSelectedSuggestion",title:u.localize(3,null)},group:"left",order:1,when:i.ContextKeyExpr.and(l.Context.HasInsertAndReplaceRange,l.Context.InsertMode.isEqualTo("insert"))}),O.MenuRegistry.appendMenuItem(l.suggestWidgetStatusbarMenu,{command:{id:"acceptSelectedSuggestion",title:u.localize(4,null)},group:"left",order:1,when:i.ContextKeyExpr.and(l.Context.HasInsertAndReplaceRange,l.Context.InsertMode.isEqualTo("replace"))}),d.registerEditorCommand(new Y({id:"acceptAlternativeSelectedSuggestion",precondition:i.ContextKeyExpr.and(l.Context.Visible,c.EditorContextKeys.textInputFocus),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:1024|3,secondary:[1024|2]},handler(ee){ee.acceptSelectedSuggestion(!1,!0)},menuOpts:[{menuId:l.suggestWidgetStatusbarMenu,group:"left",order:2,when:i.ContextKeyExpr.and(l.Context.HasInsertAndReplaceRange,l.Context.InsertMode.isEqualTo("insert")),title:u.localize(5,null)},{menuId:l.suggestWidgetStatusbarMenu,group:"left",order:2,when:i.ContextKeyExpr.and(l.Context.HasInsertAndReplaceRange,l.Context.InsertMode.isEqualTo("replace")),title:u.localize(6,null)}]})),r.CommandsRegistry.registerCommandAlias("acceptSelectedSuggestionOnEnter","acceptSelectedSuggestion"),d.registerEditorCommand(new Y({id:"hideSuggestWidget",precondition:l.Context.Visible,handler:ee=>ee.cancelSuggestWidget(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:9,secondary:[1024|9]}})),d.registerEditorCommand(new Y({id:"selectNextSuggestion",precondition:i.ContextKeyExpr.and(l.Context.Visible,l.Context.MultipleSuggestions),handler:ee=>ee.selectNextSuggestion(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:18,secondary:[2048|18],mac:{primary:18,secondary:[2048|18,256|44]}}})),d.registerEditorCommand(new Y({id:"selectNextPageSuggestion",precondition:i.ContextKeyExpr.and(l.Context.Visible,l.Context.MultipleSuggestions),handler:ee=>ee.selectNextPageSuggestion(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:12,secondary:[2048|12]}})),d.registerEditorCommand(new Y({id:"selectLastSuggestion",precondition:i.ContextKeyExpr.and(l.Context.Visible,l.Context.MultipleSuggestions),handler:ee=>ee.selectLastSuggestion()})),d.registerEditorCommand(new Y({id:"selectPrevSuggestion",precondition:i.ContextKeyExpr.and(l.Context.Visible,l.Context.MultipleSuggestions),handler:ee=>ee.selectPrevSuggestion(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:16,secondary:[2048|16],mac:{primary:16,secondary:[2048|16,256|46]}}})),d.registerEditorCommand(new Y({id:"selectPrevPageSuggestion",precondition:i.ContextKeyExpr.and(l.Context.Visible,l.Context.MultipleSuggestions),handler:ee=>ee.selectPrevPageSuggestion(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:11,secondary:[2048|11]}})),d.registerEditorCommand(new Y({id:"selectFirstSuggestion",precondition:i.ContextKeyExpr.and(l.Context.Visible,l.Context.MultipleSuggestions),handler:ee=>ee.selectFirstSuggestion()})),d.registerEditorCommand(new Y({id:"toggleSuggestionDetails",precondition:l.Context.Visible,handler:ee=>ee.toggleSuggestionDetails(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:2048|10,mac:{primary:256|10}},menuOpts:[{menuId:l.suggestWidgetStatusbarMenu,group:"right",order:1,when:i.ContextKeyExpr.and(l.Context.DetailsVisible,l.Context.CanResolve),title:u.localize(7,null)},{menuId:l.suggestWidgetStatusbarMenu,group:"right",order:1,when:i.ContextKeyExpr.and(l.Context.DetailsVisible.toNegated(),l.Context.CanResolve),title:u.localize(8,null)}]})),d.registerEditorCommand(new Y({id:"toggleExplainMode",precondition:l.Context.Visible,handler:ee=>ee.toggleExplainMode(),kbOpts:{weight:100,primary:2048|85}})),d.registerEditorCommand(new Y({id:"toggleSuggestionFocus",precondition:l.Context.Visible,handler:ee=>ee.toggleSuggestionFocus(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:2048|512|10,mac:{primary:256|512|10}}})),d.registerEditorCommand(new Y({id:"insertBestCompletion",precondition:i.ContextKeyExpr.and(c.EditorContextKeys.textInputFocus,i.ContextKeyExpr.equals("config.editor.tabCompletion","on"),f.WordContextKey.AtEnd,l.Context.Visible.toNegated(),h.SuggestAlternatives.OtherSuggestions.toNegated(),o.SnippetController2.InSnippetMode.toNegated()),handler:(ee,se)=>{ee.triggerSuggestAndAcceptBest(L.isObject(se)?Object.assign({fallback:"tab"},se):{fallback:"tab"})},kbOpts:{weight:K,primary:2}})),d.registerEditorCommand(new Y({id:"insertNextSuggestion",precondition:i.ContextKeyExpr.and(c.EditorContextKeys.textInputFocus,i.ContextKeyExpr.equals("config.editor.tabCompletion","on"),h.SuggestAlternatives.OtherSuggestions,l.Context.Visible.toNegated(),o.SnippetController2.InSnippetMode.toNegated()),handler:ee=>ee.acceptNextSuggestion(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:2}})),d.registerEditorCommand(new Y({id:"insertPrevSuggestion",precondition:i.ContextKeyExpr.and(c.EditorContextKeys.textInputFocus,i.ContextKeyExpr.equals("config.editor.tabCompletion","on"),h.SuggestAlternatives.OtherSuggestions,l.Context.Visible.toNegated(),o.SnippetController2.InSnippetMode.toNegated()),handler:ee=>ee.acceptPrevSuggestion(),kbOpts:{weight:K,kbExpr:c.EditorContextKeys.textInputFocus,primary:1024|2}})),d.registerEditorAction(class extends d.EditorAction{constructor(){super({id:"editor.action.resetSuggestSize",label:u.localize(9,null),alias:"Reset Suggest Widget Size",precondition:void 0})}run(ee,se){W.get(se).resetWidgetSize()}})}),define(Q[683],J([0,1,188,143,266,215,611,612,613,614,615,635,618,670,619,620,621,622,267,659,623,624,545,271,272,268,273,629,630,628,631,632,633,662,664,636,637,192,682,638,264,639,671,640,265,641,64,123]),function(q,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0})});var mt=this&&this.__createBinding||(Object.create?function(q,e,b,N){N===void 0&&(N=b),Object.defineProperty(q,N,{enumerable:!0,get:function(){return e[b]}})}:function(q,e,b,N){N===void 0&&(N=b),q[N]=e[b]}),pt=this&&this.__exportStar||function(q,e){for(var b in q)b!=="default"&&!Object.prototype.hasOwnProperty.call(e,b)&&mt(e,q,b)};define(Q[686],J([0,1,677,683,642,643,644,573,646,647,645,672,648]),function(q,e,b){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),pt(b,e)})}).call(this); + + +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-typescript version: 4.3.1(d4e91e07d8144e428885c93029b3d3cbf1994c33) + * Released under the MIT license + * https://github.com/Microsoft/monaco-typescript/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/language/typescript/lib/typescriptServicesMetadata",["require","exports"],(function(t,e){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.typescriptVersion=void 0,e.typescriptVersion="4.2.3"})),define("vs/language/typescript/fillers/monaco-editor-core",['vs/editor/editor.api'],(function(){return self.monaco})),define("vs/language/typescript/monaco.contribution",["require","exports","./lib/typescriptServicesMetadata","./fillers/monaco-editor-core"],(function(t,e,i,n){"use strict";var r,o,s,a,c;Object.defineProperty(e,"__esModule",{value:!0}),e.getJavaScriptWorker=e.getTypeScriptWorker=e.javascriptDefaults=e.typescriptDefaults=e.typescriptVersion=e.ModuleResolutionKind=e.ScriptTarget=e.NewLineKind=e.JsxEmit=e.ModuleKind=void 0,function(t){t[t.None=0]="None",t[t.CommonJS=1]="CommonJS",t[t.AMD=2]="AMD",t[t.UMD=3]="UMD",t[t.System=4]="System",t[t.ES2015=5]="ES2015",t[t.ESNext=99]="ESNext"}(r=e.ModuleKind||(e.ModuleKind={})),function(t){t[t.None=0]="None",t[t.Preserve=1]="Preserve",t[t.React=2]="React",t[t.ReactNative=3]="ReactNative",t[t.ReactJSX=4]="ReactJSX",t[t.ReactJSXDev=5]="ReactJSXDev"}(o=e.JsxEmit||(e.JsxEmit={})),function(t){t[t.CarriageReturnLineFeed=0]="CarriageReturnLineFeed",t[t.LineFeed=1]="LineFeed"}(s=e.NewLineKind||(e.NewLineKind={})),function(t){t[t.ES3=0]="ES3",t[t.ES5=1]="ES5",t[t.ES2015=2]="ES2015",t[t.ES2016=3]="ES2016",t[t.ES2017=4]="ES2017",t[t.ES2018=5]="ES2018",t[t.ES2019=6]="ES2019",t[t.ES2020=7]="ES2020",t[t.ESNext=99]="ESNext",t[t.JSON=100]="JSON",t[t.Latest=99]="Latest"}(a=e.ScriptTarget||(e.ScriptTarget={})),function(t){t[t.Classic=1]="Classic",t[t.NodeJs=2]="NodeJs"}(c=e.ModuleResolutionKind||(e.ModuleResolutionKind={}));var p=function(){function t(t,e,i){this._onDidChange=new n.Emitter,this._onDidExtraLibsChange=new n.Emitter,this._extraLibs=Object.create(null),this._removedExtraLibs=Object.create(null),this._eagerModelSync=!1,this.setCompilerOptions(t),this.setDiagnosticsOptions(e),this.setWorkerOptions(i),this._onDidExtraLibsChangeTimeout=-1}return Object.defineProperty(t.prototype,"onDidChange",{get:function(){return this._onDidChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"onDidExtraLibsChange",{get:function(){return this._onDidExtraLibsChange.event},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"workerOptions",{get:function(){return this._workerOptions},enumerable:!1,configurable:!0}),t.prototype.getExtraLibs=function(){return this._extraLibs},t.prototype.addExtraLib=function(t,e){var i,n=this;if(i=void 0===e?"ts:extralib-"+Math.random().toString(36).substring(2,15):e,this._extraLibs[i]&&this._extraLibs[i].content===t)return{dispose:function(){}};var r=1;return this._removedExtraLibs[i]&&(r=this._removedExtraLibs[i]+1),this._extraLibs[i]&&(r=this._extraLibs[i].version+1),this._extraLibs[i]={content:t,version:r},this._fireOnDidExtraLibsChangeSoon(),{dispose:function(){var t=n._extraLibs[i];t&&t.version===r&&(delete n._extraLibs[i],n._removedExtraLibs[i]=r,n._fireOnDidExtraLibsChangeSoon())}}},t.prototype.setExtraLibs=function(t){for(var e in this._extraLibs)this._removedExtraLibs[e]=this._extraLibs[e].version;if(this._extraLibs=Object.create(null),t&&t.length>0)for(var i=0,n=t;i console.log", weil "log" vor Kurzem abgeschlossen wurde.','W\xE4hlen Sie Vorschl\xE4ge basierend auf fr\xFCheren Pr\xE4fixen aus, die diese Vorschl\xE4ge abgeschlossen haben, z.B. "co -> console" und "con ->" const".',"Steuert, wie Vorschl\xE4ge bei Anzeige der Vorschlagsliste vorab ausgew\xE4hlt werden.","Die Tab-Vervollst\xE4ndigung f\xFCgt den passendsten Vorschlag ein, wenn auf Tab gedr\xFCckt wird.","Tab-Vervollst\xE4ndigungen deaktivieren.",'Codeausschnitte per Tab vervollst\xE4ndigen, wenn die Pr\xE4fixe \xFCbereinstimmen. Funktioniert am besten, wenn "quickSuggestions" deaktiviert sind.',"Tab-Vervollst\xE4ndigungen aktivieren.","Ungew\xF6hnliche Zeilenabschlusszeichen werden automatisch entfernt.","Ungew\xF6hnliche Zeilenabschlusszeichen werden ignoriert.","Zum Entfernen ungew\xF6hnlicher Zeilenabschlusszeichen wird eine Eingabeaufforderung angezeigt.","Entfernen Sie un\xFCbliche Zeilenabschlusszeichen, die Probleme verursachen k\xF6nnen.","Das Einf\xFCgen und L\xF6schen von Leerzeichen erfolgt nach Tabstopps.","Zeichen, die als Worttrennzeichen verwendet werden, wenn wortbezogene Navigationen oder Vorg\xE4nge ausgef\xFChrt werden.","Zeilenumbr\xFCche erfolgen nie.","Der Zeilenumbruch erfolgt an der Breite des Anzeigebereichs.",'Der Zeilenumbruch erfolgt bei "#editor.wordWrapColumn#".','Der Zeilenumbruch erfolgt beim Mindestanzeigebereich und "#editor.wordWrapColumn".',"Steuert, wie der Zeilenumbruch durchgef\xFChrt werden soll.",'Steuert die umschlie\xDFende Spalte des Editors, wenn "#editor.wordWrap#" den Wert "wordWrapColumn" oder "bounded" aufweist.',"Kein Einzug. Umbrochene Zeilen beginnen bei Spalte 1.","Umbrochene Zeilen erhalten den gleichen Einzug wie das \xFCbergeordnete Element.","Umbrochene Zeilen erhalten + 1 Einzug auf das \xFCbergeordnete Element.","Umgebrochene Zeilen werden im Vergleich zum \xFCbergeordneten Element +2 einger\xFCckt.","Steuert die Einr\xFCckung der umbrochenen Zeilen.","Es wird angenommen, dass alle Zeichen gleich breit sind. Dies ist ein schneller Algorithmus, der f\xFCr Festbreitenschriftarten und bestimmte Alphabete (wie dem lateinischen), bei denen die Glyphen gleich breit sind, korrekt funktioniert.","Delegiert die Berechnung von Umbruchpunkten an den Browser. Dies ist ein langsamer Algorithmus, der bei gro\xDFen Dateien Code Freezes verursachen kann, aber in allen F\xE4llen korrekt funktioniert.","Steuert den Algorithmus, der Umbruchpunkte berechnet."],"vs/editor/common/editorContextKeys":["Gibt an, ob der Editor-Text den Fokus besitzt (Cursor blinkt).","Gibt an, ob der Editor oder ein Editor-Widget den Fokus besitzt (z.\xA0B. ob der Fokus sich im Suchwidget befindet).","Gibt an, ob ein Editor oder eine Rich-Text-Eingabe den Fokus besitzt (Cursor blinkt).","Gibt an, ob der Editor schreibgesch\xFCtzt ist.","Gibt an, ob der Kontext ein Diff-Editor ist.",'Gibt an, ob "editor.columnSelection" aktiviert ist.',"Gibt an, ob im Editor Text ausgew\xE4hlt ist.","Gibt an, ob der Editor \xFCber Mehrfachauswahl verf\xFCgt.","Gibt an, ob die TAB-TASTE den Fokus aus dem Editor verschiebt.","Gibt an, ob Hover im Editor sichtbar ist.","Gibt an, ob der Editor Bestandteil eines gr\xF6\xDFeren Editors ist (z.\xA0B. Notebooks).","Der Sprachbezeichner des Editors.","Gibt an, ob der Editor \xFCber einen Vervollst\xE4ndigungselementanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Codeaktionsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen CodeLens-Anbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Definitionsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Deklarationsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Implementierungsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Typdefinitionsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Hoveranbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Dokumenthervorhebungsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Dokumentsymbolanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Verweisanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Umbenennungsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Signaturhilfeanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Inlinehinweisanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Dokumentformatierungsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber einen Anbieter f\xFCr Dokumentauswahlformatierung verf\xFCgt.","Gibt an, ob der Editor \xFCber mehrere Dokumentformatierungsanbieter verf\xFCgt.","Gibt an, ob der Editor \xFCber mehrere Anbieter f\xFCr Dokumentauswahlformatierung verf\xFCgt."],"vs/editor/common/model/editStack":["Eingabe"],"vs/editor/common/modes/modesRegistry":["Nur-Text"],"vs/editor/common/standaloneStrings":["Keine Auswahl","Zeile {0}, Spalte {1} ({2} ausgew\xE4hlt)","Zeile {0}, Spalte {1}","{0} Auswahlen ({1} Zeichen ausgew\xE4hlt)","{0} Auswahlen",'Die Einstellung "accessibilitySupport" wird jetzt in "on" ge\xE4ndert.',"Die Dokumentationsseite zur Barrierefreiheit des Editors wird ge\xF6ffnet."," in einem schreibgesch\xFCtzten Bereich eines Diff-Editors."," in einem Bereich eines Diff-Editors."," in einem schreibgesch\xFCtzten Code-Editor"," in einem Code-Editor","Dr\xFCcken Sie BEFEHLSTASTE + E, um den Editor f\xFCr eine optimierte Verwendung mit Sprachausgabe zu konfigurieren.","Dr\xFCcken Sie STRG + E, um den Editor f\xFCr eine optimierte Verwendung mit Sprachausgabe zu konfigurieren.","Der Editor ist auf eine optimale Verwendung mit Sprachausgabe konfiguriert.","Der Editor ist so konfiguriert, dass er nie auf die Verwendung mit Sprachausgabe hin optimiert wird. Dies ist zu diesem Zeitpunkt nicht der Fall.","Durch Dr\xFCcken der TAB-TASTE im aktuellen Editor wird der Fokus in das n\xE4chste Element verschoben, das den Fokus erhalten kann. Schalten Sie dieses Verhalten um, indem Sie {0} dr\xFCcken.","Durch Dr\xFCcken der TAB-TASTE im aktuellen Editor wird der Fokus in das n\xE4chste Element verschoben, das den Fokus erhalten kann. Der {0}-Befehl kann zurzeit nicht durch eine Tastenzuordnung ausgel\xF6st werden.","Durch Dr\xFCcken der TAB-TASTE im aktuellen Editor wird das Tabstoppzeichen eingef\xFCgt. Schalten Sie dieses Verhalten um, indem Sie {0} dr\xFCcken.","Durch Dr\xFCcken der TAB-TASTE im aktuellen Editor wird das Tabstoppzeichen eingef\xFCgt. Der {0}-Befehl kann zurzeit nicht durch eine Tastenzuordnung ausgel\xF6st werden.","Dr\xFCcken Sie BEFEHLSTASTE + H, um ein Browserfenster mit weiteren Informationen zur Barrierefreiheit des Editors zu \xF6ffnen.","Dr\xFCcken Sie STRG + H, um ein Browserfenster mit weiteren Informationen zur Barrierefreiheit des Editors zu \xF6ffnen.","Sie k\xF6nnen diese QuickInfo schlie\xDFen und durch Dr\xFCcken von ESC oder UMSCHALT+ESC zum Editor zur\xFCckkehren.","Hilfe zur Barrierefreiheit anzeigen","Entwickler: Token \xFCberpr\xFCfen","Gehe zu Zeile/Spalte...","Alle Anbieter f\xFCr den Schnellzugriff anzeigen","Befehlspalette","Befehle anzeigen und ausf\xFChren","Gehe zu Symbol...","Gehe zu Symbol nach Kategorie...","Editor-Inhalt","Dr\xFCcken Sie ALT + F1, um die Barrierefreiheitsoptionen aufzurufen.","Zu Design mit hohem Kontrast umschalten","{0} Bearbeitungen in {1} Dateien durchgef\xFChrt"],"vs/editor/common/view/editorColorRegistry":["Hintergrundfarbe zur Hervorhebung der Zeile an der Cursorposition.","Hintergrundfarbe f\xFCr den Rahmen um die Zeile an der Cursorposition.","Hintergrundfarbe der markierten Bereiche, wie z.B. Quick Open oder die Suche. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Hintergrundfarbe f\xFCr den Rahmen um hervorgehobene Bereiche.",'Hintergrundfarbe des hervorgehobenen Symbols, z. B. "Gehe zu Definition" oder "Gehe zu n\xE4chster/vorheriger". Die Farbe darf nicht undurchsichtig sein, um zugrunde liegende Dekorationen nicht zu verbergen.',"Hintergrundfarbe des Rahmens um hervorgehobene Symbole","Farbe des Cursors im Editor.","Hintergrundfarbe vom Editor-Cursor. Erlaubt die Anpassung der Farbe von einem Zeichen, welches von einem Block-Cursor \xFCberdeckt wird.","Farbe der Leerzeichen im Editor.","Farbe der F\xFChrungslinien f\xFCr Einz\xFCge im Editor.","Farbe der F\xFChrungslinien f\xFCr Einz\xFCge im aktiven Editor.","Zeilennummernfarbe im Editor.","Zeilennummernfarbe der aktiven Editorzeile.",'Die ID ist veraltet. Verwenden Sie stattdessen "editorLineNumber.activeForeground".',"Zeilennummernfarbe der aktiven Editorzeile.","Farbe des Editor-Lineals.","Vordergrundfarbe der CodeLens-Links im Editor","Hintergrundfarbe f\xFCr zusammengeh\xF6rige Klammern","Farbe f\xFCr zusammengeh\xF6rige Klammern","Farbe des Rahmens f\xFCr das \xDCbersicht-Lineal.","Hintergrundfarbe des \xDCbersichtslineals im Editor. Wird nur verwendet, wenn die Minimap aktiviert ist und auf der rechten Seite des Editors platziert wird.","Hintergrundfarbe der Editorleiste. Die Leiste enth\xE4lt die Glyphenr\xE4nder und die Zeilennummern.","Rahmenfarbe unn\xF6tigen (nicht genutzten) Quellcodes im Editor.",'Deckkraft des unn\xF6tigen (nicht genutzten) Quellcodes im Editor. "#000000c0" rendert z.B. den Code mit einer Deckkraft von 75%. Verwenden Sie f\xFCr Designs mit hohem Kontrast das Farbdesign "editorUnnecessaryCode.border", um unn\xF6tigen Code zu unterstreichen statt ihn abzublenden.',"\xDCbersichtslinealmarkerfarbe f\xFCr das Hervorheben von Bereichen. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","\xDCbersichtslineal-Markierungsfarbe f\xFCr Fehler.","\xDCbersichtslineal-Markierungsfarbe f\xFCr Warnungen.","\xDCbersichtslineal-Markierungsfarbe f\xFCr Informationen."],"vs/editor/contrib/anchorSelect/anchorSelect":["Auswahlanker",'Anker festgelegt bei "{0}:{1}"',"Auswahlanker festlegen","Zu Auswahlanker wechseln","Auswahl von Anker zu Cursor","Auswahlanker abbrechen"],"vs/editor/contrib/bracketMatching/bracketMatching":["\xDCbersichtslineal-Markierungsfarbe f\xFCr zusammengeh\xF6rige Klammern.","Gehe zu Klammer","Ausw\xE4hlen bis Klammer","Gehe zu &&Klammer"],"vs/editor/contrib/caretOperations/caretOperations":["Ausgew\xE4hlten Text nach links verschieben","Ausgew\xE4hlten Text nach rechts verschieben"],"vs/editor/contrib/caretOperations/transpose":["Buchstaben austauschen"],"vs/editor/contrib/clipboard/clipboard":["&&Ausschneiden","Ausschneiden","Ausschneiden","&&Kopieren","Kopieren","Kopieren","&&Einf\xFCgen","Einf\xFCgen","Einf\xFCgen","Mit Syntaxhervorhebung kopieren"],"vs/editor/contrib/codeAction/codeActionCommands":["Art der auszuf\xFChrenden Codeaktion","Legt fest, wann die zur\xFCckgegebenen Aktionen angewendet werden","Die erste zur\xFCckgegebene Codeaktion immer anwenden","Die erste zur\xFCckgegebene Codeaktion anwenden, wenn nur eine vorhanden ist","Zur\xFCckgegebene Codeaktionen nicht anwenden","Legt fest, ob nur bevorzugte Codeaktionen zur\xFCckgegeben werden sollen","Beim Anwenden der Code-Aktion ist ein unbekannter Fehler aufgetreten","Schnelle Problembehebung ...","Keine Codeaktionen verf\xFCgbar",'Keine bevorzugten Codeaktionen f\xFCr "{0}" verf\xFCgbar','Keine Codeaktionen f\xFCr "{0}" verf\xFCgbar',"Keine bevorzugten Codeaktionen verf\xFCgbar","Keine Codeaktionen verf\xFCgbar","Refactoring durchf\xFChren...",'Keine bevorzugten Refactorings f\xFCr "{0}" verf\xFCgbar','Keine Refactorings f\xFCr "{0}" verf\xFCgbar',"Keine bevorzugten Refactorings verf\xFCgbar","Keine Refactorings verf\xFCgbar","Quellaktion...",'Keine bevorzugten Quellaktionen f\xFCr "{0}" verf\xFCgbar','Keine Quellaktionen f\xFCr "{0}" verf\xFCgbar',"Keine bevorzugten Quellaktionen verf\xFCgbar","Keine Quellaktionen verf\xFCgbar","Importe organisieren","Keine Aktion zum Organisieren von Importen verf\xFCgbar","Alle korrigieren",'Aktion "Alle korrigieren" nicht verf\xFCgbar',"Automatisch korrigieren...","Keine automatischen Korrekturen verf\xFCgbar"],"vs/editor/contrib/codeAction/lightBulbWidget":["Fixes anzeigen. Bevorzugter Fix verf\xFCgbar ({0})","Korrekturen anzeigen ({0})","Korrekturen anzeigen"],"vs/editor/contrib/codelens/codelensController":["CodeLens-Befehle f\xFCr aktuelle Zeile anzeigen"],"vs/editor/contrib/comment/comment":["Zeilenkommentar umschalten","Zeilenkommen&&tar umschalten","Zeilenkommentar hinzuf\xFCgen","Zeilenkommentar entfernen","Blockkommentar umschalten","&&Blockkommentar umschalten"],"vs/editor/contrib/contextmenu/contextmenu":["Editor-Kontextmen\xFC anzeigen"],"vs/editor/contrib/cursorUndo/cursorUndo":["Mit Cursor r\xFCckg\xE4ngig machen","Wiederholen mit Cursor"],"vs/editor/contrib/find/findController":["Suchen","&&Suchen","Mit Auswahl suchen","Weitersuchen","Weitersuchen","Vorheriges Element suchen","Vorheriges Element suchen","N\xE4chste Auswahl suchen","Vorherige Auswahl suchen","Ersetzen","&&Ersetzen"],"vs/editor/contrib/find/findWidget":['Symbol f\xFCr "In Auswahl suchen" im Editor-Such-Widget.',"Symbol f\xFCr die Anzeige, dass das Editor-Such-Widget zugeklappt wurde.","Symbol f\xFCr die Anzeige, dass das Editor-Such-Widget aufgeklappt wurde.",'Symbol f\xFCr "Ersetzen" im Editor-Such-Widget.','Symbol f\xFCr "Alle ersetzen" im Editor-Such-Widget.','Symbol f\xFCr "Vorheriges Element suchen" im Editor-Such-Widget.','Symbol f\xFCr "N\xE4chstes Element suchen" im Editor-Such-Widget.',"Suchen","Suchen","Vorheriger Treffer","N\xE4chste \xDCbereinstimmung","In Auswahl suchen","Schlie\xDFen","Ersetzen","Ersetzen","Ersetzen","Alle ersetzen","Ersetzen-Modus wechseln","Nur die ersten {0} Ergebnisse wurden hervorgehoben, aber alle Suchoperationen werden auf dem gesamten Text durchgef\xFChrt.","{0} von {1}","Keine Ergebnisse","{0} gefunden",'{0} f\xFCr "{1}" gefunden','{0} f\xFCr "{1}" gefunden, bei {2}','{0} f\xFCr "{1}" gefunden','STRG+EINGABE f\xFCgt jetzt einen Zeilenumbruch ein, statt alles zu ersetzen. Sie k\xF6nnen die Tastenzuordnung f\xFCr "editor.action.replaceAll" \xE4ndern, um dieses Verhalten au\xDFer Kraft zu setzen.'],"vs/editor/contrib/folding/folding":["Auffalten","Faltung rekursiv aufheben","Falten","Einklappung umschalten","Rekursiv falten","Alle Blockkommentare falten","Alle Regionen falten","Alle Regionen auffalten","Alle falten","Alle auffalten","Faltebene {0}","Hintergrundfarbe hinter gefalteten Bereichen. Die Farbe darf nicht deckend sein, sodass zugrunde liegende Dekorationen nicht ausgeblendet werden.","Farbe des Faltsteuerelements im Editor-Bundsteg."],"vs/editor/contrib/folding/foldingDecorations":["Symbol f\xFCr aufgeklappte Bereiche im Editor-Glyphenrand.","Symbol f\xFCr zugeklappte Bereiche im Editor-Glyphenrand."],"vs/editor/contrib/fontZoom/fontZoom":["Editorschriftart vergr\xF6\xDFern","Editorschriftart verkleinern","Editor Schriftart Vergr\xF6\xDFerung zur\xFCcksetzen"],"vs/editor/contrib/format/format":["1 Formatierung in Zeile {0} vorgenommen","{0} Formatierungen in Zeile {1} vorgenommen","1 Formatierung zwischen Zeilen {0} und {1} vorgenommen","{0} Formatierungen zwischen Zeilen {1} und {2} vorgenommen"],"vs/editor/contrib/format/formatActions":["Dokument formatieren","Auswahl formatieren"],"vs/editor/contrib/gotoError/gotoError":["Gehe zu n\xE4chstem Problem (Fehler, Warnung, Information)","Symbol f\xFCr den Marker zum Wechseln zum n\xE4chsten Element.","Gehe zu vorigem Problem (Fehler, Warnung, Information)","Symbol f\xFCr den Marker zum Wechseln zum vorherigen Element.","Gehe zu dem n\xE4chsten Problem in den Dateien (Fehler, Warnung, Info)","N\xE4chstes &&Problem","Gehe zu dem vorherigen Problem in den Dateien (Fehler, Warnung, Info)","Vorheriges &&Problem"],"vs/editor/contrib/gotoError/gotoErrorWidget":["Fehler","Warnung","Info","Hinweis","{0} bei {1}. ","{0} von {1} Problemen","{0} von {1} Problemen","Editormarkierung: Farbe bei Fehler des Navigationswidgets.","Editormarkierung: Farbe bei Warnung des Navigationswidgets.","Editormarkierung: Farbe bei Information des Navigationswidgets.","Editormarkierung: Hintergrund des Navigationswidgets."],"vs/editor/contrib/gotoSymbol/goToCommands":["Vorschau","Definitionen",'Keine Definition gefunden f\xFCr "{0}".',"Keine Definition gefunden","Gehe zu Definition","Gehe &&zu Definition","Definition an der Seite \xF6ffnen","Definition einsehen","Deklarationen",'Keine Deklaration f\xFCr "{0}" gefunden.',"Keine Deklaration gefunden.","Zur Deklaration wechseln","Gehe zu &&Deklaration",'Keine Deklaration f\xFCr "{0}" gefunden.',"Keine Deklaration gefunden.","Vorschau f\xFCr Deklaration anzeigen","Typdefinitionen",'Keine Typendefinition gefunden f\xFCr "{0}"',"Keine Typendefinition gefunden","Zur Typdefinition wechseln","Zur &&Typdefinition wechseln","Vorschau der Typdefinition anzeigen","Implementierungen",'Keine Implementierung gefunden f\xFCr "{0}"',"Keine Implementierung gefunden","Gehe zu Implementierungen","Gehe zu &&Implementierungen","Vorschau f\xFCr Implementierungen anzeigen",'F\xFCr "{0}" wurden keine Verweise gefunden.',"Keine Referenzen gefunden","Gehe zu Verweisen","Gehe zu &&Verweisen","Verweise","Vorschau f\xFCr Verweise anzeigen","Verweise","Gehe zu beliebigem Symbol","Speicherorte",'Keine Ergebnisse f\xFCr "{0}"',"Verweise"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["Klicken Sie, um {0} Definitionen anzuzeigen."],"vs/editor/contrib/gotoSymbol/peek/referencesController":["Wird geladen...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["{0} Verweise","{0} Verweis","Verweise"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["Keine Vorschau verf\xFCgbar.","Keine Ergebnisse","Verweise"],"vs/editor/contrib/gotoSymbol/referencesModel":["Symbol in {0} in Zeile {1}, Spalte {2}",'Symbol in "{0}" in Zeile {1}, Spalte {2}, {3}',"1 Symbol in {0}, vollst\xE4ndiger Pfad {1}","{0} Symbole in {1}, vollst\xE4ndiger Pfad {2}","Es wurden keine Ergebnisse gefunden.","1 Symbol in {0} gefunden","{0} Symbole in {1} gefunden","{0} Symbole in {1} Dateien gefunden"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["Symbol {0} von {1}, {2} f\xFCr n\xE4chstes","Symbol {0} von {1}"],"vs/editor/contrib/hover/hover":["Hovern anzeigen","Definitionsvorschauhover anzeigen"],"vs/editor/contrib/hover/markdownHoverParticipant":["Wird geladen..."],"vs/editor/contrib/hover/markerHoverParticipant":["Problem anzeigen","Keine Schnellkorrekturen verf\xFCgbar","Es wird nach Schnellkorrekturen gesucht...","Keine Schnellkorrekturen verf\xFCgbar","Schnelle Problembehebung ..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["Durch vorherigen Wert ersetzen","Durch n\xE4chsten Wert ersetzen"],"vs/editor/contrib/indentation/indentation":["Einzug in Leerzeichen konvertieren","Einzug in Tabstopps konvertieren","Konfigurierte Tabulatorgr\xF6\xDFe","Tabulatorgr\xF6\xDFe f\xFCr aktuelle Datei ausw\xE4hlen","Einzug mithilfe von Tabstopps","Einzug mithilfe von Leerzeichen","Einzug aus Inhalt erkennen","Neuen Einzug f\xFCr Zeilen festlegen","Gew\xE4hlte Zeilen zur\xFCckziehen"],"vs/editor/contrib/linesOperations/linesOperations":["Zeile nach oben kopieren","Zeile nach oben &&kopieren","Zeile nach unten kopieren","Zeile nach unten ko&&pieren","Auswahl duplizieren","&&Auswahl duplizieren","Zeile nach oben verschieben","Zeile nach oben &&verschieben","Zeile nach unten verschieben","Zeile nach &&unten verschieben","Zeilen aufsteigend sortieren","Zeilen absteigend sortieren","Nachgestelltes Leerzeichen k\xFCrzen","Zeile l\xF6schen","Zeileneinzug","Zeile ausr\xFCcken","Zeile oben einf\xFCgen","Zeile unten einf\xFCgen","Alle \xFCbrigen l\xF6schen","Alle rechts l\xF6schen","Zeilen verkn\xFCpfen","Zeichen um den Cursor herum transponieren","In Gro\xDFbuchstaben umwandeln","In Kleinbuchstaben umwandeln","In gro\xDFe Anfangsbuchstaben umwandeln","In Snake Case umwandeln"],"vs/editor/contrib/linkedEditing/linkedEditing":["Verkn\xFCpfte Bearbeitung starten","Hintergrundfarbe, wenn der Editor automatisch nach Typ umbenennt."],"vs/editor/contrib/links/links":["Befehl ausf\xFChren","Link folgen","BEFEHL + Klicken","STRG + Klicken","OPTION + Klicken","alt + klicken",'F\xFChren Sie den Befehl "{0}" aus.',"Fehler beim \xD6ffnen dieses Links, weil er nicht wohlgeformt ist: {0}","Fehler beim \xD6ffnen dieses Links, weil das Ziel fehlt.","Link \xF6ffnen"],"vs/editor/contrib/message/messageController":["Gibt an, ob der Editor zurzeit eine Inlinenachricht anzeigt.","Ein Bearbeiten ist im schreibgesch\xFCtzten Editor nicht m\xF6glich"],"vs/editor/contrib/multicursor/multicursor":["Cursor oberhalb hinzuf\xFCgen","Cursor oberh&&alb hinzuf\xFCgen","Cursor unterhalb hinzuf\xFCgen","Cursor unterhal&&b hinzuf\xFCgen","Cursor an Zeilenenden hinzuf\xFCgen","C&&ursor an Zeilenenden hinzuf\xFCgen","Cursor am Ende hinzuf\xFCgen","Cursor am Anfang hinzuf\xFCgen","Auswahl zur n\xE4chsten \xDCbereinstimmungssuche hinzuf\xFCgen","&&N\xE4chstes Vorkommen hinzuf\xFCgen","Letzte Auswahl zu vorheriger \xDCbereinstimmungssuche hinzuf\xFCgen","Vo&&rheriges Vorkommen hinzuf\xFCgen","Letzte Auswahl in n\xE4chste \xDCbereinstimmungssuche verschieben","Letzte Auswahl in vorherige \xDCbereinstimmungssuche verschieben","Alle Vorkommen ausw\xE4hlen und \xDCbereinstimmung suchen","Alle V&&orkommen ausw\xE4hlen","Alle Vorkommen \xE4ndern"],"vs/editor/contrib/parameterHints/parameterHints":["Parameterhinweise ausl\xF6sen"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["Symbol f\xFCr die Anzeige des n\xE4chsten Parameterhinweises.","Symbol f\xFCr die Anzeige des vorherigen Parameterhinweises.","{0}, Hinweis"],"vs/editor/contrib/peekView/peekView":["Schlie\xDFen","Hintergrundfarbe des Titelbereichs der Peek-Ansicht.","Farbe des Titels in der Peek-Ansicht.","Farbe der Titelinformationen in der Peek-Ansicht.","Farbe der Peek-Ansichtsr\xE4nder und des Pfeils.","Hintergrundfarbe der Ergebnisliste in der Peek-Ansicht.","Vordergrundfarbe f\xFCr Zeilenknoten in der Ergebnisliste der Peek-Ansicht.","Vordergrundfarbe f\xFCr Dateiknoten in der Ergebnisliste der Peek-Ansicht.","Hintergrundfarbe des ausgew\xE4hlten Eintrags in der Ergebnisliste der Peek-Ansicht.","Vordergrundfarbe des ausgew\xE4hlten Eintrags in der Ergebnisliste der Peek-Ansicht.","Hintergrundfarbe des Peek-Editors.","Hintergrundfarbe der Leiste im Peek-Editor.","Farbe f\xFCr \xDCbereinstimmungsmarkierungen in der Ergebnisliste der Peek-Ansicht.","Farbe f\xFCr \xDCbereinstimmungsmarkierungen im Peek-Editor.","Rahmen f\xFCr \xDCbereinstimmungsmarkierungen im Peek-Editor."],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["\xD6ffnen Sie zuerst einen Text-Editor, um zu einer Zeile zu wechseln.","Wechseln Sie zu Zeile {0} und Spalte {1}.","Zu Zeile {0} wechseln.","Aktuelle Zeile: {0}, Zeichen: {1}. Geben Sie eine Zeilennummer zwischen 1 und {2} ein, zu der Sie navigieren m\xF6chten.","Aktuelle Zeile: {0}, Zeichen: {1}. Geben Sie eine Zeilennummer ein, zu der Sie navigieren m\xF6chten."],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["\xD6ffnen Sie zun\xE4chst einen Text-Editor mit Symbolinformationen, um zu einem Symbol zu navigieren.","Der aktive Text-Editor stellt keine Symbolinformationen bereit.","Keine \xFCbereinstimmenden Editorsymbole.","Keine Editorsymbole.","An der Seite \xF6ffnen","Unten \xF6ffnen","Symbole ({0})","Eigenschaften ({0})","Methoden ({0})","Funktionen ({0})","Konstruktoren ({0})","Variablen ({0})","Klassen ({0})","Strukturen ({0})","Ereignisse ({0})","Operatoren ({0})","Schnittstellen ({0})","Namespaces ({0})","Pakete ({0})","Typparameter ({0})","Module ({0})","Eigenschaften ({0})","Enumerationen ({0})","Enumerationsmember ({0})","Zeichenfolgen ({0})","Dateien ({0})","Arrays ({0})","Zahlen ({0})","Boolesche Werte ({0})","Objekte ({0})","Schl\xFCssel ({0})","Felder ({0})","Konstanten ({0})"],"vs/editor/contrib/rename/rename":["Kein Ergebnis.","Ein unbekannter Fehler ist beim Aufl\xF6sen der Umbenennung eines Ortes aufgetreten.",'"{0}" wird umbenannt.',"{0} wird umbenannt.",'"{0}" erfolgreich in "{1}" umbenannt. Zusammenfassung: {2}',"Die rename-Funktion konnte die \xC4nderungen nicht anwenden.","Die rename-Funktion konnte die \xC4nderungen nicht berechnen.","Symbol umbenennen","M\xF6glichkeit aktivieren/deaktivieren, \xC4nderungen vor dem Umbenennen als Vorschau anzeigen zu lassen"],"vs/editor/contrib/rename/renameInputField":["Benennen Sie die Eingabe um. Geben Sie einen neuen Namen ein, und dr\xFCcken Sie die EINGABETASTE, um den Commit auszuf\xFChren.","{0} zur Umbenennung, {1} zur Vorschau"],"vs/editor/contrib/smartSelect/smartSelect":["Auswahl aufklappen","Auswahl &&erweitern","Markierung verkleinern","Au&&swahl verkleinern"],"vs/editor/contrib/snippet/snippetVariables":["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag","So","Mo","Di","Mi","Do","Fr","Sa","Januar","Februar","M\xE4rz","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember","Jan","Feb","M\xE4r","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"],"vs/editor/contrib/suggest/suggestController":['Das Akzeptieren von "{0}" ergab {1} zus\xE4tzliche Bearbeitungen.',"Vorschlag ausl\xF6sen","Einf\xFCgen","Einf\xFCgen","Ersetzen","Ersetzen","Einf\xFCgen","weniger anzeigen","mehr anzeigen","Gr\xF6\xDFe des Vorschlagswidgets zur\xFCcksetzen"],"vs/editor/contrib/suggest/suggestWidget":["Hintergrundfarbe des Vorschlagswidgets.","Rahmenfarbe des Vorschlagswidgets.","Vordergrundfarbe des Vorschlagswidgets.","Hintergrundfarbe des ausgew\xE4hlten Eintrags im Vorschlagswidget.","Farbe der Trefferhervorhebung im Vorschlagswidget.","Wird geladen...","Keine Vorschl\xE4ge.","{0}, Dokumente: {1}","Vorschlagen"],"vs/editor/contrib/suggest/suggestWidgetDetails":["Schlie\xDFen","Wird geladen..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["Symbol f\xFCr weitere Informationen im Vorschlags-Widget.","Weitere Informationen"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["Die Vordergrundfarbe f\xFCr Arraysymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr boolesche Symbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Klassensymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Farbsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr konstante Symbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Konstruktorsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Enumeratorsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Enumeratormembersymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Ereignissymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Feldsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Dateisymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Ordnersymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Funktionssymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Schnittstellensymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Schl\xFCsselsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Schl\xFCsselwortsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Methodensymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Modulsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Namespacesymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr NULL-Symbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Zahlensymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Objektsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Operatorsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Paketsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Eigenschaftensymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Referenzsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Codeausschnittsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Zeichenfolgensymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Struktursymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Textsymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Typparametersymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr Einheitensymbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt.","Die Vordergrundfarbe f\xFCr variable Symbole. Diese Symbole werden in den Widgets f\xFCr Gliederung, Breadcrumbs und Vorschl\xE4ge angezeigt."],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["TAB-Umschalttaste verschiebt Fokus","Beim Dr\xFCcken auf Tab wird der Fokus jetzt auf das n\xE4chste fokussierbare Element verschoben","Beim Dr\xFCcken von Tab wird jetzt das Tabulator-Zeichen eingef\xFCgt"],"vs/editor/contrib/tokenization/tokenization":["Entwickler: Force Retokenize"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["Ungew\xF6hnliche Zeilentrennzeichen","Ungew\xF6hnliche Zeilentrennzeichen erkannt",`Diese Datei enth\xE4lt mindestens ein ung\xFCltiges Zeilenabschlusszeichen, z.\xA0B. Zeilentrennzeichen (LS) oder Absatztrennzeichen (PS).\r +\r +Es wird empfohlen, diese Zeichen aus der Datei zu entfernen. Die betreffende Einstellung kann \xFCber "editor.unusualLineTerminators" konfiguriert werden.`,"Diese Datei korrigieren","Problem f\xFCr diese Datei ignorieren"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["Hintergrundfarbe eines Symbols beim Lesezugriff, z.B. beim Lesen einer Variablen. Die Farbe darf nicht deckend sein, damit sie nicht die zugrunde liegenden Dekorationen verdeckt.","Hintergrundfarbe eines Symbols bei Schreibzugriff, z.B. beim Schreiben in eine Variable. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Randfarbe eines Symbols beim Lesezugriff, wie etwa beim Lesen einer Variablen.","Randfarbe eines Symbols beim Schreibzugriff, wie etwa beim Schreiben einer Variablen.","\xDCbersichtslinealmarkerfarbd f\xFCr das Hervorheben von Symbolen. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","\xDCbersichtslinealmarkerfarbe f\xFCr Symbolhervorhebungen bei Schreibzugriff. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Gehe zur n\xE4chsten Symbolhervorhebungen","Gehe zur vorherigen Symbolhervorhebungen","Symbol-Hervorhebung ein-/ausschalten"],"vs/editor/contrib/wordOperations/wordOperations":["Wort l\xF6schen"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["Au\xDFerkraftsetzungen f\xFCr die Standardsprachkonfiguration","Zu \xFCberschreibende Editor-Einstellungen f\xFCr eine Sprache konfigurieren.","Diese Einstellung unterst\xFCtzt keine sprachspezifische Konfiguration.","Eine leere Eigenschaft kann nicht registriert werden.",'"{0}" kann nicht registriert werden. Stimmt mit dem Eigenschaftsmuster "\\\\[.*\\\\]$" zum Beschreiben sprachspezifischer Editor-Einstellungen \xFCberein. Verwenden Sie den Beitrag "configurationDefaults".','{0}" kann nicht registriert werden. Diese Eigenschaft ist bereits registriert.'],"vs/platform/contextkey/browser/contextKeyService":["Ein Befehl, der Informationen zu Kontextschl\xFCsseln zur\xFCckgibt"],"vs/platform/contextkey/common/contextkeys":["Gibt an, ob Windows als Betriebssystem verwendet wird."],"vs/platform/keybinding/common/abstractKeybindingService":["({0}) wurde gedr\xFCckt. Es wird auf die zweite Taste in der Kombination gewartet...","Die Tastenkombination ({0}, {1}) ist kein Befehl."],"vs/platform/list/browser/listService":["Workbench","Ist unter Windows und Linux der STRG-Taste und unter macOS der Befehlstaste zugeordnet.","Ist unter Windows und Linux der ALT-Taste und unter macOS der Wahltaste zugeordnet.",'Der Modifizierer zum Hinzuf\xFCgen eines Elements in B\xE4umen und Listen zu einer Mehrfachauswahl mit der Maus (zum Beispiel im Explorer, in ge\xF6ffneten Editoren und in der SCM-Ansicht). Die Mausbewegung "Seitlich \xF6ffnen" wird \u2013 sofern unterst\xFCtzt \u2013 so angepasst, dass kein Konflikt mit dem Modifizierer f\xFCr Mehrfachauswahl entsteht.',"Steuert, wie Elemente in Strukturen und Listen mithilfe der Maus ge\xF6ffnet werden (sofern unterst\xFCtzt). Bei \xFCbergeordneten Elementen, deren untergeordnete Elemente sich in Strukturen befinden, steuert diese Einstellung, ob ein Einfachklick oder ein Doppelklick das \xFCbergeordnete Elemente erweitert. Beachten Sie, dass einige Strukturen und Listen diese Einstellung ggf. ignorieren, wenn sie nicht zutrifft.","Steuert, ob Listen und Strukturen ein horizontales Scrollen in der Workbench unterst\xFCtzen. Warnung: Das Aktivieren dieser Einstellung kann sich auf die Leistung auswirken.","Steuert den Struktureinzug in Pixeln.","Steuert, ob die Struktur Einzugsf\xFChrungslinien rendern soll.","Steuert, ob Listen und Strukturen einen optimierten Bildlauf verwenden.","Bei der einfachen Tastaturnavigation werden Elemente in den Fokus genommen, die mit der Tastatureingabe \xFCbereinstimmen. Die \xDCbereinstimmungen gelten nur f\xFCr Pr\xE4fixe.","Hervorheben von Tastaturnavigationshervorgebungselemente, die mit der Tastatureingabe \xFCbereinstimmen. Beim nach oben und nach unten Navigieren werden nur die hervorgehobenen Elemente durchlaufen.","Durch das Filtern der Tastaturnavigation werden alle Elemente herausgefiltert und ausgeblendet, die nicht mit der Tastatureingabe \xFCbereinstimmen.",'Steuert die Tastaturnavigation in Listen und Strukturen in der Workbench. Kann "simple" (einfach), "highlight" (hervorheben) und "filter" (filtern) sein.','Legt fest, ob die Tastaturnavigation in Listen und Strukturen automatisch durch Eingaben ausgel\xF6st wird. Wenn der Wert auf "false" festgelegt ist, wird die Tastaturnavigation nur ausgel\xF6st, wenn der Befehl "list.toggleKeyboardNavigation" ausgef\xFChrt wird. Diesem Befehl k\xF6nnen Sie eine Tastenkombination zuweisen.',"Steuert, wie Strukturordner beim Klicken auf die Ordnernamen erweitert werden. Beachten Sie, dass einige Strukturen und Listen diese Einstellung ggf. ignorieren, wenn sie nicht zutrifft."],"vs/platform/markers/common/markers":["Fehler","Warnung","Info"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","zuletzt verwendet","andere Befehle","Der Befehl {0} hat einen Fehler ausgel\xF6st ({1})."],"vs/platform/quickinput/browser/helpQuickAccess":["Globale Befehle","Editor-Befehle","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["Allgemeine Vordergrundfarbe. Diese Farbe wird nur verwendet, wenn sie nicht durch eine Komponente \xFCberschrieben wird.","Allgemeine Vordergrundfarbe f\xFCr Fehlermeldungen. Diese Farbe wird nur verwendet, wenn sie nicht durch eine Komponente \xFCberschrieben wird.","Die f\xFCr Symbole in der Workbench verwendete Standardfarbe.","Allgemeine Rahmenfarbe f\xFCr fokussierte Elemente. Diese Farbe wird nur verwendet, wenn sie nicht durch eine Komponente \xFCberschrieben wird.","Ein zus\xE4tzlicher Rahmen um Elemente, mit dem diese von anderen getrennt werden, um einen gr\xF6\xDFeren Kontrast zu erreichen.","Ein zus\xE4tzlicher Rahmen um aktive Elemente, mit dem diese von anderen getrennt werden, um einen gr\xF6\xDFeren Kontrast zu erreichen.","Vordergrundfarbe f\xFCr Links im Text.","Hintergrundfarbe f\xFCr Codebl\xF6cke im Text.","Schattenfarbe von Widgets wie zum Beispiel Suchen/Ersetzen innerhalb des Editors.","Hintergrund f\xFCr Eingabefeld.","Vordergrund f\xFCr Eingabefeld.","Rahmen f\xFCr Eingabefeld.","Rahmenfarbe f\xFCr aktivierte Optionen in Eingabefeldern.","Hintergrundfarbe f\xFCr aktivierte Optionen in Eingabefeldern.","Vordergrundfarbe f\xFCr aktivierte Optionen in Eingabefeldern.","Hintergrundfarbe bei der Eingabevalidierung f\xFCr den Schweregrad der Information.","Vordergrundfarbe bei der Eingabevalidierung f\xFCr den Schweregrad der Information.","Rahmenfarbe bei der Eingabevalidierung f\xFCr den Schweregrad der Information.","Hintergrundfarbe bei der Eingabevalidierung f\xFCr den Schweregrad der Warnung.","Vordergrundfarbe bei der Eingabevalidierung f\xFCr den Schweregrad der Warnung.","Rahmenfarbe bei der Eingabevalidierung f\xFCr den Schweregrad der Warnung.","Hintergrundfarbe bei der Eingabevalidierung f\xFCr den Schweregrad des Fehlers.","Vordergrundfarbe bei der Eingabevalidierung f\xFCr den Schweregrad des Fehlers.","Rahmenfarbe bei der Eingabevalidierung f\xFCr den Schweregrad des Fehlers.","Hintergrund f\xFCr Dropdown.","Vordergrund f\xFCr Dropdown.","Vordergrundfarbe der Schaltfl\xE4che.","Hintergrundfarbe der Schaltfl\xE4che.","Hintergrundfarbe der Schaltfl\xE4che, wenn darauf gezeigt wird.","Hintergrundfarbe f\xFCr Badge. Badges sind kurze Info-Texte, z.B. f\xFCr Anzahl Suchergebnisse.","Vordergrundfarbe f\xFCr Badge. Badges sind kurze Info-Texte, z.B. f\xFCr Anzahl Suchergebnisse.","Schatten der Scrollleiste, um anzuzeigen, dass die Ansicht gescrollt wird.","Hintergrundfarbe vom Scrollbar-Schieber","Hintergrundfarbe des Schiebereglers, wenn darauf gezeigt wird.","Hintergrundfarbe des Schiebereglers, wenn darauf geklickt wird.","Hintergrundfarbe des Fortschrittbalkens, der f\xFCr zeitintensive Vorg\xE4nge angezeigt werden kann.","Hintergrundfarbe f\xFCr Fehlertext im Editor. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Vordergrundfarbe von Fehlerunterstreichungen im Editor.","Randfarbe von Fehlerfeldern im Editor.","Hintergrundfarbe f\xFCr Warnungstext im Editor. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Vordergrundfarbe von Warnungsunterstreichungen im Editor.","Randfarbe der Warnfelder im Editor.","Hintergrundfarbe f\xFCr Infotext im Editor. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Vordergrundfarbe von Informationsunterstreichungen im Editor.","Randfarbe der Infofelder im Editor.","Vordergrundfarbe der Hinweisunterstreichungen im Editor.","Randfarbe der Hinweisfelder im Editor.","Hintergrundfarbe des Editors.","Standardvordergrundfarbe des Editors.","Hintergrundfarbe von Editor-Widgets wie zum Beispiel Suchen/Ersetzen.","Vordergrundfarbe f\xFCr Editorwidgets wie Suchen/Ersetzen.","Rahmenfarbe von Editorwigdets. Die Farbe wird nur verwendet, wenn f\xFCr das Widget ein Rahmen verwendet wird und die Farbe nicht von einem Widget \xFCberschrieben wird.","Rahmenfarbe der Gr\xF6\xDFenanpassungsleiste von Editorwigdets. Die Farbe wird nur verwendet, wenn f\xFCr das Widget ein Gr\xF6\xDFenanpassungsrahmen verwendet wird und die Farbe nicht von einem Widget au\xDFer Kraft gesetzt wird.","Schnellauswahl der Hintergrundfarbe. Im Widget f\xFCr die Schnellauswahl sind Auswahlelemente wie die Befehlspalette enthalten.","Vordergrundfarbe der Schnellauswahl. Im Widget f\xFCr die Schnellauswahl sind Auswahlelemente wie die Befehlspalette enthalten.","Hintergrundfarbe f\xFCr den Titel der Schnellauswahl. Im Widget f\xFCr die Schnellauswahl sind Auswahlelemente wie die Befehlspalette enthalten.","Die Hintergrundfarbe der Schnellauswahl f\xFCr das fokussierte Element.","Schnellauswahlfarbe f\xFCr das Gruppieren von Bezeichnungen.","Schnellauswahlfarbe f\xFCr das Gruppieren von Rahmen.","Farbe der Editor-Auswahl.","Farbe des gew\xE4hlten Text f\xFCr einen hohen Kontrast","Die Farbe der Auswahl befindet sich in einem inaktiven Editor. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegende Dekorationen verdeckt.","Farbe f\xFCr Bereiche mit dem gleichen Inhalt wie die Auswahl. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Randfarbe f\xFCr Bereiche, deren Inhalt der Auswahl entspricht.","Farbe des aktuellen Suchergebnisses.","Farbe der anderen Suchergebnisse. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Farbe des Bereichs, der die Suche eingrenzt. Die Farbe darf nicht deckend sein, damit sie nicht die zugrunde liegenden Dekorationen verdeckt.","Randfarbe des aktuellen Suchergebnisses.","Randfarbe der anderen Suchtreffer.","Rahmenfarbe des Bereichs, der die Suche eingrenzt. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Hervorhebung unterhalb des Worts, f\xFCr das ein Hoverelement angezeigt wird. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Hintergrundfarbe des Editor-Mauszeigers.","Vordergrundfarbe des Editor-Mauszeigers","Rahmenfarbe des Editor-Mauszeigers.","Hintergrundfarbe der Hoverstatusleiste des Editors.","Farbe der aktiven Links.","Vordergrundfarbe f\xFCr Inlinehinweise","Hintergrundfarbe f\xFCr Inlinehinweise",'Die f\xFCr das Aktionssymbol "Gl\xFChbirne" verwendete Farbe.','Die f\xFCr das Aktionssymbol "Automatische Gl\xFChbirnenkorrektur" verwendete Farbe.',"Hintergrundfarbe f\xFCr eingef\xFCgten Text. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Hintergrundfarbe f\xFCr Text, der entfernt wurde. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Konturfarbe f\xFCr eingef\xFCgten Text.","Konturfarbe f\xFCr entfernten Text.","Die Rahmenfarbe zwischen zwei Text-Editoren.","Farbe der diagonalen F\xFCllung des Vergleichs-Editors. Die diagonale F\xFCllung wird in Ansichten mit parallelem Vergleich verwendet.","Hintergrundfarbe der Liste/Struktur f\xFCr das fokussierte Element, wenn die Liste/Struktur aktiv ist. Eine aktive Liste/Struktur hat Tastaturfokus, eine inaktive hingegen nicht.","Vordergrundfarbe der Liste/Struktur f\xFCr das fokussierte Element, wenn die Liste/Struktur aktiv ist. Eine aktive Liste/Struktur hat Tastaturfokus, eine inaktive hingegen nicht.","Konturfarbe der Liste/Struktur f\xFCr das fokussierte Element, wenn die Liste/Struktur aktiv ist. Eine aktive Liste/Struktur hat Tastaturfokus, eine inaktive hingegen nicht.","Hintergrundfarbe der Liste/Struktur f\xFCr das ausgew\xE4hlte Element, wenn die Liste/Struktur aktiv ist. Eine aktive Liste/Struktur hat Tastaturfokus, eine inaktive hingegen nicht.","Vordergrundfarbe der Liste/Struktur f\xFCr das ausgew\xE4hlte Element, wenn die Liste/Struktur aktiv ist. Eine aktive Liste/Struktur hat Tastaturfokus, eine inaktive hingegen nicht.","Hintergrundfarbe der Liste/Struktur f\xFCr das ausgew\xE4hlte Element, wenn die Liste/Struktur inaktiv ist. Eine aktive Liste/Struktur hat Tastaturfokus, eine inaktive hingegen nicht.","Vordergrundfarbe der Liste/Struktur f\xFCr das ausgew\xE4hlte Element, wenn die Liste/Baumstruktur inaktiv ist. Eine aktive Liste/Baumstruktur hat Tastaturfokus, eine inaktive hingegen nicht.","Hintergrundfarbe der Liste/Struktur f\xFCr das fokussierte Element, wenn die Liste/Struktur inaktiv ist. Eine aktive Liste/Struktur hat Tastaturfokus, eine inaktive hingegen nicht.","Konturfarbe der Liste/Struktur f\xFCr das fokussierte Element, wenn die Liste/Struktur inaktiv ist. Eine aktive Liste/Struktur hat Tastaturfokus, eine inaktive hingegen nicht.","Hintergrund der Liste/Struktur, wenn mit der Maus auf Elemente gezeigt wird.","Vordergrund der Liste/Struktur, wenn mit der Maus auf Elemente gezeigt wird.","Drag & Drop-Hintergrund der Liste/Struktur, wenn Elemente mithilfe der Maus verschoben werden.","Vordergrundfarbe der Liste/Struktur zur Trefferhervorhebung beim Suchen innerhalb der Liste/Struktur.","Hintergrundfarbe des Typfilterwidgets in Listen und Strukturen.","Konturfarbe des Typfilterwidgets in Listen und Strukturen.","Konturfarbe des Typfilterwidgets in Listen und Strukturen, wenn es keine \xDCbereinstimmungen gibt.","Strukturstrichfarbe f\xFCr die Einzugsf\xFChrungslinien.","Strukturstrichfarbe f\xFCr die Einzugsf\xFChrungslinien.","Rahmenfarbe von Men\xFCs.","Vordergrundfarbe von Men\xFCelementen.","Hintergrundfarbe von Men\xFCelementen.","Vordergrundfarbe des ausgew\xE4hlten Men\xFCelements im Men\xFC.","Hintergrundfarbe des ausgew\xE4hlten Men\xFCelements im Men\xFC.","Rahmenfarbe des ausgew\xE4hlten Men\xFCelements im Men\xFC.","Farbe eines Trenner-Men\xFCelements in Men\xFCs.","Hervorhebungs-Hintergrundfarbe eines Codeausschnitt-Tabstopps.","Hervorhebungs-Rahmenfarbe eines Codeausschnitt-Tabstopps.","Hervorhebungs-Hintergrundfarbe des letzten Tabstopps eines Codeausschnitts.","Rahmenfarbe zur Hervorhebung des letzten Tabstopps eines Codeausschnitts.","\xDCbersichtslinealmarkerfarbe f\xFCr das Suchen von \xDCbereinstimmungen. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","\xDCbersichtslinealmarkerfarbe f\xFCr das Hervorheben der Auswahl. Die Farbe darf nicht deckend sein, weil sie sonst die zugrunde liegenden Dekorationen verdeckt.","Minimap-Markerfarbe f\xFCr gefundene \xDCbereinstimmungen.","Minimap-Markerfarbe f\xFCr die Editorauswahl.","Minimapmarkerfarbe f\xFCr Fehler","Minimapmarkerfarbe f\xFCr Warnungen","Hintergrundfarbe der Minimap.","Hintergrundfarbe des Minimap-Schiebereglers.","Hintergrundfarbe des Minimap-Schiebereglers beim Daraufzeigen.","Hintergrundfarbe des Minimap-Schiebereglers, wenn darauf geklickt wird.","Die Farbe, die f\xFCr das Problemfehlersymbol verwendet wird.","Die Farbe, die f\xFCr das Problemwarnsymbol verwendet wird.","Die Farbe, die f\xFCr das Probleminfosymbol verwendet wird."],"vs/platform/theme/common/iconRegistry":["Die ID der zu verwendenden Schriftart. Sofern nicht festgelegt, wird die zuerst definierte Schriftart verwendet.","Das der Symboldefinition zugeordnete Schriftzeichen.","Symbol f\xFCr Aktion zum Schlie\xDFen in Widgets"],"vs/platform/undoRedo/common/undoRedoService":["Die folgenden Dateien wurden geschlossen und auf dem Datentr\xE4ger ge\xE4ndert: {0}.","Die folgenden Dateien wurden auf inkompatible Weise ge\xE4ndert: {0}.",'"{0}" konnte nicht f\xFCr alle Dateien r\xFCckg\xE4ngig gemacht werden. {1}','"{0}" konnte nicht f\xFCr alle Dateien r\xFCckg\xE4ngig gemacht werden. {1}','"{0}" konnte nicht f\xFCr alle Dateien r\xFCckg\xE4ngig gemacht werden, da \xC4nderungen an {1} vorgenommen wurden.','"{0}" konnte nicht f\xFCr alle Dateien r\xFCckg\xE4ngig gemacht werden, weil bereits ein Vorgang zum R\xFCckg\xE4ngigmachen oder Wiederholen f\xFCr "{1}" durchgef\xFChrt wird.','"{0}" konnte nicht f\xFCr alle Dateien r\xFCckg\xE4ngig gemacht werden, weil in der Zwischenzeit bereits ein Vorgang zum R\xFCckg\xE4ngigmachen oder Wiederholen durchgef\xFChrt wurde.','M\xF6chten Sie "{0}" f\xFCr alle Dateien r\xFCckg\xE4ngig machen?',"In {0} Dateien r\xFCckg\xE4ngig machen","Datei r\xFCckg\xE4ngig machen","Abbrechen",'"{0}" konnte nicht r\xFCckg\xE4ngig gemacht werden, weil bereits ein Vorgang zum R\xFCckg\xE4ngigmachen oder Wiederholen durchgef\xFChrt wird.','M\xF6chten Sie "{0}" r\xFCckg\xE4ngig machen?',"R\xFCckg\xE4ngig machen","Abbrechen",'"{0}" konnte nicht in allen Dateien wiederholt werden. {1}','"{0}" konnte nicht in allen Dateien wiederholt werden. {1}','"{0}" konnte nicht in allen Dateien wiederholt werden, da \xC4nderungen an {1} vorgenommen wurden.','"{0}" konnte nicht f\xFCr alle Dateien wiederholt werden, weil bereits ein Vorgang zum R\xFCckg\xE4ngigmachen oder Wiederholen f\xFCr "{1}" durchgef\xFChrt wird.','"{0}" konnte nicht f\xFCr alle Dateien wiederholt werden, weil in der Zwischenzeit bereits ein Vorgang zum R\xFCckg\xE4ngigmachen oder Wiederholen durchgef\xFChrt wurde.','"{0}" konnte nicht wiederholt werden, weil bereits ein Vorgang zum R\xFCckg\xE4ngigmachen oder Wiederholen durchgef\xFChrt wird.']}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.es.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.es.js new file mode 100644 index 0000000..d71fd7d --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.es.js @@ -0,0 +1,8 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/define("vs/editor/editor.main.nls.es",{"vs/base/browser/ui/actionbar/actionViewItems":["{0} ({1})"],"vs/base/browser/ui/findinput/findInput":["entrada"],"vs/base/browser/ui/findinput/findInputCheckboxes":["Coincidir may\xFAsculas y min\xFAsculas","Solo palabras completas","Usar expresi\xF3n regular"],"vs/base/browser/ui/findinput/replaceInput":["entrada","Conservar may/min"],"vs/base/browser/ui/iconLabel/iconLabel":["Cargando..."],"vs/base/browser/ui/inputbox/inputBox":["Error: {0}","Advertencia: {0}","Informaci\xF3n: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["Sin enlazar"],"vs/base/browser/ui/menu/menu":["{0} ({1})"],"vs/base/browser/ui/tree/abstractTree":["Borrar","Desactivar filtro en tipo","Activar filtro en el tipo","No se encontraron elementos","{0} de {1} elementos coincidentes"],"vs/base/common/actions":["(vac\xEDo)"],"vs/base/common/errorMessage":["{0}: {1}","Error del sistema ({0})","Se ha producido un error desconocido. Consulte el registro para obtener m\xE1s detalles.","Se ha producido un error desconocido. Consulte el registro para obtener m\xE1s detalles.","{0} ({1} errores en total)","Se ha producido un error desconocido. Consulte el registro para obtener m\xE1s detalles."],"vs/base/common/keybindingLabels":["Ctrl","May\xFAs","Alt","Windows","Ctrl","May\xFAs","Alt","Super","Control","May\xFAs","Alt","Comando","Control","May\xFAs","Alt","Windows","Control","May\xFAs","Alt","Super"],"vs/base/parts/quickinput/browser/quickInput":["Atr\xE1s","{0}/{1}","Escriba para restringir los resultados.","{0} resultados","{0} seleccionados","Aceptar","Personalizado","Atr\xE1s ({0})","Atr\xE1s"],"vs/base/parts/quickinput/browser/quickInputList":["Entrada r\xE1pida"],"vs/editor/browser/controller/coreCommands":["Anclar al final incluso cuando se vayan a l\xEDneas m\xE1s largas","Anclar al final incluso cuando se vayan a l\xEDneas m\xE1s largas"],"vs/editor/browser/controller/textAreaHandler":["editor","El editor no es accesible en este momento. Pulse {0} para ver las opciones."],"vs/editor/browser/core/keybindingCancellation":['Indica si el editor ejecuta una operaci\xF3n que se puede cancelar como, por ejemplo, "Inspeccionar referencias"'],"vs/editor/browser/editorExtensions":["&&Deshacer","Deshacer","&&Rehacer","Rehacer","&&Seleccionar todo","Seleccionar todo"],"vs/editor/browser/widget/codeEditorWidget":["El n\xFAmero de cursores se ha limitado a {0}."],"vs/editor/browser/widget/diffEditorWidget":["Decoraci\xF3n de l\xEDnea para las inserciones en el editor de diferencias.","Decoraci\xF3n de l\xEDnea para las eliminaciones en el editor de diferencias.","Los archivos no se pueden comparar porque uno de ellos es demasiado grande."],"vs/editor/browser/widget/diffReview":['Icono para "Insertar" en la revisi\xF3n de diferencias.','Icono para "Quitar" en la revisi\xF3n de diferencias.','Icono para "Cerrar" en la revisi\xF3n de diferencias.',"Cerrar","no se han cambiado l\xEDneas","1 l\xEDnea cambiada","{0} l\xEDneas cambiadas","Diferencia {0} de {1}: l\xEDnea original {2}, {3}, l\xEDnea modificada {4}, {5}","vac\xEDo","{0} l\xEDnea sin cambios {1}","{0} l\xEDnea original {1} l\xEDnea modificada {2}","+ {0} l\xEDnea modificada {1}","- {0} l\xEDnea original {1}","Ir a la siguiente diferencia","Ir a la diferencia anterior"],"vs/editor/browser/widget/inlineDiffMargin":["Copiar l\xEDneas eliminadas","Copiar l\xEDnea eliminada","Copiar la l\xEDnea eliminada ({0})","Revertir este cambio","Copiar la l\xEDnea eliminada ({0})"],"vs/editor/common/config/commonEditorConfig":["Editor",'El n\xFAmero de espacios a los que equivale una tabulaci\xF3n. Este valor se invalida en funci\xF3n del contenido del archivo cuando "#editor.detectIndentation#" est\xE1 activado.','Insertar espacios al presionar "TAB". Este valor se invalida en funci\xF3n del contenido del archivo cuando "#editor.detectIndentation#" est\xE1 activado. ','Controla si "#editor.tabSize#" y "#editor.insertSpaces#" se detectar\xE1n autom\xE1ticamente al abrir un archivo en funci\xF3n del contenido de este.',"Quitar el espacio en blanco final autoinsertado.","Manejo especial para archivos grandes para desactivar ciertas funciones de memoria intensiva.","Habilita sugerencias basadas en palabras.","Sugerir palabras solo del documento activo.","Sugerir palabras de todos los documentos abiertos del mismo idioma.","Sugerir palabras de todos los documentos abiertos.","Controla de qu\xE9 documentos se calculan las finalizaciones basadas en palabras.","El resaltado sem\xE1ntico est\xE1 habilitado para todos los temas de color.","El resaltado sem\xE1ntico est\xE1 deshabilitado para todos los temas de color.",'El resaltado sem\xE1ntico est\xE1 configurado con el valor "semanticHighlighting" del tema de color actual.',"Controla si se muestra semanticHighlighting para los idiomas que lo admiten.",'Mantiene abiertos los editores interactivos, incluso al hacer doble clic en su contenido o presionar "Escape".',"Las lineas por encima de esta longitud no se tokenizar\xE1n por razones de rendimiento.","Tiempo de espera en milisegundos despu\xE9s del cual se cancela el c\xE1lculo de diferencias. Utilice 0 para no usar tiempo de espera.","Controla si el editor de diferencias muestra las diferencias en paralelo o alineadas.","Cuando est\xE1 habilitado, el editor de diferencias omite los cambios en los espacios en blanco iniciales o finales.","Controla si el editor de diferencias muestra los indicadores +/- para los cambios agregados o quitados.","Controla si el editor muestra CodeLens.","Las l\xEDneas no se ajustar\xE1n nunca.","Las l\xEDneas se ajustar\xE1n en el ancho de la ventanilla.",'Las l\xEDneas se ajustar\xE1n en funci\xF3n de la configuraci\xF3n de "#editor.wordWrap#".'],"vs/editor/common/config/editorOptions":["El editor usar\xE1 API de plataforma para detectar cu\xE1ndo est\xE1 conectado un lector de pantalla.","El editor se optimizar\xE1 de forma permanente para su uso con un lector de pantalla. El ajuste de l\xEDneas se deshabilitar\xE1.","El editor nunca se optimizar\xE1 para su uso con un lector de pantalla.","Controla si el editor se debe ejecutar en un modo optimizado para lectores de pantalla. Si se activa, se deshabilitar\xE1 el ajuste de l\xEDneas.","Controla si se inserta un car\xE1cter de espacio al comentar.","Controla si las l\xEDneas vac\xEDas deben ignorarse con la opci\xF3n de alternar, agregar o quitar acciones para los comentarios de l\xEDnea.","Controla si al copiar sin selecci\xF3n se copia la l\xEDnea actual.","Controla si el cursor debe saltar para buscar coincidencias mientras se escribe.","Controla si la cadena de b\xFAsqueda del widget de b\xFAsqueda se inicializa desde la selecci\xF3n del editor.","No activar nunca Buscar en la selecci\xF3n autom\xE1ticamente (predeterminado)","Activar siempre autom\xE1ticamente Buscar en la selecci\xF3n","Active Buscar en la selecci\xF3n autom\xE1ticamente cuando se seleccionen varias l\xEDneas de contenido.","Controla la condici\xF3n para activar la b\xFAsqueda en la selecci\xF3n de forma autom\xE1tica.","Controla si el widget de b\xFAsqueda debe leer o modificar el Portapapeles de b\xFAsqueda compartido en macOS.","Controla si Encontrar widget debe agregar m\xE1s l\xEDneas en la parte superior del editor. Si es true, puede desplazarse m\xE1s all\xE1 de la primera l\xEDnea cuando Encontrar widget est\xE1 visible.","Controla si la b\xFAsqueda se reinicia autom\xE1ticamente desde el principio (o el final) cuando no se encuentran m\xE1s coincidencias.",'Habilita o deshabilita las ligaduras tipogr\xE1ficas (caracter\xEDsticas de fuente "calt" y "liga"). C\xE1mbielo a una cadena para el control espec\xEDfico de la propiedad de CSS "font-feature-settings".','Propiedad de CSS "font-feature-settings" expl\xEDcita. En su lugar, puede pasarse un valor booleano si solo es necesario activar o desactivar las ligaduras.','Configura las ligaduras tipogr\xE1ficas o las caracter\xEDsticas de fuente. Puede ser un valor booleano para habilitar o deshabilitar las ligaduras o bien una cadena para el valor de la propiedad "font-feature-settings" de CSS.',"Controla el tama\xF1o de fuente en p\xEDxeles.",'Solo se permiten las palabras clave "normal" y "negrita" o los n\xFAmeros entre 1 y 1000.','Controla el grosor de la fuente. Acepta las palabras clave "normal" y "negrita" o los n\xFAmeros entre 1 y 1000.',"Mostrar vista de inspecci\xF3n de los resultados (predeterminado)","Ir al resultado principal y mostrar una vista de inspecci\xF3n","Vaya al resultado principal y habilite la navegaci\xF3n sin peek para otros",'Esta configuraci\xF3n est\xE1 en desuso. Use configuraciones separadas como "editor.editor.gotoLocation.multipleDefinitions" o "editor.editor.gotoLocation.multipleImplementations" en su lugar.','Controla el comportamiento del comando "Ir a definici\xF3n" cuando existen varias ubicaciones de destino.','Controla el comportamiento del comando "Ir a definici\xF3n de tipo" cuando existen varias ubicaciones de destino.','Controla el comportamiento del comando "Ir a declaraci\xF3n" cuando existen varias ubicaciones de destino.','Controla el comportamiento del comando "Ir a implementaciones" cuando existen varias ubicaciones de destino.','Controla el comportamiento del comando "Ir a referencias" cuando existen varias ubicaciones de destino.','Identificador de comando alternativo que se ejecuta cuando el resultado de "Ir a definici\xF3n" es la ubicaci\xF3n actual.','Id. de comando alternativo que se est\xE1 ejecutando cuando el resultado de "Ir a definici\xF3n de tipo" es la ubicaci\xF3n actual.','Id. de comando alternativo que se est\xE1 ejecutando cuando el resultado de "Ir a declaraci\xF3n" es la ubicaci\xF3n actual.','Id. de comando alternativo que se est\xE1 ejecutando cuando el resultado de "Ir a implementaci\xF3n" es la ubicaci\xF3n actual.','Identificador de comando alternativo que se ejecuta cuando el resultado de "Ir a referencia" es la ubicaci\xF3n actual.',"Controla si se muestra la informaci\xF3n al mantener el puntero sobre un elemento.","Controla el retardo en milisegundos despu\xE9s del cual se muestra la informaci\xF3n al mantener el puntero sobre un elemento.","Controla si la informaci\xF3n que aparece al mantener el puntero sobre un elemento permanece visible al mover el mouse sobre este.","Habilita la bombilla de acci\xF3n de c\xF3digo en el editor.","Habilita las sugerencias insertadas en el editor.",'Controla el tama\xF1o de fuente de las sugerencias insertadas en el editor. Cuando se establece en "0", se usa el 90\xA0% de "#editor.fontSize#".',"Controla la familia de fuentes de las sugerencias insertadas en el editor.","Controla la altura de l\xEDnea. Usa 0 para utilizar la altura del tama\xF1o de fuente.","Controla si se muestra el minimapa.","El minimapa tiene el mismo tama\xF1o que el contenido del editor (y podr\xEDa desplazarse).","El minimapa se estirar\xE1 o reducir\xE1 seg\xFAn sea necesario para ocupar la altura del editor (sin desplazamiento).","El minimapa se reducir\xE1 seg\xFAn sea necesario para no ser nunca m\xE1s grande que el editor (sin desplazamiento).","Controla el tama\xF1o del minimapa.","Controla en qu\xE9 lado se muestra el minimapa.","Controla cu\xE1ndo se muestra el control deslizante del minimapa.","Escala del contenido dibujado en el minimapa: 1, 2 o 3.","Represente los caracteres reales en una l\xEDnea, por oposici\xF3n a los bloques de color.","Limite el ancho del minimapa para representar como mucho un n\xFAmero de columnas determinado.","Controla la cantidad de espacio entre el borde superior del editor y la primera l\xEDnea.","Controla el espacio entre el borde inferior del editor y la \xFAltima l\xEDnea.","Habilita un elemento emergente que muestra documentaci\xF3n de los par\xE1metros e informaci\xF3n de los tipos mientras escribe.","Controla si el men\xFA de sugerencias de par\xE1metros se cicla o se cierra al llegar al final de la lista.","Habilita sugerencias r\xE1pidas en las cadenas.","Habilita sugerencias r\xE1pidas en los comentarios.","Habilita sugerencias r\xE1pidas fuera de las cadenas y los comentarios.","Controla si deben mostrarse sugerencias autom\xE1ticamente mientras se escribe.","Los n\xFAmeros de l\xEDnea no se muestran.","Los n\xFAmeros de l\xEDnea se muestran como un n\xFAmero absoluto.","Los n\xFAmeros de l\xEDnea se muestran como distancia en l\xEDneas a la posici\xF3n del cursor.","Los n\xFAmeros de l\xEDnea se muestran cada 10 l\xEDneas.","Controla la visualizaci\xF3n de los n\xFAmeros de l\xEDnea.","N\xFAmero de caracteres monoespaciales en los que se representar\xE1 esta regla del editor.","Color de esta regla del editor.","Muestra reglas verticales despu\xE9s de un cierto n\xFAmero de caracteres monoespaciados. Usa m\xFAltiples valores para mostrar m\xFAltiples reglas. Si la matriz est\xE1 vac\xEDa, no se muestran reglas.","Inserte la sugerencia sin sobrescribir el texto a la derecha del cursor.","Inserte la sugerencia y sobrescriba el texto a la derecha del cursor.","Controla si las palabras se sobrescriben al aceptar la finalizaci\xF3n. Tenga en cuenta que esto depende de las extensiones que participan en esta caracter\xEDstica.","Controla si el filtrado y la ordenaci\xF3n de sugerencias se tienen en cuenta para los errores ortogr\xE1ficos peque\xF1os.","Controla si la ordenaci\xF3n de palabras mejora lo que aparece cerca del cursor.",'Controla si las selecciones de sugerencias recordadas se comparten entre m\xFAltiples \xE1reas de trabajo y ventanas (necesita "#editor.suggestSelection#").',"Controla si un fragmento de c\xF3digo activo impide sugerencias r\xE1pidas.","Controla si mostrar u ocultar iconos en sugerencias.","Controla la visibilidad de la barra de estado en la parte inferior del widget de sugerencias.","Controla si los detalles de sugerencia se muestran incorporados con la etiqueta o solo en el widget de detalles.","La configuraci\xF3n est\xE1 en desuso. Ahora puede cambiarse el tama\xF1o del widget de sugerencias.",'Esta configuraci\xF3n est\xE1 en desuso. Use configuraciones separadas como "editor.suggest.showKeyword" o "editor.suggest.showSnippets" en su lugar.','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "method".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de "funci\xF3n".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "constructor".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "field".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "variable".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "class".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "struct".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "interface".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "module".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "property".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "event".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "operator".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "unit".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de "value".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "constant".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "enum".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "enumMember".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "keyword".','Si est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "text".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de "color".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "file".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "reference".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "customcolor".','Si est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "folder".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "typeParameter".','Cuando est\xE1 habilitado, IntelliSense muestra sugerencias de tipo "snippet".',"Cuando est\xE1 habilitado, IntelliSense muestra sugerencias del usuario.","Cuando est\xE1 habilitado IntelliSense muestra sugerencias para problemas.","Indica si los espacios en blanco iniciales y finales deben seleccionarse siempre.",'Controla si se deben aceptar sugerencias en los caracteres de confirmaci\xF3n. Por ejemplo, en Javascript, el punto y coma (";") puede ser un car\xE1cter de confirmaci\xF3n que acepta una sugerencia y escribe ese car\xE1cter.','Aceptar solo una sugerencia con "Entrar" cuando realiza un cambio textual.','Controla si las sugerencias deben aceptarse con "Entrar", adem\xE1s de "TAB". Ayuda a evitar la ambig\xFCedad entre insertar nuevas l\xEDneas o aceptar sugerencias.',"Controla el n\xFAmero de l\xEDneas en el editor que puede leer un lector de pantalla. Advertencia: Esto puede afectar al rendimiento de n\xFAmeros superiores al predeterminado.","Contenido del editor","Utilizar las configuraciones del lenguaje para determinar cu\xE1ndo cerrar los corchetes autom\xE1ticamente.","Cerrar autom\xE1ticamente los corchetes cuando el cursor est\xE9 a la izquierda de un espacio en blanco.","Controla si el editor debe cerrar autom\xE1ticamente los corchetes despu\xE9s de que el usuario agregue un corchete de apertura.","Escriba en las comillas o los corchetes solo si se insertaron autom\xE1ticamente.","Controla si el editor debe escribir entre comillas o corchetes.","Utilizar las configuraciones del lenguaje para determinar cu\xE1ndo cerrar las comillas autom\xE1ticamente. ","Cerrar autom\xE1ticamente las comillas cuando el cursor est\xE9 a la izquierda de un espacio en blanco. ","Controla si el editor debe cerrar autom\xE1ticamente las comillas despu\xE9s de que el usuario agrega uma comilla de apertura.","El editor no insertar\xE1 la sangr\xEDa autom\xE1ticamente.","El editor mantendr\xE1 la sangr\xEDa de la l\xEDnea actual.","El editor respetar\xE1 la sangr\xEDa de la l\xEDnea actual y los corchetes definidos por el idioma.","El editor mantendr\xE1 la sangr\xEDa de la l\xEDnea actual, respetar\xE1 los corchetes definidos por el idioma e invocar\xE1 onEnterRules especiales definidos por idiomas.","El editor respetar\xE1 la sangr\xEDa de la l\xEDnea actual, los corchetes definidos por idiomas y las reglas indentationRules definidas por idiomas, adem\xE1s de invocar reglas onEnterRules especiales.","Controla si el editor debe ajustar autom\xE1ticamente la sangr\xEDa mientras los usuarios escriben, pegan, mueven o sangran l\xEDneas.","Use las configuraciones de idioma para determinar cu\xE1ndo delimitar las selecciones autom\xE1ticamente.","Envolver con comillas, pero no con corchetes.","Envolver con corchetes, pero no con comillas.","Controla si el editor debe rodear autom\xE1ticamente las selecciones al escribir comillas o corchetes.","Emule el comportamiento de selecci\xF3n de los caracteres de tabulaci\xF3n al usar espacios para la sangr\xEDa. La selecci\xF3n se aplicar\xE1 a las tabulaciones.","Controla si el editor muestra CodeLens.","Controla la familia de fuentes para CodeLens.",'Controla el tama\xF1o de fuente de CodeLens en p\xEDxeles. Cuando se establece en "0", se usa el 90\xA0% de "#editor.fontSize#".',"Controla si el editor debe representar el Selector de colores y los elementos Decorator de color en l\xEDnea.","Habilite que la selecci\xF3n con el mouse y las teclas est\xE9 realizando la selecci\xF3n de columnas.","Controla si el resaltado de sintaxis debe ser copiado al portapapeles.","Controla el estilo de animaci\xF3n del cursor.","Controla si la animaci\xF3n suave del cursor debe estar habilitada.","Controla el estilo del cursor.",'Controla el n\xFAmero m\xEDnimo de l\xEDneas iniciales y finales visibles que rodean al cursor. En algunos otros editores, se conoce como "scrollOff" o "scrollOffset".','Solo se aplica "cursorSurroundingLines" cuando se desencadena mediante el teclado o la API.','"cursorSurroundingLines" se aplica siempre.','Controla cuando se debe aplicar "cursorSurroundingLines".','Controla el ancho del cursor cuando "#editor.cursorStyle#" se establece en "line".',"Controla si el editor debe permitir mover las selecciones mediante arrastrar y colocar.",'Multiplicador de la velocidad de desplazamiento al presionar "Alt".',"Controla si el editor tiene el plegado de c\xF3digo habilitado.","Utilice una estrategia de plegado espec\xEDfica del idioma, si est\xE1 disponible, de lo contrario la basada en sangr\xEDa.","Utilice la estrategia de plegado basada en sangr\xEDa.","Controla la estrategia para calcular rangos de plegado.","Controla si el editor debe destacar los rangos plegados.","Controla si al hacer clic en el contenido vac\xEDo despu\xE9s de una l\xEDnea plegada se desplegar\xE1 la l\xEDnea.","Controla la familia de fuentes.","Controla si el editor debe dar formato autom\xE1ticamente al contenido pegado. Debe haber disponible un formateador capaz de aplicar formato a un rango dentro de un documento. ","Controla si el editor debe dar formato a la l\xEDnea autom\xE1ticamente despu\xE9s de escribirla.","Controla si el editor debe representar el margen de glifo vertical. El margen de glifo se usa, principalmente, para depuraci\xF3n.","Controla si el cursor debe ocultarse en la regla de informaci\xF3n general.","Controla si el editor debe resaltar la gu\xEDa de sangr\xEDa activa.","Controla el espacio entre letras en p\xEDxeles.","Controla si el editor tiene habilitada la edici\xF3n vinculada. Dependiendo del lenguaje, los s\xEDmbolos relacionados (por ejemplo, las etiquetas HTML) se actualizan durante la edici\xF3n.","Controla si el editor debe detectar v\xEDnculos y hacerlos interactivos.","Resaltar par\xE9ntesis coincidentes.",'Se usar\xE1 un multiplicador en los eventos de desplazamiento de la rueda del mouse "deltaX" y "deltaY". ','Ampliar la fuente del editor cuando se use la rueda del mouse mientras se presiona "Ctrl".',"Combinar varios cursores cuando se solapan.",'Se asigna a "Control" en Windows y Linux y a "Comando" en macOS.','Se asigna a "Alt" en Windows y Linux y a "Opci\xF3n" en macOS.',"El modificador que se usar\xE1 para agregar varios cursores con el mouse. Los gestos del mouse Ir a definici\xF3n y Abrir v\xEDnculo se adaptar\xE1n de modo que no entren en conflicto con el modificador multicursor. [M\xE1s informaci\xF3n](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).","Cada cursor pega una \xFAnica l\xEDnea del texto.","Cada cursor pega el texto completo.","Controla el pegado cuando el recuento de l\xEDneas del texto pegado coincide con el recuento de cursores.","Controla si el editor debe resaltar las apariciones de s\xEDmbolos sem\xE1nticos.","Controla si debe dibujarse un borde alrededor de la regla de informaci\xF3n general.","Enfocar el \xE1rbol al abrir la inspecci\xF3n","Enfocar el editor al abrir la inspecci\xF3n","Controla si se debe enfocar el editor en l\xEDnea o el \xE1rbol en el widget de vista.","Controla si el gesto del mouse Ir a definici\xF3n siempre abre el widget interactivo.","Controla el retraso, en milisegundos, tras el cual aparecer\xE1n sugerencias r\xE1pidas.","Controla si el editor cambia el nombre autom\xE1ticamente en el tipo.",'En desuso. Utilice "editor.linkedEditing" en su lugar.',"Controla si el editor debe representar caracteres de control.","Controla si el editor debe representar gu\xEDas de sangr\xEDa.","Representar el n\xFAmero de la \xFAltima l\xEDnea cuando el archivo termina con un salto de l\xEDnea.","Resalta el medianil y la l\xEDnea actual.","Controla c\xF3mo debe representar el editor el resaltado de l\xEDnea actual.","Controla si el editor debe representar el resaltado de la l\xEDnea actual solo cuando el editor est\xE1 enfocado","Representa caracteres de espacio en blanco, excepto los espacios individuales entre palabras.","Represente los caracteres de espacio en blanco solo en el texto seleccionado.","Representar solo los caracteres de espacio en blanco al final","Controla la forma en que el editor debe representar los caracteres de espacio en blanco.","Controla si las selecciones deber\xEDan tener las esquinas redondeadas.","Controla el n\xFAmero de caracteres adicionales a partir del cual el editor se desplazar\xE1 horizontalmente.","Controla si el editor seguir\xE1 haciendo scroll despu\xE9s de la \xFAltima l\xEDnea.","Despl\xE1cese solo a lo largo del eje predominante cuando se desplace vertical y horizontalmente al mismo tiempo. Evita la deriva horizontal cuando se desplaza verticalmente en un trackpad.","Controla si el portapapeles principal de Linux debe admitirse.","Controla si el editor debe destacar las coincidencias similares a la selecci\xF3n.","Mostrar siempre los controles de plegado.","Mostrar solo los controles de plegado cuando el mouse est\xE1 sobre el medianil.","Controla cu\xE1ndo se muestran los controles de plegado en el medianil.","Controla el fundido de salida del c\xF3digo no usado.","Controla las variables en desuso tachadas.","Mostrar sugerencias de fragmentos de c\xF3digo por encima de otras sugerencias.","Mostrar sugerencias de fragmentos de c\xF3digo por debajo de otras sugerencias.","Mostrar sugerencias de fragmentos de c\xF3digo con otras sugerencias.","No mostrar sugerencias de fragmentos de c\xF3digo.","Controla si se muestran los fragmentos de c\xF3digo con otras sugerencias y c\xF3mo se ordenan.","Controla si el editor se desplazar\xE1 con una animaci\xF3n.","Tama\xF1o de la fuente para el widget de sugerencias. Cuando se establece a `0`, se utilizar\xE1 el valor `#editor.fontSize#`.",'Altura de la l\xEDnea del widget de sugerencias. Cuando se establece en "0", se usa el valor "#editor.lineHeight#". El valor m\xEDnimo es 8.',"Controla si deben aparecer sugerencias de forma autom\xE1tica al escribir caracteres desencadenadores.","Seleccionar siempre la primera sugerencia.",'Seleccione sugerencias recientes a menos que al escribir m\xE1s se seleccione una, por ejemplo, "console.| -> console.log" porque "log" se ha completado recientemente.','Seleccione sugerencias basadas en prefijos anteriores que han completado esas sugerencias, por ejemplo, "co -> console" y "con -> const".',"Controla c\xF3mo se preseleccionan las sugerencias cuando se muestra la lista,","La pesta\xF1a se completar\xE1 insertando la mejor sugerencia de coincidencia encontrada al presionar la pesta\xF1a","Deshabilitar los complementos para pesta\xF1as.","La pesta\xF1a se completa con fragmentos de c\xF3digo cuando su prefijo coincide. Funciona mejor cuando las 'quickSuggestions' no est\xE1n habilitadas.","Habilita completar pesta\xF1as.","Los terminadores de l\xEDnea no habituales se quitan autom\xE1ticamente.","Los terminadores de l\xEDnea no habituales se omiten.","Advertencia de terminadores de l\xEDnea inusuales que se quitar\xE1n.","Quite los terminadores de l\xEDnea inusuales que podr\xEDan provocar problemas.","La inserci\xF3n y eliminaci\xF3n del espacio en blanco sigue a las tabulaciones.","Caracteres que se usar\xE1n como separadores de palabras al realizar operaciones o navegaciones relacionadas con palabras.","Las l\xEDneas no se ajustar\xE1n nunca.","Las l\xEDneas se ajustar\xE1n en el ancho de la ventanilla.",'Las l\xEDneas se ajustar\xE1n al valor de "#editor.wordWrapColumn#". ','Las l\xEDneas se ajustar\xE1n al valor que sea inferior: el tama\xF1o de la ventanilla o el valor de "#editor.wordWrapColumn#".',"Controla c\xF3mo deben ajustarse las l\xEDneas.",'Controla la columna de ajuste del editor cuando "#editor.wordWrap#" es "wordWrapColumn" o "bounded".',"No hay sangr\xEDa. Las l\xEDneas ajustadas comienzan en la columna 1.","A las l\xEDneas ajustadas se les aplica la misma sangr\xEDa que al elemento primario.","A las l\xEDneas ajustadas se les aplica una sangr\xEDa de +1 respecto al elemento primario.","A las l\xEDneas ajustadas se les aplica una sangr\xEDa de +2 respecto al elemento primario.","Controla la sangr\xEDa de las l\xEDneas ajustadas.","Se supone que todos los caracteres son del mismo ancho. Este es un algoritmo r\xE1pido que funciona correctamente para fuentes monoespaciales y ciertos scripts (como caracteres latinos) donde los glifos tienen el mismo ancho.","Delega el c\xE1lculo de puntos de ajuste en el explorador. Es un algoritmo lento, que podr\xEDa causar bloqueos para archivos grandes, pero funciona correctamente en todos los casos.","Controla el algoritmo que calcula los puntos de ajuste."],"vs/editor/common/editorContextKeys":["Si el texto del editor tiene el foco (el cursor parpadea)","Si el editor o un widget del editor tiene el foco (por ejemplo, el foco est\xE1 en el widget de b\xFAsqueda)","Si un editor o una entrada de texto enriquecido tienen el foco (el cursor parpadea)","Si el editor es de solo lectura","Si el contexto es un editor de diferencias",'Si "editor.columnSelection" se ha habilitado',"Si el editor tiene texto seleccionado","Si el editor tiene varias selecciones",'Si "Tabulaci\xF3n" mover\xE1 el foco fuera del editor',"Si el mantenimiento del puntero del editor es visible","Si el editor forma parte de otro m\xE1s grande (por ejemplo, blocs de notas)","Identificador de idioma del editor","Si el editor tiene un proveedor de elementos de finalizaci\xF3n","Si el editor tiene un proveedor de acciones de c\xF3digo","Si el editor tiene un proveedor de CodeLens","Si el editor tiene un proveedor de definiciones","Si el editor tiene un proveedor de declaraciones","Si el editor tiene un proveedor de implementaci\xF3n","Si el editor tiene un proveedor de definiciones de tipo","Si el editor tiene un proveedor de contenido con mantenimiento del puntero","Si el editor tiene un proveedor de resaltado de documentos","Si el editor tiene un proveedor de s\xEDmbolos de documentos","Si el editor tiene un proveedor de referencia","Si el editor tiene un proveedor de cambio de nombre","Si el editor tiene un proveedor de ayuda de signatura","Si el editor tiene un proveedor de sugerencias insertadas","Si el editor tiene un proveedor de formatos de documento","Si el editor tiene un proveedor de formatos de selecci\xF3n de documentos","Si el editor tiene varios proveedores de formatos del documento","Si el editor tiene varios proveedores de formato de la selecci\xF3n de documentos"],"vs/editor/common/model/editStack":["Escribiendo"],"vs/editor/common/modes/modesRegistry":["Texto sin formato"],"vs/editor/common/standaloneStrings":["Sin selecci\xF3n","L\xEDnea {0}, columna {1} ({2} seleccionadas)","L\xEDnea {0}, columna {1}","{0} selecciones ({1} caracteres seleccionados)","{0} selecciones",'Se cambiar\xE1 ahora el valor "accessibilitySupport" a "activado".',"Se abrir\xE1 ahora la p\xE1gina de documentaci\xF3n de accesibilidad del editor.","en un panel de solo lectura de un editor de diferencias.","en un panel de un editor de diferencias.","en un editor de c\xF3digo de solo lectura"," en un editor de c\xF3digo","Para configurar el editor de forma que se optimice su uso con un lector de pantalla, presione ahora Comando+E.","Para configurar el editor de forma que se optimice su uso con un lector de pantalla, presione ahora Control+E.","El editor est\xE1 configurado para optimizarse para su uso con un lector de pantalla.","El editor est\xE1 configurado para que no se optimice nunca su uso con un lector de pantalla, que en este momento no es el caso.","Al presionar TAB en el editor actual, el foco se mueve al siguiente elemento activable. Presione {0} para activar o desactivar este comportamiento.","Al presionar TAB en el editor actual, el foco se mueve al siguiente elemento activable. El comando {0} no se puede desencadenar actualmente mediante un enlace de teclado.","Al presionar TAB en el editor actual, se insertar\xE1 el car\xE1cter de tabulaci\xF3n. Presione {0} para activar o desactivar este comportamiento.","Al presionar TAB en el editor actual, se insertar\xE1 el car\xE1cter de tabulaci\xF3n. El comando {0} no se puede desencadenar actualmente mediante un enlace de teclado.","Presione ahora Comando+H para abrir una ventana del explorador con m\xE1s informaci\xF3n relacionada con la accesibilidad del editor.","Presione ahora Control+H para abrir una ventana del explorador con m\xE1s informaci\xF3n relacionada con la accesibilidad del editor.","Para descartar esta informaci\xF3n sobre herramientas y volver al editor, presione Esc o May\xFAs+Escape.","Mostrar ayuda de accesibilidad","Desarrollador: inspeccionar tokens","Vaya a L\xEDnea/Columna...","Mostrar todos los proveedores de acceso r\xE1pido","Paleta de comandos","Mostrar y ejecutar comandos","Ir a s\xEDmbolo...","Ir a s\xEDmbolo por categor\xEDa...","Contenido del editor","Presione Alt+F1 para ver las opciones de accesibilidad.","Alternar tema de contraste alto","{0} ediciones realizadas en {1} archivos"],"vs/editor/common/view/editorColorRegistry":["Color de fondo para la l\xEDnea resaltada en la posici\xF3n del cursor.","Color de fondo del borde alrededor de la l\xEDnea en la posici\xF3n del cursor.","Color de fondo de rangos resaltados, como en abrir r\xE1pido y encontrar caracter\xEDsticas. El color no debe ser opaco para no ocultar decoraciones subyacentes.","Color de fondo del borde alrededor de los intervalos resaltados.","Color de fondo del s\xEDmbolo destacado, como Ir a definici\xF3n o Ir al siguiente/anterior s\xEDmbolo. El color no debe ser opaco para no ocultar la decoraci\xF3n subyacente.","Color de fondo del borde alrededor de los s\xEDmbolos resaltados.","Color del cursor del editor.","Color de fondo del cursor de edici\xF3n. Permite personalizar el color del caracter solapado por el bloque del cursor.","Color de los caracteres de espacio en blanco del editor.","Color de las gu\xEDas de sangr\xEDa del editor.","Color de las gu\xEDas de sangr\xEDa activas del editor.","Color de n\xFAmeros de l\xEDnea del editor.","Color del n\xFAmero de l\xEDnea activa en el editor","ID es obsoleto. Usar en lugar 'editorLineNumber.activeForeground'. ","Color del n\xFAmero de l\xEDnea activa en el editor","Color de las reglas del editor","Color principal de lentes de c\xF3digo en el editor","Color de fondo tras corchetes coincidentes","Color de bloques con corchetes coincidentes","Color del borde de la regla de visi\xF3n general.","Color de fondo de la regla de informaci\xF3n general del editor. Solo se usa cuando el minimapa est\xE1 habilitado y est\xE1 ubicado en el lado derecho del editor.","Color de fondo del margen del editor. Este espacio contiene los m\xE1rgenes de glifos y los n\xFAmeros de l\xEDnea.","Color del borde de c\xF3digo fuente innecesario (sin usar) en el editor.",`Opacidad de c\xF3digo fuente innecesario (sin usar) en el editor. Por ejemplo, "#000000c0" representar\xE1 el c\xF3digo con un 75 % de opacidad. Para temas de alto contraste, utilice el color del tema 'editorUnnecessaryCode.border' para resaltar el c\xF3digo innecesario en vez de atenuarlo.`,"Color de marcador de regla general para los destacados de rango. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color de marcador de regla de informaci\xF3n general para errores. ","Color de marcador de regla de informaci\xF3n general para advertencias.","Color de marcador de regla de informaci\xF3n general para mensajes informativos. "],"vs/editor/contrib/anchorSelect/anchorSelect":["Delimitador de la selecci\xF3n","Delimitador establecido en {0}:{1}","Establecer el delimitador de la selecci\xF3n","Ir al delimitador de la selecci\xF3n","Seleccionar desde el delimitador hasta el cursor","Cancelar el delimitador de la selecci\xF3n"],"vs/editor/contrib/bracketMatching/bracketMatching":["Resumen color de marcador de regla para corchetes.","Ir al corchete","Seleccionar para corchete","Ir al &&corchete"],"vs/editor/contrib/caretOperations/caretOperations":["Mover el texto seleccionado a la izquierda","Mover el texto seleccionado a la derecha"],"vs/editor/contrib/caretOperations/transpose":["Transponer letras"],"vs/editor/contrib/clipboard/clipboard":["Cor&&tar","Cortar","Cortar","&&Copiar","Copiar","Copiar","&&Pegar","Pegar","Pegar","Copiar con resaltado de sintaxis"],"vs/editor/contrib/codeAction/codeActionCommands":["Tipo de la acci\xF3n de c\xF3digo que se va a ejecutar.","Controla cu\xE1ndo se aplican las acciones devueltas.","Aplicar siempre la primera acci\xF3n de c\xF3digo devuelto.","Aplicar la primera acci\xF3n de c\xF3digo devuelta si solo hay una.","No aplique las acciones de c\xF3digo devuelto.","Controla si solo se deben devolver las acciones de c\xF3digo preferidas.","Se ha producido un error desconocido al aplicar la acci\xF3n de c\xF3digo","Correcci\xF3n R\xE1pida","No hay acciones de c\xF3digo disponibles",'No hay acciones de c\xF3digo preferidas para "{0}" disponibles','No hay ninguna acci\xF3n de c\xF3digo para "{0}" disponible.',"No hay acciones de c\xF3digo preferidas disponibles","No hay acciones de c\xF3digo disponibles","Refactorizar...",'No hay refactorizaciones preferidas de "{0}" disponibles','No hay refactorizaciones de "{0}" disponibles',"No hay ninguna refactorizaci\xF3n favorita disponible.","No hay refactorizaciones disponibles","Acci\xF3n de Origen...",'No hay acciones de origen preferidas para "{0}" disponibles','No hay ninguna acci\xF3n de origen para "{0}" disponible.',"No hay ninguna acci\xF3n de origen favorita disponible.","No hay acciones de origen disponibles","Organizar Importaciones","No hay acciones de importaci\xF3n disponibles","Corregir todo","No est\xE1 disponible la acci\xF3n de corregir todo","Corregir autom\xE1ticamente...","No hay autocorrecciones disponibles"],"vs/editor/contrib/codeAction/lightBulbWidget":["Mostrar correcciones. Soluci\xF3n preferida disponible ({0})","Mostrar correcciones ({0})","Mostrar correcciones"],"vs/editor/contrib/codelens/codelensController":["Mostrar comandos de lente de c\xF3digo para la l\xEDnea actual"],"vs/editor/contrib/comment/comment":["Alternar comentario de l\xEDnea","&&Alternar comentario de l\xEDnea","Agregar comentario de l\xEDnea","Quitar comentario de l\xEDnea","Alternar comentario de bloque","Alternar &&bloque de comentario"],"vs/editor/contrib/contextmenu/contextmenu":["Mostrar men\xFA contextual del editor"],"vs/editor/contrib/cursorUndo/cursorUndo":["Cursor Deshacer","Cursor Rehacer"],"vs/editor/contrib/find/findController":["Buscar","&&Buscar","Buscar con selecci\xF3n","Buscar siguiente","Buscar siguiente","Buscar anterior","Buscar anterior","Buscar selecci\xF3n siguiente","Buscar selecci\xF3n anterior","Reemplazar","&&Reemplazar"],"vs/editor/contrib/find/findWidget":['Icono para "Buscar en selecci\xF3n" en el widget de b\xFAsqueda del editor.',"Icono para indicar que el widget de b\xFAsqueda del editor est\xE1 contra\xEDdo.","Icono para indicar que el widget de b\xFAsqueda del editor est\xE1 expandido.",'Icono para "Reemplazar" en el widget de b\xFAsqueda del editor.','Icono para "Reemplazar todo" en el widget de b\xFAsqueda del editor.','Icono para "Buscar anterior" en el widget de b\xFAsqueda del editor.','Icono para "Buscar siguiente" en el widget de b\xFAsqueda del editor.',"Buscar","Buscar","Coincidencia anterior","Pr\xF3xima coincidencia","Buscar en selecci\xF3n","Cerrar","Reemplazar","Reemplazar","Reemplazar","Reemplazar todo","Alternar modo de reemplazar","S\xF3lo los primeros {0} resultados son resaltados, pero todas las operaciones de b\xFAsqueda trabajan en todo el texto.","{0} de {1}","No hay resultados","Encontrados: {0}",'{0} encontrado para "{1}"','{0} encontrado para "{1}", en {2}','{0} encontrado para "{1}"',"Ctrl+Entrar ahora inserta un salto de l\xEDnea en lugar de reemplazar todo. Puede modificar el enlace de claves para editor.action.replaceAll para invalidar este comportamiento."],"vs/editor/contrib/folding/folding":["Desplegar","Desplegar de forma recursiva","Plegar","Alternar plegado","Plegar de forma recursiva","Cerrar todos los comentarios de bloque","Plegar todas las regiones","Desplegar Todas las Regiones","Plegar todo","Desplegar todo","Nivel de plegamiento {0}","Color de fondo detr\xE1s de los rangos plegados. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color del control plegable en el medianil del editor."],"vs/editor/contrib/folding/foldingDecorations":["Icono de rangos expandidos en el margen de glifo del editor.","Icono de rangos contra\xEDdos en el margen de glifo del editor."],"vs/editor/contrib/fontZoom/fontZoom":["Acercarse a la tipograf\xEDa del editor","Alejarse de la tipograf\xEDa del editor","Restablecer alejamiento de la tipograf\xEDa del editor"],"vs/editor/contrib/format/format":["1 edici\xF3n de formato en la l\xEDnea {0}","{0} ediciones de formato en la l\xEDnea {1}","1 edici\xF3n de formato entre las l\xEDneas {0} y {1}","{0} ediciones de formato entre las l\xEDneas {1} y {2}"],"vs/editor/contrib/format/formatActions":["Dar formato al documento","Dar formato a la selecci\xF3n"],"vs/editor/contrib/gotoError/gotoError":["Ir al siguiente problema (Error, Advertencia, Informaci\xF3n)","Icono para ir al marcador siguiente.","Ir al problema anterior (Error, Advertencia, Informaci\xF3n)","Icono para ir al marcador anterior.","Ir al siguiente problema en Archivos (Error, Advertencia, Informaci\xF3n)","Siguiente &&problema","Ir al problema anterior en Archivos (Error, Advertencia, Informaci\xF3n)","Anterior &&problema"],"vs/editor/contrib/gotoError/gotoErrorWidget":["Error","Advertencia","Informaci\xF3n","Sugerencia","{0} en {1}. ","{0} de {1} problemas","{0} de {1} problema","Color de los errores del widget de navegaci\xF3n de marcadores del editor.","Color de las advertencias del widget de navegaci\xF3n de marcadores del editor.","Color del widget informativo marcador de navegaci\xF3n en el editor.","Fondo del widget de navegaci\xF3n de marcadores del editor."],"vs/editor/contrib/gotoSymbol/goToCommands":["Ver","Definiciones",'No se encontr\xF3 ninguna definici\xF3n para "{0}"',"No se encontr\xF3 ninguna definici\xF3n","Ir a definici\xF3n","Ir a &&definici\xF3n","Abrir definici\xF3n en el lateral","Ver la definici\xF3n sin salir","Declaraciones","No se encontr\xF3 ninguna definici\xF3n para '{0}'","No se encontr\xF3 ninguna declaraci\xF3n","Ir a Definici\xF3n","Ir a &&Declaraci\xF3n","No se encontr\xF3 ninguna definici\xF3n para '{0}'","No se encontr\xF3 ninguna declaraci\xF3n","Inspeccionar Definici\xF3n","Definiciones de tipo",'No se encontr\xF3 ninguna definici\xF3n de tipo para "{0}"',"No se encontr\xF3 ninguna definici\xF3n de tipo","Ir a la definici\xF3n de tipo","Ir a la definici\xF3n de &&tipo","Inspeccionar definici\xF3n de tipo","Implementaciones",'No se encontr\xF3 ninguna implementaci\xF3n para "{0}"',"No se encontr\xF3 ninguna implementaci\xF3n","Ir a Implementaciones","Ir a &&Implementaciones","Inspeccionar implementaciones",'No se ha encontrado ninguna referencia para "{0}".',"No se encontraron referencias","Ir a Referencias","Ir a &&Referencias","Referencias","Inspeccionar Referencias","Referencias","Ir a cualquier s\xEDmbolo","Ubicaciones",'No hay resultados para "{0}"',"Referencias"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["Haga clic para mostrar {0} definiciones."],"vs/editor/contrib/gotoSymbol/peek/referencesController":["Cargando...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["{0} referencias","{0} referencia","Referencias"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["vista previa no disponible","No hay resultados","Referencias"],"vs/editor/contrib/gotoSymbol/referencesModel":["s\xEDmbolo en {0} linea {1} en la columna {2}","s\xEDmbolo en {0} l\xEDnea {1} en la columna {2}, {3}","1 s\xEDmbolo en {0}, ruta de acceso completa {1}","{0} s\xEDmbolos en {1}, ruta de acceso completa {2}","No se encontraron resultados","Encontr\xF3 1 s\xEDmbolo en {0}","Encontr\xF3 {0} s\xEDmbolos en {1}","Encontr\xF3 {0} s\xEDmbolos en {1} archivos"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["S\xEDmbolo {0} de {1}, {2} para el siguiente","S\xEDmbolo {0} de {1}"],"vs/editor/contrib/hover/hover":["Mostrar al mantener el puntero","Mostrar vista previa de la definici\xF3n que aparece al mover el puntero"],"vs/editor/contrib/hover/markdownHoverParticipant":["Cargando..."],"vs/editor/contrib/hover/markerHoverParticipant":["Ver el problema","No hay correcciones r\xE1pidas disponibles","Buscando correcciones r\xE1pidas...","No hay correcciones r\xE1pidas disponibles","Correcci\xF3n R\xE1pida"],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["Reemplazar con el valor anterior","Reemplazar con el valor siguiente"],"vs/editor/contrib/indentation/indentation":["Convertir sangr\xEDa en espacios","Convertir sangr\xEDa en tabulaciones","Tama\xF1o de tabulaci\xF3n configurado","Seleccionar tama\xF1o de tabulaci\xF3n para el archivo actual","Aplicar sangr\xEDa con tabulaciones","Aplicar sangr\xEDa con espacios","Detectar sangr\xEDa del contenido","Volver a aplicar sangr\xEDa a l\xEDneas","Volver a aplicar sangr\xEDa a l\xEDneas seleccionadas"],"vs/editor/contrib/linesOperations/linesOperations":["Copiar l\xEDnea arriba","&&Copiar l\xEDnea arriba","Copiar l\xEDnea abajo","Co&&piar l\xEDnea abajo","Selecci\xF3n duplicada","&&Duplicar selecci\xF3n","Mover l\xEDnea hacia arriba","Mo&&ver l\xEDnea arriba","Mover l\xEDnea hacia abajo","Mover &&l\xEDnea abajo","Ordenar l\xEDneas en orden ascendente","Ordenar l\xEDneas en orden descendente","Recortar espacio final","Eliminar l\xEDnea","Sangr\xEDa de l\xEDnea","Anular sangr\xEDa de l\xEDnea","Insertar l\xEDnea arriba","Insertar l\xEDnea debajo","Eliminar todo a la izquierda","Eliminar todo lo que est\xE1 a la derecha","Unir l\xEDneas","Transponer caracteres alrededor del cursor","Transformar a may\xFAsculas","Transformar a min\xFAsculas","Transformar en Title Case","Transformar en Snake Case"],"vs/editor/contrib/linkedEditing/linkedEditing":["Iniciar edici\xF3n vinculada","Color de fondo cuando el editor cambia el nombre autom\xE1ticamente al escribir."],"vs/editor/contrib/links/links":["Ejecutar comando","Seguir v\xEDnculo","cmd + clic","ctrl + clic","opci\xF3n + clic","alt + clic","Ejecutar el comando {0}","No se pudo abrir este v\xEDnculo porque no tiene un formato correcto: {0}","No se pudo abrir este v\xEDnculo porque falta el destino.","Abrir v\xEDnculo"],"vs/editor/contrib/message/messageController":["Indica si el editor muestra actualmente un mensaje insertado","No se puede editar en un editor de s\xF3lo lectura"],"vs/editor/contrib/multicursor/multicursor":["Agregar cursor arriba","&&Agregar cursor arriba","Agregar cursor debajo","A&&gregar cursor abajo","A\xF1adir cursores a finales de l\xEDnea","Agregar c&&ursores a extremos de l\xEDnea","A\xF1adir cursores a la parte inferior","A\xF1adir cursores a la parte superior","Agregar selecci\xF3n hasta la siguiente coincidencia de b\xFAsqueda","Agregar &&siguiente repetici\xF3n","Agregar selecci\xF3n hasta la anterior coincidencia de b\xFAsqueda","Agregar r&&epetici\xF3n anterior","Mover \xFAltima selecci\xF3n hasta la siguiente coincidencia de b\xFAsqueda","Mover \xFAltima selecci\xF3n hasta la anterior coincidencia de b\xFAsqueda","Seleccionar todas las repeticiones de coincidencia de b\xFAsqueda","Seleccionar todas las &&repeticiones","Cambiar todas las ocurrencias"],"vs/editor/contrib/parameterHints/parameterHints":["Sugerencias para par\xE1metros Trigger"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["Icono para mostrar la sugerencia de par\xE1metro siguiente.","Icono para mostrar la sugerencia de par\xE1metro anterior.","{0}, sugerencia"],"vs/editor/contrib/peekView/peekView":["Cerrar","Color de fondo del \xE1rea de t\xEDtulo de la vista de inspecci\xF3n.","Color del t\xEDtulo de la vista de inpecci\xF3n.","Color de la informaci\xF3n del t\xEDtulo de la vista de inspecci\xF3n.","Color de los bordes y la flecha de la vista de inspecci\xF3n.","Color de fondo de la lista de resultados de vista de inspecci\xF3n.","Color de primer plano de los nodos de inspecci\xF3n en la lista de resultados.","Color de primer plano de los archivos de inspecci\xF3n en la lista de resultados.","Color de fondo de la entrada seleccionada en la lista de resultados de vista de inspecci\xF3n.","Color de primer plano de la entrada seleccionada en la lista de resultados de vista de inspecci\xF3n.","Color de fondo del editor de vista de inspecci\xF3n.","Color de fondo del margen en el editor de vista de inspecci\xF3n.","Buscar coincidencia con el color de resaltado de la lista de resultados de vista de inspecci\xF3n.","Buscar coincidencia del color de resultado del editor de vista de inspecci\xF3n.","Hacer coincidir el borde resaltado en el editor de vista previa."],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["Abra primero un editor de texto para ir a una l\xEDnea.","Vaya a la l\xEDnea {0} y a la columna {1}.","Ir a la l\xEDnea {0}.","L\xEDnea actual: {0}, Car\xE1cter: {1}. Escriba un n\xFAmero de l\xEDnea entre 1 y {2} a los que navegar.","L\xEDnea actual: {0}, Car\xE1cter: {1}. Escriba un n\xFAmero de l\xEDnea al que navegar."],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["Para ir a un s\xEDmbolo, primero abra un editor de texto con informaci\xF3n de s\xEDmbolo.","El editor de texto activo no proporciona informaci\xF3n de s\xEDmbolos.","No hay ning\xFAn s\xEDmbolo del editor coincidente.","No hay s\xEDmbolos del editor.","Abrir en el lateral","Abrir en la parte inferior","s\xEDmbolos ({0})","propiedades ({0})","m\xE9todos ({0})","funciones ({0})","constructores ({0})","variables ({0})","clases ({0})","estructuras ({0})","eventos ({0})","operadores ({0})","interfaces ({0})","espacios de nombres ({0})","paquetes ({0})","par\xE1metros de tipo ({0})","m\xF3dulos ({0})","propiedades ({0})","enumeraciones ({0})","miembros de enumeraci\xF3n ({0})","cadenas ({0})","archivos ({0})","matrices ({0})","n\xFAmeros ({0})","booleanos ({0})","objetos ({0})","claves ({0})","campos ({0})","constantes ({0})"],"vs/editor/contrib/rename/rename":["No hay ning\xFAn resultado.","Error desconocido al resolver el cambio de nombre de la ubicaci\xF3n",'Cambiando el nombre de "{0}"',"Cambiar el nombre de {0}","Nombre cambiado correctamente de '{0}' a '{1}'. Resumen: {2}","No se pudo cambiar el nombre a las ediciones de aplicaci\xF3n","No se pudo cambiar el nombre de las ediciones de c\xE1lculo","Cambiar el nombre del s\xEDmbolo","Activar/desactivar la capacidad de previsualizar los cambios antes de cambiar el nombre"],"vs/editor/contrib/rename/renameInputField":["Cambie el nombre de la entrada. Escriba el nuevo nombre y presione Entrar para confirmar.","{0} para cambiar de nombre, {1} para obtener una vista previa"],"vs/editor/contrib/smartSelect/smartSelect":["Expandir selecci\xF3n","&&Expandir selecci\xF3n","Reducir la selecci\xF3n","&&Reducir selecci\xF3n"],"vs/editor/contrib/snippet/snippetVariables":["Domingo","Lunes","Martes","Mi\xE9rcoles","Jueves","Viernes","S\xE1bado","Dom","Lun","Mar","Mi\xE9","Jue","Vie","S\xE1b","Enero","Febrero","Marzo","Abril","May","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre","Ene","Feb","Mar","Abr","May","Jun","Jul","Ago","Sep","Oct","Nov","Dic"],"vs/editor/contrib/suggest/suggestController":['Aceptando "{0}" ediciones adicionales de {1} realizadas',"Sugerencias para Trigger","Insertar","Insertar","Reemplazar","Reemplazar","Insertar","mostrar menos","mostrar m\xE1s","Restablecer tama\xF1o del widget de sugerencias"],"vs/editor/contrib/suggest/suggestWidget":["Color de fondo del widget sugerido.","Color de borde del widget sugerido.","Color de primer plano del widget sugerido.","Color de fondo de la entrada seleccionada del widget sugerido.","Color del resaltado coincidido en el widget sugerido.","Cargando...","No hay sugerencias.","{0}, documentos: {1}","Sugerir"],"vs/editor/contrib/suggest/suggestWidgetDetails":["Cerrar","Cargando..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["Icono para obtener m\xE1s informaci\xF3n en el widget de sugerencias.","Leer m\xE1s"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["Color de primer plano de los s\xEDmbolos de matriz. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos booleanos. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de clase. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de color. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos constantes. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de constructor. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de enumerador. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de miembro del enumerador. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de evento. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de campo. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de archivo. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de carpeta. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de funci\xF3n. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de interfaz. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de claves. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de palabra clave. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de m\xE9todo. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de m\xF3dulo. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de espacio de nombres. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos nulos. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano para los s\xEDmbolos num\xE9ricos. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de objeto. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano para los s\xEDmbolos del operador. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de paquete. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de propiedad. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de referencia. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de fragmento de c\xF3digo. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de cadena. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de estructura. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de texto. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano para los s\xEDmbolos de par\xE1metro de tipo. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos de unidad. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias.","Color de primer plano de los s\xEDmbolos variables. Estos s\xEDmbolos aparecen en el contorno, la ruta de navegaci\xF3n y el widget de sugerencias."],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["Alternar tecla de tabulaci\xF3n para mover el punto de atenci\xF3n","Presionando la pesta\xF1a ahora mover\xE1 el foco al siguiente elemento enfocable.","Presionando la pesta\xF1a ahora insertar\xE1 el car\xE1cter de tabulaci\xF3n"],"vs/editor/contrib/tokenization/tokenization":["Desarrollador: forzar nueva aplicaci\xF3n de token"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["Terminadores de l\xEDnea inusuales","Se han detectado terminadores de l\xEDnea inusuales",`Este archivo contiene uno o varios caracteres de terminador de l\xEDnea inusuales, como Separador de l\xEDneas (LS) o Separador de p\xE1rrafos (PS).\r +\r +Se recomienda quitarlos del archivo. Se puede configurar a trav\xE9s de "editor.unusualLineTerminators".`,"Corregir este archivo","Ignorar problema para este archivo"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["Color de fondo de un s\xEDmbolo durante el acceso de lectura, como la lectura de una variable. El color no debe ser opaco para no ocultar decoraciones subyacentes.","Color de fondo de un s\xEDmbolo durante el acceso de escritura, como escribir en una variable. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color de fondo de un s\xEDmbolo durante el acceso de lectura; por ejemplo, cuando se lee una variable.","Color de fondo de un s\xEDmbolo durante el acceso de escritura; por ejemplo, cuando se escribe una variable.","Color del marcador de regla general para destacados de s\xEDmbolos. El color no debe ser opaco para no ocultar decoraciones subyacentes.","Color de marcador de regla general para destacados de s\xEDmbolos de acceso de escritura. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Ir al siguiente s\xEDmbolo destacado","Ir al s\xEDmbolo destacado anterior","Desencadenar los s\xEDmbolos destacados"],"vs/editor/contrib/wordOperations/wordOperations":["Eliminar palabra"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["La configuraci\xF3n del lenguaje predeterminada se reemplaza","Establecer los valores de configuraci\xF3n que se reemplazar\xE1n para un lenguaje.","Esta configuraci\xF3n no admite la configuraci\xF3n por idioma.","No se puede registrar una propiedad vac\xEDa.",`No se puede registrar "{0}". Coincide con el patr\xF3n de propiedad '\\\\[.*\\\\]$' para describir la configuraci\xF3n del editor espec\xEDfica del lenguaje. Utilice la contribuci\xF3n "configurationDefaults".`,'No se puede registrar "{0}". Esta propiedad ya est\xE1 registrada.'],"vs/platform/contextkey/browser/contextKeyService":["Comando que devuelve informaci\xF3n sobre las claves de contexto"],"vs/platform/contextkey/common/contextkeys":["Si el sistema operativo es Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["Se presion\xF3 ({0}). Esperando la siguiente tecla...","La combinaci\xF3n de claves ({0}, {1}) no es un comando."],"vs/platform/list/browser/listService":["\xC1rea de trabajo",'Se asigna a "Control" en Windows y Linux y a "Comando" en macOS.','Se asigna a "Alt" en Windows y Linux y a "Opci\xF3n" en macOS.',"El modificador que se utilizar\xE1 para agregar un elemento en los \xE1rboles y listas para una selecci\xF3n m\xFAltiple con el rat\xF3n (por ejemplo en el explorador, abiertos editores y vista de scm). Los gestos de rat\xF3n 'Abrir hacia' - si est\xE1n soportados - se adaptar\xE1n de forma tal que no tenga conflicto con el modificador m\xFAltiple.","Controla c\xF3mo abrir elementos en los \xE1rboles y las listas mediante el mouse (si se admite). Tenga en cuenta que algunos \xE1rboles y listas pueden optar por ignorar esta configuraci\xF3n si no es aplicable.","Controla si las listas y los \xE1rboles admiten el desplazamiento horizontal en el \xE1rea de trabajo. Advertencia: La activaci\xF3n de esta configuraci\xF3n repercute en el rendimiento.","Controla la sangr\xEDa de \xE1rbol en p\xEDxeles.","Controla si el \xE1rbol debe representar gu\xEDas de sangr\xEDa.","Controla si las listas y los \xE1rboles tienen un desplazamiento suave.","La navegaci\xF3n simple del teclado se centra en elementos que coinciden con la entrada del teclado. El emparejamiento se hace solo en prefijos.","Destacar la navegaci\xF3n del teclado resalta los elementos que coinciden con la entrada del teclado. M\xE1s arriba y abajo la navegaci\xF3n atravesar\xE1 solo los elementos destacados.","La navegaci\xF3n mediante el teclado de filtro filtrar\xE1 y ocultar\xE1 todos los elementos que no coincidan con la entrada del teclado.","Controla el estilo de navegaci\xF3n del teclado para listas y \xE1rboles en el \xE1rea de trabajo. Puede ser simple, resaltar y filtrar.",'Controla si la navegaci\xF3n del teclado en listas y \xE1rboles se activa autom\xE1ticamente simplemente escribiendo. Si se establece en "false", la navegaci\xF3n con el teclado solo se activa al ejecutar el comando "list.toggleKeyboardNavigation", para el cual puede asignar un m\xE9todo abreviado de teclado.',"Controla c\xF3mo se expanden las carpetas de \xE1rbol al hacer clic en sus nombres. Tenga en cuenta que algunos \xE1rboles y listas pueden optar por omitir esta configuraci\xF3n si no es aplicable."],"vs/platform/markers/common/markers":["Error","Advertencia","Informaci\xF3n"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","usado recientemente","otros comandos",'El comando "{0}" dio lugar a un error ({1})'],"vs/platform/quickinput/browser/helpQuickAccess":["comandos globales","comandos del editor","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["Color de primer plano general. Este color solo se usa si un componente no lo invalida.","Color de primer plano general para los mensajes de erroe. Este color solo se usa si un componente no lo invalida.","El color predeterminado para los iconos en el \xE1rea de trabajo.","Color de borde de los elementos con foco. Este color solo se usa si un componente no lo invalida.","Un borde adicional alrededor de los elementos para separarlos unos de otros y as\xED mejorar el contraste.","Un borde adicional alrededor de los elementos activos para separarlos unos de otros y as\xED mejorar el contraste.","Color de primer plano para los v\xEDnculos en el texto.","Color de fondo para los bloques de c\xF3digo en el texto.","Color de sombra de los widgets dentro del editor, como buscar/reemplazar","Fondo de cuadro de entrada.","Primer plano de cuadro de entrada.","Borde de cuadro de entrada.","Color de borde de opciones activadas en campos de entrada.","Color de fondo de las opciones activadas en los campos de entrada.","Color de primer plano de las opciones activadas en los campos de entrada.","Color de fondo de validaci\xF3n de entrada para gravedad de informaci\xF3n.","Color de primer plano de validaci\xF3n de entrada para informaci\xF3n de gravedad.","Color de borde de validaci\xF3n de entrada para gravedad de informaci\xF3n.","Color de fondo de validaci\xF3n de entrada para gravedad de advertencia.","Color de primer plano de validaci\xF3n de entrada para informaci\xF3n de advertencia.","Color de borde de validaci\xF3n de entrada para gravedad de advertencia.","Color de fondo de validaci\xF3n de entrada para gravedad de error.","Color de primer plano de validaci\xF3n de entrada para informaci\xF3n de error.","Color de borde de valdaci\xF3n de entrada para gravedad de error.","Fondo de lista desplegable.","Primer plano de lista desplegable.","Color de primer plano del bot\xF3n.","Color de fondo del bot\xF3n.","Color de fondo del bot\xF3n al mantener el puntero.","Color de fondo de la insignia. Las insignias son peque\xF1as etiquetas de informaci\xF3n, por ejemplo los resultados de un n\xFAmero de resultados.","Color de primer plano de la insignia. Las insignias son peque\xF1as etiquetas de informaci\xF3n, por ejemplo los resultados de un n\xFAmero de resultados.","Sombra de la barra de desplazamiento indica que la vista se ha despazado.","Color de fondo de control deslizante de barra de desplazamiento.","Color de fondo de barra de desplazamiento cursor cuando se pasar sobre el control.","Color de fondo de la barra de desplazamiento al hacer clic.","Color de fondo para la barra de progreso que se puede mostrar para las operaciones de larga duraci\xF3n.","Color de fondo del texto de error del editor. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color de primer plano de squigglies de error en el editor.","Color del borde de los cuadros de error en el editor.","Color de fondo del texto de advertencia del editor. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color de primer plano de squigglies de advertencia en el editor.","Color del borde de los cuadros de advertencia en el editor.","Color de fondo del texto de informaci\xF3n del editor. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color de primer plano de los subrayados ondulados informativos en el editor.","Color del borde de los cuadros de informaci\xF3n en el editor.","Color de primer plano de pista squigglies en el editor.","Color del borde de los cuadros de sugerencia en el editor.","Color de fondo del editor.","Color de primer plano predeterminado del editor.","Color de fondo del editor de widgets como buscar/reemplazar","Color de primer plano de los widgets del editor, como buscar y reemplazar.","Color de borde de los widgets del editor. El color solo se usa si el widget elige tener un borde y no invalida el color.","Color del borde de la barra de cambio de tama\xF1o de los widgets del editor. El color se utiliza solo si el widget elige tener un borde de cambio de tama\xF1o y si un widget no invalida el color.","Color de fondo del selector r\xE1pido. El widget del selector r\xE1pido es el contenedor para selectores como la paleta de comandos.","Color de primer plano del selector r\xE1pido. El widget del selector r\xE1pido es el contenedor para selectores como la paleta de comandos.","Color de fondo del t\xEDtulo del selector r\xE1pido. El widget del selector r\xE1pido es el contenedor para selectores como la paleta de comandos.","Color de fondo del selector r\xE1pido para el elemento con el foco.","Selector de color r\xE1pido para la agrupaci\xF3n de etiquetas.","Selector de color r\xE1pido para la agrupaci\xF3n de bordes.","Color de la selecci\xF3n del editor.","Color del texto seleccionado para alto contraste.","Color de la selecci\xF3n en un editor inactivo. El color no debe ser opaco para no ocultar decoraciones subyacentes.","Color en las regiones con el mismo contenido que la selecci\xF3n. El color no debe ser opaco para no ocultar decoraciones subyacentes.","Color de borde de las regiones con el mismo contenido que la selecci\xF3n.","Color de la coincidencia de b\xFAsqueda actual.","Color de los otros resultados de la b\xFAsqueda. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color de la gama que limita la b\xFAsqueda. El color no debe ser opaco para no ocultar decoraciones subyacentes.","Color de borde de la coincidencia de b\xFAsqueda actual.","Color de borde de otra b\xFAsqueda que coincide.","Color del borde de la gama que limita la b\xFAsqueda. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Destacar debajo de la palabra para la que se muestra un mensaje al mantener el mouse. El color no debe ser opaco para no ocultar decoraciones subyacentes.","Color de fondo al mantener el puntero en el editor.","Color de primer plano al mantener el puntero en el editor.","Color del borde al mantener el puntero en el editor.","Color de fondo de la barra de estado al mantener el puntero en el editor.","Color de los v\xEDnculos activos.","Color de primer plano de las sugerencias insertadas","Color de fondo de las sugerencias insertadas","El color utilizado para el icono de bombilla de acciones.","El color utilizado para el icono de la bombilla de acciones de correcci\xF3n autom\xE1tica.","Color de fondo para el texto que se insert\xF3. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color de fondo para el texto que se elimin\xF3. El color no debe ser opaco para no ocultar decoraciones subyacentes.","Color de contorno para el texto insertado.","Color de contorno para el texto quitado.","Color del borde entre ambos editores de texto.","Color de relleno diagonal del editor de diferencias. El relleno diagonal se usa en las vistas de diferencias en paralelo.","Color de fondo de la lista o el \xE1rbol del elemento con el foco cuando la lista o el \xE1rbol est\xE1n activos. Una lista o un \xE1rbol tienen el foco del teclado cuando est\xE1n activos, cuando est\xE1n inactivos no.","Color de primer plano de la lista o el \xE1rbol del elemento con el foco cuando la lista o el \xE1rbol est\xE1n activos. Una lista o un \xE1rbol tienen el foco del teclado cuando est\xE1n activos, cuando est\xE1n inactivos no.","Color de contorno de la lista o el \xE1rbol del elemento con el foco cuando la lista o el \xE1rbol est\xE1n activos. Una lista o un \xE1rbol tienen el foco del teclado cuando est\xE1n activos, pero no cuando est\xE1n inactivos.","Color de fondo de la lista o el \xE1rbol del elemento seleccionado cuando la lista o el \xE1rbol est\xE1n activos. Una lista o un \xE1rbol tienen el foco del teclado cuando est\xE1n activos, cuando est\xE1n inactivos no.","Color de primer plano de la lista o el \xE1rbol del elemento seleccionado cuando la lista o el \xE1rbol est\xE1n activos. Una lista o un \xE1rbol tienen el foco del teclado cuando est\xE1n activos, cuando est\xE1n inactivos no.","Color de fondo de la lista o el \xE1rbol del elemento seleccionado cuando la lista o el \xE1rbol est\xE1n inactivos. Una lista o un \xE1rbol tienen el foco del teclado cuando est\xE1n activos, cuando est\xE1n inactivos no.","Color de primer plano de la lista o el \xE1rbol del elemento con el foco cuando la lista o el \xE1rbol esta inactiva. Una lista o un \xE1rbol tiene el foco del teclado cuando est\xE1 activo, cuando esta inactiva no.","Color de fondo de la lista o el \xE1rbol del elemento con el foco cuando la lista o el \xE1rbol est\xE1n inactivos. Una lista o un \xE1rbol tienen el foco del teclado cuando est\xE1n activos, pero no cuando est\xE1n inactivos.","Color de contorno de la lista o el \xE1rbol del elemento con el foco cuando la lista o el \xE1rbol est\xE1n inactivos. Una lista o un \xE1rbol tienen el foco del teclado cuando est\xE1n activos, pero no cuando est\xE1n inactivos.","Fondo de la lista o el \xE1rbol al mantener el mouse sobre los elementos.","Color de primer plano de la lista o el \xE1rbol al pasar por encima de los elementos con el rat\xF3n.","Fondo de arrastrar y colocar la lista o el \xE1rbol al mover los elementos con el mouse.","Color de primer plano de la lista o el \xE1rbol de las coincidencias resaltadas al buscar dentro de la lista o el \xE1bol.","Color de fondo del widget de filtro de tipo en listas y \xE1rboles.","Color de contorno del widget de filtro de tipo en listas y \xE1rboles.","Color de contorno del widget de filtro de tipo en listas y \xE1rboles, cuando no hay coincidencias.","Color de trazo de \xE1rbol para las gu\xEDas de sangr\xEDa.","Color de trazo de \xE1rbol para las gu\xEDas de sangr\xEDa.","Color del borde de los men\xFAs.","Color de primer plano de los elementos de men\xFA.","Color de fondo de los elementos de men\xFA.","Color de primer plano del menu para el elemento del men\xFA seleccionado.","Color de fondo del menu para el elemento del men\xFA seleccionado.","Color del borde del elemento seleccionado en los men\xFAs.","Color del separador del menu para un elemento del men\xFA.","Resaltado del color de fondo para una ficha de un fragmento de c\xF3digo.","Resaltado del color del borde para una ficha de un fragmento de c\xF3digo.","Resaltado del color de fondo para la \xFAltima ficha de un fragmento de c\xF3digo.","Resaltado del color del borde para la \xFAltima tabulaci\xF3n de un fragmento de c\xF3digo.","Color del marcador de regla general para buscar actualizaciones. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color del marcador de la regla general para los destacados de la selecci\xF3n. El color no debe ser opaco para no ocultar las decoraciones subyacentes.","Color de marcador de minimapa para coincidencias de b\xFAsqueda.","Color del marcador de minimapa para la selecci\xF3n del editor.","Color del marcador de minimapa para errores.","Color del marcador de minimapa para advertencias.","Color de fondo del minimapa.","Color de fondo del deslizador del minimapa.","Color de fondo del deslizador del minimapa al pasar el puntero.","Color de fondo del deslizador de minimapa al hacer clic en \xE9l.","Color utilizado para el icono de error de problemas.","Color utilizado para el icono de advertencia de problemas.","Color utilizado para el icono de informaci\xF3n de problemas."],"vs/platform/theme/common/iconRegistry":["Identificador de la fuente que se va a usar. Si no se establece, se usa la fuente definida en primer lugar.","Car\xE1cter de fuente asociado a la definici\xF3n del icono.","Icono de la acci\xF3n de cierre en los widgets."],"vs/platform/undoRedo/common/undoRedoService":["Se han cerrado los siguientes archivos y se han modificado en el disco: {0}.","Los siguientes archivos se han modificado de forma incompatible: {0}.",'No se pudo deshacer "{0}" en todos los archivos. {1}','No se pudo deshacer "{0}" en todos los archivos. {1}','No se pudo deshacer "{0}" en todos los archivos porque se realizaron cambios en {1}','No se pudo deshacer "{0}" en todos los archivos porque ya hay una operaci\xF3n de deshacer o rehacer en ejecuci\xF3n en {1}','No se pudo deshacer "{0}" en todos los archivos porque se produjo una operaci\xF3n de deshacer o rehacer mientras tanto','\xBFDesea deshacer "{0}" en todos los archivos?',"Deshacer en {0} archivos","Deshacer este archivo","Cancelar",'No se pudo deshacer "{0}" porque ya hay una operaci\xF3n de deshacer o rehacer en ejecuci\xF3n.','\xBFQuiere deshacer "{0}"?',"Deshacer","Cancelar",'No se pudo rehacer "{0}" en todos los archivos. {1}','No se pudo rehacer "{0}" en todos los archivos. {1}','No se pudo volver a hacer "{0}" en todos los archivos porque se realizaron cambios en {1}','No se pudo rehacer "{0}" en todos los archivos porque ya hay una operaci\xF3n de deshacer o rehacer en ejecuci\xF3n en {1}','No se pudo rehacer "{0}" en todos los archivos porque se produjo una operaci\xF3n de deshacer o rehacer mientras tanto','No se pudo rehacer "{0}" porque ya hay una operaci\xF3n de deshacer o rehacer en ejecuci\xF3n.']}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.fr.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.fr.js new file mode 100644 index 0000000..d16d576 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.fr.js @@ -0,0 +1,8 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/define("vs/editor/editor.main.nls.fr",{"vs/base/browser/ui/actionbar/actionViewItems":["{0} ({1})"],"vs/base/browser/ui/findinput/findInput":["entr\xE9e"],"vs/base/browser/ui/findinput/findInputCheckboxes":["Respecter la casse","Mot entier","Utiliser une expression r\xE9guli\xE8re"],"vs/base/browser/ui/findinput/replaceInput":["entr\xE9e","Pr\xE9server la casse"],"vs/base/browser/ui/iconLabel/iconLabel":["Chargement..."],"vs/base/browser/ui/inputbox/inputBox":["Erreur\xA0: {0}","Avertissement\xA0: {0}","Info\xA0: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["Ind\xE9pendant"],"vs/base/browser/ui/menu/menu":["{0} ({1})"],"vs/base/browser/ui/tree/abstractTree":["Effacer","D\xE9sactiver le filtre sur le type","Activer le filtre sur le type","Aucun \xE9l\xE9ment","{0}\xA0\xE9l\xE9ments sur {1} correspondants"],"vs/base/common/actions":["(vide)"],"vs/base/common/errorMessage":["{0}: {1}","Une erreur syst\xE8me s'est produite ({0})","Une erreur inconnue s\u2019est produite. Veuillez consulter le journal pour plus de d\xE9tails.","Une erreur inconnue s\u2019est produite. Veuillez consulter le journal pour plus de d\xE9tails.","{0} ({1}\xA0erreurs au total)","Une erreur inconnue s\u2019est produite. Veuillez consulter le journal pour plus de d\xE9tails."],"vs/base/common/keybindingLabels":["Ctrl","Maj","Alt","Windows","Ctrl","Maj","Alt","Super","Contr\xF4le","Maj","Alt","Commande","Contr\xF4le","Maj","Alt","Windows","Contr\xF4le","Maj","Alt","Super"],"vs/base/parts/quickinput/browser/quickInput":["Pr\xE9c\xE9dent","{0}/{1}","Taper pour affiner les r\xE9sultats.","{0}\xA0r\xE9sultats","{0} S\xE9lectionn\xE9s","OK","Personnalis\xE9","Pr\xE9c\xE9dent ({0})","Pr\xE9c\xE9dent"],"vs/base/parts/quickinput/browser/quickInputList":["Entr\xE9e rapide"],"vs/editor/browser/controller/coreCommands":["Aligner par rapport \xE0 la fin m\xEAme en cas de passage \xE0 des lignes plus longues","Aligner par rapport \xE0 la fin m\xEAme en cas de passage \xE0 des lignes plus longues"],"vs/editor/browser/controller/textAreaHandler":["\xE9diteur","L'\xE9diteur n'est pas accessible pour le moment. Appuyez sur {0} pour voir les options."],"vs/editor/browser/core/keybindingCancellation":["Indique si l'\xE9diteur ex\xE9cute une op\xE9ration annulable, par exemple 'Avoir un aper\xE7u des r\xE9f\xE9rences'"],"vs/editor/browser/editorExtensions":["Ann&&uler","Annuler","&&R\xE9tablir","R\xE9tablir","&&S\xE9lectionner tout","Tout s\xE9lectionner"],"vs/editor/browser/widget/codeEditorWidget":["Le nombre de curseurs a \xE9t\xE9 limit\xE9 \xE0\xA0{0}."],"vs/editor/browser/widget/diffEditorWidget":["\xC9l\xE9ment d\xE9coratif de ligne pour les insertions dans l'\xE9diteur de diff\xE9rences.","\xC9l\xE9ment d\xE9coratif de ligne pour les suppressions dans l'\xE9diteur de diff\xE9rences.","Impossible de comparer les fichiers car l'un d'eux est trop volumineux."],"vs/editor/browser/widget/diffReview":["Ic\xF4ne de l'option Ins\xE9rer dans la revue des diff\xE9rences.","Ic\xF4ne de l'option Supprimer dans la revue des diff\xE9rences.","Ic\xF4ne de l'option Fermer dans la revue des diff\xE9rences.","Fermer","aucune ligne chang\xE9e","1\xA0ligne chang\xE9e","{0}\xA0lignes chang\xE9es","Diff\xE9rence\xA0{0} sur\xA0{1}\xA0: ligne d'origine {2}, {3}, ligne modifi\xE9e {4}, {5}","vide","{0} ligne inchang\xE9e {1}","{0}\xA0ligne d'origine {1}\xA0ligne modifi\xE9e {2}","+ {0}\xA0ligne modifi\xE9e {1}","- {0} ligne d'origine {1}","Acc\xE9der \xE0 la diff\xE9rence suivante","Acc\xE9der la diff\xE9rence pr\xE9c\xE9dente"],"vs/editor/browser/widget/inlineDiffMargin":["Copier les lignes supprim\xE9es","Copier la ligne supprim\xE9e","Copier la ligne supprim\xE9e ({0})","Annuler la modification","Copier la ligne supprim\xE9e ({0})"],"vs/editor/common/config/commonEditorConfig":["\xC9diteur","Le nombre d'espaces auxquels une tabulation est \xE9gale. Ce param\xE8tre est substitu\xE9 bas\xE9 sur le contenu du fichier lorsque `#editor.detectIndentation#` est \xE0 'on'.","Espaces ins\xE9r\xE9s quand vous appuyez sur la touche Tab. Ce param\xE8tre est remplac\xE9 en fonction du contenu du fichier quand '#editor.detectIndentation#' est activ\xE9.","Contr\xF4le si '#editor.tabSize#' et '#editor.insertSpaces#' sont automatiquement d\xE9tect\xE9s lors de l\u2019ouverture d\u2019un fichier en fonction de son contenu.","Supprimer l'espace blanc de fin ins\xE9r\xE9 automatiquement.","Traitement sp\xE9cial des fichiers volumineux pour d\xE9sactiver certaines fonctionnalit\xE9s utilisant beaucoup de m\xE9moire.","Contr\xF4le si la saisie semi-automatique doit \xEAtre calcul\xE9e en fonction des mots pr\xE9sents dans le document.","Sugg\xE8re uniquement des mots dans le document actif.","Sugg\xE8re des mots dans tous les documents ouverts du m\xEAme langage.","Sugg\xE8re des mots dans tous les documents ouverts.","Contr\xF4le la fa\xE7on dont sont calcul\xE9es les compl\xE9tions bas\xE9es sur des mots dans les documents.","Coloration s\xE9mantique activ\xE9e pour tous les th\xE8mes de couleur.","Coloration s\xE9mantique d\xE9sactiv\xE9e pour tous les th\xE8mes de couleur.","La coloration s\xE9mantique est configur\xE9e par le param\xE8tre 'semanticHighlighting' du th\xE8me de couleur actuel.","Contr\xF4le si semanticHighlighting est affich\xE9 pour les langages qui le prennent en charge.","Garder les \xE9diteurs d'aper\xE7u ouverts m\xEAme si l'utilisateur double-clique sur son contenu ou appuie sur la touche \xC9chap. ","Les lignes plus longues que cette valeur ne sont pas tokenis\xE9es pour des raisons de performances","D\xE9lai d'expiration en millisecondes avant annulation du calcul de diff. Utilisez\xA00 pour supprimer le d\xE9lai d'expiration.","Contr\xF4le si l'\xE9diteur de diff\xE9rences affiche les diff\xE9rences en mode c\xF4te \xE0 c\xF4te ou inline.","Quand il est activ\xE9, l'\xE9diteur de diff\xE9rences ignore les changements d'espace blanc de d\xE9but ou de fin.","Contr\xF4le si l'\xE9diteur de diff\xE9rences affiche les indicateurs +/- pour les changements ajout\xE9s/supprim\xE9s .","Contr\xF4le si l'\xE9diteur affiche CodeLens.","Le retour automatique \xE0 la ligne n'est jamais effectu\xE9.","Le retour automatique \xE0 la ligne s'effectue en fonction de la largeur de la fen\xEAtre d'affichage.","Le retour automatique \xE0 la ligne d\xE9pend du param\xE8tre '#editor.wordWrap#'."],"vs/editor/common/config/editorOptions":["L'\xE9diteur utilise les API de la plateforme pour d\xE9tecter si un lecteur d'\xE9cran est attach\xE9.","L'\xE9diteur est optimis\xE9 en permanence pour les lecteurs d'\xE9cran. Le retour automatique \xE0 la ligne est d\xE9sactiv\xE9.","L'\xE9diteur n'est jamais optimis\xE9 pour une utilisation avec un lecteur d'\xE9cran.","Contr\xF4le si l'\xE9diteur doit s'ex\xE9cuter dans un mode optimis\xE9 pour les lecteurs d'\xE9cran. Si la valeur est on, le retour automatique \xE0 la ligne est d\xE9sactiv\xE9.","Contr\xF4le si un espace est ins\xE9r\xE9 pour les commentaires.","Contr\xF4le si les lignes vides doivent \xEAtre ignor\xE9es avec des actions d'activation/de d\xE9sactivation, d'ajout ou de suppression des commentaires de ligne.","Contr\xF4le si la copie sans s\xE9lection permet de copier la ligne actuelle.","Contr\xF4le si le curseur doit sauter pour rechercher les correspondances lors de la saisie.","D\xE9termine si la cha\xEEne de recherche dans le Widget Recherche est initialis\xE9e avec la s\xE9lection de l\u2019\xE9diteur.","Ne jamais activer Rechercher automatiquement dans la s\xE9lection (par d\xE9faut)","Toujours activer Rechercher automatiquement dans la s\xE9lection","Activez Rechercher automatiquement dans la s\xE9lection quand plusieurs lignes de contenu sont s\xE9lectionn\xE9es.","Contr\xF4le la condition d'activation automatique de la recherche dans la s\xE9lection.","D\xE9termine si le Widget Recherche devrait lire ou modifier le presse-papiers de recherche partag\xE9 sur macOS.","Contr\xF4le si le widget Recherche doit ajouter des lignes suppl\xE9mentaires en haut de l'\xE9diteur. Quand la valeur est true, vous pouvez faire d\xE9filer au-del\xE0 de la premi\xE8re ligne si le widget Recherche est visible.","Contr\xF4le si la recherche red\xE9marre automatiquement depuis le d\xE9but (ou la fin) quand il n'existe aucune autre correspondance.","Active/d\xE9sactive les ligatures de police (fonctionnalit\xE9s de police 'calt' et 'liga'). Remplacez ceci par une cha\xEEne pour contr\xF4ler de mani\xE8re pr\xE9cise la propri\xE9t\xE9 CSS 'font-feature-settings'.","Propri\xE9t\xE9 CSS 'font-feature-settings' explicite. Vous pouvez passer une valeur bool\xE9enne \xE0 la place si vous devez uniquement activer/d\xE9sactiver les ligatures.","Configure les ligatures de police ou les fonctionnalit\xE9s de police. Il peut s'agir d'une valeur bool\xE9enne permettant d'activer/de d\xE9sactiver les ligatures, ou d'une cha\xEEne correspondant \xE0 la valeur de la propri\xE9t\xE9 CSS 'font-feature-settings'.","Contr\xF4le la taille de police en pixels.",'Seuls les mots cl\xE9s "normal" et "bold", ou les nombres compris entre\xA01 et\xA01\xA0000 sont autoris\xE9s.',`Contr\xF4le l'\xE9paisseur de police. Accepte les mots cl\xE9s "normal" et "bold", ou les nombres compris entre\xA01 et\xA01\xA0000.`,"Montrer l'aper\xE7u des r\xE9sultats (par d\xE9faut)","Acc\xE9der au r\xE9sultat principal et montrer un aper\xE7u","Acc\xE9der au r\xE9sultat principal et activer l'acc\xE8s sans aper\xE7u pour les autres","Ce param\xE8tre est d\xE9pr\xE9ci\xE9, utilisez des param\xE8tres distincts comme 'editor.editor.gotoLocation.multipleDefinitions' ou 'editor.editor.gotoLocation.multipleImplementations' \xE0 la place.","Contr\xF4le le comportement de la commande 'Atteindre la d\xE9finition' quand plusieurs emplacements cibles existent.","Contr\xF4le le comportement de la commande 'Atteindre la d\xE9finition de type' quand plusieurs emplacements cibles existent.","Contr\xF4le le comportement de la commande 'Atteindre la d\xE9claration' quand plusieurs emplacements cibles existent.","Contr\xF4le le comportement de la commande 'Atteindre les impl\xE9mentations' quand plusieurs emplacements cibles existent.","Contr\xF4le le comportement de la commande 'Atteindre les r\xE9f\xE9rences' quand plusieurs emplacements cibles existent.","ID de commande alternatif ex\xE9cut\xE9 quand le r\xE9sultat de 'Atteindre la d\xE9finition' est l'emplacement actuel.","ID de commande alternatif ex\xE9cut\xE9 quand le r\xE9sultat de 'Atteindre la d\xE9finition de type' est l'emplacement actuel.","ID de commande alternatif ex\xE9cut\xE9 quand le r\xE9sultat de 'Atteindre la d\xE9claration' est l'emplacement actuel.","ID de commande alternatif ex\xE9cut\xE9 quand le r\xE9sultat de 'Atteindre l'impl\xE9mentation' est l'emplacement actuel.","ID de commande alternatif ex\xE9cut\xE9 quand le r\xE9sultat de 'Atteindre la r\xE9f\xE9rence' est l'emplacement actuel.","Contr\xF4le si le pointage est affich\xE9.","Contr\xF4le le d\xE9lai en millisecondes, apr\xE8s lequel le survol est affich\xE9.","Contr\xF4le si le pointage doit rester visible quand la souris est d\xE9plac\xE9e au-dessus.","Active l\u2019ampoule d\u2019action de code dans l\u2019\xE9diteur.","Active les indicateurs inline dans l'\xE9diteur.","Contr\xF4le la taille de police des indicateurs inline dans l'\xE9diteur. Quand la valeur est '0', 90\xA0% de '#editor.fontSize#' est utilis\xE9.","Contr\xF4le la famille de polices des indicateurs inline dans l'\xE9diteur.","Contr\xF4le la hauteur de ligne. Utilisez 0 pour calculer la hauteur de ligne de la taille de la police.","Contr\xF4le si la minimap est affich\xE9e.","Le minimap a la m\xEAme taille que le contenu de l'\xE9diteur (d\xE9filement possible).","Le minimap s'agrandit ou se r\xE9duit selon les besoins pour remplir la hauteur de l'\xE9diteur (pas de d\xE9filement).","Le minimap est r\xE9duit si n\xE9cessaire pour ne jamais d\xE9passer la taille de l'\xE9diteur (pas de d\xE9filement).","Contr\xF4le la taille du minimap.","Contr\xF4le le c\xF4t\xE9 o\xF9 afficher la minimap.","Contr\xF4le quand afficher le curseur du minimap.","\xC9chelle du contenu dessin\xE9 dans le minimap\xA0: 1, 2\xA0ou\xA03.","Afficher les caract\xE8res r\xE9els sur une ligne par opposition aux blocs de couleur.","Limiter la largeur de la minimap pour afficher au plus un certain nombre de colonnes.","Contr\xF4le la quantit\xE9 d\u2019espace entre le bord sup\xE9rieur de l\u2019\xE9diteur et la premi\xE8re ligne.","Contr\xF4le la quantit\xE9 d'espace entre le bord inf\xE9rieur de l'\xE9diteur et la derni\xE8re ligne.","Active une fen\xEAtre contextuelle qui affiche de la documentation sur les param\xE8tres et des informations sur les types \xE0 mesure que vous tapez.","D\xE9termine si le menu de suggestions de param\xE8tres se ferme ou reviens au d\xE9but lorsque la fin de la liste est atteinte.","Activez les suggestions rapides dans les cha\xEEnes.","Activez les suggestions rapides dans les commentaires.","Activez les suggestions rapides en dehors des cha\xEEnes et des commentaires.","Contr\xF4le si les suggestions doivent appara\xEEtre automatiquement pendant la saisie.","Les num\xE9ros de ligne ne sont pas affich\xE9s.","Les num\xE9ros de ligne sont affich\xE9s en nombre absolu.","Les num\xE9ros de ligne sont affich\xE9s sous la forme de distance en lignes \xE0 la position du curseur.","Les num\xE9ros de ligne sont affich\xE9s toutes les 10 lignes.","Contr\xF4le l'affichage des num\xE9ros de ligne.","Nombre de caract\xE8res monospace auxquels cette r\xE8gle d'\xE9diteur effectue le rendu.","Couleur de cette r\xE8gle d'\xE9diteur.","Rendre les r\xE8gles verticales apr\xE8s un certain nombre de caract\xE8res \xE0 espacement fixe. Utiliser plusieurs valeurs pour plusieurs r\xE8gles. Aucune r\xE8gle n'est dessin\xE9e si le tableau est vide.","Ins\xE9rez une suggestion sans remplacer le texte \xE0 droite du curseur.","Ins\xE9rez une suggestion et remplacez le texte \xE0 droite du curseur.","Contr\xF4le si les mots sont remplac\xE9s en cas d'acceptation de la saisie semi-automatique. Notez que cela d\xE9pend des extensions adh\xE9rant \xE0 cette fonctionnalit\xE9.","D\xE9termine si le filtre et le tri des suggestions doivent prendre en compte les fautes de frappes mineures.","Contr\xF4le si le tri favorise trier les mots qui apparaissent pr\xE8s du curseur.","Contr\xF4le si les s\xE9lections de suggestion m\xE9moris\xE9es sont partag\xE9es entre plusieurs espaces de travail et fen\xEAtres (n\xE9cessite '#editor.suggestSelection#').","Contr\xF4le si un extrait de code actif emp\xEAche les suggestions rapides.","Contr\xF4le s'il faut montrer ou masquer les ic\xF4nes dans les suggestions.","Contr\xF4le la visibilit\xE9 de la barre d'\xE9tat en bas du widget de suggestion.","D\xE9termine si les d\xE9tails du widget de suggestion sont inclus dans l'\xE9tiquette ou uniquement dans le widget de d\xE9tails","Ce param\xE8tre est d\xE9pr\xE9ci\xE9. Le widget de suggestion peut d\xE9sormais \xEAtre redimensionn\xE9.","Ce param\xE8tre est d\xE9pr\xE9ci\xE9, veuillez utiliser des param\xE8tres distincts comme 'editor.suggest.showKeywords' ou 'editor.suggest.showSnippets' \xE0 la place.","Si activ\xE9, IntelliSense montre des suggestions de type 'method'.","Si activ\xE9, IntelliSense montre des suggestions de type 'function'.","Si activ\xE9, IntelliSense montre des suggestions de type 'constructor'.","Si activ\xE9, IntelliSense montre des suggestions de type 'field'.","Si activ\xE9, IntelliSense montre des suggestions de type 'variable'.","Si activ\xE9, IntelliSense montre des suggestions de type 'class'.","Si activ\xE9, IntelliSense montre des suggestions de type 'struct'.","Si activ\xE9, IntelliSense montre des suggestions de type 'interface'.","Si activ\xE9, IntelliSense montre des suggestions de type 'module'.","Si activ\xE9, IntelliSense montre des suggestions de type 'property'.","Si activ\xE9, IntelliSense montre des suggestions de type 'event'.","Si activ\xE9, IntelliSense montre des suggestions de type 'operator'.","Si activ\xE9, IntelliSense montre des suggestions de type 'unit'.","Si activ\xE9, IntelliSense montre des suggestions de type 'value'.","Si activ\xE9, IntelliSense montre des suggestions de type 'constant'.","Si activ\xE9, IntelliSense montre des suggestions de type 'enum'.","Si activ\xE9, IntelliSense montre des suggestions de type 'enumMember'.","Si activ\xE9, IntelliSense montre des suggestions de type 'keyword'.","Si activ\xE9, IntelliSense montre des suggestions de type 'text'.","Si activ\xE9, IntelliSense montre des suggestions de type 'color'.","Si activ\xE9, IntelliSense montre des suggestions de type 'file'.","Si activ\xE9, IntelliSense montre des suggestions de type 'reference'.","Si activ\xE9, IntelliSense montre des suggestions de type 'customcolor'.","Si activ\xE9, IntelliSense montre des suggestions de type 'folder'.","Si activ\xE9, IntelliSense montre des suggestions de type 'typeParameter'.","Si activ\xE9, IntelliSense montre des suggestions de type 'snippet'.","Si activ\xE9, IntelliSense montre des suggestions de type 'utilisateur'.","Si activ\xE9, IntelliSense montre des suggestions de type 'probl\xE8mes'.","Indique si les espaces blancs de d\xE9but et de fin doivent toujours \xEAtre s\xE9lectionn\xE9s.","Contr\xF4le si les suggestions doivent \xEAtre accept\xE9es sur les caract\xE8res de validation. Par exemple, en JavaScript, le point-virgule (`;`) peut \xEAtre un caract\xE8re de validation qui accepte une suggestion et tape ce caract\xE8re.","Accepter uniquement une suggestion avec 'Entr\xE9e' quand elle effectue une modification textuelle.","Contr\xF4le si les suggestions sont accept\xE9es apr\xE8s appui sur 'Entr\xE9e', en plus de 'Tab'. Permet d\u2019\xE9viter toute ambigu\xEFt\xE9 entre l\u2019insertion de nouvelles lignes et l'acceptation de suggestions.","Contr\xF4le le nombre de lignes dans l'\xE9diteur qui peuvent \xEAtre lues par un lecteur d'\xE9cran. Avertissement : Ce param\xE8tre a une incidence sur les performances quand le nombre est sup\xE9rieur \xE0 la valeur par d\xE9faut.","Contenu de l'\xE9diteur","Utilisez les configurations de langage pour d\xE9terminer quand fermer automatiquement les parenth\xE8ses.","Fermer automatiquement les parenth\xE8ses uniquement lorsque le curseur est \xE0 gauche de l\u2019espace.","Contr\xF4le si l\u2019\xE9diteur doit fermer automatiquement les parenth\xE8ses quand l\u2019utilisateur ajoute une parenth\xE8se ouvrante.","Tapez avant les guillemets ou les crochets fermants uniquement s'ils sont automatiquement ins\xE9r\xE9s.","Contr\xF4le si l'\xE9diteur doit taper avant les guillemets ou crochets fermants.","Utilisez les configurations de langage pour d\xE9terminer quand fermer automatiquement les guillemets.","Fermer automatiquement les guillemets uniquement lorsque le curseur est \xE0 gauche de l\u2019espace.","Contr\xF4le si l\u2019\xE9diteur doit fermer automatiquement les guillemets apr\xE8s que l\u2019utilisateur ajoute un guillemet ouvrant.","L'\xE9diteur n'ins\xE8re pas de retrait automatiquement.","L'\xE9diteur conserve le retrait de la ligne actuelle.","L'\xE9diteur conserve le retrait de la ligne actuelle et honore les crochets d\xE9finis par le langage.","L'\xE9diteur conserve le retrait de la ligne actuelle, honore les crochets d\xE9finis par le langage et appelle des objets onEnterRules sp\xE9ciaux d\xE9finis par les langages.","L'\xE9diteur conserve le retrait de la ligne actuelle, honore les crochets d\xE9finis par le langage, appelle des objets onEnterRules sp\xE9ciaux d\xE9finis par les langages et honore les objets indentationRules d\xE9finis par les langages.","Contr\xF4le si l'\xE9diteur doit ajuster automatiquement le retrait quand les utilisateurs tapent, collent, d\xE9placent ou mettent en retrait des lignes.","Utilisez les configurations de langue pour d\xE9terminer quand entourer automatiquement les s\xE9lections.","Entourez avec des guillemets et non des crochets.","Entourez avec des crochets et non des guillemets.","Contr\xF4le si l'\xE9diteur doit automatiquement entourer les s\xE9lections quand l'utilisateur tape des guillemets ou des crochets.","\xC9mule le comportement des tabulations pour la s\xE9lection quand des espaces sont utilis\xE9s \xE0 des fins de mise en retrait. La s\xE9lection respecte les taquets de tabulation.","Contr\xF4le si l'\xE9diteur affiche CodeLens.","Contr\xF4le la famille de polices pour CodeLens.","Contr\xF4le la taille de police en pixels pour CodeLens. Quand la valeur est '0', 90\xA0% de '#editor.fontSize#' est utilis\xE9.","Contr\xF4le si l'\xE9diteur doit afficher les \xE9l\xE9ments d\xE9coratifs de couleurs inline et le s\xE9lecteur de couleurs.","Autoriser l'utilisation de la souris et des touches pour s\xE9lectionner des colonnes.","Contr\xF4le si la coloration syntaxique doit \xEAtre copi\xE9e dans le presse-papiers.","Contr\xF4ler le style d\u2019animation du curseur.","Contr\xF4le si l'animation du point d'insertion doit \xEAtre activ\xE9e.","Contr\xF4le le style du curseur.","Contr\xF4le le nombre minimal de lignes de d\xE9but et de fin visibles autour du curseur. \xC9galement appel\xE9 'scrollOff' ou 'scrollOffset' dans d'autres \xE9diteurs.","'cursorSurroundingLines' est appliqu\xE9 seulement s'il est d\xE9clench\xE9 via le clavier ou une API.","'cursorSurroundingLines' est toujours appliqu\xE9.","Contr\xF4le quand 'cursorSurroundingLines' doit \xEAtre appliqu\xE9.","D\xE9termine la largeur du curseur lorsque `#editor.cursorStyle#` est \xE0 `line`.","Contr\xF4le si l\u2019\xE9diteur autorise le d\xE9placement de s\xE9lections par glisser-d\xE9placer.","Multiplicateur de vitesse de d\xE9filement quand vous appuyez sur 'Alt'.","Contr\xF4le si l'\xE9diteur a le pliage de code activ\xE9.","Utilisez une strat\xE9gie de pliage propre \xE0 la langue, si disponible, sinon utilisez la strat\xE9gie bas\xE9e sur le retrait.","Utilisez la strat\xE9gie de pliage bas\xE9e sur le retrait.","Contr\xF4le la strat\xE9gie de calcul des plages de pliage.","Contr\xF4le si l'\xE9diteur doit mettre en \xE9vidence les plages pli\xE9es.","Contr\xF4le si le fait de cliquer sur le contenu vide apr\xE8s une ligne pli\xE9e d\xE9plie la ligne.","Contr\xF4le la famille de polices.","D\xE9termine si l\u2019\xE9diteur doit automatiquement mettre en forme le contenu coll\xE9. Un formateur doit \xEAtre disponible et \xEAtre capable de mettre en forme une plage dans un document.","Contr\xF4le si l\u2019\xE9diteur doit mettre automatiquement en forme la ligne apr\xE8s la saisie.","Contr\xF4le si l'\xE9diteur doit afficher la marge de glyphes verticale. La marge de glyphes sert principalement au d\xE9bogage.","Contr\xF4le si le curseur doit \xEAtre masqu\xE9 dans la r\xE8gle de la vue d\u2019ensemble.","Contr\xF4le si l\u2019\xE9diteur doit mettre en surbrillance le guide de mise en retrait actif.","Contr\xF4le l'espacement des lettres en pixels.","Contr\xF4le si la modification li\xE9e est activ\xE9e dans l'\xE9diteur. En fonction du langage, les symboles associ\xE9s, par exemple les balises HTML, sont mis \xE0 jour durant le processus de modification.","Contr\xF4le si l\u2019\xE9diteur doit d\xE9tecter les liens et les rendre cliquables.","Mettez en surbrillance les crochets correspondants.","Un multiplicateur \xE0 utiliser sur les `deltaX` et `deltaY` des \xE9v\xE9nements de d\xE9filement de roulette de souris.","Faire un zoom sur la police de l'\xE9diteur quand l'utilisateur fait tourner la roulette de la souris tout en maintenant la touche 'Ctrl' enfonc\xE9e.","Fusionnez plusieurs curseurs quand ils se chevauchent.","Mappe vers 'Contr\xF4le' dans Windows et Linux, et vers 'Commande' dans macOS.","Mappe vers 'Alt' dans Windows et Linux, et vers 'Option' dans macOS.","Le modificateur \xE0 utiliser pour ajouter plusieurs curseurs avec la souris. Les gestes de souris Atteindre la d\xE9finition et Ouvrir le lien s'adapteront tels qu\u2019ils n\u2019entrent pas en conflit avec le modificateur multicursor. [Lire la suite] (https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).","Chaque curseur colle une seule ligne de texte.","Chaque curseur colle le texte en entier.","Contr\xF4le le collage quand le nombre de lignes du texte coll\xE9 correspond au nombre de curseurs.","Contr\xF4le si l'\xE9diteur doit mettre en surbrillance les occurrences de symboles s\xE9mantiques.","Contr\xF4le si une bordure doit \xEAtre dessin\xE9e autour de la r\xE8gle de la vue d'ensemble.","Focus sur l'arborescence \xE0 l'ouverture de l'aper\xE7u","Placer le focus sur l'\xE9diteur \xE0 l'ouverture de l'aper\xE7u","Contr\xF4le s'il faut mettre le focus sur l'\xE9diteur inline ou sur l'arborescence dans le widget d'aper\xE7u.","Contr\xF4le si le geste de souris Acc\xE9der \xE0 la d\xE9finition ouvre toujours le widget d'aper\xE7u.","Contr\xF4le le d\xE9lai en millisecondes apr\xE8s lequel des suggestions rapides sont affich\xE9es.","Contr\xF4le si l'\xE9diteur renomme automatiquement selon le type.","D\xE9pr\xE9ci\xE9. Utilisez 'editor.linkedEditing' \xE0 la place.","Contr\xF4le si l\u2019\xE9diteur doit afficher les caract\xE8res de contr\xF4le.","Contr\xF4le si l\u2019\xE9diteur doit afficher les guides de mise en retrait.","Affichez le dernier num\xE9ro de ligne quand le fichier se termine par un saut de ligne.","Met en surbrillance la goutti\xE8re et la ligne actuelle.","Contr\xF4le la fa\xE7on dont l\u2019\xE9diteur doit afficher la mise en surbrillance de la ligne actuelle.","Contr\xF4le si l'\xE9diteur doit afficher la mise en surbrillance de la ligne actuelle seulement quand l'\xE9diteur a le focus","Affiche les espaces blancs \xE0 l'exception des espaces uniques entre les mots.","Afficher les espaces blancs uniquement sur le texte s\xE9lectionn\xE9.","Afficher uniquement les caract\xE8res correspondant aux espaces blancs de fin","Contr\xF4le la fa\xE7on dont l\u2019\xE9diteur doit restituer les caract\xE8res espaces.","Contr\xF4le si les s\xE9lections doivent avoir des angles arrondis.","Contr\xF4le le nombre de caract\xE8res suppl\xE9mentaires, au-del\xE0 duquel l\u2019\xE9diteur d\xE9file horizontalement.","Contr\xF4le si l\u2019\xE9diteur d\xE9file au-del\xE0 de la derni\xE8re ligne.","Faites d\xE9filer uniquement le long de l'axe pr\xE9dominant quand le d\xE9filement est \xE0 la fois vertical et horizontal. Emp\xEAche la d\xE9rive horizontale en cas de d\xE9filement vertical sur un pav\xE9 tactile.","Contr\xF4le si le presse-papiers principal Linux doit \xEAtre pris en charge.","Contr\xF4le si l'\xE9diteur doit mettre en surbrillance les correspondances similaires \xE0 la s\xE9lection.","Affichez toujours les contr\xF4les de pliage.","Affichez uniquement les contr\xF4les de pliage quand la souris est au-dessus de la reliure.","Contr\xF4le quand afficher les contr\xF4les de pliage sur la reliure.","Contr\xF4le la disparition du code inutile.","Contr\xF4le les variables d\xE9pr\xE9ci\xE9es barr\xE9es.","Afficher des suggestions d\u2019extraits au-dessus d\u2019autres suggestions.","Afficher des suggestions d\u2019extraits en-dessous d\u2019autres suggestions.","Afficher des suggestions d\u2019extraits avec d\u2019autres suggestions.","Ne pas afficher de suggestions d\u2019extrait de code.","Contr\xF4le si les extraits de code s'affichent en m\xEAme temps que d'autres suggestions, ainsi que leur mode de tri.","Contr\xF4le si l'\xE9diteur d\xE9file en utilisant une animation.","Taille de la police pour le widget de suggestion. Lorsque la valeur est \xE0 `0`, la valeur de `#editor.fontSize` est utilis\xE9e.","Hauteur de ligne du widget de suggestion. Quand la valeur est '0', la valeur de '#editor.lineHeight#' est utilis\xE9e. La valeur minimale est\xA08.","Contr\xF4le si les suggestions devraient automatiquement s\u2019afficher lorsque vous tapez les caract\xE8res de d\xE9clencheur.","S\xE9lectionnez toujours la premi\xE8re suggestion.","S\xE9lectionnez les suggestions r\xE9centes sauf si une entr\xE9e ult\xE9rieure en a s\xE9lectionn\xE9 une, par ex., 'console.| -> console.log', car 'log' a \xE9t\xE9 effectu\xE9 r\xE9cemment.","S\xE9lectionnez des suggestions en fonction des pr\xE9fixes pr\xE9c\xE9dents qui ont compl\xE9t\xE9 ces suggestions, par ex., 'co -> console' et 'con -> const'.","Contr\xF4le comment les suggestions sont pr\xE9-s\xE9lectionn\xE9s lors de l\u2019affichage de la liste de suggestion.","La compl\xE9tion par tabulation ins\xE9rera la meilleure suggestion lorsque vous appuyez sur tab.","D\xE9sactiver les compl\xE9tions par tabulation.","Compl\xE9ter les extraits de code par tabulation lorsque leur pr\xE9fixe correspond. Fonctionne mieux quand les 'quickSuggestions' ne sont pas activ\xE9es.","Active les compl\xE9tions par tabulation","Les marques de fin de ligne inhabituelles sont automatiquement supprim\xE9es.","Les marques de fin de ligne inhabituelles sont ignor\xE9es.","Les marques de fin de ligne inhabituelles demandent \xE0 \xEAtre supprim\xE9es.","Supprimez les marques de fin de ligne inhabituelles susceptibles de causer des probl\xE8mes.","L'insertion et la suppression des espaces blancs suit les taquets de tabulation.","Caract\xE8res utilis\xE9s comme s\xE9parateurs de mots durant la navigation ou les op\xE9rations bas\xE9es sur les mots","Le retour automatique \xE0 la ligne n'est jamais effectu\xE9.","Le retour automatique \xE0 la ligne s'effectue en fonction de la largeur de la fen\xEAtre d'affichage.","Les lignes seront termin\xE9es \xE0 `#editor.wordWrapColumn#`.","Les lignes seront termin\xE9es au minimum du viewport et `#editor.wordWrapColumn#`.","Contr\xF4le comment les lignes doivent \xEAtre limit\xE9es.","Contr\xF4le la colonne de terminaison de l\u2019\xE9diteur lorsque `#editor.wordWrap#` est \xE0 `wordWrapColumn` ou `bounded`.","Aucune mise en retrait. Les lignes envelopp\xE9es commencent \xE0 la colonne 1.","Les lignes envelopp\xE9es obtiennent la m\xEAme mise en retrait que le parent.","Les lignes justifi\xE9es obtiennent une mise en retrait +1 vers le parent.","Les lignes justifi\xE9es obtiennent une mise en retrait +2 vers le parent. ","Contr\xF4le la mise en retrait des lignes justifi\xE9es.","Suppose que tous les caract\xE8res ont la m\xEAme largeur. Il s'agit d'un algorithme rapide qui fonctionne correctement pour les polices \xE0 espacement fixe et certains scripts (comme les caract\xE8res latins) o\xF9 les glyphes ont la m\xEAme largeur.","D\xE9l\xE8gue le calcul des points de wrapping au navigateur. Il s'agit d'un algorithme lent qui peut provoquer le gel des grands fichiers, mais qui fonctionne correctement dans tous les cas.","Contr\xF4le l'algorithme qui calcule les points de wrapping."],"vs/editor/common/editorContextKeys":["Indique si le texte de l'\xE9diteur a le focus (le curseur clignote)","Indique si l'\xE9diteur ou un widget de l'\xE9diteur a le focus (par exemple, le focus se trouve sur le widget de recherche)","Indique si un \xE9diteur ou une entr\xE9e de texte mis en forme a le focus (le curseur clignote)","Indique si l'\xE9diteur est en lecture seule","Indique si le contexte est celui d'un \xE9diteur de diff\xE9rences","Indique si 'editor.columnSelection' est activ\xE9","Indique si du texte est s\xE9lectionn\xE9 dans l'\xE9diteur","Indique si l'\xE9diteur a plusieurs s\xE9lections","Indique si la touche Tab permet de d\xE9placer le focus hors de l'\xE9diteur","Indique si le pointage de l'\xE9diteur est visible","Indique si l'\xE9diteur fait partie d'un \xE9diteur plus important (par exemple Notebooks)","Identificateur de langage de l'\xE9diteur","Indique si l'\xE9diteur a un fournisseur d'\xE9l\xE9ments de compl\xE9tion","Indique si l'\xE9diteur a un fournisseur d'actions de code","Indique si l'\xE9diteur a un fournisseur d'informations CodeLens","Indique si l'\xE9diteur a un fournisseur de d\xE9finitions","Indique si l'\xE9diteur a un fournisseur de d\xE9clarations","Indique si l'\xE9diteur a un fournisseur d'impl\xE9mentation","Indique si l'\xE9diteur a un fournisseur de d\xE9finitions de type","Indique si l'\xE9diteur a un fournisseur de pointage","Indique si l'\xE9diteur a un fournisseur de mise en surbrillance pour les documents","Indique si l'\xE9diteur a un fournisseur de symboles pour les documents","Indique si l'\xE9diteur a un fournisseur de r\xE9f\xE9rence","Indique si l'\xE9diteur a un fournisseur de renommage","Indique si l'\xE9diteur a un fournisseur d'aide sur les signatures","Indique si l'\xE9diteur a un fournisseur d'indicateurs inline","Indique si l'\xE9diteur a un fournisseur de mise en forme pour les documents","Indique si l'\xE9diteur a un fournisseur de mise en forme de s\xE9lection pour les documents","Indique si l'\xE9diteur a plusieurs fournisseurs de mise en forme pour les documents","Indique si l'\xE9diteur a plusieurs fournisseurs de mise en forme de s\xE9lection pour les documents"],"vs/editor/common/model/editStack":["Frappe en cours"],"vs/editor/common/modes/modesRegistry":["Texte brut"],"vs/editor/common/standaloneStrings":["Aucune s\xE9lection","Ligne {0}, colonne {1} ({2} s\xE9lectionn\xE9)","Ligne {0}, colonne {1}","{0} s\xE9lections ({1} caract\xE8res s\xE9lectionn\xE9s)","{0} s\xE9lections","Remplacement du param\xE8tre 'accessibilitySupport' par 'on'.","Ouverture de la page de documentation sur l'accessibilit\xE9 de l'\xE9diteur.","dans un volet en lecture seule d'un \xE9diteur de diff\xE9rences.","dans un volet d'un \xE9diteur de diff\xE9rences."," dans un \xE9diteur de code en lecture seule"," dans un \xE9diteur de code","Pour configurer l'\xE9diteur de mani\xE8re \xE0 \xEAtre optimis\xE9 en cas d'utilisation d'un lecteur d'\xE9cran, appuyez sur Commande+E maintenant.","Pour configurer l'\xE9diteur de mani\xE8re \xE0 \xEAtre optimis\xE9 en cas d'utilisation d'un lecteur d'\xE9cran, appuyez sur Contr\xF4le+E maintenant.","L'\xE9diteur est configur\xE9 pour \xEAtre optimis\xE9 en cas d'utilisation avec un lecteur d'\xE9cran.","L'\xE9diteur est configur\xE9 pour ne jamais \xEAtre optimis\xE9 en cas d'utilisation avec un lecteur d'\xE9cran, ce qui n'est pas le cas pour le moment.","Appuyez sur Tab dans l'\xE9diteur pour d\xE9placer le focus vers le prochain \xE9l\xE9ment pouvant \xEAtre d\xE9sign\xE9 comme \xE9l\xE9ment actif. Activez ou d\xE9sactivez ce comportement en appuyant sur {0}.","Appuyez sur Tab dans l'\xE9diteur pour d\xE9placer le focus vers le prochain \xE9l\xE9ment pouvant \xEAtre d\xE9sign\xE9 comme \xE9l\xE9ment actif. La commande {0} ne peut pas \xEAtre d\xE9clench\xE9e par une combinaison de touches.","Appuyez sur Tab dans l'\xE9diteur pour ins\xE9rer le caract\xE8re de tabulation. Activez ou d\xE9sactivez ce comportement en appuyant sur {0}.","Appuyez sur Tab dans l'\xE9diteur pour ins\xE9rer le caract\xE8re de tabulation. La commande {0} ne peut pas \xEAtre d\xE9clench\xE9e par une combinaison de touches.","Appuyez sur Commande+H maintenant pour ouvrir une fen\xEAtre de navigateur avec plus d'informations sur l'accessibilit\xE9 de l'\xE9diteur.","Appuyez sur Contr\xF4le+H maintenant pour ouvrir une fen\xEAtre de navigateur avec plus d'informations sur l'accessibilit\xE9 de l'\xE9diteur.","Vous pouvez masquer cette info-bulle et revenir \xE0 l'\xE9diteur en appuyant sur \xC9chap ou Maj+\xC9chap.","Afficher l'aide sur l'accessibilit\xE9","D\xE9veloppeur\xA0: Inspecter les jetons","Acc\xE9der \xE0 la ligne/colonne...","Afficher tous les fournisseurs d'acc\xE8s rapide","Palette de commandes","Commandes d'affichage et d'ex\xE9cution","Acc\xE9der au symbole...","Acc\xE9der au symbole par cat\xE9gorie...","Contenu de l'\xE9diteur","Appuyez sur Alt+F1 pour voir les options d'accessibilit\xE9.","Activer/d\xE9sactiver le th\xE8me \xE0 contraste \xE9lev\xE9","{0} modifications dans {1} fichiers"],"vs/editor/common/view/editorColorRegistry":["Couleur d'arri\xE8re-plan de la mise en surbrillance de la ligne \xE0 la position du curseur.","Couleur d'arri\xE8re-plan de la bordure autour de la ligne \xE0 la position du curseur.","Couleur d'arri\xE8re-plan des plages mises en surbrillance, comme par les fonctionnalit\xE9s de recherche et Quick Open. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur d'arri\xE8re-plan de la bordure autour des plages mises en surbrillance.","Couleur d'arri\xE8re-plan du symbole mis en surbrillance, comme le symbole Atteindre la d\xE9finition ou Suivant/Pr\xE9c\xE9dent. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les d\xE9corations sous-jacentes.","Couleur d'arri\xE8re-plan de la bordure autour des symboles mis en surbrillance.","Couleur du curseur de l'\xE9diteur.","La couleur de fond du curseur de l'\xE9diteur. Permet de personnaliser la couleur d'un caract\xE8re survol\xE9 par un curseur de bloc.","Couleur des espaces blancs dans l'\xE9diteur.","Couleur des rep\xE8res de retrait de l'\xE9diteur.","Couleur des guides d'indentation de l'\xE9diteur actif","Couleur des num\xE9ros de ligne de l'\xE9diteur.","Couleur des num\xE9ros de lignes actives de l'\xE9diteur","L\u2019ID est d\xE9pr\xE9ci\xE9. Utilisez \xE0 la place 'editorLineNumber.activeForeground'.","Couleur des num\xE9ros de lignes actives de l'\xE9diteur","Couleur des r\xE8gles de l'\xE9diteur","Couleur pour les indicateurs CodeLens","Couleur d'arri\xE8re-plan pour les accolades associ\xE9es","Couleur pour le contour des accolades associ\xE9es","Couleur de la bordure de la r\xE8gle d'aper\xE7u.","Couleur d'arri\xE8re-plan de la r\xE8gle d'aper\xE7u de l'\xE9diteur. Utilis\xE9e uniquement quand la minimap est activ\xE9e et plac\xE9e sur le c\xF4t\xE9 droit de l'\xE9diteur.","Couleur de fond pour la bordure de l'\xE9diteur. La bordure contient les marges pour les symboles et les num\xE9ros de ligne.","Couleur de bordure du code source inutile (non utilis\xE9) dans l'\xE9diteur.","Opacit\xE9 du code source inutile (non utilis\xE9) dans l'\xE9diteur. Par exemple, '#000000c0' affiche le code avec une opacit\xE9 de 75\xA0%. Pour les th\xE8mes \xE0 fort contraste, utilisez la couleur de th\xE8me 'editorUnnecessaryCode.border' pour souligner le code inutile au lieu d'utiliser la transparence.","Couleur de marqueur de la r\xE8gle d'aper\xE7u pour la mise en surbrillance des plages. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur du marqueur de la r\xE8gle d'aper\xE7u pour les erreurs.","Couleur du marqueur de la r\xE8gle d'aper\xE7u pour les avertissements.","Couleur du marqueur de la r\xE8gle d'aper\xE7u pour les informations."],"vs/editor/contrib/anchorSelect/anchorSelect":["Ancre de s\xE9lection","Ancre d\xE9finie sur {0}:{1}","D\xE9finir l'ancre de s\xE9lection","Atteindre l'ancre de s\xE9lection","S\xE9lectionner de l'ancre au curseur","Annuler l'ancre de s\xE9lection"],"vs/editor/contrib/bracketMatching/bracketMatching":["Couleur du marqueur de la r\xE8gle d'aper\xE7u pour rechercher des parenth\xE8ses.","Atteindre le crochet","S\xE9lectionner jusqu'au crochet","Acc\xE9der au &&crochet"],"vs/editor/contrib/caretOperations/caretOperations":["D\xE9placer le texte s\xE9lectionn\xE9 \xE0 gauche","D\xE9placer le texte s\xE9lectionn\xE9 \xE0 droite"],"vs/editor/contrib/caretOperations/transpose":["Transposer les lettres"],"vs/editor/contrib/clipboard/clipboard":["Co&&uper","Couper","Couper","&&Copier","Copier","Copier","Co&&ller","Coller","Coller","Copier avec la coloration syntaxique"],"vs/editor/contrib/codeAction/codeActionCommands":["Type d'action de code \xE0 ex\xE9cuter.","Contr\xF4le quand les actions retourn\xE9es sont appliqu\xE9es.","Appliquez toujours la premi\xE8re action de code retourn\xE9e.","Appliquez la premi\xE8re action de code retourn\xE9e si elle est la seule.","N'appliquez pas les actions de code retourn\xE9es.","Contr\xF4le si seules les actions de code par d\xE9faut doivent \xEAtre retourn\xE9es.","Une erreur inconnue s'est produite \xE0 l'application de l'action du code","Correction rapide...","Aucune action de code disponible","Aucune action de code pr\xE9f\xE9r\xE9e n'est disponible pour '{0}'","Aucune action de code disponible pour '{0}'","Aucune action de code par d\xE9faut disponible","Aucune action de code disponible","Remanier...","Aucune refactorisation par d\xE9faut disponible pour '{0}'","Aucune refactorisation disponible pour '{0}'","Aucune refactorisation par d\xE9faut disponible","Aucune refactorisation disponible","Action de la source","Aucune action source par d\xE9faut disponible pour '{0}'","Aucune action source disponible pour '{0}'","Aucune action source par d\xE9faut disponible","Aucune action n'est disponible","Organiser les importations","Aucune action organiser les imports disponible","Tout corriger","Aucune action Tout corriger disponible","Corriger automatiquement...","Aucun correctif automatique disponible"],"vs/editor/contrib/codeAction/lightBulbWidget":["Affichez les corrections. Correction pr\xE9f\xE9r\xE9e disponible ({0})","Afficher les correctifs ({0})","Afficher les correctifs"],"vs/editor/contrib/codelens/codelensController":["Afficher les commandes Code Lens de la ligne actuelle"],"vs/editor/contrib/comment/comment":["Activer/d\xE9sactiver le commentaire de ligne","Afficher/masquer le commen&&taire de ligne","Ajouter le commentaire de ligne","Supprimer le commentaire de ligne","Activer/d\xE9sactiver le commentaire de bloc","Afficher/masquer le commentaire de &&bloc"],"vs/editor/contrib/contextmenu/contextmenu":["Afficher le menu contextuel de l'\xE9diteur"],"vs/editor/contrib/cursorUndo/cursorUndo":["Annulation du curseur","Restauration du curseur"],"vs/editor/contrib/find/findController":["Rechercher","&&Rechercher","Rechercher dans la s\xE9lection","Rechercher suivant","Rechercher suivant","Rechercher pr\xE9c\xE9dent","Rechercher pr\xE9c\xE9dent","S\xE9lection suivante","S\xE9lection pr\xE9c\xE9dente","Remplacer","&&Remplacer"],"vs/editor/contrib/find/findWidget":["Ic\xF4ne de l'option Rechercher dans la s\xE9lection dans le widget de recherche de l'\xE9diteur.","Ic\xF4ne permettant d'indiquer que le widget de recherche de l'\xE9diteur est r\xE9duit.","Ic\xF4ne permettant d'indiquer que le widget de recherche de l'\xE9diteur est d\xE9velopp\xE9.","Ic\xF4ne de l'option Remplacer dans le widget de recherche de l'\xE9diteur.","Ic\xF4ne de l'option Tout remplacer dans le widget de recherche de l'\xE9diteur.","Ic\xF4ne de l'option Rechercher pr\xE9c\xE9dent dans le widget de recherche de l'\xE9diteur.","Ic\xF4ne de l'option Rechercher suivant dans le widget de recherche de l'\xE9diteur.","Rechercher","Rechercher","Correspondance pr\xE9c\xE9dente","Prochaine correspondance","Rechercher dans la s\xE9lection","Fermer","Remplacer","Remplacer","Remplacer","Tout remplacer","Changer le mode de remplacement","Seuls les {0} premiers r\xE9sultats sont mis en \xE9vidence, mais toutes les op\xE9rations de recherche fonctionnent sur l\u2019ensemble du texte.","{0} sur {1}","Aucun r\xE9sultat","{0} trouv\xE9(s)","{0} trouv\xE9 pour '{1}'","{0} trouv\xE9 pour '{1}', sur {2}","{0} trouv\xE9 pour '{1}'","La combinaison Ctrl+Entr\xE9e permet d\xE9sormais d'ajouter un saut de ligne au lieu de tout remplacer. Vous pouvez modifier le raccourci clavier de editor.action.replaceAll pour red\xE9finir le comportement."],"vs/editor/contrib/folding/folding":["D\xE9plier","D\xE9plier de mani\xE8re r\xE9cursive","Plier","Activer/d\xE9sactiver le pliage","Plier de mani\xE8re r\xE9cursive","Replier tous les commentaires de bloc","Replier toutes les r\xE9gions","D\xE9plier toutes les r\xE9gions","Plier tout","D\xE9plier tout","Niveau de pliage {0}","Couleur d'arri\xE8re-plan des gammes pli\xE9es. La couleur ne doit pas \xEAtre opaque pour ne pas cacher les d\xE9corations sous-jacentes.","Couleur du contr\xF4le de pliage dans la marge de l'\xE9diteur."],"vs/editor/contrib/folding/foldingDecorations":["Ic\xF4ne des plages d\xE9velopp\xE9es dans la marge de glyphes de l'\xE9diteur.","Ic\xF4ne des plages r\xE9duites dans la marge de glyphes de l'\xE9diteur."],"vs/editor/contrib/fontZoom/fontZoom":["Agrandissement de l'\xE9diteur de polices de caract\xE8res","R\xE9tr\xE9cissement de l'\xE9diteur de polices de caract\xE8res","Remise \xE0 niveau du zoom de l'\xE9diteur de polices de caract\xE8res"],"vs/editor/contrib/format/format":["1\xA0modification de format effectu\xE9e \xE0 la ligne {0}","{0} modifications de format effectu\xE9es \xE0 la ligne {1}","1\xA0modification de format effectu\xE9e entre les lignes {0} et {1}","{0} modifications de format effectu\xE9es entre les lignes {1} et {2}"],"vs/editor/contrib/format/formatActions":["Mettre le document en forme","Mettre la s\xE9lection en forme"],"vs/editor/contrib/gotoError/gotoError":["Aller au probl\xE8me suivant (Erreur, Avertissement, Info)","Ic\xF4ne du prochain marqueur goto.","Aller au probl\xE8me pr\xE9c\xE9dent (Erreur, Avertissement, Info)","Ic\xF4ne du pr\xE9c\xE9dent marqueur goto.","Aller au probl\xE8me suivant dans Fichiers (Erreur, Avertissement, Info)","&&Probl\xE8me suivant","Aller au probl\xE8me pr\xE9c\xE9dent dans Fichiers (Erreur, Avertissement, Info)","&&Probl\xE8me pr\xE9c\xE9dent"],"vs/editor/contrib/gotoError/gotoErrorWidget":["Erreur","Avertissement","Info","Conseil","{0} \xE0 {1}. ","{0}\xA0probl\xE8mes sur\xA0{1}","{0}\xA0probl\xE8me(s) sur {1}","Couleur d'erreur du widget de navigation dans les marqueurs de l'\xE9diteur.","Couleur d'avertissement du widget de navigation dans les marqueurs de l'\xE9diteur.","Couleur d\u2019information du widget de navigation du marqueur de l'\xE9diteur.","Arri\xE8re-plan du widget de navigation dans les marqueurs de l'\xE9diteur."],"vs/editor/contrib/gotoSymbol/goToCommands":["Aper\xE7u","D\xE9finitions","D\xE9finition introuvable pour '{0}'","D\xE9finition introuvable","Atteindre la d\xE9finition","Atteindre la &&d\xE9finition","Ouvrir la d\xE9finition sur le c\xF4t\xE9","Faire un Peek de la D\xE9finition","D\xE9clarations","Aucune d\xE9claration pour '{0}'","Aucune d\xE9claration","Acc\xE9der \xE0 la d\xE9claration","Atteindre la &&d\xE9claration","Aucune d\xE9claration pour '{0}'","Aucune d\xE9claration","Aper\xE7u de la d\xE9claration","D\xE9finitions de type","D\xE9finition de type introuvable pour '{0}'","D\xE9finition de type introuvable","Atteindre la d\xE9finition de type","Acc\xE9der \xE0 la d\xE9finition de &&type","Aper\xE7u de la d\xE9finition du type","Impl\xE9mentations","Impl\xE9mentation introuvable pour '{0}'","Impl\xE9mentation introuvable","Atteindre les impl\xE9mentations","Atteindre les &&impl\xE9mentations","Impl\xE9mentations d'aper\xE7u","Aucune r\xE9f\xE9rence pour '{0}'","Aucune r\xE9f\xE9rence","Atteindre les r\xE9f\xE9rences","Atteindre les &&r\xE9f\xE9rences","R\xE9f\xE9rences","Aper\xE7u des r\xE9f\xE9rences","R\xE9f\xE9rences","Atteindre un symbole","Emplacements","Aucun r\xE9sultat pour \xAB\xA0{0}\xA0\xBB","R\xE9f\xE9rences"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["Cliquez pour afficher {0}\xA0d\xE9finitions."],"vs/editor/contrib/gotoSymbol/peek/referencesController":["Chargement en cours...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["{0} r\xE9f\xE9rences","{0} r\xE9f\xE9rence","R\xE9f\xE9rences"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["aper\xE7u non disponible","Aucun r\xE9sultat","R\xE9f\xE9rences"],"vs/editor/contrib/gotoSymbol/referencesModel":["symbole dans {0} sur la ligne {1}, colonne {2}","symbole dans {0} \xE0 la ligne {1}, colonne {2}, {3}","1 symbole dans {0}, chemin complet {1}","{0} symboles dans {1}, chemin complet {2}","R\xE9sultats introuvables","1\xA0symbole dans {0}","{0}\xA0symboles dans {1}","{0}\xA0symboles dans {1} fichiers"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["Symbole {0} sur {1}, {2} pour le suivant","Symbole {0} sur {1}"],"vs/editor/contrib/hover/hover":["Afficher par pointage","Afficher le pointeur de l'aper\xE7u de d\xE9finition"],"vs/editor/contrib/hover/markdownHoverParticipant":["Chargement en cours..."],"vs/editor/contrib/hover/markerHoverParticipant":["Voir le probl\xE8me","Aucune solution disponible dans l'imm\xE9diat","Recherche de correctifs rapides...","Aucune solution disponible dans l'imm\xE9diat","Correction rapide..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["Remplacer par la valeur pr\xE9c\xE9dente","Remplacer par la valeur suivante"],"vs/editor/contrib/indentation/indentation":["Convertir les retraits en espaces","Convertir les retraits en tabulations","Taille des tabulations configur\xE9e","S\xE9lectionner la taille des tabulations pour le fichier actuel","Mettre en retrait avec des tabulations","Mettre en retrait avec des espaces","D\xE9tecter la mise en retrait \xE0 partir du contenu","Remettre en retrait les lignes","R\xE9indenter les lignes s\xE9lectionn\xE9es"],"vs/editor/contrib/linesOperations/linesOperations":["Copier la ligne en haut","&&Copier la ligne en haut","Copier la ligne en bas","Co&&pier la ligne en bas","Dupliquer la s\xE9lection","&&Dupliquer la s\xE9lection","D\xE9placer la ligne vers le haut","D\xE9placer la ligne &&vers le haut","D\xE9placer la ligne vers le bas","D\xE9placer la &&ligne vers le bas","Trier les lignes dans l'ordre croissant","Trier les lignes dans l'ordre d\xE9croissant","D\xE9couper l'espace blanc de fin","Supprimer la ligne","Mettre en retrait la ligne","Ajouter un retrait n\xE9gatif \xE0 la ligne","Ins\xE9rer une ligne au-dessus","Ins\xE9rer une ligne sous","Supprimer tout ce qui est \xE0 gauche","Supprimer tout ce qui est \xE0 droite","Joindre les lignes","Transposer les caract\xE8res autour du curseur","Transformer en majuscule","Transformer en minuscule",'Appliquer la casse "1re lettre des mots en majuscule"',"Transformer en snake case"],"vs/editor/contrib/linkedEditing/linkedEditing":["D\xE9marrer la modification li\xE9e","Couleur d'arri\xE8re-plan quand l'\xE9diteur renomme automatiquement le type."],"vs/editor/contrib/links/links":["Ex\xE9cuter la commande","suivre le lien","cmd + clic","ctrl + clic","option + clic","alt + clic","Ex\xE9cuter la commande {0}","\xC9chec de l'ouverture de ce lien, car il n'est pas bien form\xE9\xA0: {0}","\xC9chec de l'ouverture de ce lien, car sa cible est manquante.","Ouvrir le lien"],"vs/editor/contrib/message/messageController":["Indique si l'\xE9diteur affiche un message inline","Impossible de modifier dans l\u2019\xE9diteur en lecture seule"],"vs/editor/contrib/multicursor/multicursor":["Ajouter un curseur au-dessus","&&Ajouter un curseur au-dessus","Ajouter un curseur en dessous","Aj&&outer un curseur en dessous","Ajouter des curseurs \xE0 la fin des lignes","Ajouter des c&&urseurs \xE0 la fin des lignes","Ajouter des curseurs en bas","Ajouter des curseurs en haut","Ajouter la s\xE9lection \xE0 la correspondance de recherche suivante","Ajouter l'occurrence suiva&&nte","Ajouter la s\xE9lection \xE0 la correspondance de recherche pr\xE9c\xE9dente","Ajouter l'occurrence p&&r\xE9c\xE9dente","D\xE9placer la derni\xE8re s\xE9lection vers la correspondance de recherche suivante","D\xE9placer la derni\xE8re s\xE9lection \xE0 la correspondance de recherche pr\xE9c\xE9dente","S\xE9lectionner toutes les occurrences des correspondances de la recherche","S\xE9lectionner toutes les &&occurrences","Modifier toutes les occurrences"],"vs/editor/contrib/parameterHints/parameterHints":["Indicateurs des param\xE8tres Trigger"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["Ic\xF4ne d'affichage du prochain conseil de param\xE8tre.","Ic\xF4ne d'affichage du pr\xE9c\xE9dent conseil de param\xE8tre.","{0}, conseil"],"vs/editor/contrib/peekView/peekView":["Fermer","Couleur d'arri\xE8re-plan de la zone de titre de l'affichage d'aper\xE7u.","Couleur du titre de l'affichage d'aper\xE7u.","Couleur des informations sur le titre de l'affichage d'aper\xE7u.","Couleur des bordures et de la fl\xE8che de l'affichage d'aper\xE7u.","Couleur d'arri\xE8re-plan de la liste des r\xE9sultats de l'affichage d'aper\xE7u.","Couleur de premier plan des noeuds de lignes dans la liste des r\xE9sultats de l'affichage d'aper\xE7u.","Couleur de premier plan des noeuds de fichiers dans la liste des r\xE9sultats de l'affichage d'aper\xE7u.","Couleur d'arri\xE8re-plan de l'entr\xE9e s\xE9lectionn\xE9e dans la liste des r\xE9sultats de l'affichage d'aper\xE7u.","Couleur de premier plan de l'entr\xE9e s\xE9lectionn\xE9e dans la liste des r\xE9sultats de l'affichage d'aper\xE7u.","Couleur d'arri\xE8re-plan de l'\xE9diteur d'affichage d'aper\xE7u.","Couleur d'arri\xE8re-plan de la bordure de l'\xE9diteur d'affichage d'aper\xE7u.","Couleur de mise en surbrillance d'une correspondance dans la liste des r\xE9sultats de l'affichage d'aper\xE7u.","Couleur de mise en surbrillance d'une correspondance dans l'\xE9diteur de l'affichage d'aper\xE7u.","Bordure de mise en surbrillance d'une correspondance dans l'\xE9diteur de l'affichage d'aper\xE7u."],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["Ouvrez d'abord un \xE9diteur de texte pour acc\xE9der \xE0 une ligne.","Allez \xE0 la ligne {0}, colonne {1}.","Acc\xE9dez \xE0 la ligne {0}.","Ligne actuelle\xA0: {0}, caract\xE8re\xA0: {1}. Tapez un num\xE9ro de ligne entre\xA01 et\xA0{2} auquel acc\xE9der.","Ligne actuelle\xA0: {0}, caract\xE8re\xA0: {1}. Tapez un num\xE9ro de ligne auquel acc\xE9der."],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["Pour acc\xE9der \xE0 un symbole, ouvrez d'abord un \xE9diteur de texte avec des informations de symbole.","L'\xE9diteur de texte actif ne fournit pas les informations de symbole.","Aucun symbole d'\xE9diteur correspondant","Aucun symbole d'\xE9diteur","Ouvrir sur le c\xF4t\xE9","Ouvrir en bas","symboles ({0})","propri\xE9t\xE9s ({0})","m\xE9thodes ({0})","fonctions ({0})","constructeurs ({0})","variables ({0})","classes ({0})","structs ({0})","\xE9v\xE9nements ({0})","op\xE9rateurs ({0})","interfaces ({0})","espaces de noms ({0})","packages ({0})","param\xE8tres de type ({0})","modules ({0})","propri\xE9t\xE9s ({0})","\xE9num\xE9rations ({0})","membres d'\xE9num\xE9ration ({0})","cha\xEEnes ({0})","fichiers ({0})","tableaux ({0})","nombres ({0})","bool\xE9ens ({0})","objets ({0})","cl\xE9s ({0})","champs ({0})","constantes ({0})"],"vs/editor/contrib/rename/rename":["Aucun r\xE9sultat.","Une erreur inconnue s'est produite lors de la r\xE9solution de l'emplacement de renommage","Renommage de '{0}'","Changement du nom de {0}","'{0}' renomm\xE9 en '{1}'. R\xE9capitulatif : {2}","Le renommage n'a pas pu appliquer les modifications","Le renommage n'a pas pu calculer les modifications","Renommer le symbole","Activer/d\xE9sactiver la possibilit\xE9 d'afficher un aper\xE7u des changements avant le renommage"],"vs/editor/contrib/rename/renameInputField":["Renommez l'entr\xE9e. Tapez le nouveau nom et appuyez sur Entr\xE9e pour valider.","{0} pour renommer, {1} pour afficher un aper\xE7u"],"vs/editor/contrib/smartSelect/smartSelect":["\xC9tendre la s\xE9lection","D\xE9v&&elopper la s\xE9lection","R\xE9duire la s\xE9lection","&&R\xE9duire la s\xE9lection"],"vs/editor/contrib/snippet/snippetVariables":["Dimanche","Lundi","Mardi","Mercredi","Jeudi","Vendredi","Samedi","Dim","Lun","Mar","Mer","Jeu","Ven","Sam","Janvier","F\xE9vrier","Mars","Avril","Mai","Juin","Juillet","Ao\xFBt","Septembre","Octobre","Novembre","D\xE9cembre","Jan","F\xE9v","Mar","Avr","Mai","Juin","Jul","Ao\xFB","Sept","Oct","Nov","D\xE9c"],"vs/editor/contrib/suggest/suggestController":["L'acceptation de '{0}' a entra\xEEn\xE9 {1}\xA0modifications suppl\xE9mentaires","Suggestions pour Trigger","Ins\xE9rer","Ins\xE9rer","Remplacer","Remplacer","Ins\xE9rer","afficher moins","afficher plus","R\xE9initialiser la taille du widget de suggestion"],"vs/editor/contrib/suggest/suggestWidget":["Couleur d'arri\xE8re-plan du widget de suggestion.","Couleur de bordure du widget de suggestion.","Couleur de premier plan du widget de suggestion.","Couleur d'arri\xE8re-plan de l'entr\xE9e s\xE9lectionn\xE9e dans le widget de suggestion.","Couleur de la surbrillance des correspondances dans le widget de suggestion.","Chargement en cours...","Pas de suggestions.","{0}, documents\xA0: {1}","Sugg\xE9rer"],"vs/editor/contrib/suggest/suggestWidgetDetails":["Fermer","Chargement en cours..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["Ic\xF4ne d'affichage d'informations suppl\xE9mentaires dans le widget de suggestion.","Lire la suite"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["Couleur de premier plan des symboles de tableau. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles bool\xE9ens. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de classe. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de couleur. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan pour les symboles de constante. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de constructeur. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles d'\xE9num\xE9rateur. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de membre d'\xE9num\xE9rateur. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles d'\xE9v\xE9nement. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de champ. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de fichier. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de dossier. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de fonction. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles d'interface. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de cl\xE9. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de mot cl\xE9. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de m\xE9thode. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de module. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles d'espace de noms. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles null. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de nombre. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles d'objet. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles d'op\xE9rateur. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de package. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de propri\xE9t\xE9. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de r\xE9f\xE9rence. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles d'extrait de code. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de cha\xEEne. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de struct. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de texte. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de param\xE8tre de type. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles d'unit\xE9. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion.","Couleur de premier plan des symboles de variable. Ces symboles apparaissent dans le plan, la barre de navigation et le widget de suggestion."],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["Activer/d\xE9sactiver l'utilisation de la touche Tab pour d\xE9placer le focus","Appuyer sur Tab d\xE9placera le focus vers le prochain \xE9l\xE9ment pouvant \xEAtre d\xE9sign\xE9 comme \xE9l\xE9ment actif","Appuyer sur Tab ins\xE9rera le caract\xE8re de tabulation"],"vs/editor/contrib/tokenization/tokenization":["D\xE9veloppeur\xA0: forcer la retokenisation"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["Marques de fin de ligne inhabituelles","Marques de fin de ligne inhabituelles d\xE9tect\xE9es",`Ce fichier contient un ou plusieurs caract\xE8res de fin de ligne inhabituels, par exemple le s\xE9parateur de ligne (LS) ou le s\xE9parateur de paragraphe (PS).\r +\r +Il est recommand\xE9 de les supprimer du fichier. Vous pouvez le configurer via 'editor.unusualLineTerminators'.`,"Corriger ce fichier","Ignorer le probl\xE8me pour ce fichier"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["Couleur d'arri\xE8re-plan d'un symbole pendant l'acc\xE8s en lecture, comme la lecture d'une variable. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur d'arri\xE8re-plan d'un symbole pendant l'acc\xE8s en \xE9criture, comme l'\xE9criture d'une variable. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur de bordure d'un symbole durant l'acc\xE8s en lecture, par exemple la lecture d'une variable.","Couleur de bordure d'un symbole durant l'acc\xE8s en \xE9criture, par exemple l'\xE9criture dans une variable.","Couleur de marqueur de la r\xE8gle d'aper\xE7u pour la mise en surbrillance des symboles. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur de marqueur de la r\xE8gle d'aper\xE7u pour la mise en surbrillance des symboles d'acc\xE8s en \xE9criture. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Aller \xE0 la prochaine mise en \xE9vidence de symbole","Aller \xE0 la mise en \xE9vidence de symbole pr\xE9c\xE9dente","D\xE9clencher la mise en \xE9vidence de symbole"],"vs/editor/contrib/wordOperations/wordOperations":["Supprimer le mot"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["Substitutions de configuration du langage par d\xE9faut","Configurez les param\xE8tres d'\xE9diteur \xE0 remplacer pour un langage.","Ce param\xE8tre ne prend pas en charge la configuration par langage.","Impossible d'inscrire une propri\xE9t\xE9 vide","Impossible d'inscrire '{0}'. Ceci correspond au mod\xE8le de propri\xE9t\xE9 '\\\\[.*\\\\]$' permettant de d\xE9crire les param\xE8tres d'\xE9diteur sp\xE9cifiques \xE0 un langage. Utilisez la contribution 'configurationDefaults'.","Impossible d'inscrire '{0}'. Cette propri\xE9t\xE9 est d\xE9j\xE0 inscrite."],"vs/platform/contextkey/browser/contextKeyService":["Commande qui retourne des informations sur les cl\xE9s de contexte"],"vs/platform/contextkey/common/contextkeys":["Indique si le syst\xE8me d'exploitation est Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["Touche ({0}) utilis\xE9e. En attente d'une seconde touche...","La combinaison de touches ({0}, {1}) n\u2019est pas une commande."],"vs/platform/list/browser/listService":["Banc d'essai","Mappe vers 'Contr\xF4le' dans Windows et Linux, et vers 'Commande' dans macOS.","Mappe vers 'Alt' dans Windows et Linux, et vers 'Option' dans macOS.","Le modificateur \xE0 utiliser pour ajouter un \xE9l\xE9ment dans les arbres et listes pour une s\xE9lection multiple avec la souris (par exemple dans l\u2019Explorateur, les \xE9diteurs ouverts et la vue scm). Les mouvements de la souris 'Ouvrir \xE0 c\xF4t\xE9' (si pris en charge) s'adapteront tels qu\u2019ils n'entrent pas en conflit avec le modificateur multiselect.","Contr\xF4le l'ouverture des \xE9l\xE9ments dans les arborescences et les listes \xE0 l'aide de la souris (si cela est pris en charge). Notez que certaines arborescences et listes peuvent choisir d'ignorer ce param\xE8tre, s'il est non applicable.","Contr\xF4le si les listes et les arborescences prennent en charge le d\xE9filement horizontal dans le banc d'essai. Avertissement : L'activation de ce param\xE8tre a un impact sur les performances.","Contr\xF4le la mise en retrait de l'arborescence, en pixels.","Contr\xF4le si l'arborescence doit afficher les rep\xE8res de mise en retrait.","D\xE9termine si les listes et les arborescences ont un d\xE9filement fluide.","La navigation au clavier Simple place le focus sur les \xE9l\xE9ments qui correspondent \xE0 l'entr\xE9e de clavier. La mise en correspondance est effectu\xE9e sur les pr\xE9fixes uniquement.","La navigation de mise en surbrillance au clavier met en surbrillance les \xE9l\xE9ments qui correspondent \xE0 l'entr\xE9e de clavier. La navigation ult\xE9rieure vers le haut ou vers le bas parcourt uniquement les \xE9l\xE9ments mis en surbrillance.","La navigation au clavier Filtrer filtre et masque tous les \xE9l\xE9ments qui ne correspondent pas \xE0 l'entr\xE9e de clavier.","Contr\xF4le le style de navigation au clavier pour les listes et les arborescences dans le banc d'essai. Les options sont Simple, Mise en surbrillance et Filtrer.","Contr\xF4le si la navigation au clavier dans les listes et les arborescences est automatiquement d\xE9clench\xE9e simplement par la frappe. Si d\xE9fini sur 'false', la navigation au clavier est seulement d\xE9clench\xE9e avec l'ex\xE9cution de la commande 'list.toggleKeyboardNavigation', \xE0 laquelle vous pouvez attribuer un raccourci clavier.","Contr\xF4le la fa\xE7on dont les dossiers de l'arborescence sont d\xE9velopp\xE9s quand vous cliquez sur les noms de dossiers. Notez que certaines arborescences et listes peuvent choisir d'ignorer ce param\xE8tre, s'il est non applicable."],"vs/platform/markers/common/markers":["Erreur","Avertissement","Info"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","r\xE9cemment utilis\xE9es","autres commandes","La commande '{0}' a entra\xEEn\xE9 une erreur ({1})"],"vs/platform/quickinput/browser/helpQuickAccess":["commandes globales","commandes de l'\xE9diteur","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["Couleur de premier plan globale. Cette couleur est utilis\xE9e si elle n'est pas remplac\xE9e par un composant.","Couleur principale de premier plan pour les messages d'erreur. Cette couleur est utilis\xE9e uniquement si elle n'est pas red\xE9finie par un composant.","Couleur par d\xE9faut des ic\xF4nes du banc d'essai.","Couleur de bordure globale des \xE9l\xE9ments ayant le focus. Cette couleur est utilis\xE9e si elle n'est pas remplac\xE9e par un composant.","Bordure suppl\xE9mentaire autour des \xE9l\xE9ments pour les s\xE9parer des autres et obtenir un meilleur contraste.","Bordure suppl\xE9mentaire autour des \xE9l\xE9ments actifs pour les s\xE9parer des autres et obtenir un meilleur contraste.","Couleur des liens dans le texte.","Couleur d'arri\xE8re-plan des blocs de code dans le texte.","Couleur de l'ombre des widgets, comme rechercher/remplacer, au sein de l'\xE9diteur.","Arri\xE8re-plan de la zone d'entr\xE9e.","Premier plan de la zone d'entr\xE9e.","Bordure de la zone d'entr\xE9e.","Couleur de la bordure des options activ\xE9es dans les champs d'entr\xE9e.","Couleur d'arri\xE8re-plan des options activ\xE9es dans les champs d'entr\xE9e.","Couleur de premier plan des options activ\xE9es dans les champs d'entr\xE9e.","Couleur d'arri\xE8re-plan de la validation d'entr\xE9e pour la gravit\xE9 des informations.","Couleur de premier plan de validation de saisie pour la s\xE9v\xE9rit\xE9 Information.","Couleur de bordure de la validation d'entr\xE9e pour la gravit\xE9 des informations.","Couleur d'arri\xE8re-plan de la validation d'entr\xE9e pour la gravit\xE9 de l'avertissement.","Couleur de premier plan de la validation de la saisie pour la s\xE9v\xE9rit\xE9 Avertissement.","Couleur de bordure de la validation d'entr\xE9e pour la gravit\xE9 de l'avertissement.","Couleur d'arri\xE8re-plan de la validation d'entr\xE9e pour la gravit\xE9 de l'erreur.","Couleur de premier plan de la validation de saisie pour la s\xE9v\xE9rit\xE9 Erreur.","Couleur de bordure de la validation d'entr\xE9e pour la gravit\xE9 de l'erreur. ","Arri\xE8re-plan de la liste d\xE9roulante.","Premier plan de la liste d\xE9roulante.","Couleur de premier plan du bouton.","Couleur d'arri\xE8re-plan du bouton.","Couleur d'arri\xE8re-plan du bouton pendant le pointage.","Couleur de fond des badges. Les badges sont de courts libell\xE9s d'information, ex. le nombre de r\xE9sultats de recherche.","Couleur des badges. Les badges sont de courts libell\xE9s d'information, ex. le nombre de r\xE9sultats de recherche.","Ombre de la barre de d\xE9filement pour indiquer que la vue d\xE9file.","Couleur de fond du curseur de la barre de d\xE9filement.","Couleur de fond du curseur de la barre de d\xE9filement lors du survol.","Couleur d\u2019arri\xE8re-plan de la barre de d\xE9filement lorsqu'on clique dessus.","Couleur de fond pour la barre de progression qui peut s'afficher lors d'op\xE9rations longues.","Couleur d'arri\xE8re-plan du texte d'erreur dans l'\xE9diteur. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les d\xE9corations sous-jacentes.","Couleur de premier plan de la ligne ondul\xE9e marquant les erreurs dans l'\xE9diteur.","Couleur de bordure des zones d'erreur dans l'\xE9diteur.","Couleur d'arri\xE8re-plan du texte d'avertissement dans l'\xE9diteur. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les d\xE9corations sous-jacentes.","Couleur de premier plan de la ligne ondul\xE9e marquant les avertissements dans l'\xE9diteur.","Couleur de bordure des zones d'avertissement dans l'\xE9diteur.","Couleur d'arri\xE8re-plan du texte d'information dans l'\xE9diteur. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les d\xE9corations sous-jacentes.","Couleur de premier plan de la ligne ondul\xE9e marquant les informations dans l'\xE9diteur.","Couleur de bordure des zones d'informations dans l'\xE9diteur.","Couleur de premier plan de la ligne ondul\xE9e d'indication dans l'\xE9diteur.","Couleur de bordure des zones d'indication dans l'\xE9diteur.","Couleur d'arri\xE8re-plan de l'\xE9diteur.","Couleur de premier plan par d\xE9faut de l'\xE9diteur.","Couleur d'arri\xE8re-plan des gadgets de l'\xE9diteur tels que rechercher/remplacer.","Couleur de premier plan des widgets de l'\xE9diteur, notamment Rechercher/remplacer.","Couleur de bordure des widgets de l'\xE9diteur. La couleur est utilis\xE9e uniquement si le widget choisit d'avoir une bordure et si la couleur n'est pas remplac\xE9e par un widget.","Couleur de bordure de la barre de redimensionnement des widgets de l'\xE9diteur. La couleur est utilis\xE9e uniquement si le widget choisit une bordure de redimensionnement et si la couleur n'est pas remplac\xE9e par un widget.","Couleur d'arri\xE8re-plan du s\xE9lecteur rapide. Le widget de s\xE9lecteur rapide est le conteneur de s\xE9lecteurs comme la palette de commandes.","Couleur de premier plan du s\xE9lecteur rapide. Le widget de s\xE9lecteur rapide est le conteneur de s\xE9lecteurs comme la palette de commandes.","Couleur d'arri\xE8re-plan du titre du s\xE9lecteur rapide. Le widget de s\xE9lecteur rapide est le conteneur de s\xE9lecteurs comme la palette de commandes.","Couleur d'arri\xE8re-plan du s\xE9lecteur rapide pour l'\xE9l\xE9ment ayant le focus.","Couleur du s\xE9lecteur rapide pour les \xE9tiquettes de regroupement.","Couleur du s\xE9lecteur rapide pour les bordures de regroupement.","Couleur de la s\xE9lection de l'\xE9diteur.","Couleur du texte s\xE9lectionn\xE9 pour le contraste \xE9lev\xE9.","Couleur de la s\xE9lection dans un \xE9diteur inactif. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur des r\xE9gions dont le contenu est le m\xEAme que celui de la s\xE9lection. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur de bordure des r\xE9gions dont le contenu est identique \xE0 la s\xE9lection.","Couleur du r\xE9sultat de recherche actif.","Couleur des autres correspondances de recherche. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur de la plage limitant la recherche. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur de bordure du r\xE9sultat de recherche actif.","Couleur de bordure des autres r\xE9sultats de recherche.","Couleur de bordure de la plage limitant la recherche. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Surlignage sous le mot s\xE9lectionn\xE9 par pointage. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur d'arri\xE8re-plan du pointage de l'\xE9diteur.","Couleur de premier plan du pointage de l'\xE9diteur.","Couleur de bordure du pointage de l'\xE9diteur.","Couleur d'arri\xE8re-plan de la barre d'\xE9tat du pointage de l'\xE9diteur.","Couleur des liens actifs.","Couleur de premier plan des indicateurs inline","Couleur d'arri\xE8re-plan des indicateurs inline","Couleur utilis\xE9e pour l'ic\xF4ne d'ampoule sugg\xE9rant des actions.","Couleur utilis\xE9e pour l'ic\xF4ne d'ampoule sugg\xE9rant des actions de correction automatique.","Couleur d'arri\xE8re-plan du texte ins\xE9r\xE9. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur d'arri\xE8re-plan du texte supprim\xE9. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur de contour du texte ins\xE9r\xE9.","Couleur de contour du texte supprim\xE9.","Couleur de bordure entre les deux \xE9diteurs de texte.","Couleur du remplissage diagonal de l'\xE9diteur de diff\xE9rences. Le remplissage diagonal est utilis\xE9 dans les vues de diff\xE9rences c\xF4te \xE0 c\xF4te.","Couleur d'arri\xE8re-plan de la liste/l'arborescence pour l'\xE9l\xE9ment ayant le focus quand la liste/l'arborescence est active. Une liste/arborescence active peut \xEAtre s\xE9lectionn\xE9e au clavier, elle ne l'est pas quand elle est inactive.","Couleur de premier plan de la liste/l'arborescence pour l'\xE9l\xE9ment ayant le focus quand la liste/l'arborescence est active. Une liste/arborescence active peut \xEAtre s\xE9lectionn\xE9e au clavier, elle ne l'est pas quand elle est inactive.","Couleur de contour de la liste/l'arborescence pour l'\xE9l\xE9ment ayant le focus quand la liste/l'arborescence est active. Une liste/arborescence active a le focus clavier, contrairement \xE0 une liste/arborescence inactive.","Couleur d'arri\xE8re-plan de la liste/l'arborescence de l'\xE9l\xE9ment s\xE9lectionn\xE9 quand la liste/l'arborescence est active. Une liste/arborescence active peut \xEAtre s\xE9lectionn\xE9e au clavier, elle ne l'est pas quand elle est inactive.","Couleur de premier plan de la liste/l'arborescence pour l'\xE9l\xE9ment s\xE9lectionn\xE9 quand la liste/l'arborescence est active. Une liste/arborescence active peut \xEAtre s\xE9lectionn\xE9e au clavier, elle ne l'est pas quand elle est inactive.","Couleur d'arri\xE8re-plan de la liste/l'arborescence pour l'\xE9l\xE9ment s\xE9lectionn\xE9 quand la liste/l'arborescence est inactive. Une liste/arborescence active peut \xEAtre s\xE9lectionn\xE9e au clavier, elle ne l'est pas quand elle est inactive.","Couleur de premier plan de la liste/l'arborescence pour l'\xE9l\xE9ment s\xE9lectionn\xE9 quand la liste/l'arborescence est inactive. Une liste/arborescence active peut \xEAtre s\xE9lectionn\xE9e au clavier, elle ne l'est pas quand elle est inactive.","Couleur d'arri\xE8re-plan de la liste/l'arborescence pour l'\xE9l\xE9ment ayant le focus quand la liste/l'arborescence est active. Une liste/arborescence active peut \xEAtre s\xE9lectionn\xE9e au clavier (elle ne l'est pas quand elle est inactive).","Couleur de contour de la liste/l'arborescence pour l'\xE9l\xE9ment ayant le focus quand la liste/l'arborescence est inactive. Une liste/arborescence active a le focus clavier, contrairement \xE0 une liste/arborescence inactive.","Arri\xE8re-plan de la liste/l'arborescence pendant le pointage sur des \xE9l\xE9ments avec la souris.","Premier plan de la liste/l'arborescence pendant le pointage sur des \xE9l\xE9ments avec la souris.","Arri\xE8re-plan de l'op\xE9ration de glisser-d\xE9placer dans une liste/arborescence pendant le d\xE9placement d'\xE9l\xE9ments avec la souris.","Couleur de premier plan dans la liste/l'arborescence pour la surbrillance des correspondances pendant la recherche dans une liste/arborescence.","Couleur d'arri\xE8re-plan du widget de filtre de type dans les listes et les arborescences.","Couleur de contour du widget de filtre de type dans les listes et les arborescences.","Couleur de contour du widget de filtre de type dans les listes et les arborescences, en l'absence de correspondance.","Couleur de trait de l'arborescence pour les rep\xE8res de mise en retrait.","Couleur de trait de l'arborescence pour les rep\xE8res de mise en retrait.","Couleur de bordure des menus.","Couleur de premier plan des \xE9l\xE9ments de menu.","Couleur d'arri\xE8re-plan des \xE9l\xE9ments de menu.","Couleur de premier plan de l'\xE9l\xE9ment de menu s\xE9lectionn\xE9 dans les menus.","Couleur d'arri\xE8re-plan de l'\xE9l\xE9ment de menu s\xE9lectionn\xE9 dans les menus.","Couleur de bordure de l'\xE9l\xE9ment de menu s\xE9lectionn\xE9 dans les menus.","Couleur d'un \xE9l\xE9ment de menu s\xE9parateur dans les menus.","Couleur d\u2019arri\xE8re-plan de mise en surbrillance d\u2019un extrait tabstop.","Couleur de bordure de mise en surbrillance d\u2019un extrait tabstop.","Couleur d\u2019arri\xE8re-plan de mise en surbrillance du tabstop final d\u2019un extrait.","Mettez en surbrillance la couleur de bordure du dernier taquet de tabulation d'un extrait de code.","Couleur de marqueur de la r\xE8gle d'aper\xE7u pour rechercher les correspondances. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur de marqueur de la r\xE8gle d'aper\xE7u pour la mise en surbrillance des s\xE9lections. La couleur ne doit pas \xEAtre opaque pour ne pas masquer les ornements sous-jacents.","Couleur de marqueur de la minimap pour les correspondances.","Couleur de marqueur du minimap pour la s\xE9lection de l'\xE9diteur.","Couleur de marqueur de minimap pour les erreurs.","Couleur de marqueur de minimap pour les avertissements.","Couleur d'arri\xE8re-plan du minimap.","Couleur d'arri\xE8re-plan du curseur de minimap.","Couleur d'arri\xE8re-plan du curseur de minimap pendant le survol.","Couleur d'arri\xE8re-plan du curseur de minimap pendant un clic.","Couleur utilis\xE9e pour l'ic\xF4ne d'erreur des probl\xE8mes.","Couleur utilis\xE9e pour l'ic\xF4ne d'avertissement des probl\xE8mes.","Couleur utilis\xE9e pour l'ic\xF4ne d'informations des probl\xE8mes."],"vs/platform/theme/common/iconRegistry":["ID de la police \xE0 utiliser. Si aucune valeur n'est d\xE9finie, la police d\xE9finie en premier est utilis\xE9e.","Caract\xE8re de police associ\xE9 \xE0 la d\xE9finition d'ic\xF4ne.","Ic\xF4ne de l'action de fermeture dans les widgets."],"vs/platform/undoRedo/common/undoRedoService":["Les fichiers suivants ont \xE9t\xE9 ferm\xE9s et modifi\xE9s sur le disque\xA0: {0}.","Les fichiers suivants ont \xE9t\xE9 modifi\xE9s de mani\xE8re incompatible : {0}.","Impossible d'annuler '{0}' dans tous les fichiers. {1}","Impossible d'annuler '{0}' dans tous les fichiers. {1}","Impossible d'annuler '{0}' dans tous les fichiers, car des modifications ont \xE9t\xE9 apport\xE9es \xE0 {1}","Impossible d'annuler '{0}' dans tous les fichiers, car une op\xE9ration d'annulation ou de r\xE9tablissement est d\xE9j\xE0 en cours d'ex\xE9cution sur {1}","Impossible d'annuler '{0}' dans tous les fichiers, car une op\xE9ration d'annulation ou de r\xE9tablissement s'est produite dans l'intervalle","Souhaitez-vous annuler '{0}' dans tous les fichiers\xA0?","Annuler dans {0} fichiers","Annuler ce fichier","Annuler","Impossible d'annuler '{0}', car une op\xE9ration d'annulation ou de r\xE9tablissement est d\xE9j\xE0 en cours d'ex\xE9cution.","Voulez-vous annuler '{0}'\xA0?","Annuler","Annuler","Impossible de r\xE9p\xE9ter '{0}' dans tous les fichiers. {1}","Impossible de r\xE9p\xE9ter '{0}' dans tous les fichiers. {1}","Impossible de r\xE9p\xE9ter '{0}' dans tous les fichiers, car des modifications ont \xE9t\xE9 apport\xE9es \xE0 {1}","Impossible de r\xE9tablir '{0}' dans tous les fichiers, car une op\xE9ration d'annulation ou de r\xE9tablissement est d\xE9j\xE0 en cours d'ex\xE9cution pour {1}","Impossible de r\xE9tablir '{0}' dans tous les fichiers, car une op\xE9ration d'annulation ou de r\xE9tablissement s'est produite dans l'intervalle","Impossible de r\xE9tablir '{0}', car une op\xE9ration d'annulation ou de r\xE9tablissement est d\xE9j\xE0 en cours d'ex\xE9cution."]}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.it.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.it.js new file mode 100644 index 0000000..13c210f --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.it.js @@ -0,0 +1,6 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/define("vs/editor/editor.main.nls.it",{"vs/base/browser/ui/actionbar/actionViewItems":["{0} ({1})"],"vs/base/browser/ui/findinput/findInput":["input"],"vs/base/browser/ui/findinput/findInputCheckboxes":["Maiuscole/minuscole","Parola intera","Usa espressione regolare"],"vs/base/browser/ui/findinput/replaceInput":["input","Mantieni maiuscole/minuscole"],"vs/base/browser/ui/iconLabel/iconLabel":["Caricamento..."],"vs/base/browser/ui/inputbox/inputBox":["Errore: {0}","Avviso: {0}","Info: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["Non associato"],"vs/base/browser/ui/menu/menu":["{0} ({1})"],"vs/base/browser/ui/tree/abstractTree":["Cancella","Disabilita filtro sul tipo","Abilita filtro sul tipo","Non sono stati trovati elementi","Abbinamento di {0} su {1} elementi"],"vs/base/common/actions":["(vuoto)"],"vs/base/common/errorMessage":["{0}: {1}","Si \xE8 verificato un errore di sistema ({0})","Si \xE8 verificato un errore sconosciuto. Per altri dettagli, vedere il log.","Si \xE8 verificato un errore sconosciuto. Per altri dettagli, vedere il log.","{0} ({1} errori in totale)","Si \xE8 verificato un errore sconosciuto. Per altri dettagli, vedere il log."],"vs/base/common/keybindingLabels":["CTRL","MAIUSC","ALT","Windows","CTRL","MAIUSC","ALT","Super","CTRL","MAIUSC","ALT","Comando","CTRL","MAIUSC","ALT","Windows","CTRL","MAIUSC","ALT","Super"],"vs/base/parts/quickinput/browser/quickInput":["Indietro","{0}/{1}","Digitare per ridurre il numero di risultati.","{0} risultati","{0} selezionati","OK","Personalizzato","Indietro ({0})","Indietro"],"vs/base/parts/quickinput/browser/quickInputList":["Input rapido"],"vs/editor/browser/controller/coreCommands":["Si attiene alla fine anche quando si passa a righe pi\xF9 lunghe","Si attiene alla fine anche quando si passa a righe pi\xF9 lunghe"],"vs/editor/browser/controller/textAreaHandler":["editor","L'editor non \xE8 accessibile in questo momento. Premere {0} per le opzioni."],"vs/editor/browser/core/keybindingCancellation":["Indica se l'editor esegue un'operazione annullabile, ad esempio 'Anteprima riferimenti'"],"vs/editor/browser/editorExtensions":["&&Annulla","Annulla","&&Ripeti","Ripeti","&&Seleziona tutto","Seleziona tutto"],"vs/editor/browser/widget/codeEditorWidget":["Il numero di cursori \xE8 stato limitato a {0}."],"vs/editor/browser/widget/diffEditorWidget":["Effetto di riga per gli inserimenti nell'editor diff.","Effetto di riga per le rimozioni nell'editor diff.","Non \xE8 possibile confrontare i file perch\xE9 uno \xE8 troppo grande."],"vs/editor/browser/widget/diffReview":["Icona per 'Inserisci' nella revisione diff.","Icona per 'Rimuovi' nella revisione diff.","Icona per 'Chiudi' nella revisione diff.","Chiudi","nessuna riga modificata","1 riga modificata","{0} righe modificate","Differenza {0} di {1}: riga originale {2}, {3}, riga modificata {4}, {5}","vuota","{0} riga non modificata {1}","{0} riga originale {1} riga modificata {2}","+ {0} riga modificata {1}","- {0} riga originale {1}","Vai alla differenza successiva","Vai alla differenza precedente"],"vs/editor/browser/widget/inlineDiffMargin":["Copia le righe eliminate","Copia la riga eliminata","Copia la riga eliminata ({0})","Ripristina questa modifica","Copia la riga eliminata ({0})"],"vs/editor/common/config/commonEditorConfig":["Editor","Numero di spazi a cui equivale una tabulazione. Quando `#editor.detectIndentation#` \xE8 attivo, questa impostazione viene sostituita in base al contenuto del file.","Inserisce spazi quando viene premuto TAB. Quando `#editor.detectIndentation#` \xE8 attivo, questa impostazione viene sostituita in base al contenuto del file.","Controlla se `#editor.tabSize#` e `#editor.insertSpaces#` verranno rilevati automaticamente quando un file viene aperto in base al contenuto del file.","Rimuovi gli spazi finali inseriti automaticamente.","Gestione speciale dei file di grandi dimensioni per disabilitare alcune funzionalit\xE0 che fanno un uso intensivo della memoria.","Controlla se calcolare i completamenti in base alle parole presenti nel documento.","Suggerisci parole solo dal documento attivo.","Suggerisci parole da tutti i documenti aperti della stessa lingua.","Suggerisci parole da tutti i documenti aperti.","Controlla i documenti da cui vengono calcolati i completamenti basati su parole.","L'evidenziazione semantica \xE8 abilitata per tutti i temi colore.","L'evidenziazione semantica \xE8 disabilitata per tutti i temi colore.","La configurazione dell'evidenziazione semantica \xE8 gestita tramite l'impostazione `semanticHighlighting` del tema colori corrente.","Controlla se l'evidenziazione semanticHighlighting \xE8 visualizzata per i linguaggi che la supportano.","Mantiene aperti gli editor rapidi anche quando si fa doppio clic sul contenuto o si preme 'ESC'.","Per motivi di prestazioni le righe di lunghezza superiore non verranno tokenizzate","Timeout in millisecondi dopo il quale il calcolo delle differenze viene annullato. Usare 0 per indicare nessun timeout.","Controlla se l'editor diff mostra le differenze affiancate o incorporate.","Se abilitato, l'editor differenze ignora le modifiche relative a spazi vuoti iniziali e finali.","Controlla se l'editor diff mostra gli indicatori +/- per le modifiche aggiunte/rimosse.","Controlla se l'editor visualizza CodeLens.","Il ritorno a capo automatico delle righe non viene mai applicato.","Il ritorno a capo automatico delle righe viene applicato in corrispondenza della larghezza del viewport.","Il ritorno a capo automatico delle righe viene applicato in base all'impostazione `#editor.wordWrap#`."],"vs/editor/common/config/editorOptions":["L'editor user\xE0 le API della piattaforma per rilevare quando viene collegata un'utilit\xE0 per la lettura dello schermo.","L'editor verr\xE0 definitivamente ottimizzato per l'utilizzo con un'utilit\xE0 per la lettura dello schermo. Il ritorno a capo automatico verr\xE0 disabilitato.","L'editor non verr\xE0 mai ottimizzato per l'utilizzo con un'utilit\xE0 per la lettura dello schermo.","Controlla se l'editor deve essere eseguito in una modalit\xE0 ottimizzata per le utilit\xE0 per la lettura dello schermo. Se viene attivata, il ritorno a capo automatico verr\xE0 disabilitato.","Consente di controllare se viene inserito uno spazio quando si aggiungono commenti.","Controlla se ignorare le righe vuote con le opzioni per attivare/disattivare, aggiungere o rimuovere relative ai commenti di riga.","Controlla se, quando si copia senza aver effettuato una selezione, viene copiata la riga corrente.","Controlla se il cursore deve passare direttamente alla ricerca delle corrispondenze durante la digitazione.","Controlla se inizializzare la stringa di ricerca nel Widget Trova con il testo selezionato nell'editor.","Non attivare mai automaticamente la funzione Trova nella selezione (impostazione predefinita)","Attiva sempre automaticamente la funzione Trova nella selezione","Attiva automaticamente la funzione Trova nella selezione quando sono selezionate pi\xF9 righe di contenuto.","Controlla la condizione per attivare automaticamente la funzione Trova nella selezione.","Controlla se il widget Trova deve leggere o modificare gli appunti di ricerca condivisi in macOS.","Controlla se il widget Trova deve aggiungere altre righe nella parte superiore dell'editor. Quando \xE8 true, \xE8 possibile scorrere oltre la prima riga quando il widget Trova \xE8 visibile.","Controlla se la ricerca viene riavviata automaticamente dall'inizio o dalla fine quando non \xE8 possibile trovare ulteriori corrispondenze.","Abilita/Disabilita i caratteri legatura (funzionalit\xE0 dei tipi di carattere 'calt' e 'liga'). Impostare su una stringa per un controllo pi\xF9 specifico sulla propriet\xE0 CSS 'font-feature-settings'.","Propriet\xE0 CSS 'font-feature-settings' esplicita. Se \xE8 necessario solo attivare/disattivare le legature, \xE8 possibile passare un valore booleano.","Consente di configurare i caratteri legatura o le funzionalit\xE0 dei tipi di carattere. Pu\xF2 essere un valore booleano per abilitare/disabilitare le legature o una stringa per il valore della propriet\xE0 CSS 'font-feature-settings'.","Controlla le dimensioni del carattere in pixel.",'Sono consentiti solo le parole chiave "normal" e "bold" o i numeri compresi tra 1 e 1000.','Controlla lo spessore del carattere. Accetta le parole chiave "normal" e "bold" o i numeri compresi tra 1 e 1000.',"Mostra la visualizzazione rapida dei risultati (impostazione predefinita)","Passa al risultato principale e mostra una visualizzazione rapida","Passa al risultato principale e abilita l'esplorazione senza anteprima per gli altri","Questa impostazione \xE8 deprecata. In alternativa, usare impostazioni diverse, come 'editor.editor.gotoLocation.multipleDefinitions' o 'editor.editor.gotoLocation.multipleImplementations'.","Controlla il comportamento del comando 'Vai alla definizione' quando esistono pi\xF9 posizioni di destinazione.","Controlla il comportamento del comando 'Vai alla definizione di tipo' quando esistono pi\xF9 posizioni di destinazione.","Controlla il comportamento del comando 'Vai a dichiarazione' quando esistono pi\xF9 posizioni di destinazione.","Controlla il comportamento del comando 'Vai a implementazioni' quando esistono pi\xF9 posizioni di destinazione.","Controlla il comportamento del comando 'Vai a riferimenti' quando esistono pi\xF9 posizioni di destinazione.","ID comando alternativo eseguito quando il risultato di 'Vai alla definizione' \xE8 la posizione corrente.","ID comando alternativo eseguito quando il risultato di 'Vai alla definizione di tipo' \xE8 la posizione corrente.","ID comando alternativo eseguito quando il risultato di 'Vai a dichiarazione' \xE8 la posizione corrente.","ID comando alternativo eseguito quando il risultato di 'Vai a implementazione' \xE8 la posizione corrente.","ID comando alternativo eseguito quando il risultato di 'Vai a riferimento' \xE8 la posizione corrente.","Controlla se mostrare l'area sensibile al passaggio del mouse.","Controlla il ritardo in millisecondi dopo il quale viene mostrato il passaggio del mouse.","Controlla se l'area sensibile al passaggio del mouse deve rimanere visibile quando vi si passa sopra con il puntatore del mouse.","Abilita la lampadina delle azioni codice nell'editor.","Abilita i suggerimenti inline nell'editor.","Controlla le dimensioni del carattere dei suggerimenti inline nell'editor. Quando \xE8 impostata su `0`, viene usato il 90% del valore di `#editor.fontSize#`.","Controlla la famiglia di caratteri dei suggerimenti inline nell'editor.","Controlla l'altezza della riga. Usare 0 per calcolare l'altezza della riga dalle dimensioni del carattere.","Controlla se la minimappa \xE8 visualizzata.","La minimappa ha le stesse dimensioni del contenuto dell'editor (e potrebbe supportare lo scorrimento).","Se necessario, la minimappa si ridurr\xE0 o si ingrandir\xE0 in modo da adattarsi all'altezza dell'editor (nessuno scorrimento).","Se necessario, la minimappa si ridurr\xE0 in modo che la larghezza non superi mai quella dell'editor (nessuno scorrimento).","Controlla le dimensioni della minimappa.","Definisce il lato in cui eseguire il rendering della minimappa.","Controlla se il dispositivo di scorrimento della minimappa \xE8 visualizzato.","Scala del contenuto disegnato nella minimappa: 1, 2 o 3.","Esegue il rendering dei caratteri effettivi di una riga in contrapposizione ai blocchi colore.","Limita la larghezza della minimappa in modo da eseguire il rendering al massimo di un certo numero di colonne.","Controlla la quantit\xE0 di spazio tra il bordo superiore dell'editor e la prima riga.","Controlla la quantit\xE0 di spazio tra il bordo inferiore dell'editor e l'ultima riga.","Abilita un popup che mostra documentazione sui parametri e informazioni sui tipi mentre si digita.","Controlla se il menu dei suggerimenti per i parametri esegue un ciclo o si chiude quando viene raggiunta la fine dell'elenco.","Abilita i suggerimenti rapidi all'interno di stringhe.","Abilita i suggerimenti rapidi all'interno di commenti.","Abilita i suggerimenti rapidi all'esterno di stringhe e commenti.","Controlla se visualizzare automaticamente i suggerimenti durante la digitazione.","I numeri di riga non vengono visualizzati.","I numeri di riga vengono visualizzati come numeri assoluti.","I numeri di riga vengono visualizzati come distanza in linee alla posizione del cursore.","I numeri di riga vengono visualizzati ogni 10 righe.","Controlla la visualizzazione dei numeri di riga.","Numero di caratteri a spaziatura fissa in corrispondenza del quale verr\xE0 eseguito il rendering di questo righello dell'editor.","Colore di questo righello dell'editor.","Esegue il rendering dei righelli verticali dopo un certo numero di caratteri a spaziatura fissa. Usare pi\xF9 valori per pi\xF9 righelli. Se la matrice \xE8 vuota, non viene disegnato alcun righello.","Inserisce il suggerimento senza sovrascrivere il testo a destra del cursore.","Inserisce il suggerimento e sovrascrive il testo a destra del cursore.","Controlla se le parole vengono sovrascritte quando si accettano i completamenti. Tenere presente che questa opzione dipende dalle estensioni che accettano esplicitamente questa funzionalit\xE0.","Controlla se i suggerimenti di filtro e ordinamento valgono per piccoli errori di battitura.","Controlla se l'ordinamento privilegia le parole che appaiono pi\xF9 vicine al cursore.","Controlla se condividere le selezioni dei suggerimenti memorizzati tra aree di lavoro e finestre (richiede `#editor.suggestSelection#`).","Controlla se un frammento attivo impedisce i suggerimenti rapidi.","Controlla se mostrare o nascondere le icone nei suggerimenti.","Controlla la visibilit\xE0 della barra di stato nella parte inferiore del widget dei suggerimenti.","Controlla se i dettagli del suggerimento vengono visualizzati inline con l'etichetta o solo nel widget dei dettagli","Questa impostazione \xE8 deprecata. Il widget dei suggerimenti pu\xF2 ora essere ridimensionato.","Questa impostazione \xE8 deprecata. In alternativa, usare impostazioni diverse, come 'editor.suggest.showKeywords' o 'editor.suggest.showSnippets'.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `method`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `function`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `constructor`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `field`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `variable`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `class`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `struct`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `interface`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `module`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `property`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `event`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `operator`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `unit`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `value`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `constant`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `enum`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `enumMember`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `keyword`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `text`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `color`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `file`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `reference`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `customcolor`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `folder`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `typeParameter`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `snippet`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `user`.","Se \xE8 abilitata, IntelliSense mostra i suggerimenti relativi a `issues`.","Indica se gli spazi vuoti iniziali e finali devono essere sempre selezionati.","Controlla se accettare i suggerimenti con i caratteri di commit. Ad esempio, in JavaScript il punto e virgola (';') pu\xF2 essere un carattere di commit che accetta un suggerimento e digita tale carattere.","Accetta un suggerimento con 'Invio' solo quando si apporta una modifica al testo.","Controlla se i suggerimenti devono essere accettati con 'INVIO' in aggiunta a 'TAB'. In questo modo \xE8 possibile evitare ambiguit\xE0 tra l'inserimento di nuove righe e l'accettazione di suggerimenti.","Controlla il numero di righe nell'editor che possono essere lette da un utilit\xE0 per la lettura dello schermo. Avviso: questa opzione pu\xF2 influire sulle prestazioni se il numero di righe \xE8 superiore a quello predefinito.","Contenuto editor","Usa le configurazioni del linguaggio per determinare la chiusura automatica delle parentesi.","Chiudi automaticamente le parentesi solo quando il cursore si trova alla sinistra di uno spazio vuoto.","Controlla se l'editor deve chiudere automaticamente le parentesi quadre dopo che sono state aperte.","Digita sopra le virgolette o le parentesi quadre di chiusura solo se sono state inserite automaticamente.","Controlla se l'editor deve digitare su virgolette o parentesi quadre.","Usa le configurazioni del linguaggio per determinare la chiusura automatica delle virgolette.","Chiudi automaticamente le virgolette solo quando il cursore si trova alla sinistra di uno spazio vuoto.","Controlla se l'editor deve chiudere automaticamente le citazioni dopo che sono state aperte.","L'editor non inserir\xE0 automaticamente il rientro.","L'editor manterr\xE0 il rientro della riga corrente.","L'editor manterr\xE0 il rientro della riga corrente e rispetter\xE0 le parentesi definite dalla lingua.","L'editor manterr\xE0 il rientro della riga corrente, rispetter\xE0 le parentesi definite dalla lingua e richiamer\xE0 le regole onEnterRules speciali definite dalle lingue.","L'editor manterr\xE0 il rientro della riga corrente, rispetter\xE0 le parentesi definite dalla lingua, richiamer\xE0 le regole onEnterRules speciali definite dalle lingue e rispetter\xE0 le regole indentationRules definite dalle lingue.","Controlla se l'editor deve regolare automaticamente il rientro quando gli utenti digitano, incollano, spostano le righe o applicano il rientro.","Usa le configurazioni del linguaggio per determinare quando racchiudere automaticamente le selezioni tra parentesi quadre o virgolette.","Racchiude la selezione tra virgolette ma non tra parentesi quadre.","Racchiude la selezione tra parentesi quadre ma non tra virgolette.","Controlla se l'editor deve racchiudere automaticamente le selezioni quando si digitano virgolette o parentesi quadre.","Emula il comportamento di selezione dei caratteri di tabulazione quando si usano gli spazi per il rientro. La selezione verr\xE0 applicata alle tabulazioni.","Controlla se l'editor visualizza CodeLens.","Controlla la famiglia di caratteri per CodeLens.","Controlla le dimensioni del carattere in pixel per CodeLens. Quando \xE8 impostata su `0`, viene usato il 90% del valore di `#editor.fontSize#`.","Controlla se l'editor deve eseguire il rendering della selezione colori e degli elementi Decorator di tipo colore inline.","Abilita l'uso di mouse e tasti per la selezione delle colonne.","Controlla se l'evidenziazione della sintassi deve essere copiata negli Appunti.","Controllo dello stile di animazione del cursore.","Controlla se l'animazione del cursore con anti-aliasing deve essere abilitata.","Controlla lo stile del cursore.","Controlla il numero minimo di righe iniziali e finali visibili che circondano il cursore. Noto come 'scrollOff' o 'scrollOffset' in altri editor.","`cursorSurroundingLines` viene applicato solo quando \xE8 attivato tramite la tastiera o l'API.","`cursorSurroundingLines` viene sempre applicato.","Controlla quando deve essere applicato `cursorSurroundingLines`.","Controlla la larghezza del cursore quando `#editor.cursorStyle#` \xE8 impostato su `line`.","Controlla se l'editor deve consentire lo spostamento di selezioni tramite trascinamento della selezione.","Moltiplicatore della velocit\xE0 di scorrimento quando si preme `Alt`.","Controlla se per l'editor \xE8 abilitata la riduzione del codice.","Usa una strategia di riduzione specifica della lingua, se disponibile; altrimenti ne usa una basata sui rientri.","Usa la strategia di riduzione basata sui rientri.","Controlla la strategia per il calcolo degli intervalli di riduzione.","Controlla se l'editor deve evidenziare gli intervalli con riduzione del codice.","Controlla se, facendo clic sul contenuto vuoto dopo una riga ridotta, la riga viene espansa.","Controlla la famiglia di caratteri.","Controlla se l'editor deve formattare automaticamente il contenuto incollato. Deve essere disponibile un formattatore che deve essere in grado di formattare un intervallo in un documento.","Controlla se l'editor deve formattare automaticamente la riga dopo la digitazione.","Controlla se l'editor deve eseguire il rendering del margine verticale del glifo. Il margine del glifo viene usato principalmente per il debug.","Controlla se il cursore deve essere nascosto nel righello delle annotazioni.","Controlla se l'editor deve evidenziare la guida con rientro attiva.","Controlla la spaziatura tra le lettere in pixel.","Controlla se la modifica collegata \xE8 abilitata per l'editor. A seconda del linguaggio, i simboli correlati, ad esempio i tag HTML, vengono aggiornati durante la modifica.","Controlla se l'editor deve individuare i collegamenti e renderli selezionabili.","Evidenzia le parentesi graffe corrispondenti.","Moltiplicatore da usare sui valori `deltaX` e `deltaY` degli eventi di scorrimento della rotellina del mouse.","Ingrandisce il carattere dell'editor quando si usa la rotellina del mouse e si tiene premuto 'CTRL'.","Unire i cursori multipli se sovrapposti.","Rappresenta il tasto 'Control' in Windows e Linux e il tasto 'Comando' in macOS.","Rappresenta il tasto 'Alt' in Windows e Linux e il tasto 'Opzione' in macOS.","Modificatore da usare per aggiungere pi\xF9 cursori con il mouse. I gesti del mouse Vai alla definizione e Apri il collegamento si adatteranno in modo da non entrare in conflitto con il modificatore di selezione multipla. [Altre informazioni](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).","Ogni cursore incolla una singola riga del testo.","Ogni cursore incolla il testo completo.","Controlla l'operazione Incolla quando il conteggio delle righe del testo incollato corrisponde al conteggio dei cursori.","Controlla se l'editor deve evidenziare le occorrenze di simboli semantici.","Controlla se deve essere disegnato un bordo intorno al righello delle annotazioni.","Sposta lo stato attivo sull'albero quando si apre l'anteprima","Sposta lo stato attivo sull'editor quando si apre l'anteprima","Controlla se spostare lo stato attivo sull'editor inline o sull'albero nel widget di anteprima.","Controlla se il movimento del mouse Vai alla definizione consente sempre di aprire il widget di anteprima.","Controlla il ritardo in millisecondi dopo il quale verranno visualizzati i suggerimenti rapidi.","Controlla se l'editor viene rinominato automaticamente in base al tipo.","Deprecata. In alternativa, usare `editor.linkedEditing`.","Controlla se l'editor deve eseguire il rendering dei caratteri di controllo.","Controlla se l'editor deve eseguire il rendering delle guide con rientro.","Esegue il rendering dell'ultimo numero di riga quando il file termina con un carattere di nuova riga.","Mette in evidenza sia la barra di navigazione sia la riga corrente.","Controlla in che modo l'editor deve eseguire il rendering dell'evidenziazione di riga corrente.","Controlla se l'editor deve eseguire il rendering dell'evidenziazione della riga corrente solo quando l'editor ha lo stato attivo","Esegue il rendering dei caratteri di spazio vuoto ad eccezione dei singoli spazi tra le parole.","Esegui il rendering dei caratteri di spazio vuoto solo nel testo selezionato.","Esegui il rendering solo dei caratteri di spazio vuoto finali","Controlla in che modo l'editor deve eseguire il rendering dei caratteri di spazio vuoto.","Controlla se le selezioni devono avere gli angoli arrotondati.","Controlla il numero di caratteri aggiuntivi oltre i quali l'editor scorrer\xE0 orizzontalmente.","Controlla se l'editor scorrer\xE0 oltre l'ultima riga.","Scorre solo lungo l'asse predominante durante lo scorrimento verticale e orizzontale simultaneo. Impedisce la deviazione orizzontale quando si scorre in verticale su un trackpad.","Controlla se gli appunti primari di Linux devono essere supportati.","Controlla se l'editor deve evidenziare gli elementi corrispondenti simili alla selezione.","Mostra sempre i comandi di riduzione.","Mostra i comandi di riduzione solo quando il mouse \xE8 posizionato sul margine della barra di scorrimento.","Controlla se i controlli di riduzione sul margine della barra di scorrimento vengono visualizzati.","Controllo dissolvenza del codice inutilizzato.","Controlla le variabili deprecate barrate.","Visualizza i suggerimenti del frammento prima degli altri suggerimenti.","Visualizza i suggerimenti del frammento dopo gli altri suggerimenti.","Visualizza i suggerimenti del frammento insieme agli altri suggerimenti.","Non mostrare i suggerimenti del frammento.","Controlla se i frammenti di codice sono visualizzati con altri suggerimenti e il modo in cui sono ordinati.","Controlla se per lo scorrimento dell'editor verr\xE0 usata un'animazione.","Dimensioni del carattere per il widget dei suggerimenti. Se impostato su `0`, viene usato il valore di `#editor.fontSize#`.","Altezza della riga per il widget dei suggerimenti. Se impostato su `0`, viene usato il valore `editor.lineHeight#`. Il valore minimo \xE8 8.","Controlla se i suggerimenti devono essere visualizzati automaticamente durante la digitazione dei caratteri trigger.","Consente di selezionare sempre il primo suggerimento.","Consente di selezionare suggerimenti recenti a meno che continuando a digitare non ne venga selezionato uno, ad esempio `console.| ->; console.log` perch\xE9 `log` \xE8 stato completato di recente.","Consente di selezionare i suggerimenti in base a prefissi precedenti che hanno completato tali suggerimenti, ad esempio `co ->; console` e `con -> const`.","Controlla la modalit\xE0 di preselezione dei suggerimenti durante la visualizzazione dell'elenco dei suggerimenti.","La funzionalit\xE0 di completamento con tasto TAB inserir\xE0 il migliore suggerimento alla pressione del tasto TAB.","Disabilita le funzionalit\xE0 di completamento con tasto TAB.","Completa i frammenti con il tasto TAB quando i rispettivi prefissi corrispondono. Funziona in modo ottimale quando 'quickSuggestions' non \xE8 abilitato.","Abilit\xE0 la funzionalit\xE0 di completamento con tasto TAB.","I caratteri di terminazione di riga insoliti vengono rimossi automaticamente.","I caratteri di terminazione di riga insoliti vengono ignorati.","Prompt per i caratteri di terminazione di riga insoliti da rimuovere.","Rimuovi caratteri di terminazione di riga insoliti che potrebbero causare problemi.","Inserimento ed eliminazione dello spazio vuoto dopo le tabulazioni.","Caratteri che verranno usati come separatori di parola quando si eseguono operazioni o spostamenti correlati a parole.","Il ritorno a capo automatico delle righe non viene mai applicato.","Il ritorno a capo automatico delle righe viene applicato in corrispondenza della larghezza del viewport.","Il ritorno a capo automatico delle righe viene applicato in corrispondenza di `#editor.wordWrapColumn#`.","Il ritorno a capo automatico delle righe viene applicato in corrispondenza della larghezza minima del viewport e di `#editor.wordWrapColumn#`.","Controlla il ritorno a capo automatico delle righe.","Controlla la colonna per il ritorno a capo automatico dell'editor quando il valore di `#editor.wordWrap#` \xE8 `wordWrapColumn` o `bounded`.","Nessun rientro. Le righe con ritorno a capo iniziano dalla colonna 1. ","Le righe con ritorno a capo hanno lo stesso rientro della riga padre.","Le righe con ritorno a capo hanno un rientro di +1 rispetto alla riga padre.","Le righe con ritorno a capo hanno un rientro di +2 rispetto alla riga padre.","Controlla il rientro delle righe con ritorno a capo.","Presuppone che la larghezza sia identica per tutti caratteri. Si tratta di un algoritmo veloce che funziona correttamente per i tipi di carattere a spaziatura fissa e determinati script (come i caratteri latini) in cui i glifi hanno larghezza identica.","Delega il calcolo dei punti di ritorno a capo al browser. Si tratta di un algoritmo lento che potrebbe causare blocchi con file di grandi dimensioni, ma funziona correttamente in tutti gli altri casi.","Controlla l'algoritmo che calcola i punti di ritorno a capo."],"vs/editor/common/editorContextKeys":["Whether the editor text has focus (cursor is blinking)","Whether the editor or an editor widget has focus (e.g. focus is in the find widget)","Whether an editor or a rich text input has focus (cursor is blinking)","Whether the editor is read only","Whether the context is a diff editor","Whether `editor.columnSelection` is enabled","Whether the editor has text selected","Whether the editor has multiple selections","Whether `Tab` will move focus out of the editor","Whether the editor hover is visible","Whether the editor is part of a larger editor (e.g. notebooks)","The language identifier of the editor","Whether the editor has a completion item provider","Whether the editor has a code actions provider","Whether the editor has a code lens provider","Whether the editor has a definition provider","Whether the editor has a declaration provider","Whether the editor has an implementation provider","Whether the editor has a type definition provider","Whether the editor has a hover provider","Whether the editor has a document highlight provider","Whether the editor has a document symbol provider","Whether the editor has a reference provider","Whether the editor has a rename provider","Whether the editor has a signature help provider","Whether the editor has an inline hints provider","Whether the editor has a document formatting provider","Whether the editor has a document selection formatting provider","Whether the editor has multiple document formatting providers","Whether the editor has multiple document selection formatting providers"],"vs/editor/common/model/editStack":["Digitazione"],"vs/editor/common/modes/modesRegistry":["Testo normale"],"vs/editor/common/standaloneStrings":["Nessuna selezione","Riga {0}, colonna {1} ({2} selezionate)","Riga {0}, colonna {1}","{0} selezioni ({1} caratteri selezionati)","{0} selezioni","Modifica dell'impostazione `accessibilitySupport` in `on`.","Apertura della pagina di documentazione sull'accessibilit\xE0 dell'editor.","in un riquadro di sola lettura di un editor diff.","in un riquadro di un editor diff."," in un editor di codice di sola lettura"," in un editor di codice","Per configurare l'editor da ottimizzare per l'utilizzo con un'utilit\xE0 per la lettura dello schermo, premere Comando+E.","Per configurare l'editor da ottimizzare per l'utilizzo con un'utilit\xE0 per la lettura dello schermo, premere CTRL+E.","L'editor \xE8 configurato per essere ottimizzato per l'utilizzo con un'utilit\xE0 per la lettura dello schermo.","L'editor \xE8 configurato per non essere ottimizzato per l'utilizzo con un'utilit\xE0 per la lettura dello schermo, che non viene usata in questo momento.","Premere TAB nell'editor corrente per spostare lo stato attivo sull'elemento con stato attivabile successivo. Per attivare/disattivare questo comportamento, premere {0}.","Premere TAB nell'editor corrente per spostare lo stato attivo sull'elemento con stato attivabile successivo. Il comando {0} non pu\xF2 essere attualmente attivato con un tasto di scelta rapida.","Premere TAB nell'editor corrente per inserire il carattere di tabulazione. Per attivare/disattivare questo comportamento, premere {0}.","Premere TAB nell'editor corrente per inserire il carattere di tabulazione. Il comando {0} non pu\xF2 essere attualmente attivato con un tasto di scelta rapida.","Premere Comando+H per aprire una finestra del browser contenente maggiori informazioni correlate all'accessibilit\xE0 dell'editor.","Premere CTRL+H per aprire una finestra del browser contenente maggiori informazioni correlate all'accessibilit\xE0 dell'editor.","Per chiudere questa descrizione comando e tornare all'editor, premere ESC o MAIUSC+ESC.","Visualizza la Guida sull'accessibilit\xE0","Sviluppatore: Controlla token","Vai a Riga/Colonna...","Mostra tutti i provider di accesso rapido","Riquadro comandi","Mostra ed esegui comandi","Vai al simbolo...","Vai al simbolo per categoria...","Contenuto editor","Premere ALT+F1 per le opzioni di accessibilit\xE0.","Attiva/disattiva tema a contrasto elevato","Effettuate {0} modifiche in {1} file"],"vs/editor/common/view/editorColorRegistry":["Colore di sfondo per l'evidenziazione della riga alla posizione del cursore.","Colore di sfondo per il bordo intorno alla riga alla posizione del cursore.","Colore di sfondo degli intervalli evidenziati, ad esempio dalle funzionalit\xE0 Quick Open e Trova. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore di sfondo del bordo intorno agli intervalli selezionati.","Colore di sfondo del simbolo evidenziato, ad esempio per passare alla definizione o al simbolo successivo/precedente. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore di sfondo del bordo intorno ai simboli selezionati.","Colore del cursore dell'editor.","Colore di sfondo del cursore editor. Permette di personalizzare il colore di un carattere quando sovrapposto da un blocco cursore.","Colore dei caratteri di spazio vuoto nell'editor.","Colore delle guide per i rientri dell'editor.","Colore delle guide di indentazione dell'editor attivo","Colore dei numeri di riga dell'editor.","Colore del numero di riga attivo dell'editor","Id \xE8 deprecato. In alternativa usare 'editorLineNumber.activeForeground'.","Colore del numero di riga attivo dell'editor","Colore dei righelli dell'editor.","Colore primo piano delle finestre di CodeLens dell'editor","Colore di sfondo delle parentesi corrispondenti","Colore delle caselle di parentesi corrispondenti","Colore del bordo del righello delle annotazioni.","Colore di sfondo del righello delle annotazioni dell'editor. Viene usato solo quando la minimappa \xE8 abilitata e posizionata sul lato destro dell'editor.","Colore di sfondo della barra di navigazione dell'editor. La barra contiene i margini di glifo e i numeri di riga.","Colore del bordo del codice sorgente non necessario (non usato) nell'editor.",`Opacit\xE0 del codice sorgente non necessario (non usato) nell'editor. Ad esempio, con "#000000c0" il rendering del codice verr\xE0 eseguito con il 75% di opacit\xE0. Per i temi a contrasto elevato, usare il colore del tema 'editorUnnecessaryCode.border' per sottolineare il codice non necessario invece di opacizzarlo.`,"Colore del marcatore del righello delle annotazioni per le evidenziazioni degli intervalli. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del marcatore del righello delle annotazioni per gli errori.","Colore del marcatore del righello delle annotazioni per gli avvisi.","Colore del marcatore del righello delle annotazioni per i messaggi di tipo informativo."],"vs/editor/contrib/anchorSelect/anchorSelect":["Ancoraggio della selezione","Ancoraggio impostato alla posizione {0}:{1}","Imposta ancoraggio della selezione","Vai ad ancoraggio della selezione","Seleziona da ancoraggio a cursore","Annulla ancoraggio della selezione"],"vs/editor/contrib/bracketMatching/bracketMatching":["Colore del marcatore del righello delle annotazioni per la corrispondenza delle parentesi.","Vai alla parentesi quadra","Seleziona fino alla parentesi","Vai alla parentesi &&quadra"],"vs/editor/contrib/caretOperations/caretOperations":["Sposta testo selezionato a sinistra","Sposta testo selezionato a destra"],"vs/editor/contrib/caretOperations/transpose":["Trasponi lettere"],"vs/editor/contrib/clipboard/clipboard":["&&Taglia","Taglia","Taglia","&&Copia","Copia","Copia","&&Incolla","Incolla","Incolla","Copia con evidenziazione sintassi"],"vs/editor/contrib/codeAction/codeActionCommands":["Tipo dell'azione codice da eseguire.","Controlla quando vengono applicate le azioni restituite.","Applica sempre la prima azione codice restituita.","Applica la prima azione codice restituita se \xE8 l'unica.","Non applicare le azioni codice restituite.","Controlla se devono essere restituite solo le azioni codice preferite.","Si \xE8 verificato un errore sconosciuto durante l'applicazione dell'azione del codice","Correzione rapida...","Azioni codice non disponibili","Non sono disponibili azioni codice preferite per '{0}'","Non sono disponibili azioni codice per '{0}'","Non sono disponibili azioni codice preferite","Azioni codice non disponibili","Effettua refactoring...","Non sono disponibili refactoring preferiti per '{0}'","Non sono disponibili refactoring per '{0}'","Non sono disponibili refactoring preferiti","Refactoring non disponibili","Azione origine...","Non sono disponibili azioni origine preferite per '{0}'","Non sono disponibili azioni origine per '{0}'","Non sono disponibili azioni origine preferite","Azioni origine non disponibili","Organizza import","Azioni di organizzazione Imports non disponibili","Correggi tutto","Non \xE8 disponibile alcuna azione Correggi tutto","Correzione automatica...","Non sono disponibili correzioni automatiche"],"vs/editor/contrib/codeAction/lightBulbWidget":["Mostra correzioni. Correzione preferita disponibile ({0})","Mostra correzioni ({0})","Mostra correzioni"],"vs/editor/contrib/codelens/codelensController":["Mostra comandi di CodeLens per la riga corrente"],"vs/editor/contrib/comment/comment":["Attiva/disattiva commento per la riga","Attiva/Disattiva commento per la &&riga","Aggiungi commento per la riga","Rimuovi commento per la riga","Attiva/Disattiva commento per il blocco","Attiva/Disattiva commento per il &&blocco"],"vs/editor/contrib/contextmenu/contextmenu":["Mostra il menu di scelta rapida editor"],"vs/editor/contrib/cursorUndo/cursorUndo":["Cursore - Annulla","Cursore - Ripeti"],"vs/editor/contrib/find/findController":["Trova","&&Trova","Trova con selezione","Trova successivo","Trova successivo","Trova precedente","Trova precedente","Trova selezione successiva","Trova selezione precedente","Sostituisci","&&Sostituisci"],"vs/editor/contrib/find/findWidget":["Icona per 'Trova nella selezione' nel widget di ricerca dell'editor.","Icona per indicare che il widget di ricerca dell'editor \xE8 compresso.","Icona per indicare che il widget di ricerca dell'editor \xE8 espanso.","Icona per 'Sostituisci' nel widget di ricerca dell'editor.","Icona per 'Sostituisci tutto' nel widget di ricerca dell'editor.","Icona per 'Trova precedente' nel widget di ricerca dell'editor.","Icona per 'Trova successivo' nel widget di ricerca dell'editor.","Trova","Trova","Corrispondenza precedente","Corrispondenza successiva","Trova nella selezione","Chiudi","Sostituisci","Sostituisci","Sostituisci","Sostituisci tutto","Attiva/Disattiva modalit\xE0 sostituzione","Solo i primi {0} risultati vengono evidenziati, ma tutte le operazioni di ricerca funzionano su tutto il testo.","{0} di {1}","Nessun risultato","{0} trovato","{0} trovati per '{1}'","{0} trovati per '{1}' alla posizione {2}","{0} trovati per '{1}'","Il tasto di scelta rapida CTRL+INVIO ora consente di inserire l'interruzione di linea invece di sostituire tutto. Per eseguire l'override di questo comportamento, \xE8 possibile modificare il tasto di scelta rapida per editor.action.replaceAll."],"vs/editor/contrib/folding/folding":["Espandi","Espandi in modo ricorsivo","Riduci","Attiva/Disattiva riduzione","Riduci in modo ricorsivo","Riduci tutti i blocchi commento","Riduci tutte le regioni","Espandi tutte le regioni","Riduci tutto","Espandi tutto","Livello riduzione {0}","Colore di sfondo degli intervalli con riduzione. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del controllo di riduzione nella barra di navigazione dell'editor."],"vs/editor/contrib/folding/foldingDecorations":["Icona per gli intervalli espansi nel margine del glifo dell'editor.","Icona per gli intervalli compressi nel margine del glifo dell'editor."],"vs/editor/contrib/fontZoom/fontZoom":["Zoom avanti tipo di carattere editor","Zoom indietro tipo di carattere editor","Reimpostazione zoom tipo di carattere editor"],"vs/editor/contrib/format/format":["\xC8 stata apportata 1 modifica di formattazione a riga {0}","Sono state apportate {0} modifiche di formattazione a riga {1}","\xC8 stata apportata 1 modifica di formattazione tra le righe {0} e {1}","Sono state apportate {0} modifiche di formattazione tra le righe {1} e {2}"],"vs/editor/contrib/format/formatActions":["Formatta documento","Formatta selezione"],"vs/editor/contrib/gotoError/gotoError":["Vai al problema successivo (Errore, Avviso, Informazioni)","Icona per il marcatore Vai a successivo.","Vai al problema precedente (Errore, Avviso, Informazioni)","Icona per il marcatore Vai a precedente.","Vai al problema successivo nei file (Errore, Avviso, Informazioni)","&&Problema successivo","Vai al problema precedente nei file (Errore, Avviso, Informazioni)","&&Problema precedente"],"vs/editor/contrib/gotoError/gotoErrorWidget":["Errore","Avviso","Info","Suggerimento","{0} a {1}. ","{0} di {1} problemi","{0} di {1} problema","Colore per gli errori del widget di spostamento tra marcatori dell'editor.","Colore per gli avvisi del widget di spostamento tra marcatori dell'editor.","Colore delle informazioni del widget di navigazione marcatori dell'editor.","Sfondo del widget di spostamento tra marcatori dell'editor."],"vs/editor/contrib/gotoSymbol/goToCommands":["Anteprima","Definizioni","Non \xE8 stata trovata alcuna definizione per '{0}'","Non \xE8 stata trovata alcuna definizione","Vai alla definizione","Vai alla &&definizione","Apri definizione lateralmente","Visualizza in anteprima la definizione","Dichiarazioni","Non \xE8 stata trovata alcuna dichiarazione per '{0}'","Dichiarazione non trovata","Vai a dichiarazione","Vai a &&dichiarazione","Non \xE8 stata trovata alcuna dichiarazione per '{0}'","Dichiarazione non trovata","Anteprima dichiarazione","Definizioni di tipo","Non sono state trovate definizioni di tipi per '{0}'","Non sono state trovate definizioni di tipi","Vai alla definizione di tipo","Vai alla &&definizione di tipo","Anteprima definizione di tipo","Implementazioni","Non sono state trovate implementazioni per '{0}'","Non sono state trovate implementazioni","Vai a implementazioni","Vai a &&Implementazioni","Visualizza implementazioni","Non sono stati trovati riferimenti per '{0}'","Non sono stati trovati riferimenti","Vai a Riferimenti","Vai a &&riferimenti","Riferimenti","Anteprima riferimenti","Riferimenti","Vai a qualsiasi simbolo","Posizioni","Nessun risultato per '{0}'","Riferimenti"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["Fare clic per visualizzare {0} definizioni."],"vs/editor/contrib/gotoSymbol/peek/referencesController":["Caricamento...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["{0} riferimenti","{0} riferimento","Riferimenti"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["anteprima non disponibile","Nessun risultato","Riferimenti"],"vs/editor/contrib/gotoSymbol/referencesModel":["simbolo in {0} alla riga {1} colonna {2}","simbolo in {0} alla riga {1} colonna {2}, {3}","1 simbolo in {0}, percorso completo {1}","{0} simboli in {1}, percorso completo {2}","Non sono stati trovati risultati","Trovato 1 simbolo in {0}","Trovati {0} simboli in {1}","Trovati {0} simboli in {1} file"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["Simbolo {0} di {1}, {2} per il successivo","Simbolo {0} di {1}"],"vs/editor/contrib/hover/hover":["Visualizza passaggio del mouse","Mostra anteprima definizione al passaggio del mouse"],"vs/editor/contrib/hover/markdownHoverParticipant":["Caricamento..."],"vs/editor/contrib/hover/markerHoverParticipant":["View Problem","Non sono disponibili correzioni rapide","Verifica disponibilit\xE0 correzioni rapide...","Non sono disponibili correzioni rapide","Correzione rapida..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["Sostituisci con il valore precedente","Sostituisci con il valore successivo"],"vs/editor/contrib/indentation/indentation":["Converti rientro in spazi","Converti rientro in tabulazioni","Dimensione tabulazione configurata","Seleziona dimensione tabulazione per il file corrente","Imposta rientro con tabulazioni","Imposta rientro con spazi","Rileva rientro dal contenuto","Imposta nuovo rientro per righe","Re-Indenta le Linee Selezionate"],"vs/editor/contrib/linesOperations/linesOperations":["Copia la riga in alto","&&Copia la riga in alto","Copia la riga in basso","Co&&pia la riga in basso","Duplica selezione","&&Duplica selezione","Sposta la riga in alto","Sposta la riga in &&alto","Sposta la riga in basso","Sposta la riga in &&basso","Ordinamento righe crescente","Ordinamento righe decrescente","Taglia spazio vuoto finale","Elimina riga","Imposta un rientro per la riga","Riduci il rientro per la riga","Inserisci la riga sopra","Inserisci la riga sotto","Elimina tutto a sinistra","Elimina tutto a destra","Unisci righe","Trasponi caratteri intorno al cursore","Converti in maiuscolo","Converti in minuscolo","Trasforma in Tutte Iniziali Maiuscole","Trasforma in snake case"],"vs/editor/contrib/linkedEditing/linkedEditing":["Avvia modifica collegata","Colore di sfondo quando l'editor viene rinominato automaticamente in base al tipo."],"vs/editor/contrib/links/links":["Esegui il comando","Visita il collegamento","CMD+clic","CTRL+clic","Opzione+clic","ALT+clic","Esegue il comando {0}","Non \xE8 stato possibile aprire questo collegamento perch\xE9 il formato non \xE8 valido: {0}","Non \xE8 stato possibile aprire questo collegamento perch\xE9 manca la destinazione.","Apri collegamento"],"vs/editor/contrib/message/messageController":["Indica se l'editor visualizza attualmente un messaggio inline","Non \xE8 possibile modificare nell'editor di sola lettura"],"vs/editor/contrib/multicursor/multicursor":["Aggiungi cursore sopra","&&Aggiungi cursore sopra","Aggiungi cursore sotto","A&&ggiungi cursore sotto","Aggiungi cursori a fine riga","Aggiungi c&&ursori a fine riga","Aggiungi cursori alla fine","Aggiungi cursori all'inizio","Aggiungi selezione a risultato ricerca successivo","Aggiungi &&occorrenza successiva","Aggiungi selezione a risultato ricerca precedente","Aggiungi occorrenza &&precedente","Sposta ultima selezione a risultato ricerca successivo","Sposta ultima selezione a risultato ricerca precedente","Seleziona tutte le occorrenze del risultato ricerca","Seleziona &&tutte le occorrenze","Cambia tutte le occorrenze"],"vs/editor/contrib/parameterHints/parameterHints":["Attiva i suggerimenti per i parametri"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["Icona per visualizzare il suggerimento del parametro successivo.","Icona per visualizzare il suggerimento del parametro precedente.","{0}, suggerimento"],"vs/editor/contrib/peekView/peekView":["Chiudi","Colore di sfondo dell'area del titolo della visualizzazione rapida.","Colore del titolo della visualizzazione rapida.","Colore delle informazioni del titolo della visualizzazione rapida.","Colore dei bordi e della freccia della visualizzazione rapida.","Colore di sfondo dell'elenco risultati della visualizzazione rapida.","Colore primo piano dei nodi riga nell'elenco risultati della visualizzazione rapida.","Colore primo piano dei nodi file nell'elenco risultati della visualizzazione rapida.","Colore di sfondo della voce selezionata nell'elenco risultati della visualizzazione rapida.","Colore primo piano della voce selezionata nell'elenco risultati della visualizzazione rapida.","Colore di sfondo dell'editor di visualizzazioni rapide.","Colore di sfondo della barra di navigazione nell'editor visualizzazione rapida.","Colore dell'evidenziazione delle corrispondenze nell'elenco risultati della visualizzazione rapida.","Colore dell'evidenziazione delle corrispondenze nell'editor di visualizzazioni rapide.","Bordo dell'evidenziazione delle corrispondenze nell'editor di visualizzazioni rapide."],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["Aprire prima un editor di testo per passare a una riga.","Passa a riga {0} e colonna {1}.","Vai alla riga {0}.","Riga corrente: {0}, carattere: {1}. Digitare un numero di riga a cui passare compreso tra 1 e {2}.","Riga corrente: {0}, Carattere: {1}. Digitare un numero di riga a cui passare."],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["Per passare a un simbolo, aprire prima un editor di testo con informazioni sui simboli.","L'editor di testo attivo non fornisce informazioni sui simboli.","Non ci sono simboli dell'editor corrispondenti","Non ci sono simboli dell'editor","Apri lateralmente","Apri in basso","simboli ({0})","propriet\xE0 ({0})","metodi ({0})","funzioni ({0})","costruttori ({0})","variabili ({0})","classi ({0})","struct ({0})","eventi ({0})","operatori ({0})","interfacce ({0})","spazi dei nomi ({0})","pacchetti ({0})","parametri di tipo ({0})","moduli ({0})","propriet\xE0 ({0})","enumerazioni ({0})","membri di enumerazione ({0})","stringhe ({0})","file ({0})","matrici ({0})","numeri ({0})","valori booleani ({0})","oggetti ({0})","chiavi ({0})","campi ({0})","costanti ({0})"],"vs/editor/contrib/rename/rename":["Nessun risultato.","Si \xE8 verificato un errore sconosciuto durante la risoluzione del percorso di ridenominazione","Ridenominazione di '{0}'","Ridenominazione di {0}","Correttamente rinominato '{0}' in '{1}'. Sommario: {2}","La ridenominazione non \xE8 riuscita ad applicare le modifiche","La ridenominazione non \xE8 riuscita a calcolare le modifiche","Rinomina simbolo","Abilita/Disabilita l'opzione per visualizzare le modifiche in anteprima prima della ridenominazione"],"vs/editor/contrib/rename/renameInputField":["Consente di rinominare l'input. Digitare il nuovo nome e premere INVIO per eseguire il commit.","{0} per rinominare, {1} per visualizzare in anteprima"],"vs/editor/contrib/smartSelect/smartSelect":["Espandi selezione","Espan&&di selezione","Riduci selezione","&&Riduci selezione"],"vs/editor/contrib/snippet/snippetVariables":["Domenica","Luned\xEC","Marted\xEC","Mercoled\xEC","Gioved\xEC","Venerd\xEC","Sabato","Dom","Lun","Mar","Mer","Gio","Ven","Sab","Gennaio","Febbraio","Marzo","Aprile","Mag","Giugno","Luglio","Agosto","Settembre","Ottobre","Novembre","Dicembre","Gen","Feb","Mar","Apr","Mag","Giu","Lug","Ago","Set","Ott","Nov","Dic"],"vs/editor/contrib/suggest/suggestController":["In seguito all'accettazione di '{0}' sono state apportate altre {1} modifiche","Attiva suggerimento","Inserisci","Inserisci","Sostituisci","Sostituisci","Inserisci","nascondi dettagli","mostra dettagli","Reimposta le dimensioni del widget dei suggerimenti"],"vs/editor/contrib/suggest/suggestWidget":["Colore di sfondo del widget dei suggerimenti.","Colore del bordo del widget dei suggerimenti.","Colore primo piano del widget dei suggerimenti.","Colore di sfondo della voce selezionata del widget dei suggerimenti.","Colore delle evidenziazioni corrispondenze nel widget dei suggerimenti.","Caricamento...","Non ci sono suggerimenti.","{0}, documenti: {1}","Suggerisci"],"vs/editor/contrib/suggest/suggestWidgetDetails":["Chiudi","Caricamento..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["Icona per visualizzare altre informazioni nel widget dei suggerimenti.","Altre informazioni"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["Colore primo piano per i simboli di matrice. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli booleani. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di classe. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di colore. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di costante. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di costruttore. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di enumeratore. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di membro di enumeratore. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di evento. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di campo. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di file. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di cartella. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di funzione. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di interfaccia. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di chiave. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di parola chiave. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di metodo. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di modulo. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di spazio dei nomi. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli Null. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli numerici. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di oggetto. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di operatore. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di pacchetto. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di propriet\xE0. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di riferimento. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di frammento. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di stringa. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di struct. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di testo. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di parametro di tipo. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di unit\xE0. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti.","Colore primo piano per i simboli di variabile. Questi simboli vengono visualizzati nella struttura, nell'elemento di navigazione e nel widget dei suggerimenti."],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["Attiva/Disattiva l'uso di TAB per spostare lo stato attivo","Se si preme TAB, lo stato attivo verr\xE0 spostato sull'elemento con stato attivabile successivo.","Se si preme TAB, verr\xE0 inserito il carattere di tabulazione"],"vs/editor/contrib/tokenization/tokenization":["Sviluppatore: Forza retokenizzazione"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["Caratteri di terminazione di riga insoliti","Sono stati rilevati caratteri di terminazione di riga insoliti","Questo file contiene uno o pi\xF9 caratteri di terminazione di riga insoliti, come separatore di riga (LS) o separatore di paragrafo (PS).\r\n\r\n\xC8 consigliabile rimuoverli dal file. \xC8 possibile configurare questa opzione tramite `editor.unusualLineTerminators`.","Correggi questo file","Ignora il problema per questo file"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["Colore di sfondo di un simbolo durante l'accesso in lettura, ad esempio durante la lettura di una variabile. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore di sfondo di un simbolo durante l'accesso in scrittura, ad esempio durante la scrittura in una variabile. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del bordo di un simbolo durante l'accesso in lettura, ad esempio durante la lettura di una variabile.","Colore del bordo di un simbolo durante l'accesso in scrittura, ad esempio durante la scrittura in una variabile.","Colore del marcatore del righello delle annotazioni per le evidenziazioni dei simboli. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del marcatore del righello delle annotazioni per le evidenziazioni dei simboli di accesso in scrittura. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Vai al prossimo simbolo evidenziato","Vai al precedente simbolo evidenziato","Attiva/disattiva evidenziazione simbolo"],"vs/editor/contrib/wordOperations/wordOperations":["Elimina parola"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["Override configurazione predefinita del linguaggio","Consente di configurare le impostazioni dell'editor di cui eseguire l'override per un linguaggio.","Questa impostazione non supporta la configurazione per lingua.","Non \xE8 possibile registrare una propriet\xE0 vuota","Non \xE8 possibile registrare '{0}'. Corrisponde al criterio di propriet\xE0 '\\\\[.*\\\\]$' per la descrizione delle impostazioni dell'editor specifiche del linguaggio. Usare il contributo 'configurationDefaults'.","Non \xE8 possibile registrare '{0}'. Questa propriet\xE0 \xE8 gi\xE0 registrata."],"vs/platform/contextkey/browser/contextKeyService":["Comando che restituisce informazioni sulle chiavi di contesto"],"vs/platform/contextkey/common/contextkeys":["Whether the operating system is Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["\xC8 stato premuto ({0}). In attesa del secondo tasto...","La combinazione di tasti ({0}, {1}) non \xE8 un comando."],"vs/platform/list/browser/listService":["Workbench","Rappresenta il tasto 'Control' in Windows e Linux e il tasto 'Comando' in macOS.","Rappresenta il tasto 'Alt' in Windows e Linux e il tasto 'Opzione' in macOS.","Il modificatore da utilizzare per aggiungere un elemento di alberi e liste ad una selezione multipla con il mouse (ad esempio in Esplora Risorse, apre gli editor e le viste scm). Le gesture del mouse 'Apri a lato' - se supportate - si adatteranno in modo da non creare conflitti con il modificatore di selezione multipla.","Controls how to open items in trees and lists using the mouse (if supported). Note that some trees and lists might choose to ignore this setting if it is not applicable.","Controlla se elenchi e alberi supportano lo scorrimento orizzontale nell'area di lavoro. Avviso: l'attivazione di questa impostazione pu\xF2 influire sulle prestazioni.","Controlla il rientro dell'albero in pixel.","Controlla se l'albero deve eseguire il rendering delle guide per i rientri.","Controlla se elenchi e alberi prevedono lo scorrimento uniforme.","Con lo stile di spostamento da tastiera simple lo stato attivo si trova sugli elementi che corrispondono all'input da tastiera. L'abbinamento viene effettuato solo in base ai prefissi.","Con lo stile di spostamento da tastiera highlight vengono evidenziati gli elementi corrispondenti all'input da tastiera. Spostandosi ulteriormente verso l'alto o verso il basso ci si sposter\xE0 solo negli elementi evidenziati.","Con lo stile di spostamento da tastiera filter verranno filtrati e nascosti tutti gli elementi che non corrispondono all'input da tastiera.","Controlla lo stile di spostamento da tastiera per elenchi e alberi nel workbench. Le opzioni sono: simple, highlight e filter.","Controlla se gli spostamenti da tastiera per elenchi e alberi vengono attivati semplicemente premendo un tasto. Se \xE8 impostato su `false`, gli spostamenti da tastiera vengono attivati solo durante l'esecuzione del comando `list.toggleKeyboardNavigation`, al quale \xE8 possibile assegnare un tasto di scelta rapida.","Controlla l'espansione delle cartelle di alberi quando si fa clic sui nomi delle cartelle. Tenere presente che alcuni alberi ed elenchi potrebbero scegliere di ignorare questa impostazione se non \xE8 applicabile."],"vs/platform/markers/common/markers":["Errore","Avviso","Info"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","usate di recente","altri comandi","Il comando '{0}' ha restituito un errore ({1})"],"vs/platform/quickinput/browser/helpQuickAccess":["comandi globali","comandi dell'editor","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["Colore primo piano generale. Questo colore viene usato solo se non \xE8 sostituito da quello di un componente.","Colore primo piano globale per i messaggi di errore. Questo colore viene usato solo se non \xE8 sostituito da quello di un componente.","Colore predefinito per le icone nel workbench.","Colore del bordo globale per gli elementi evidenziati. Questo colore viene usato solo se non \xE8 sostituito da quello di un componente.","Un bordo supplementare attorno agli elementi per contrastarli maggiormente rispetto agli altri.","Un bordo supplementare intorno agli elementi attivi per contrastarli maggiormente rispetto agli altri.","Colore primo piano dei link nel testo.","Colore di sfondo per i blocchi di codice nel testo.","Colore ombreggiatura dei widget, ad es. Trova/Sostituisci all'interno dell'editor.","Sfondo della casella di input.","Primo piano della casella di input.","Bordo della casella di input.","Colore del bordo di opzioni attivate nei campi di input.","Colore di sfondo di opzioni attivate nei campi di input.","Colore primo piano di opzioni attivate nei campi di input.","Colore di sfondo di convalida dell'input di tipo Informazione.","Colore primo piano di convalida dell'input di tipo Informazione.","Colore del bordo della convalida dell'input di tipo Informazione.","Colore di sfondo di convalida dell'input di tipo Avviso.","Colore primo piano di convalida dell'input di tipo Avviso.","Colore del bordo della convalida dell'input di tipo Avviso.","Colore di sfondo di convalida dell'input di tipo Errore.","Colore primo piano di convalida dell'input di tipo Errore.","Colore del bordo della convalida dell'input di tipo Errore.","Sfondo dell'elenco a discesa.","Primo piano dell'elenco a discesa.","Colore primo piano del pulsante.","Colore di sfondo del pulsante.","Colore di sfondo del pulsante al passaggio del mouse.","Colore di sfondo del badge. I badge sono piccole etichette informative, ad esempio per mostrare il conteggio dei risultati della ricerca.","Colore primo piano del badge. I badge sono piccole etichette informative, ad esempio per mostrare il conteggio dei risultati di una ricerca.","Ombra della barra di scorrimento per indicare lo scorrimento della visualizzazione.","Colore di sfondo del cursore della barra di scorrimento.","Colore di sfondo del cursore della barra di scorrimento al passaggio del mouse.","Colore di sfondo del cursore della barra di scorrimento quando si fa clic con il mouse.","Colore di sfondo dell'indicatore di stato che pu\xF2 essere mostrato per operazioni a esecuzione prolungata.","Colore di sfondo del testo dell'errore nell'editor. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore primo piano degli indicatori di errore nell'editor.","Colore del bordo delle caselle di errore nell'editor.","Colore di sfondo del testo dell'avviso nell'editor. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore primo piano degli indicatori di avviso nell'editor.","Colore del bordo delle caselle di avviso nell'editor.","Colore di sfondo del testo delle informazioni nell'editor. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore primo piano degli indicatori di informazioni nell'editor.","Colore del bordo delle caselle informative nell'editor.","Colore primo piano degli indicatori di suggerimento nell'editor.","Colore del bordo delle caselle dei suggerimenti nell'editor.","Colore di sfondo dell'editor.","Colore primo piano predefinito dell'editor.","Colore di sfondo dei widget dell'editor, ad esempio Trova/Sostituisci.","Colore primo piano dei widget dell'editor, ad esempio Trova/Sostituisci.","Colore del bordo dei widget dell'editor. Il colore viene usato solo se il widget sceglie di avere un bordo e se il colore non \xE8 sottoposto a override da un widget.","Colore del bordo della barra di ridimensionamento dei widget dell'editor. Il colore viene usato solo se il widget sceglie di avere un bordo di ridimensionamento e se il colore non \xE8 sostituito da quello di un widget.","Colore di sfondo di Selezione rapida. Il widget Selezione rapida \xE8 il contenitore di selezioni quali il riquadro comandi.","Colore primo piano di Selezione rapida. Il widget Selezione rapida \xE8 il contenitore di selezioni quali il riquadro comandi.","Colore di sfondo del titolo di Selezione rapida. Il widget Selezione rapida \xE8 il contenitore di selezioni quali il riquadro comandi.","Colore di sfondo di Selezione rapida per l'elemento con lo stato attivo.","Colore di selezione rapida per il raggruppamento delle etichette.","Colore di selezione rapida per il raggruppamento dei bordi.","Colore della selezione dell'editor.","Colore del testo selezionato per il contrasto elevato.","Colore della selezione in un editor inattivo. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore delle aree con lo stesso contenuto della selezione. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del bordo delle regioni con lo stesso contenuto della selezione.","Colore della corrispondenza di ricerca corrente.","Colore degli altri risultati della ricerca. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore dell'intervallo di limite della ricerca. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del bordo della corrispondenza della ricerca corrente.","Colore del bordo delle altre corrispondenze della ricerca.","Colore del bordo dell'intervallo che limita la ricerca. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Evidenziazione sotto la parola per cui \xE8 visualizzata un'area sensibile al passaggio del mouse. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore di sfondo dell'area sensibile al passaggio del mouse dell'editor.","Colore primo piano dell'area sensibile al passaggio del mouse dell'editor.","Colore del bordo dell'area sensibile al passaggio del mouse dell'editor.","Colore di sfondo della barra di stato sensibile al passaggio del mouse dell'editor.","Colore dei collegamenti attivi.","Colore primo piano dei suggerimenti inline","Colore di sfondo dei suggerimenti inline","Colore usato per l'icona delle azioni con lampadina.","Colore usato per l'icona delle azioni di correzione automatica con lampadina.","Colore di sfondo per il testo che \xE8 stato inserito. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore di sfondo per il testo che \xE8 stato rimosso. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del contorno del testo che \xE8 stato inserito.","Colore del contorno del testo che \xE8 stato rimosso.","Colore del bordo tra due editor di testo.","Colore del riempimento diagonale dell'editor diff. Il riempimento diagonale viene usato nelle visualizzazioni diff affiancate.","Colore di sfondo dell'elenco/albero per l'elemento con lo stato attivo quando l'elenco/albero \xE8 attivo. Un elenco/albero attivo ha lo stato attivo della tastiera, a differenza di uno inattivo.","Colore primo piano dell'elenco/albero per l'elemento con lo stato attivo quando l'elenco/albero \xE8 attivo. Un elenco/albero attivo ha lo stato attivo della tastiera, a differenza di uno inattivo.","Colore del contorno dell'elenco/albero per l'elemento con lo stato attivo quando l'elenco/albero \xE8 attivo. Un elenco/albero attivo ha lo stato attivo della tastiera, a differenza di uno inattivo.","Colore di sfondo dell'elenco/albero per l'elemento selezionato quando l'elenco/albero \xE8 attivo. Un elenco/albero attivo ha lo stato attivo della tastiera, a differenza di uno inattivo.","Colore primo piano dell'elenco/albero per l'elemento selezionato quando l'elenco/albero \xE8 attivo. Un elenco/albero attivo ha lo stato attivo della tastiera, a differenza di uno inattivo.","Colore di sfondo dell'elenco/albero per l'elemento selezionato quando l'elenco/albero \xE8 inattivo. Un elenco/albero attivo ha lo stato attivo della tastiera, a differenza di uno inattivo.","Colore primo piano dell'elenco/albero per l'elemento selezionato quando l'elenco/albero \xE8 inattivo. Un elenco/albero attivo ha lo stato attivo della tastiera, a differenza di uno inattivo.","Colore di sfondo dell'elenco/albero per l'elemento con lo stato attivo quando l'elenco/albero \xE8 inattivo. Un elenco/albero attivo ha lo stato attivo della tastiera, uno inattivo no.","Colore del contorno dell'elenco/albero per l'elemento con lo stato attivo quando l'elenco/albero \xE8 inattivo. Un elenco/albero attivo ha lo stato attivo della tastiera, a differenza di uno inattivo.","Sfondo dell'elenco/albero al passaggio del mouse sugli elementi.","Primo piano dell'elenco/albero al passaggio del mouse sugli elementi.","Sfondo dell'elenco/albero durante il trascinamento degli elementi selezionati.","Colore primo piano Elenco/Struttura ad albero delle occorrenze trovate durante la ricerca nell'Elenco/Struttura ad albero.","Colore di sfondo del widget del filtro per tipo in elenchi e alberi.","Colore del contorno del widget del filtro per tipo in elenchi e alberi.","Colore del contorno del widget del filtro per tipo in elenchi e alberi quando non sono presenti corrispondenze.","Colore del tratto dell'albero per le guide per i rientri.","Colore del tratto dell'albero per le guide per i rientri.","Colore del bordo del menu.","Colore primo piano delle voci di menu.","Colore di sfondo delle voci di menu.","Colore primo piano della voce di menu selezionata nei menu.","Colore di sfondo della voce di menu selezionata nei menu.","Colore del bordo della voce di menu selezionata nei menu.","Colore di un elemento separatore delle voci di menu.","Colore di sfondo dell'evidenziazione della tabulazione di un frammento.","Colore del bordo dell'evidenziazione della tabulazione di un frammento.","Colore di sfondo dell'evidenziazione della tabulazione finale di un frammento.","Colore del bordo dell'evidenziazione della tabulazione finale di un frammento.","Colore del marcatore del righello delle annotazioni per la ricerca di corrispondenze. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del marcatore del righello delle annotazioni per le evidenziazioni delle selezioni. Il colore non deve essere opaco per evitare di nascondere le decorazioni sottostanti.","Colore del marcatore della minimappa per la ricerca delle corrispondenze.","Colore del marcatore della minimappa per la selezione dell'editor.","Colore del marcatore della minimappa per gli errori.","Colore del marcatore della minimappa per gli avvisi.","Colore di sfondo della minimappa.","Colore di sfondo del dispositivo di scorrimento della minimappa.","Colore di sfondo del dispositivo di scorrimento della minimappa al passaggio del mouse.","Colore di sfondo del dispositivo di scorrimento della minimappa quando si fa clic con il mouse.","Colore usato per l'icona di errore dei problemi.","Colore usato per l'icona di avviso dei problemi.","Colore usato per l'icona informazioni dei problemi."],"vs/platform/theme/common/iconRegistry":["ID del tipo di carattere da usare. Se non \xE8 impostato, viene usato il tipo di carattere definito per primo.","Tipo di carattere associato alla definizione di icona.","Icona dell'azione di chiusura nei widget."],"vs/platform/undoRedo/common/undoRedoService":["I file seguenti sono stati chiusi e modificati nel disco: {0}.","I file seguenti sono stati modificati in modo incompatibile: {0}.","Non \xE8 stato possibile annullare '{0}' in tutti i file. {1}","Non \xE8 stato possibile annullare '{0}' in tutti i file. {1}","Non \xE8 stato possibile annullare '{0}' in tutti i file perch\xE9 sono state apportate modifiche a {1}","Non \xE8 stato possibile annullare '{0}' su tutti i file perch\xE9 \xE8 gi\xE0 in esecuzione un'operazione di annullamento o ripetizione su {1}","Non \xE8 stato possibile annullare '{0}' su tutti i file perch\xE9 nel frattempo \xE8 stata eseguita un'operazione di annullamento o ripetizione","Annullare '{0}' in tutti i file?","Annulla in {0} file","Annulla questo file","Annulla","Non \xE8 stato possibile annullare '{0}' perch\xE9 \xE8 gi\xE0 in esecuzione un'operazione di annullamento o ripetizione.","Annullare '{0}'?","Annulla","Annulla","Non \xE8 stato possibile ripetere '{0}' in tutti i file. {1}","Non \xE8 stato possibile ripetere '{0}' in tutti i file. {1}","Non \xE8 stato possibile ripetere '{0}' in tutti i file perch\xE9 sono state apportate modifiche a {1}","Non \xE8 stato possibile ripetere l'operazione '{0}' su tutti i file perch\xE9 \xE8 gi\xE0 in esecuzione un'operazione di annullamento o ripetizione sull'elenco di file {1}","Non \xE8 stato possibile ripetere '{0}' su tutti i file perch\xE9 nel frattempo \xE8 stata eseguita un'operazione di annullamento o ripetizione","Non \xE8 stato possibile ripetere '{0}' perch\xE9 \xE8 gi\xE0 in esecuzione un'operazione di annullamento o ripetizione."]}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ja.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ja.js new file mode 100644 index 0000000..009489c --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ja.js @@ -0,0 +1,8 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/define("vs/editor/editor.main.nls.ja",{"vs/base/browser/ui/actionbar/actionViewItems":["{0} ({1})"],"vs/base/browser/ui/findinput/findInput":["\u5165\u529B"],"vs/base/browser/ui/findinput/findInputCheckboxes":["\u5927\u6587\u5B57\u3068\u5C0F\u6587\u5B57\u3092\u533A\u5225\u3059\u308B","\u5358\u8A9E\u5358\u4F4D\u3067\u691C\u7D22\u3059\u308B","\u6B63\u898F\u8868\u73FE\u3092\u4F7F\u7528\u3059\u308B"],"vs/base/browser/ui/findinput/replaceInput":["\u5165\u529B","\u4FDD\u6301\u3059\u308B"],"vs/base/browser/ui/iconLabel/iconLabel":["\u8AAD\u307F\u8FBC\u307F\u4E2D..."],"vs/base/browser/ui/inputbox/inputBox":["\u30A8\u30E9\u30FC: {0}","\u8B66\u544A: {0}","\u60C5\u5831: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["\u30D0\u30A4\u30F3\u30C9\u306A\u3057"],"vs/base/browser/ui/menu/menu":["{0} ({1})"],"vs/base/browser/ui/tree/abstractTree":["\u30AF\u30EA\u30A2","\u578B\u306E\u30D5\u30A3\u30EB\u30BF\u30FC\u3092\u7121\u52B9\u306B\u3059\u308B","\u578B\u306E\u30D5\u30A3\u30EB\u30BF\u30FC\u3092\u6709\u52B9\u306B\u3059\u308B","\u8981\u7D20\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","{1} \u500B\u306E\u8981\u7D20\u306E\u3046\u3061 {0} \u500B\u306E\u8981\u7D20\u304C\u4E00\u81F4\u3057\u307E\u3057\u305F"],"vs/base/common/actions":["(\u7A7A)"],"vs/base/common/errorMessage":["{0}: {1}","\u30B7\u30B9\u30C6\u30E0 \u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F ({0})","\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u30ED\u30B0\u3067\u8A73\u7D30\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u30ED\u30B0\u3067\u8A73\u7D30\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002","{0} (\u5408\u8A08 {1} \u30A8\u30E9\u30FC)","\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F\u3002\u30ED\u30B0\u3067\u8A73\u7D30\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002"],"vs/base/common/keybindingLabels":["Ctrl","Shift","Alt","Windows","Ctrl","Shift","Alt","Super","Control","Shift","Alt","\u30B3\u30DE\u30F3\u30C9","Control","Shift","Alt","Windows","Control","Shift","Alt","Super"],"vs/base/parts/quickinput/browser/quickInput":["\u623B\u308B","{0}/{1}","\u5165\u529B\u3059\u308B\u3068\u7D50\u679C\u304C\u7D5E\u308A\u8FBC\u307E\u308C\u307E\u3059\u3002","{0} \u4EF6\u306E\u7D50\u679C","{0} \u500B\u9078\u629E\u6E08\u307F","OK","\u30AB\u30B9\u30BF\u30E0","\u623B\u308B ({0})","\u623B\u308B"],"vs/base/parts/quickinput/browser/quickInputList":["\u30AF\u30A4\u30C3\u30AF\u5165\u529B"],"vs/editor/browser/controller/coreCommands":["\u9577\u3044\u884C\u306B\u79FB\u52D5\u3057\u3066\u3082\u884C\u672B\u306B\u4F4D\u7F6E\u3057\u307E\u3059","\u9577\u3044\u884C\u306B\u79FB\u52D5\u3057\u3066\u3082\u884C\u672B\u306B\u4F4D\u7F6E\u3057\u307E\u3059"],"vs/editor/browser/controller/textAreaHandler":["\u30A8\u30C7\u30A3\u30BF\u30FC","\u3053\u306E\u6642\u70B9\u3067\u306F\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u306B\u30A2\u30AF\u30BB\u30B9\u3067\u304D\u307E\u305B\u3093\u3002\u30AA\u30D7\u30B7\u30E7\u30F3\u3092\u8868\u793A\u3059\u308B\u306B\u306F\u3001{0} \u3092\u62BC\u3057\u307E\u3059\u3002"],"vs/editor/browser/core/keybindingCancellation":["\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u53D6\u308A\u6D88\u3057\u53EF\u80FD\u306A\u64CD\u4F5C ('\u53C2\u7167\u3092\u3053\u3053\u306B\u8868\u793A' \u306A\u3069) \u3092\u5B9F\u884C\u3059\u308B\u304B\u3069\u3046\u304B"],"vs/editor/browser/editorExtensions":["\u5143\u306B\u623B\u3059(&&U)","\u5143\u306B\u623B\u3059","\u3084\u308A\u76F4\u3057(&&R)","\u3084\u308A\u76F4\u3057","\u3059\u3079\u3066\u9078\u629E(&&S)","\u3059\u3079\u3066\u3092\u9078\u629E"],"vs/editor/browser/widget/codeEditorWidget":["\u30AB\u30FC\u30BD\u30EB\u306E\u6570\u306F {0} \u500B\u306B\u5236\u9650\u3055\u308C\u3066\u3044\u307E\u3059\u3002"],"vs/editor/browser/widget/diffEditorWidget":["\u5DEE\u5206\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u633F\u5165\u3092\u793A\u3059\u7DDA\u306E\u88C5\u98FE\u3002","\u5DEE\u5206\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u524A\u9664\u3092\u793A\u3059\u7DDA\u306E\u88C5\u98FE\u3002","\u4E00\u65B9\u306E\u30D5\u30A1\u30A4\u30EB\u304C\u5927\u304D\u3059\u304E\u308B\u305F\u3081\u3001\u30D5\u30A1\u30A4\u30EB\u3092\u6BD4\u8F03\u3067\u304D\u307E\u305B\u3093\u3002"],"vs/editor/browser/widget/diffReview":["\u5DEE\u5206\u30EC\u30D3\u30E5\u30FC\u3067\u306E '\u633F\u5165' \u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u5DEE\u5206\u30EC\u30D3\u30E5\u30FC\u3067\u306E '\u524A\u9664' \u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u5DEE\u5206\u30EC\u30D3\u30E5\u30FC\u3067\u306E '\u9589\u3058\u308B' \u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u9589\u3058\u308B","\u5909\u66F4\u3055\u308C\u305F\u884C\u306F\u3042\u308A\u307E\u305B\u3093","1 \u884C\u304C\u5909\u66F4\u3055\u308C\u307E\u3057\u305F","{0} \u884C\u304C\u5909\u66F4\u3055\u308C\u307E\u3057\u305F","\u76F8\u9055 {0}/{1}: \u5143\u306E\u884C {2}\u3001{3}\u3002\u5909\u66F4\u3055\u308C\u305F\u884C {4}\u3001{5}","\u7A7A\u767D","{0} \u5909\u66F4\u3055\u308C\u3066\u3044\u306A\u3044\u884C {1}","{0} \u5143\u306E\u884C {1} \u5909\u66F4\u3055\u308C\u305F\u884C {2}","+ {0} \u5909\u66F4\u3055\u308C\u305F\u884C {1}","- {0} \u5143\u306E\u884C {1}","\u6B21\u306E\u5DEE\u5206\u306B\u79FB\u52D5","\u524D\u306E\u5DEE\u5206\u306B\u79FB\u52D5"],"vs/editor/browser/widget/inlineDiffMargin":["\u524A\u9664\u3055\u308C\u305F\u884C\u306E\u30B3\u30D4\u30FC","\u524A\u9664\u3055\u308C\u305F\u884C\u306E\u30B3\u30D4\u30FC","\u524A\u9664\u3055\u308C\u305F\u884C\u306E\u30B3\u30D4\u30FC ({0})","\u3053\u306E\u5909\u66F4\u3092\u5143\u306B\u623B\u3059","\u524A\u9664\u3055\u308C\u305F\u884C\u306E\u30B3\u30D4\u30FC ({0})"],"vs/editor/common/config/commonEditorConfig":["\u30A8\u30C7\u30A3\u30BF\u30FC","1 \u3064\u306E\u30BF\u30D6\u306B\u76F8\u5F53\u3059\u308B\u30B9\u30DA\u30FC\u30B9\u306E\u6570\u3002`#editor.detectIndentation#` \u304C\u30AA\u30F3\u306E\u5834\u5408\u3001\u3053\u306E\u8A2D\u5B9A\u306F\u30D5\u30A1\u30A4\u30EB \u30B3\u30F3\u30C6\u30F3\u30C4\u306B\u57FA\u3065\u3044\u3066\u4E0A\u66F8\u304D\u3055\u308C\u307E\u3059\u3002","`Tab` \u30AD\u30FC\u3092\u62BC\u3059\u3068\u30B9\u30DA\u30FC\u30B9\u304C\u633F\u5165\u3055\u308C\u307E\u3059\u3002`#editor.detectIndentation#` \u304C\u30AA\u30F3\u306E\u5834\u5408\u3001\u3053\u306E\u8A2D\u5B9A\u306F\u30D5\u30A1\u30A4\u30EB \u30B3\u30F3\u30C6\u30F3\u30C4\u306B\u57FA\u3065\u3044\u3066\u4E0A\u66F8\u304D\u3055\u308C\u307E\u3059\u3002","\u30D5\u30A1\u30A4\u30EB\u304C\u30D5\u30A1\u30A4\u30EB\u306E\u5185\u5BB9\u306B\u57FA\u3065\u3044\u3066\u958B\u304B\u308C\u308B\u5834\u5408\u3001`#editor.tabSize#` \u3068 `#editor.insertSpaces#` \u3092\u81EA\u52D5\u7684\u306B\u691C\u51FA\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u81EA\u52D5\u633F\u5165\u3055\u308C\u305F\u672B\u5C3E\u306E\u7A7A\u767D\u3092\u524A\u9664\u3057\u307E\u3059\u3002","\u5927\u304D\u306A\u30D5\u30A1\u30A4\u30EB\u3067\u30E1\u30E2\u30EA\u304C\u96C6\u4E2D\u3059\u308B\u7279\u5B9A\u306E\u6A5F\u80FD\u3092\u7121\u52B9\u306B\u3059\u308B\u305F\u3081\u306E\u7279\u5225\u306A\u51E6\u7406\u3002","\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u5185\u306E\u5358\u8A9E\u306B\u57FA\u3065\u3044\u3066\u5165\u529B\u5019\u88DC\u3092\u8A08\u7B97\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u304B\u3089\u306E\u307F\u5358\u8A9E\u306E\u5019\u88DC\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u540C\u3058\u8A00\u8A9E\u306E\u958B\u3044\u3066\u3044\u308B\u3059\u3079\u3066\u306E\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u304B\u3089\u5358\u8A9E\u306E\u5019\u88DC\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u958B\u3044\u3066\u3044\u308B\u3059\u3079\u3066\u306E\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u304B\u3089\u5358\u8A9E\u306E\u5019\u88DC\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u5358\u8A9E\u30D9\u30FC\u30B9\u306E\u88DC\u5B8C\u304C\u8A08\u7B97\u3055\u308C\u308B\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30BB\u30DE\u30F3\u30C6\u30A3\u30C3\u30AF\u306E\u5F37\u8ABF\u8868\u793A\u304C\u3059\u3079\u3066\u306E\u914D\u8272\u30C6\u30FC\u30DE\u306B\u3064\u3044\u3066\u6709\u52B9\u306B\u306A\u308A\u307E\u3057\u305F\u3002","\u30BB\u30DE\u30F3\u30C6\u30A3\u30C3\u30AF\u306E\u5F37\u8ABF\u8868\u793A\u304C\u3059\u3079\u3066\u306E\u914D\u8272\u30C6\u30FC\u30DE\u306B\u3064\u3044\u3066\u7121\u52B9\u306B\u306A\u308A\u307E\u3057\u305F\u3002","\u30BB\u30DE\u30F3\u30C6\u30A3\u30C3\u30AF\u306E\u5F37\u8ABF\u8868\u793A\u306F\u3001\u73FE\u5728\u306E\u914D\u8272\u30C6\u30FC\u30DE\u306E 'semanticHighlighting' \u8A2D\u5B9A\u306B\u3088\u3063\u3066\u69CB\u6210\u3055\u308C\u3066\u3044\u307E\u3059\u3002","semanticHighlighting \u3092\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u308B\u8A00\u8A9E\u3067\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30B3\u30F3\u30C6\u30F3\u30C4\u3092\u30C0\u30D6\u30EB\u30AF\u30EA\u30C3\u30AF\u3059\u308B\u304B\u3001`Escape` \u30AD\u30FC\u3092\u62BC\u3057\u3066\u3082\u3001\u30D4\u30FC\u30AF \u30A8\u30C7\u30A3\u30BF\u30FC\u3092\u958B\u3044\u305F\u307E\u307E\u306B\u3057\u307E\u3059\u3002","\u3053\u306E\u9577\u3055\u3092\u8D8A\u3048\u308B\u884C\u306F\u3001\u30D1\u30D5\u30A9\u30FC\u30DE\u30F3\u30B9\u4E0A\u306E\u7406\u7531\u306B\u3088\u308A\u30C8\u30FC\u30AF\u30F3\u5316\u3055\u308C\u307E\u305B\u3093\u3002","\u5DEE\u5206\u8A08\u7B97\u304C\u53D6\u308A\u6D88\u3055\u308C\u305F\u5F8C\u306E\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8 (\u30DF\u30EA\u79D2\u5358\u4F4D)\u3002\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u306A\u3057\u306B\u306F 0 \u3092\u4F7F\u7528\u3057\u307E\u3059\u3002","\u5DEE\u5206\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u5DEE\u5206\u3092\u6A2A\u306B\u4E26\u3079\u3066\u8868\u793A\u3059\u308B\u304B\u3001\u884C\u5185\u306B\u8868\u793A\u3059\u308B\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001\u5DEE\u5206\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u5148\u982D\u307E\u305F\u306F\u672B\u5C3E\u306E\u7A7A\u767D\u6587\u5B57\u306E\u5909\u66F4\u3092\u7121\u8996\u3057\u307E\u3059\u3002","\u5DEE\u5206\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u8FFD\u52A0/\u524A\u9664\u3055\u308C\u305F\u5909\u66F4\u306B +/- \u30A4\u30F3\u30B8\u30B1\u30FC\u30BF\u30FC\u3092\u793A\u3059\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067 CodeLens \u3092\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u884C\u3092\u6298\u308A\u8FD4\u3057\u307E\u305B\u3093\u3002","\u884C\u3092\u30D3\u30E5\u30FC\u30DD\u30FC\u30C8\u306E\u5E45\u3067\u6298\u308A\u8FD4\u3057\u307E\u3059\u3002","\u884C\u306F\u3001`#editor.wordWrap#` \u8A2D\u5B9A\u306B\u5F93\u3063\u3066\u6298\u308A\u8FD4\u3055\u308C\u307E\u3059\u3002"],"vs/editor/common/config/editorOptions":["\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u30B9\u30AF\u30EA\u30FC\u30F3 \u30EA\u30FC\u30C0\u30FC\u304C\u3044\u3064\u63A5\u7D9A\u3055\u308C\u305F\u304B\u3092\u691C\u51FA\u3059\u308B\u305F\u3081\u306B\u30D7\u30E9\u30C3\u30C8\u30D5\u30A9\u30FC\u30E0 API \u3092\u4F7F\u7528\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u6C38\u7D9A\u7684\u306B\u30B9\u30AF\u30EA\u30FC\u30F3 \u30EA\u30FC\u30C0\u30FC\u3067\u306E\u4F7F\u7528\u5411\u3051\u306B\u6700\u9069\u5316\u3055\u308C\u307E\u3059\u3002\u5358\u8A9E\u306E\u6298\u308A\u8FD4\u3057\u306F\u7121\u52B9\u306B\u306A\u308A\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u30B9\u30AF\u30EA\u30FC\u30F3 \u30EA\u30FC\u30C0\u30FC\u5411\u3051\u306B\u6700\u9069\u5316\u3055\u308C\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3092\u30B9\u30AF\u30EA\u30FC\u30F3 \u30EA\u30FC\u30C0\u30FC\u306B\u6700\u9069\u5316\u3055\u308C\u305F\u30E2\u30FC\u30C9\u3067\u5B9F\u884C\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u30AA\u30F3\u306B\u8A2D\u5B9A\u3059\u308B\u3068\u5358\u8A9E\u306E\u6298\u308A\u8FD4\u3057\u304C\u7121\u52B9\u306B\u306A\u308A\u307E\u3059\u3002","\u30B3\u30E1\u30F3\u30C8\u6642\u306B\u7A7A\u767D\u6587\u5B57\u3092\u633F\u5165\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u884C\u30B3\u30E1\u30F3\u30C8\u306E\u8FFD\u52A0\u307E\u305F\u306F\u524A\u9664\u30A2\u30AF\u30B7\u30E7\u30F3\u306E\u5207\u308A\u66FF\u3048\u3067\u3001\u7A7A\u306E\u884C\u3092\u7121\u8996\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u9078\u629E\u7BC4\u56F2\u3092\u6307\u5B9A\u3057\u306A\u3044\u3067\u30B3\u30D4\u30FC\u3059\u308B\u5834\u5408\u306B\u73FE\u5728\u306E\u884C\u3092\u30B3\u30D4\u30FC\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5165\u529B\u4E2D\u306B\u4E00\u81F4\u3092\u691C\u7D22\u3059\u308B\u305F\u3081\u306B\u30AB\u30FC\u30BD\u30EB\u3092\u30B8\u30E3\u30F3\u30D7\u3055\u305B\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u9078\u629E\u7BC4\u56F2\u304B\u3089\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u5185\u306E\u691C\u7D22\u6587\u5B57\u5217\u3092\u4E0E\u3048\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","[\u9078\u629E\u7BC4\u56F2\u3092\u691C\u7D22] \u3092\u81EA\u52D5\u7684\u306B\u30AA\u30F3\u306B\u3057\u306A\u3044 (\u65E2\u5B9A)","[\u9078\u629E\u7BC4\u56F2\u3092\u691C\u7D22] \u3092\u5E38\u306B\u81EA\u52D5\u7684\u306B\u30AA\u30F3\u306B\u3059\u308B","\u8907\u6570\u884C\u306E\u30B3\u30F3\u30C6\u30F3\u30C4\u304C\u9078\u629E\u3055\u308C\u3066\u3044\u308B\u5834\u5408\u306F\u3001\u81EA\u52D5\u7684\u306B [\u9078\u629E\u7BC4\u56F2\u3092\u691C\u7D22] \u3092\u30AA\u30F3\u306B\u3057\u307E\u3059\u3002","[\u9078\u629E\u7BC4\u56F2\u3092\u691C\u7D22] \u3092\u81EA\u52D5\u7684\u306B\u30AA\u30F3\u306B\u3059\u308B\u6761\u4EF6\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","macOS \u3067\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u304C\u5171\u6709\u306E\u691C\u7D22\u30AF\u30EA\u30C3\u30D7\u30DC\u30FC\u30C9\u3092\u8AAD\u307F\u53D6\u308A\u307E\u305F\u306F\u5909\u66F4\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u304C\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u4E0A\u306B\u884C\u3092\u3055\u3089\u306B\u8FFD\u52A0\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002true \u306E\u5834\u5408\u3001\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u304C\u8868\u793A\u3055\u308C\u3066\u3044\u308B\u3068\u304D\u306B\u6700\u521D\u306E\u884C\u3092\u8D85\u3048\u3066\u30B9\u30AF\u30ED\u30FC\u30EB\u3067\u304D\u307E\u3059\u3002","\u4EE5\u964D\u3067\u4E00\u81F4\u304C\u898B\u3064\u304B\u3089\u306A\u3044\u5834\u5408\u306B\u3001\u691C\u7D22\u3092\u5148\u982D\u304B\u3089 (\u307E\u305F\u306F\u672B\u5C3E\u304B\u3089) \u81EA\u52D5\u7684\u306B\u518D\u5B9F\u884C\u3059\u308B\u304B\u3069\u3046\u304B\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30D5\u30A9\u30F3\u30C8\u306E\u5408\u5B57 ('calt' \u304A\u3088\u3073 'liga' \u30D5\u30A9\u30F3\u30C8\u306E\u6A5F\u80FD) \u3092\u6709\u52B9\u307E\u305F\u306F\u7121\u52B9\u306B\u3057\u307E\u3059\u3002'font-feature-settings' CSS \u30D7\u30ED\u30D1\u30C6\u30A3\u3092\u8A73\u7D30\u306B\u5236\u5FA1\u3059\u308B\u306B\u306F\u3001\u3053\u308C\u3092\u6587\u5B57\u5217\u306B\u5909\u66F4\u3057\u307E\u3059\u3002","\u660E\u793A\u7684\u306A 'font-feature-settings' CSS \u30D7\u30ED\u30D1\u30C6\u30A3\u3002\u5408\u5B57\u3092\u6709\u52B9\u307E\u305F\u306F\u7121\u52B9\u306B\u3059\u308B\u5FC5\u8981\u304C\u3042\u308B\u306E\u304C 1 \u3064\u3060\u3051\u3067\u3042\u308B\u5834\u5408\u306F\u3001\u4EE3\u308F\u308A\u306B\u30D6\u30FC\u30EB\u5024\u3092\u6E21\u3059\u3053\u3068\u304C\u3067\u304D\u307E\u3059\u3002","\u30D5\u30A9\u30F3\u30C8\u306E\u5408\u5B57\u3084\u30D5\u30A9\u30F3\u30C8\u306E\u6A5F\u80FD\u3092\u69CB\u6210\u3057\u307E\u3059\u3002\u5408\u5B57\u3092\u6709\u52B9\u307E\u305F\u306F\u7121\u52B9\u306B\u3059\u308B\u30D6\u30FC\u30EB\u5024\u307E\u305F\u306F CSS 'font-feature-settings' \u30D7\u30ED\u30D1\u30C6\u30A3\u306E\u5024\u306E\u6587\u5B57\u5217\u3092\u6307\u5B9A\u3067\u304D\u307E\u3059\u3002","\u30D5\u30A9\u30F3\u30C8 \u30B5\u30A4\u30BA (\u30D4\u30AF\u30BB\u30EB\u5358\u4F4D) \u3092\u5236\u5FA1\u3057\u307E\u3059\u3002",'\u4F7F\u7528\u3067\u304D\u308B\u306E\u306F "\u6A19\u6E96" \u304A\u3088\u3073 "\u592A\u5B57" \u306E\u30AD\u30FC\u30EF\u30FC\u30C9\u307E\u305F\u306F 1 \uFF5E 1000 \u306E\u6570\u5B57\u306E\u307F\u3067\u3059\u3002','\u30D5\u30A9\u30F3\u30C8\u306E\u592A\u3055\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002"\u6A19\u6E96" \u304A\u3088\u3073 "\u592A\u5B57" \u306E\u30AD\u30FC\u30EF\u30FC\u30C9\u307E\u305F\u306F 1 \uFF5E 1000 \u306E\u6570\u5B57\u3092\u53D7\u3051\u5165\u308C\u307E\u3059\u3002',"\u7D50\u679C\u306E\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u3092\u8868\u793A (\u65E2\u5B9A)","\u4E3B\u306A\u7D50\u679C\u306B\u79FB\u52D5\u3057\u3001\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u3092\u8868\u793A\u3057\u307E\u3059","\u30D7\u30E9\u30A4\u30DE\u30EA\u7D50\u679C\u306B\u79FB\u52D5\u3057\u3001\u4ED6\u306E\u30E6\u30FC\u30B6\u30FC\u3078\u306E\u30D4\u30FC\u30AF\u30EC\u30B9 \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3\u3092\u6709\u52B9\u306B\u3057\u307E\u3059","\u3053\u306E\u8A2D\u5B9A\u306F\u975E\u63A8\u5968\u3067\u3059\u3002\u4EE3\u308F\u308A\u306B\u3001'editor.editor.gotoLocation.multipleDefinitions' \u3084 'editor.editor.gotoLocation.multipleImplementations' \u306A\u3069\u306E\u500B\u5225\u306E\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u8907\u6570\u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u306E\u5834\u6240\u304C\u3042\u308B\u3068\u304D\u306E '\u5B9A\u7FA9\u3078\u79FB\u52D5' \u30B3\u30DE\u30F3\u30C9\u306E\u52D5\u4F5C\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u8907\u6570\u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u306E\u5834\u6240\u304C\u3042\u308B\u3068\u304D\u306E '\u578B\u5B9A\u7FA9\u3078\u79FB\u52D5' \u30B3\u30DE\u30F3\u30C9\u306E\u52D5\u4F5C\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u8907\u6570\u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u306E\u5834\u6240\u304C\u3042\u308B\u3068\u304D\u306E '\u5BA3\u8A00\u3078\u79FB\u52D5' \u30B3\u30DE\u30F3\u30C9\u306E\u52D5\u4F5C\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u8907\u6570\u306E\u30BF\u30FC\u30B2\u30C3\u30C8\u306E\u5834\u6240\u304C\u3042\u308B\u3068\u304D\u306E '\u5B9F\u88C5\u306B\u79FB\u52D5' \u30B3\u30DE\u30F3\u30C9\u306E\u52D5\u4F5C\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30BF\u30FC\u30B2\u30C3\u30C8\u306E\u5834\u6240\u304C\u8907\u6570\u5B58\u5728\u3059\u308B\u5834\u5408\u306E '\u53C2\u7167\u3078\u79FB\u52D5' \u30B3\u30DE\u30F3\u30C9\u306E\u52D5\u4F5C\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","'\u5B9A\u7FA9\u3078\u79FB\u52D5' \u306E\u7D50\u679C\u304C\u73FE\u5728\u306E\u5834\u6240\u3067\u3042\u308B\u5834\u5408\u306B\u5B9F\u884C\u3055\u308C\u308B\u4EE3\u66FF\u30B3\u30DE\u30F3\u30C9 ID\u3002","'\u578B\u5B9A\u7FA9\u3078\u79FB\u52D5' \u306E\u7D50\u679C\u304C\u73FE\u5728\u306E\u5834\u6240\u3067\u3042\u308B\u5834\u5408\u306B\u5B9F\u884C\u3055\u308C\u308B\u4EE3\u66FF\u30B3\u30DE\u30F3\u30C9 ID\u3002","'\u5BA3\u8A00\u3078\u79FB\u52D5' \u306E\u7D50\u679C\u304C\u73FE\u5728\u306E\u5834\u6240\u3067\u3042\u308B\u5834\u5408\u306B\u5B9F\u884C\u3055\u308C\u308B\u4EE3\u66FF\u30B3\u30DE\u30F3\u30C9 ID\u3002","'\u5B9F\u88C5\u3078\u79FB\u52D5' \u306E\u7D50\u679C\u304C\u73FE\u5728\u306E\u5834\u6240\u3067\u3042\u308B\u5834\u5408\u306B\u5B9F\u884C\u3055\u308C\u308B\u4EE3\u66FF\u30B3\u30DE\u30F3\u30C9 ID\u3002","'\u53C2\u7167\u3078\u79FB\u52D5' \u306E\u7D50\u679C\u304C\u73FE\u5728\u306E\u5834\u6240\u3067\u3042\u308B\u5834\u5408\u306B\u5B9F\u884C\u3055\u308C\u308B\u4EE3\u66FF\u30B3\u30DE\u30F3\u30C9 ID\u3002","\u30DB\u30D0\u30FC\u3092\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30DB\u30D0\u30FC\u3092\u8868\u793A\u5F8C\u306E\u5F85\u3061\u6642\u9593 (\u30DF\u30EA\u79D2) \u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30DB\u30D0\u30FC\u306B\u30DE\u30A6\u30B9\u3092\u79FB\u52D5\u3057\u305F\u3068\u304D\u306B\u3001\u30DB\u30D0\u30FC\u3092\u8868\u793A\u3057\u7D9A\u3051\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u306E\u96FB\u7403\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30A4\u30F3\u30E9\u30A4\u30F3 \u30D2\u30F3\u30C8\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30A4\u30F3\u30E9\u30A4\u30F3 \u30D2\u30F3\u30C8\u306E\u30D5\u30A9\u30F3\u30C8 \u30B5\u30A4\u30BA\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002'0' \u306B\u8A2D\u5B9A\u3059\u308B\u3068\u3001`#editor.fontSize#` \u306E 90% \u304C\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30A4\u30F3\u30E9\u30A4\u30F3 \u30D2\u30F3\u30C8\u306E\u30D5\u30A9\u30F3\u30C8 \u30D5\u30A1\u30DF\u30EA\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u884C\u306E\u9AD8\u3055\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u30D5\u30A9\u30F3\u30C8 \u30B5\u30A4\u30BA\u306B\u57FA\u3065\u3044\u3066\u884C\u306E\u9AD8\u3055\u3092\u8A08\u7B97\u3059\u308B\u5834\u5408\u306B\u306F\u30010 \u3092\u4F7F\u7528\u3057\u307E\u3059\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7\u3092\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7\u306E\u30B5\u30A4\u30BA\u306F\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30B3\u30F3\u30C6\u30F3\u30C4\u3068\u540C\u3058\u3067\u3059 (\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u5834\u5408\u304C\u3042\u308A\u307E\u3059)\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7\u306F\u3001\u5FC5\u8981\u306B\u5FDC\u3058\u3066\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u9AD8\u3055\u3092\u57CB\u3081\u308B\u305F\u3081\u3001\u62E1\u5927\u307E\u305F\u306F\u7E2E\u5C0F\u3057\u307E\u3059 (\u30B9\u30AF\u30ED\u30FC\u30EB\u3057\u307E\u305B\u3093)\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7\u306F\u5FC5\u8981\u306B\u5FDC\u3058\u3066\u7E2E\u5C0F\u3057\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u3088\u308A\u5927\u304D\u304F\u306A\u308B\u3053\u3068\u306F\u3042\u308A\u307E\u305B\u3093 (\u30B9\u30AF\u30ED\u30FC\u30EB\u3057\u307E\u305B\u3093)\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7\u306E\u30B5\u30A4\u30BA\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7\u3092\u8868\u793A\u3059\u308B\u5834\u6240\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7 \u30B9\u30E9\u30A4\u30C0\u30FC\u3092\u8868\u793A\u3059\u308B\u30BF\u30A4\u30DF\u30F3\u30B0\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7\u306B\u63CF\u753B\u3055\u308C\u308B\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u30B9\u30B1\u30FC\u30EB: 1\u30012\u3001\u307E\u305F\u306F 3\u3002","\u884C\u306B\u30AB\u30E9\u30FC \u30D6\u30ED\u30C3\u30AF\u3067\u306F\u306A\u304F\u5B9F\u969B\u306E\u6587\u5B57\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u8868\u793A\u3059\u308B\u30DF\u30CB\u30DE\u30C3\u30D7\u306E\u6700\u5927\u5E45\u3092\u7279\u5B9A\u306E\u5217\u6570\u306B\u5236\u9650\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u4E0A\u7AEF\u3068\u6700\u521D\u306E\u884C\u306E\u9593\u306E\u4F59\u767D\u306E\u5927\u304D\u3055\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u4E0B\u7AEF\u3068\u6700\u5F8C\u306E\u884C\u306E\u9593\u306E\u4F59\u767D\u306E\u5927\u304D\u3055\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5165\u529B\u6642\u306B\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC \u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u3068\u578B\u60C5\u5831\u3092\u8868\u793A\u3059\u308B\u30DD\u30C3\u30D7\u30A2\u30C3\u30D7\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002","\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC \u30D2\u30F3\u30C8 \u30E1\u30CB\u30E5\u30FC\u3092\u5468\u56DE\u3059\u308B\u304B\u3001\u30EA\u30B9\u30C8\u306E\u6700\u5F8C\u3067\u9589\u3058\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u6587\u5B57\u5217\u5185\u3067\u30AF\u30A4\u30C3\u30AF\u5019\u88DC\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002","\u30B3\u30E1\u30F3\u30C8\u5185\u3067\u30AF\u30A4\u30C3\u30AF\u5019\u88DC\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002","\u6587\u5B57\u5217\u304A\u3088\u3073\u30B3\u30E1\u30F3\u30C8\u5916\u3067\u30AF\u30A4\u30C3\u30AF\u5019\u88DC\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002","\u5165\u529B\u4E2D\u306B\u5019\u88DC\u3092\u81EA\u52D5\u7684\u306B\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u884C\u756A\u53F7\u306F\u8868\u793A\u3055\u308C\u307E\u305B\u3093\u3002","\u884C\u756A\u53F7\u306F\u3001\u7D76\u5BFE\u5024\u3068\u3057\u3066\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u884C\u756A\u53F7\u306F\u3001\u30AB\u30FC\u30BD\u30EB\u4F4D\u7F6E\u307E\u3067\u306E\u884C\u6570\u3068\u3057\u3066\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u884C\u756A\u53F7\u306F 10 \u884C\u3054\u3068\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u884C\u756A\u53F7\u306E\u8868\u793A\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u3053\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30EB\u30FC\u30E9\u30FC\u304C\u30EC\u30F3\u30C0\u30EA\u30F3\u30B0\u3059\u308B\u5358\u4E00\u9818\u57DF\u306E\u6587\u5B57\u6570\u3002","\u3053\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30EB\u30FC\u30E9\u30FC\u306E\u8272\u3067\u3059\u3002","\u7279\u5B9A\u306E\u7B49\u5E45\u6587\u5B57\u6570\u306E\u5F8C\u306B\u5782\u76F4\u30EB\u30FC\u30E9\u30FC\u3092\u8868\u793A\u3057\u307E\u3059\u3002\u8907\u6570\u306E\u30EB\u30FC\u30E9\u30FC\u306B\u306F\u8907\u6570\u306E\u5024\u3092\u4F7F\u7528\u3057\u307E\u3059\u3002\u914D\u5217\u304C\u7A7A\u306E\u5834\u5408\u306F\u30EB\u30FC\u30E9\u30FC\u3092\u8868\u793A\u3057\u307E\u305B\u3093\u3002","\u30AB\u30FC\u30BD\u30EB\u306E\u53F3\u306E\u30C6\u30AD\u30B9\u30C8\u3092\u4E0A\u66F8\u304D\u305B\u305A\u306B\u5019\u88DC\u3092\u633F\u5165\u3057\u307E\u3059\u3002","\u5019\u88DC\u3092\u633F\u5165\u3057\u3001\u30AB\u30FC\u30BD\u30EB\u306E\u53F3\u306E\u30C6\u30AD\u30B9\u30C8\u3092\u4E0A\u66F8\u304D\u3057\u307E\u3059\u3002","\u5165\u529B\u5019\u88DC\u3092\u53D7\u3051\u5165\u308C\u308B\u3068\u304D\u306B\u5358\u8A9E\u3092\u4E0A\u66F8\u304D\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u3053\u308C\u306F\u3001\u3053\u306E\u6A5F\u80FD\u306E\u5229\u7528\u3092\u9078\u629E\u3059\u308B\u62E1\u5F35\u6A5F\u80FD\u306B\u4F9D\u5B58\u3059\u308B\u3053\u3068\u306B\u3054\u6CE8\u610F\u304F\u3060\u3055\u3044\u3002","\u5019\u88DC\u306E\u30D5\u30A3\u30EB\u30BF\u30FC\u51E6\u7406\u3068\u4E26\u3073\u66FF\u3048\u3067\u3055\u3055\u3044\u306A\u5165\u529B\u30DF\u30B9\u3092\u8003\u616E\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u4E26\u3079\u66FF\u3048\u304C\u30AB\u30FC\u30BD\u30EB\u4ED8\u8FD1\u306B\u8868\u793A\u3055\u308C\u308B\u5358\u8A9E\u3092\u512A\u5148\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u4FDD\u5B58\u3055\u308C\u305F\u5019\u88DC\u30BB\u30AF\u30B7\u30E7\u30F3\u3092\u8907\u6570\u306E\u30EF\u30FC\u30AF\u30D7\u30EC\u30FC\u30B9\u3068\u30A6\u30A3\u30F3\u30C9\u30A6\u3067\u5171\u6709\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059 (`#editor.suggestSelection#` \u304C\u5FC5\u8981)\u3002","\u30A2\u30AF\u30C6\u30A3\u30D6 \u30B9\u30CB\u30DA\u30C3\u30C8\u304C\u30AF\u30A4\u30C3\u30AF\u5019\u88DC\u3092\u9632\u6B62\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u63D0\u6848\u306E\u30A2\u30A4\u30B3\u30F3\u3092\u8868\u793A\u3059\u308B\u304B\u3001\u975E\u8868\u793A\u306B\u3059\u308B\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5019\u88DC\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u4E0B\u90E8\u306B\u3042\u308B\u30B9\u30C6\u30FC\u30BF\u30B9 \u30D0\u30FC\u306E\u8868\u793A\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5019\u88DC\u306E\u8A73\u7D30\u3092\u30E9\u30D9\u30EB\u4ED8\u304D\u306E\u30A4\u30F3\u30E9\u30A4\u30F3\u3067\u8868\u793A\u3059\u308B\u304B\u3001\u8A73\u7D30\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u306E\u307F\u8868\u793A\u3059\u308B\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059","\u3053\u306E\u8A2D\u5B9A\u306F\u975E\u63A8\u5968\u3067\u3059\u3002\u5019\u88DC\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u30B5\u30A4\u30BA\u5909\u66F4\u304C\u3067\u304D\u308B\u3088\u3046\u306B\u306A\u308A\u307E\u3057\u305F\u3002","\u3053\u306E\u8A2D\u5B9A\u306F\u975E\u63A8\u5968\u3067\u3059\u3002\u4EE3\u308F\u308A\u306B\u3001'editor.suggest.showKeywords' \u3084 'editor.suggest.showSnippets' \u306A\u3069\u306E\u500B\u5225\u306E\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30E1\u30BD\u30C3\u30C9` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u95A2\u6570` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30B3\u30F3\u30B9\u30C8\u30E9\u30AF\u30BF\u30FC` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30D5\u30A3\u30FC\u30EB\u30C9` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u5909\u6570` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B '\u30AF\u30E9\u30B9' \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u69CB\u9020\u4F53` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30A4\u30F3\u30BF\u30FC\u30D5\u30A7\u30A4\u30B9` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30E2\u30B8\u30E5\u30FC\u30EB` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30D7\u30ED\u30D1\u30C6\u30A3` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30A4\u30D9\u30F3\u30C8` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u6F14\u7B97\u5B50` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30E6\u30CB\u30C3\u30C8` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u5024` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u5B9A\u6570` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u5217\u6319\u578B` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `enumMember` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30AD\u30FC\u30EF\u30FC\u30C9` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B '\u30C6\u30AD\u30B9\u30C8' -\u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u8272` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B '\u30D5\u30A1\u30A4\u30EB' \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u53C2\u7167` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `customcolor` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30D5\u30A9\u30EB\u30C0\u30FC` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `typeParameter` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B `\u30B9\u30CB\u30DA\u30C3\u30C8` \u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306A\u5834\u5408\u3001IntelliSense \u306B\u3088\u3063\u3066 '\u30E6\u30FC\u30B6\u30FC' \u5019\u88DC\u304C\u793A\u3055\u308C\u307E\u3059\u3002","\u6709\u52B9\u306B\u3059\u308B\u3068\u3001IntelliSense \u306B\u3088\u3063\u3066 '\u554F\u984C' \u5019\u88DC\u304C\u793A\u3055\u308C\u307E\u3059\u3002","\u5148\u982D\u3068\u672B\u5C3E\u306E\u7A7A\u767D\u3092\u5E38\u306B\u9078\u629E\u3059\u308B\u304B\u3069\u3046\u304B\u3002","\u30B3\u30DF\u30C3\u30C8\u6587\u5B57\u3067\u5019\u88DC\u3092\u53D7\u3051\u5165\u308C\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u305F\u3068\u3048\u3070\u3001JavaScript \u3067\u306F\u30BB\u30DF\u30B3\u30ED\u30F3 (`;`) \u3092\u30B3\u30DF\u30C3\u30C8\u6587\u5B57\u306B\u3057\u3066\u3001\u5019\u88DC\u3092\u53D7\u3051\u5165\u308C\u3066\u305D\u306E\u6587\u5B57\u3092\u5165\u529B\u3059\u308B\u3053\u3068\u304C\u3067\u304D\u307E\u3059\u3002","\u30C6\u30AD\u30B9\u30C8\u306E\u5909\u66F4\u3092\u884C\u3046\u3068\u304D\u3001`Enter` \u3092\u4F7F\u7528\u3059\u308B\u5834\u5408\u306B\u306E\u307F\u5019\u88DC\u3092\u53D7\u3051\u4ED8\u3051\u307E\u3059\u3002","`Tab` \u30AD\u30FC\u306B\u52A0\u3048\u3066 `Enter` \u30AD\u30FC\u3067\u5019\u88DC\u3092\u53D7\u3051\u5165\u308C\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u6539\u884C\u306E\u633F\u5165\u3084\u5019\u88DC\u306E\u53CD\u6620\u306E\u9593\u3067\u3042\u3044\u307E\u3044\u3055\u3092\u89E3\u6D88\u3059\u308B\u306E\u306B\u5F79\u7ACB\u3061\u307E\u3059\u3002","\u30B9\u30AF\u30EA\u30FC\u30F3 \u30EA\u30FC\u30C0\u30FC\u3067\u8AAD\u307F\u4E0A\u3052\u308B\u3053\u3068\u304C\u3067\u304D\u308B\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u884C\u6570\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u8B66\u544A: \u65E2\u5B9A\u5024\u3092\u4E0A\u56DE\u308B\u6570\u3092\u6307\u5B9A\u3059\u308B\u3068\u3001\u30D1\u30D5\u30A9\u30FC\u30DE\u30F3\u30B9\u306B\u5F71\u97FF\u3092\u4E0E\u3048\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30B3\u30F3\u30C6\u30F3\u30C4","\u8A00\u8A9E\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u3044\u3064\u304B\u3063\u3053\u3092\u81EA\u52D5\u30AF\u30ED\u30FC\u30BA\u3059\u308B\u304B\u6C7A\u5B9A\u3057\u307E\u3059\u3002","\u30AB\u30FC\u30BD\u30EB\u304C\u7A7A\u767D\u6587\u5B57\u306E\u5DE6\u306B\u3042\u308B\u3068\u304D\u3060\u3051\u3001\u304B\u3063\u3053\u3092\u81EA\u52D5\u30AF\u30ED\u30FC\u30BA\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u5DE6\u89D2\u304B\u3063\u3053\u3092\u8FFD\u52A0\u3057\u305F\u5F8C\u306B\u81EA\u52D5\u7684\u306B\u53F3\u89D2\u304B\u3063\u3053\u3092\u633F\u5165\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u7D42\u308F\u308A\u5F15\u7528\u7B26\u307E\u305F\u306F\u62EC\u5F27\u304C\u81EA\u52D5\u7684\u306B\u633F\u5165\u3055\u308C\u305F\u5834\u5408\u306B\u306E\u307F\u3001\u305D\u308C\u3089\u3092\u4E0A\u66F8\u304D\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u7D42\u308F\u308A\u5F15\u7528\u7B26\u307E\u305F\u306F\u62EC\u5F27\u3092\u4E0A\u66F8\u304D\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u8A00\u8A9E\u8A2D\u5B9A\u3092\u4F7F\u7528\u3057\u3066\u3001\u3044\u3064\u5F15\u7528\u7B26\u3092\u81EA\u52D5\u30AF\u30ED\u30FC\u30BA\u3059\u308B\u304B\u6C7A\u5B9A\u3057\u307E\u3059\u3002","\u30AB\u30FC\u30BD\u30EB\u304C\u7A7A\u767D\u6587\u5B57\u306E\u5DE6\u306B\u3042\u308B\u3068\u304D\u3060\u3051\u3001\u5F15\u7528\u7B26\u3092\u81EA\u52D5\u30AF\u30ED\u30FC\u30BA\u3057\u307E\u3059\u3002","\u30E6\u30FC\u30B6\u30FC\u304C\u958B\u59CB\u5F15\u7528\u7B26\u3092\u8FFD\u52A0\u3057\u305F\u5F8C\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u81EA\u52D5\u7684\u306B\u5F15\u7528\u7B26\u3092\u9589\u3058\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u81EA\u52D5\u7684\u306B\u633F\u5165\u3057\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u3001\u73FE\u5728\u306E\u884C\u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u4FDD\u6301\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u3001\u73FE\u5728\u306E\u884C\u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u4FDD\u6301\u3057\u3001\u8A00\u8A9E\u304C\u5B9A\u7FA9\u3055\u308C\u305F\u304B\u3063\u3053\u3092\u512A\u5148\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u3001\u73FE\u5728\u306E\u884C\u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u4FDD\u6301\u3057\u3001\u8A00\u8A9E\u304C\u5B9A\u7FA9\u3055\u308C\u305F\u304B\u3063\u3053\u3092\u512A\u5148\u3057\u3001\u8A00\u8A9E\u3067\u5B9A\u7FA9\u3055\u308C\u305F\u7279\u5225\u306A onEnterRules \u3092\u547C\u3073\u51FA\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u3001\u73FE\u5728\u306E\u884C\u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u4FDD\u6301\u3057\u3001\u8A00\u8A9E\u304C\u5B9A\u7FA9\u3055\u308C\u305F\u304B\u3063\u3053\u3092\u512A\u5148\u3057\u3001\u8A00\u8A9E\u3067\u5B9A\u7FA9\u3055\u308C\u305F\u7279\u5225\u306A onEnterRules \u3092\u547C\u3073\u51FA\u3057\u3001\u8A00\u8A9E\u3067\u5B9A\u7FA9\u3055\u308C\u305F indentationRules \u3092\u512A\u5148\u3057\u307E\u3059\u3002","\u30E6\u30FC\u30B6\u30FC\u304C\u884C\u3092\u5165\u529B\u3001\u8CBC\u308A\u4ED8\u3051\u3001\u79FB\u52D5\u3001\u307E\u305F\u306F\u30A4\u30F3\u30C7\u30F3\u30C8\u3059\u308B\u3068\u304D\u306B\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u81EA\u52D5\u7684\u306B\u8ABF\u6574\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u8A00\u8A9E\u69CB\u6210\u3092\u4F7F\u7528\u3057\u3066\u3001\u9078\u629E\u7BC4\u56F2\u3092\u3044\u3064\u81EA\u52D5\u7684\u306B\u56F2\u3080\u304B\u3092\u5224\u65AD\u3057\u307E\u3059\u3002","\u89D2\u304B\u3063\u3053\u3067\u306F\u306A\u304F\u3001\u5F15\u7528\u7B26\u3067\u56F2\u307F\u307E\u3059\u3002","\u5F15\u7528\u7B26\u3067\u306F\u306A\u304F\u3001\u89D2\u304B\u3063\u3053\u3067\u56F2\u307F\u307E\u3059\u3002","\u5F15\u7528\u7B26\u307E\u305F\u306F\u89D2\u304B\u3063\u3053\u3092\u5165\u529B\u3059\u308B\u3068\u304D\u306B\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u9078\u629E\u7BC4\u56F2\u3092\u81EA\u52D5\u7684\u306B\u56F2\u3080\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A4\u30F3\u30C7\u30F3\u30C8\u306B\u30B9\u30DA\u30FC\u30B9\u3092\u4F7F\u7528\u3059\u308B\u3068\u304D\u306F\u3001\u30BF\u30D6\u6587\u5B57\u306E\u9078\u629E\u52D5\u4F5C\u3092\u30A8\u30DF\u30E5\u30EC\u30FC\u30C8\u3057\u307E\u3059\u3002\u9078\u629E\u7BC4\u56F2\u306F\u30BF\u30D6\u4F4D\u7F6E\u306B\u7559\u307E\u308A\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067 CodeLens \u3092\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","CodeLens \u306E\u30D5\u30A9\u30F3\u30C8 \u30D5\u30A1\u30DF\u30EA\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","CodeLens \u306E\u30D5\u30A9\u30F3\u30C8 \u30B5\u30A4\u30BA\u3092\u30D4\u30AF\u30BB\u30EB\u5358\u4F4D\u3067\u5236\u5FA1\u3057\u307E\u3059\u3002'0' \u306B\u8A2D\u5B9A\u3059\u308B\u3068\u3001'#editor.fontSize#' \u306E 90% \u304C\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30A4\u30F3\u30E9\u30A4\u30F3 \u30AB\u30E9\u30FC \u30C7\u30B3\u30EC\u30FC\u30BF\u30FC\u3068\u8272\u306E\u9078\u629E\u3092\u8868\u793A\u3059\u308B\u5FC5\u8981\u304C\u3042\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30DE\u30A6\u30B9\u3068\u30AD\u30FC\u3067\u306E\u9078\u629E\u306B\u3088\u308A\u5217\u306E\u9078\u629E\u3092\u5B9F\u884C\u3067\u304D\u308B\u3088\u3046\u306B\u3057\u307E\u3059\u3002","\u69CB\u6587\u30CF\u30A4\u30E9\u30A4\u30C8\u3092\u30AF\u30EA\u30C3\u30D7\u30DC\u30FC\u30C9\u306B\u30B3\u30D4\u30FC\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30AB\u30FC\u30BD\u30EB\u306E\u30A2\u30CB\u30E1\u30FC\u30B7\u30E7\u30F3\u65B9\u5F0F\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u6ED1\u3089\u304B\u306A\u30AD\u30E3\u30EC\u30C3\u30C8\u30A2\u30CB\u30E1\u30FC\u30B7\u30E7\u30F3\u3092\u6709\u52B9\u306B\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30AB\u30FC\u30BD\u30EB\u306E\u30B9\u30BF\u30A4\u30EB\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30AB\u30FC\u30BD\u30EB\u524D\u5F8C\u306E\u8868\u793A\u53EF\u80FD\u306A\u5148\u982D\u3068\u672B\u5C3E\u306E\u884C\u306E\u6700\u5C0F\u6570\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u4ED6\u306E\u4E00\u90E8\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u306F 'scrollOff' \u307E\u305F\u306F `scrollOffset` \u3068\u547C\u3070\u308C\u307E\u3059\u3002","`cursorSurroundingLines` \u306F\u3001\u30AD\u30FC\u30DC\u30FC\u30C9\u307E\u305F\u306F API \u3067\u30C8\u30EA\u30AC\u30FC\u3055\u308C\u305F\u5834\u5408\u306B\u306E\u307F\u5F37\u5236\u3055\u308C\u307E\u3059\u3002","`cursorSurroundingLines` \u306F\u5E38\u306B\u9069\u7528\u3055\u308C\u307E\u3059\u3002","'\u30AB\u30FC\u30BD\u30EB\u306E\u5468\u56F2\u306E\u884C' \u3092\u9069\u7528\u3059\u308B\u30BF\u30A4\u30DF\u30F3\u30B0\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","`#editor.cursorStyle#` \u304C `line` \u306B\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u308B\u5834\u5408\u3001\u30AB\u30FC\u30BD\u30EB\u306E\u5E45\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30C9\u30E9\u30C3\u30B0 \u30A2\u30F3\u30C9 \u30C9\u30ED\u30C3\u30D7\u306B\u3088\u308B\u9078\u629E\u7BC4\u56F2\u306E\u79FB\u52D5\u3092\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u8A31\u53EF\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","`Alt` \u3092\u62BC\u3059\u3068\u3001\u30B9\u30AF\u30ED\u30FC\u30EB\u901F\u5EA6\u304C\u500D\u5897\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30B3\u30FC\u30C9\u306E\u6298\u308A\u305F\u305F\u307F\u3092\u6709\u52B9\u306B\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5229\u7528\u53EF\u80FD\u306A\u5834\u5408\u306F\u8A00\u8A9E\u56FA\u6709\u306E\u6298\u308A\u305F\u305F\u307F\u65B9\u6CD5\u3092\u4F7F\u7528\u3057\u3001\u5229\u7528\u53EF\u80FD\u3067\u306F\u306A\u3044\u5834\u5408\u306F\u30A4\u30F3\u30C7\u30F3\u30C8\u30D9\u30FC\u30B9\u306E\u65B9\u6CD5\u3092\u4F7F\u7528\u3057\u307E\u3059\u3002","\u30A4\u30F3\u30C7\u30F3\u30C8\u30D9\u30FC\u30B9\u306E\u6298\u308A\u305F\u305F\u307F\u65B9\u6CD5\u3092\u4F7F\u7528\u3057\u307E\u3059\u3002","\u6298\u308A\u305F\u305F\u307F\u7BC4\u56F2\u306E\u8A08\u7B97\u65B9\u6CD5\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u6298\u308A\u305F\u305F\u307E\u308C\u305F\u7BC4\u56F2\u3092\u5F37\u8ABF\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB\u3057\u307E\u3059\u3002","\u6298\u308A\u305F\u305F\u307E\u308C\u305F\u7DDA\u306E\u5F8C\u306E\u7A7A\u306E\u30B3\u30F3\u30C6\u30F3\u30C4\u3092\u30AF\u30EA\u30C3\u30AF\u3059\u308B\u3068\u7DDA\u304C\u5C55\u958B\u3055\u308C\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30D5\u30A9\u30F3\u30C8 \u30D5\u30A1\u30DF\u30EA\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u8CBC\u308A\u4ED8\u3051\u305F\u5185\u5BB9\u304C\u30A8\u30C7\u30A3\u30BF\u30FC\u306B\u3088\u308A\u81EA\u52D5\u7684\u306B\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u3055\u308C\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u30D5\u30A9\u30FC\u30DE\u30C3\u30BF\u3092\u4F7F\u7528\u53EF\u80FD\u306B\u3059\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002\u307E\u305F\u3001\u30D5\u30A9\u30FC\u30DE\u30C3\u30BF\u304C\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u5185\u306E\u7BC4\u56F2\u3092\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u3067\u304D\u306A\u3051\u308C\u3070\u306A\u308A\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u5165\u529B\u5F8C\u306B\u81EA\u52D5\u7684\u306B\u884C\u306E\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8\u3092\u884C\u3046\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u7E26\u306E\u30B0\u30EA\u30D5\u4F59\u767D\u304C\u8868\u793A\u3055\u308C\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u307B\u3068\u3093\u3069\u306E\u5834\u5408\u3001\u30B0\u30EA\u30D5\u4F59\u767D\u306F\u30C7\u30D0\u30C3\u30B0\u306B\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u3067\u30AB\u30FC\u30BD\u30EB\u3092\u975E\u8868\u793A\u306B\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30A4\u30F3\u30C7\u30F3\u30C8\u306E\u30AC\u30A4\u30C9\u3092\u5F37\u8ABF\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u6587\u5B57\u9593\u9694 (\u30D4\u30AF\u30BB\u30EB\u5358\u4F4D) \u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30EA\u30F3\u30AF\u3055\u308C\u305F\u7DE8\u96C6\u304C\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u6709\u52B9\u306B\u3055\u308C\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u8A00\u8A9E\u306B\u3088\u3063\u3066\u306F\u3001\u7DE8\u96C6\u4E2D\u306B HTML \u30BF\u30B0\u306A\u3069\u306E\u95A2\u9023\u3059\u308B\u8A18\u53F7\u304C\u66F4\u65B0\u3055\u308C\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u30EA\u30F3\u30AF\u3092\u691C\u51FA\u3057\u3066\u30AF\u30EA\u30C3\u30AF\u53EF\u80FD\u306A\u72B6\u614B\u306B\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5BFE\u5FDC\u3059\u308B\u304B\u3063\u3053\u3092\u5F37\u8ABF\u8868\u793A\u3057\u307E\u3059\u3002","\u30DE\u30A6\u30B9 \u30DB\u30A4\u30FC\u30EB \u30B9\u30AF\u30ED\u30FC\u30EB \u30A4\u30D9\u30F3\u30C8\u306E `deltaX` \u3068 `deltaY` \u3067\u4F7F\u7528\u3055\u308C\u308B\u4E57\u6570\u3002","`Ctrl` \u30AD\u30FC\u3092\u62BC\u3057\u306A\u304C\u3089\u30DE\u30A6\u30B9 \u30DB\u30A4\u30FC\u30EB\u3092\u4F7F\u7528\u3057\u3066\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30D5\u30A9\u30F3\u30C8\u3092\u30BA\u30FC\u30E0\u3057\u307E\u3059\u3002","\u8907\u6570\u306E\u30AB\u30FC\u30BD\u30EB\u304C\u91CD\u306A\u3063\u3066\u3044\u308B\u3068\u304D\u306F\u3001\u30DE\u30FC\u30B8\u3057\u307E\u3059\u3002","Windows \u304A\u3088\u3073 Linux \u4E0A\u306E `Control` \u30AD\u30FC\u3068 macOS \u4E0A\u306E `Command` \u30AD\u30FC\u306B\u5272\u308A\u5F53\u3066\u307E\u3059\u3002","Windows \u304A\u3088\u3073 Linux \u4E0A\u306E `Alt` \u30AD\u30FC\u3068 macOS \u4E0A\u306E `Option` \u30AD\u30FC\u306B\u5272\u308A\u5F53\u3066\u307E\u3059\u3002","\u30DE\u30A6\u30B9\u3092\u4F7F\u7528\u3057\u3066\u8907\u6570\u306E\u30AB\u30FC\u30BD\u30EB\u3092\u8FFD\u52A0\u3059\u308B\u3068\u304D\u306B\u4F7F\u7528\u3059\u308B\u4FEE\u98FE\u30AD\u30FC\u3067\u3059\u3002\u300C\u5B9A\u7FA9\u306B\u79FB\u52D5\u300D\u3084\u300C\u30EA\u30F3\u30AF\u3092\u958B\u304F\u300D\u306E\u30DE\u30A6\u30B9\u64CD\u4F5C\u306F\u3001\u30DE\u30EB\u30C1\u30AB\u30FC\u30BD\u30EB\u306E\u4FEE\u98FE\u30AD\u30FC\u3068\u7AF6\u5408\u3057\u306A\u3044\u3088\u3046\u306B\u9069\u7528\u3055\u308C\u307E\u3059\u3002[\u8A73\u7D30](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier)","\u30AB\u30FC\u30BD\u30EB\u3054\u3068\u306B\u30C6\u30AD\u30B9\u30C8\u3092 1 \u884C\u305A\u3064\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002","\u5404\u30AB\u30FC\u30BD\u30EB\u306F\u5168\u6587\u3092\u8CBC\u308A\u4ED8\u3051\u307E\u3059\u3002","\u8CBC\u308A\u4ED8\u3051\u305F\u30C6\u30AD\u30B9\u30C8\u306E\u884C\u6570\u304C\u30AB\u30FC\u30BD\u30EB\u6570\u3068\u4E00\u81F4\u3059\u308B\u5834\u5408\u306E\u8CBC\u308A\u4ED8\u3051\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30BB\u30DE\u30F3\u30C6\u30A3\u30C3\u30AF \u30B7\u30F3\u30DC\u30EB\u306E\u51FA\u73FE\u7B87\u6240\u3092\u5F37\u8ABF\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u5468\u56F2\u306B\u5883\u754C\u7DDA\u304C\u63CF\u753B\u3055\u308C\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30D4\u30FC\u30AF\u3092\u958B\u304F\u3068\u304D\u306B\u30C4\u30EA\u30FC\u306B\u30D5\u30A9\u30FC\u30AB\u30B9\u3059\u308B","\u30D4\u30FC\u30AF\u3092\u958B\u304F\u3068\u304D\u306B\u30A8\u30C7\u30A3\u30BF\u30FC\u306B\u30D5\u30A9\u30FC\u30AB\u30B9\u3059\u308B","\u30D4\u30FC\u30AF \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u30A4\u30F3\u30E9\u30A4\u30F3 \u30A8\u30C7\u30A3\u30BF\u30FC\u307E\u305F\u306F\u30C4\u30EA\u30FC\u3092\u30D5\u30A9\u30FC\u30AB\u30B9\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","[\u5B9A\u7FA9\u3078\u79FB\u52D5] \u30DE\u30A6\u30B9 \u30B8\u30A7\u30B9\u30C1\u30E3\u30FC\u3067\u3001\u5E38\u306B\u30D4\u30FC\u30AF \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u3092\u958B\u304F\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30AF\u30A4\u30C3\u30AF\u5019\u88DC\u304C\u8868\u793A\u3055\u308C\u308B\u307E\u3067\u306E\u30DF\u30EA\u79D2\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u306E\u578B\u306E\u81EA\u52D5\u540D\u524D\u5909\u66F4\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u975E\u63A8\u5968\u3067\u3059\u3002\u4EE3\u308F\u308A\u306B\u3001`editor.linkedEditing` \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u5236\u5FA1\u6587\u5B57\u3092\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30A4\u30F3\u30C7\u30F3\u30C8 \u30AC\u30A4\u30C9\u3092\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30D5\u30A1\u30A4\u30EB\u306E\u672B\u5C3E\u304C\u6539\u884C\u306E\u5834\u5408\u306F\u3001\u6700\u5F8C\u306E\u884C\u756A\u53F7\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u4F59\u767D\u3068\u73FE\u5728\u306E\u884C\u3092\u5F37\u8ABF\u8868\u793A\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u73FE\u5728\u306E\u884C\u3092\u3069\u306E\u3088\u3046\u306B\u5F37\u8ABF\u8868\u793A\u3059\u308B\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306B\u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308B\u5834\u5408\u306B\u306E\u307F\u73FE\u5728\u306E\u884C\u3092\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u5F37\u8ABF\u8868\u793A\u3059\u308B\u5FC5\u8981\u304C\u3042\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059","\u5358\u8A9E\u9593\u306E\u5358\u4E00\u30B9\u30DA\u30FC\u30B9\u4EE5\u5916\u306E\u7A7A\u767D\u6587\u5B57\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u9078\u629E\u3057\u305F\u30C6\u30AD\u30B9\u30C8\u306B\u306E\u307F\u7A7A\u767D\u6587\u5B57\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u672B\u5C3E\u306E\u7A7A\u767D\u6587\u5B57\u306E\u307F\u3092\u8868\u793A\u3059\u308B","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u7A7A\u767D\u6587\u5B57\u3092\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u9078\u629E\u7BC4\u56F2\u306E\u89D2\u3092\u4E38\u304F\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u6C34\u5E73\u65B9\u5411\u306B\u4F59\u5206\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u6587\u5B57\u6570\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u6700\u5F8C\u306E\u884C\u3092\u8D8A\u3048\u3066\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5782\u76F4\u304A\u3088\u3073\u6C34\u5E73\u65B9\u5411\u306E\u4E21\u65B9\u306B\u540C\u6642\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u5834\u5408\u306F\u3001\u4E3B\u8981\u306A\u8EF8\u306B\u6CBF\u3063\u3066\u30B9\u30AF\u30ED\u30FC\u30EB\u3057\u307E\u3059\u3002\u30C8\u30E9\u30C3\u30AF\u30D1\u30C3\u30C9\u4E0A\u3067\u5782\u76F4\u65B9\u5411\u306B\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u5834\u5408\u306F\u3001\u6C34\u5E73\u30C9\u30EA\u30D5\u30C8\u3092\u9632\u6B62\u3057\u307E\u3059\u3002","Linux \u306E PRIMARY \u30AF\u30EA\u30C3\u30D7\u30DC\u30FC\u30C9\u3092\u30B5\u30DD\u30FC\u30C8\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u9078\u629E\u9805\u76EE\u3068\u985E\u4F3C\u306E\u4E00\u81F4\u9805\u76EE\u3092\u5F37\u8ABF\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5E38\u306B\u6298\u308A\u305F\u305F\u307F\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u30DE\u30A6\u30B9\u304C\u3068\u3058\u3057\u308D\u306E\u4E0A\u306B\u3042\u308B\u3068\u304D\u306B\u306E\u307F\u3001\u6298\u308A\u305F\u305F\u307F\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u3068\u3058\u3057\u308D\u306E\u306E\u6298\u308A\u305F\u305F\u307F\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB\u3092\u8868\u793A\u3059\u308B\u30BF\u30A4\u30DF\u30F3\u30B0\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u4F7F\u7528\u3055\u308C\u3066\u3044\u306A\u3044\u30B3\u30FC\u30C9\u306E\u30D5\u30A7\u30FC\u30C9\u30A2\u30A6\u30C8\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u975E\u63A8\u5968\u306E\u5909\u6570\u306E\u53D6\u308A\u6D88\u3057\u7DDA\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u4ED6\u306E\u5019\u88DC\u306E\u4E0A\u306B\u30B9\u30CB\u30DA\u30C3\u30C8\u306E\u5019\u88DC\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u4ED6\u306E\u5019\u88DC\u306E\u4E0B\u306B\u30B9\u30CB\u30DA\u30C3\u30C8\u306E\u5019\u88DC\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u4ED6\u306E\u5019\u88DC\u3068\u4E00\u7DD2\u306B\u30B9\u30CB\u30DA\u30C3\u30C8\u306E\u5019\u88DC\u3092\u8868\u793A\u3057\u307E\u3059\u3002","\u30B9\u30CB\u30DA\u30C3\u30C8\u306E\u5019\u88DC\u3092\u8868\u793A\u3057\u307E\u305B\u3093\u3002","\u4ED6\u306E\u4FEE\u6B63\u5019\u88DC\u3068\u4E00\u7DD2\u306B\u30B9\u30CB\u30DA\u30C3\u30C8\u3092\u8868\u793A\u3059\u308B\u304B\u3069\u3046\u304B\u3001\u304A\u3088\u3073\u305D\u306E\u4E26\u3073\u66FF\u3048\u306E\u65B9\u6CD5\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A2\u30CB\u30E1\u30FC\u30B7\u30E7\u30F3\u3067\u30A8\u30C7\u30A3\u30BF\u30FC\u3092\u30B9\u30AF\u30ED\u30FC\u30EB\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5019\u88DC\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u30D5\u30A9\u30F3\u30C8 \u30B5\u30A4\u30BA\u3002`0` \u306B\u8A2D\u5B9A\u3059\u308B\u3068\u3001`#editor.fontSize#` \u306E\u5024\u304C\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u5019\u88DC\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u884C\u306E\u9AD8\u3055\u3002`0` \u306B\u8A2D\u5B9A\u3059\u308B\u3068\u3001`#editor.lineHeight#` \u306E\u5024\u304C\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002\u6700\u5C0F\u5024\u306F 8 \u3067\u3059\u3002","\u30C8\u30EA\u30AC\u30FC\u6587\u5B57\u306E\u5165\u529B\u6642\u306B\u5019\u88DC\u304C\u81EA\u52D5\u7684\u306B\u8868\u793A\u3055\u308C\u308B\u3088\u3046\u306B\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u5E38\u306B\u6700\u521D\u306E\u5019\u88DC\u3092\u9078\u629E\u3057\u307E\u3059\u3002","`console.| -> console.log` \u306A\u3069\u3068\u9078\u629E\u5BFE\u8C61\u306B\u95A2\u3057\u3066\u5165\u529B\u3057\u306A\u3044\u9650\u308A\u306F\u3001\u6700\u8FD1\u306E\u5019\u88DC\u3092\u9078\u629E\u3057\u307E\u3059\u3002`log` \u306F\u6700\u8FD1\u5B8C\u4E86\u3057\u305F\u305F\u3081\u3067\u3059\u3002","\u3053\u308C\u3089\u306E\u5019\u88DC\u3092\u5B8C\u4E86\u3057\u305F\u4EE5\u524D\u306E\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u306B\u57FA\u3065\u3044\u3066\u5019\u88DC\u3092\u9078\u629E\u3057\u307E\u3059\u3002\u4F8B: `co -> console` \u304A\u3088\u3073 `con -> const`\u3002","\u5019\u88DC\u30EA\u30B9\u30C8\u3092\u8868\u793A\u3059\u308B\u3068\u304D\u306B\u5019\u88DC\u3092\u4E8B\u524D\u306B\u9078\u629E\u3059\u308B\u65B9\u6CD5\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30BF\u30D6\u88DC\u5B8C\u306F\u3001tab \u30AD\u30FC\u3092\u62BC\u3057\u305F\u3068\u304D\u306B\u6700\u9069\u306A\u5019\u88DC\u3092\u633F\u5165\u3057\u307E\u3059\u3002","\u30BF\u30D6\u88DC\u5B8C\u3092\u7121\u52B9\u306B\u3057\u307E\u3059\u3002","\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u304C\u4E00\u81F4\u3059\u308B\u5834\u5408\u306B\u3001\u30BF\u30D6\u3067\u30B9\u30CB\u30DA\u30C3\u30C8\u3092\u88DC\u5B8C\u3057\u307E\u3059\u3002'quickSuggestions' \u304C\u7121\u52B9\u306A\u5834\u5408\u306B\u6700\u9069\u3067\u3059\u3002","\u30BF\u30D6\u88DC\u5B8C\u3092\u6709\u52B9\u306B\u3057\u307E\u3059\u3002","\u901A\u5E38\u3068\u306F\u7570\u306A\u308B\u884C\u306E\u7D42\u7AEF\u6587\u5B57\u306F\u81EA\u52D5\u7684\u306B\u524A\u9664\u3055\u308C\u308B\u3002","\u901A\u5E38\u3068\u306F\u7570\u306A\u308B\u884C\u306E\u7D42\u7AEF\u6587\u5B57\u306F\u7121\u8996\u3055\u308C\u308B\u3002","\u901A\u5E38\u3068\u306F\u7570\u306A\u308B\u884C\u306E\u7D42\u7AEF\u6587\u5B57\u306E\u524A\u9664\u30D7\u30ED\u30F3\u30D7\u30C8\u304C\u8868\u793A\u3055\u308C\u308B\u3002","\u554F\u984C\u3092\u8D77\u3053\u3059\u53EF\u80FD\u6027\u304C\u3042\u308B\u3001\u666E\u901A\u3067\u306F\u306A\u3044\u884C\u7D42\u7AEF\u8A18\u53F7\u306F\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u7A7A\u767D\u306E\u633F\u5165\u3084\u524A\u9664\u306F\u30BF\u30D6\u4F4D\u7F6E\u306B\u5F93\u3063\u3066\u884C\u308F\u308C\u307E\u3059\u3002","\u5358\u8A9E\u306B\u95A2\u9023\u3057\u305F\u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3\u307E\u305F\u306F\u64CD\u4F5C\u3092\u5B9F\u884C\u3059\u308B\u3068\u304D\u306B\u3001\u5358\u8A9E\u306E\u533A\u5207\u308A\u6587\u5B57\u3068\u3057\u3066\u4F7F\u7528\u3055\u308C\u308B\u6587\u5B57\u3002","\u884C\u3092\u6298\u308A\u8FD4\u3057\u307E\u305B\u3093\u3002","\u884C\u3092\u30D3\u30E5\u30FC\u30DD\u30FC\u30C8\u306E\u5E45\u3067\u6298\u308A\u8FD4\u3057\u307E\u3059\u3002","`#editor.wordWrapColumn#` \u3067\u884C\u3092\u6298\u308A\u8FD4\u3057\u307E\u3059\u3002","\u30D3\u30E5\u30FC\u30DD\u30FC\u30C8\u3068 `#editor.wordWrapColumn#` \u306E\u6700\u5C0F\u5024\u3067\u884C\u3092\u6298\u308A\u8FD4\u3057\u307E\u3059\u3002","\u884C\u306E\u6298\u308A\u8FD4\u3057\u65B9\u6CD5\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","`#editor.wordWrap#` \u304C `wordWrapColumn` \u307E\u305F\u306F `bounded` \u306E\u5834\u5408\u306B\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u6298\u308A\u8FD4\u3057\u6841\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30A4\u30F3\u30C7\u30F3\u30C8\u3057\u307E\u305B\u3093\u3002 \u6298\u308A\u8FD4\u3057\u884C\u306F\u5217 1 \u304B\u3089\u59CB\u307E\u308A\u307E\u3059\u3002","\u6298\u308A\u8FD4\u3057\u884C\u306F\u3001\u89AA\u3068\u540C\u3058\u30A4\u30F3\u30C7\u30F3\u30C8\u306B\u306A\u308A\u307E\u3059\u3002","\u6298\u308A\u8FD4\u3057\u884C\u306F\u3001\u89AA +1 \u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u306B\u306A\u308A\u307E\u3059\u3002","\u6298\u308A\u8FD4\u3057\u884C\u306F\u3001\u89AA +2 \u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u306B\u306A\u308A\u307E\u3059\u3002","\u6298\u308A\u8FD4\u3057\u884C\u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u3059\u3079\u3066\u306E\u6587\u5B57\u306E\u5E45\u304C\u540C\u3058\u3067\u3042\u308B\u3068\u4EEE\u5B9A\u3057\u307E\u3059\u3002\u3053\u308C\u306F\u3001\u30E2\u30CE\u30B9\u30DA\u30FC\u30B9 \u30D5\u30A9\u30F3\u30C8\u3084\u3001\u30B0\u30EA\u30D5\u306E\u5E45\u304C\u7B49\u3057\u3044\u7279\u5B9A\u306E\u30B9\u30AF\u30EA\u30D7\u30C8 (\u30E9\u30C6\u30F3\u6587\u5B57\u306A\u3069) \u3067\u6B63\u3057\u304F\u52D5\u4F5C\u3059\u308B\u9AD8\u901F\u30A2\u30EB\u30B4\u30EA\u30BA\u30E0\u3067\u3059\u3002","\u6298\u308A\u8FD4\u3057\u30DD\u30A4\u30F3\u30C8\u306E\u8A08\u7B97\u3092\u30D6\u30E9\u30A6\u30B6\u30FC\u306B\u30C7\u30EA\u30B2\u30FC\u30C8\u3057\u307E\u3059\u3002\u3053\u308C\u306F\u3001\u5927\u304D\u306A\u30D5\u30A1\u30A4\u30EB\u306E\u30D5\u30EA\u30FC\u30BA\u3092\u5F15\u304D\u8D77\u3053\u3059\u53EF\u80FD\u6027\u304C\u3042\u308B\u3082\u306E\u306E\u3001\u3059\u3079\u3066\u306E\u30B1\u30FC\u30B9\u3067\u6B63\u3057\u304F\u52D5\u4F5C\u3059\u308B\u4F4E\u901F\u306A\u30A2\u30EB\u30B4\u30EA\u30BA\u30E0\u3067\u3059\u3002","\u6298\u308A\u8FD4\u3057\u30DD\u30A4\u30F3\u30C8\u3092\u8A08\u7B97\u3059\u308B\u30A2\u30EB\u30B4\u30EA\u30BA\u30E0\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002"],"vs/editor/common/editorContextKeys":["Whether the editor text has focus (cursor is blinking)","Whether the editor or an editor widget has focus (e.g. focus is in the find widget)","Whether an editor or a rich text input has focus (cursor is blinking)","Whether the editor is read only","Whether the context is a diff editor","Whether `editor.columnSelection` is enabled","Whether the editor has text selected","Whether the editor has multiple selections","Whether `Tab` will move focus out of the editor","Whether the editor hover is visible","Whether the editor is part of a larger editor (e.g. notebooks)","The language identifier of the editor","Whether the editor has a completion item provider","Whether the editor has a code actions provider","Whether the editor has a code lens provider","Whether the editor has a definition provider","Whether the editor has a declaration provider","Whether the editor has an implementation provider","Whether the editor has a type definition provider","Whether the editor has a hover provider","Whether the editor has a document highlight provider","Whether the editor has a document symbol provider","Whether the editor has a reference provider","Whether the editor has a rename provider","Whether the editor has a signature help provider","Whether the editor has an inline hints provider","Whether the editor has a document formatting provider","Whether the editor has a document selection formatting provider","Whether the editor has multiple document formatting providers","Whether the editor has multiple document selection formatting providers"],"vs/editor/common/model/editStack":["\u5165\u529B\u3057\u3066\u3044\u307E\u3059"],"vs/editor/common/modes/modesRegistry":["\u30D7\u30EC\u30FC\u30F3\u30C6\u30AD\u30B9\u30C8"],"vs/editor/common/standaloneStrings":["\u9078\u629E\u3055\u308C\u3066\u3044\u307E\u305B\u3093","\u884C {0}\u3001\u5217 {1} ({2} \u500B\u9078\u629E\u6E08\u307F)","\u884C {0}\u3001\u5217 {1}","{0} \u500B\u306E\u9078\u629E\u9805\u76EE ({1} \u6587\u5B57\u3092\u9078\u629E)","{0} \u500B\u306E\u9078\u629E\u9805\u76EE","`accessibilitySupport` \u8A2D\u5B9A\u3092 'on' \u306B\u5909\u66F4\u3057\u3066\u3044\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u306B\u95A2\u9023\u3059\u308B\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8 \u30DA\u30FC\u30B8\u3092\u958B\u3044\u3066\u3044\u307E\u3059\u3002","\u5DEE\u5206\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u8AAD\u307F\u53D6\u308A\u5C02\u7528\u30A6\u30A3\u30F3\u30C9\u30A6\u5185\u3002","\u5DEE\u5206\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30A6\u30A3\u30F3\u30C9\u30A6\u5185\u3002","\u8AAD\u307F\u53D6\u308A\u5C02\u7528\u30B3\u30FC\u30C9 \u30A8\u30C7\u30A3\u30BF\u30FC\u5185","\u30B3\u30FC\u30C9 \u30A8\u30C7\u30A3\u30BF\u30FC\u5185","\u30A8\u30C7\u30A3\u30BF\u30FC\u3092\u69CB\u6210\u3057\u3066\u30B9\u30AF\u30EA\u30FC\u30F3 \u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u4F7F\u7528\u3059\u308B\u3088\u3046\u306B\u6700\u9069\u5316\u3059\u308B\u306B\u306F\u3001Command+E \u3092\u62BC\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3092\u69CB\u6210\u3057\u3066\u30B9\u30AF\u30EA\u30FC\u30F3 \u30EA\u30FC\u30C0\u30FC\u3067\u4F7F\u7528\u3059\u308B\u3088\u3046\u306B\u6700\u9069\u5316\u3059\u308B\u306B\u306F\u3001Control+E \u3092\u62BC\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u3001\u30B9\u30AF\u30EA\u30FC\u30F3 \u30EA\u30FC\u30C0\u30FC\u3067\u4F7F\u7528\u3059\u308B\u3088\u3046\u6700\u9069\u5316\u3055\u308C\u308B\u3088\u3046\u306B\u69CB\u6210\u3055\u308C\u3066\u3044\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u3001\u30B9\u30AF\u30EA\u30FC\u30F3 \u30EA\u30FC\u30C0\u30FC\u3067\u4F7F\u7528\u3059\u308B\u3088\u3046\u6700\u9069\u5316\u3055\u308C\u306A\u3044\u3088\u3046\u306B\u69CB\u6210\u3055\u308C\u3066\u3044\u307E\u3059\u304C\u3001\u73FE\u6642\u70B9\u3067\u3053\u306E\u8A2D\u5B9A\u306F\u5F53\u3066\u306F\u307E\u308A\u307E\u305B\u3093\u3002","\u73FE\u5728\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u3067 Tab \u30AD\u30FC\u3092\u62BC\u3059\u3068\u3001\u6B21\u306E\u30D5\u30A9\u30FC\u30AB\u30B9\u53EF\u80FD\u306A\u8981\u7D20\u306B\u30D5\u30A9\u30FC\u30AB\u30B9\u3092\u79FB\u52D5\u3057\u307E\u3059\u3002{0} \u3092\u62BC\u3059\u3068\u3001\u3053\u306E\u52D5\u4F5C\u304C\u5207\u308A\u66FF\u308F\u308A\u307E\u3059\u3002","\u73FE\u5728\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u3067 Tab \u30AD\u30FC\u3092\u62BC\u3059\u3068\u3001\u6B21\u306E\u30D5\u30A9\u30FC\u30AB\u30B9\u53EF\u80FD\u306A\u8981\u7D20\u306B\u30D5\u30A9\u30FC\u30AB\u30B9\u3092\u79FB\u52D5\u3057\u307E\u3059\u3002\u30B3\u30DE\u30F3\u30C9 {0} \u306F\u3001\u30AD\u30FC \u30D0\u30A4\u30F3\u30C9\u3067\u306F\u73FE\u5728\u30C8\u30EA\u30AC\u30FC\u3067\u304D\u307E\u305B\u3093\u3002","\u73FE\u5728\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u3067 Tab \u30AD\u30FC\u3092\u62BC\u3059\u3068\u3001\u30BF\u30D6\u6587\u5B57\u304C\u633F\u5165\u3055\u308C\u307E\u3059\u3002{0} \u3092\u62BC\u3059\u3068\u3001\u3053\u306E\u52D5\u4F5C\u304C\u5207\u308A\u66FF\u308F\u308A\u307E\u3059\u3002","\u73FE\u5728\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u3067 Tab \u30AD\u30FC\u3092\u62BC\u3059\u3068\u3001\u30BF\u30D6\u6587\u5B57\u304C\u633F\u5165\u3055\u308C\u307E\u3059\u3002\u30B3\u30DE\u30F3\u30C9 {0} \u306F\u3001\u30AD\u30FC \u30D0\u30A4\u30F3\u30C9\u3067\u306F\u73FE\u5728\u30C8\u30EA\u30AC\u30FC\u3067\u304D\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u306B\u95A2\u3059\u308B\u8A73\u7D30\u60C5\u5831\u304C\u8A18\u3055\u308C\u305F\u30D6\u30E9\u30A6\u30B6\u30FC \u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304F\u306B\u306F\u3001Command+H \u3092\u62BC\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u306B\u95A2\u3059\u308B\u8A73\u7D30\u60C5\u5831\u304C\u8A18\u3055\u308C\u305F\u30D6\u30E9\u30A6\u30B6\u30FC \u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304F\u306B\u306F\u3001Control+H \u3092\u62BC\u3057\u3066\u304F\u3060\u3055\u3044\u3002","Esc \u30AD\u30FC \u304B Shift+Esc \u3092\u62BC\u3059\u3068\u3001\u30D2\u30F3\u30C8\u3092\u6D88\u3057\u3066\u30A8\u30C7\u30A3\u30BF\u30FC\u306B\u623B\u308B\u3053\u3068\u304C\u3067\u304D\u307E\u3059\u3002","\u30A2\u30AF\u30BB\u30B7\u30D3\u30EA\u30C6\u30A3\u306E\u30D8\u30EB\u30D7\u3092\u8868\u793A\u3057\u307E\u3059","\u958B\u767A\u8005: \u30C8\u30FC\u30AF\u30F3\u306E\u691C\u67FB","\u884C/\u5217\u306B\u79FB\u52D5\u3059\u308B...","\u3059\u3079\u3066\u306E\u30AF\u30A4\u30C3\u30AF \u30A2\u30AF\u30BB\u30B9 \u30D7\u30ED\u30D0\u30A4\u30C0\u30FC\u3092\u8868\u793A","\u30B3\u30DE\u30F3\u30C9 \u30D1\u30EC\u30C3\u30C8","\u30B3\u30DE\u30F3\u30C9\u306E\u8868\u793A\u3068\u5B9F\u884C","\u30B7\u30F3\u30DC\u30EB\u306B\u79FB\u52D5...","\u30AB\u30C6\u30B4\u30EA\u5225\u306E\u30B7\u30F3\u30DC\u30EB\u3078\u79FB\u52D5...","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30B3\u30F3\u30C6\u30F3\u30C4","\u30A2\u30AF\u30C6\u30A3\u30D3\u30C6\u30A3 \u30AA\u30D7\u30B7\u30E7\u30F3\u3092\u8868\u793A\u3059\u308B\u306B\u306F\u3001Alt+F1 \u30AD\u30FC\u3092\u62BC\u3057\u307E\u3059\u3002","\u30CF\u30A4 \u30B3\u30F3\u30C8\u30E9\u30B9\u30C8 \u30C6\u30FC\u30DE\u306E\u5207\u308A\u66FF\u3048","{1} \u500B\u306E\u30D5\u30A1\u30A4\u30EB\u306B {0} \u500B\u306E\u7DE8\u96C6\u304C\u884C\u308F\u308C\u307E\u3057\u305F"],"vs/editor/common/view/editorColorRegistry":["\u30AB\u30FC\u30BD\u30EB\u4F4D\u7F6E\u306E\u884C\u3092\u5F37\u8ABF\u8868\u793A\u3059\u308B\u80CC\u666F\u8272\u3002","\u30AB\u30FC\u30BD\u30EB\u4F4D\u7F6E\u306E\u884C\u306E\u5883\u754C\u7DDA\u3092\u5F37\u8ABF\u8868\u793A\u3059\u308B\u80CC\u666F\u8272\u3002","(Quick Open \u3084\u691C\u51FA\u6A5F\u80FD\u306A\u3069\u306B\u3088\u308A) \u5F37\u8ABF\u8868\u793A\u3055\u308C\u3066\u3044\u308B\u7BC4\u56F2\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u5F37\u8ABF\u8868\u793A\u3055\u308C\u305F\u7BC4\u56F2\u306E\u5883\u754C\u7DDA\u306E\u80CC\u666F\u8272\u3002","\u5F37\u8ABF\u8868\u793A\u3055\u308C\u305F\u8A18\u53F7\u306E\u80CC\u666F\u8272 (\u5B9A\u7FA9\u3078\u79FB\u52D5\u3001\u6B21\u307E\u305F\u306F\u524D\u306E\u8A18\u53F7\u3078\u79FB\u52D5\u306A\u3069)\u3002\u57FA\u306B\u306A\u308B\u88C5\u98FE\u304C\u8986\u308F\u308C\u306A\u3044\u3088\u3046\u306B\u3059\u308B\u305F\u3081\u3001\u8272\u3092\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u5F37\u8ABF\u8868\u793A\u3055\u308C\u305F\u8A18\u53F7\u306E\u5468\u308A\u306E\u5883\u754C\u7DDA\u306E\u80CC\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30AB\u30FC\u30BD\u30EB\u306E\u8272\u3002","\u9078\u629E\u3055\u308C\u305F\u6587\u5B57\u5217\u306E\u80CC\u666F\u8272\u3067\u3059\u3002\u9078\u629E\u3055\u308C\u305F\u6587\u5B57\u5217\u306E\u80CC\u666F\u8272\u3092\u30AB\u30B9\u30BF\u30DE\u30A4\u30BA\u51FA\u6765\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30B9\u30DA\u30FC\u30B9\u6587\u5B57\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC \u30A4\u30F3\u30C7\u30F3\u30C8 \u30AC\u30A4\u30C9\u306E\u8272\u3002","\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30A4\u30F3\u30C7\u30F3\u30C8 \u30AC\u30A4\u30C9\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u884C\u756A\u53F7\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30A2\u30AF\u30C6\u30A3\u30D6\u884C\u756A\u53F7\u306E\u8272","id \u306F\u4F7F\u7528\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002\u4EE3\u308F\u308A\u306B 'EditorLineNumber.activeForeground' \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30A2\u30AF\u30C6\u30A3\u30D6\u884C\u756A\u53F7\u306E\u8272","\u30A8\u30C7\u30A3\u30BF\u30FC \u30EB\u30FC\u30E9\u30FC\u306E\u8272\u3002","CodeLens \u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u524D\u666F\u8272\u3002","\u4E00\u81F4\u3059\u308B\u304B\u3063\u3053\u306E\u80CC\u666F\u8272","\u4E00\u81F4\u3059\u308B\u304B\u3063\u3053\u5185\u306E\u30DC\u30C3\u30AF\u30B9\u306E\u8272","\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u5883\u754C\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u80CC\u666F\u8272\u3067\u3059\u3002\u30DF\u30CB\u30DE\u30C3\u30D7\u304C\u6709\u52B9\u3067\u3001\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u53F3\u5074\u306B\u914D\u7F6E\u3055\u308C\u3066\u3044\u308B\u5834\u5408\u306B\u306E\u307F\u4F7F\u7528\u3057\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u4F59\u767D\u306E\u80CC\u666F\u8272\u3002\u4F59\u767D\u306B\u306F\u30B0\u30EA\u30D5 \u30DE\u30FC\u30B8\u30F3\u3068\u884C\u756A\u53F7\u304C\u542B\u307E\u308C\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u306E\u4E0D\u8981\u306A (\u672A\u4F7F\u7528\u306E) \u30BD\u30FC\u30B9 \u30B3\u30FC\u30C9\u306E\u7F6B\u7DDA\u306E\u8272\u3002",`\u30A8\u30C7\u30A3\u30BF\u30FC\u5185\u306E\u4E0D\u8981\u306A (\u672A\u4F7F\u7528\u306E) \u30BD\u30FC\u30B9 \u30B3\u30FC\u30C9\u306E\u4E0D\u900F\u660E\u5EA6\u3002\u305F\u3068\u3048\u3070\u3001"#000000c0" \u306F\u4E0D\u900F\u660E\u5EA6 75% \u3067\u30B3\u30FC\u30C9\u3092\u8868\u793A\u3057\u307E\u3059\u3002\u30CF\u30A4 \u30B3\u30F3\u30C8\u30E9\u30B9\u30C8\u306E\u30C6\u30FC\u30DE\u306E\u5834\u5408\u3001'editorUnnecessaryCode.border' \u30C6\u30FC\u30DE\u8272\u3092\u4F7F\u7528\u3057\u3066\u3001\u4E0D\u8981\u306A\u30B3\u30FC\u30C9\u3092\u30D5\u30A7\u30FC\u30C9\u30A2\u30A6\u30C8\u3059\u308B\u306E\u3067\u306F\u306A\u304F\u4E0B\u7DDA\u3092\u4ED8\u3051\u307E\u3059\u3002`,"\u7BC4\u56F2\u5F37\u8ABF\u8868\u793A\u306E\u305F\u3081\u306E\u6982\u8981\u30EB\u30FC\u30E9\u30FC \u30DE\u30FC\u30AB\u30FC\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u30A8\u30E9\u30FC\u3092\u793A\u3059\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u30DE\u30FC\u30AB\u30FC\u8272\u3002","\u8B66\u544A\u3092\u793A\u3059\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u30DE\u30FC\u30AB\u30FC\u8272\u3002","\u60C5\u5831\u3092\u793A\u3059\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u30DE\u30FC\u30AB\u30FC\u8272\u3002"],"vs/editor/contrib/anchorSelect/anchorSelect":["\u9078\u629E\u30A2\u30F3\u30AB\u30FC","\u30A2\u30F3\u30AB\u30FC\u304C {0}:{1} \u306B\u8A2D\u5B9A\u3055\u308C\u307E\u3057\u305F","\u9078\u629E\u30A2\u30F3\u30AB\u30FC\u306E\u8A2D\u5B9A","\u9078\u629E\u30A2\u30F3\u30AB\u30FC\u3078\u79FB\u52D5","\u30A2\u30F3\u30AB\u30FC\u304B\u3089\u30AB\u30FC\u30BD\u30EB\u3078\u9078\u629E","\u9078\u629E\u30A2\u30F3\u30AB\u30FC\u306E\u53D6\u308A\u6D88\u3057"],"vs/editor/contrib/bracketMatching/bracketMatching":["\u4E00\u81F4\u3059\u308B\u30D6\u30E9\u30B1\u30C3\u30C8\u3092\u793A\u3059\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u30DE\u30FC\u30AB\u30FC\u8272\u3002","\u30D6\u30E9\u30B1\u30C3\u30C8\u3078\u79FB\u52D5","\u30D6\u30E9\u30B1\u30C3\u30C8\u306B\u9078\u629E","\u30D6\u30E9\u30B1\u30C3\u30C8\u306B\u79FB\u52D5(&&B)"],"vs/editor/contrib/caretOperations/caretOperations":["\u9078\u629E\u3057\u305F\u30C6\u30AD\u30B9\u30C8\u3092\u5DE6\u306B\u79FB\u52D5","\u9078\u629E\u3057\u305F\u30C6\u30AD\u30B9\u30C8\u3092\u53F3\u306B\u79FB\u52D5"],"vs/editor/contrib/caretOperations/transpose":["\u6587\u5B57\u306E\u5165\u308C\u66FF\u3048"],"vs/editor/contrib/clipboard/clipboard":["\u5207\u308A\u53D6\u308A(&&T)","\u5207\u308A\u53D6\u308A","\u5207\u308A\u53D6\u308A","\u30B3\u30D4\u30FC(&&C)","\u30B3\u30D4\u30FC","\u30B3\u30D4\u30FC","\u8CBC\u308A\u4ED8\u3051(&&P)","\u8CBC\u308A\u4ED8\u3051","\u8CBC\u308A\u4ED8\u3051","\u69CB\u6587\u3092\u5F37\u8ABF\u8868\u793A\u3057\u3066\u30B3\u30D4\u30FC"],"vs/editor/contrib/codeAction/codeActionCommands":["\u5B9F\u884C\u3059\u308B\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u306E\u7A2E\u985E\u3002","\u8FD4\u3055\u308C\u305F\u30A2\u30AF\u30B7\u30E7\u30F3\u304C\u9069\u7528\u3055\u308C\u308B\u30BF\u30A4\u30DF\u30F3\u30B0\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u6700\u521D\u306B\u8FD4\u3055\u308C\u305F\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u3092\u5E38\u306B\u9069\u7528\u3057\u307E\u3059\u3002","\u6700\u521D\u306B\u8FD4\u3055\u308C\u305F\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u4EE5\u5916\u306B\u8FD4\u3055\u308C\u305F\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u304C\u306A\u3044\u5834\u5408\u306F\u3001\u305D\u306E\u30A2\u30AF\u30B7\u30E7\u30F3\u3092\u9069\u7528\u3057\u307E\u3059\u3002","\u8FD4\u3055\u308C\u305F\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u306F\u9069\u7528\u3057\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002","\u512A\u5148\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u306E\u307F\u3092\u8FD4\u3059\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u306E\u9069\u7528\u4E2D\u306B\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F","\u30AF\u30A4\u30C3\u30AF \u30D5\u30A3\u30C3\u30AF\u30B9...","\u5229\u7528\u53EF\u80FD\u306A\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u306F\u3042\u308A\u307E\u305B\u3093","'{0}' \u306B\u5BFE\u3057\u3066\u4F7F\u7528\u3067\u304D\u308B\u512A\u5148\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u304C\u3042\u308A\u307E\u305B\u3093","{0}' \u306B\u5BFE\u3057\u3066\u4F7F\u7528\u3067\u304D\u308B\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u304C\u3042\u308A\u307E\u305B\u3093","\u4F7F\u7528\u3067\u304D\u308B\u512A\u5148\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u304C\u3042\u308A\u307E\u305B\u3093","\u5229\u7528\u53EF\u80FD\u306A\u30B3\u30FC\u30C9 \u30A2\u30AF\u30B7\u30E7\u30F3\u306F\u3042\u308A\u307E\u305B\u3093","\u30EA\u30D5\u30A1\u30AF\u30BF\u30FC...","'{0}' \u306B\u5BFE\u3057\u3066\u4F7F\u7528\u3067\u304D\u308B\u512A\u5148\u30EA\u30D5\u30A1\u30AF\u30BF\u30EA\u30F3\u30B0\u304C\u3042\u308A\u307E\u305B\u3093","'{0}' \u306B\u5BFE\u3057\u3066\u4F7F\u7528\u3067\u304D\u308B\u30EA\u30D5\u30A1\u30AF\u30BF\u30EA\u30F3\u30B0\u304C\u3042\u308A\u307E\u305B\u3093","\u4F7F\u7528\u3067\u304D\u308B\u512A\u5148\u30EA\u30D5\u30A1\u30AF\u30BF\u30EA\u30F3\u30B0\u304C\u3042\u308A\u307E\u305B\u3093","\u5229\u7528\u53EF\u80FD\u306A\u30EA\u30D5\u30A1\u30AF\u30BF\u30EA\u30F3\u30B0\u306F\u3042\u308A\u307E\u305B\u3093","\u30BD\u30FC\u30B9 \u30A2\u30AF\u30B7\u30E7\u30F3...","'{0}' \u306B\u5BFE\u3057\u3066\u4F7F\u7528\u3067\u304D\u308B\u512A\u5148\u30BD\u30FC\u30B9 \u30A2\u30AF\u30B7\u30E7\u30F3\u304C\u3042\u308A\u307E\u305B\u3093","'{0}' \u306B\u5BFE\u3057\u3066\u4F7F\u7528\u3067\u304D\u308B\u30BD\u30FC\u30B9 \u30A2\u30AF\u30B7\u30E7\u30F3\u304C\u3042\u308A\u307E\u305B\u3093","\u4F7F\u7528\u3067\u304D\u308B\u512A\u5148\u30BD\u30FC\u30B9 \u30A2\u30AF\u30B7\u30E7\u30F3\u304C\u3042\u308A\u307E\u305B\u3093","\u5229\u7528\u53EF\u80FD\u306A\u30BD\u30FC\u30B9 \u30A2\u30AF\u30B7\u30E7\u30F3\u306F\u3042\u308A\u307E\u305B\u3093","\u30A4\u30F3\u30DD\u30FC\u30C8\u3092\u6574\u7406","\u5229\u7528\u53EF\u80FD\u306A\u30A4\u30F3\u30DD\u30FC\u30C8\u306E\u6574\u7406\u30A2\u30AF\u30B7\u30E7\u30F3\u306F\u3042\u308A\u307E\u305B\u3093","\u3059\u3079\u3066\u4FEE\u6B63","\u3059\u3079\u3066\u3092\u4FEE\u6B63\u3059\u308B\u30A2\u30AF\u30B7\u30E7\u30F3\u306F\u5229\u7528\u3067\u304D\u307E\u305B\u3093","\u81EA\u52D5\u4FEE\u6B63...","\u5229\u7528\u53EF\u80FD\u306A\u81EA\u52D5\u4FEE\u6B63\u306F\u3042\u308A\u307E\u305B\u3093"],"vs/editor/contrib/codeAction/lightBulbWidget":["\u4FEE\u6B63\u30D7\u30ED\u30B0\u30E9\u30E0\u3092\u8868\u793A\u3057\u307E\u3059\u3002\u63A8\u5968\u3055\u308C\u308B\u5229\u7528\u53EF\u80FD\u306A\u4FEE\u6B63\u30D7\u30ED\u30B0\u30E9\u30E0 ({0})","\u4FEE\u6B63\u30D7\u30ED\u30B0\u30E9\u30E0 ({0}) \u3092\u8868\u793A\u3059\u308B","\u4FEE\u6B63\u30D7\u30ED\u30B0\u30E9\u30E0\u3092\u8868\u793A\u3059\u308B"],"vs/editor/contrib/codelens/codelensController":["\u73FE\u5728\u306E\u884C\u306E\u30B3\u30FC\u30C9 \u30EC\u30F3\u30BA \u30B3\u30DE\u30F3\u30C9\u3092\u8868\u793A"],"vs/editor/contrib/comment/comment":["\u884C\u30B3\u30E1\u30F3\u30C8\u306E\u5207\u308A\u66FF\u3048","\u884C\u30B3\u30E1\u30F3\u30C8\u306E\u5207\u308A\u66FF\u3048(&&T)","\u884C\u30B3\u30E1\u30F3\u30C8\u306E\u8FFD\u52A0","\u884C\u30B3\u30E1\u30F3\u30C8\u306E\u524A\u9664","\u30D6\u30ED\u30C3\u30AF \u30B3\u30E1\u30F3\u30C8\u306E\u5207\u308A\u66FF\u3048","\u30D6\u30ED\u30C3\u30AF \u30B3\u30E1\u30F3\u30C8\u306E\u5207\u308A\u66FF\u3048(&&B)"],"vs/editor/contrib/contextmenu/contextmenu":["\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8 \u30E1\u30CB\u30E5\u30FC\u306E\u8868\u793A"],"vs/editor/contrib/cursorUndo/cursorUndo":["\u30AB\u30FC\u30BD\u30EB\u3092\u5143\u306B\u623B\u3059","\u30AB\u30FC\u30BD\u30EB\u306E\u3084\u308A\u76F4\u3057"],"vs/editor/contrib/find/findController":["\u691C\u7D22","\u691C\u7D22(&&F)","\u9078\u629E\u7BC4\u56F2\u3067\u691C\u7D22","\u6B21\u3092\u691C\u7D22","\u6B21\u3092\u691C\u7D22","\u524D\u3092\u691C\u7D22","\u524D\u3092\u691C\u7D22","\u6B21\u306E\u9078\u629E\u9805\u76EE\u3092\u691C\u7D22","\u524D\u306E\u9078\u629E\u9805\u76EE\u3092\u691C\u7D22","\u7F6E\u63DB","\u7F6E\u63DB(&&R)"],"vs/editor/contrib/find/findWidget":["\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u5185\u306E '\u9078\u629E\u7BC4\u56F2\u3092\u691C\u7D22' \u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u304C\u6298\u308A\u305F\u305F\u307E\u308C\u3066\u3044\u308B\u3053\u3068\u3092\u793A\u3059\u30A2\u30A4\u30B3\u30F3\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u304C\u5C55\u958B\u3055\u308C\u3066\u3044\u308B\u3053\u3068\u3092\u793A\u3059\u30A2\u30A4\u30B3\u30F3\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u5185\u306E '\u7F6E\u63DB' \u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u5185\u306E '\u3059\u3079\u3066\u7F6E\u63DB' \u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u5185\u306E '\u524D\u3092\u691C\u7D22' \u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u691C\u7D22\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u5185\u306E '\u6B21\u3092\u691C\u7D22' \u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u691C\u7D22","\u691C\u7D22","\u524D\u306E\u691C\u7D22\u7D50\u679C","\u6B21\u306E\u4E00\u81F4\u9805\u76EE","\u9078\u629E\u7BC4\u56F2\u3092\u691C\u7D22","\u9589\u3058\u308B","\u7F6E\u63DB","\u7F6E\u63DB","\u7F6E\u63DB","\u3059\u3079\u3066\u7F6E\u63DB","\u7F6E\u63DB\u30E2\u30FC\u30C9\u306E\u5207\u308A\u66FF\u3048","\u6700\u521D\u306E {0} \u4EF6\u306E\u7D50\u679C\u3060\u3051\u304C\u5F37\u8ABF\u8868\u793A\u3055\u308C\u307E\u3059\u304C\u3001\u3059\u3079\u3066\u306E\u691C\u7D22\u64CD\u4F5C\u306F\u30C6\u30AD\u30B9\u30C8\u5168\u4F53\u3067\u6A5F\u80FD\u3057\u307E\u3059\u3002","{0} / {1} \u4EF6","\u7D50\u679C\u306F\u3042\u308A\u307E\u305B\u3093\u3002","{0} \u304C\u898B\u3064\u304B\u308A\u307E\u3057\u305F","{0} \u304C '{1}' \u3067\u898B\u3064\u304B\u308A\u307E\u3057\u305F","{0} \u306F '{1}' \u3067 {2} \u306B\u898B\u3064\u304B\u308A\u307E\u3057\u305F","{0} \u304C '{1}' \u3067\u898B\u3064\u304B\u308A\u307E\u3057\u305F","Ctrl + Enter \u30AD\u30FC\u3092\u62BC\u3059\u3068\u3001\u3059\u3079\u3066\u7F6E\u63DB\u3059\u308B\u306E\u3067\u306F\u306A\u304F\u3001\u6539\u884C\u304C\u633F\u5165\u3055\u308C\u308B\u3088\u3046\u306B\u306A\u308A\u307E\u3057\u305F\u3002editor.action.replaceAll \u306E\u30AD\u30FC\u30D0\u30A4\u30F3\u30C9\u3092\u5909\u66F4\u3057\u3066\u3001\u3053\u306E\u52D5\u4F5C\u3092\u30AA\u30FC\u30D0\u30FC\u30E9\u30A4\u30C9\u3067\u304D\u307E\u3059\u3002"],"vs/editor/contrib/folding/folding":["\u5C55\u958B","\u518D\u5E30\u7684\u306B\u5C55\u958B\u3059\u308B","\u6298\u308A\u305F\u305F\u307F","\u6298\u308A\u305F\u305F\u307F\u306E\u5207\u308A\u66FF\u3048","\u518D\u5E30\u7684\u306B\u6298\u308A\u305F\u305F\u3080","\u3059\u3079\u3066\u306E\u30D6\u30ED\u30C3\u30AF \u30B3\u30E1\u30F3\u30C8\u306E\u6298\u308A\u305F\u305F\u307F","\u3059\u3079\u3066\u306E\u9818\u57DF\u3092\u6298\u308A\u305F\u305F\u3080","\u3059\u3079\u3066\u306E\u9818\u57DF\u3092\u5C55\u958B","\u3059\u3079\u3066\u6298\u308A\u305F\u305F\u307F","\u3059\u3079\u3066\u5C55\u958B","\u30EC\u30D9\u30EB {0} \u3067\u6298\u308A\u305F\u305F\u3080","\u6298\u308A\u66F2\u3052\u308B\u7BC4\u56F2\u306E\u80CC\u666F\u8272\u3002\u57FA\u306E\u88C5\u98FE\u3092\u96A0\u3055\u306A\u3044\u3088\u3046\u306B\u3001\u8272\u306F\u4E0D\u900F\u660E\u3067\u3042\u3063\u3066\u306F\u306A\u308A\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u4F59\u767D\u306B\u3042\u308B\u6298\u308A\u305F\u305F\u307F\u30B3\u30F3\u30C8\u30ED\u30FC\u30EB\u306E\u8272\u3002"],"vs/editor/contrib/folding/foldingDecorations":["\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30B0\u30EA\u30D5\u4F59\u767D\u5185\u306E\u5C55\u958B\u3055\u308C\u305F\u7BC4\u56F2\u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30B0\u30EA\u30D5\u4F59\u767D\u5185\u306E\u6298\u308A\u305F\u305F\u307E\u308C\u305F\u7BC4\u56F2\u306E\u30A2\u30A4\u30B3\u30F3\u3002"],"vs/editor/contrib/fontZoom/fontZoom":["\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30D5\u30A9\u30F3\u30C8\u3092\u62E1\u5927","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30D5\u30A9\u30F3\u30C8\u3092\u7E2E\u5C0F","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30D5\u30A9\u30F3\u30C8\u306E\u30BA\u30FC\u30E0\u3092\u30EA\u30BB\u30C3\u30C8"],"vs/editor/contrib/format/format":["\u884C {0} \u3067 1 \u3064\u306E\u66F8\u5F0F\u8A2D\u5B9A\u3092\u7DE8\u96C6","\u884C {1} \u3067 {0} \u500B\u306E\u66F8\u5F0F\u8A2D\u5B9A\u3092\u7DE8\u96C6","\u884C {0} \u3068 {1} \u306E\u9593\u3067 1 \u3064\u306E\u66F8\u5F0F\u8A2D\u5B9A\u3092\u7DE8\u96C6","\u884C {1} \u3068 {2} \u306E\u9593\u3067 {0} \u500B\u306E\u66F8\u5F0F\u8A2D\u5B9A\u3092\u7DE8\u96C6"],"vs/editor/contrib/format/formatActions":["\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u306E\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8","\u9078\u629E\u7BC4\u56F2\u306E\u30D5\u30A9\u30FC\u30DE\u30C3\u30C8"],"vs/editor/contrib/gotoError/gotoError":["\u6B21\u306E\u554F\u984C (\u30A8\u30E9\u30FC\u3001\u8B66\u544A\u3001\u60C5\u5831) \u3078\u79FB\u52D5","\u6B21\u306E\u30DE\u30FC\u30AB\u30FC\u3078\u79FB\u52D5\u3059\u308B\u305F\u3081\u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u524D\u306E\u554F\u984C (\u30A8\u30E9\u30FC\u3001\u8B66\u544A\u3001\u60C5\u5831) \u3078\u79FB\u52D5","\u524D\u306E\u30DE\u30FC\u30AB\u30FC\u3078\u79FB\u52D5\u3059\u308B\u305F\u3081\u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u30D5\u30A1\u30A4\u30EB\u5185\u306E\u6B21\u306E\u554F\u984C (\u30A8\u30E9\u30FC\u3001\u8B66\u544A\u3001\u60C5\u5831) \u3078\u79FB\u52D5","\u6B21\u306E\u554F\u984C\u7B87\u6240(&&P)","\u30D5\u30A1\u30A4\u30EB\u5185\u306E\u524D\u306E\u554F\u984C (\u30A8\u30E9\u30FC\u3001\u8B66\u544A\u3001\u60C5\u5831) \u3078\u79FB\u52D5","\u524D\u306E\u554F\u984C\u7B87\u6240(&&P)"],"vs/editor/contrib/gotoError/gotoErrorWidget":["\u30A8\u30E9\u30FC","\u8B66\u544A","\u60C5\u5831","\u30D2\u30F3\u30C8","{0} ({1})\u3002","{1} \u4EF6\u4E2D {0} \u4EF6\u306E\u554F\u984C","\u554F\u984C {0} / {1}","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30DE\u30FC\u30AB\u30FC \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3 \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u30A8\u30E9\u30FC\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30DE\u30FC\u30AB\u30FC \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3 \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u8B66\u544A\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30DE\u30FC\u30AB\u30FC \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3 \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u60C5\u5831\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30DE\u30FC\u30AB\u30FC \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3 \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u80CC\u666F\u3002"],"vs/editor/contrib/gotoSymbol/goToCommands":["\u30D4\u30FC\u30AF","\u5B9A\u7FA9","'{0}' \u306E\u5B9A\u7FA9\u306F\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u5B9A\u7FA9\u3078\u79FB\u52D5","\u5B9A\u7FA9\u306B\u79FB\u52D5(&&D)","\u5B9A\u7FA9\u3092\u6A2A\u306B\u958B\u304F","\u5B9A\u7FA9\u3092\u3053\u3053\u306B\u8868\u793A","\u5BA3\u8A00","'{0}' \u306E\u5BA3\u8A00\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u5BA3\u8A00\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u5BA3\u8A00\u3078\u79FB\u52D5","\u5BA3\u8A00\u3078\u79FB\u52D5(&&D)","'{0}' \u306E\u5BA3\u8A00\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u5BA3\u8A00\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u5BA3\u8A00\u3092\u3053\u3053\u306B\u8868\u793A","\u578B\u5B9A\u7FA9","'{0}' \u306E\u578B\u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u578B\u5B9A\u7FA9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u578B\u5B9A\u7FA9\u3078\u79FB\u52D5","\u578B\u5B9A\u7FA9\u306B\u79FB\u52D5(&&T)","\u578B\u5B9A\u7FA9\u3092\u8868\u793A","\u5B9F\u88C5","'{0}' \u306E\u5B9F\u88C5\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u5B9F\u88C5\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u5B9F\u88C5\u3078\u79FB\u52D5","\u5B9F\u88C5\u7B87\u6240\u306B\u79FB\u52D5(&&I)","\u5B9F\u88C5\u306E\u30D4\u30FC\u30AF","'{0}' \u306E\u53C2\u7167\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u53C2\u7167\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093","\u53C2\u7167\u3078\u79FB\u52D5","\u53C2\u7167\u3078\u79FB\u52D5(&&R)","\u53C2\u7167\u8A2D\u5B9A","\u53C2\u7167\u3092\u3053\u3053\u306B\u8868\u793A","\u53C2\u7167\u8A2D\u5B9A","\u4EFB\u610F\u306E\u8A18\u53F7\u3078\u79FB\u52D5","\u5834\u6240","'{0}' \u306B\u4E00\u81F4\u3059\u308B\u7D50\u679C\u306F\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F","\u53C2\u7167\u8A2D\u5B9A"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u3001{0} \u306E\u5B9A\u7FA9\u3092\u8868\u793A\u3057\u307E\u3059\u3002"],"vs/editor/contrib/gotoSymbol/peek/referencesController":["\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["{0} \u500B\u306E\u53C2\u7167","{0} \u500B\u306E\u53C2\u7167","\u53C2\u7167"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["\u30D7\u30EC\u30D3\u30E5\u30FC\u3092\u8868\u793A\u3067\u304D\u307E\u305B\u3093","\u7D50\u679C\u306F\u3042\u308A\u307E\u305B\u3093\u3002","\u53C2\u7167\u8A2D\u5B9A"],"vs/editor/contrib/gotoSymbol/referencesModel":["\u5217 {2} \u306E {1} \u884C\u76EE\u306B {0} \u3064\u306E\u30B7\u30F3\u30DC\u30EB","\u5217 {2}\u3001{3} \u306E {1} \u884C\u76EE\u306E {0} \u306B\u3042\u308B\u8A18\u53F7","{0} \u306B 1 \u500B\u306E\u30B7\u30F3\u30DC\u30EB\u3001\u5B8C\u5168\u306A\u30D1\u30B9 {1}","{1} \u306B {0} \u500B\u306E\u30B7\u30F3\u30DC\u30EB\u3001\u5B8C\u5168\u306A\u30D1\u30B9 {2}","\u4E00\u81F4\u3059\u308B\u9805\u76EE\u306F\u3042\u308A\u307E\u305B\u3093","{0} \u306B 1 \u500B\u306E\u30B7\u30F3\u30DC\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u3057\u305F","{1} \u306B {0} \u500B\u306E\u30B7\u30F3\u30DC\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u3057\u305F","{1} \u500B\u306E\u30D5\u30A1\u30A4\u30EB\u306B {0} \u500B\u306E\u30B7\u30F3\u30DC\u30EB\u304C\u898B\u3064\u304B\u308A\u307E\u3057\u305F"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["{1} \u306E\u30B7\u30F3\u30DC\u30EB {0}\u3001\u6B21\u306B {2}","\u30B7\u30F3\u30DC\u30EB {0}/{1}"],"vs/editor/contrib/hover/hover":["\u30DB\u30D0\u30FC\u306E\u8868\u793A","\u5B9A\u7FA9\u30D7\u30EC\u30D3\u30E5\u30FC\u306E\u30DB\u30D0\u30FC\u3092\u8868\u793A\u3059\u308B"],"vs/editor/contrib/hover/markdownHoverParticipant":["\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059..."],"vs/editor/contrib/hover/markerHoverParticipant":["View Problem","\u5229\u7528\u3067\u304D\u308B\u30AF\u30A4\u30C3\u30AF\u30D5\u30A3\u30C3\u30AF\u30B9\u306F\u3042\u308A\u307E\u305B\u3093","\u30AF\u30A4\u30C3\u30AF\u30D5\u30A3\u30C3\u30AF\u30B9\u3092\u78BA\u8A8D\u3057\u3066\u3044\u307E\u3059...","\u5229\u7528\u3067\u304D\u308B\u30AF\u30A4\u30C3\u30AF\u30D5\u30A3\u30C3\u30AF\u30B9\u306F\u3042\u308A\u307E\u305B\u3093","\u30AF\u30A4\u30C3\u30AF \u30D5\u30A3\u30C3\u30AF\u30B9..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["\u524D\u306E\u5024\u306B\u7F6E\u63DB","\u6B21\u306E\u5024\u306B\u7F6E\u63DB"],"vs/editor/contrib/indentation/indentation":["\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u30B9\u30DA\u30FC\u30B9\u306B\u5909\u63DB","\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u30BF\u30D6\u306B\u5909\u63DB","\u69CB\u6210\u3055\u308C\u305F\u30BF\u30D6\u306E\u30B5\u30A4\u30BA","\u73FE\u5728\u306E\u30D5\u30A1\u30A4\u30EB\u306E\u30BF\u30D6\u306E\u30B5\u30A4\u30BA\u3092\u9078\u629E","\u30BF\u30D6\u306B\u3088\u308B\u30A4\u30F3\u30C7\u30F3\u30C8","\u30B9\u30DA\u30FC\u30B9\u306B\u3088\u308B\u30A4\u30F3\u30C7\u30F3\u30C8","\u5185\u5BB9\u304B\u3089\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u691C\u51FA","\u884C\u306E\u518D\u30A4\u30F3\u30C7\u30F3\u30C8","\u9078\u629E\u884C\u3092\u518D\u30A4\u30F3\u30C7\u30F3\u30C8"],"vs/editor/contrib/linesOperations/linesOperations":["\u884C\u3092\u4E0A\u3078\u30B3\u30D4\u30FC","\u884C\u3092\u4E0A\u3078\u30B3\u30D4\u30FC(&&C)","\u884C\u3092\u4E0B\u3078\u30B3\u30D4\u30FC","\u884C\u3092\u4E0B\u3078\u30B3\u30D4\u30FC(&&P)","\u9078\u629E\u7BC4\u56F2\u306E\u8907\u88FD","\u9078\u629E\u7BC4\u56F2\u306E\u8907\u88FD(&&D)","\u884C\u3092\u4E0A\u3078\u79FB\u52D5","\u884C\u3092\u4E0A\u3078\u79FB\u52D5(&&V)","\u884C\u3092\u4E0B\u3078\u79FB\u52D5","\u884C\u3092\u4E0B\u3078\u79FB\u52D5(&&L)","\u884C\u3092\u6607\u9806\u306B\u4E26\u3079\u66FF\u3048","\u884C\u3092\u964D\u9806\u306B\u4E26\u3079\u66FF\u3048","\u672B\u5C3E\u306E\u7A7A\u767D\u306E\u30C8\u30EA\u30DF\u30F3\u30B0","\u884C\u306E\u524A\u9664","\u884C\u306E\u30A4\u30F3\u30C7\u30F3\u30C8","\u884C\u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u89E3\u9664","\u884C\u3092\u4E0A\u306B\u633F\u5165","\u884C\u3092\u4E0B\u306B\u633F\u5165","\u5DE6\u5074\u3092\u3059\u3079\u3066\u524A\u9664","\u53F3\u5074\u3092\u3059\u3079\u3066\u524A\u9664","\u884C\u3092\u3064\u306A\u3052\u308B","\u30AB\u30FC\u30BD\u30EB\u306E\u5468\u56F2\u306E\u6587\u5B57\u3092\u5165\u308C\u66FF\u3048\u308B","\u5927\u6587\u5B57\u306B\u5909\u63DB","\u5C0F\u6587\u5B57\u306B\u5909\u63DB","\u5148\u982D\u6587\u5B57\u3092\u5927\u6587\u5B57\u306B\u5909\u63DB\u3059\u308B","\u30B9\u30CD\u30FC\u30AF \u30B1\u30FC\u30B9\u306B\u5909\u63DB\u3059\u308B"],"vs/editor/contrib/linkedEditing/linkedEditing":["\u30EA\u30F3\u30AF\u3055\u308C\u305F\u7DE8\u96C6\u306E\u958B\u59CB","\u30A8\u30C7\u30A3\u30BF\u30FC\u304C\u578B\u306E\u540D\u524D\u306E\u81EA\u52D5\u5909\u66F4\u3092\u884C\u3046\u3068\u304D\u306E\u80CC\u666F\u8272\u3067\u3059\u3002"],"vs/editor/contrib/links/links":["\u30B3\u30DE\u30F3\u30C9\u306E\u5B9F\u884C","\u30EA\u30F3\u30AF\u5148\u3092\u8868\u793A","cmd + \u30AF\u30EA\u30C3\u30AF","ctrl + \u30AF\u30EA\u30C3\u30AF","option + \u30AF\u30EA\u30C3\u30AF","alt + \u30AF\u30EA\u30C3\u30AF","\u30B3\u30DE\u30F3\u30C9 {0} \u306E\u5B9F\u884C","\u3053\u306E\u30EA\u30F3\u30AF\u306F\u5F62\u5F0F\u304C\u6B63\u3057\u304F\u306A\u3044\u305F\u3081\u958B\u304F\u3053\u3068\u304C\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F: {0}","\u3053\u306E\u30EA\u30F3\u30AF\u306F\u30BF\u30FC\u30B2\u30C3\u30C8\u304C\u5B58\u5728\u3057\u306A\u3044\u305F\u3081\u958B\u304F\u3053\u3068\u304C\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002","\u30EA\u30F3\u30AF\u3092\u958B\u304F"],"vs/editor/contrib/message/messageController":["\u30A8\u30C7\u30A3\u30BF\u30FC\u306B\u73FE\u5728\u30A4\u30F3\u30E9\u30A4\u30F3 \u30E1\u30C3\u30BB\u30FC\u30B8\u304C\u8868\u793A\u3055\u308C\u3066\u3044\u308B\u304B\u3069\u3046\u304B","\u8AAD\u307F\u53D6\u308A\u5C02\u7528\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u306F\u7DE8\u96C6\u3067\u304D\u307E\u305B\u3093"],"vs/editor/contrib/multicursor/multicursor":["\u30AB\u30FC\u30BD\u30EB\u3092\u4E0A\u306B\u633F\u5165","\u30AB\u30FC\u30BD\u30EB\u3092\u4E0A\u306B\u633F\u5165(&&A)","\u30AB\u30FC\u30BD\u30EB\u3092\u4E0B\u306B\u633F\u5165","\u30AB\u30FC\u30BD\u30EB\u3092\u4E0B\u306B\u633F\u5165(&&D)","\u30AB\u30FC\u30BD\u30EB\u3092\u884C\u672B\u306B\u633F\u5165","\u30AB\u30FC\u30BD\u30EB\u3092\u884C\u672B\u306B\u633F\u5165(&&U)","\u30AB\u30FC\u30BD\u30EB\u3092\u4E0B\u306B\u633F\u5165","\u30AB\u30FC\u30BD\u30EB\u3092\u4E0A\u306B\u633F\u5165","\u9078\u629E\u3057\u305F\u9805\u76EE\u3092\u6B21\u306E\u4E00\u81F4\u9805\u76EE\u306B\u8FFD\u52A0","\u6B21\u306E\u51FA\u73FE\u500B\u6240\u3092\u8FFD\u52A0(&&N)","\u9078\u629E\u9805\u76EE\u3092\u6B21\u306E\u4E00\u81F4\u9805\u76EE\u306B\u8FFD\u52A0","\u524D\u306E\u51FA\u73FE\u7B87\u6240\u3092\u8FFD\u52A0(&&R)","\u6700\u5F8C\u306B\u9078\u629E\u3057\u305F\u9805\u76EE\u3092\u6B21\u306E\u4E00\u81F4\u9805\u76EE\u306B\u79FB\u52D5","\u6700\u5F8C\u306B\u9078\u3093\u3060\u9805\u76EE\u3092\u524D\u306E\u4E00\u81F4\u9805\u76EE\u306B\u79FB\u52D5\u3059\u308B","\u4E00\u81F4\u3059\u308B\u3059\u3079\u3066\u306E\u51FA\u73FE\u7B87\u6240\u3092\u9078\u629E\u3057\u307E\u3059","\u3059\u3079\u3066\u306E\u51FA\u73FE\u7B87\u6240\u3092\u9078\u629E(&&O)","\u3059\u3079\u3066\u306E\u51FA\u73FE\u7B87\u6240\u3092\u5909\u66F4"],"vs/editor/contrib/parameterHints/parameterHints":["\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC \u30D2\u30F3\u30C8\u3092\u30C8\u30EA\u30AC\u30FC"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["\u6B21\u306E\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC \u30D2\u30F3\u30C8\u3092\u8868\u793A\u3059\u308B\u305F\u3081\u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u524D\u306E\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC \u30D2\u30F3\u30C8\u3092\u8868\u793A\u3059\u308B\u305F\u3081\u306E\u30A2\u30A4\u30B3\u30F3\u3002","{0}\u3001\u30D2\u30F3\u30C8"],"vs/editor/contrib/peekView/peekView":["\u9589\u3058\u308B","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u306E\u30BF\u30A4\u30C8\u30EB\u9818\u57DF\u306E\u80CC\u666F\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC \u30BF\u30A4\u30C8\u30EB\u306E\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u306E\u30BF\u30A4\u30C8\u30EB\u60C5\u5831\u306E\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u306E\u5883\u754C\u3068\u77E2\u5370\u306E\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u7D50\u679C\u30EA\u30B9\u30C8\u306E\u80CC\u666F\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u7D50\u679C\u30EA\u30B9\u30C8\u306E\u30E9\u30A4\u30F3 \u30CE\u30FC\u30C9\u306E\u524D\u666F\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u7D50\u679C\u30EA\u30B9\u30C8\u306E\u30D5\u30A1\u30A4\u30EB \u30CE\u30FC\u30C9\u306E\u524D\u666F\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u7D50\u679C\u30EA\u30B9\u30C8\u306E\u9078\u629E\u6E08\u307F\u30A8\u30F3\u30C8\u30EA\u306E\u80CC\u666F\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u7D50\u679C\u30EA\u30B9\u30C8\u306E\u9078\u629E\u6E08\u307F\u30A8\u30F3\u30C8\u30EA\u306E\u524D\u666F\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC \u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u80CC\u666F\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC \u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u4F59\u767D\u306E\u80CC\u666F\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC\u7D50\u679C\u30EA\u30B9\u30C8\u306E\u4E00\u81F4\u3057\u305F\u5F37\u8ABF\u8868\u793A\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC \u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u4E00\u81F4\u3057\u305F\u5F37\u8ABF\u8868\u793A\u8272\u3002","\u30D4\u30FC\u30AF \u30D3\u30E5\u30FC \u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u4E00\u81F4\u3057\u305F\u5F37\u8ABF\u5883\u754C\u8272\u3002"],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["\u6700\u521D\u306B\u30C6\u30AD\u30B9\u30C8 \u30A8\u30C7\u30A3\u30BF\u30FC\u3092\u958B\u3044\u3066\u3001\u884C\u306B\u79FB\u52D5\u3057\u307E\u3059\u3002","\u884C {0}\u3001\u5217 {1} \u306B\u79FB\u52D5\u3057\u307E\u3059\u3002","{0} \u884C\u306B\u79FB\u52D5\u3057\u307E\u3059\u3002","\u73FE\u5728\u306E\u884C: {0}\u3001\u6587\u5B57: {1}\u3002\u79FB\u52D5\u5148\u3068\u306A\u308B\u30011 \u304B\u3089 {2} \u307E\u3067\u306E\u884C\u756A\u53F7\u3092\u5165\u529B\u3057\u307E\u3059\u3002","\u73FE\u5728\u306E\u884C: {0}\u3001\u6587\u5B57: {1}\u3002\u79FB\u52D5\u5148\u306E\u884C\u756A\u53F7\u3092\u5165\u529B\u3057\u307E\u3059\u3002"],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["\u30B7\u30F3\u30DC\u30EB\u306B\u79FB\u52D5\u3059\u308B\u306B\u306F\u3001\u307E\u305A\u30B7\u30F3\u30DC\u30EB\u60C5\u5831\u3092\u542B\u3080\u30C6\u30AD\u30B9\u30C8 \u30A8\u30C7\u30A3\u30BF\u30FC\u3092\u958B\u304D\u307E\u3059\u3002","\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C6\u30AD\u30B9\u30C8 \u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u306F\u3001\u30B7\u30F3\u30DC\u30EB\u60C5\u5831\u306F\u8868\u793A\u3055\u308C\u307E\u305B\u3093\u3002","\u4E00\u81F4\u3059\u308B\u30A8\u30C7\u30A3\u30BF\u30FC \u30B7\u30F3\u30DC\u30EB\u304C\u3042\u308A\u307E\u305B\u3093","\u30A8\u30C7\u30A3\u30BF\u30FC \u30B7\u30F3\u30DC\u30EB\u304C\u3042\u308A\u307E\u305B\u3093","\u6A2A\u306B\u4E26\u3079\u3066\u958B\u304F","\u4E00\u756A\u4E0B\u3067\u958B\u304F","\u30B7\u30F3\u30DC\u30EB ({0})","\u30D7\u30ED\u30D1\u30C6\u30A3 ({0})","\u30E1\u30BD\u30C3\u30C9 ({0})","\u95A2\u6570 ({0})","\u30B3\u30F3\u30B9\u30C8\u30E9\u30AF\u30BF\u30FC ({0})","\u5909\u6570 ({0})","\u30AF\u30E9\u30B9 ({0})","\u69CB\u9020\u4F53 ({0})","\u30A4\u30D9\u30F3\u30C8 ({0})","\u6F14\u7B97\u5B50 ({0})","\u30A4\u30F3\u30BF\u30FC\u30D5\u30A7\u30A4\u30B9 ({0})","\u540D\u524D\u7A7A\u9593 ({0})","\u30D1\u30C3\u30B1\u30FC\u30B8 ({0})","\u578B\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC ({0})","\u30E2\u30B8\u30E5\u30FC\u30EB ({0})","\u30D7\u30ED\u30D1\u30C6\u30A3 ({0})","\u5217\u6319\u578B ({0})","\u5217\u6319\u578B\u30E1\u30F3\u30D0\u30FC ({0})","\u6587\u5B57\u5217 ({0})","\u30D5\u30A1\u30A4\u30EB ({0})","\u914D\u5217 ({0})","\u6570\u5024 ({0})","\u30D6\u30FC\u30EB\u5024 ({0})","\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8 ({0})","\u30AD\u30FC ({0})","\u30D5\u30A3\u30FC\u30EB\u30C9 ({0})","\u5B9A\u6570 ({0})"],"vs/editor/contrib/rename/rename":["\u7D50\u679C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u540D\u524D\u5909\u66F4\u306E\u5834\u6240\u3092\u89E3\u6C7A\u3057\u3088\u3046\u3068\u3057\u3066\u4E0D\u660E\u306A\u30A8\u30E9\u30FC\u304C\u767A\u751F\u3057\u307E\u3057\u305F","'{0}' \u306E\u540D\u524D\u306E\u5909\u66F4\u4E2D","{0} \u306E\u540D\u524D\u3092\u5909\u66F4\u3057\u3066\u3044\u307E\u3059","'{0}' \u304B\u3089 '{1}' \u3078\u306E\u540D\u524D\u5909\u66F4\u304C\u6B63\u5E38\u306B\u5B8C\u4E86\u3057\u307E\u3057\u305F\u3002\u6982\u8981: {2}","\u540D\u524D\u306E\u5909\u66F4\u3067\u7DE8\u96C6\u3092\u9069\u7528\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F","\u540D\u524D\u306E\u5909\u66F4\u306B\u3088\u3063\u3066\u7DE8\u96C6\u306E\u8A08\u7B97\u306B\u5931\u6557\u3057\u307E\u3057\u305F","\u30B7\u30F3\u30DC\u30EB\u306E\u540D\u524D\u5909\u66F4","\u540D\u524D\u3092\u5909\u66F4\u3059\u308B\u524D\u306B\u5909\u66F4\u3092\u30D7\u30EC\u30D3\u30E5\u30FC\u3059\u308B\u6A5F\u80FD\u3092\u6709\u52B9\u307E\u305F\u306F\u7121\u52B9\u306B\u3059\u308B"],"vs/editor/contrib/rename/renameInputField":["\u540D\u524D\u5909\u66F4\u5165\u529B\u3002\u65B0\u3057\u3044\u540D\u524D\u3092\u5165\u529B\u3057\u3001Enter \u30AD\u30FC\u3092\u62BC\u3057\u3066\u30B3\u30DF\u30C3\u30C8\u3057\u3066\u304F\u3060\u3055\u3044\u3002","\u540D\u524D\u3092\u5909\u66F4\u3059\u308B\u306B\u306F {0}\u3001\u30D7\u30EC\u30D3\u30E5\u30FC\u3059\u308B\u306B\u306F {1}"],"vs/editor/contrib/smartSelect/smartSelect":["\u9078\u629E\u7BC4\u56F2\u3092\u62E1\u5F35","\u9078\u629E\u7BC4\u56F2\u306E\u5C55\u958B(&&E)","\u9078\u629E\u7BC4\u56F2\u3092\u7E2E\u5C0F","\u9078\u629E\u7BC4\u56F2\u306E\u7E2E\u5C0F(&&S)"],"vs/editor/contrib/snippet/snippetVariables":["\u65E5\u66DC\u65E5","\u6708\u66DC\u65E5","\u706B\u66DC\u65E5","\u6C34\u66DC\u65E5","\u6728\u66DC\u65E5","\u91D1\u66DC\u65E5","\u571F\u66DC\u65E5","\u65E5","\u6708","\u706B","\u6C34","\u6728","\u91D1","\u571F","1 \u6708","2 \u6708","3 \u6708","4 \u6708","5 \u6708","6 \u6708","7 \u6708","8 \u6708","9 \u6708","10 \u6708","11 \u6708","12 \u6708","1 \u6708","2 \u6708","3 \u6708","4 \u6708","5 \u6708","6 \u6708","7 \u6708","8 \u6708","9 \u6708","10 \u6708","11 \u6708","12 \u6708"],"vs/editor/contrib/suggest/suggestController":["{1} \u304C\u8FFD\u52A0\u7DE8\u96C6\u3057\u305F '{0}' \u3092\u53D7\u3051\u5165\u308C\u308B","\u5019\u88DC\u3092\u30C8\u30EA\u30AC\u30FC","\u633F\u5165","\u633F\u5165","\u7F6E\u63DB","\u7F6E\u63DB","\u633F\u5165","\u8868\u793A\u3092\u6E1B\u3089\u3059","\u3055\u3089\u306B\u8868\u793A","\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u30B5\u30A4\u30BA\u3092\u30EA\u30BB\u30C3\u30C8"],"vs/editor/contrib/suggest/suggestWidget":["\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u80CC\u666F\u8272\u3002","\u5019\u88DC\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u5883\u754C\u7DDA\u8272\u3002","\u5019\u88DC\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u524D\u666F\u8272\u3002","\u5019\u88DC\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u5185\u3067\u9078\u629E\u6E08\u307F\u30A8\u30F3\u30C8\u30EA\u306E\u80CC\u666F\u8272\u3002","\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u5185\u3067\u4E00\u81F4\u3057\u305F\u30CF\u30A4\u30E9\u30A4\u30C8\u306E\u8272\u3002","\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059...","\u5019\u88DC\u306F\u3042\u308A\u307E\u305B\u3093\u3002","{0}\u3001\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8: {1}","\u63D0\u6848"],"vs/editor/contrib/suggest/suggestWidgetDetails":["\u9589\u3058\u308B","\u8AAD\u307F\u8FBC\u3093\u3067\u3044\u307E\u3059..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["\u63D0\u6848\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u8A73\u7D30\u60C5\u5831\u306E\u30A2\u30A4\u30B3\u30F3\u3002","\u8A73\u7D30\u3092\u53C2\u7167"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["\u914D\u5217\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30D6\u30FC\u30EB\u5024\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30AF\u30E9\u30B9\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u8272\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u5B9A\u6570\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30B3\u30F3\u30B9\u30C8\u30E9\u30AF\u30BF\u30FC\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u5217\u6319\u5B50\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u5217\u6319\u5B50\u30E1\u30F3\u30D0\u30FC\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30A4\u30D9\u30F3\u30C8\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30D5\u30A3\u30FC\u30EB\u30C9\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30D5\u30A1\u30A4\u30EB\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30D5\u30A9\u30EB\u30C0\u30FC\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u95A2\u6570\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30A4\u30F3\u30BF\u30FC\u30D5\u30A7\u30A4\u30B9\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30AD\u30FC\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30AD\u30FC\u30EF\u30FC\u30C9\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30E1\u30BD\u30C3\u30C9\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30E2\u30B8\u30E5\u30FC\u30EB\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u540D\u524D\u7A7A\u9593\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","Null \u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6570\u5024\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6F14\u7B97\u5B50\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30D1\u30C3\u30B1\u30FC\u30B8\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30D7\u30ED\u30D1\u30C6\u30A3\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u53C2\u7167\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30B9\u30CB\u30DA\u30C3\u30C8\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u6587\u5B57\u5217\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u69CB\u9020\u4F53\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30C6\u30AD\u30B9\u30C8\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u5358\u4F4D\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002","\u5909\u6570\u8A18\u53F7\u306E\u524D\u666F\u8272\u3002\u3053\u308C\u3089\u306E\u8A18\u53F7\u306F\u3001\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u3001\u968E\u5C64\u30EA\u30F3\u30AF\u3001\u304A\u3088\u3073\u5019\u88DC\u306E\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u8868\u793A\u3055\u308C\u307E\u3059\u3002"],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["Tab \u30AD\u30FC\u3092\u5207\u308A\u66FF\u3048\u308B\u3068\u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u79FB\u52D5\u3057\u307E\u3059","Tab \u30AD\u30FC\u3092\u62BC\u3059\u3068\u3001\u6B21\u306E\u30D5\u30A9\u30FC\u30AB\u30B9\u53EF\u80FD\u306A\u8981\u7D20\u306B\u30D5\u30A9\u30FC\u30AB\u30B9\u3092\u79FB\u52D5\u3057\u307E\u3059","Tab \u30AD\u30FC\u3092\u62BC\u3059\u3068\u3001\u30BF\u30D6\u6587\u5B57\u304C\u633F\u5165\u3055\u308C\u307E\u3059"],"vs/editor/contrib/tokenization/tokenization":["\u958B\u767A\u8005: \u30C8\u30FC\u30AF\u30F3\u518D\u4F5C\u6210\u306E\u5F37\u5236"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["\u666E\u901A\u3067\u306F\u306A\u3044\u884C\u7D42\u7AEF\u8A18\u53F7","\u666E\u901A\u3067\u306F\u306A\u3044\u884C\u7D42\u7AEF\u8A18\u53F7\u304C\u691C\u51FA\u3055\u308C\u307E\u3057\u305F",`\u3053\u306E\u30D5\u30A1\u30A4\u30EB\u306B\u306F\u3001\u884C\u533A\u5207\u308A\u6587\u5B57 (LS) \u3084\u6BB5\u843D\u533A\u5207\u308A\u8A18\u53F7 (PS) \u306A\u3069\u306E\u7279\u6B8A\u306A\u884C\u306E\u7D42\u7AEF\u6587\u5B57\u304C 1 \u3064\u4EE5\u4E0A\u542B\u307E\u308C\u3066\u3044\u307E\u3059\u3002\r +\r +\u305D\u308C\u3089\u306E\u7D42\u7AEF\u6587\u5B57\u306F\u30D5\u30A1\u30A4\u30EB\u304B\u3089\u524A\u9664\u3059\u308B\u3053\u3068\u3092\u304A\u52E7\u3081\u3057\u307E\u3059\u3002\u3053\u308C\u306F 'editor.unusualLineTerminators' \u3092\u4F7F\u7528\u3057\u3066\u69CB\u6210\u3067\u304D\u307E\u3059\u3002`,"\u3053\u306E\u30D5\u30A1\u30A4\u30EB\u3092\u4FEE\u6B63","\u3053\u306E\u30D5\u30A1\u30A4\u30EB\u3067\u306F\u554F\u984C\u3092\u7121\u8996\u3059\u308B"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["\u5909\u6570\u306E\u8AAD\u307F\u53D6\u308A\u306A\u3069\u3001\u8AAD\u307F\u53D6\u308A\u30A2\u30AF\u30BB\u30B9\u4E2D\u306E\u30B7\u30F3\u30DC\u30EB\u306E\u80CC\u666F\u8272\u3002\u4E0B\u306B\u3042\u308B\u88C5\u98FE\u3092\u96A0\u3055\u306A\u3044\u305F\u3081\u306B\u3001\u8272\u306F\u4E0D\u900F\u904E\u3067\u3042\u3063\u3066\u306F\u306A\u308A\u307E\u305B\u3093\u3002","\u5909\u6570\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306A\u3069\u3001\u66F8\u304D\u8FBC\u307F\u30A2\u30AF\u30BB\u30B9\u4E2D\u306E\u30B7\u30F3\u30DC\u30EB\u80CC\u666F\u8272\u3002\u4E0B\u306B\u3042\u308B\u88C5\u98FE\u3092\u96A0\u3055\u306A\u3044\u305F\u3081\u306B\u3001\u8272\u306F\u4E0D\u900F\u904E\u3067\u3042\u3063\u3066\u306F\u306A\u308A\u307E\u305B\u3093\u3002","\u5909\u6570\u306E\u8AAD\u307F\u53D6\u308A\u306A\u3069\u8AAD\u307F\u53D6\u308A\u30A2\u30AF\u30BB\u30B9\u4E2D\u306E\u30B7\u30F3\u30DC\u30EB\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u5909\u6570\u3078\u306E\u66F8\u304D\u8FBC\u307F\u306A\u3069\u66F8\u304D\u8FBC\u307F\u30A2\u30AF\u30BB\u30B9\u4E2D\u306E\u30B7\u30F3\u30DC\u30EB\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u30B7\u30F3\u30DC\u30EB\u306B\u3088\u3063\u3066\u5F37\u8ABF\u8868\u793A\u3055\u308C\u308B\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u30DE\u30FC\u30AB\u30FC\u306E\u8272\u3002\u30DE\u30FC\u30AB\u30FC\u306E\u8272\u306F\u3001\u57FA\u306B\u306A\u308B\u88C5\u98FE\u3092\u96A0\u3055\u306A\u3044\u3088\u3046\u306B\u4E0D\u900F\u660E\u4EE5\u5916\u306B\u3057\u307E\u3059\u3002","\u66F8\u304D\u8FBC\u307F\u30A2\u30AF\u30BB\u30B9 \u30B7\u30F3\u30DC\u30EB\u3092\u5F37\u8ABF\u8868\u793A\u3059\u308B\u6982\u8981\u30EB\u30FC\u30E9\u30FC\u306E\u30DE\u30FC\u30AB\u30FC\u8272\u3002\u4E0B\u306B\u3042\u308B\u88C5\u98FE\u3092\u96A0\u3055\u306A\u3044\u305F\u3081\u306B\u3001\u8272\u306F\u4E0D\u900F\u904E\u3067\u3042\u3063\u3066\u306F\u306A\u308A\u307E\u305B\u3093\u3002","\u6B21\u306E\u30B7\u30F3\u30DC\u30EB \u30CF\u30A4\u30E9\u30A4\u30C8\u306B\u79FB\u52D5","\u524D\u306E\u30B7\u30F3\u30DC\u30EB \u30CF\u30A4\u30E9\u30A4\u30C8\u306B\u79FB\u52D5","\u30B7\u30F3\u30DC\u30EB \u30CF\u30A4\u30E9\u30A4\u30C8\u3092\u30C8\u30EA\u30AC\u30FC"],"vs/editor/contrib/wordOperations/wordOperations":["\u5358\u8A9E\u306E\u524A\u9664"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["\u65E2\u5B9A\u306E\u8A00\u8A9E\u69CB\u6210\u306E\u30AA\u30FC\u30D0\u30FC\u30E9\u30A4\u30C9","\u8A00\u8A9E\u306B\u5BFE\u3057\u3066\u4E0A\u66F8\u304D\u3055\u308C\u308B\u30A8\u30C7\u30A3\u30BF\u30FC\u8A2D\u5B9A\u3092\u69CB\u6210\u3057\u307E\u3059\u3002","\u3053\u306E\u8A2D\u5B9A\u3067\u306F\u3001\u8A00\u8A9E\u3054\u3068\u306E\u69CB\u6210\u306F\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002","\u7A7A\u306E\u30D7\u30ED\u30D1\u30C6\u30A3\u306F\u767B\u9332\u3067\u304D\u307E\u305B\u3093","'{0}' \u3092\u767B\u9332\u3067\u304D\u307E\u305B\u3093\u3002\u3053\u308C\u306F\u3001\u8A00\u8A9E\u56FA\u6709\u306E\u30A8\u30C7\u30A3\u30BF\u30FC\u8A2D\u5B9A\u3092\u8A18\u8FF0\u3059\u308B\u30D7\u30ED\u30D1\u30C6\u30A3 \u30D1\u30BF\u30FC\u30F3 '\\\\[.*\\\\]$' \u306B\u4E00\u81F4\u3057\u3066\u3044\u307E\u3059\u3002'configurationDefaults' \u30B3\u30F3\u30C8\u30EA\u30D3\u30E5\u30FC\u30B7\u30E7\u30F3\u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002","'{0}' \u3092\u767B\u9332\u3067\u304D\u307E\u305B\u3093\u3002\u3053\u306E\u30D7\u30ED\u30D1\u30C6\u30A3\u306F\u65E2\u306B\u767B\u9332\u3055\u308C\u3066\u3044\u307E\u3059\u3002"],"vs/platform/contextkey/browser/contextKeyService":["\u30B3\u30F3\u30C6\u30AD\u30B9\u30C8 \u30AD\u30FC\u306B\u95A2\u3059\u308B\u60C5\u5831\u3092\u8FD4\u3059\u30B3\u30DE\u30F3\u30C9"],"vs/platform/contextkey/common/contextkeys":["Whether the operating system is Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["({0}) \u304C\u6E21\u3055\u308C\u307E\u3057\u305F\u30022 \u756A\u76EE\u306E\u30AD\u30FC\u3092\u5F85\u3063\u3066\u3044\u307E\u3059...","\u30AD\u30FC\u306E\u7D44\u307F\u5408\u308F\u305B ({0}\u3001{1}) \u306F\u30B3\u30DE\u30F3\u30C9\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002"],"vs/platform/list/browser/listService":["\u30EF\u30FC\u30AF\u30D9\u30F3\u30C1","Windows \u304A\u3088\u3073 Linux \u4E0A\u306E `Control` \u30AD\u30FC\u3068 macOS \u4E0A\u306E `Command` \u30AD\u30FC\u306B\u5272\u308A\u5F53\u3066\u307E\u3059\u3002","Windows \u304A\u3088\u3073 Linux \u4E0A\u306E `Alt` \u30AD\u30FC\u3068 macOS \u4E0A\u306E `Option` \u30AD\u30FC\u306B\u5272\u308A\u5F53\u3066\u307E\u3059\u3002","\u30DE\u30A6\u30B9\u3092\u4F7F\u7528\u3057\u3066\u9805\u76EE\u3092\u8907\u6570\u9078\u629E\u3059\u308B\u3068\u304D\u306B\u4F7F\u7528\u3059\u308B\u4FEE\u98FE\u30AD\u30FC\u3067\u3059 (\u305F\u3068\u3048\u3070\u3001\u30A8\u30AF\u30B9\u30D7\u30ED\u30FC\u30E9\u30FC\u3067\u30A8\u30C7\u30A3\u30BF\u30FC\u3068 scm \u30D3\u30E5\u30FC\u3092\u958B\u304F\u306A\u3069)\u3002'\u6A2A\u306B\u4E26\u3079\u3066\u958B\u304F' \u30DE\u30A6\u30B9 \u30B8\u30A7\u30B9\u30C1\u30E3\u30FC (\u304C\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u308B\u5834\u5408) \u306F\u3001\u8907\u6570\u9078\u629E\u306E\u4FEE\u98FE\u30AD\u30FC\u3068\u7AF6\u5408\u3057\u306A\u3044\u3088\u3046\u306B\u8ABF\u6574\u3055\u308C\u307E\u3059\u3002","\u30DE\u30A6\u30B9\u3092\u4F7F\u7528\u3057\u3066\u3001\u30C4\u30EA\u30FC\u3068\u30EA\u30B9\u30C8\u5185\u306E\u9805\u76EE\u3092\u958B\u304F\u65B9\u6CD5\u3092\u5236\u5FA1\u3057\u307E\u3059 (\u30B5\u30DD\u30FC\u30C8\u3055\u308C\u3066\u3044\u308B\u5834\u5408)\u3002\u9069\u7528\u3067\u304D\u306A\u3044\u5834\u5408\u3001\u4E00\u90E8\u306E\u30C4\u30EA\u30FC\u3084\u30EA\u30B9\u30C8\u3067\u306F\u3053\u306E\u8A2D\u5B9A\u304C\u7121\u8996\u3055\u308C\u308B\u3053\u3068\u304C\u3042\u308A\u307E\u3059\u3002","\u30EA\u30B9\u30C8\u3068\u30C4\u30EA\u30FC\u304C\u30EF\u30FC\u30AF\u30D9\u30F3\u30C1\u3067\u6C34\u5E73\u30B9\u30AF\u30ED\u30FC\u30EB\u3092\u30B5\u30DD\u30FC\u30C8\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u8B66\u544A: \u3053\u306E\u8A2D\u5B9A\u3092\u30AA\u30F3\u306B\u3059\u308B\u3068\u3001\u30D1\u30D5\u30A9\u30FC\u30DE\u30F3\u30B9\u306B\u5F71\u97FF\u304C\u3042\u308A\u307E\u3059\u3002","\u30C4\u30EA\u30FC\u306E\u30A4\u30F3\u30C7\u30F3\u30C8\u3092\u30D4\u30AF\u30BB\u30EB\u5358\u4F4D\u3067\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30C4\u30EA\u30FC\u3067\u30A4\u30F3\u30B7\u30C7\u30F3\u30C8\u306E\u30AC\u30A4\u30C9\u3092\u8868\u793A\u3059\u308B\u5FC5\u8981\u304C\u3042\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u30EA\u30B9\u30C8\u3068\u30C4\u30EA\u30FC\u3067\u30B9\u30E0\u30FC\u30BA \u30B9\u30AF\u30ED\u30FC\u30EB\u3092\u4F7F\u7528\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002","\u7C21\u5358\u306A\u30AD\u30FC\u30DC\u30FC\u30C9 \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3\u306F\u3001\u30AD\u30FC\u30DC\u30FC\u30C9\u5165\u529B\u306B\u4E00\u81F4\u3059\u308B\u8981\u7D20\u306B\u7126\u70B9\u3092\u5F53\u3066\u307E\u3059\u3002\u4E00\u81F4\u51E6\u7406\u306F\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u3067\u306E\u307F\u5B9F\u884C\u3055\u308C\u307E\u3059\u3002","\u30AD\u30FC\u30DC\u30FC\u30C9 \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3\u306E\u5F37\u8ABF\u8868\u793A\u3092\u4F7F\u7528\u3059\u308B\u3068\u3001\u30AD\u30FC\u30DC\u30FC\u30C9\u5165\u529B\u306B\u4E00\u81F4\u3059\u308B\u8981\u7D20\u304C\u5F37\u8ABF\u8868\u793A\u3055\u308C\u307E\u3059\u3002\u4E0A\u304A\u3088\u3073\u4E0B\u3078\u306E\u79FB\u52D5\u306F\u3001\u5F37\u8ABF\u8868\u793A\u3055\u308C\u3066\u3044\u308B\u8981\u7D20\u306E\u307F\u3092\u79FB\u52D5\u3057\u307E\u3059\u3002","\u30AD\u30FC\u30DC\u30FC\u30C9 \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3\u306E\u30D5\u30A3\u30EB\u30BF\u30FC\u3067\u306F\u3001\u30AD\u30FC\u30DC\u30FC\u30C9\u5165\u529B\u306B\u4E00\u81F4\u3057\u306A\u3044\u3059\u3079\u3066\u306E\u8981\u7D20\u304C\u30D5\u30A3\u30EB\u30BF\u30FC\u51E6\u7406\u3055\u308C\u3001\u975E\u8868\u793A\u306B\u306A\u308A\u307E\u3059\u3002","\u30EF\u30FC\u30AF\u30D9\u30F3\u30C1\u306E\u30EA\u30B9\u30C8\u304A\u3088\u3073\u30C4\u30EA\u30FC\u306E\u30AD\u30FC\u30DC\u30FC\u30C9 \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3 \u30B9\u30BF\u30A4\u30EB\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u5358\u7D14\u3001\u5F37\u8ABF\u8868\u793A\u3001\u30D5\u30A3\u30EB\u30BF\u30FC\u3092\u6307\u5B9A\u3067\u304D\u307E\u3059\u3002","\u30EA\u30B9\u30C8\u3084\u30C4\u30EA\u30FC\u3067\u306E\u30AD\u30FC\u30DC\u30FC\u30C9 \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3\u3092\u3001\u5358\u306B\u5165\u529B\u3059\u308B\u3060\u3051\u3067\u81EA\u52D5\u7684\u306B\u30C8\u30EA\u30AC\u30FC\u3059\u308B\u304B\u3069\u3046\u304B\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002`false` \u306B\u8A2D\u5B9A\u3057\u305F\u5834\u5408\u3001\u30AD\u30FC\u30DC\u30FC\u30C9 \u30CA\u30D3\u30B2\u30FC\u30B7\u30E7\u30F3\u306F `list.toggleKeyboardNavigation` \u30B3\u30DE\u30F3\u30C9\u3092\u5B9F\u884C\u3057\u305F\u3068\u304D\u306B\u306E\u307F\u30C8\u30EA\u30AC\u30FC\u3055\u308C\u307E\u3059\u3002\u3053\u308C\u306B\u5BFE\u3057\u3066\u30AD\u30FC\u30DC\u30FC\u30C9 \u30B7\u30E7\u30FC\u30C8\u30AB\u30C3\u30C8\u3092\u5272\u308A\u5F53\u3066\u308B\u3053\u3068\u304C\u3067\u304D\u307E\u3059\u3002","\u30D5\u30A9\u30EB\u30C0\u30FC\u540D\u3092\u30AF\u30EA\u30C3\u30AF\u3057\u305F\u3068\u304D\u306B\u30C4\u30EA\u30FC \u30D5\u30A9\u30EB\u30C0\u30FC\u304C\u5C55\u958B\u3055\u308C\u308B\u65B9\u6CD5\u3092\u5236\u5FA1\u3057\u307E\u3059\u3002\u9069\u7528\u3067\u304D\u306A\u3044\u5834\u5408\u3001\u4E00\u90E8\u306E\u30C4\u30EA\u30FC\u3084\u30EA\u30B9\u30C8\u3067\u306F\u3053\u306E\u8A2D\u5B9A\u304C\u7121\u8996\u3055\u308C\u308B\u3053\u3068\u304C\u3042\u308A\u307E\u3059\u3002"],"vs/platform/markers/common/markers":["\u30A8\u30E9\u30FC","\u8B66\u544A","\u60C5\u5831"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","\u6700\u8FD1\u4F7F\u7528\u3057\u305F\u3082\u306E","\u305D\u306E\u4ED6\u306E\u30B3\u30DE\u30F3\u30C9","\u30B3\u30DE\u30F3\u30C9 '{0}' \u3067\u30A8\u30E9\u30FC ({1}) \u304C\u767A\u751F\u3057\u307E\u3057\u305F"],"vs/platform/quickinput/browser/helpQuickAccess":["\u30B0\u30ED\u30FC\u30D0\u30EB \u30B3\u30DE\u30F3\u30C9","\u30A8\u30C7\u30A3\u30BF\u30FC \u30B3\u30DE\u30F3\u30C9","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["\u5168\u4F53\u306E\u524D\u666F\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u306B\u3088\u3063\u3066\u30AA\u30FC\u30D0\u30FC\u30E9\u30A4\u30C9\u3055\u308C\u3066\u3044\u306A\u3044\u5834\u5408\u306B\u306E\u307F\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30A8\u30E9\u30FC \u30E1\u30C3\u30BB\u30FC\u30B8\u5168\u4F53\u306E\u524D\u666F\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u306B\u3088\u3063\u3066\u4E0A\u66F8\u304D\u3055\u308C\u3066\u3044\u306A\u3044\u5834\u5408\u306B\u306E\u307F\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30EF\u30FC\u30AF\u30D9\u30F3\u30C1\u306E\u30A2\u30A4\u30B3\u30F3\u306E\u65E2\u5B9A\u306E\u8272\u3002","\u30D5\u30A9\u30FC\u30AB\u30B9\u3055\u308C\u305F\u8981\u7D20\u306E\u5883\u754C\u7DDA\u5168\u4F53\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u306B\u3088\u3063\u3066\u4E0A\u66F8\u304D\u3055\u308C\u3066\u3044\u306A\u3044\u5834\u5408\u306B\u306E\u307F\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30B3\u30F3\u30C8\u30E9\u30B9\u30C8\u3092\u5F37\u3081\u308B\u305F\u3081\u306B\u3001\u4ED6\u306E\u8981\u7D20\u3068\u9694\u3066\u308B\u8FFD\u52A0\u306E\u5883\u754C\u7DDA\u3002","\u30B3\u30F3\u30C8\u30E9\u30B9\u30C8\u3092\u5F37\u3081\u308B\u305F\u3081\u306B\u3001\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u4ED6\u8981\u7D20\u3068\u9694\u3066\u308B\u8FFD\u52A0\u306E\u5883\u754C\u7DDA\u3002","\u30C6\u30AD\u30B9\u30C8\u5185\u306E\u30EA\u30F3\u30AF\u306E\u524D\u666F\u8272\u3002","\u30C6\u30AD\u30B9\u30C8\u5185\u306E\u30B3\u30FC\u30C9 \u30D6\u30ED\u30C3\u30AF\u306E\u80CC\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u5185\u306E\u691C\u7D22/\u7F6E\u63DB\u7A93\u306A\u3069\u3001\u30A8\u30C7\u30A3\u30BF\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u5F71\u306E\u8272\u3002","\u5165\u529B\u30DC\u30C3\u30AF\u30B9\u306E\u80CC\u666F\u3002","\u5165\u529B\u30DC\u30C3\u30AF\u30B9\u306E\u524D\u666F\u3002","\u5165\u529B\u30DC\u30C3\u30AF\u30B9\u306E\u5883\u754C\u7DDA\u3002","\u5165\u529B\u30D5\u30A3\u30FC\u30EB\u30C9\u306E\u30A2\u30AF\u30C6\u30A3\u30D6 \u30AA\u30D7\u30B7\u30E7\u30F3\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u5165\u529B\u30D5\u30A3\u30FC\u30EB\u30C9\u3067\u30A2\u30AF\u30C6\u30A3\u30D6\u5316\u3055\u308C\u305F\u30AA\u30D7\u30B7\u30E7\u30F3\u306E\u80CC\u666F\u8272\u3002","\u5165\u529B\u30D5\u30A3\u30FC\u30EB\u30C9\u3067\u30A2\u30AF\u30C6\u30A3\u30D6\u5316\u3055\u308C\u305F\u30AA\u30D7\u30B7\u30E7\u30F3\u306E\u524D\u666F\u8272\u3002","\u60C5\u5831\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u80CC\u666F\u8272\u3002","\u60C5\u5831\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u524D\u666F\u8272\u3002","\u60C5\u5831\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u5883\u754C\u7DDA\u8272\u3002","\u8B66\u544A\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u80CC\u666F\u8272\u3002","\u8B66\u544A\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u524D\u666F\u8272\u3002","\u8B66\u544A\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u5883\u754C\u7DDA\u8272\u3002","\u30A8\u30E9\u30FC\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u80CC\u666F\u8272\u3002","\u30A8\u30E9\u30FC\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u524D\u666F\u8272\u3002","\u30A8\u30E9\u30FC\u306E\u91CD\u5927\u5EA6\u3092\u793A\u3059\u5165\u529B\u691C\u8A3C\u306E\u5883\u754C\u7DDA\u8272\u3002","\u30C9\u30ED\u30C3\u30D7\u30C0\u30A6\u30F3\u306E\u80CC\u666F\u3002","\u30C9\u30ED\u30C3\u30D7\u30C0\u30A6\u30F3\u306E\u524D\u666F\u3002","\u30DC\u30BF\u30F3\u306E\u524D\u666F\u8272\u3002","\u30DC\u30BF\u30F3\u306E\u80CC\u666F\u8272\u3002","\u30DB\u30D0\u30FC\u6642\u306E\u30DC\u30BF\u30F3\u80CC\u666F\u8272\u3002","\u30D0\u30C3\u30B8\u306E\u80CC\u666F\u8272\u3002\u30D0\u30C3\u30B8\u3068\u306F\u5C0F\u3055\u306A\u60C5\u5831\u30E9\u30D9\u30EB\u306E\u3053\u3068\u3067\u3059\u3002\u4F8B:\u691C\u7D22\u7D50\u679C\u306E\u6570","\u30D0\u30C3\u30B8\u306E\u524D\u666F\u8272\u3002\u30D0\u30C3\u30B8\u3068\u306F\u5C0F\u3055\u306A\u60C5\u5831\u30E9\u30D9\u30EB\u306E\u3053\u3068\u3067\u3059\u3002\u4F8B:\u691C\u7D22\u7D50\u679C\u306E\u6570","\u30D3\u30E5\u30FC\u304C\u30B9\u30AF\u30ED\u30FC\u30EB\u3055\u308C\u305F\u3053\u3068\u3092\u793A\u3059\u30B9\u30AF\u30ED\u30FC\u30EB \u30D0\u30FC\u306E\u5F71\u3002","\u30B9\u30AF\u30ED\u30FC\u30EB \u30D0\u30FC\u306E\u30B9\u30E9\u30A4\u30C0\u30FC\u306E\u80CC\u666F\u8272\u3002","\u30DB\u30D0\u30FC\u6642\u306E\u30B9\u30AF\u30ED\u30FC\u30EB \u30D0\u30FC \u30B9\u30E9\u30A4\u30C0\u30FC\u80CC\u666F\u8272\u3002","\u30AF\u30EA\u30C3\u30AF\u6642\u306E\u30B9\u30AF\u30ED\u30FC\u30EB \u30D0\u30FC \u30B9\u30E9\u30A4\u30C0\u30FC\u80CC\u666F\u8272\u3002","\u6642\u9593\u306E\u304B\u304B\u308B\u64CD\u4F5C\u3067\u8868\u793A\u3059\u308B\u30D7\u30ED\u30B0\u30EC\u30B9 \u30D0\u30FC\u306E\u80CC\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u5185\u306E\u30A8\u30E9\u30FC \u30C6\u30AD\u30B9\u30C8\u306E\u80CC\u666F\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30A8\u30E9\u30FC\u3092\u793A\u3059\u6CE2\u7DDA\u306E\u524D\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u5185\u306E\u30A8\u30E9\u30FC \u30DC\u30C3\u30AF\u30B9\u306E\u5883\u754C\u7DDA\u306E\u8272\u3067\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u5185\u306E\u8B66\u544A\u30C6\u30AD\u30B9\u30C8\u306E\u80CC\u666F\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u8B66\u544A\u3092\u793A\u3059\u6CE2\u7DDA\u306E\u524D\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u306E\u8B66\u544A\u30DC\u30C3\u30AF\u30B9\u306E\u5883\u754C\u7DDA\u306E\u8272\u3067\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u5185\u306E\u60C5\u5831\u30C6\u30AD\u30B9\u30C8\u306E\u80CC\u666F\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u60C5\u5831\u3092\u793A\u3059\u6CE2\u7DDA\u306E\u524D\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u5185\u306E\u60C5\u5831\u30DC\u30C3\u30AF\u30B9\u306E\u5883\u754C\u7DDA\u306E\u8272\u3067\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u3067\u30D2\u30F3\u30C8\u3092\u793A\u3059\u6CE2\u7DDA\u306E\u524D\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u5185\u306E\u30D2\u30F3\u30C8 \u30DC\u30C3\u30AF\u30B9\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u80CC\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u65E2\u5B9A\u306E\u524D\u666F\u8272\u3002","\u691C\u7D22/\u7F6E\u63DB\u7A93\u306A\u3069\u3001\u30A8\u30C7\u30A3\u30BF\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u80CC\u666F\u8272\u3002","\u691C\u7D22/\u7F6E\u63DB\u306A\u3069\u3092\u884C\u3046\u30A8\u30C7\u30A3\u30BF\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u524D\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u5883\u754C\u7DDA\u8272\u3002\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u5883\u754C\u7DDA\u304C\u3042\u308A\u3001\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u3088\u3063\u3066\u914D\u8272\u3092\u4E0A\u66F8\u304D\u3055\u308C\u3066\u3044\u306A\u3044\u5834\u5408\u3067\u306E\u307F\u3053\u306E\u914D\u8272\u306F\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u30B5\u30A4\u30BA\u5909\u66F4\u30D0\u30FC\u306E\u5883\u754C\u7DDA\u8272\u3002\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u30B5\u30A4\u30BA\u5909\u66F4\u306E\u5883\u754C\u7DDA\u304C\u3042\u308A\u3001\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u3088\u3063\u3066\u914D\u8272\u3092\u4E0A\u66F8\u304D\u3055\u308C\u3066\u3044\u306A\u3044\u5834\u5408\u3067\u306E\u307F\u3053\u306E\u914D\u8272\u306F\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30AF\u30A4\u30C3\u30AF \u30D4\u30C3\u30AB\u30FC\u306E\u80CC\u666F\u8272\u3002\u30AF\u30A4\u30C3\u30AF \u30D4\u30C3\u30AB\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306F\u3001\u30B3\u30DE\u30F3\u30C9 \u30D1\u30EC\u30C3\u30C8\u306E\u3088\u3046\u306A\u30D4\u30C3\u30AB\u30FC\u306E\u30B3\u30F3\u30C6\u30CA\u30FC\u3067\u3059\u3002","\u30AF\u30A4\u30C3\u30AF \u30D4\u30C3\u30AB\u30FC\u306E\u524D\u666F\u8272\u3002\u30AF\u30A4\u30C3\u30AF \u30D4\u30C3\u30AB\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306F\u3001\u30B3\u30DE\u30F3\u30C9 \u30D1\u30EC\u30C3\u30C8\u306E\u3088\u3046\u306A\u30D4\u30C3\u30AB\u30FC\u306E\u30B3\u30F3\u30C6\u30CA\u30FC\u3067\u3059\u3002","\u30AF\u30A4\u30C3\u30AF \u30D4\u30C3\u30AB\u30FC \u306E\u30BF\u30A4\u30C8\u30EB\u306E\u80CC\u666F\u8272\u3002\u30AF\u30A4\u30C3\u30AF \u30D4\u30C3\u30AB\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306F\u3001\u30B3\u30DE\u30F3\u30C9 \u30D1\u30EC\u30C3\u30C8\u306E\u3088\u3046\u306A\u30D4\u30C3\u30AB\u30FC\u306E\u30B3\u30F3\u30C6\u30CA\u30FC\u3067\u3059\u3002","\u30D5\u30A9\u30FC\u30AB\u30B9\u3055\u308C\u305F\u9805\u76EE\u306E\u30AF\u30A4\u30C3\u30AF\u9078\u629E\u306E\u80CC\u666F\u8272\u3002","\u30E9\u30D9\u30EB\u3092\u30B0\u30EB\u30FC\u30D7\u5316\u3059\u308B\u305F\u3081\u306E\u30AF\u30EA\u30C3\u30AF\u9078\u629E\u306E\u8272\u3002","\u5883\u754C\u7DDA\u3092\u30B0\u30EB\u30FC\u30D7\u5316\u3059\u308B\u305F\u3081\u306E\u30AF\u30A4\u30C3\u30AF\u9078\u629E\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u9078\u629E\u7BC4\u56F2\u306E\u8272\u3002","\u30CF\u30A4 \u30B3\u30F3\u30C8\u30E9\u30B9\u30C8\u306E\u9078\u629E\u6E08\u307F\u30C6\u30AD\u30B9\u30C8\u306E\u8272\u3002","\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u9078\u629E\u7BC4\u56F2\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u9078\u629E\u7BC4\u56F2\u306E\u540C\u3058\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u9818\u57DF\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u9078\u629E\u7BC4\u56F2\u3068\u540C\u3058\u30B3\u30F3\u30C6\u30F3\u30C4\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u73FE\u5728\u306E\u691C\u7D22\u4E00\u81F4\u9805\u76EE\u306E\u8272\u3002","\u305D\u306E\u4ED6\u306E\u691C\u7D22\u6761\u4EF6\u306B\u4E00\u81F4\u3059\u308B\u9805\u76EE\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u691C\u7D22\u3092\u5236\u9650\u3059\u308B\u7BC4\u56F2\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u73FE\u5728\u306E\u691C\u7D22\u4E00\u81F4\u9805\u76EE\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u4ED6\u306E\u691C\u7D22\u4E00\u81F4\u9805\u76EE\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u691C\u7D22\u3092\u5236\u9650\u3059\u308B\u7BC4\u56F2\u306E\u5883\u754C\u7DDA\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u30DB\u30D0\u30FC\u304C\u8868\u793A\u3055\u308C\u3066\u3044\u308B\u8A9E\u306E\u4E0B\u3092\u5F37\u8ABF\u8868\u793A\u3057\u307E\u3059\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC \u30DB\u30D0\u30FC\u306E\u80CC\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC \u30DB\u30D0\u30FC\u306E\u524D\u666F\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC \u30DB\u30D0\u30FC\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u30DB\u30D0\u30FC\u306E\u30B9\u30C6\u30FC\u30BF\u30B9 \u30D0\u30FC\u306E\u80CC\u666F\u8272\u3002","\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30EA\u30F3\u30AF\u306E\u8272\u3002","\u30A4\u30F3\u30E9\u30A4\u30F3 \u30D2\u30F3\u30C8\u306E\u524D\u666F\u8272","\u30A4\u30F3\u30E9\u30A4\u30F3 \u30D2\u30F3\u30C8\u306E\u80CC\u666F\u8272","\u96FB\u7403\u30A2\u30AF\u30B7\u30E7\u30F3 \u30A2\u30A4\u30B3\u30F3\u306B\u4F7F\u7528\u3059\u308B\u8272\u3002","\u81EA\u52D5\u4FEE\u6B63\u306E\u96FB\u7403\u30A2\u30AF\u30B7\u30E7\u30F3 \u30A2\u30A4\u30B3\u30F3\u3068\u3057\u3066\u4F7F\u7528\u3055\u308C\u308B\u8272\u3002","\u633F\u5165\u3055\u308C\u305F\u30C6\u30AD\u30B9\u30C8\u306E\u80CC\u666F\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u524A\u9664\u3057\u305F\u30C6\u30AD\u30B9\u30C8\u306E\u80CC\u666F\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u633F\u5165\u3055\u308C\u305F\u30C6\u30AD\u30B9\u30C8\u306E\u8F2A\u90ED\u306E\u8272\u3002","\u524A\u9664\u3055\u308C\u305F\u30C6\u30AD\u30B9\u30C8\u306E\u8F2A\u90ED\u306E\u8272\u3002","2 \u3064\u306E\u30C6\u30AD\u30B9\u30C8 \u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u9593\u306E\u5883\u754C\u7DDA\u306E\u8272\u3002","\u5DEE\u5206\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u5BFE\u89D2\u7DDA\u306E\u5857\u308A\u3064\u3076\u3057\u8272\u3002\u5BFE\u89D2\u7DDA\u306E\u5857\u308A\u3064\u3076\u3057\u306F\u3001\u6A2A\u306B\u4E26\u3079\u3066\u6BD4\u8F03\u3059\u308B\u30D3\u30E5\u30FC\u3067\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u304C\u30A2\u30AF\u30C6\u30A3\u30D6\u306E\u3068\u304D\u3001\u30D5\u30A9\u30FC\u30AB\u30B9\u3055\u308C\u305F\u9805\u76EE\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u80CC\u666F\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u3067\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u304C\u30A2\u30AF\u30C6\u30A3\u30D6\u306E\u3068\u304D\u3001\u30D5\u30A9\u30FC\u30AB\u30B9\u3055\u308C\u305F\u9805\u76EE\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u524D\u666F\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u3067\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30EA\u30B9\u30C8\u3084\u30C4\u30EA\u30FC\u304C\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u5834\u5408\u306E\u3001\u30D5\u30A9\u30FC\u30AB\u30B9\u3055\u308C\u305F\u9805\u76EE\u306E\u30EA\u30B9\u30C8\u3084\u30C4\u30EA\u30FC\u306E\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30EA\u30B9\u30C8\u3084\u30C4\u30EA\u30FC\u306B\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306B\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u304C\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306E\u3068\u304D\u3001\u9078\u629E\u3055\u308C\u305F\u9805\u76EE\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u80CC\u666F\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u3067\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u304C\u30A2\u30AF\u30C6\u30A3\u30D6\u306E\u3068\u304D\u3001\u9078\u629E\u3055\u308C\u305F\u9805\u76EE\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u524D\u666F\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u3067\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u304C\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306E\u3068\u304D\u3001\u9078\u629E\u3055\u308C\u305F\u9805\u76EE\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u80CC\u666F\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u3067\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u304C\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306E\u3068\u304D\u3001\u9078\u629E\u3055\u308C\u305F\u9805\u76EE\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u524D\u666F\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u3067\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u304C\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306E\u3068\u304D\u3001\u30D5\u30A9\u30FC\u30AB\u30B9\u3055\u308C\u305F\u9805\u76EE\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u80CC\u666F\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u3067\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30EA\u30B9\u30C8\u3084\u30C4\u30EA\u30FC\u304C\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u5834\u5408\u306E\u3001\u30D5\u30A9\u30FC\u30AB\u30B9\u3055\u308C\u305F\u9805\u76EE\u306E\u30EA\u30B9\u30C8\u3084\u30C4\u30EA\u30FC\u306E\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u8272\u3002\u30A2\u30AF\u30C6\u30A3\u30D6\u306A\u30EA\u30B9\u30C8\u3084\u30C4\u30EA\u30FC\u306B\u306F\u30AD\u30FC\u30DC\u30FC\u30C9 \u30D5\u30A9\u30FC\u30AB\u30B9\u304C\u3042\u308A\u3001\u975E\u30A2\u30AF\u30C6\u30A3\u30D6\u306B\u306F\u3053\u308C\u304C\u3042\u308A\u307E\u305B\u3093\u3002","\u30DE\u30A6\u30B9\u64CD\u4F5C\u3067\u9805\u76EE\u3092\u30DB\u30D0\u30FC\u3059\u308B\u3068\u304D\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u80CC\u666F\u3002","\u30DE\u30A6\u30B9\u64CD\u4F5C\u3067\u9805\u76EE\u3092\u30DB\u30D0\u30FC\u3059\u308B\u3068\u304D\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u524D\u666F\u3002","\u30DE\u30A6\u30B9\u64CD\u4F5C\u3067\u9805\u76EE\u3092\u79FB\u52D5\u3059\u308B\u3068\u304D\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8 \u30C9\u30E9\u30C3\u30B0 \u30A2\u30F3\u30C9 \u30C9\u30ED\u30C3\u30D7\u306E\u80CC\u666F\u3002","\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u5185\u3092\u691C\u7D22\u3057\u3066\u3044\u308B\u3068\u304D\u3001\u4E00\u81F4\u3057\u305F\u5F37\u8ABF\u306E\u30C4\u30EA\u30FC\u30EA\u30B9\u30C8\u524D\u666F\u8272\u3002","\u30EA\u30B9\u30C8\u304A\u3088\u3073\u30C4\u30EA\u30FC\u306E\u578B\u30D5\u30A3\u30EB\u30BF\u30FC \u30A6\u30A7\u30B8\u30A7\u30C3\u30C8\u306E\u80CC\u666F\u8272\u3002","\u30EA\u30B9\u30C8\u304A\u3088\u3073\u30C4\u30EA\u30FC\u306E\u578B\u30D5\u30A3\u30EB\u30BF\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u8272\u3002","\u4E00\u81F4\u9805\u76EE\u304C\u306A\u3044\u5834\u5408\u306E\u3001\u30EA\u30B9\u30C8\u304A\u3088\u3073\u30C4\u30EA\u30FC\u306E\u578B\u30D5\u30A3\u30EB\u30BF\u30FC \u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306E\u30A2\u30A6\u30C8\u30E9\u30A4\u30F3\u8272\u3002","\u30A4\u30F3\u30C7\u30F3\u30C8 \u30AC\u30A4\u30C9\u306E\u30C4\u30EA\u30FC \u30B9\u30C8\u30ED\u30FC\u30AF\u306E\u8272\u3002","\u30A4\u30F3\u30C7\u30F3\u30C8 \u30AC\u30A4\u30C9\u306E\u30C4\u30EA\u30FC \u30B9\u30C8\u30ED\u30FC\u30AF\u306E\u8272\u3002","\u30E1\u30CB\u30E5\u30FC\u306E\u5883\u754C\u7DDA\u8272\u3002","\u30E1\u30CB\u30E5\u30FC\u9805\u76EE\u306E\u524D\u666F\u8272\u3002","\u30E1\u30CB\u30E5\u30FC\u9805\u76EE\u306E\u80CC\u666F\u8272\u3002","\u30E1\u30CB\u30E5\u30FC\u3067\u9078\u629E\u3055\u308C\u305F\u30E1\u30CB\u30E5\u30FC\u9805\u76EE\u306E\u524D\u666F\u8272\u3002","\u30E1\u30CB\u30E5\u30FC\u3067\u9078\u629E\u3055\u308C\u305F\u30E1\u30CB\u30E5\u30FC\u9805\u76EE\u306E\u80CC\u666F\u8272\u3002","\u30E1\u30CB\u30E5\u30FC\u3067\u9078\u629E\u3055\u308C\u305F\u30E1\u30CB\u30E5\u30FC\u9805\u76EE\u306E\u5883\u754C\u7DDA\u8272\u3002","\u30E1\u30CB\u30E5\u30FC\u5185\u306E\u30E1\u30CB\u30E5\u30FC\u9805\u76EE\u306E\u5883\u754C\u7DDA\u8272\u3002","\u30B9\u30CB\u30DA\u30C3\u30C8 tabstop \u306E\u80CC\u666F\u8272\u3092\u5F37\u8ABF\u8868\u793A\u3057\u307E\u3059\u3002","\u30B9\u30CB\u30DA\u30C3\u30C8 tabstop \u306E\u5883\u754C\u7DDA\u306E\u8272\u3092\u5F37\u8ABF\u8868\u793A\u3057\u307E\u3059\u3002","\u30B9\u30CB\u30DA\u30C3\u30C8\u306E\u6700\u5F8C\u306E tabstop \u306E\u80CC\u666F\u8272\u3092\u5F37\u8ABF\u8868\u793A\u3057\u307E\u3059\u3002","\u30B9\u30CB\u30DA\u30C3\u30C8\u306E\u6700\u5F8C\u306E\u30BF\u30D6\u30B9\u30C8\u30C3\u30D7\u3067\u5883\u754C\u7DDA\u306E\u8272\u3092\u5F37\u8ABF\u8868\u793A\u3057\u307E\u3059\u3002","\u691C\u51FA\u3055\u308C\u305F\u4E00\u81F4\u9805\u76EE\u306E\u6982\u8981\u30EB\u30FC\u30E9\u30FC \u30DE\u30FC\u30AB\u30FC\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u9078\u629E\u7BC4\u56F2\u3092\u5F37\u8ABF\u8868\u793A\u3059\u308B\u305F\u3081\u306E\u6982\u8981\u30EB\u30FC\u30E9\u30FC \u30DE\u30FC\u30AB\u30FC\u306E\u8272\u3002\u3053\u306E\u8272\u306F\u3001\u57FA\u672C\u88C5\u98FE\u304C\u975E\u8868\u793A\u306B\u306A\u3089\u306A\u3044\u3088\u3046\u4E0D\u900F\u660E\u306B\u3059\u308B\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3002","\u4E00\u81F4\u3092\u691C\u7D22\u3059\u308B\u305F\u3081\u306E\u30DF\u30CB\u30DE\u30C3\u30D7 \u30DE\u30FC\u30AB\u30FC\u306E\u8272\u3002","\u30A8\u30C7\u30A3\u30BF\u30FC\u306E\u9078\u629E\u7BC4\u56F2\u306E\u30DF\u30CB\u30DE\u30C3\u30D7 \u30DE\u30FC\u30AB\u30FC\u306E\u8272\u3002","\u30A8\u30E9\u30FC\u306E\u30DF\u30CB\u30DE\u30C3\u30D7 \u30DE\u30FC\u30AB\u30FC\u306E\u8272\u3002","\u8B66\u544A\u306E\u30DF\u30CB\u30DE\u30C3\u30D7 \u30DE\u30FC\u30AB\u30FC\u306E\u8272\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7\u306E\u80CC\u666F\u8272\u3002","\u30DF\u30CB\u30DE\u30C3\u30D7 \u30B9\u30E9\u30A4\u30C0\u30FC\u306E\u80CC\u666F\u8272\u3002","\u30DB\u30D0\u30FC\u30EA\u30F3\u30B0\u6642\u306E\u30DF\u30CB\u30DE\u30C3\u30D7 \u30B9\u30E9\u30A4\u30C0\u30FC\u306E\u80CC\u666F\u8272\u3002","\u30AF\u30EA\u30C3\u30AF\u3057\u305F\u3068\u304D\u306E\u30DF\u30CB\u30DE\u30C3\u30D7 \u30B9\u30E9\u30A4\u30C0\u30FC\u306E\u80CC\u666F\u8272\u3002","\u554F\u984C\u306E\u30A8\u30E9\u30FC \u30A2\u30A4\u30B3\u30F3\u306B\u4F7F\u7528\u3055\u308C\u308B\u8272\u3002","\u554F\u984C\u306E\u8B66\u544A\u30A2\u30A4\u30B3\u30F3\u306B\u4F7F\u7528\u3055\u308C\u308B\u8272\u3002","\u554F\u984C\u60C5\u5831\u30A2\u30A4\u30B3\u30F3\u306B\u4F7F\u7528\u3055\u308C\u308B\u8272\u3002"],"vs/platform/theme/common/iconRegistry":["\u4F7F\u7528\u3059\u308B\u30D5\u30A9\u30F3\u30C8\u306E ID\u3002\u8A2D\u5B9A\u3055\u308C\u3066\u3044\u306A\u3044\u5834\u5408\u306F\u3001\u6700\u521D\u306B\u5B9A\u7FA9\u3055\u308C\u3066\u3044\u308B\u30D5\u30A9\u30F3\u30C8\u304C\u4F7F\u7528\u3055\u308C\u307E\u3059\u3002","\u30A2\u30A4\u30B3\u30F3\u5B9A\u7FA9\u306B\u95A2\u9023\u4ED8\u3051\u3089\u308C\u305F\u30D5\u30A9\u30F3\u30C8\u6587\u5B57\u3002","\u30A6\u30A3\u30B8\u30A7\u30C3\u30C8\u306B\u3042\u308B\u9589\u3058\u308B\u30A2\u30AF\u30B7\u30E7\u30F3\u306E\u30A2\u30A4\u30B3\u30F3\u3002"],"vs/platform/undoRedo/common/undoRedoService":["\u6B21\u306E\u30D5\u30A1\u30A4\u30EB\u304C\u9589\u3058\u3089\u308C\u3001\u30C7\u30A3\u30B9\u30AF\u4E0A\u3067\u5909\u66F4\u3055\u308C\u307E\u3057\u305F: {0}\u3002","\u4EE5\u4E0B\u306E\u30D5\u30A1\u30A4\u30EB\u306F\u4E92\u63DB\u6027\u306E\u306A\u3044\u65B9\u6CD5\u3067\u5909\u66F4\u3055\u308C\u307E\u3057\u305F: {0}\u3002","\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u3067 '{0}' \u3092\u5143\u306B\u623B\u305B\u307E\u305B\u3093\u3067\u3057\u305F\u3002{1}","\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u3067 '{0}' \u3092\u5143\u306B\u623B\u305B\u307E\u305B\u3093\u3067\u3057\u305F\u3002{1}","{1} \u306B\u5909\u66F4\u304C\u52A0\u3048\u3089\u308C\u305F\u305F\u3081\u3001\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u3067 '{0}' \u3092\u5143\u306B\u623B\u305B\u307E\u305B\u3093\u3067\u3057\u305F","{1} \u3067\u5143\u306B\u623B\u3059\u307E\u305F\u306F\u3084\u308A\u76F4\u3057\u64CD\u4F5C\u304C\u65E2\u306B\u5B9F\u884C\u3055\u308C\u3066\u3044\u308B\u305F\u3081\u3001\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u306B\u5BFE\u3057\u3066 '{0}' \u3092\u5143\u306B\u623B\u3059\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F","\u5143\u306B\u623B\u3059\u307E\u305F\u306F\u3084\u308A\u76F4\u3057\u64CD\u4F5C\u304C\u305D\u306E\u671F\u9593\u306B\u5B9F\u884C\u4E2D\u3067\u3042\u3063\u305F\u305F\u3081\u3001\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u306B\u5BFE\u3057\u3066 '{0}' \u3092\u5143\u306B\u623B\u3059\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F","\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u3067 '{0}' \u3092\u5143\u306B\u623B\u3057\u307E\u3059\u304B?","{0} \u500B\u306E\u30D5\u30A1\u30A4\u30EB\u3067\u5143\u306B\u623B\u3059","\u3053\u306E\u30D5\u30A1\u30A4\u30EB\u3092\u5143\u306B\u623B\u3059","\u30AD\u30E3\u30F3\u30BB\u30EB","\u5143\u306B\u623B\u3059\u307E\u305F\u306F\u3084\u308A\u76F4\u3057\u64CD\u4F5C\u304C\u65E2\u306B\u5B9F\u884C\u3055\u308C\u3066\u3044\u308B\u305F\u3081\u3001'{0}' \u3092\u5143\u306B\u623B\u3059\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002","'{0}' \u3092\u5143\u306B\u623B\u3057\u307E\u3059\u304B?","\u5143\u306B\u623B\u3059","\u30AD\u30E3\u30F3\u30BB\u30EB","\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u3067 '{0}' \u3092\u3084\u308A\u76F4\u3057\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002{1}","\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u3067 '{0}' \u3092\u3084\u308A\u76F4\u3057\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002{1}","{1} \u306B\u5909\u66F4\u304C\u52A0\u3048\u3089\u308C\u305F\u305F\u3081\u3001\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u3067 '{0}' \u3092\u518D\u5B9F\u884C\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F","{1} \u3067\u5143\u306B\u623B\u3059\u307E\u305F\u306F\u3084\u308A\u76F4\u3057\u64CD\u4F5C\u304C\u65E2\u306B\u5B9F\u884C\u3055\u308C\u3066\u3044\u308B\u305F\u3081\u3001\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u306B\u5BFE\u3057\u3066 '{0}' \u3092\u3084\u308A\u76F4\u3059\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F","\u5143\u306B\u623B\u3059\u307E\u305F\u306F\u3084\u308A\u76F4\u3057\u64CD\u4F5C\u304C\u305D\u306E\u671F\u9593\u306B\u5B9F\u884C\u4E2D\u3067\u3042\u3063\u305F\u305F\u3081\u3001\u3059\u3079\u3066\u306E\u30D5\u30A1\u30A4\u30EB\u306B\u5BFE\u3057\u3066 '{0}' \u3092\u3084\u308A\u76F4\u3059\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F","\u5143\u306B\u623B\u3059\u307E\u305F\u306F\u3084\u308A\u76F4\u3057\u64CD\u4F5C\u304C\u65E2\u306B\u5B9F\u884C\u3055\u308C\u3066\u3044\u308B\u305F\u3081\u3001'{0}' \u3092\u3084\u308A\u76F4\u3059\u3053\u3068\u306F\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002"]}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.js new file mode 100644 index 0000000..2286f81 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.js @@ -0,0 +1 @@ +define("vs/editor/editor.main.nls",{"vs/base/browser/ui/actionbar/actionViewItems":["{0} ({1})"],"vs/base/browser/ui/findinput/findInput":["input"],"vs/base/browser/ui/findinput/findInputCheckboxes":["Match Case","Match Whole Word","Use Regular Expression"],"vs/base/browser/ui/findinput/replaceInput":["input","Preserve Case"],"vs/base/browser/ui/iconLabel/iconLabel":["Loading..."],"vs/base/browser/ui/inputbox/inputBox":["Error: {0}","Warning: {0}","Info: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["Unbound"],"vs/base/browser/ui/menu/menu":["{0} ({1})"],"vs/base/browser/ui/tree/abstractTree":["Clear","Disable Filter on Type","Enable Filter on Type","No elements found","Matched {0} out of {1} elements"],"vs/base/common/actions":["(empty)"],"vs/base/common/errorMessage":["{0}: {1}","A system error occurred ({0})","An unknown error occurred. Please consult the log for more details.","An unknown error occurred. Please consult the log for more details.","{0} ({1} errors in total)","An unknown error occurred. Please consult the log for more details."],"vs/base/common/keybindingLabels":["Ctrl","Shift","Alt","Windows","Ctrl","Shift","Alt","Super","Control","Shift","Alt","Command","Control","Shift","Alt","Windows","Control","Shift","Alt","Super"],"vs/base/parts/quickinput/browser/quickInput":["Back","{0}/{1}","Type to narrow down results.","{0} Results","{0} Selected","OK","Custom","Back ({0})","Back"],"vs/base/parts/quickinput/browser/quickInputList":["Quick Input"],"vs/editor/browser/controller/coreCommands":["Stick to the end even when going to longer lines","Stick to the end even when going to longer lines"],"vs/editor/browser/controller/textAreaHandler":["editor","The editor is not accessible at this time. Press {0} for options."],"vs/editor/browser/core/keybindingCancellation":["Whether the editor runs a cancellable operation, e.g. like 'Peek References'"],"vs/editor/browser/editorExtensions":["&&Undo","Undo","&&Redo","Redo","&&Select All","Select All"],"vs/editor/browser/widget/codeEditorWidget":["The number of cursors has been limited to {0}."],"vs/editor/browser/widget/diffEditorWidget":["Line decoration for inserts in the diff editor.","Line decoration for removals in the diff editor.","Cannot compare files because one file is too large."],"vs/editor/browser/widget/diffReview":["Icon for 'Insert' in diff review.","Icon for 'Remove' in diff review.","Icon for 'Close' in diff review.","Close","no lines changed","1 line changed","{0} lines changed","Difference {0} of {1}: original line {2}, {3}, modified line {4}, {5}","blank","{0} unchanged line {1}","{0} original line {1} modified line {2}","+ {0} modified line {1}","- {0} original line {1}","Go to Next Difference","Go to Previous Difference"],"vs/editor/browser/widget/inlineDiffMargin":["Copy deleted lines","Copy deleted line","Copy deleted line ({0})","Revert this change","Copy deleted line ({0})"],"vs/editor/common/config/commonEditorConfig":["Editor","The number of spaces a tab is equal to. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on.","Insert spaces when pressing `Tab`. This setting is overridden based on the file contents when `#editor.detectIndentation#` is on.","Controls whether `#editor.tabSize#` and `#editor.insertSpaces#` will be automatically detected when a file is opened based on the file contents.","Remove trailing auto inserted whitespace.","Special handling for large files to disable certain memory intensive features.","Controls whether completions should be computed based on words in the document.","Only suggest words from the active document.","Suggest words from all open documents of the same language.","Suggest words from all open documents.","Controls from what documents word based completions are computed.","Semantic highlighting enabled for all color themes.","Semantic highlighting disabled for all color themes.","Semantic highlighting is configured by the current color theme's `semanticHighlighting` setting.","Controls whether the semanticHighlighting is shown for the languages that support it.","Keep peek editors open even when double clicking their content or when hitting `Escape`.","Lines above this length will not be tokenized for performance reasons","Timeout in milliseconds after which diff computation is cancelled. Use 0 for no timeout.","Controls whether the diff editor shows the diff side by side or inline.","When enabled, the diff editor ignores changes in leading or trailing whitespace.","Controls whether the diff editor shows +/- indicators for added/removed changes.","Controls whether the editor shows CodeLens.","Lines will never wrap.","Lines will wrap at the viewport width.","Lines will wrap according to the `#editor.wordWrap#` setting."],"vs/editor/common/config/editorOptions":["The editor will use platform APIs to detect when a Screen Reader is attached.","The editor will be permanently optimized for usage with a Screen Reader. Word wrapping will be disabled.","The editor will never be optimized for usage with a Screen Reader.","Controls whether the editor should run in a mode where it is optimized for screen readers. Setting to on will disable word wrapping.","Controls whether a space character is inserted when commenting.","Controls if empty lines should be ignored with toggle, add or remove actions for line comments.","Controls whether copying without a selection copies the current line.","Controls whether the cursor should jump to find matches while typing.","Controls whether the search string in the Find Widget is seeded from the editor selection.","Never turn on Find in selection automatically (default)","Always turn on Find in selection automatically","Turn on Find in selection automatically when multiple lines of content are selected.","Controls the condition for turning on find in selection automatically.","Controls whether the Find Widget should read or modify the shared find clipboard on macOS.","Controls whether the Find Widget should add extra lines on top of the editor. When true, you can scroll beyond the first line when the Find Widget is visible.","Controls whether the search automatically restarts from the beginning (or the end) when no further matches can be found.","Enables/Disables font ligatures ('calt' and 'liga' font features). Change this to a string for fine-grained control of the 'font-feature-settings' CSS property.","Explicit 'font-feature-settings' CSS property. A boolean can be passed instead if one only needs to turn on/off ligatures.","Configures font ligatures or font features. Can be either a boolean to enable/disable ligatures or a string for the value of the CSS 'font-feature-settings' property.","Controls the font size in pixels.",'Only "normal" and "bold" keywords or numbers between 1 and 1000 are allowed.','Controls the font weight. Accepts "normal" and "bold" keywords or numbers between 1 and 1000.',"Show peek view of the results (default)","Go to the primary result and show a peek view","Go to the primary result and enable peek-less navigation to others","This setting is deprecated, please use separate settings like 'editor.editor.gotoLocation.multipleDefinitions' or 'editor.editor.gotoLocation.multipleImplementations' instead.","Controls the behavior the 'Go to Definition'-command when multiple target locations exist.","Controls the behavior the 'Go to Type Definition'-command when multiple target locations exist.","Controls the behavior the 'Go to Declaration'-command when multiple target locations exist.","Controls the behavior the 'Go to Implementations'-command when multiple target locations exist.","Controls the behavior the 'Go to References'-command when multiple target locations exist.","Alternative command id that is being executed when the result of 'Go to Definition' is the current location.","Alternative command id that is being executed when the result of 'Go to Type Definition' is the current location.","Alternative command id that is being executed when the result of 'Go to Declaration' is the current location.","Alternative command id that is being executed when the result of 'Go to Implementation' is the current location.","Alternative command id that is being executed when the result of 'Go to Reference' is the current location.","Controls whether the hover is shown.","Controls the delay in milliseconds after which the hover is shown.","Controls whether the hover should remain visible when mouse is moved over it.","Enables the code action lightbulb in the editor.","Enables the inline hints in the editor.","Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.","Controls font family of inline hints in the editor.","Controls the line height. Use 0 to compute the line height from the font size.","Controls whether the minimap is shown.","The minimap has the same size as the editor contents (and might scroll).","The minimap will stretch or shrink as necessary to fill the height of the editor (no scrolling).","The minimap will shrink as necessary to never be larger than the editor (no scrolling).","Controls the size of the minimap.","Controls the side where to render the minimap.","Controls when the minimap slider is shown.","Scale of content drawn in the minimap: 1, 2 or 3.","Render the actual characters on a line as opposed to color blocks.","Limit the width of the minimap to render at most a certain number of columns.","Controls the amount of space between the top edge of the editor and the first line.","Controls the amount of space between the bottom edge of the editor and the last line.","Enables a pop-up that shows parameter documentation and type information as you type.","Controls whether the parameter hints menu cycles or closes when reaching the end of the list.","Enable quick suggestions inside strings.","Enable quick suggestions inside comments.","Enable quick suggestions outside of strings and comments.","Controls whether suggestions should automatically show up while typing.","Line numbers are not rendered.","Line numbers are rendered as absolute number.","Line numbers are rendered as distance in lines to cursor position.","Line numbers are rendered every 10 lines.","Controls the display of line numbers.","Number of monospace characters at which this editor ruler will render.","Color of this editor ruler.","Render vertical rulers after a certain number of monospace characters. Use multiple values for multiple rulers. No rulers are drawn if array is empty.","Insert suggestion without overwriting text right of the cursor.","Insert suggestion and overwrite text right of the cursor.","Controls whether words are overwritten when accepting completions. Note that this depends on extensions opting into this feature.","Controls whether filtering and sorting suggestions accounts for small typos.","Controls whether sorting favours words that appear close to the cursor.","Controls whether remembered suggestion selections are shared between multiple workspaces and windows (needs `#editor.suggestSelection#`).","Controls whether an active snippet prevents quick suggestions.","Controls whether to show or hide icons in suggestions.","Controls the visibility of the status bar at the bottom of the suggest widget.","Controls whether suggest details show inline with the label or only in the details widget","This setting is deprecated. The suggest widget can now be resized.","This setting is deprecated, please use separate settings like 'editor.suggest.showKeywords' or 'editor.suggest.showSnippets' instead.","When enabled IntelliSense shows `method`-suggestions.","When enabled IntelliSense shows `function`-suggestions.","When enabled IntelliSense shows `constructor`-suggestions.","When enabled IntelliSense shows `field`-suggestions.","When enabled IntelliSense shows `variable`-suggestions.","When enabled IntelliSense shows `class`-suggestions.","When enabled IntelliSense shows `struct`-suggestions.","When enabled IntelliSense shows `interface`-suggestions.","When enabled IntelliSense shows `module`-suggestions.","When enabled IntelliSense shows `property`-suggestions.","When enabled IntelliSense shows `event`-suggestions.","When enabled IntelliSense shows `operator`-suggestions.","When enabled IntelliSense shows `unit`-suggestions.","When enabled IntelliSense shows `value`-suggestions.","When enabled IntelliSense shows `constant`-suggestions.","When enabled IntelliSense shows `enum`-suggestions.","When enabled IntelliSense shows `enumMember`-suggestions.","When enabled IntelliSense shows `keyword`-suggestions.","When enabled IntelliSense shows `text`-suggestions.","When enabled IntelliSense shows `color`-suggestions.","When enabled IntelliSense shows `file`-suggestions.","When enabled IntelliSense shows `reference`-suggestions.","When enabled IntelliSense shows `customcolor`-suggestions.","When enabled IntelliSense shows `folder`-suggestions.","When enabled IntelliSense shows `typeParameter`-suggestions.","When enabled IntelliSense shows `snippet`-suggestions.","When enabled IntelliSense shows `user`-suggestions.","When enabled IntelliSense shows `issues`-suggestions.","Whether leading and trailing whitespace should always be selected.","Controls whether suggestions should be accepted on commit characters. For example, in JavaScript, the semi-colon (`;`) can be a commit character that accepts a suggestion and types that character.","Only accept a suggestion with `Enter` when it makes a textual change.","Controls whether suggestions should be accepted on `Enter`, in addition to `Tab`. Helps to avoid ambiguity between inserting new lines or accepting suggestions.","Controls the number of lines in the editor that can be read out by a screen reader. Warning: this has a performance implication for numbers larger than the default.","Editor content","Use language configurations to determine when to autoclose brackets.","Autoclose brackets only when the cursor is to the left of whitespace.","Controls whether the editor should automatically close brackets after the user adds an opening bracket.","Type over closing quotes or brackets only if they were automatically inserted.","Controls whether the editor should type over closing quotes or brackets.","Use language configurations to determine when to autoclose quotes.","Autoclose quotes only when the cursor is to the left of whitespace.","Controls whether the editor should automatically close quotes after the user adds an opening quote.","The editor will not insert indentation automatically.","The editor will keep the current line's indentation.","The editor will keep the current line's indentation and honor language defined brackets.","The editor will keep the current line's indentation, honor language defined brackets and invoke special onEnterRules defined by languages.","The editor will keep the current line's indentation, honor language defined brackets, invoke special onEnterRules defined by languages, and honor indentationRules defined by languages.","Controls whether the editor should automatically adjust the indentation when users type, paste, move or indent lines.","Use language configurations to determine when to automatically surround selections.","Surround with quotes but not brackets.","Surround with brackets but not quotes.","Controls whether the editor should automatically surround selections when typing quotes or brackets.","Emulate selection behaviour of tab characters when using spaces for indentation. Selection will stick to tab stops.","Controls whether the editor shows CodeLens.","Controls the font family for CodeLens.","Controls the font size in pixels for CodeLens. When set to `0`, the 90% of `#editor.fontSize#` is used.","Controls whether the editor should render the inline color decorators and color picker.","Enable that the selection with the mouse and keys is doing column selection.","Controls whether syntax highlighting should be copied into the clipboard.","Control the cursor animation style.","Controls whether the smooth caret animation should be enabled.","Controls the cursor style.","Controls the minimal number of visible leading and trailing lines surrounding the cursor. Known as 'scrollOff' or 'scrollOffset' in some other editors.","`cursorSurroundingLines` is enforced only when triggered via the keyboard or API.","`cursorSurroundingLines` is enforced always.","Controls when `cursorSurroundingLines` should be enforced.","Controls the width of the cursor when `#editor.cursorStyle#` is set to `line`.","Controls whether the editor should allow moving selections via drag and drop.","Scrolling speed multiplier when pressing `Alt`.","Controls whether the editor has code folding enabled.","Use a language-specific folding strategy if available, else the indentation-based one.","Use the indentation-based folding strategy.","Controls the strategy for computing folding ranges.","Controls whether the editor should highlight folded ranges.","Controls whether clicking on the empty content after a folded line will unfold the line.","Controls the font family.","Controls whether the editor should automatically format the pasted content. A formatter must be available and the formatter should be able to format a range in a document.","Controls whether the editor should automatically format the line after typing.","Controls whether the editor should render the vertical glyph margin. Glyph margin is mostly used for debugging.","Controls whether the cursor should be hidden in the overview ruler.","Controls whether the editor should highlight the active indent guide.","Controls the letter spacing in pixels.","Controls whether the editor has linked editing enabled. Depending on the language, related symbols, e.g. HTML tags, are updated while editing.","Controls whether the editor should detect links and make them clickable.","Highlight matching brackets.","A multiplier to be used on the `deltaX` and `deltaY` of mouse wheel scroll events.","Zoom the font of the editor when using mouse wheel and holding `Ctrl`.","Merge multiple cursors when they are overlapping.","Maps to `Control` on Windows and Linux and to `Command` on macOS.","Maps to `Alt` on Windows and Linux and to `Option` on macOS.","The modifier to be used to add multiple cursors with the mouse. The Go To Definition and Open Link mouse gestures will adapt such that they do not conflict with the multicursor modifier. [Read more](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).","Each cursor pastes a single line of the text.","Each cursor pastes the full text.","Controls pasting when the line count of the pasted text matches the cursor count.","Controls whether the editor should highlight semantic symbol occurrences.","Controls whether a border should be drawn around the overview ruler.","Focus the tree when opening peek","Focus the editor when opening peek","Controls whether to focus the inline editor or the tree in the peek widget.","Controls whether the Go to Definition mouse gesture always opens the peek widget.","Controls the delay in milliseconds after which quick suggestions will show up.","Controls whether the editor auto renames on type.","Deprecated, use `editor.linkedEditing` instead.","Controls whether the editor should render control characters.","Controls whether the editor should render indent guides.","Render last line number when the file ends with a newline.","Highlights both the gutter and the current line.","Controls how the editor should render the current line highlight.","Controls if the editor should render the current line highlight only when the editor is focused","Render whitespace characters except for single spaces between words.","Render whitespace characters only on selected text.","Render only trailing whitespace characters","Controls how the editor should render whitespace characters.","Controls whether selections should have rounded corners.","Controls the number of extra characters beyond which the editor will scroll horizontally.","Controls whether the editor will scroll beyond the last line.","Scroll only along the predominant axis when scrolling both vertically and horizontally at the same time. Prevents horizontal drift when scrolling vertically on a trackpad.","Controls whether the Linux primary clipboard should be supported.","Controls whether the editor should highlight matches similar to the selection.","Always show the folding controls.","Only show the folding controls when the mouse is over the gutter.","Controls when the folding controls on the gutter are shown.","Controls fading out of unused code.","Controls strikethrough deprecated variables.","Show snippet suggestions on top of other suggestions.","Show snippet suggestions below other suggestions.","Show snippets suggestions with other suggestions.","Do not show snippet suggestions.","Controls whether snippets are shown with other suggestions and how they are sorted.","Controls whether the editor will scroll using an animation.","Font size for the suggest widget. When set to `0`, the value of `#editor.fontSize#` is used.","Line height for the suggest widget. When set to `0`, the value of `#editor.lineHeight#` is used. The minimum value is 8.","Controls whether suggestions should automatically show up when typing trigger characters.","Always select the first suggestion.","Select recent suggestions unless further typing selects one, e.g. `console.| -> console.log` because `log` has been completed recently.","Select suggestions based on previous prefixes that have completed those suggestions, e.g. `co -> console` and `con -> const`.","Controls how suggestions are pre-selected when showing the suggest list.","Tab complete will insert the best matching suggestion when pressing tab.","Disable tab completions.","Tab complete snippets when their prefix match. Works best when 'quickSuggestions' aren't enabled.","Enables tab completions.","Unusual line terminators are automatically removed.","Unusual line terminators are ignored.","Unusual line terminators prompt to be removed.","Remove unusual line terminators that might cause problems.","Inserting and deleting whitespace follows tab stops.","Characters that will be used as word separators when doing word related navigations or operations.","Lines will never wrap.","Lines will wrap at the viewport width.","Lines will wrap at `#editor.wordWrapColumn#`.","Lines will wrap at the minimum of viewport and `#editor.wordWrapColumn#`.","Controls how lines should wrap.","Controls the wrapping column of the editor when `#editor.wordWrap#` is `wordWrapColumn` or `bounded`.","No indentation. Wrapped lines begin at column 1.","Wrapped lines get the same indentation as the parent.","Wrapped lines get +1 indentation toward the parent.","Wrapped lines get +2 indentation toward the parent.","Controls the indentation of wrapped lines.","Assumes that all characters are of the same width. This is a fast algorithm that works correctly for monospace fonts and certain scripts (like Latin characters) where glyphs are of equal width.","Delegates wrapping points computation to the browser. This is a slow algorithm, that might cause freezes for large files, but it works correctly in all cases.","Controls the algorithm that computes wrapping points."],"vs/editor/common/editorContextKeys":["Whether the editor text has focus (cursor is blinking)","Whether the editor or an editor widget has focus (e.g. focus is in the find widget)","Whether an editor or a rich text input has focus (cursor is blinking)","Whether the editor is read only","Whether the context is a diff editor","Whether `editor.columnSelection` is enabled","Whether the editor has text selected","Whether the editor has multiple selections","Whether `Tab` will move focus out of the editor","Whether the editor hover is visible","Whether the editor is part of a larger editor (e.g. notebooks)","The language identifier of the editor","Whether the editor has a completion item provider","Whether the editor has a code actions provider","Whether the editor has a code lens provider","Whether the editor has a definition provider","Whether the editor has a declaration provider","Whether the editor has an implementation provider","Whether the editor has a type definition provider","Whether the editor has a hover provider","Whether the editor has a document highlight provider","Whether the editor has a document symbol provider","Whether the editor has a reference provider","Whether the editor has a rename provider","Whether the editor has a signature help provider","Whether the editor has an inline hints provider","Whether the editor has a document formatting provider","Whether the editor has a document selection formatting provider","Whether the editor has multiple document formatting providers","Whether the editor has multiple document selection formatting providers"],"vs/editor/common/model/editStack":["Typing"],"vs/editor/common/modes/modesRegistry":["Plain Text"],"vs/editor/common/standaloneStrings":["No selection","Line {0}, Column {1} ({2} selected)","Line {0}, Column {1}","{0} selections ({1} characters selected)","{0} selections","Now changing the setting `accessibilitySupport` to 'on'.","Now opening the Editor Accessibility documentation page."," in a read-only pane of a diff editor."," in a pane of a diff editor."," in a read-only code editor"," in a code editor","To configure the editor to be optimized for usage with a Screen Reader press Command+E now.","To configure the editor to be optimized for usage with a Screen Reader press Control+E now.","The editor is configured to be optimized for usage with a Screen Reader.","The editor is configured to never be optimized for usage with a Screen Reader, which is not the case at this time.","Pressing Tab in the current editor will move focus to the next focusable element. Toggle this behavior by pressing {0}.","Pressing Tab in the current editor will move focus to the next focusable element. The command {0} is currently not triggerable by a keybinding.","Pressing Tab in the current editor will insert the tab character. Toggle this behavior by pressing {0}.","Pressing Tab in the current editor will insert the tab character. The command {0} is currently not triggerable by a keybinding.","Press Command+H now to open a browser window with more information related to editor accessibility.","Press Control+H now to open a browser window with more information related to editor accessibility.","You can dismiss this tooltip and return to the editor by pressing Escape or Shift+Escape.","Show Accessibility Help","Developer: Inspect Tokens","Go to Line/Column...","Show all Quick Access Providers","Command Palette","Show And Run Commands","Go to Symbol...","Go to Symbol by Category...","Editor content","Press Alt+F1 for Accessibility Options.","Toggle High Contrast Theme","Made {0} edits in {1} files"],"vs/editor/common/view/editorColorRegistry":["Background color for the highlight of line at the cursor position.","Background color for the border around the line at the cursor position.","Background color of highlighted ranges, like by quick open and find features. The color must not be opaque so as not to hide underlying decorations.","Background color of the border around highlighted ranges.","Background color of highlighted symbol, like for go to definition or go next/previous symbol. The color must not be opaque so as not to hide underlying decorations.","Background color of the border around highlighted symbols.","Color of the editor cursor.","The background color of the editor cursor. Allows customizing the color of a character overlapped by a block cursor.","Color of whitespace characters in the editor.","Color of the editor indentation guides.","Color of the active editor indentation guides.","Color of editor line numbers.","Color of editor active line number","Id is deprecated. Use 'editorLineNumber.activeForeground' instead.","Color of editor active line number","Color of the editor rulers.","Foreground color of editor CodeLens","Background color behind matching brackets","Color for matching brackets boxes","Color of the overview ruler border.","Background color of the editor overview ruler. Only used when the minimap is enabled and placed on the right side of the editor.","Background color of the editor gutter. The gutter contains the glyph margins and the line numbers.","Border color of unnecessary (unused) source code in the editor.","Opacity of unnecessary (unused) source code in the editor. For example, \"#000000c0\" will render the code with 75% opacity. For high contrast themes, use the 'editorUnnecessaryCode.border' theme color to underline unnecessary code instead of fading it out.","Overview ruler marker color for range highlights. The color must not be opaque so as not to hide underlying decorations.","Overview ruler marker color for errors.","Overview ruler marker color for warnings.","Overview ruler marker color for infos."],"vs/editor/contrib/anchorSelect/anchorSelect":["Selection Anchor","Anchor set at {0}:{1}","Set Selection Anchor","Go to Selection Anchor","Select from Anchor to Cursor","Cancel Selection Anchor"],"vs/editor/contrib/bracketMatching/bracketMatching":["Overview ruler marker color for matching brackets.","Go to Bracket","Select to Bracket","Go to &&Bracket"],"vs/editor/contrib/caretOperations/caretOperations":["Move Selected Text Left","Move Selected Text Right"],"vs/editor/contrib/caretOperations/transpose":["Transpose Letters"],"vs/editor/contrib/clipboard/clipboard":["Cu&&t","Cut","Cut","&&Copy","Copy","Copy","&&Paste","Paste","Paste","Copy With Syntax Highlighting"],"vs/editor/contrib/codeAction/codeActionCommands":["Kind of the code action to run.","Controls when the returned actions are applied.","Always apply the first returned code action.","Apply the first returned code action if it is the only one.","Do not apply the returned code actions.","Controls if only preferred code actions should be returned.","An unknown error occurred while applying the code action","Quick Fix...","No code actions available","No preferred code actions for '{0}' available","No code actions for '{0}' available","No preferred code actions available","No code actions available","Refactor...","No preferred refactorings for '{0}' available","No refactorings for '{0}' available","No preferred refactorings available","No refactorings available","Source Action...","No preferred source actions for '{0}' available","No source actions for '{0}' available","No preferred source actions available","No source actions available","Organize Imports","No organize imports action available","Fix All","No fix all action available","Auto Fix...","No auto fixes available"],"vs/editor/contrib/codeAction/lightBulbWidget":["Show Fixes. Preferred Fix Available ({0})","Show Fixes ({0})","Show Fixes"],"vs/editor/contrib/codelens/codelensController":["Show CodeLens Commands For Current Line"],"vs/editor/contrib/comment/comment":["Toggle Line Comment","&&Toggle Line Comment","Add Line Comment","Remove Line Comment","Toggle Block Comment","Toggle &&Block Comment"],"vs/editor/contrib/contextmenu/contextmenu":["Show Editor Context Menu"],"vs/editor/contrib/cursorUndo/cursorUndo":["Cursor Undo","Cursor Redo"],"vs/editor/contrib/find/findController":["Find","&&Find","Find With Selection","Find Next","Find Next","Find Previous","Find Previous","Find Next Selection","Find Previous Selection","Replace","&&Replace"],"vs/editor/contrib/find/findWidget":["Icon for 'Find in Selection' in the editor find widget.","Icon to indicate that the editor find widget is collapsed.","Icon to indicate that the editor find widget is expanded.","Icon for 'Replace' in the editor find widget.","Icon for 'Replace All' in the editor find widget.","Icon for 'Find Previous' in the editor find widget.","Icon for 'Find Next' in the editor find widget.","Find","Find","Previous match","Next match","Find in selection","Close","Replace","Replace","Replace","Replace All","Toggle Replace mode","Only the first {0} results are highlighted, but all find operations work on the entire text.","{0} of {1}","No results","{0} found","{0} found for '{1}'","{0} found for '{1}', at {2}","{0} found for '{1}'","Ctrl+Enter now inserts line break instead of replacing all. You can modify the keybinding for editor.action.replaceAll to override this behavior."],"vs/editor/contrib/folding/folding":["Unfold","Unfold Recursively","Fold","Toggle Fold","Fold Recursively","Fold All Block Comments","Fold All Regions","Unfold All Regions","Fold All","Unfold All","Fold Level {0}","Background color behind folded ranges. The color must not be opaque so as not to hide underlying decorations.","Color of the folding control in the editor gutter."],"vs/editor/contrib/folding/foldingDecorations":["Icon for expanded ranges in the editor glyph margin.","Icon for collapsed ranges in the editor glyph margin."],"vs/editor/contrib/fontZoom/fontZoom":["Editor Font Zoom In","Editor Font Zoom Out","Editor Font Zoom Reset"],"vs/editor/contrib/format/format":["Made 1 formatting edit on line {0}","Made {0} formatting edits on line {1}","Made 1 formatting edit between lines {0} and {1}","Made {0} formatting edits between lines {1} and {2}"],"vs/editor/contrib/format/formatActions":["Format Document","Format Selection"],"vs/editor/contrib/gotoError/gotoError":["Go to Next Problem (Error, Warning, Info)","Icon for goto next marker.","Go to Previous Problem (Error, Warning, Info)","Icon for goto previous marker.","Go to Next Problem in Files (Error, Warning, Info)","Next &&Problem","Go to Previous Problem in Files (Error, Warning, Info)","Previous &&Problem"],"vs/editor/contrib/gotoError/gotoErrorWidget":["Error","Warning","Info","Hint","{0} at {1}. ","{0} of {1} problems","{0} of {1} problem","Editor marker navigation widget error color.","Editor marker navigation widget warning color.","Editor marker navigation widget info color.","Editor marker navigation widget background."],"vs/editor/contrib/gotoSymbol/goToCommands":["Peek","Definitions","No definition found for '{0}'","No definition found","Go to Definition","Go to &&Definition","Open Definition to the Side","Peek Definition","Declarations","No declaration found for '{0}'","No declaration found","Go to Declaration","Go to &&Declaration","No declaration found for '{0}'","No declaration found","Peek Declaration","Type Definitions","No type definition found for '{0}'","No type definition found","Go to Type Definition","Go to &&Type Definition","Peek Type Definition","Implementations","No implementation found for '{0}'","No implementation found","Go to Implementations","Go to &&Implementations","Peek Implementations","No references found for '{0}'","No references found","Go to References","Go to &&References","References","Peek References","References","Go To Any Symbol","Locations","No results for '{0}'","References"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["Click to show {0} definitions."],"vs/editor/contrib/gotoSymbol/peek/referencesController":["Loading...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["{0} references","{0} reference","References"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["no preview available","No results","References"],"vs/editor/contrib/gotoSymbol/referencesModel":["symbol in {0} on line {1} at column {2}","symbol in {0} on line {1} at column {2}, {3}","1 symbol in {0}, full path {1}","{0} symbols in {1}, full path {2}","No results found","Found 1 symbol in {0}","Found {0} symbols in {1}","Found {0} symbols in {1} files"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["Symbol {0} of {1}, {2} for next","Symbol {0} of {1}"],"vs/editor/contrib/hover/hover":["Show Hover","Show Definition Preview Hover"],"vs/editor/contrib/hover/markdownHoverParticipant":["Loading..."],"vs/editor/contrib/hover/markerHoverParticipant":["View Problem","No quick fixes available","Checking for quick fixes...","No quick fixes available","Quick Fix..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["Replace with Previous Value","Replace with Next Value"],"vs/editor/contrib/indentation/indentation":["Convert Indentation to Spaces","Convert Indentation to Tabs","Configured Tab Size","Select Tab Size for Current File","Indent Using Tabs","Indent Using Spaces","Detect Indentation from Content","Reindent Lines","Reindent Selected Lines"],"vs/editor/contrib/linesOperations/linesOperations":["Copy Line Up","&&Copy Line Up","Copy Line Down","Co&&py Line Down","Duplicate Selection","&&Duplicate Selection","Move Line Up","Mo&&ve Line Up","Move Line Down","Move &&Line Down","Sort Lines Ascending","Sort Lines Descending","Trim Trailing Whitespace","Delete Line","Indent Line","Outdent Line","Insert Line Above","Insert Line Below","Delete All Left","Delete All Right","Join Lines","Transpose characters around the cursor","Transform to Uppercase","Transform to Lowercase","Transform to Title Case","Transform to Snake Case"],"vs/editor/contrib/linkedEditing/linkedEditing":["Start Linked Editing","Background color when the editor auto renames on type."],"vs/editor/contrib/links/links":["Execute command","Follow link","cmd + click","ctrl + click","option + click","alt + click","Execute command {0}","Failed to open this link because it is not well-formed: {0}","Failed to open this link because its target is missing.","Open Link"],"vs/editor/contrib/message/messageController":["Whether the editor is currently showing an inline message","Cannot edit in read-only editor"],"vs/editor/contrib/multicursor/multicursor":["Add Cursor Above","&&Add Cursor Above","Add Cursor Below","A&&dd Cursor Below","Add Cursors to Line Ends","Add C&&ursors to Line Ends","Add Cursors To Bottom","Add Cursors To Top","Add Selection To Next Find Match","Add &&Next Occurrence","Add Selection To Previous Find Match","Add P&&revious Occurrence","Move Last Selection To Next Find Match","Move Last Selection To Previous Find Match","Select All Occurrences of Find Match","Select All &&Occurrences","Change All Occurrences"],"vs/editor/contrib/parameterHints/parameterHints":["Trigger Parameter Hints"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["Icon for show next parameter hint.","Icon for show previous parameter hint.","{0}, hint"],"vs/editor/contrib/peekView/peekView":["Close","Background color of the peek view title area.","Color of the peek view title.","Color of the peek view title info.","Color of the peek view borders and arrow.","Background color of the peek view result list.","Foreground color for line nodes in the peek view result list.","Foreground color for file nodes in the peek view result list.","Background color of the selected entry in the peek view result list.","Foreground color of the selected entry in the peek view result list.","Background color of the peek view editor.","Background color of the gutter in the peek view editor.","Match highlight color in the peek view result list.","Match highlight color in the peek view editor.","Match highlight border in the peek view editor."],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["Open a text editor first to go to a line.","Go to line {0} and column {1}.","Go to line {0}.","Current Line: {0}, Character: {1}. Type a line number between 1 and {2} to navigate to.","Current Line: {0}, Character: {1}. Type a line number to navigate to."],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["To go to a symbol, first open a text editor with symbol information.","The active text editor does not provide symbol information.","No matching editor symbols","No editor symbols","Open to the Side","Open to the Bottom","symbols ({0})","properties ({0})","methods ({0})","functions ({0})","constructors ({0})","variables ({0})","classes ({0})","structs ({0})","events ({0})","operators ({0})","interfaces ({0})","namespaces ({0})","packages ({0})","type parameters ({0})","modules ({0})","properties ({0})","enumerations ({0})","enumeration members ({0})","strings ({0})","files ({0})","arrays ({0})","numbers ({0})","booleans ({0})","objects ({0})","keys ({0})","fields ({0})","constants ({0})"],"vs/editor/contrib/rename/rename":["No result.","An unknown error occurred while resolving rename location","Renaming '{0}'","Renaming {0}","Successfully renamed '{0}' to '{1}'. Summary: {2}","Rename failed to apply edits","Rename failed to compute edits","Rename Symbol","Enable/disable the ability to preview changes before renaming"],"vs/editor/contrib/rename/renameInputField":["Rename input. Type new name and press Enter to commit.","{0} to Rename, {1} to Preview"],"vs/editor/contrib/smartSelect/smartSelect":["Expand Selection","&&Expand Selection","Shrink Selection","&&Shrink Selection"],"vs/editor/contrib/snippet/snippetVariables":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sun","Mon","Tue","Wed","Thu","Fri","Sat","January","February","March","April","May","June","July","August","September","October","November","December","Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"vs/editor/contrib/suggest/suggestController":["Accepting '{0}' made {1} additional edits","Trigger Suggest","Insert","Insert","Replace","Replace","Insert","show less","show more","Reset Suggest Widget Size"],"vs/editor/contrib/suggest/suggestWidget":["Background color of the suggest widget.","Border color of the suggest widget.","Foreground color of the suggest widget.","Background color of the selected entry in the suggest widget.","Color of the match highlights in the suggest widget.","Loading...","No suggestions.","{0}, docs: {1}","Suggest"],"vs/editor/contrib/suggest/suggestWidgetDetails":["Close","Loading..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["Icon for more information in the suggest widget.","Read More"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for class symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for color symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for constant symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for constructor symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for enumerator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for enumerator member symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for event symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for field symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for file symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for folder symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for function symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for interface symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for key symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for keyword symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for method symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for module symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for namespace symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for null symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for number symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for object symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for operator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for package symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for property symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for reference symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for snippet symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for string symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for struct symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for text symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for type parameter symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.","The foreground color for variable symbols. These symbols appear in the outline, breadcrumb, and suggest widget."],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["Toggle Tab Key Moves Focus","Pressing Tab will now move focus to the next focusable element","Pressing Tab will now insert the tab character"],"vs/editor/contrib/tokenization/tokenization":["Developer: Force Retokenize"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["Unusual Line Terminators","Detected unusual line terminators","This file contains one or more unusual line terminator characters, like Line Separator (LS) or Paragraph Separator (PS).\n\nIt is recommended to remove them from the file. This can be configured via `editor.unusualLineTerminators`.","Fix this file","Ignore problem for this file"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["Background color of a symbol during read-access, like reading a variable. The color must not be opaque so as not to hide underlying decorations.","Background color of a symbol during write-access, like writing to a variable. The color must not be opaque so as not to hide underlying decorations.","Border color of a symbol during read-access, like reading a variable.","Border color of a symbol during write-access, like writing to a variable.","Overview ruler marker color for symbol highlights. The color must not be opaque so as not to hide underlying decorations.","Overview ruler marker color for write-access symbol highlights. The color must not be opaque so as not to hide underlying decorations.","Go to Next Symbol Highlight","Go to Previous Symbol Highlight","Trigger Symbol Highlight"],"vs/editor/contrib/wordOperations/wordOperations":["Delete Word"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["Default Language Configuration Overrides","Configure editor settings to be overridden for a language.","This setting does not support per-language configuration.","Cannot register an empty property","Cannot register '{0}'. This matches property pattern '\\\\[.*\\\\]$' for describing language specific editor settings. Use 'configurationDefaults' contribution.","Cannot register '{0}'. This property is already registered."],"vs/platform/contextkey/browser/contextKeyService":["A command that returns information about context keys"],"vs/platform/contextkey/common/contextkeys":["Whether the operating system is Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["({0}) was pressed. Waiting for second key of chord...","The key combination ({0}, {1}) is not a command."],"vs/platform/list/browser/listService":["Workbench","Maps to `Control` on Windows and Linux and to `Command` on macOS.","Maps to `Alt` on Windows and Linux and to `Option` on macOS.","The modifier to be used to add an item in trees and lists to a multi-selection with the mouse (for example in the explorer, open editors and scm view). The 'Open to Side' mouse gestures - if supported - will adapt such that they do not conflict with the multiselect modifier.","Controls how to open items in trees and lists using the mouse (if supported). Note that some trees and lists might choose to ignore this setting if it is not applicable.","Controls whether lists and trees support horizontal scrolling in the workbench. Warning: turning on this setting has a performance implication.","Controls tree indentation in pixels.","Controls whether the tree should render indent guides.","Controls whether lists and trees have smooth scrolling.","Simple keyboard navigation focuses elements which match the keyboard input. Matching is done only on prefixes.","Highlight keyboard navigation highlights elements which match the keyboard input. Further up and down navigation will traverse only the highlighted elements.","Filter keyboard navigation will filter out and hide all the elements which do not match the keyboard input.","Controls the keyboard navigation style for lists and trees in the workbench. Can be simple, highlight and filter.","Controls whether keyboard navigation in lists and trees is automatically triggered simply by typing. If set to `false`, keyboard navigation is only triggered when executing the `list.toggleKeyboardNavigation` command, for which you can assign a keyboard shortcut.","Controls how tree folders are expanded when clicking the folder names. Note that some trees and lists might choose to ignore this setting if it is not applicable."],"vs/platform/markers/common/markers":["Error","Warning","Info"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","recently used","other commands","Command '{0}' resulted in an error ({1})"],"vs/platform/quickinput/browser/helpQuickAccess":["global commands","editor commands","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["Overall foreground color. This color is only used if not overridden by a component.","Overall foreground color for error messages. This color is only used if not overridden by a component.","The default color for icons in the workbench.","Overall border color for focused elements. This color is only used if not overridden by a component.","An extra border around elements to separate them from others for greater contrast.","An extra border around active elements to separate them from others for greater contrast.","Foreground color for links in text.","Background color for code blocks in text.","Shadow color of widgets such as find/replace inside the editor.","Input box background.","Input box foreground.","Input box border.","Border color of activated options in input fields.","Background color of activated options in input fields.","Foreground color of activated options in input fields.","Input validation background color for information severity.","Input validation foreground color for information severity.","Input validation border color for information severity.","Input validation background color for warning severity.","Input validation foreground color for warning severity.","Input validation border color for warning severity.","Input validation background color for error severity.","Input validation foreground color for error severity.","Input validation border color for error severity.","Dropdown background.","Dropdown foreground.","Button foreground color.","Button background color.","Button background color when hovering.","Badge background color. Badges are small information labels, e.g. for search results count.","Badge foreground color. Badges are small information labels, e.g. for search results count.","Scrollbar shadow to indicate that the view is scrolled.","Scrollbar slider background color.","Scrollbar slider background color when hovering.","Scrollbar slider background color when clicked on.","Background color of the progress bar that can show for long running operations.","Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.","Foreground color of error squigglies in the editor.","Border color of error boxes in the editor.","Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.","Foreground color of warning squigglies in the editor.","Border color of warning boxes in the editor.","Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.","Foreground color of info squigglies in the editor.","Border color of info boxes in the editor.","Foreground color of hint squigglies in the editor.","Border color of hint boxes in the editor.","Editor background color.","Editor default foreground color.","Background color of editor widgets, such as find/replace.","Foreground color of editor widgets, such as find/replace.","Border color of editor widgets. The color is only used if the widget chooses to have a border and if the color is not overridden by a widget.","Border color of the resize bar of editor widgets. The color is only used if the widget chooses to have a resize border and if the color is not overridden by a widget.","Quick picker background color. The quick picker widget is the container for pickers like the command palette.","Quick picker foreground color. The quick picker widget is the container for pickers like the command palette.","Quick picker title background color. The quick picker widget is the container for pickers like the command palette.","Quick picker background color for the focused item.","Quick picker color for grouping labels.","Quick picker color for grouping borders.","Color of the editor selection.","Color of the selected text for high contrast.","Color of the selection in an inactive editor. The color must not be opaque so as not to hide underlying decorations.","Color for regions with the same content as the selection. The color must not be opaque so as not to hide underlying decorations.","Border color for regions with the same content as the selection.","Color of the current search match.","Color of the other search matches. The color must not be opaque so as not to hide underlying decorations.","Color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.","Border color of the current search match.","Border color of the other search matches.","Border color of the range limiting the search. The color must not be opaque so as not to hide underlying decorations.","Highlight below the word for which a hover is shown. The color must not be opaque so as not to hide underlying decorations.","Background color of the editor hover.","Foreground color of the editor hover.","Border color of the editor hover.","Background color of the editor hover status bar.","Color of active links.","Foreground color of inline hints","Background color of inline hints","The color used for the lightbulb actions icon.","The color used for the lightbulb auto fix actions icon.","Background color for text that got inserted. The color must not be opaque so as not to hide underlying decorations.","Background color for text that got removed. The color must not be opaque so as not to hide underlying decorations.","Outline color for the text that got inserted.","Outline color for text that got removed.","Border color between the two text editors.","Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views.","List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.","List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.","List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.","List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.","List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.","List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.","List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.","List/Tree background color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.","List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.","List/Tree background when hovering over items using the mouse.","List/Tree foreground when hovering over items using the mouse.","List/Tree drag and drop background when moving items around using the mouse.","List/Tree foreground color of the match highlights when searching inside the list/tree.","Background color of the type filter widget in lists and trees.","Outline color of the type filter widget in lists and trees.","Outline color of the type filter widget in lists and trees, when there are no matches.","Tree stroke color for the indentation guides.","Tree stroke color for the indentation guides.","Border color of menus.","Foreground color of menu items.","Background color of menu items.","Foreground color of the selected menu item in menus.","Background color of the selected menu item in menus.","Border color of the selected menu item in menus.","Color of a separator menu item in menus.","Highlight background color of a snippet tabstop.","Highlight border color of a snippet tabstop.","Highlight background color of the final tabstop of a snippet.","Highlight border color of the final tabstop of a snippet.","Overview ruler marker color for find matches. The color must not be opaque so as not to hide underlying decorations.","Overview ruler marker color for selection highlights. The color must not be opaque so as not to hide underlying decorations.","Minimap marker color for find matches.","Minimap marker color for the editor selection.","Minimap marker color for errors.","Minimap marker color for warnings.","Minimap background color.","Minimap slider background color.","Minimap slider background color when hovering.","Minimap slider background color when clicked on.","The color used for the problems error icon.","The color used for the problems warning icon.","The color used for the problems info icon."],"vs/platform/theme/common/iconRegistry":["The id of the font to use. If not set, the font that is defined first is used.","The font character associated with the icon definition.","Icon for the close action in widgets."],"vs/platform/undoRedo/common/undoRedoService":["The following files have been closed and modified on disk: {0}.","The following files have been modified in an incompatible way: {0}.","Could not undo '{0}' across all files. {1}","Could not undo '{0}' across all files. {1}","Could not undo '{0}' across all files because changes were made to {1}","Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}","Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime","Would you like to undo '{0}' across all files?","Undo in {0} Files","Undo this File","Cancel","Could not undo '{0}' because there is already an undo or redo operation running.","Would you like to undo '{0}'?","Undo","Cancel","Could not redo '{0}' across all files. {1}","Could not redo '{0}' across all files. {1}","Could not redo '{0}' across all files because changes were made to {1}","Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}","Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime","Could not redo '{0}' because there is already an undo or redo operation running."]}); \ No newline at end of file diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ko.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ko.js new file mode 100644 index 0000000..557cb4d --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ko.js @@ -0,0 +1,6 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/define("vs/editor/editor.main.nls.ko",{"vs/base/browser/ui/actionbar/actionViewItems":["{0}({1})"],"vs/base/browser/ui/findinput/findInput":["\uC785\uB825"],"vs/base/browser/ui/findinput/findInputCheckboxes":["\uB300/\uC18C\uBB38\uC790 \uAD6C\uBD84","\uB2E8\uC5B4 \uB2E8\uC704\uB85C","\uC815\uADDC\uC2DD \uC0AC\uC6A9"],"vs/base/browser/ui/findinput/replaceInput":["\uC785\uB825","\uB300/\uC18C\uBB38\uC790 \uBCF4\uC874"],"vs/base/browser/ui/iconLabel/iconLabel":["\uB85C\uB4DC \uC911..."],"vs/base/browser/ui/inputbox/inputBox":["\uC624\uB958: {0}","\uACBD\uACE0: {0}","\uC815\uBCF4: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["\uBC14\uC778\uB529 \uC548 \uB428"],"vs/base/browser/ui/menu/menu":["{0}({1})"],"vs/base/browser/ui/tree/abstractTree":["\uC9C0\uC6B0\uAE30","\uD615\uC2DD\uC744 \uAE30\uC900\uC73C\uB85C \uD544\uD130\uB9C1 \uC0AC\uC6A9 \uC548 \uD568","\uD615\uC2DD\uC744 \uAE30\uC900\uC73C\uB85C \uD544\uD130\uB9C1 \uC0AC\uC6A9","\uCC3E\uC740 \uC694\uC18C \uC5C6\uC74C","{1}\uAC1C \uC694\uC18C \uC911 {0}\uAC1C \uC77C\uCE58"],"vs/base/common/actions":["(\uBE44\uC5B4 \uC788\uC74C)"],"vs/base/common/errorMessage":["{0}: {1}","\uC2DC\uC2A4\uD15C \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4({0}).","\uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uC790\uC138\uD55C \uB0B4\uC6A9\uC740 \uB85C\uADF8\uB97C \uCC38\uC870\uD558\uC138\uC694.","\uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uC790\uC138\uD55C \uB0B4\uC6A9\uC740 \uB85C\uADF8\uB97C \uCC38\uC870\uD558\uC138\uC694.","{0}(\uCD1D {1}\uAC1C\uC758 \uC624\uB958)","\uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uC790\uC138\uD55C \uB0B4\uC6A9\uC740 \uB85C\uADF8\uB97C \uCC38\uC870\uD558\uC138\uC694."],"vs/base/common/keybindingLabels":["Ctrl","","","Windows","Ctrl","","","\uC288\uD37C","\uC81C\uC5B4","","","\uBA85\uB839","\uC81C\uC5B4","","","Windows","\uC81C\uC5B4","","","\uC288\uD37C"],"vs/base/parts/quickinput/browser/quickInput":["\uB4A4\uB85C","{0} / {1}","\uACB0\uACFC\uC758 \uBC94\uC704\uB97C \uCD95\uC18C\uD558\uB824\uBA74 \uC785\uB825\uD558\uC138\uC694.","{0}\uAC1C \uACB0\uACFC","{0} \uC120\uD0DD\uB428","\uD655\uC778","\uC0AC\uC6A9\uC790 \uC9C0\uC815","\uB4A4\uB85C({0})","\uB4A4\uB85C"],"vs/base/parts/quickinput/browser/quickInputList":["\uBE60\uB978 \uC785\uB825"],"vs/editor/browser/controller/coreCommands":["\uB354 \uAE34 \uC904\uB85C \uC774\uB3D9\uD558\uB294 \uACBD\uC6B0\uC5D0\uB3C4 \uB05D\uC5D0 \uACE0\uC815","\uB354 \uAE34 \uC904\uB85C \uC774\uB3D9\uD558\uB294 \uACBD\uC6B0\uC5D0\uB3C4 \uB05D\uC5D0 \uACE0\uC815"],"vs/editor/browser/controller/textAreaHandler":["\uD3B8\uC9D1\uAE30","\uD604\uC7AC \uD3B8\uC9D1\uAE30\uC5D0 \uC561\uC138\uC2A4\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC635\uC158\uC744 \uBCF4\uB824\uBA74 {0}\uC744(\uB97C) \uB204\uB985\uB2C8\uB2E4."],"vs/editor/browser/core/keybindingCancellation":["\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uCDE8\uC18C \uAC00\uB2A5\uD55C \uC791\uC5C5(\uC608: '\uCC38\uC870 \uD53C\uD0B9')\uC744 \uC2E4\uD589\uD558\uB294\uC9C0 \uC5EC\uBD80"],"vs/editor/browser/editorExtensions":["\uC2E4\uD589 \uCDE8\uC18C(&&U)","\uC2E4\uD589 \uCDE8\uC18C","\uB2E4\uC2DC \uC2E4\uD589(&&R)","\uB2E4\uC2DC \uC2E4\uD589","\uBAA8\uB450 \uC120\uD0DD(&&S)","\uBAA8\uB450 \uC120\uD0DD"],"vs/editor/browser/widget/codeEditorWidget":["\uCEE4\uC11C \uC218\uB294 {0}(\uC73C)\uB85C \uC81C\uD55C\uB418\uC5C8\uC2B5\uB2C8\uB2E4."],"vs/editor/browser/widget/diffEditorWidget":["diff \uD3B8\uC9D1\uAE30\uC758 \uC0BD\uC785\uC5D0 \uB300\uD55C \uC904 \uB370\uCF54\uB808\uC774\uC158\uC785\uB2C8\uB2E4.","diff \uD3B8\uC9D1\uAE30\uC758 \uC81C\uAC70\uC5D0 \uB300\uD55C \uC904 \uB370\uCF54\uB808\uC774\uC158\uC785\uB2C8\uB2E4.","\uD30C\uC77C 1\uAC1C\uAC00 \uB108\uBB34 \uCEE4\uC11C \uD30C\uC77C\uC744 \uBE44\uAD50\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."],"vs/editor/browser/widget/diffReview":["Diff \uAC80\uD1A0\uC5D0\uC11C '\uC0BD\uC785'\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","Diff \uAC80\uD1A0\uC5D0\uC11C '\uC81C\uAC70'\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","Diff \uAC80\uD1A0\uC5D0\uC11C '\uB2EB\uAE30'\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uB2EB\uAE30","\uBCC0\uACBD\uB41C \uC904 \uC5C6\uC74C","\uC120 1\uAC1C \uBCC0\uACBD\uB428","\uC904 {0}\uAC1C \uBCC0\uACBD\uB428","\uCC28\uC774 {0}/{1}: \uC6D0\uB798 \uC904 {2}, {3}, \uC218\uC815\uB41C \uC904 {4}, {5}","\uBE44\uC5B4 \uC788\uC74C","{0} \uBCC0\uACBD\uB418\uC9C0 \uC54A\uC740 \uC904 {1}","{0} \uC6D0\uB798 \uC904 {1} \uC218\uC815\uB41C \uC904 {2}","+ {0} \uC218\uC815\uB41C \uC904 {1}","- {0} \uC6D0\uB798 \uC904 {1}","\uB2E4\uC74C \uB2E4\uB978 \uD56D\uBAA9\uC73C\uB85C \uC774\uB3D9","\uB2E4\uC74C \uB2E4\uB978 \uD56D\uBAA9\uC73C\uB85C \uC774\uB3D9"],"vs/editor/browser/widget/inlineDiffMargin":["\uC0AD\uC81C\uB41C \uC904 \uBCF5\uC0AC","\uC0AD\uC81C\uB41C \uC904 \uBCF5\uC0AC","\uC0AD\uC81C\uB41C \uC904 \uBCF5\uC0AC({0})","\uC774 \uBCC0\uACBD \uB0B4\uC6A9 \uB418\uB3CC\uB9AC\uAE30","\uC0AD\uC81C\uB41C \uC904 \uBCF5\uC0AC({0})"],"vs/editor/common/config/commonEditorConfig":["\uD3B8\uC9D1\uAE30","\uD0ED \uD55C \uAC1C\uC5D0 \uD574\uB2F9\uD558\uB294 \uACF5\uBC31 \uC218\uC785\uB2C8\uB2E4. `#editor.detectIndentation#`\uC774 \uCF1C\uC838 \uC788\uB294 \uACBD\uC6B0 \uC774 \uC124\uC815\uC740 \uD30C\uC77C \uCF58\uD150\uCE20\uC5D0 \uB530\uB77C \uC7AC\uC815\uC758\uB429\uB2C8\uB2E4.","'\uD0ED' \uD0A4\uB97C \uB204\uB97C \uB54C \uACF5\uBC31\uC744 \uC0BD\uC785\uD569\uB2C8\uB2E4. `#editor.detectIndentation#`\uC774 \uCF1C\uC838 \uC788\uB294 \uACBD\uC6B0 \uC774 \uC124\uC815\uC740 \uD30C\uC77C \uCF58\uD150\uCE20\uC5D0 \uB530\uB77C \uC7AC\uC815\uC758\uB429\uB2C8\uB2E4.","\uD30C\uC77C\uC744 \uC5F4 \uB54C \uD30C\uC77C \uCF58\uD150\uCE20\uB97C \uAE30\uBC18\uC73C\uB85C `#editor.tabSize#`\uC640 `#editor.insertSpaces#`\uAC00 \uC790\uB3D9\uC73C\uB85C \uAC80\uC0C9\uB418\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB05D\uC5D0 \uC790\uB3D9 \uC0BD\uC785\uB41C \uACF5\uBC31\uC744 \uC81C\uAC70\uD569\uB2C8\uB2E4.","\uD070 \uD30C\uC77C\uC5D0 \uB300\uD55C \uD2B9\uC218 \uCC98\uB9AC\uB85C, \uBA54\uBAA8\uB9AC\uB97C \uB9CE\uC774 \uC0AC\uC6A9\uD558\uB294 \uD2B9\uC815 \uAE30\uB2A5\uC744 \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB3C4\uB85D \uC124\uC815\uD569\uB2C8\uB2E4.","\uBB38\uC11C \uB0B4 \uB2E8\uC5B4\uB97C \uAE30\uBC18\uC73C\uB85C \uC644\uC131\uC744 \uACC4\uC0B0\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD65C\uC131 \uBB38\uC11C\uC5D0\uC11C\uB9CC \uB2E8\uC5B4\uB97C \uC81C\uC548\uD569\uB2C8\uB2E4.","\uAC19\uC740 \uC5B8\uC5B4\uC758 \uBAA8\uB4E0 \uC5F4\uB9B0 \uBB38\uC11C\uC5D0\uC11C \uB2E8\uC5B4\uB97C \uC81C\uC548\uD569\uB2C8\uB2E4.","\uBAA8\uB4E0 \uC5F4\uB9B0 \uBB38\uC11C\uC5D0\uC11C \uB2E8\uC5B4\uB97C \uC81C\uC548\uD569\uB2C8\uB2E4.","\uB2E8\uC5B4 \uAE30\uBC18 \uC644\uC131\uC774 \uCEF4\uD4E8\uD305\uB418\uB294 \uBB38\uC11C\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBAA8\uB4E0 \uC0C9 \uD14C\uB9C8\uC5D0 \uB300\uD574 \uC758\uBBF8 \uCCB4\uACC4 \uAC15\uC870 \uD45C\uC2DC\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","\uBAA8\uB4E0 \uC0C9 \uD14C\uB9C8\uC5D0 \uB300\uD574 \uC758\uBBF8 \uCCB4\uACC4 \uAC15\uC870 \uD45C\uC2DC\uB97C \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uC758\uBBF8 \uCCB4\uACC4 \uAC15\uC870 \uD45C\uC2DC\uB294 \uD604\uC7AC \uC0C9 \uD14C\uB9C8\uC758 `semanticHighlighting` \uC124\uC815\uC5D0 \uB530\uB77C \uAD6C\uC131\uB429\uB2C8\uB2E4.","semanticHighlighting\uC774 \uC9C0\uC6D0\uD558\uB294 \uC5B8\uC5B4\uC5D0 \uB300\uD574 \uD45C\uC2DC\uB418\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD574\uB2F9 \uCF58\uD150\uCE20\uB97C \uB450 \uBC88 \uD074\uB9AD\uD558\uAC70\uB098 'Esc' \uD0A4\uB97C \uB204\uB974\uB354\uB77C\uB3C4 Peek \uD3B8\uC9D1\uAE30\uB97C \uC5F4\uB9B0 \uC0C1\uD0DC\uB85C \uC720\uC9C0\uD569\uB2C8\uB2E4.","\uC774 \uAE38\uC774\uB97C \uCD08\uACFC\uD558\uB294 \uC904\uC740 \uC131\uB2A5\uC0C1\uC758 \uC774\uC720\uB85C \uD1A0\uD070\uD654\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","diff \uACC4\uC0B0\uC774 \uCDE8\uC18C\uB41C \uD6C4 \uBC00\uB9AC\uCD08 \uB2E8\uC704\uB85C \uC2DC\uAC04\uC744 \uC81C\uD55C\uD569\uB2C8\uB2E4. \uC81C\uD55C \uC2DC\uAC04\uC774 \uC5C6\uB294 \uACBD\uC6B0 0\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","diff \uD3B8\uC9D1\uAE30\uC5D0\uC11C diff\uB97C \uB098\uB780\uD788 \uD45C\uC2DC\uD560\uC9C0 \uC778\uB77C\uC778\uC73C\uB85C \uD45C\uC2DC\uD560\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uD558\uBA74 Diff \uD3B8\uC9D1\uAE30\uAC00 \uC120\uD589 \uB610\uB294 \uD6C4\uD589 \uACF5\uBC31\uC758 \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uBB34\uC2DC\uD569\uB2C8\uB2E4.","diff \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uCD94\uAC00/\uC81C\uAC70\uB41C \uBCC0\uACBD \uB0B4\uC6A9\uC5D0 \uB300\uD574 +/- \uD45C\uC2DC\uAE30\uB97C \uD45C\uC2DC\uD558\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C CodeLens\uB97C \uD45C\uC2DC\uD560 \uAC83\uC778\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC904\uC774 \uBC14\uB00C\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uBDF0\uD3EC\uD2B8 \uB108\uBE44\uC5D0\uC11C \uC904\uC774 \uBC14\uB01D\uB2C8\uB2E4.","`#editor.wordWrap#` \uC124\uC815\uC5D0 \uB530\uB77C \uC904\uC774 \uBC14\uB01D\uB2C8\uB2E4."],"vs/editor/common/config/editorOptions":["\uD3B8\uC9D1\uAE30\uAC00 \uC2A4\uD06C\uB9B0 \uB9AC\uB354\uAC00 \uC5F0\uACB0\uB418\uBA74 \uD50C\uB7AB\uD3FC API\uB97C \uC0AC\uC6A9\uD558\uC5EC \uAC10\uC9C0\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uAC00 \uD654\uBA74 \uC77D\uAE30 \uD504\uB85C\uADF8\uB7A8\uACFC \uD568\uAED8 \uC0AC\uC6A9\uB418\uB3C4\uB85D \uC601\uAD6C\uC801\uC73C\uB85C \uCD5C\uC801\uD654\uB418\uBA70, \uC790\uB3D9 \uC904 \uBC14\uAFC8\uC774 \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB3C4\uB85D \uC124\uC815\uB429\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uAC00 \uC2A4\uD06C\uB9B0 \uB9AC\uB354 \uC0AC\uC6A9\uC744 \uC704\uD574 \uCD5C\uC801\uD654\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uB97C \uD654\uBA74 \uC77D\uAE30 \uD504\uB85C\uADF8\uB7A8\uC5D0 \uCD5C\uC801\uD654\uB41C \uBAA8\uB4DC\uB85C \uC2E4\uD589\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uD558\uBA74 \uC790\uB3D9 \uC904 \uBC14\uAFC8\uC774 \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB3C4\uB85D \uC124\uC815\uB429\uB2C8\uB2E4.","\uC8FC\uC11D\uC744 \uB2EC \uB54C \uACF5\uBC31 \uBB38\uC790\uB97C \uC0BD\uC785\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBE48 \uC904\uC744 \uC904 \uC8FC\uC11D\uC5D0 \uB300\uD55C \uD1A0\uAE00, \uCD94\uAC00 \uB610\uB294 \uC81C\uAC70 \uC791\uC5C5\uC73C\uB85C \uBB34\uC2DC\uD574\uC57C \uD558\uB294\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC120\uD0DD \uC601\uC5ED \uC5C6\uC774 \uD604\uC7AC \uC904 \uBCF5\uC0AC \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC785\uB825\uD558\uB294 \uB3D9\uC548 \uC77C\uCE58 \uD56D\uBAA9\uC744 \uCC3E\uAE30 \uC704\uD55C \uCEE4\uC11C \uC774\uB3D9 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uC120\uD0DD\uC5D0\uC11C Find Widget\uC758 \uAC80\uC0C9 \uBB38\uC790\uC5F4\uC744 \uC2DC\uB529\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC120\uD0DD \uD56D\uBAA9\uC5D0\uC11C \uCC3E\uAE30\uB97C \uC790\uB3D9\uC73C\uB85C \uCF1C\uC9C0 \uC54A\uC74C(\uAE30\uBCF8\uAC12)","\uC120\uD0DD \uD56D\uBAA9\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uD56D\uC0C1 \uCC3E\uAE30 \uCF1C\uAE30","\uC5EC\uB7EC \uC904\uC758 \uCF58\uD150\uCE20\uB97C \uC120\uD0DD\uD558\uBA74 \uC120\uD0DD \uD56D\uBAA9\uC5D0\uC11C \uCC3E\uAE30\uAC00 \uC790\uB3D9\uC73C\uB85C \uCF1C\uC9D1\uB2C8\uB2E4.","\uC120\uD0DD \uC601\uC5ED\uC5D0\uC11C \uCC3E\uAE30\uB97C \uC790\uB3D9\uC73C\uB85C \uC124\uC815\uD558\uB294 \uC870\uAC74\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","macOS\uC5D0\uC11C Find Widget\uC774 \uACF5\uC720 \uD074\uB9BD\uBCF4\uB4DC \uCC3E\uAE30\uB97C \uC77D\uC744\uC9C0 \uC218\uC815\uD560\uC9C0 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC704\uC82F \uCC3E\uAE30\uC5D0\uC11C \uD3B8\uC9D1\uAE30 \uB9E8 \uC704\uC5D0 \uC904\uC744 \uCD94\uAC00\uD574\uC57C \uD558\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. true\uC778 \uACBD\uC6B0 \uC704\uC82F \uCC3E\uAE30\uAC00 \uD45C\uC2DC\uB418\uBA74 \uCCAB \uBC88\uC9F8 \uC904 \uC704\uB85C \uC2A4\uD06C\uB864\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uB354 \uC774\uC0C1 \uC77C\uCE58\uD558\uB294 \uD56D\uBAA9\uC774 \uC5C6\uC744 \uB54C \uAC80\uC0C9\uC744 \uCC98\uC74C\uC774\uB098 \uB05D\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uB2E4\uC2DC \uC2DC\uC791\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uAE00\uAF34 \uD569\uC790('calt' \uBC0F 'liga' \uAE00\uAF34 \uAE30\uB2A5)\uB97C \uC0AC\uC6A9\uD558\uAC70\uB098 \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB3C4\uB85D \uC124\uC815\uD569\uB2C8\uB2E4. 'font-feature-settings' CSS \uC18D\uC131\uC758 \uC138\uBD84\uD654\uB41C \uC81C\uC5B4\uB97C \uC704\uD574 \uBB38\uC790\uC5F4\uB85C \uBCC0\uACBD\uD569\uB2C8\uB2E4.","\uBA85\uC2DC\uC801 'font-feature-settings' CSS \uC18D\uC131\uC785\uB2C8\uB2E4. \uD569\uC790\uB97C \uCF1C\uAC70\uB098 \uAEBC\uC57C \uD558\uB294 \uACBD\uC6B0\uC5D0\uB9CC \uBD80\uC6B8\uC744 \uB300\uC2E0 \uC804\uB2EC\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uAE00\uAF34 \uD569\uC790 \uB610\uB294 \uAE00\uAF34 \uAE30\uB2A5\uC744 \uAD6C\uC131\uD569\uB2C8\uB2E4. CSS 'font-feature-settings' \uC18D\uC131\uC758 \uAC12\uC5D0 \uB300\uD574 \uD569\uC790 \uB610\uB294 \uBB38\uC790\uC5F4\uC744 \uC0AC\uC6A9\uD558\uAC70\uB098 \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB3C4\uB85D \uC124\uC815\uD558\uAE30 \uC704\uD55C \uBD80\uC6B8\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uAE00\uAF34 \uD06C\uAE30(\uD53D\uC140)\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.",'"\uD45C\uC900" \uBC0F "\uAD75\uAC8C" \uD0A4\uC6CC\uB4DC \uB610\uB294 1~1000 \uC0AC\uC774\uC758 \uC22B\uC790\uB9CC \uD5C8\uC6A9\uB429\uB2C8\uB2E4.','\uAE00\uAF34 \uB450\uAED8\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. "\uD45C\uC900" \uBC0F "\uAD75\uAC8C" \uD0A4\uC6CC\uB4DC \uB610\uB294 1~1000 \uC0AC\uC774\uC758 \uC22B\uC790\uB97C \uD5C8\uC6A9\uD569\uB2C8\uB2E4.',"\uACB0\uACFC Peek \uBDF0 \uD45C\uC2DC(\uAE30\uBCF8)","\uAE30\uBCF8 \uACB0\uACFC\uB85C \uC774\uB3D9\uD558\uC5EC Peek \uBCF4\uAE30\uB97C \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uAE30\uBCF8 \uACB0\uACFC\uB85C \uC774\uB3D9\uD558\uACE0 \uB2E4\uB978 \uD56D\uBAA9\uC5D0 \uB300\uD574 peek \uC5C6\uB294 \uD0D0\uC0C9\uC744 \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815","\uC774 \uC124\uC815\uC740 \uB354 \uC774\uC0C1 \uC0AC\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB300\uC2E0 'editor.editor.gotoLocation.multipleDefinitions' \uB610\uB294 'editor.editor.gotoLocation.multipleImplementations'\uC640 \uAC19\uC740 \uBCC4\uB3C4\uC758 \uC124\uC815\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.","\uC5EC\uB7EC \uB300\uC0C1 \uC704\uCE58\uAC00 \uC788\uB294 \uACBD\uC6B0 '\uC815\uC758\uB85C \uC774\uB3D9' \uBA85\uB839 \uB3D9\uC791\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC5EC\uB7EC \uB300\uC0C1 \uC704\uCE58\uAC00 \uC788\uB294 \uACBD\uC6B0 '\uC720\uD615 \uC815\uC758\uB85C \uC774\uB3D9' \uBA85\uB839 \uB3D9\uC791\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC5EC\uB7EC \uB300\uC0C1 \uC704\uCE58\uAC00 \uC788\uB294 \uACBD\uC6B0 'Go to Declaration' \uBA85\uB839 \uB3D9\uC791\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC5EC\uB7EC \uB300\uC0C1 \uC704\uCE58\uAC00 \uC788\uB294 \uACBD\uC6B0 '\uAD6C\uD604\uC73C\uB85C \uC774\uB3D9' \uBA85\uB839 \uB3D9\uC791\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC5EC\uB7EC \uB300\uC0C1 \uC704\uCE58\uAC00 \uC788\uB294 \uACBD\uC6B0 '\uCC38\uC870\uB85C \uC774\uB3D9' \uBA85\uB839 \uB3D9\uC791\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","'\uC815\uC758\uB85C \uC774\uB3D9'\uC758 \uACB0\uACFC\uAC00 \uD604\uC7AC \uC704\uCE58\uC77C \uB54C \uC2E4\uD589\uB418\uB294 \uB300\uCCB4 \uBA85\uB839 ID\uC785\uB2C8\uB2E4.","'\uD615\uC2DD \uC815\uC758\uB85C \uC774\uB3D9'\uC758 \uACB0\uACFC\uAC00 \uD604\uC7AC \uC704\uCE58\uC77C \uB54C \uC2E4\uD589\uB418\uB294 \uB300\uCCB4 \uBA85\uB839 ID\uC785\uB2C8\uB2E4.","'\uC120\uC5B8\uC73C\uB85C \uC774\uB3D9'\uC758 \uACB0\uACFC\uAC00 \uD604\uC7AC \uC704\uCE58\uC77C \uB54C \uC2E4\uD589\uB418\uB294 \uB300\uCCB4 \uBA85\uB839 ID\uC785\uB2C8\uB2E4.","'\uAD6C\uD604\uC73C\uB85C \uC774\uB3D9'\uC758 \uACB0\uACFC\uAC00 \uD604\uC7AC \uC704\uCE58\uC77C \uB54C \uC2E4\uD589\uB418\uB294 \uB300\uCCB4 \uBA85\uB839 ID\uC785\uB2C8\uB2E4.","'\uCC38\uC870\uB85C \uC774\uB3D9'\uC758 \uACB0\uACFC\uAC00 \uD604\uC7AC \uC704\uCE58\uC77C \uB54C \uC2E4\uD589\uB418\uB294 \uB300\uCCB4 \uBA85\uB839 ID\uC785\uB2C8\uB2E4.","\uD638\uBC84 \uD45C\uC2DC \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD638\uBC84\uAC00 \uD45C\uC2DC\uB418\uAE30 \uC804\uAE4C\uC9C0\uC758 \uC9C0\uC5F0 \uC2DC\uAC04(\uBC00\uB9AC\uCD08)\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB97C \uD574\uB2F9 \uD56D\uBAA9 \uC704\uB85C \uC774\uB3D9\uD560 \uB54C \uD638\uBC84\uB97C \uACC4\uC18D \uD45C\uC2DC\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uCF54\uB4DC \uB3D9\uC791 \uC804\uAD6C\uB97C \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC778\uB77C\uC778 \uD78C\uD2B8\uB97C \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC778\uB77C\uC778 \uD78C\uD2B8\uC758 \uAE00\uAF34 \uD06C\uAE30\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. `0`\uC73C\uB85C \uC124\uC815\uD558\uBA74 `#editor.fontSize#`\uC758 90%\uAC00 \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC778\uB77C\uC778 \uD78C\uD2B8\uC758 \uAE00\uAF34 \uD328\uBC00\uB9AC\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC904 \uB192\uC774\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uAE00\uAF34 \uD06C\uAE30\uC5D0\uC11C \uC904 \uB192\uC774\uB97C \uACC4\uC0B0\uD558\uB824\uBA74 0\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","\uBBF8\uB2C8\uB9F5 \uD45C\uC2DC \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBBF8\uB2C8\uB9F5\uC758 \uD06C\uAE30\uB294 \uD3B8\uC9D1\uAE30 \uB0B4\uC6A9\uACFC \uB3D9\uC77C\uD558\uBA70 \uC2A4\uD06C\uB864\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC758 \uB192\uC774\uB97C \uB9DE\uCD94\uAE30 \uC704\uD574 \uD544\uC694\uC5D0 \uB530\uB77C \uBBF8\uB2C8\uB9F5\uC774 \uD655\uC7A5\uB418\uAC70\uB098 \uCD95\uC18C\uB429\uB2C8\uB2E4(\uC2A4\uD06C\uB864 \uC5C6\uC74C).","\uBBF8\uB2C8\uB9F5\uC744 \uD3B8\uC9D1\uAE30\uBCF4\uB2E4 \uC791\uAC8C \uC720\uC9C0\uD560 \uC218 \uC788\uB3C4\uB85D \uD544\uC694\uC5D0 \uB530\uB77C \uBBF8\uB2C8\uB9F5\uC774 \uCD95\uC18C\uB429\uB2C8\uB2E4(\uC2A4\uD06C\uB864 \uC5C6\uC74C).","\uBBF8\uB2C8\uB9F5\uC758 \uD06C\uAE30\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBBF8\uB2C8\uB9F5\uC744 \uB80C\uB354\uB9C1\uD560 \uCE21\uBA74\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBBF8\uB2C8\uB9F5 \uC2AC\uB77C\uC774\uB354\uAC00 \uD45C\uC2DC\uB418\uB294 \uC2DC\uAE30\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBBF8\uB2C8\uB9F5\uC5D0 \uADF8\uB824\uC9C4 \uCF58\uD150\uCE20\uC758 \uBC30\uC728: 1, 2 \uB610\uB294 3.","\uC904\uC758 \uC2E4\uC81C \uBB38\uC790(\uC0C9 \uBE14\uB85D \uC544\uB2D8)\uB97C \uB80C\uB354\uB9C1\uD569\uB2C8\uB2E4.","\uCD5C\uB300 \uD2B9\uC815 \uC218\uC758 \uC5F4\uC744 \uB80C\uB354\uB9C1\uD558\uB3C4\uB85D \uBBF8\uB2C8\uB9F5\uC758 \uB108\uBE44\uB97C \uC81C\uD55C\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC758 \uC704\uCABD \uAC00\uC7A5\uC790\uB9AC\uC640 \uCCAB \uBC88\uC9F8 \uC904 \uC0AC\uC774\uC758 \uACF5\uBC31\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC758 \uC544\uB798\uCABD \uAC00\uC7A5\uC790\uB9AC\uC640 \uB9C8\uC9C0\uB9C9 \uC904 \uC0AC\uC774\uC758 \uACF5\uBC31\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC785\uB825\uACFC \uB3D9\uC2DC\uC5D0 \uB9E4\uAC1C\uBCC0\uC218 \uBB38\uC11C\uC640 \uC720\uD615 \uC815\uBCF4\uB97C \uD45C\uC2DC\uD558\uB294 \uD31D\uC5C5\uC744 \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uD569\uB2C8\uB2E4.","\uB9E4\uAC1C\uBCC0\uC218 \uD78C\uD2B8 \uBA54\uB274\uC758 \uC8FC\uAE30 \uD639\uC740 \uBAA9\uB85D\uC758 \uB05D\uC5D0 \uB3C4\uB2EC\uD558\uC600\uC744\uB54C \uC885\uB8CC\uD560 \uAC83\uC778\uC9C0 \uC5EC\uBD80\uB97C \uACB0\uC815\uD569\uB2C8\uB2E4.","\uBB38\uC790\uC5F4 \uB0B4\uC5D0\uC11C \uBE60\uB978 \uC81C\uC548\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","\uC8FC\uC11D \uB0B4\uC5D0\uC11C \uBE60\uB978 \uC81C\uC548\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","\uBB38\uC790\uC5F4 \uBC0F \uC8FC\uC11D \uC678\uBD80\uC5D0\uC11C \uBE60\uB978 \uC81C\uC548\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","\uC785\uB825\uD558\uB294 \uB3D9\uC548 \uC81C\uC548\uC744 \uC790\uB3D9\uC73C\uB85C \uD45C\uC2DC\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC904 \uBC88\uD638\uB294 \uB80C\uB354\uB9C1\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uC904 \uBC88\uD638\uB294 \uC808\uB300\uAC12\uC73C\uB85C \uB80C\uB354\uB9C1 \uB429\uB2C8\uB2E4.","\uC904 \uBC88\uD638\uB294 \uCEE4\uC11C \uC704\uCE58\uC5D0\uC11C \uC904 \uAC04\uACA9 \uAC70\uB9AC\uB85C \uB80C\uB354\uB9C1 \uB429\uB2C8\uB2E4.","\uC904 \uBC88\uD638\uB294 \uB9E4 10 \uC904\uB9C8\uB2E4 \uB80C\uB354\uB9C1\uC774 \uC774\uB8E8\uC5B4\uC9D1\uB2C8\uB2E4.","\uC904 \uBC88\uD638\uC758 \uD45C\uC2DC \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC774 \uD3B8\uC9D1\uAE30 \uB208\uAE08\uC790\uC5D0\uC11C \uB80C\uB354\uB9C1\uD560 \uACE0\uC815 \uD3ED \uBB38\uC790 \uC218\uC785\uB2C8\uB2E4.","\uC774 \uD3B8\uC9D1\uAE30 \uB208\uAE08\uC790\uC758 \uC0C9\uC785\uB2C8\uB2E4.","\uD2B9\uC815 \uC218\uC758 \uACE0\uC815 \uD3ED \uBB38\uC790 \uB4A4\uC5D0 \uC138\uB85C \uB208\uAE08\uC790\uB97C \uB80C\uB354\uB9C1\uD569\uB2C8\uB2E4. \uC5EC\uB7EC \uB208\uAE08\uC790\uC758 \uACBD\uC6B0 \uC5EC\uB7EC \uAC12\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4. \uBC30\uC5F4\uC774 \uBE44\uC5B4 \uC788\uB294 \uACBD\uC6B0 \uB208\uAE08\uC790\uAC00 \uADF8\uB824\uC9C0\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uCEE4\uC11C\uC758 \uD14D\uC2A4\uD2B8 \uC624\uB978\uCABD\uC744 \uB36E\uC5B4 \uC4F0\uC9C0\uC54A\uACE0 \uC81C\uC548\uC744 \uC0BD\uC785\uD569\uB2C8\uB2E4.","\uC81C\uC548\uC744 \uC0BD\uC785\uD558\uACE0 \uCEE4\uC11C\uC758 \uC624\uB978\uCABD \uD14D\uC2A4\uD2B8\uB97C \uB36E\uC5B4\uC501\uB2C8\uB2E4.","\uC644\uB8CC\uB97C \uC218\uB77D\uD560 \uB54C \uB2E8\uC5B4\uB97C \uB36E\uC5B4\uC4F8\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uC774\uAC83\uC740 \uC774 \uAE30\uB2A5\uC744 \uC120\uD0DD\uD558\uB294 \uD655\uC7A5\uC5D0 \uB530\uB77C \uB2E4\uB985\uB2C8\uB2E4.","\uC81C\uC548 \uD544\uD130\uB9C1 \uBC0F \uC815\uB82C\uC5D0\uC11C \uC791\uC740 \uC624\uD0C0\uB97C \uC124\uBA85\uD558\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC815\uB82C\uD560 \uB54C \uCEE4\uC11C \uADFC\uCC98\uC5D0 \uD45C\uC2DC\uB418\uB294 \uB2E8\uC5B4\uB97C \uC6B0\uC120\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC800\uC7A5\uB41C \uC81C\uC548 \uC0AC\uD56D \uC120\uD0DD \uD56D\uBAA9\uC744 \uC5EC\uB7EC \uC791\uC5C5 \uC601\uC5ED \uBC0F \uCC3D\uC5D0\uC11C \uACF5\uC720\uD560 \uAC83\uC778\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4(`#editor.suggestSelection#` \uD544\uC694).","\uD65C\uC131 \uCF54\uB4DC \uC870\uAC01\uC774 \uBE60\uB978 \uC81C\uC548\uC744 \uBC29\uC9C0\uD558\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC81C\uC548\uC758 \uC544\uC774\uCF58\uC744 \uD45C\uC2DC\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC81C\uC548 \uC704\uC82F \uD558\uB2E8\uC758 \uC0C1\uD0DC \uD45C\uC2DC\uC904 \uAC00\uC2DC\uC131\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC81C\uC548 \uC138\uBD80 \uC815\uBCF4\uAC00 \uB808\uC774\uBE14\uACFC \uD568\uAED8 \uC778\uB77C\uC778\uC5D0 \uD45C\uC2DC\uB418\uB294\uC9C0 \uC544\uB2C8\uBA74 \uC138\uBD80 \uC815\uBCF4 \uC704\uC82F\uC5D0\uB9CC \uD45C\uC2DC\uB418\uB294\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC774 \uC124\uC815\uC740 \uB354 \uC774\uC0C1 \uC0AC\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uC774\uC81C \uC81C\uC548 \uC704\uC82F\uC758 \uD06C\uAE30\uB97C \uC870\uC815\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uC774 \uC124\uC815\uC740 \uB354 \uC774\uC0C1 \uC0AC\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB300\uC2E0 'editor.suggest.showKeywords'\uB610\uB294 'editor.suggest.showSnippets'\uC640 \uAC19\uC740 \uBCC4\uB3C4\uC758 \uC124\uC815\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 `\uBA54\uC11C\uB4DC` \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uD568\uC218' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uC0DD\uC131\uC790' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uD544\uB4DC' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uBCC0\uC218' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uD074\uB798\uC2A4' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uAD6C\uC870' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uC778\uD130\uD398\uC774\uC2A4' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uBAA8\uB4C8' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uC18D\uC131' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uC774\uBCA4\uD2B8' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 `\uC5F0\uC0B0\uC790` \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uB2E8\uC704' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uAC12' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uC0C1\uC218' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uC5F4\uAC70\uD615' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 `enumMember` \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uD0A4\uC6CC\uB4DC' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uD14D\uC2A4\uD2B8' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uC0C9' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 `\uD30C\uC77C` \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uCC38\uC870' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uC0AC\uC6A9\uC790 \uC9C0\uC815 \uC0C9' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uD3F4\uB354' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB41C \uACBD\uC6B0 IntelliSense\uC5D0 'typeParameter' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uBA74 IntelliSense\uC5D0 '\uCF54\uB4DC \uC870\uAC01' \uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","IntelliSense\uB97C \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uD558\uBA74 `user`-\uC81C\uC548\uC774 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","IntelliSense\uB97C \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uD55C \uACBD\uC6B0 `issues`-\uC81C\uC548\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uC120\uD589 \uBC0F \uD6C4\uD589 \uACF5\uBC31\uC744 \uD56D\uC0C1 \uC120\uD0DD\uD574\uC57C \uD558\uB294\uC9C0 \uC5EC\uBD80\uC785\uB2C8\uB2E4.","\uCEE4\uBC0B \uBB38\uC790\uC5D0 \uB300\uD55C \uC81C\uC548\uC744 \uD5C8\uC6A9\uD560\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uC608\uB97C \uB4E4\uC5B4 JavaScript\uC5D0\uC11C\uB294 \uC138\uBBF8\uCF5C\uB860(';')\uC774 \uC81C\uC548\uC744 \uD5C8\uC6A9\uD558\uACE0 \uD574\uB2F9 \uBB38\uC790\uB97C \uC785\uB825\uD558\uB294 \uCEE4\uBC0B \uBB38\uC790\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uD14D\uC2A4\uD2B8\uB97C \uBCC0\uACBD\uD560 \uB54C `Enter` \uD0A4\uB97C \uC0AC\uC6A9\uD55C \uC81C\uC548\uB9CC \uD5C8\uC6A9\uD569\uB2C8\uB2E4.","'Tab' \uD0A4 \uC678\uC5D0 'Enter' \uD0A4\uC5D0 \uB300\uD55C \uC81C\uC548\uB3C4 \uD5C8\uC6A9\uD560\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uC0C8 \uC904\uC744 \uC0BD\uC785\uD558\uB294 \uB3D9\uC791\uACFC \uC81C\uC548\uC744 \uD5C8\uC6A9\uD558\uB294 \uB3D9\uC791 \uAC04\uC758 \uBAA8\uD638\uD568\uC744 \uC5C6\uC568 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uD654\uBA74 \uD310\uB3C5\uAE30\uAC00 \uC77D\uC744 \uC218 \uC788\uB294 \uD3B8\uC9D1\uAE30\uC758 \uC904 \uC218\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uACBD\uACE0: \uAE30\uBCF8\uAC12\uBCF4\uB2E4 \uD070 \uC22B\uC790\uC778 \uACBD\uC6B0 \uC131\uB2A5\uC5D0 \uC601\uD5A5\uC744 \uBBF8\uCE69\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCF58\uD150\uCE20","\uC5B8\uC5B4 \uAD6C\uC131\uC744 \uC0AC\uC6A9\uD558\uC5EC \uB300\uAD04\uD638\uB97C \uC790\uB3D9\uC73C\uB85C \uB2EB\uC744 \uACBD\uC6B0\uB97C \uACB0\uC815\uD569\uB2C8\uB2E4.","\uCEE4\uC11C\uAC00 \uACF5\uBC31\uC758 \uC67C\uCABD\uC5D0 \uC788\uB294 \uACBD\uC6B0\uC5D0\uB9CC \uB300\uAD04\uD638\uB97C \uC790\uB3D9\uC73C\uB85C \uB2EB\uC2B5\uB2C8\uB2E4.","\uC0AC\uC6A9\uC790\uAC00 \uC5EC\uB294 \uAD04\uD638\uB97C \uCD94\uAC00\uD55C \uD6C4 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uAD04\uD638\uB97C \uC790\uB3D9\uC73C\uB85C \uB2EB\uC744\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB2EB\uAE30 \uB530\uC634\uD45C \uB610\uB294 \uB300\uAD04\uD638\uAC00 \uC790\uB3D9\uC73C\uB85C \uC0BD\uC785\uB41C \uACBD\uC6B0\uC5D0\uB9CC \uD574\uB2F9 \uD56D\uBAA9 \uC704\uC5D0 \uC785\uB825\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uC790\uAC00 \uB2EB\uB294 \uB530\uC634\uD45C \uB610\uB294 \uB300\uAD04\uD638 \uC704\uC5D0 \uC785\uB825\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC5B8\uC5B4 \uAD6C\uC131\uC744 \uC0AC\uC6A9\uD558\uC5EC \uB530\uC634\uD45C\uB97C \uC790\uB3D9\uC73C\uB85C \uB2EB\uC744 \uACBD\uC6B0\uB97C \uACB0\uC815\uD569\uB2C8\uB2E4.","\uCEE4\uC11C\uAC00 \uACF5\uBC31\uC758 \uC67C\uCABD\uC5D0 \uC788\uB294 \uACBD\uC6B0\uC5D0\uB9CC \uB530\uC634\uD45C\uB97C \uC790\uB3D9\uC73C\uB85C \uB2EB\uC2B5\uB2C8\uB2E4.","\uC0AC\uC6A9\uC790\uAC00 \uC5EC\uB294 \uB530\uC634\uD45C\uB97C \uCD94\uAC00\uD55C \uD6C4 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uB530\uC634\uD45C\uB97C \uC790\uB3D9\uC73C\uB85C \uB2EB\uC744\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uB294 \uB4E4\uC5EC\uC4F0\uAE30\uB97C \uC790\uB3D9\uC73C\uB85C \uC0BD\uC785\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uB294 \uD604\uC7AC \uC904\uC758 \uB4E4\uC5EC\uC4F0\uAE30\uB97C \uC720\uC9C0\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uB294 \uD604\uC7AC \uC904\uC758 \uB4E4\uC5EC\uC4F0\uAE30\uB97C \uC720\uC9C0\uD558\uACE0 \uC5B8\uC5B4 \uC815\uC758 \uB300\uAD04\uD638\uB97C \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uB294 \uD604\uC7AC \uC904\uC758 \uB4E4\uC5EC\uC4F0\uAE30\uB97C \uC720\uC9C0\uD558\uACE0 \uC5B8\uC5B4 \uC815\uC758 \uB300\uAD04\uD638\uB97C \uC874\uC911\uD558\uBA70 \uC5B8\uC5B4\uBCC4\uB85C \uC815\uC758\uB41C \uD2B9\uBCC4 EnterRules\uB97C \uD638\uCD9C\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uB294 \uD604\uC7AC \uC904\uC758 \uB4E4\uC5EC\uC4F0\uAE30\uB97C \uC720\uC9C0\uD558\uACE0, \uC5B8\uC5B4 \uC815\uC758 \uB300\uAD04\uD638\uB97C \uC874\uC911\uD558\uACE0, \uC5B8\uC5B4\uC5D0 \uC758\uD574 \uC815\uC758\uB41C \uD2B9\uBCC4 EnterRules\uB97C \uD638\uCD9C\uD558\uACE0, \uC5B8\uC5B4\uC5D0 \uC758\uD574 \uC815\uC758\uB41C \uB4E4\uC5EC\uC4F0\uAE30 \uADDC\uCE59\uC744 \uC874\uC911\uD569\uB2C8\uB2E4.","\uC0AC\uC6A9\uC790\uAC00 \uC904\uC744 \uC785\uB825, \uBD99\uC5EC\uB123\uAE30, \uC774\uB3D9 \uB610\uB294 \uB4E4\uC5EC\uC4F0\uAE30 \uD560 \uB54C \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uB4E4\uC5EC\uC4F0\uAE30\uB97C \uC790\uB3D9\uC73C\uB85C \uC870\uC815\uD558\uB3C4\uB85D \uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC5B8\uC5B4 \uAD6C\uC131\uC744 \uC0AC\uC6A9\uD558\uC5EC \uC120\uD0DD \uD56D\uBAA9\uC744 \uC790\uB3D9\uC73C\uB85C \uB458\uB7EC\uC300 \uACBD\uC6B0\uB97C \uACB0\uC815\uD569\uB2C8\uB2E4.","\uB300\uAD04\uD638\uAC00 \uC544\uB2CC \uB530\uC634\uD45C\uB85C \uB458\uB7EC\uC309\uB2C8\uB2E4.","\uB530\uC634\uD45C\uAC00 \uC544\uB2CC \uB300\uAD04\uD638\uB85C \uB458\uB7EC\uC309\uB2C8\uB2E4.","\uB530\uC634\uD45C \uB610\uB294 \uB300\uAD04\uD638 \uC785\uB825 \uC2DC \uD3B8\uC9D1\uAE30\uAC00 \uC790\uB3D9\uC73C\uB85C \uC120\uD0DD \uC601\uC5ED\uC744 \uB458\uB7EC\uC300\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB4E4\uC5EC\uC4F0\uAE30\uC5D0 \uACF5\uBC31\uC744 \uC0AC\uC6A9\uD560 \uB54C \uD0ED \uBB38\uC790\uC758 \uC120\uD0DD \uB3D9\uC791\uC744 \uC5D0\uBBAC\uB808\uC774\uD2B8\uD569\uB2C8\uB2E4. \uC120\uD0DD \uC601\uC5ED\uC774 \uD0ED \uC815\uC9C0\uC5D0 \uACE0\uC815\uB429\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C CodeLens\uB97C \uD45C\uC2DC\uD560 \uAC83\uC778\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","CodeLens\uC758 \uAE00\uAF34 \uD328\uBC00\uB9AC\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","CodeLens\uC758 \uAE00\uAF34 \uD06C\uAE30(\uD53D\uC140)\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. '0'\uC73C\uB85C \uC124\uC815\uD558\uBA74 `#editor.fontSize#`\uC758 90%\uAC00 \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC778\uB77C\uC778 \uC0C9 \uB370\uCF54\uB808\uC774\uD130 \uBC0F \uC0C9 \uC120\uD0DD\uC744 \uB80C\uB354\uB9C1\uD560\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uC640 \uD0A4\uB85C \uC120\uD0DD\uD55C \uC601\uC5ED\uC5D0\uC11C \uC5F4\uC744 \uC120\uD0DD\uD558\uB3C4\uB85D \uC124\uC815\uD569\uB2C8\uB2E4.","\uAD6C\uBB38 \uAC15\uC870 \uD45C\uC2DC\uB97C \uD074\uB9BD\uBCF4\uB4DC\uB85C \uBCF5\uC0AC\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uCEE4\uC11C \uC560\uB2C8\uBA54\uC774\uC158 \uC2A4\uD0C0\uC77C\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB9E4\uB044\uB7EC\uC6B4 \uCE90\uB7FF \uC560\uB2C8\uBA54\uC774\uC158\uC758 \uC0AC\uC6A9 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uCEE4\uC11C \uC2A4\uD0C0\uC77C\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uCEE4\uC11C \uC8FC\uC704\uC5D0 \uD45C\uC2DC\uB418\uB294 \uC120\uD589 \uBC0F \uD6C4\uD589 \uC904\uC758 \uCD5C\uC18C \uC218\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uC77C\uBD80 \uB2E4\uB978 \uD3B8\uC9D1\uAE30\uC5D0\uC11C\uB294 'scrollOff' \uB610\uB294 'scrollOffset'\uC774\uB77C\uACE0 \uD569\uB2C8\uB2E4.","'cursorSurroundingLines'\uB294 \uD0A4\uBCF4\uB4DC \uB098 API\uB97C \uD1B5\uD574 \uD2B8\uB9AC\uAC70\uB420 \uB54C\uB9CC \uC801\uC6A9\uB429\uB2C8\uB2E4.","`cursorSurroundingLines`\uB294 \uD56D\uC0C1 \uC801\uC6A9\uB429\uB2C8\uB2E4.","'cursorSurroundingLines'\uB97C \uC801\uC6A9\uD574\uC57C \uD558\uB294 \uACBD\uC6B0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","`#editor.cursorStyle#` \uC124\uC815\uC774 'line'\uC73C\uB85C \uC124\uC815\uB418\uC5B4 \uC788\uC744 \uB54C \uCEE4\uC11C\uC758 \uB113\uC774\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uB04C\uC5B4\uC11C \uB193\uAE30\uB85C \uC120\uD0DD \uC601\uC5ED\uC744 \uC774\uB3D9\uD560 \uC218 \uC788\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","'Alt' \uD0A4\uB97C \uB204\uB97C \uB54C \uC2A4\uD06C\uB864 \uC18D\uB3C4 \uC2B9\uC218\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0 \uCF54\uB4DC \uC811\uAE30\uAC00 \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uACBD\uC6B0 \uC5B8\uC5B4\uBCC4 \uC811\uAE30 \uC804\uB7B5\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4. \uADF8\uB807\uC9C0 \uC54A\uC740 \uACBD\uC6B0 \uB4E4\uC5EC\uC4F0\uAE30 \uAE30\uBC18 \uC804\uB7B5\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","\uB4E4\uC5EC\uC4F0\uAE30 \uAE30\uBC18 \uC811\uAE30 \uC804\uB7B5\uC744 \uC0AC\uC6A9\uD569\uB2C8\uB2E4.","\uC811\uAE30 \uBC94\uC704\uB97C \uACC4\uC0B0\uD558\uAE30 \uC704\uD55C \uC804\uB7B5\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC811\uD78C \uBC94\uC704\uB97C \uAC15\uC870 \uD45C\uC2DC\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC811\uD78C \uC904\uC774 \uC904\uC744 \uD3BC\uCE5C \uD6C4 \uBE48 \uCF58\uD150\uCE20\uB97C \uD074\uB9AD\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uAE00\uAF34 \uD328\uBC00\uB9AC\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBD99\uC5EC\uB123\uC740 \uCF58\uD150\uCE20\uC758 \uC11C\uC2DD\uC744 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uC9C0\uC815\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uD3EC\uB9F7\uD130\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC788\uC5B4\uC57C \uD558\uBA70 \uD3EC\uB9F7\uD130\uAC00 \uBB38\uC11C\uC5D0\uC11C \uBC94\uC704\uC758 \uC11C\uC2DD\uC744 \uC9C0\uC815\uD560 \uC218 \uC788\uC5B4\uC57C \uD569\uB2C8\uB2E4.","\uC785\uB825 \uD6C4 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uC904\uC758 \uC11C\uC2DD\uC744 \uC9C0\uC815\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC138\uB85C \uBB38\uC790 \uBAA8\uC591 \uC5EC\uBC31\uC744 \uB80C\uB354\uB9C1\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uBB38\uC790 \uBAA8\uC591 \uC5EC\uBC31\uC740 \uC8FC\uB85C \uB514\uBC84\uAE45\uC5D0 \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uCEE4\uC11C\uAC00 \uAC1C\uC694 \uB208\uAE08\uC790\uC5D0\uC11C \uAC00\uB824\uC838\uC57C \uD558\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD65C\uC131 \uB4E4\uC5EC\uC4F0\uAE30 \uAC00\uC774\uB4DC\uB97C \uAC15\uC870 \uD45C\uC2DC\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBB38\uC790 \uAC04\uACA9(\uD53D\uC140)\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC5F0\uACB0\uB41C \uD3B8\uC9D1\uC774 \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uC5C8\uB294\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uC5B8\uC5B4\uC5D0 \uB530\uB77C \uAD00\uB828 \uAE30\uD638(\uC608: HTML \uD0DC\uADF8)\uAC00 \uD3B8\uC9D1 \uC911\uC5D0 \uC5C5\uB370\uC774\uD2B8\uB429\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uB9C1\uD06C\uB97C \uAC10\uC9C0\uD558\uACE0 \uD074\uB9AD\uD560 \uC218 \uC788\uAC8C \uB9CC\uB4E4\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC77C\uCE58\uD558\uB294 \uB300\uAD04\uD638\uB97C \uAC15\uC870 \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4 \uD720 \uC2A4\uD06C\uB864 \uC774\uBCA4\uD2B8\uC758 `deltaX` \uBC0F `deltaY`\uC5D0\uC11C \uC0AC\uC6A9\uD560 \uC2B9\uC218\uC785\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4 \uD720\uC744 \uC0AC\uC6A9\uD560 \uB54C 'Ctrl' \uD0A4\uB97C \uB204\uB974\uACE0 \uC788\uC73C\uBA74 \uD3B8\uC9D1\uAE30\uC758 \uAE00\uAF34\uC744 \uD655\uB300/\uCD95\uC18C\uD569\uB2C8\uB2E4.","\uC5EC\uB7EC \uCEE4\uC11C\uAC00 \uACB9\uCE58\uB294 \uACBD\uC6B0 \uCEE4\uC11C\uB97C \uBCD1\uD569\uD569\uB2C8\uB2E4.","Windows\uC640 Linux\uC758 'Control'\uC744 macOS\uC758 'Command'\uB85C \uB9E4\uD551\uD569\uB2C8\uB2E4.","Windows\uC640 Linux\uC758 'Alt'\uB97C macOS\uC758 'Option'\uC73C\uB85C \uB9E4\uD551\uD569\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB85C \uC5EC\uB7EC \uCEE4\uC11C\uB97C \uCD94\uAC00\uD560 \uB54C \uC0AC\uC6A9\uD560 \uC218\uC815\uC790\uC785\uB2C8\uB2E4. [\uC815\uC758\uB85C \uC774\uB3D9] \uBC0F [\uB9C1\uD06C \uC5F4\uAE30] \uB9C8\uC6B0\uC2A4 \uC81C\uC2A4\uCC98\uAC00 \uBA40\uD2F0\uCEE4\uC11C \uC218\uC815\uC790\uC640 \uCDA9\uB3CC\uD558\uC9C0 \uC54A\uB3C4\uB85D \uC870\uC815\uB429\uB2C8\uB2E4. [\uC790\uC138\uD55C \uC815\uBCF4](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier).","\uAC01 \uCEE4\uC11C\uB294 \uD14D\uC2A4\uD2B8 \uD55C \uC904\uC744 \uBD99\uC5EC\uB123\uC2B5\uB2C8\uB2E4.","\uAC01 \uCEE4\uC11C\uB294 \uC804\uCCB4 \uD14D\uC2A4\uD2B8\uB97C \uBD99\uC5EC\uB123\uC2B5\uB2C8\uB2E4.","\uBD99\uC5EC\uB123\uC740 \uD14D\uC2A4\uD2B8\uC758 \uC904 \uC218\uAC00 \uCEE4\uC11C \uC218\uC640 \uC77C\uCE58\uD558\uB294 \uACBD\uC6B0 \uBD99\uC5EC\uB123\uAE30\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC758\uBBF8 \uCCB4\uACC4 \uAE30\uD638 \uD56D\uBAA9\uC744 \uAC15\uC870 \uD45C\uC2DC\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uAC1C\uC694 \uB208\uAE08\uC790 \uC8FC\uC704\uC5D0 \uD14C\uB450\uB9AC\uB97C \uADF8\uB9B4\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","Peek\uB97C \uC5EC\uB294 \uB3D9\uC548 \uD2B8\uB9AC\uC5D0 \uD3EC\uCEE4\uC2A4","\uBBF8\uB9AC \uBCF4\uAE30\uB97C \uC5F4 \uB54C \uD3B8\uC9D1\uAE30\uC5D0 \uD3EC\uCEE4\uC2A4","\uBBF8\uB9AC \uBCF4\uAE30 \uC704\uC82F\uC5D0\uC11C \uC778\uB77C\uC778 \uD3B8\uC9D1\uAE30\uC5D0 \uD3EC\uCEE4\uC2A4\uB97C \uB458\uC9C0 \uB610\uB294 \uD2B8\uB9AC\uC5D0 \uD3EC\uCEE4\uC2A4\uB97C \uB458\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC774\uB3D9 \uC815\uC758 \uB9C8\uC6B0\uC2A4 \uC81C\uC2A4\uCC98\uAC00 \uD56D\uC0C1 \uBBF8\uB9AC \uBCF4\uAE30 \uC704\uC82F\uC744 \uC5F4\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBE60\uB978 \uC81C\uC548\uC744 \uD45C\uC2DC\uD558\uAE30 \uC804\uAE4C\uC9C0\uC758 \uC9C0\uC5F0 \uC2DC\uAC04(\uBC00\uB9AC\uCD08)\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uAC00 \uC720\uD615\uC5D0 \uB530\uB77C \uC790\uB3D9\uC73C\uB85C \uC774\uB984\uC744 \uBC14\uAFC0\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC0AC\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB300\uC2E0 `editor.linkedEditing`\uC744 \uC0AC\uC6A9\uD558\uC138\uC694.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC81C\uC5B4 \uBB38\uC790\uB97C \uB80C\uB354\uB9C1\uD560\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uB4E4\uC5EC\uC4F0\uAE30 \uAC00\uC774\uB4DC\uB97C \uB80C\uB354\uB9C1\uD560\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD30C\uC77C\uC774 \uC904 \uBC14\uAFC8\uC73C\uB85C \uB05D\uB098\uBA74 \uB9C8\uC9C0\uB9C9 \uC904 \uBC88\uD638\uB97C \uB80C\uB354\uB9C1\uD569\uB2C8\uB2E4.","\uC81C\uBCF8\uC6A9 \uC5EC\uBC31\uACFC \uD604\uC7AC \uC904\uC744 \uBAA8\uB450 \uAC15\uC870 \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uAC00 \uD604\uC7AC \uC904 \uAC15\uC870 \uD45C\uC2DC\uB97C \uB80C\uB354\uB9C1\uD558\uB294 \uBC29\uC2DD\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0 \uD3EC\uCEE4\uC2A4\uAC00 \uC788\uB294 \uACBD\uC6B0\uC5D0\uB9CC \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD604\uC7AC \uC904 \uAC15\uC870 \uD45C\uC2DC\uB97C \uB80C\uB354\uB9C1\uD574\uC57C \uD558\uB294\uC9C0 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB2E8\uC5B4 \uC0AC\uC774\uC758 \uACF5\uBC31 \uD558\uB098\uB97C \uC81C\uC678\uD55C \uACF5\uBC31 \uBB38\uC790\uB97C \uB80C\uB354\uB9C1\uD569\uB2C8\uB2E4.","\uC120\uD0DD\uD55C \uD14D\uC2A4\uD2B8\uC5D0\uC11C\uB9CC \uACF5\uBC31 \uBB38\uC790\uB97C \uB80C\uB354\uB9C1\uD569\uB2C8\uB2E4.","\uD6C4\uD589 \uACF5\uBC31 \uBB38\uC790\uB9CC \uB80C\uB354\uB9C1","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uACF5\uBC31 \uBB38\uC790\uB97C \uB80C\uB354\uB9C1\uD560 \uBC29\uBC95\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC120\uD0DD \uD56D\uBAA9\uC758 \uBAA8\uC11C\uB9AC\uB97C \uB465\uAE00\uAC8C \uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uAC00\uB85C\uB85C \uC2A4\uD06C\uB864\uB418\uB294 \uBC94\uC704\uB97C \uBC97\uC5B4\uB098\uB294 \uCD94\uAC00 \uBB38\uC790\uC758 \uC218\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uB9C8\uC9C0\uB9C9 \uC904 \uC774\uD6C4\uB85C \uC2A4\uD06C\uB864\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC138\uB85C\uC640 \uAC00\uB85C\uB85C \uB3D9\uC2DC\uC5D0 \uC2A4\uD06C\uB864\uD560 \uB54C\uC5D0\uB9CC \uC8FC\uCD95\uC744 \uB530\uB77C\uC11C \uC2A4\uD06C\uB864\uD569\uB2C8\uB2E4. \uD2B8\uB799\uD328\uB4DC\uC5D0\uC11C \uC138\uB85C\uB85C \uC2A4\uD06C\uB864\uD560 \uB54C \uAC00\uB85C \uB4DC\uB9AC\uD504\uD2B8\uB97C \uBC29\uC9C0\uD569\uB2C8\uB2E4.","Linux \uC8FC \uD074\uB9BD\uBCF4\uB4DC\uC758 \uC9C0\uC6D0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uAC00 \uC120\uD0DD \uD56D\uBAA9\uACFC \uC720\uC0AC\uD55C \uC77C\uCE58 \uD56D\uBAA9\uC744 \uAC15\uC870 \uD45C\uC2DC\uD574\uC57C\uD558\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC811\uAE30 \uCEE8\uD2B8\uB864\uC744 \uD56D\uC0C1 \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uAC00 \uC5EC\uBC31 \uC704\uC5D0 \uC788\uC744 \uB54C\uC5D0\uB9CC \uC811\uAE30 \uCEE8\uD2B8\uB864\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uC5EC\uBC31\uC758 \uC811\uAE30 \uCEE8\uD2B8\uB864\uC774 \uD45C\uC2DC\uB418\uB294 \uC2DC\uAE30\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB294 \uCF54\uB4DC\uC758 \uD398\uC774\uB4DC \uC544\uC6C3\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uCDE8\uC18C\uC120 \uC0AC\uC6A9\uB418\uC9C0 \uC54A\uB294 \uBCC0\uC218\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB2E4\uB978 \uC81C\uC548 \uC704\uC5D0 \uC870\uAC01 \uC81C\uC548\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uB2E4\uB978 \uC81C\uC548 \uC544\uB798\uC5D0 \uC870\uAC01 \uC81C\uC548\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uB2E4\uB978 \uC81C\uC548\uACFC \uD568\uAED8 \uC870\uAC01 \uC81C\uC548\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uCF54\uB4DC \uC870\uAC01 \uC81C\uC548\uC744 \uD45C\uC2DC\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uCF54\uB4DC \uC870\uAC01\uC774 \uB2E4\uB978 \uCD94\uCC9C\uACFC \uD568\uAED8 \uD45C\uC2DC\uB418\uB294\uC9C0 \uC5EC\uBD80 \uBC0F \uC815\uB82C \uBC29\uBC95\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC560\uB2C8\uBA54\uC774\uC158\uC744 \uC0AC\uC6A9\uD558\uC5EC \uC2A4\uD06C\uB864\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uC81C\uC548 \uC704\uC82F\uC758 \uAE00\uAF34 \uD06C\uAE30\uC785\uB2C8\uB2E4. '0'\uC73C\uB85C \uC124\uC815\uD558\uBA74 '#editor.fontSize#'\uC758 \uAC12\uC774 \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uC81C\uC548 \uC704\uC82F\uC758 \uC904 \uB192\uC774\uC785\uB2C8\uB2E4. '0'\uC73C\uB85C \uC124\uC815\uD558\uBA74 `#editor.lineHeight#`\uC758 \uAC12\uC774 \uC0AC\uC6A9\uB429\uB2C8\uB2E4. \uCD5C\uC19F\uAC12\uC740 8\uC785\uB2C8\uB2E4.","\uD2B8\uB9AC\uAC70 \uBB38\uC790\uB97C \uC785\uB825\uD560 \uB54C \uC81C\uC548\uC744 \uC790\uB3D9\uC73C\uB85C \uD45C\uC2DC\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD56D\uC0C1 \uCCAB \uBC88\uC9F8 \uC81C\uC548\uC744 \uC120\uD0DD\uD569\uB2C8\uB2E4.","`log`\uAC00 \uCD5C\uADFC\uC5D0 \uC644\uB8CC\uB418\uC5C8\uC73C\uBBC0\uB85C \uCD94\uAC00 \uC785\uB825\uC5D0\uC11C \uC81C\uC548\uC744 \uC120\uD0DD\uD558\uC9C0 \uC54A\uC740 \uACBD\uC6B0 \uCD5C\uADFC \uC81C\uC548\uC744 \uC120\uD0DD\uD558\uC138\uC694(\uC608: `console.| -> console.log`).","\uD574\uB2F9 \uC81C\uC548\uC744 \uC644\uB8CC\uD55C \uC774\uC804 \uC811\uB450\uC0AC\uC5D0 \uB530\uB77C \uC81C\uC548\uC744 \uC120\uD0DD\uD569\uB2C8\uB2E4(\uC608: `co -> console` \uBC0F `con -> const`).","\uC81C\uC548 \uBAA9\uB85D\uC744 \uD45C\uC2DC\uD560 \uB54C \uC81C\uD55C\uC774 \uBBF8\uB9AC \uC120\uD0DD\uB418\uB294 \uBC29\uC2DD\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD0ED \uC644\uB8CC\uB294 \uD0ED\uC744 \uB204\uB97C \uB54C \uAC00\uC7A5 \uC77C\uCE58\uD558\uB294 \uC81C\uC548\uC744 \uC0BD\uC785\uD569\uB2C8\uB2E4.","\uD0ED \uC644\uC131\uC744 \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB3C4\uB85D \uC124\uC815\uD569\uB2C8\uB2E4.","\uC811\uB450\uC0AC\uAC00 \uC77C\uCE58\uD558\uB294 \uACBD\uC6B0 \uCF54\uB4DC \uC870\uAC01\uC744 \uD0ED \uC644\uB8CC\uD569\uB2C8\uB2E4. 'quickSuggestions'\uB97C \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uC744 \uB54C \uAC00\uC7A5 \uC798 \uC791\uB3D9\uD569\uB2C8\uB2E4.","\uD0ED \uC644\uC131\uC744 \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uD569\uB2C8\uB2E4.","\uBE44\uC815\uC0C1\uC801\uC778 \uC904 \uC885\uACB0\uC790\uAC00 \uC790\uB3D9\uC73C\uB85C \uC81C\uAC70\uB429\uB2C8\uB2E4.","\uBE44\uC815\uC0C1\uC801\uC778 \uC904 \uC885\uACB0\uC790\uAC00 \uBB34\uC2DC\uB429\uB2C8\uB2E4.","\uC81C\uAC70\uD560 \uBE44\uC815\uC0C1\uC801\uC778 \uC904 \uC885\uACB0\uC790 \uD504\uB86C\uD504\uD2B8\uC785\uB2C8\uB2E4.","\uBB38\uC81C\uB97C \uC77C\uC73C\uD0AC \uC218 \uC788\uB294 \uBE44\uC815\uC0C1\uC801\uC778 \uC904 \uC885\uACB0\uC790\uB97C \uC81C\uAC70\uD569\uB2C8\uB2E4.","\uD0ED \uC815\uC9C0 \uB4A4\uC5D0 \uACF5\uBC31\uC744 \uC0BD\uC785 \uBC0F \uC0AD\uC81C\uD569\uB2C8\uB2E4.","\uB2E8\uC5B4 \uAD00\uB828 \uD0D0\uC0C9 \uB610\uB294 \uC791\uC5C5\uC744 \uC218\uD589\uD560 \uB54C \uB2E8\uC5B4 \uAD6C\uBD84 \uAE30\uD638\uB85C \uC0AC\uC6A9\uD560 \uBB38\uC790\uC785\uB2C8\uB2E4.","\uC904\uC774 \uBC14\uB00C\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uBDF0\uD3EC\uD2B8 \uB108\uBE44\uC5D0\uC11C \uC904\uC774 \uBC14\uB01D\uB2C8\uB2E4.","`#editor.wordWrapColumn#`\uC5D0\uC11C \uC904\uC774 \uBC14\uB01D\uB2C8\uB2E4.","\uBDF0\uD3EC\uD2B8\uC758 \uCD5C\uC18C\uAC12 \uBC0F `#editor.wordWrapColumn#`\uC5D0\uC11C \uC904\uC774 \uBC14\uB01D\uB2C8\uB2E4.","\uC904 \uBC14\uAFC8 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","`#editor.wordWrap#`\uC774 `wordWrapColumn` \uB610\uB294 'bounded'\uC778 \uACBD\uC6B0 \uD3B8\uC9D1\uAE30\uC758 \uC5F4 \uC904 \uBC14\uAFC8\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uB4E4\uC5EC\uC4F0\uAE30\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uC904 \uBC14\uAFC8 \uD589\uC774 \uC5F4 1\uC5D0\uC11C \uC2DC\uC791\uB429\uB2C8\uB2E4.","\uC904 \uBC14\uAFC8 \uD589\uC758 \uB4E4\uC5EC\uC4F0\uAE30\uAC00 \uBD80\uBAA8\uC640 \uB3D9\uC77C\uD569\uB2C8\uB2E4.","\uC904 \uBC14\uAFC8 \uD589\uC774 \uBD80\uBAA8 \uCABD\uC73C\uB85C +1\uB9CC\uD07C \uB4E4\uC5EC\uC4F0\uAE30\uB429\uB2C8\uB2E4.","\uC904 \uBC14\uAFC8 \uD589\uC774 \uBD80\uBAA8 \uCABD\uC73C\uB85C +2\uB9CC\uD07C \uB4E4\uC5EC\uC4F0\uAE30\uB429\uB2C8\uB2E4.","\uC904 \uBC14\uAFC8 \uD589\uC758 \uB4E4\uC5EC\uC4F0\uAE30\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBAA8\uB4E0 \uBB38\uC790\uAC00 \uB3D9\uC77C\uD55C \uB108\uBE44\uB77C\uACE0 \uAC00\uC815\uD569\uB2C8\uB2E4. \uC774 \uC54C\uACE0\uB9AC\uC998\uC740 \uACE0\uC815 \uD3ED \uAE00\uAF34\uACFC \uBB38\uC790 \uBAA8\uC591\uC758 \uB108\uBE44\uAC00 \uAC19\uC740 \uD2B9\uC815 \uC2A4\uD06C\uB9BD\uD2B8(\uC608: \uB77C\uD2F4 \uBB38\uC790)\uC5D0 \uC801\uC808\uD788 \uC791\uB3D9\uD558\uB294 \uBE60\uB978 \uC54C\uACE0\uB9AC\uC998\uC785\uB2C8\uB2E4.","\uB798\uD551 \uC810 \uACC4\uC0B0\uC744 \uBE0C\uB77C\uC6B0\uC800\uC5D0 \uC704\uC784\uD569\uB2C8\uB2E4. \uC774 \uC54C\uACE0\uB9AC\uC998\uC740 \uB9E4\uC6B0 \uB290\uB824\uC11C \uB300\uC6A9\uB7C9 \uD30C\uC77C\uC758 \uACBD\uC6B0 \uC911\uB2E8\uB420 \uC218 \uC788\uC9C0\uB9CC \uBAA8\uB4E0 \uACBD\uC6B0\uC5D0 \uC801\uC808\uD788 \uC791\uB3D9\uD569\uB2C8\uB2E4.","\uB798\uD551 \uC810\uC744 \uACC4\uC0B0\uD558\uB294 \uC54C\uACE0\uB9AC\uC998\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4."],"vs/editor/common/editorContextKeys":["Whether the editor text has focus (cursor is blinking)","Whether the editor or an editor widget has focus (e.g. focus is in the find widget)","Whether an editor or a rich text input has focus (cursor is blinking)","Whether the editor is read only","Whether the context is a diff editor","Whether `editor.columnSelection` is enabled","Whether the editor has text selected","Whether the editor has multiple selections","Whether `Tab` will move focus out of the editor","Whether the editor hover is visible","Whether the editor is part of a larger editor (e.g. notebooks)","The language identifier of the editor","Whether the editor has a completion item provider","Whether the editor has a code actions provider","Whether the editor has a code lens provider","Whether the editor has a definition provider","Whether the editor has a declaration provider","Whether the editor has an implementation provider","Whether the editor has a type definition provider","Whether the editor has a hover provider","Whether the editor has a document highlight provider","Whether the editor has a document symbol provider","Whether the editor has a reference provider","Whether the editor has a rename provider","Whether the editor has a signature help provider","Whether the editor has an inline hints provider","Whether the editor has a document formatting provider","Whether the editor has a document selection formatting provider","Whether the editor has multiple document formatting providers","Whether the editor has multiple document selection formatting providers"],"vs/editor/common/model/editStack":["\uC785\uB825\uD558\uB294 \uC911"],"vs/editor/common/modes/modesRegistry":["\uC77C\uBC18 \uD14D\uC2A4\uD2B8"],"vs/editor/common/standaloneStrings":["\uC5C6\uC74C \uC120\uD0DD","\uC904 {0}, \uC5F4 {1}({2} \uC120\uD0DD\uB428)\uC785\uB2C8\uB2E4.","\uD589 {0}, \uC5F4 {1}","{0} \uC120\uD0DD \uD56D\uBAA9({1}\uC790 \uC120\uD0DD\uB428)","{0} \uC120\uD0DD \uD56D\uBAA9","\uC774\uC81C 'accessibilitySupport' \uC124\uC815\uC744 'on'\uC73C\uB85C \uBCC0\uACBD\uD569\uB2C8\uB2E4.","\uC9C0\uAE08 \uD3B8\uC9D1\uAE30 \uC811\uADFC\uC131 \uBB38\uC11C \uD398\uC774\uC9C0\uB97C \uC5EC\uC138\uC694.","\uCC28\uC774 \uD3B8\uC9D1\uAE30\uC758 \uC77D\uAE30 \uC804\uC6A9 \uCC3D\uC5D0\uC11C.","diff \uD3B8\uC9D1\uAE30 \uCC3D\uC5D0\uC11C."," \uC77D\uAE30 \uC804\uC6A9 \uCF54\uB4DC \uD3B8\uC9D1\uAE30\uC5D0\uC11C"," \uCF54\uB4DC \uD3B8\uC9D1\uAE30\uC5D0\uC11C","\uD654\uBA74 \uD310\uB3C5\uAE30 \uC0AC\uC6A9\uC5D0 \uCD5C\uC801\uD654\uB418\uB3C4\uB85D \uD3B8\uC9D1\uAE30\uB97C \uAD6C\uC131\uD558\uB824\uBA74 \uC9C0\uAE08 Command+E\uB97C \uB204\uB974\uC138\uC694.","\uD654\uBA74 \uD310\uB3C5\uAE30\uC5D0 \uC0AC\uC6A9\uD560 \uC218 \uC788\uB3C4\uB85D \uD3B8\uC9D1\uAE30\uB97C \uCD5C\uC801\uD654\uD558\uB824\uBA74 \uC9C0\uAE08 Ctrl+E\uB97C \uB204\uB974\uC138\uC694.","\uC5D0\uB514\uD130\uB97C \uD654\uBA74 \uD310\uB3C5\uAE30\uC640 \uD568\uAED8 \uC0AC\uC6A9\uD558\uAE30\uC5D0 \uC801\uD569\uD558\uB3C4\uB85D \uAD6C\uC131\uD588\uC2B5\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uB294 \uD654\uBA74 \uD310\uB3C5\uAE30 \uC0AC\uC6A9\uC744 \uC704\uD574 \uC808\uB300\uB85C \uCD5C\uC801\uD654\uB418\uC9C0 \uC54A\uB3C4\uB85D \uAD6C\uC131\uB429\uB2C8\uB2E4. \uD604\uC7AC\uB85C\uC11C\uB294 \uADF8\uB807\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uD604\uC7AC \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD0A4\uB97C \uB204\uB974\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uB2E4\uC74C \uD3EC\uCEE4\uC2A4 \uAC00\uB2A5\uD55C \uC694\uC18C\uB85C \uC774\uB3D9\uD569\uB2C8\uB2E4. {0}\uC744(\uB97C) \uB20C\uB7EC\uC11C \uC774 \uB3D9\uC791\uC744 \uC124\uC815/\uD574\uC81C\uD569\uB2C8\uB2E4.","\uD604\uC7AC \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD0A4\uB97C \uB204\uB974\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uB2E4\uC74C \uD3EC\uCEE4\uC2A4 \uAC00\uB2A5\uD55C \uC694\uC18C\uB85C \uC774\uB3D9\uD569\uB2C8\uB2E4. {0} \uBA85\uB839\uC740 \uD604\uC7AC \uD0A4 \uBC14\uC778\uB529\uC73C\uB85C \uD2B8\uB9AC\uAC70\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uD604\uC7AC \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD0A4\uB97C \uB204\uB974\uBA74 \uD0ED \uBB38\uC790\uAC00 \uC0BD\uC785\uB429\uB2C8\uB2E4. {0}\uC744(\uB97C) \uB20C\uB7EC\uC11C \uC774 \uB3D9\uC791\uC744 \uC124\uC815/\uD574\uC81C\uD569\uB2C8\uB2E4.","\uD604\uC7AC \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD0A4\uB97C \uB204\uB974\uBA74 \uD0ED \uBB38\uC790\uAC00 \uC0BD\uC785\uB429\uB2C8\uB2E4. {0} \uBA85\uB839\uC740 \uD604\uC7AC \uD0A4 \uBC14\uC778\uB529\uC73C\uB85C \uD2B8\uB9AC\uAC70\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","Command+H\uB97C \uB20C\uB7EC \uD3B8\uC9D1\uAE30 \uC811\uADFC\uC131\uACFC \uAD00\uB828\uB41C \uC790\uC138\uD55C \uC815\uBCF4\uAC00 \uC788\uB294 \uBE0C\uB77C\uC6B0\uC800 \uCC3D\uC744 \uC5EC\uC138\uC694.","Ctrl+H\uB97C \uB20C\uB7EC \uD3B8\uC9D1\uAE30 \uC811\uADFC\uC131\uACFC \uAD00\uB828\uB41C \uC790\uC138\uD55C \uC815\uBCF4\uAC00 \uC788\uB294 \uBE0C\uB77C\uC6B0\uC800 \uCC3D\uC744 \uC5FD\uB2C8\uB2E4.","\uC774 \uB3C4\uAD6C \uC124\uBA85\uC744 \uD574\uC81C\uD558\uACE0 Esc \uD0A4 \uB610\uB294 Shift+Esc\uB97C \uB20C\uB7EC\uC11C \uD3B8\uC9D1\uAE30\uB85C \uB3CC\uC544\uAC08 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uC811\uADFC\uC131 \uB3C4\uC6C0\uB9D0 \uD45C\uC2DC","\uAC1C\uBC1C\uC790: \uAC80\uC0AC \uD1A0\uD070","\uC904/\uC5F4\uB85C \uC774\uB3D9...","\uBE60\uB978 \uC561\uC138\uC2A4 \uACF5\uAE09\uC790 \uBAA8\uB450 \uD45C\uC2DC","\uBA85\uB839 \uD314\uB808\uD2B8","\uBA85\uB839 \uD45C\uC2DC \uBC0F \uC2E4\uD589","\uAE30\uD638\uB85C \uAC00\uC11C...","\uBC94\uC8FC\uBCC4 \uAE30\uD638\uB85C \uC774\uB3D9...","\uD3B8\uC9D1\uAE30 \uCF58\uD150\uCE20","\uC811\uADFC\uC131 \uC635\uC158\uC740 Alt+F1\uC744 \uB20C\uB7EC\uC5EC \uD569\uB2C8\uB2E4.","\uACE0\uB300\uBE44 \uD14C\uB9C8\uB85C \uC804\uD658","{1} \uD30C\uC77C\uC5D0\uC11C \uD3B8\uC9D1\uC744 {0}\uAC1C \uD588\uC2B5\uB2C8\uB2E4."],"vs/editor/common/view/editorColorRegistry":["\uCEE4\uC11C \uC704\uCE58\uC758 \uC904 \uAC15\uC870 \uD45C\uC2DC\uC5D0 \uB300\uD55C \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uCEE4\uC11C \uC704\uCE58\uC758 \uC904 \uD14C\uB450\uB9AC\uC5D0 \uB300\uD55C \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBE60\uB978 \uC5F4\uAE30 \uBC0F \uCC3E\uAE30 \uAE30\uB2A5 \uB4F1\uC744 \uD1B5\uD574 \uAC15\uC870 \uD45C\uC2DC\uB41C \uC601\uC5ED\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uAC15\uC870 \uC601\uC5ED \uC8FC\uBCC0\uC758 \uD14C\uB450\uB9AC\uC5D0 \uB300\uD55C \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4","\uAC15\uC870 \uD45C\uC2DC\uB41C \uAE30\uD638(\uC608: \uC815\uC758\uB85C \uC774\uB3D9 \uB610\uB294 \uB2E4\uC74C/\uC774\uC804 \uAE30\uD638\uB85C \uC774\uB3D9)\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774 \uC0C9\uC0C1\uC740 \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uAC15\uC870 \uD45C\uC2DC\uB41C \uAE30\uD638 \uC8FC\uC704\uC758 \uD14C\uB450\uB9AC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCEE4\uC11C \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCEE4\uC11C\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBE14\uB85D \uCEE4\uC11C\uC640 \uACB9\uCE58\uB294 \uAE00\uC790\uC758 \uC0C9\uC0C1\uC744 \uC0AC\uC6A9\uC790 \uC815\uC758\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC758 \uACF5\uBC31 \uBB38\uC790 \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uB4E4\uC5EC\uC4F0\uAE30 \uC548\uB0B4\uC120 \uC0C9\uC785\uB2C8\uB2E4.","\uD65C\uC131 \uD3B8\uC9D1\uAE30 \uB4E4\uC5EC\uC4F0\uAE30 \uC548\uB0B4\uC120 \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uC904 \uBC88\uD638 \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uD65C\uC131 \uC601\uC5ED \uC904\uBC88\uD638 \uC0C9\uC0C1","ID\uB294 \uC0AC\uC6A9\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. \uB300\uC2E0 'editorLineNumber.activeForeground'\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.","\uD3B8\uC9D1\uAE30 \uD65C\uC131 \uC601\uC5ED \uC904\uBC88\uD638 \uC0C9\uC0C1","\uD3B8\uC9D1\uAE30 \uB208\uAE08\uC758 \uC0C9\uC0C1\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCF54\uB4DC \uB80C\uC988\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC77C\uCE58\uD558\uB294 \uAD04\uD638 \uB4A4\uC758 \uBC30\uACBD\uC0C9","\uC77C\uCE58\uD558\uB294 \uBE0C\uB798\uD0B7 \uBC15\uC2A4\uC758 \uC0C9\uC0C1","\uAC1C\uC694 \uB208\uAE08 \uACBD\uACC4\uC758 \uC0C9\uC0C1\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uAC1C\uC694 \uB208\uAE08\uC790\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBBF8\uB2C8\uB9F5\uC774 \uC0AC\uC6A9\uD558\uB3C4\uB85D \uC124\uC815\uB418\uC5B4 \uD3B8\uC9D1\uAE30\uC758 \uC624\uB978\uCABD\uC5D0 \uBC30\uCE58\uB41C \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uAC70\uD130\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAC70\uD130\uC5D0\uB294 \uAE00\uB9AC\uD504 \uC5EC\uBC31\uACFC \uD589 \uC218\uAC00 \uC788\uC2B5\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC758 \uBD88\uD544\uC694\uD55C(\uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB294) \uC18C\uC2A4 \uCF54\uB4DC \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.",`\uD3B8\uC9D1\uAE30\uC758 \uBD88\uD544\uC694\uD55C(\uC0AC\uC6A9\uD558\uC9C0 \uC54A\uB294) \uC18C\uC2A4 \uCF54\uB4DC \uBD88\uD22C\uBA85\uB3C4\uC785\uB2C8\uB2E4. \uC608\uB97C \uB4E4\uC5B4 "#000000c0"\uC740 75% \uBD88\uD22C\uBA85\uB3C4\uB85C \uCF54\uB4DC\uB97C \uB80C\uB354\uB9C1\uD569\uB2C8\uB2E4. \uACE0\uB300\uBE44 \uD14C\uB9C8\uC758 \uACBD\uC6B0 \uD398\uC774\uB4DC \uC544\uC6C3\uD558\uC9C0 \uC54A\uACE0 'editorUnnecessaryCode.border' \uD14C\uB9C8 \uC0C9\uC744 \uC0AC\uC6A9\uD558\uC5EC \uBD88\uD544\uC694\uD55C \uCF54\uB4DC\uC5D0 \uBC11\uC904\uC744 \uADF8\uC73C\uC138\uC694.`,"\uBC94\uC704\uC758 \uAC1C\uC694 \uB208\uAE08\uC790 \uD45C\uC2DD \uC0C9\uC774 \uAC15\uC870 \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uC624\uB958\uC758 \uAC1C\uC694 \uB208\uAE08\uC790 \uB9C8\uCEE4 \uC0C9\uC785\uB2C8\uB2E4.","\uACBD\uACE0\uC758 \uAC1C\uC694 \uB208\uAE08\uC790 \uB9C8\uCEE4 \uC0C9\uC785\uB2C8\uB2E4.","\uC815\uBCF4\uC758 \uAC1C\uC694 \uB208\uAE08\uC790 \uB9C8\uCEE4 \uC0C9\uC785\uB2C8\uB2E4."],"vs/editor/contrib/anchorSelect/anchorSelect":["\uC120\uD0DD \uC575\uCEE4 \uC9C0\uC810","{0}\uC5D0 \uC124\uC815\uB41C \uC575\uCEE4: {1}","\uC120\uD0DD \uC575\uCEE4 \uC9C0\uC810 \uC124\uC815","\uC120\uD0DD \uC575\uCEE4 \uC9C0\uC810\uC73C\uB85C \uC774\uB3D9","\uC575\uCEE4\uC5D0\uC11C \uCEE4\uC11C\uB85C \uC120\uD0DD","\uC120\uD0DD \uC575\uCEE4 \uC9C0\uC810 \uCDE8\uC18C"],"vs/editor/contrib/bracketMatching/bracketMatching":["\uAD04\uD638\uC5D0 \uD574\uB2F9\uD558\uB294 \uC601\uC5ED\uC744 \uD45C\uC2DC\uC790\uC5D0 \uCC44\uC0C9\uD558\uC5EC \uD45C\uC2DC\uD569\uB2C8\uB2E4.","\uB300\uAD04\uD638\uB85C \uC774\uB3D9","\uAD04\uD638\uAE4C\uC9C0 \uC120\uD0DD","\uB300\uAD04\uD638\uB85C \uC774\uB3D9(&&B)"],"vs/editor/contrib/caretOperations/caretOperations":["\uC120\uD0DD\uD55C \uD14D\uC2A4\uD2B8\uB97C \uC67C\uCABD\uC73C\uB85C \uC774\uB3D9","\uC120\uD0DD\uD55C \uD14D\uC2A4\uD2B8\uB97C \uC624\uB978\uCABD\uC73C\uB85C \uC774\uB3D9"],"vs/editor/contrib/caretOperations/transpose":["\uBB38\uC790 \uBC14\uAFB8\uAE30"],"vs/editor/contrib/clipboard/clipboard":["\uC798\uB77C\uB0B4\uAE30(&&T)","\uC798\uB77C\uB0B4\uAE30","\uC798\uB77C\uB0B4\uAE30","\uBCF5\uC0AC(&&C)","\uBCF5\uC0AC","\uBCF5\uC0AC","\uBD99\uC5EC\uB123\uAE30(&&P)","\uBD99\uC5EC\uB123\uAE30","\uBD99\uC5EC\uB123\uAE30","\uAD6C\uBB38\uC744 \uAC15\uC870 \uD45C\uC2DC\uD558\uC5EC \uBCF5\uC0AC"],"vs/editor/contrib/codeAction/codeActionCommands":["\uC2E4\uD589\uD560 \uCF54\uB4DC \uC791\uC5C5\uC758 \uC885\uB958\uC785\uB2C8\uB2E4.","\uBC18\uD658\uB41C \uC791\uC5C5\uC774 \uC801\uC6A9\uB418\uB294 \uACBD\uC6B0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD56D\uC0C1 \uBC18\uD658\uB41C \uCCAB \uBC88\uC9F8 \uCF54\uB4DC \uC791\uC5C5\uC744 \uC801\uC6A9\uD569\uB2C8\uB2E4.","\uCCAB \uBC88\uC9F8 \uBC18\uD658\uB41C \uCF54\uB4DC \uC791\uC5C5\uC744 \uC801\uC6A9\uD569\uB2C8\uB2E4(\uC774 \uC791\uC5C5\uB9CC \uC788\uB294 \uACBD\uC6B0).","\uBC18\uD658\uB41C \uCF54\uB4DC \uC791\uC5C5\uC744 \uC801\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.","\uAE30\uBCF8 \uCF54\uB4DC \uC791\uC5C5\uB9CC \uBC18\uD658\uB418\uB3C4\uB85D \uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uCF54\uB4DC \uC791\uC5C5\uC744 \uC801\uC6A9\uD558\uB294 \uC911 \uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.","\uBE60\uB978 \uC218\uC815...","\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uCF54\uB4DC \uB3D9\uC791\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.","'{0}'\uC5D0 \uB300\uD55C \uAE30\uBCF8 \uCF54\uB4DC \uC791\uC5C5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC74C","'{0}'\uC5D0 \uB300\uD55C \uCF54\uB4DC \uC791\uC5C5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC74C","\uC0AC\uC6A9\uD560 \uC218 \uC788\uB294 \uAE30\uBCF8 \uCF54\uB4DC \uC791\uC5C5 \uC5C6\uC74C","\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uCF54\uB4DC \uB3D9\uC791\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.","\uB9AC\uD329\uD130\uB9C1...","'{0}'\uC5D0 \uB300\uD55C \uAE30\uBCF8 \uB9AC\uD329\uD130\uB9C1 \uC5C6\uC74C","'{0}'\uC5D0 \uB300\uD55C \uB9AC\uD329\uD130\uB9C1 \uC5C6\uC74C","\uAE30\uBCF8 \uC124\uC815 \uB9AC\uD329\uD130\uB9C1\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC74C","\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uB9AC\uD399\uD130\uB9C1\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.","\uC18C\uC2A4 \uC791\uC5C5...","'{0}'\uC5D0 \uB300\uD55C \uAE30\uBCF8 \uC18C\uC2A4 \uC791\uC5C5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC74C","'{0}'\uC5D0 \uB300\uD55C \uC18C\uC2A4 \uC791\uC5C5\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC74C","\uC0AC\uC6A9\uD560 \uC218 \uC788\uB294 \uAE30\uBCF8 \uC6D0\uBCF8 \uC791\uC5C5 \uC5C6\uC74C","\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC18C\uC2A4 \uC791\uC5C5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.","\uAC00\uC838\uC624\uAE30 \uAD6C\uC131","\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uAC00\uC838\uC624\uAE30 \uAD6C\uC131 \uC791\uC5C5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA8\uB450 \uC218\uC815","\uBAA8\uB4E0 \uC791\uC5C5 \uC218\uC815 \uC0AC\uC6A9 \uBD88\uAC00","\uC790\uB3D9 \uC218\uC815...","\uC0AC\uC6A9\uD560 \uC218 \uC788\uB294 \uC790\uB3D9 \uC218\uC815 \uC5C6\uC74C"],"vs/editor/contrib/codeAction/lightBulbWidget":["\uC218\uC815 \uC0AC\uD56D\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uAE30\uBCF8 \uC218\uC815({0})","\uC218\uC815 \uC0AC\uD56D \uD45C\uC2DC({0})","\uC218\uC815 \uC0AC\uD56D \uD45C\uC2DC"],"vs/editor/contrib/codelens/codelensController":["\uD604\uC7AC \uC904\uC5D0 \uB300\uD55C \uCF54\uB4DC \uB80C\uC988 \uBA85\uB839 \uD45C\uC2DC"],"vs/editor/contrib/comment/comment":["\uC904 \uC8FC\uC11D \uC124\uC815/\uD574\uC81C","\uC904 \uC8FC\uC11D \uC124\uC815/\uD574\uC81C(&&T)","\uC904 \uC8FC\uC11D \uCD94\uAC00","\uC904 \uC8FC\uC11D \uC81C\uAC70","\uBE14\uB85D \uC8FC\uC11D \uC124\uC815/\uD574\uC81C","\uBE14\uB85D \uC8FC\uC11D \uC124\uC815/\uD574\uC81C(&&B)"],"vs/editor/contrib/contextmenu/contextmenu":["\uD3B8\uC9D1\uAE30 \uC0C1\uD669\uC5D0 \uB9DE\uB294 \uBA54\uB274 \uD45C\uC2DC"],"vs/editor/contrib/cursorUndo/cursorUndo":["\uCEE4\uC11C \uC2E4\uD589 \uCDE8\uC18C","\uCEE4\uC11C \uB2E4\uC2DC \uC2E4\uD589"],"vs/editor/contrib/find/findController":["\uCC3E\uAE30","\uCC3E\uAE30(&&F)","\uC120\uD0DD \uC601\uC5ED\uC5D0\uC11C \uCC3E\uAE30","\uB2E4\uC74C \uCC3E\uAE30","\uB2E4\uC74C \uCC3E\uAE30","\uC774\uC804 \uCC3E\uAE30","\uC774\uC804 \uCC3E\uAE30","\uB2E4\uC74C \uC120\uD0DD \uCC3E\uAE30","\uC774\uC804 \uC120\uD0DD \uCC3E\uAE30","\uBC14\uAFB8\uAE30","\uBC14\uAFB8\uAE30(&&R)"],"vs/editor/contrib/find/findWidget":["\uD3B8\uC9D1\uAE30 \uCC3E\uAE30 \uC704\uC82F\uC5D0\uC11C '\uC120\uD0DD \uC601\uC5ED\uC5D0\uC11C \uCC3E\uAE30'\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCC3E\uAE30 \uC704\uC82F\uC774 \uCD95\uC18C\uB418\uC5C8\uC74C\uC744 \uB098\uD0C0\uB0B4\uB294 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCC3E\uAE30 \uC704\uC82F\uC774 \uD655\uC7A5\uB418\uC5C8\uC74C\uC744 \uB098\uD0C0\uB0B4\uB294 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCC3E\uAE30 \uC704\uC82F\uC5D0\uC11C '\uBC14\uAFB8\uAE30'\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCC3E\uAE30 \uC704\uC82F\uC5D0\uC11C '\uBAA8\uB450 \uBC14\uAFB8\uAE30'\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCC3E\uAE30 \uC704\uC82F\uC5D0\uC11C '\uC774\uC804 \uCC3E\uAE30'\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uCC3E\uAE30 \uC704\uC82F\uC5D0\uC11C '\uB2E4\uC74C \uCC3E\uAE30'\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uCC3E\uAE30","\uCC3E\uAE30","\uC774\uC804 \uC77C\uCE58","\uB2E4\uC74C \uC77C\uCE58 \uD56D\uBAA9","\uC120\uD0DD \uD56D\uBAA9\uC5D0\uC11C \uCC3E\uAE30","\uB2EB\uAE30","\uBC14\uAFB8\uAE30","\uBC14\uAFB8\uAE30","\uBC14\uAFB8\uAE30","\uBAA8\uB450 \uBC14\uAFB8\uAE30","\uBC14\uAFB8\uAE30 \uBAA8\uB4DC \uC124\uC815/\uD574\uC81C","\uCC98\uC74C {0}\uAC1C\uC758 \uACB0\uACFC\uAC00 \uAC15\uC870 \uD45C\uC2DC\uB418\uC9C0\uB9CC \uBAA8\uB4E0 \uCC3E\uAE30 \uC791\uC5C5\uC740 \uC804\uCCB4 \uD14D\uC2A4\uD2B8\uC5D0 \uB300\uD574 \uC218\uD589\uB429\uB2C8\uB2E4.","{1}\uC758 {0}","\uACB0\uACFC \uC5C6\uC74C","{0}\uAC1C \uCC3E\uC74C","'{1}'\uC5D0 \uB300\uD55C {0}\uC744(\uB97C) \uCC3E\uC74C","{2}\uC5D0\uC11C '{1}'\uC5D0 \uB300\uD55C {0}\uC744(\uB97C) \uCC3E\uC74C","'{1}'\uC5D0 \uB300\uD55C {0}\uC744(\uB97C) \uCC3E\uC74C","Ctrl+Enter\uB97C \uB204\uB974\uBA74 \uC774\uC81C \uBAA8\uB4E0 \uD56D\uBAA9\uC744 \uBC14\uAFB8\uC9C0 \uC54A\uACE0 \uC904 \uBC14\uAFC8\uC744 \uC0BD\uC785\uD569\uB2C8\uB2E4. editor.action.replaceAll\uC758 \uD0A4 \uBC14\uC778\uB529\uC744 \uC218\uC815\uD558\uC5EC \uC774 \uB3D9\uC791\uC744 \uC7AC\uC815\uC758\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."],"vs/editor/contrib/folding/folding":["\uD3BC\uCE58\uAE30","\uC7AC\uADC0\uC801\uC73C\uB85C \uD3BC\uCE58\uAE30","\uC811\uAE30","\uC811\uAE30 \uC804\uD658","\uC7AC\uADC0\uC801\uC73C\uB85C \uC811\uAE30","\uBAA8\uB4E0 \uBE14\uB85D \uCF54\uBA58\uD2B8\uB97C \uC811\uAE30","\uBAA8\uB4E0 \uC601\uC5ED \uC811\uAE30","\uBAA8\uB4E0 \uC601\uC5ED \uD3BC\uCE58\uAE30","\uBAA8\uB450 \uC811\uAE30","\uBAA8\uB450 \uD3BC\uCE58\uAE30","\uC218\uC900 {0} \uC811\uAE30","\uC811\uD78C \uBC94\uC704\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC0C9\uC740 \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uAE30 \uC704\uD574 \uBD88\uD22C\uBA85\uD574\uC11C\uB294 \uC548 \uB429\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uC5EC\uBC31\uC758 \uC811\uAE30 \uCEE8\uD2B8\uB864 \uC0C9\uC785\uB2C8\uB2E4."],"vs/editor/contrib/folding/foldingDecorations":["\uD3B8\uC9D1\uAE30 \uBB38\uC790 \uBAA8\uC591 \uC5EC\uBC31\uC5D0\uC11C \uD655\uC7A5\uB41C \uBC94\uC704\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uBB38\uC790 \uBAA8\uC591 \uC5EC\uBC31\uC5D0\uC11C \uCD95\uC18C\uB41C \uBC94\uC704\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4."],"vs/editor/contrib/fontZoom/fontZoom":["\uD3B8\uC9D1\uAE30 \uAE00\uAF34 \uD655\uB300","\uD3B8\uC9D1\uAE30 \uAE00\uAF34 \uCD95\uC18C","\uD3B8\uC9D1\uAE30 \uAE00\uAF34 \uD655\uB300/\uCD95\uC18C \uB2E4\uC2DC \uC124\uC815"],"vs/editor/contrib/format/format":["\uC904 {0}\uC5D0\uC11C 1\uAC1C \uC11C\uC2DD \uD3B8\uC9D1\uC744 \uC218\uD589\uD588\uC2B5\uB2C8\uB2E4.","\uC904 {1}\uC5D0\uC11C {0}\uAC1C \uC11C\uC2DD \uD3B8\uC9D1\uC744 \uC218\uD589\uD588\uC2B5\uB2C8\uB2E4.","\uC904 {0}\uACFC(\uC640) {1} \uC0AC\uC774\uC5D0\uC11C 1\uAC1C \uC11C\uC2DD \uD3B8\uC9D1\uC744 \uC218\uD589\uD588\uC2B5\uB2C8\uB2E4.","\uC904 {1}\uACFC(\uC640) {2} \uC0AC\uC774\uC5D0\uC11C {0}\uAC1C \uC11C\uC2DD \uD3B8\uC9D1\uC744 \uC218\uD589\uD588\uC2B5\uB2C8\uB2E4."],"vs/editor/contrib/format/formatActions":["\uBB38\uC11C \uC11C\uC2DD","\uC120\uD0DD \uC601\uC5ED \uC11C\uC2DD"],"vs/editor/contrib/gotoError/gotoError":["\uB2E4\uC74C \uBB38\uC81C\uB85C \uC774\uB3D9 (\uC624\uB958, \uACBD\uACE0, \uC815\uBCF4)","\uB2E4\uC74C \uB9C8\uCEE4\uB85C \uC774\uB3D9\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uC774\uC804 \uBB38\uC81C\uB85C \uC774\uB3D9 (\uC624\uB958, \uACBD\uACE0, \uC815\uBCF4)","\uC774\uC804 \uB9C8\uCEE4\uB85C \uC774\uB3D9\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uD30C\uC77C\uC758 \uB2E4\uC74C \uBB38\uC81C\uB85C \uC774\uB3D9 (\uC624\uB958, \uACBD\uACE0, \uC815\uBCF4)","\uB2E4\uC74C \uBB38\uC81C(&&P)","\uD30C\uC77C\uC758 \uC774\uC804 \uBB38\uC81C\uB85C \uC774\uB3D9 (\uC624\uB958, \uACBD\uACE0, \uC815\uBCF4)","\uC774\uC804 \uBB38\uC81C(&&P)"],"vs/editor/contrib/gotoError/gotoErrorWidget":["\uC624\uB958","\uACBD\uACE0","\uC815\uBCF4","\uD78C\uD2B8","{1}\uC758 {0}\uC785\uB2C8\uB2E4. ","\uBB38\uC81C {1}\uAC1C \uC911 {0}\uAC1C","\uBB38\uC81C {1}\uAC1C \uC911 {0}\uAC1C","\uD3B8\uC9D1\uAE30 \uD45C\uC2DD \uD0D0\uC0C9 \uC704\uC82F \uC624\uB958 \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uD45C\uC2DD \uD0D0\uC0C9 \uC704\uC82F \uACBD\uACE0 \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uD45C\uC2DD \uD0D0\uC0C9 \uC704\uC82F \uC815\uBCF4 \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uD45C\uC2DD \uD0D0\uC0C9 \uC704\uC82F \uBC30\uACBD\uC785\uB2C8\uB2E4."],"vs/editor/contrib/gotoSymbol/goToCommands":["\uD53C\uD0B9","\uC815\uC758","'{0}'\uC5D0 \uB300\uD55C \uC815\uC758\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uC815\uC758\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC74C","\uC815\uC758\uB85C \uC774\uB3D9","\uC815\uC758\uB85C \uC774\uB3D9(&&D)","\uCE21\uBA74\uC5D0\uC11C \uC815\uC758 \uC5F4\uAE30","\uC815\uC758 \uD53C\uD0B9","\uC120\uC5B8","'{0}'\uC5D0 \uB300\uD55C \uC120\uC5B8\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C","\uC120\uC5B8\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C","\uC120\uC5B8\uC73C\uB85C \uC774\uB3D9","\uC120\uC5B8\uC73C\uB85C \uC774\uB3D9(&&D)","'{0}'\uC5D0 \uB300\uD55C \uC120\uC5B8\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C","\uC120\uC5B8\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC74C","\uC120\uC5B8 \uBBF8\uB9AC \uBCF4\uAE30","\uD615\uC2DD \uC815\uC758","'{0}'\uC5D0 \uB300\uD55C \uD615\uC2DD \uC815\uC758\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uD615\uC2DD \uC815\uC758\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uD615\uC2DD \uC815\uC758\uB85C \uC774\uB3D9","\uD615\uC2DD \uC815\uC758\uB85C \uC774\uB3D9(&&T)","\uD615\uC2DD \uC815\uC758 \uBBF8\uB9AC \uBCF4\uAE30","\uAD6C\uD604","'{0}'\uC5D0 \uB300\uD55C \uAD6C\uD604\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uAD6C\uD604\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uAD6C\uD604\uC73C\uB85C \uC774\uB3D9","\uAD6C\uD604\uC73C\uB85C \uC774\uB3D9(&&I)","\uD53C\uD0B9 \uAD6C\uD604","'{0}'\uC5D0 \uB300\uD55C \uCC38\uC870\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uCC38\uC870\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uCC38\uC870\uB85C \uC774\uB3D9","\uCC38\uC870\uB85C \uC774\uB3D9(&&R)","\uCC38\uC870","\uCC38\uC870 \uBBF8\uB9AC \uBCF4\uAE30","\uCC38\uC870","\uAE30\uD638\uB85C \uC774\uB3D9","\uC704\uCE58","'{0}'\uC5D0 \uB300\uD55C \uAC80\uC0C9 \uACB0\uACFC\uAC00 \uC5C6\uC74C","\uCC38\uC870"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["{0}\uAC1C \uC815\uC758\uB97C \uD45C\uC2DC\uD558\uB824\uBA74 \uD074\uB9AD\uD558\uC138\uC694."],"vs/editor/contrib/gotoSymbol/peek/referencesController":["\uB85C\uB4DC \uC911...","{0}({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["\uCC38\uC870 {0}\uAC1C","\uCC38\uC870 {0}\uAC1C","\uCC38\uC870"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["\uBBF8\uB9AC \uBCF4\uAE30\uB97C \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC74C","\uACB0\uACFC \uC5C6\uC74C","\uCC38\uC870"],"vs/editor/contrib/gotoSymbol/referencesModel":["{2}\uC5F4, {1}\uC904, {0}\uC758 \uAE30\uD638","\uC5F4 {2}, {3}\uC758 \uC904 {1}\uC5D0 \uC788\uB294 {0}\uC758 \uAE30\uD638","{0}\uC758 \uAE30\uD638 1\uAC1C, \uC804\uCCB4 \uACBD\uB85C {1}","{1}\uC758 \uAE30\uD638 {0}\uAC1C, \uC804\uCCB4 \uACBD\uB85C {2}","\uACB0\uACFC \uC5C6\uC74C","{0}\uC5D0\uC11C \uAE30\uD638 1\uAC1C\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.","{1}\uC5D0\uC11C \uAE30\uD638 {0}\uAC1C\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4.","{1}\uAC1C \uD30C\uC77C\uC5D0\uC11C \uAE30\uD638 {0}\uAC1C\uB97C \uCC3E\uC558\uC2B5\uB2C8\uB2E4."],"vs/editor/contrib/gotoSymbol/symbolNavigation":["{1}\uC758 {0} \uAE30\uD638, \uB2E4\uC74C\uC758 \uACBD\uC6B0 {2}","{1}\uC758 \uAE30\uD638 {0}"],"vs/editor/contrib/hover/hover":["\uAC00\uB9AC\uD0A4\uAE30 \uD45C\uC2DC","\uC815\uC758 \uBBF8\uB9AC \uBCF4\uAE30 \uAC00\uB9AC\uD0A8 \uD56D\uBAA9 \uD45C\uC2DC"],"vs/editor/contrib/hover/markdownHoverParticipant":["\uB85C\uB4DC \uC911..."],"vs/editor/contrib/hover/markerHoverParticipant":["View Problem","\uBE60\uB978 \uC218\uC815\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC74C","\uBE60\uB978 \uC218\uC815\uC744 \uD655\uC778\uD558\uB294 \uC911...","\uBE60\uB978 \uC218\uC815\uC744 \uC0AC\uC6A9\uD560 \uC218 \uC5C6\uC74C","\uBE60\uB978 \uC218\uC815..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["\uC774\uC804 \uAC12\uC73C\uB85C \uBC14\uAFB8\uAE30","\uB2E4\uC74C \uAC12\uC73C\uB85C \uBC14\uAFB8\uAE30"],"vs/editor/contrib/indentation/indentation":["\uB4E4\uC5EC\uC4F0\uAE30\uB97C \uACF5\uBC31\uC73C\uB85C \uBCC0\uD658","\uB4E4\uC5EC\uC4F0\uAE30\uB97C \uD0ED\uC73C\uB85C \uBCC0\uD658","\uAD6C\uC131\uB41C \uD0ED \uD06C\uAE30","\uD604\uC7AC \uD30C\uC77C\uC758 \uD0ED \uD06C\uAE30 \uC120\uD0DD","\uD0ED\uC744 \uC0AC\uC6A9\uD55C \uB4E4\uC5EC\uC4F0\uAE30","\uACF5\uBC31\uC744 \uC0AC\uC6A9\uD55C \uB4E4\uC5EC\uC4F0\uAE30","\uCF58\uD150\uCE20\uC5D0\uC11C \uB4E4\uC5EC\uC4F0\uAE30 \uAC10\uC9C0","\uC904 \uB2E4\uC2DC \uB4E4\uC5EC\uC4F0\uAE30","\uC120\uD0DD\uD55C \uC904 \uB2E4\uC2DC \uB4E4\uC5EC\uC4F0\uAE30"],"vs/editor/contrib/linesOperations/linesOperations":["\uC704\uC5D0 \uC904 \uBCF5\uC0AC","\uC704\uC5D0 \uC904 \uBCF5\uC0AC(&&C)","\uC544\uB798\uC5D0 \uC904 \uBCF5\uC0AC","\uC544\uB798\uC5D0 \uC904 \uBCF5\uC0AC(&&P)","\uC911\uBCF5\uB41C \uC120\uD0DD \uC601\uC5ED","\uC911\uBCF5\uB41C \uC120\uD0DD \uC601\uC5ED(&&D)","\uC904 \uC704\uB85C \uC774\uB3D9","\uC904 \uC704\uB85C \uC774\uB3D9(&&V)","\uC904 \uC544\uB798\uB85C \uC774\uB3D9","\uC904 \uC544\uB798\uB85C \uC774\uB3D9(&&L)","\uC904\uC744 \uC624\uB984\uCC28\uC21C \uC815\uB82C","\uC904\uC744 \uB0B4\uB9BC\uCC28\uC21C\uC73C\uB85C \uC815\uB82C","\uD6C4\uD589 \uACF5\uBC31 \uC790\uB974\uAE30","\uC904 \uC0AD\uC81C","\uC904 \uB4E4\uC5EC\uC4F0\uAE30","\uC904 \uB0B4\uC5B4\uC4F0\uAE30","\uC704\uC5D0 \uC904 \uC0BD\uC785","\uC544\uB798\uC5D0 \uC904 \uC0BD\uC785","\uC67C\uCABD \uBAA8\uB450 \uC0AD\uC81C","\uC6B0\uCE21\uC5D0 \uC788\uB294 \uD56D\uBAA9 \uC0AD\uC81C","\uC904 \uC5F0\uACB0","\uCEE4\uC11C \uC8FC\uC704 \uBB38\uC790 \uBC14\uAFB8\uAE30","\uB300\uBB38\uC790\uB85C \uBCC0\uD658","\uC18C\uBB38\uC790\uB85C \uBCC0\uD658","\uB2E8\uC5B4\uC758 \uCCAB \uAE00\uC790\uB97C \uB300\uBB38\uC790\uB85C \uBCC0\uD658","\uC2A4\uB124\uC774\uD06C \uD45C\uAE30\uBC95\uC73C\uB85C \uBCC0\uD658"],"vs/editor/contrib/linkedEditing/linkedEditing":["\uC5F0\uACB0\uB41C \uD3B8\uC9D1 \uC2DC\uC791","\uD615\uC2DD\uC758 \uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC790\uB3D9\uC73C\uB85C \uC774\uB984\uC744 \uBC14\uAFC0 \uB54C\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4."],"vs/editor/contrib/links/links":["\uBA85\uB839 \uC2E4\uD589","\uB9C1\uD06C\uB85C \uC774\uB3D9","Cmd+\uD074\uB9AD","Ctrl+\uD074\uB9AD","Option+\uD074\uB9AD","Alt+\uD074\uB9AD","\uBA85\uB839 {0} \uC2E4\uD589","{0} \uD615\uC2DD\uC774 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC73C\uBBC0\uB85C \uC774 \uB9C1\uD06C\uB97C \uC5F4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4","\uB300\uC0C1\uC774 \uC5C6\uC73C\uBBC0\uB85C \uC774 \uB9C1\uD06C\uB97C \uC5F4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.","\uB9C1\uD06C \uC5F4\uAE30"],"vs/editor/contrib/message/messageController":["\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD604\uC7AC \uC778\uB77C\uC778 \uBA54\uC2DC\uC9C0\uB97C \uD45C\uC2DC\uD558\uB294\uC9C0 \uC5EC\uBD80","\uC77D\uAE30 \uC804\uC6A9 \uD3B8\uC9D1\uAE30\uC5D0\uC11C\uB294 \uD3B8\uC9D1\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."],"vs/editor/contrib/multicursor/multicursor":["\uC704\uC5D0 \uCEE4\uC11C \uCD94\uAC00","\uC704\uC5D0 \uCEE4\uC11C \uCD94\uAC00(&&A)","\uC544\uB798\uC5D0 \uCEE4\uC11C \uCD94\uAC00","\uC544\uB798\uC5D0 \uCEE4\uC11C \uCD94\uAC00(&&D)","\uC904 \uB05D\uC5D0 \uCEE4\uC11C \uCD94\uAC00","\uC904 \uB05D\uC5D0 \uCEE4\uC11C \uCD94\uAC00(&&U)","\uB9E8 \uC544\uB798\uC5D0 \uCEE4\uC11C \uCD94\uAC00","\uB9E8 \uC704\uC5D0 \uCEE4\uC11C \uCD94\uAC00","\uB2E4\uC74C \uC77C\uCE58 \uD56D\uBAA9 \uCC3E\uAE30\uC5D0 \uC120\uD0DD \uD56D\uBAA9 \uCD94\uAC00","\uB2E4\uC74C \uD56D\uBAA9 \uCD94\uAC00(&&N)","\uC774\uC804 \uC77C\uCE58 \uD56D\uBAA9 \uCC3E\uAE30\uC5D0 \uC120\uD0DD \uD56D\uBAA9 \uCD94\uAC00","\uC774\uC804 \uD56D\uBAA9 \uCD94\uAC00(&&R)","\uB2E4\uC74C \uC77C\uCE58 \uD56D\uBAA9 \uCC3E\uAE30\uB85C \uB9C8\uC9C0\uB9C9 \uC120\uD0DD \uD56D\uBAA9 \uC774\uB3D9","\uB9C8\uC9C0\uB9C9 \uC120\uD0DD \uD56D\uBAA9\uC744 \uC774\uC804 \uC77C\uCE58 \uD56D\uBAA9 \uCC3E\uAE30\uB85C \uC774\uB3D9","\uC77C\uCE58 \uD56D\uBAA9 \uCC3E\uAE30\uC758 \uBAA8\uB4E0 \uD56D\uBAA9 \uC120\uD0DD","\uBAA8\uB4E0 \uD56D\uBAA9 \uC120\uD0DD(&&O)","\uBAA8\uB4E0 \uD56D\uBAA9 \uBCC0\uACBD"],"vs/editor/contrib/parameterHints/parameterHints":["\uB9E4\uAC1C \uBCC0\uC218 \uD78C\uD2B8 \uD2B8\uB9AC\uAC70"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["\uB2E4\uC74C \uB9E4\uAC1C \uBCC0\uC218 \uD78C\uD2B8 \uD45C\uC2DC\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uC774\uC804 \uB9E4\uAC1C \uBCC0\uC218 \uD78C\uD2B8 \uD45C\uC2DC\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","{0}, \uD78C\uD2B8"],"vs/editor/contrib/peekView/peekView":["\uB2EB\uAE30","Peek \uBDF0 \uC81C\uBAA9 \uC601\uC5ED\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uC81C\uBAA9 \uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uC81C\uBAA9 \uC815\uBCF4 \uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uD14C\uB450\uB9AC \uBC0F \uD654\uC0B4\uD45C \uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uACB0\uACFC \uBAA9\uB85D\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uACB0\uACFC \uBAA9\uB85D\uC5D0\uC11C \uB77C\uC778 \uB178\uB4DC\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uACB0\uACFC \uBAA9\uB85D\uC5D0\uC11C \uD30C\uC77C \uB178\uB4DC\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uACB0\uACFC \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD\uB41C \uD56D\uBAA9\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uACB0\uACFC \uBAA9\uB85D\uC5D0\uC11C \uC120\uD0DD\uB41C \uD56D\uBAA9\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uD3B8\uC9D1\uAE30\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uD3B8\uC9D1\uAE30\uC758 \uAC70\uD130 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uACB0\uACFC \uBAA9\uB85D\uC758 \uC77C\uCE58 \uD56D\uBAA9 \uAC15\uC870 \uD45C\uC2DC \uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uD3B8\uC9D1\uAE30\uC758 \uC77C\uCE58 \uD56D\uBAA9 \uAC15\uC870 \uD45C\uC2DC \uC0C9\uC785\uB2C8\uB2E4.","Peek \uBDF0 \uD3B8\uC9D1\uAE30\uC758 \uC77C\uCE58 \uD56D\uBAA9 \uAC15\uC870 \uD45C\uC2DC \uD14C\uB450\uB9AC\uC785\uB2C8\uB2E4."],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["\uC6B0\uC120 \uD14D\uC2A4\uD2B8 \uD3B8\uC9D1\uAE30\uB97C \uC5F4\uACE0 \uC904\uB85C \uC774\uB3D9\uD569\uB2C8\uB2E4.","{0} \uC904 \uBC0F {1} \uC5F4\uB85C \uC774\uB3D9\uD569\uB2C8\uB2E4.","{0} \uC904\uB85C \uC774\uB3D9\uD569\uB2C8\uB2E4.","\uD604\uC7AC \uC904: {0}, \uBB38\uC790: {1} \uC774\uB3D9\uD560 \uC904 1~{2} \uC0AC\uC774\uC758 \uBC88\uD638\uB97C \uC785\uB825\uD569\uB2C8\uB2E4.","\uD604\uC7AC \uC904: {0}, \uBB38\uC790: {1}. \uC774\uB3D9\uD560 \uC904 \uBC88\uD638\uB97C \uC785\uB825\uD569\uB2C8\uB2E4."],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["\uAE30\uD638\uB85C \uC774\uB3D9\uD558\uB824\uBA74 \uBA3C\uC800 \uAE30\uD638 \uC815\uBCF4\uAC00 \uC788\uB294 \uD14D\uC2A4\uD2B8 \uD3B8\uC9D1\uAE30\uB97C \uC5FD\uB2C8\uB2E4.","\uD65C\uC131 \uC0C1\uD0DC\uC758 \uD14D\uC2A4\uD2B8 \uD3B8\uC9D1\uAE30\uB294 \uAE30\uD638 \uC815\uBCF4\uB97C \uC81C\uACF5\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uC77C\uCE58\uD558\uB294 \uD3B8\uC9D1\uAE30 \uAE30\uD638 \uC5C6\uC74C","\uD3B8\uC9D1\uAE30 \uAE30\uD638 \uC5C6\uC74C","\uCE21\uBA74\uC5D0\uC11C \uC5F4\uAE30","\uD558\uB2E8\uC5D0 \uC5F4\uAE30","\uAE30\uD638({0})","\uC18D\uC131({0})","\uBA54\uC11C\uB4DC({0})","\uD568\uC218({0})","\uC0DD\uC131\uC790({0})","\uBCC0\uC218({0})","\uD074\uB798\uC2A4({0})","\uAD6C\uC870\uCCB4({0})","\uC774\uBCA4\uD2B8({0})","\uC5F0\uC0B0\uC790({0})","\uC778\uD130\uD398\uC774\uC2A4({0})","\uB124\uC784\uC2A4\uD398\uC774\uC2A4({0})","\uD328\uD0A4\uC9C0({0})","\uD615\uC2DD \uB9E4\uAC1C \uBCC0\uC218({0})","\uBAA8\uB4C8({0})","\uC18D\uC131({0})","\uC5F4\uAC70\uD615({0})","\uC5F4\uAC70\uD615 \uBA64\uBC84({0})","\uBB38\uC790\uC5F4({0})","\uD30C\uC77C({0})","\uBC30\uC5F4({0})","\uC22B\uC790({0})","\uBD80\uC6B8({0})","\uAC1C\uCCB4({0})","\uD0A4({0})","\uD544\uB4DC({0})","\uC0C1\uC218({0})"],"vs/editor/contrib/rename/rename":["\uACB0\uACFC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uC704\uCE58 \uC774\uB984\uC744 \uBC14\uAFB8\uB294 \uC911 \uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.","'{0}'\uC758 \uC774\uB984\uC744 \uBC14\uAFB8\uB294 \uC911","{0} \uC774\uB984 \uBC14\uAFB8\uAE30","'{0}'\uC744(\uB97C) '{1}'(\uC73C)\uB85C \uC774\uB984\uC744 \uBCC0\uACBD\uD588\uC2B5\uB2C8\uB2E4. \uC694\uC57D: {2}","\uC774\uB984 \uBC14\uAFB8\uAE30\uB97C \uD1B5\uD574 \uD3B8\uC9D1 \uB0B4\uC6A9\uC744 \uC801\uC6A9\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.","\uC774\uB984 \uBC14\uAFB8\uAE30\uB97C \uD1B5\uD574 \uD3B8\uC9D1 \uB0B4\uC6A9\uC744 \uACC4\uC0B0\uD558\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4.","\uAE30\uD638 \uC774\uB984 \uBC14\uAFB8\uAE30","\uC774\uB984\uC744 \uBC14\uAFB8\uAE30 \uC804\uC5D0 \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uBBF8\uB9AC \uBCFC \uC218 \uC788\uB294 \uAE30\uB2A5 \uC0AC\uC6A9/\uC0AC\uC6A9 \uC548 \uD568"],"vs/editor/contrib/rename/renameInputField":["\uC785\uB825 \uC774\uB984\uC744 \uBC14\uAFB8\uC138\uC694. \uC0C8 \uC774\uB984\uC744 \uC785\uB825\uD55C \uB2E4\uC74C [Enter] \uD0A4\uB97C \uB20C\uB7EC \uCEE4\uBC0B\uD558\uC138\uC694.","\uC774\uB984 \uBC14\uAFB8\uAE30 {0}, \uBBF8\uB9AC \uBCF4\uAE30 {1}"],"vs/editor/contrib/smartSelect/smartSelect":["\uC120\uD0DD \uC601\uC5ED \uD655\uC7A5","\uC120\uD0DD \uC601\uC5ED \uD655\uC7A5(&&E)","\uC120\uD0DD \uC601\uC5ED \uCD95\uC18C","\uC120\uD0DD \uC601\uC5ED \uCD95\uC18C(&&S)"],"vs/editor/contrib/snippet/snippetVariables":["\uC77C\uC694\uC77C","\uC6D4\uC694\uC77C","\uD654\uC694\uC77C","\uC218\uC694\uC77C","\uBAA9\uC694\uC77C","\uAE08\uC694\uC77C","\uD1A0\uC694\uC77C","\uC77C","\uC6D4","\uD654","\uC218","\uBAA9","\uAE08","\uD1A0","1\uC6D4","2\uC6D4","3\uC6D4","4\uC6D4","5\uC6D4","6\uC6D4","7\uC6D4","8\uC6D4","9\uC6D4","10\uC6D4","11\uC6D4","12\uC6D4","1\uC6D4","2\uC6D4","3\uC6D4","4\uC6D4","5\uC6D4","6\uC6D4","7\uC6D4","8\uC6D4","9\uC6D4","10\uC6D4","11\uC6D4","12\uC6D4"],"vs/editor/contrib/suggest/suggestController":["{0}\uC758 {1}\uAC1C\uC758 \uC218\uC815\uC0AC\uD56D\uC744 \uC218\uB77D\uD558\uB294 \uC911","\uC81C\uC548 \uD56D\uBAA9 \uD2B8\uB9AC\uAC70","\uC0BD\uC785","\uC0BD\uC785","\uBC14\uAFB8\uAE30","\uBC14\uAFB8\uAE30","\uC0BD\uC785","\uAC04\uB2E8\uD788 \uD45C\uC2DC","\uB354 \uBCF4\uAE30","\uC81C\uC548 \uC704\uC82F \uD06C\uAE30 \uB2E4\uC2DC \uC124\uC815"],"vs/editor/contrib/suggest/suggestWidget":["\uC81C\uC548 \uC704\uC82F\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC81C\uC548 \uC704\uC82F\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uC81C\uC548 \uC704\uC82F\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC81C\uD55C \uC704\uC82F\uC5D0\uC11C \uC120\uD0DD\uB41C \uD56D\uBAA9\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC81C\uC548 \uC704\uC82F\uC758 \uC77C\uCE58 \uD56D\uBAA9 \uAC15\uC870 \uD45C\uC2DC \uC0C9\uC785\uB2C8\uB2E4.","\uB85C\uB4DC \uC911...","\uC81C\uC548 \uD56D\uBAA9\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.","{0}, \uBB38\uC11C: {1}","\uC81C\uC548"],"vs/editor/contrib/suggest/suggestWidgetDetails":["\uB2EB\uAE30","\uB85C\uB4DC \uC911..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["\uC81C\uC548 \uC704\uC82F\uC5D0\uC11C \uC790\uC138\uD55C \uC815\uBCF4\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4.","\uC790\uC138\uD55C \uC815\uBCF4"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["\uBC30\uC5F4 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uBD80\uC6B8 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uD074\uB798\uC2A4 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uC0C9 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC0C1\uC218 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uC0DD\uC131\uC790 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC5F4\uAC70\uC790 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uC5F4\uAC70\uC790 \uBA64\uBC84 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uC774\uBCA4\uD2B8 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uD544\uB4DC \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uD30C\uC77C \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uD3F4\uB354 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uD568\uC218 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uC778\uD130\uD398\uC774\uC2A4 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uD0A4 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uD0A4\uC6CC\uB4DC \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uBA54\uC11C\uB4DC \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uBAA8\uB4C8 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uB124\uC784\uC2A4\uD398\uC774\uC2A4 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","null \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uC22B\uC790 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uAC1C\uCCB4 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uC5F0\uC0B0\uC790 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uD328\uD0A4\uC9C0 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uC18D\uC131 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uCC38\uC870 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uCF54\uB4DC \uC870\uAC01 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uBB38\uC790\uC5F4 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uAD6C\uC870 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uD14D\uC2A4\uD2B8 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uB098\uD0C0\uB0A9\uB2C8\uB2E4.","\uD615\uC2DD \uB9E4\uAC1C\uBCC0\uC218 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uB2E8\uC704 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4.","\uBCC0\uC218 \uAE30\uD638\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774\uB7EC\uD55C \uAE30\uD638\uB294 \uAC1C\uC694, \uC774\uB3D9 \uACBD\uB85C \uBC0F \uC81C\uC548 \uC704\uC82F\uC5D0 \uD45C\uC2DC\uB429\uB2C8\uB2E4."],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":[" \uD0A4\uB85C \uD3EC\uCEE4\uC2A4 \uC774\uB3D9 \uC124\uC815/\uD574\uC81C","\uC774\uC81C \uD0A4\uB97C \uB204\uB974\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uB2E4\uC74C \uD3EC\uCEE4\uC2A4 \uAC00\uB2A5\uD55C \uC694\uC18C\uB85C \uC774\uB3D9\uD569\uB2C8\uB2E4.","\uC774\uC81C \uD0A4\uB97C \uB204\uB974\uBA74 \uD0ED \uBB38\uC790\uAC00 \uC0BD\uC785\uB429\uB2C8\uB2E4."],"vs/editor/contrib/tokenization/tokenization":["\uAC1C\uBC1C\uC790: \uAC15\uC81C\uB85C \uB2E4\uC2DC \uD1A0\uD070\uD654"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["\uBE44\uC815\uC0C1\uC801\uC778 \uC904 \uC885\uACB0\uC790","\uBE44\uC815\uC0C1\uC801\uC778 \uC904 \uC885\uACB0\uC790\uAC00 \uAC80\uC0C9\uB428","\uC774 \uD30C\uC77C\uC5D0 LS(\uC904 \uAD6C\uBD84 \uAE30\uD638) \uB610\uB294 PS(\uB2E8\uB77D \uAD6C\uBD84 \uAE30\uD638) \uAC19\uC740 \uD558\uB098 \uC774\uC0C1\uC758 \uBE44\uC815\uC0C1\uC801\uC778 \uC904 \uC885\uACB0\uC790 \uBB38\uC790\uAC00 \uD3EC\uD568\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4.\r\n\r\n\uD30C\uC77C\uC5D0\uC11C \uC81C\uAC70\uD558\uB294 \uAC83\uC774 \uC88B\uC2B5\uB2C8\uB2E4. `editor.unusualLineTerminators`\uB97C \uD1B5\uD574 \uAD6C\uC131\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uC774 \uD30C\uC77C \uC218\uC815","\uC774 \uD30C\uC77C\uC758 \uBB38\uC81C \uBB34\uC2DC"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["\uBCC0\uC218 \uC77D\uAE30\uC640 \uAC19\uC740 \uC77D\uAE30 \uC561\uC138\uC2A4 \uC911 \uAE30\uD638\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uBCC0\uC218\uC5D0 \uC4F0\uAE30\uC640 \uAC19\uC740 \uC4F0\uAE30 \uC561\uC138\uC2A4 \uC911 \uAE30\uD638\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uBCC0\uC218 \uC77D\uAE30\uC640 \uAC19\uC740 \uC77D\uAE30 \uC561\uC138\uC2A4 \uC911 \uAE30\uD638\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uBCC0\uC218\uC5D0 \uC4F0\uAE30\uC640 \uAC19\uC740 \uC4F0\uAE30 \uC561\uC138\uC2A4 \uC911 \uAE30\uD638\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uAE30\uD638 \uAC15\uC870 \uD45C\uC2DC\uC758 \uAC1C\uC694 \uB208\uAE08\uC790 \uD45C\uC2DD \uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uC4F0\uAE30 \uC561\uC138\uC2A4 \uAE30\uD638\uC5D0 \uB300\uD55C \uAC1C\uC694 \uB208\uAE08\uC790 \uD45C\uC2DD \uC0C9\uC774 \uAC15\uC870 \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uB2E4\uC74C \uAC15\uC870 \uAE30\uD638\uB85C \uC774\uB3D9","\uC774\uC804 \uAC15\uC870 \uAE30\uD638\uB85C \uC774\uB3D9","\uAE30\uD638 \uAC15\uC870 \uD45C\uC2DC \uD2B8\uB9AC\uAC70"],"vs/editor/contrib/wordOperations/wordOperations":["\uB2E8\uC5B4 \uC0AD\uC81C"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0}({1})"],"vs/platform/configuration/common/configurationRegistry":["\uAE30\uBCF8 \uC5B8\uC5B4 \uAD6C\uC131 \uC7AC\uC815\uC758","\uC5B8\uC5B4\uC5D0 \uB300\uD574 \uC7AC\uC815\uC758\uD560 \uD3B8\uC9D1\uAE30 \uC124\uC815\uC744 \uAD6C\uC131\uD569\uB2C8\uB2E4.","\uC774 \uC124\uC815\uC740 \uC5B8\uC5B4\uBCC4 \uAD6C\uC131\uC744 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4.","\uBE48 \uC18D\uC131\uC744 \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC74C","'{0}'\uC744(\uB97C) \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774\uB294 \uC5B8\uC5B4\uBCC4 \uD3B8\uC9D1\uAE30 \uC124\uC815\uC744 \uC124\uBA85\uD558\uB294 \uC18D\uC131 \uD328\uD134\uC778 '\\\\[.*\\\\]$'\uACFC(\uC640) \uC77C\uCE58\uD569\uB2C8\uB2E4. 'configurationDefaults' \uAE30\uC5EC\uB97C \uC0AC\uC6A9\uD558\uC138\uC694.","'{0}'\uC744(\uB97C) \uB4F1\uB85D\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC774 \uC18D\uC131\uC740 \uC774\uBBF8 \uB4F1\uB85D\uB418\uC5B4 \uC788\uC2B5\uB2C8\uB2E4."],"vs/platform/contextkey/browser/contextKeyService":["\uCEE8\uD14D\uC2A4\uD2B8 \uD0A4\uC5D0 \uB300\uD55C \uC815\uBCF4\uB97C \uBC18\uD658\uD558\uB294 \uBA85\uB839"],"vs/platform/contextkey/common/contextkeys":["Whether the operating system is Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["({0})\uC744(\uB97C) \uB20C\uB800\uC2B5\uB2C8\uB2E4. \uB458\uC9F8 \uD0A4\uB294 \uC7A0\uC2DC \uAE30\uB2E4\uB838\uB2E4\uAC00 \uB204\uB974\uC2ED\uC2DC\uC624...","\uD0A4 \uC870\uD569({0}, {1})\uC740 \uBA85\uB839\uC774 \uC544\uB2D9\uB2C8\uB2E4."],"vs/platform/list/browser/listService":["\uC6CC\uD06C\uBCA4\uCE58","Windows\uC640 Linux\uC758 'Control'\uC744 macOS\uC758 'Command'\uB85C \uB9E4\uD551\uD569\uB2C8\uB2E4.","Windows\uC640 Linux\uC758 'Alt'\uB97C macOS\uC758 'Option'\uC73C\uB85C \uB9E4\uD551\uD569\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB85C \uD2B8\uB9AC\uC640 \uBAA9\uB85D\uC758 \uD56D\uBAA9\uC744 \uB2E4\uC911 \uC120\uD0DD\uC5D0 \uCD94\uAC00\uD560 \uB54C \uC0AC\uC6A9\uD560 \uD55C\uC815\uC790\uC785\uB2C8\uB2E4(\uC608\uB97C \uB4E4\uC5B4 \uD0D0\uC0C9\uAE30\uC5D0\uC11C \uD3B8\uC9D1\uAE30\uC640 SCM \uBCF4\uAE30\uB97C \uC5EC\uB294 \uACBD\uC6B0). '\uC606\uC5D0\uC11C \uC5F4\uAE30' \uB9C8\uC6B0\uC2A4 \uC81C\uC2A4\uCC98(\uC9C0\uC6D0\uB418\uB294 \uACBD\uC6B0)\uB294 \uB2E4\uC911 \uC120\uD0DD \uD55C\uC815\uC790\uC640 \uCDA9\uB3CC\uD558\uC9C0 \uC54A\uB3C4\uB85D \uC870\uC815\uB429\uB2C8\uB2E4.","\uD2B8\uB9AC\uC640 \uBAA9\uB85D\uC5D0\uC11C \uB9C8\uC6B0\uC2A4\uB97C \uC0AC\uC6A9\uD558\uC5EC \uD56D\uBAA9\uC744 \uC5EC\uB294 \uBC29\uBC95\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4(\uC9C0\uC6D0\uB418\uB294 \uACBD\uC6B0). \uC77C\uBD80 \uD2B8\uB9AC\uC640 \uBAA9\uB85D\uC5D0\uC11C\uB294 \uC774 \uC124\uC815\uC744 \uC801\uC6A9\uD560 \uC218 \uC5C6\uB294 \uACBD\uC6B0 \uBB34\uC2DC\uD558\uB3C4\uB85D \uC120\uD0DD\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uC6CC\uD06C\uBCA4\uCE58\uC5D0\uC11C \uBAA9\uB85D \uBC0F \uD2B8\uB9AC\uC758 \uAC00\uB85C \uC2A4\uD06C\uB864 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uACBD\uACE0: \uC774 \uC124\uC815\uC744 \uCF1C\uBA74 \uC131\uB2A5\uC5D0 \uC601\uD5A5\uC744 \uBBF8\uCE69\uB2C8\uB2E4.","\uD2B8\uB9AC \uB4E4\uC5EC\uC4F0\uAE30\uB97C \uD53D\uC140 \uB2E8\uC704\uB85C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uD2B8\uB9AC\uC5D0\uC11C \uB4E4\uC5EC\uC4F0\uAE30 \uAC00\uC774\uB4DC\uB97C \uB80C\uB354\uB9C1\uD560\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uBAA9\uB85D\uACFC \uD2B8\uB9AC\uC5D0 \uBD80\uB4DC\uB7EC\uC6B4 \uD654\uBA74 \uC774\uB3D9 \uAE30\uB2A5\uC774 \uC788\uB294\uC9C0\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4.","\uAC04\uB2E8\uD55C \uD0A4\uBCF4\uB4DC \uD0D0\uC0C9\uC5D0\uC11C\uB294 \uD0A4\uBCF4\uB4DC \uC785\uB825\uACFC \uC77C\uCE58\uD558\uB294 \uC694\uC18C\uC5D0 \uC9D1\uC911\uD569\uB2C8\uB2E4. \uC77C\uCE58\uB294 \uC811\uB450\uC0AC\uC5D0\uC11C\uB9CC \uC218\uD589\uB429\uB2C8\uB2E4.","\uD0A4\uBCF4\uB4DC \uD0D0\uC0C9 \uAC15\uC870 \uD45C\uC2DC\uC5D0\uC11C\uB294 \uD0A4\uBCF4\uB4DC \uC785\uB825\uACFC \uC77C\uCE58\uD558\uB294 \uC694\uC18C\uB97C \uAC15\uC870 \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uC774\uD6C4\uB85C \uD0D0\uC0C9\uC5D0\uC11C \uC704 \uBC0F \uC544\uB798\uB85C \uC774\uB3D9\uD558\uB294 \uACBD\uC6B0 \uAC15\uC870 \uD45C\uC2DC\uB41C \uC694\uC18C\uB9CC \uD2B8\uB798\uBC84\uC2A4\uD569\uB2C8\uB2E4.","\uD0A4\uBCF4\uB4DC \uD0D0\uC0C9 \uD544\uD130\uB9C1\uC5D0\uC11C\uB294 \uD0A4\uBCF4\uB4DC \uC785\uB825\uACFC \uC77C\uCE58\uD558\uC9C0 \uC54A\uB294 \uC694\uC18C\uB97C \uBAA8\uB450 \uD544\uD130\uB9C1\uD558\uC5EC \uC228\uAE41\uB2C8\uB2E4.","\uC6CC\uD06C\uBCA4\uCE58\uC758 \uBAA9\uB85D \uBC0F \uD2B8\uB9AC \uD0A4\uBCF4\uB4DC \uD0D0\uC0C9 \uC2A4\uD0C0\uC77C\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uAC04\uC18C\uD654\uD558\uACE0, \uAC15\uC870 \uD45C\uC2DC\uD558\uACE0, \uD544\uD130\uB9C1\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D \uBC0F \uD2B8\uB9AC\uC5D0\uC11C \uD0A4\uBCF4\uB4DC \uD0D0\uC0C9\uC774 \uC785\uB825\uB9CC\uC73C\uB85C \uC790\uB3D9 \uD2B8\uB9AC\uAC70\uB418\uB294\uC9C0 \uC5EC\uBD80\uB97C \uC81C\uC5B4\uD569\uB2C8\uB2E4. 'false'\uB85C \uC124\uC815\uD558\uBA74 'list.toggleKeyboardNavigation' \uBA85\uB839\uC744 \uC2E4\uD589\uD560 \uB54C\uB9CC \uD0A4\uBCF4\uB4DC \uD0D0\uC0C9\uC774 \uD2B8\uB9AC\uAC70\uB418\uC5B4 \uBC14\uB85C \uAC00\uAE30 \uD0A4\uB97C \uD560\uB2F9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.","\uD3F4\uB354 \uC774\uB984\uC744 \uD074\uB9AD\uD560 \uB54C \uD2B8\uB9AC \uD3F4\uB354\uAC00 \uD655\uC7A5\uB418\uB294 \uBC29\uBC95\uC744 \uC81C\uC5B4\uD569\uB2C8\uB2E4. \uC77C\uBD80 \uD2B8\uB9AC\uC640 \uBAA9\uB85D\uC5D0\uC11C\uB294 \uC774 \uC124\uC815\uC744 \uC801\uC6A9\uD560 \uC218 \uC5C6\uB294 \uACBD\uC6B0 \uBB34\uC2DC\uD558\uB3C4\uB85D \uC120\uD0DD\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."],"vs/platform/markers/common/markers":["\uC624\uB958","\uACBD\uACE0","\uC815\uBCF4"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","\uCD5C\uADFC\uC5D0 \uC0AC\uC6A9\uD55C \uD56D\uBAA9","\uAE30\uD0C0 \uBA85\uB839","\uBA85\uB839 '{0}'\uC5D0\uC11C \uC624\uB958({1})\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4."],"vs/platform/quickinput/browser/helpQuickAccess":["\uC804\uC5ED \uBA85\uB839","\uD3B8\uC9D1\uAE30 \uBA85\uB839","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["\uC804\uCCB4 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774 \uC0C9\uC740 \uAD6C\uC131 \uC694\uC18C\uC5D0\uC11C \uC7AC\uC815\uC758\uD558\uC9C0 \uC54A\uC740 \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uC624\uB958 \uBA54\uC2DC\uC9C0\uC5D0 \uB300\uD55C \uC804\uCCB4 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uC774 \uC0C9\uC740 \uAD6C\uC131 \uC694\uC18C\uC5D0\uC11C \uC7AC\uC815\uC758\uD558\uC9C0 \uC54A\uC740 \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uC6CC\uD06C\uBCA4\uCE58 \uC544\uC774\uCF58\uC758 \uAE30\uBCF8 \uC0C9\uC0C1\uC785\uB2C8\uB2E4.","\uD3EC\uCEE4\uC2A4\uAC00 \uC788\uB294 \uC694\uC18C\uC758 \uC804\uCCB4 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4. \uC774 \uC0C9\uC740 \uAD6C\uC131 \uC694\uC18C\uC5D0\uC11C \uC7AC\uC815\uC758\uD558\uC9C0 \uC54A\uC740 \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uB354 \uB69C\uB837\uC774 \uB300\uBE44\uB418\uB3C4\uB85D \uC694\uC18C\uB97C \uB2E4\uB978 \uC694\uC18C\uC640 \uAD6C\uBD84\uD558\uB294 \uC694\uC18C \uC8FC\uC704\uC758 \uCD94\uAC00 \uD14C\uB450\uB9AC\uC785\uB2C8\uB2E4.","\uB354 \uB69C\uB837\uC774 \uB300\uBE44\uB418\uB3C4\uB85D \uC694\uC18C\uB97C \uB2E4\uB978 \uC694\uC18C\uC640 \uAD6C\uBD84\uD558\uB294 \uD65C\uC131 \uC694\uC18C \uC8FC\uC704\uC758 \uCD94\uAC00 \uD14C\uB450\uB9AC\uC785\uB2C8\uB2E4.","\uD14D\uC2A4\uD2B8 \uB0B4 \uB9C1\uD06C\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD14D\uC2A4\uD2B8 \uB0B4 \uCF54\uB4DC \uBE14\uB85D\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uB0B4\uC5D0\uC11C \uCC3E\uAE30/\uBC14\uAFB8\uAE30 \uAC19\uC740 \uC704\uC82F\uC758 \uADF8\uB9BC\uC790 \uC0C9\uC785\uB2C8\uB2E4.","\uC785\uB825 \uC0C1\uC790 \uBC30\uACBD\uC785\uB2C8\uB2E4.","\uC785\uB825 \uC0C1\uC790 \uC804\uACBD\uC785\uB2C8\uB2E4.","\uC785\uB825 \uC0C1\uC790 \uD14C\uB450\uB9AC\uC785\uB2C8\uB2E4.","\uC785\uB825 \uD544\uB4DC\uC5D0\uC11C \uD65C\uC131\uD654\uB41C \uC635\uC158\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uC785\uB825 \uD544\uB4DC\uC5D0\uC11C \uD65C\uC131\uD654\uB41C \uC635\uC158\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC785\uB825 \uD544\uB4DC\uC5D0\uC11C \uD65C\uC131\uD654\uB41C \uC635\uC158\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC815\uBCF4 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC815\uBCF4 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC815\uBCF4 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uACBD\uACE0 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uACBD\uACE0 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uACBD\uACE0 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uC624\uB958 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC624\uB958 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC624\uB958 \uC2EC\uAC01\uB3C4\uC758 \uC785\uB825 \uC720\uD6A8\uC131 \uAC80\uC0AC \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uB4DC\uB86D\uB2E4\uC6B4 \uBC30\uACBD\uC785\uB2C8\uB2E4.","\uB4DC\uB86D\uB2E4\uC6B4 \uC804\uACBD\uC785\uB2C8\uB2E4.","\uB2E8\uCD94 \uAE30\uBCF8 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uB2E8\uCD94 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB85C \uAC00\uB9AC\uD0AC \uB54C \uB2E8\uCD94 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBC30\uC9C0 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBC30\uC9C0\uB294 \uAC80\uC0C9 \uACB0\uACFC \uC218\uC640 \uAC19\uC740 \uC18C\uB7C9\uC758 \uC815\uBCF4 \uB808\uC774\uBE14\uC785\uB2C8\uB2E4.","\uBC30\uC9C0 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBC30\uC9C0\uB294 \uAC80\uC0C9 \uACB0\uACFC \uC218\uC640 \uAC19\uC740 \uC18C\uB7C9\uC758 \uC815\uBCF4 \uB808\uC774\uBE14\uC785\uB2C8\uB2E4.","\uC2A4\uD06C\uB864\uB418\uB294 \uBCF4\uAE30\uB97C \uB098\uD0C0\uB0B4\uB294 \uC2A4\uD06C\uB864 \uB9C9\uB300 \uADF8\uB9BC\uC790\uC785\uB2C8\uB2E4.","\uC2A4\uD06C\uB864 \uB9C9\uB300 \uC2AC\uB77C\uC774\uBC84 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB85C \uAC00\uB9AC\uD0AC \uB54C \uC2A4\uD06C\uB864 \uB9C9\uB300 \uC2AC\uB77C\uC774\uB354 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD074\uB9AD\uB41C \uC0C1\uD0DC\uC77C \uB54C \uC2A4\uD06C\uB864 \uB9C9\uB300 \uC2AC\uB77C\uC774\uB354 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC7A5\uAE30 \uC791\uC5C5\uC744 \uB300\uC0C1\uC73C\uB85C \uD45C\uC2DC\uB420 \uC218 \uC788\uB294 \uC9C4\uD589\uB960 \uD45C\uC2DC\uC904\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC624\uB958 \uD14D\uC2A4\uD2B8\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uB0B4 \uC624\uB958 \uD45C\uC2DC\uC120\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC624\uB958 \uC0C1\uC790\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uACBD\uACE0 \uD14D\uC2A4\uD2B8\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uB0B4 \uACBD\uACE0 \uD45C\uC2DC\uC120\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uACBD\uACE0 \uC0C1\uC790\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC815\uBCF4 \uD14D\uC2A4\uD2B8\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uB0B4 \uC815\uBCF4 \uD45C\uC2DC\uC120\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uC815\uBCF4 \uC0C1\uC790\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD78C\uD2B8 \uD45C\uC2DC\uC120\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30\uC5D0\uC11C \uD78C\uD2B8 \uC0C1\uC790\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uAE30\uBCF8 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uCC3E\uAE30/\uBC14\uAFB8\uAE30 \uAC19\uC740 \uD3B8\uC9D1\uAE30 \uC704\uC82F\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uCC3E\uAE30/\uBC14\uAFB8\uAE30\uC640 \uAC19\uC740 \uD3B8\uC9D1\uAE30 \uC704\uC82F\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uC704\uC82F\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4. \uC704\uC82F\uC5D0 \uD14C\uB450\uB9AC\uAC00 \uC788\uACE0 \uC704\uC82F\uC774 \uC0C9\uC0C1\uC744 \uBB34\uC2DC\uD558\uC9C0 \uC54A\uC744 \uB54C\uB9CC \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uC704\uC82F \uD06C\uAE30 \uC870\uC815 \uB9C9\uB300\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4. \uC774 \uC0C9\uC740 \uC704\uC82F\uC5D0\uC11C \uD06C\uAE30 \uC870\uC815 \uB9C9\uB300\uB97C \uD45C\uC2DC\uD558\uB3C4\uB85D \uC120\uD0DD\uD558\uACE0 \uC704\uC82F\uC5D0\uC11C \uC0C9\uC744 \uC7AC\uC9C0\uC815\uD558\uC9C0 \uC54A\uB294 \uACBD\uC6B0\uC5D0\uB9CC \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uBE60\uB978 \uC120\uD0DD\uAE30 \uBC30\uACBD\uC0C9. \uBE60\uB978 \uC120\uD0DD\uAE30 \uC704\uC82F\uC740 \uBA85\uB839 \uD314\uB808\uD2B8\uC640 \uAC19\uC740 \uC120\uD0DD\uAE30\uB97C \uC704\uD55C \uCEE8\uD14C\uC774\uB108\uC785\uB2C8\uB2E4.","\uBE60\uB978 \uC120\uD0DD\uAE30 \uC804\uACBD\uC0C9. \uC774 \uBE60\uB978 \uC120\uD0DD\uAE30 \uC704\uC82F\uC740 \uBA85\uB839 \uD314\uB808\uD2B8\uC640 \uAC19\uC740 \uC120\uD0DD\uAE30\uB97C \uC704\uD55C \uCEE8\uD14C\uC774\uB108\uC785\uB2C8\uB2E4.","\uBE60\uB978 \uC120\uD0DD\uAE30 \uC81C\uBAA9 \uBC30\uACBD\uC0C9. \uC774 \uBE60\uB978 \uC120\uD0DD\uAE30 \uC704\uC82F\uC740 \uBA85\uB839 \uD314\uB808\uD2B8\uC640 \uAC19\uC740 \uC120\uD0DD\uAE30\uB97C \uC704\uD55C \uCEE8\uD14C\uC774\uB108\uC785\uB2C8\uB2E4.","\uD3EC\uCEE4\uC2A4\uAC00 \uC788\uB294 \uD56D\uBAA9\uC758 \uBE60\uB978 \uC120\uD0DD\uAE30 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uADF8\uB8F9\uD654 \uB808\uC774\uBE14\uC5D0 \uB300\uD55C \uBE60\uB978 \uC120\uD0DD\uAE30 \uC0C9\uC785\uB2C8\uB2E4.","\uADF8\uB8F9\uD654 \uD14C\uB450\uB9AC\uC5D0 \uB300\uD55C \uBE60\uB978 \uC120\uD0DD\uAE30 \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uC120\uD0DD \uC601\uC5ED\uC758 \uC0C9\uC785\uB2C8\uB2E4.","\uACE0\uB300\uBE44\uB97C \uC704\uD55C \uC120\uD0DD \uD14D\uC2A4\uD2B8\uC758 \uC0C9\uC785\uB2C8\uB2E4.","\uBE44\uD65C\uC131 \uD3B8\uC9D1\uAE30\uC758 \uC120\uD0DD \uD56D\uBAA9 \uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uC120\uD0DD \uC601\uC5ED\uACFC \uB3D9\uC77C\uD55C \uCF58\uD150\uCE20\uAC00 \uC788\uB294 \uC601\uC5ED\uC758 \uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uC120\uD0DD \uC601\uC5ED\uACFC \uB3D9\uC77C\uD55C \uCF58\uD150\uCE20\uAC00 \uC788\uB294 \uC601\uC5ED\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uD604\uC7AC \uAC80\uC0C9 \uC77C\uCE58 \uD56D\uBAA9\uC758 \uC0C9\uC785\uB2C8\uB2E4.","\uAE30\uD0C0 \uAC80\uC0C9 \uC77C\uCE58 \uD56D\uBAA9\uC758 \uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uAC80\uC0C9\uC744 \uC81C\uD55C\uD558\uB294 \uBC94\uC704\uC758 \uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uD604\uC7AC \uAC80\uC0C9\uACFC \uC77C\uCE58\uD558\uB294 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uB2E4\uB978 \uAC80\uC0C9\uACFC \uC77C\uCE58\uD558\uB294 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uAC80\uC0C9\uC744 \uC81C\uD55C\uD558\uB294 \uBC94\uC704\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uD638\uBC84\uAC00 \uD45C\uC2DC\uB41C \uB2E8\uC5B4 \uC544\uB798\uB97C \uAC15\uC870 \uD45C\uC2DC\uD569\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uD638\uBC84\uC758 \uBC30\uACBD\uC0C9.","\uD3B8\uC9D1\uAE30 \uD638\uBC84\uC758 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uD638\uBC84\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uD638\uBC84 \uC0C1\uD0DC \uD45C\uC2DC\uC904\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD65C\uC131 \uB9C1\uD06C\uC758 \uC0C9\uC785\uB2C8\uB2E4.","\uC778\uB77C\uC778 \uD78C\uD2B8\uC758 \uC804\uACBD\uC0C9","\uC778\uB77C\uC778 \uD78C\uD2B8\uC758 \uBC30\uACBD\uC0C9","\uC804\uAD6C \uC791\uC5C5 \uC544\uC774\uCF58\uC5D0 \uC0AC\uC6A9\uB418\uB294 \uC0C9\uC0C1\uC785\uB2C8\uB2E4.","\uC804\uAD6C \uC790\uB3D9 \uC218\uC815 \uC791\uC5C5 \uC544\uC774\uCF58\uC5D0 \uC0AC\uC6A9\uB418\uB294 \uC0C9\uC0C1\uC785\uB2C8\uB2E4.","\uC0BD\uC785\uB41C \uD14D\uC2A4\uD2B8\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uC81C\uAC70\uB41C \uD14D\uC2A4\uD2B8 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uC0BD\uC785\uB41C \uD14D\uC2A4\uD2B8\uC758 \uC724\uACFD\uC120 \uC0C9\uC785\uB2C8\uB2E4.","\uC81C\uAC70\uB41C \uD14D\uC2A4\uD2B8\uC758 \uC724\uACFD\uC120 \uC0C9\uC785\uB2C8\uB2E4.","\uB450 \uD14D\uC2A4\uD2B8 \uD3B8\uC9D1\uAE30 \uC0AC\uC774\uC758 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","diff \uD3B8\uC9D1\uAE30\uC758 \uB300\uAC01\uC120 \uCC44\uC6B0\uAE30 \uC0C9\uC785\uB2C8\uB2E4. \uB300\uAC01\uC120 \uCC44\uC6B0\uAE30\uB294 diff \uB098\uB780\uD788 \uBCF4\uAE30\uC5D0\uC11C \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uD3EC\uCEE4\uC2A4\uAC00 \uC788\uB294 \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uD3EC\uCEE4\uC2A4\uAC00 \uC788\uB294 \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uD3EC\uCEE4\uC2A4\uAC00 \uC788\uB294 \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uC724\uACFD\uC120 \uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uC120\uD0DD\uD55C \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uC120\uD0DD\uD55C \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uC120\uD0DD\uD55C \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uC120\uD0DD\uD55C \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uD3EC\uCEE4\uC2A4\uAC00 \uC788\uB294 \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC778 \uACBD\uC6B0 \uD3EC\uCEE4\uC2A4\uAC00 \uC788\uB294 \uD56D\uBAA9\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uC724\uACFD\uC120 \uC0C9\uC785\uB2C8\uB2E4. \uBAA9\uB85D/\uD2B8\uB9AC\uAC00 \uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD0A4\uBCF4\uB4DC \uD3EC\uCEE4\uC2A4\uB97C \uAC00\uC9C0\uBA70, \uBE44\uD65C\uC131 \uC0C1\uD0DC\uC774\uBA74 \uD3EC\uCEE4\uC2A4\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB85C \uD56D\uBAA9\uC744 \uAC00\uB9AC\uD0AC \uB54C \uBAA9\uB85D/\uD2B8\uB9AC \uBC30\uACBD\uC785\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB85C \uD56D\uBAA9\uC744 \uAC00\uB9AC\uD0AC \uB54C \uBAA9\uB85D/\uD2B8\uB9AC \uC804\uACBD\uC785\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB85C \uD56D\uBAA9\uC744 \uC774\uB3D9\uD560 \uB54C \uBAA9\uB85D/\uD2B8\uB9AC \uB04C\uC5B4\uC11C \uB193\uAE30 \uBC30\uACBD\uC785\uB2C8\uB2E4.","\uBAA9\uB85D/\uD2B8\uB9AC \uB0B4\uC5D0\uC11C \uAC80\uC0C9\uD560 \uB54C \uC77C\uCE58 \uD56D\uBAA9 \uAC15\uC870 \uD45C\uC2DC\uC758 \uBAA9\uB85D/\uD2B8\uB9AC \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBAA9\uB85D \uBC0F \uD2B8\uB9AC\uC5D0\uC11C \uD615\uC2DD \uD544\uD130 \uC704\uC82F\uC758 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBAA9\uB85D \uBC0F \uD2B8\uB9AC\uC5D0\uC11C \uD615\uC2DD \uD544\uD130 \uC704\uC82F\uC758 \uC724\uACFD\uC120 \uC0C9\uC785\uB2C8\uB2E4.","\uC77C\uCE58\uD558\uB294 \uD56D\uBAA9\uC774 \uC5C6\uC744 \uB54C \uBAA9\uB85D \uBC0F \uD2B8\uB9AC\uC5D0\uC11C \uD45C\uC2DC\uB418\uB294 \uD615\uC2DD \uD544\uD130 \uC704\uC82F\uC758 \uC724\uACFD\uC120 \uC0C9\uC785\uB2C8\uB2E4.","\uB4E4\uC5EC\uC4F0\uAE30 \uAC00\uC774\uB4DC\uC758 \uD2B8\uB9AC \uC2A4\uD2B8\uB85C\uD06C \uC0C9\uC785\uB2C8\uB2E4.","\uB4E4\uC5EC\uC4F0\uAE30 \uAC00\uC774\uB4DC\uC758 \uD2B8\uB9AC \uC2A4\uD2B8\uB85C\uD06C \uC0C9\uC785\uB2C8\uB2E4.","\uBA54\uB274 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uBA54\uB274 \uD56D\uBAA9 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBA54\uB274 \uD56D\uBAA9 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBA54\uB274\uC758 \uC120\uD0DD\uB41C \uBA54\uB274 \uD56D\uBAA9 \uC804\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBA54\uB274\uC758 \uC120\uD0DD\uB41C \uBA54\uB274 \uD56D\uBAA9 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBA54\uB274\uC758 \uC120\uD0DD\uB41C \uBA54\uB274 \uD56D\uBAA9 \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uBA54\uB274\uC5D0\uC11C \uAD6C\uBD84 \uAE30\uD638 \uBA54\uB274 \uD56D\uBAA9\uC758 \uC0C9\uC785\uB2C8\uB2E4.","\uCF54\uB4DC \uC870\uAC01 \uD0ED \uC815\uC9C0\uC758 \uAC15\uC870 \uD45C\uC2DC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uCF54\uB4DC \uC870\uAC01 \uD0ED \uC815\uC9C0\uC758 \uAC15\uC870 \uD45C\uC2DC \uD14C\uB450\uB9AC \uC0C9\uC785\uB2C8\uB2E4.","\uCF54\uB4DC \uC870\uAC01 \uB9C8\uC9C0\uB9C9 \uD0ED \uC815\uC9C0\uC758 \uAC15\uC870 \uD45C\uC2DC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uCF54\uB4DC \uC870\uAC01 \uB9C8\uC9C0\uB9C9 \uD0ED \uC815\uC9C0\uC758 \uAC15\uC870 \uD45C\uC2DC \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uC77C\uCE58 \uD56D\uBAA9 \uCC3E\uAE30\uC758 \uAC1C\uC694 \uB208\uAE08\uC790 \uD45C\uC2DD \uC0C9\uC785\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uC120\uD0DD \uD56D\uBAA9\uC758 \uAC1C\uC694 \uB208\uAE08\uC790 \uD45C\uC2DD \uC0C9\uC774 \uAC15\uC870 \uD45C\uC2DC\uB429\uB2C8\uB2E4. \uAE30\uBCF8 \uC7A5\uC2DD\uC744 \uC228\uAE30\uC9C0 \uC54A\uB3C4\uB85D \uC0C9\uC740 \uBD88\uD22C\uBA85\uD558\uC9C0 \uC54A\uC544\uC57C \uD569\uB2C8\uB2E4.","\uC77C\uCE58\uD558\uB294 \uD56D\uBAA9\uC744 \uCC3E\uAE30 \uC704\uD55C \uBBF8\uB2C8\uB9F5 \uD45C\uC2DD \uC0C9\uC785\uB2C8\uB2E4.","\uD3B8\uC9D1\uAE30 \uC120\uD0DD \uC791\uC5C5\uC744 \uC704\uD55C \uBBF8\uB2C8\uB9F5 \uB9C8\uCEE4 \uC0C9\uC785\uB2C8\uB2E4.","\uC624\uB958\uC5D0 \uB300\uD55C \uBBF8\uB2C8\uB9F5 \uB9C8\uCEE4 \uC0C9\uC0C1\uC785\uB2C8\uB2E4.","\uACBD\uACE0\uC758 \uBBF8\uB2C8\uB9F5 \uB9C8\uCEE4 \uC0C9\uC0C1\uC785\uB2C8\uB2E4.","\uBBF8\uB2C8\uB9F5 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBBF8\uB2C8\uB9F5 \uC2AC\uB77C\uC774\uB354 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uB9C8\uC6B0\uC2A4\uB85C \uAC00\uB9AC\uD0AC \uB54C \uBBF8\uB2C8\uB9F5 \uC2AC\uB77C\uC774\uB354 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uD074\uB9AD\uD588\uC744 \uB54C \uBBF8\uB2C8\uB9F5 \uC2AC\uB77C\uC774\uB354 \uBC30\uACBD\uC0C9\uC785\uB2C8\uB2E4.","\uBB38\uC81C \uC624\uB958 \uC544\uC774\uCF58\uC5D0 \uC0AC\uC6A9\uB418\uB294 \uC0C9\uC785\uB2C8\uB2E4.","\uBB38\uC81C \uACBD\uACE0 \uC544\uC774\uCF58\uC5D0 \uC0AC\uC6A9\uB418\uB294 \uC0C9\uC785\uB2C8\uB2E4.","\uBB38\uC81C \uC815\uBCF4 \uC544\uC774\uCF58\uC5D0 \uC0AC\uC6A9\uB418\uB294 \uC0C9\uC785\uB2C8\uB2E4."],"vs/platform/theme/common/iconRegistry":["\uC0AC\uC6A9\uD560 \uAE00\uAF34\uC758 ID\uC785\uB2C8\uB2E4. \uC124\uC815\uD558\uC9C0 \uC54A\uC73C\uBA74 \uCCAB \uBC88\uC9F8\uB85C \uC815\uC758\uD55C \uAE00\uAF34\uC774 \uC0AC\uC6A9\uB429\uB2C8\uB2E4.","\uC544\uC774\uCF58 \uC815\uC758\uC640 \uC5F0\uACB0\uB41C \uAE00\uAF34 \uBB38\uC790\uC785\uB2C8\uB2E4.","\uC704\uC82F\uC5D0\uC11C \uB2EB\uAE30 \uC791\uC5C5\uC758 \uC544\uC774\uCF58\uC785\uB2C8\uB2E4."],"vs/platform/undoRedo/common/undoRedoService":["{0} \uD30C\uC77C\uC774 \uB2EB\uD788\uACE0 \uB514\uC2A4\uD06C\uC5D0\uC11C \uC218\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4.","{0} \uD30C\uC77C\uC740 \uD638\uD658\uB418\uC9C0 \uC54A\uB294 \uBC29\uC2DD\uC73C\uB85C \uC218\uC815\uB418\uC5C8\uC2B5\uB2C8\uB2E4.","\uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uC2E4\uD589 \uCDE8\uC18C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. {1}","\uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uC2E4\uD589 \uCDE8\uC18C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. {1}","{1}\uC5D0 \uBCC0\uACBD \uB0B4\uC6A9\uC774 \uC801\uC6A9\uB418\uC5C8\uC73C\uBBC0\uB85C \uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uC2E4\uD589 \uCDE8\uC18C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","{1}\uC5D0\uC11C \uC2E4\uD589 \uCDE8\uC18C \uB610\uB294 \uB2E4\uC2DC \uC2E4\uD589 \uC791\uC5C5\uC774 \uC774\uBBF8 \uC2E4\uD589 \uC911\uC774\uBBC0\uB85C \uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uC2E4\uD589 \uCDE8\uC18C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uADF8\uB3D9\uC548 \uC2E4\uD589 \uCDE8\uC18C \uB610\uB294 \uB2E4\uC2DC \uC2E4\uD589 \uC791\uC5C5\uC774 \uBC1C\uC0DD\uD588\uAE30 \uB54C\uBB38\uC5D0 \uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uC2E4\uD589 \uCDE8\uC18C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uC2E4\uD589 \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?","{0}\uAC1C \uD30C\uC77C\uC5D0\uC11C \uC2E4\uD589 \uCDE8\uC18C","\uC774 \uD30C\uC77C \uC2E4\uD589 \uCDE8\uC18C","\uCDE8\uC18C","\uC2E4\uD589 \uCDE8\uC18C \uB610\uB294 \uB2E4\uC2DC \uC2E4\uD589 \uC791\uC5C5\uC774 \uC774\uBBF8 \uC2E4\uD589 \uC911\uC774\uBBC0\uB85C '{0}'\uC744(\uB97C) \uC2E4\uD589 \uCDE8\uC18C\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","'{0}'\uC744(\uB97C) \uC2E4\uD589 \uCDE8\uC18C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?","\uC2E4\uD589 \uCDE8\uC18C","\uCDE8\uC18C","\uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uB2E4\uC2DC \uC2E4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. {1}","\uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uB2E4\uC2DC \uC2E4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. {1}","{1}\uC5D0 \uBCC0\uACBD \uB0B4\uC6A9\uC774 \uC801\uC6A9\uB418\uC5C8\uC73C\uBBC0\uB85C \uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uB2E4\uC2DC \uC2E4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","{1}\uC5D0\uC11C \uC2E4\uD589 \uCDE8\uC18C \uB610\uB294 \uB2E4\uC2DC \uC2E4\uD589 \uC791\uC5C5\uC774 \uC774\uBBF8 \uC2E4\uD589 \uC911\uC774\uBBC0\uB85C \uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uB2E4\uC2DC \uC2E4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uADF8\uB3D9\uC548 \uC2E4\uD589 \uCDE8\uC18C \uB610\uB294 \uB2E4\uC2DC \uC2E4\uD589 \uC791\uC5C5\uC774 \uBC1C\uC0DD\uD588\uAE30 \uB54C\uBB38\uC5D0 \uBAA8\uB4E0 \uD30C\uC77C\uC5D0\uC11C '{0}'\uC744(\uB97C) \uB2E4\uC2DC \uC2E4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.","\uC2E4\uD589 \uCDE8\uC18C \uB610\uB294 \uB2E4\uC2DC \uC2E4\uD589 \uC791\uC5C5\uC774 \uC774\uBBF8 \uC2E4\uD589 \uC911\uC774\uBBC0\uB85C '{0}'\uC744(\uB97C) \uB2E4\uC2DC \uC2E4\uD589\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."]}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ru.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ru.js new file mode 100644 index 0000000..205ebca --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.ru.js @@ -0,0 +1,8 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/define("vs/editor/editor.main.nls.ru",{"vs/base/browser/ui/actionbar/actionViewItems":["{0} ({1})"],"vs/base/browser/ui/findinput/findInput":["\u0432\u0445\u043E\u0434\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435"],"vs/base/browser/ui/findinput/findInputCheckboxes":["\u0421 \u0443\u0447\u0435\u0442\u043E\u043C \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430","\u0421\u043B\u043E\u0432\u043E \u0446\u0435\u043B\u0438\u043A\u043E\u043C","\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0440\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u043E\u0435 \u0432\u044B\u0440\u0430\u0436\u0435\u043D\u0438\u0435"],"vs/base/browser/ui/findinput/replaceInput":["\u0432\u0445\u043E\u0434\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435","\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u0440\u0435\u0433\u0438\u0441\u0442\u0440"],"vs/base/browser/ui/iconLabel/iconLabel":["\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026"],"vs/base/browser/ui/inputbox/inputBox":["\u041E\u0448\u0438\u0431\u043A\u0430: {0}","\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435: {0}","\u0418\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["\u0441\u0432\u043E\u0431\u043E\u0434\u043D\u044B\u0439"],"vs/base/browser/ui/menu/menu":["{0} ({1})"],"vs/base/browser/ui/tree/abstractTree":["\u0421\u0431\u0440\u043E\u0441","\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0444\u0438\u043B\u044C\u0442\u0440 \u043F\u043E \u0442\u0438\u043F\u0443","\u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0444\u0438\u043B\u044C\u0442\u0440 \u043F\u043E \u0442\u0438\u043F\u0443","\u042D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B","\u0421\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432: {0} \u0438\u0437 {1}"],"vs/base/common/actions":["(\u043F\u0443\u0441\u0442\u043E)"],"vs/base/common/errorMessage":["{0}: {1}","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u0441\u0438\u0441\u0442\u0435\u043C\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430 ({0})","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0434\u0440\u043E\u0431\u043D\u044B\u0435 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u0441\u043C. \u0432 \u0436\u0443\u0440\u043D\u0430\u043B\u0435.","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0434\u0440\u043E\u0431\u043D\u044B\u0435 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u0441\u043C. \u0432 \u0436\u0443\u0440\u043D\u0430\u043B\u0435.","{0} (\u0432\u0441\u0435\u0433\u043E \u043E\u0448\u0438\u0431\u043E\u043A: {1})","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430. \u041F\u043E\u0434\u0440\u043E\u0431\u043D\u044B\u0435 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u0441\u043C. \u0432 \u0436\u0443\u0440\u043D\u0430\u043B\u0435."],"vs/base/common/keybindingLabels":["CTRL","SHIFT","ALT","Windows","CTRL","SHIFT","ALT","\u041F\u0440\u0435\u0432\u043E\u0441\u0445\u043E\u0434\u043D\u043E","CTRL","SHIFT","ALT","\u041A\u043E\u043C\u0430\u043D\u0434\u0430","CTRL","SHIFT","ALT","Windows","CTRL","SHIFT","ALT","\u041F\u0440\u0435\u0432\u043E\u0441\u0445\u043E\u0434\u043D\u043E"],"vs/base/parts/quickinput/browser/quickInput":["\u041D\u0430\u0437\u0430\u0434","{0} / {1}","\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u0435\u043A\u0441\u0442, \u0447\u0442\u043E\u0431\u044B \u0443\u043C\u0435\u043D\u044C\u0448\u0438\u0442\u044C \u0447\u0438\u0441\u043B\u043E \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432.","\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B: {0}","{0} \u0432\u044B\u0431\u0440\u0430\u043D\u043E","OK","\u0414\u0440\u0443\u0433\u043E\u0439","\u041D\u0430\u0437\u0430\u0434 ({0})","\u041D\u0430\u0437\u0430\u0434"],"vs/base/parts/quickinput/browser/quickInputList":["\u0411\u044B\u0441\u0442\u0440\u044B\u0439 \u0432\u0432\u043E\u0434"],"vs/editor/browser/controller/coreCommands":["\u0420\u0430\u0437\u043C\u0435\u0449\u0430\u0442\u044C \u043D\u0430 \u043A\u043E\u043D\u0446\u0435 \u0434\u0430\u0436\u0435 \u0434\u043B\u044F \u0431\u043E\u043B\u0435\u0435 \u0434\u043B\u0438\u043D\u043D\u044B\u0445 \u0441\u0442\u0440\u043E\u043A","\u0420\u0430\u0437\u043C\u0435\u0449\u0430\u0442\u044C \u043D\u0430 \u043A\u043E\u043D\u0446\u0435 \u0434\u0430\u0436\u0435 \u0434\u043B\u044F \u0431\u043E\u043B\u0435\u0435 \u0434\u043B\u0438\u043D\u043D\u044B\u0445 \u0441\u0442\u0440\u043E\u043A"],"vs/editor/browser/controller/textAreaHandler":["\u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440","\u0421\u0435\u0439\u0447\u0430\u0441 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D. \u041D\u0430\u0436\u043C\u0438\u0442\u0435 {0} \u0434\u043B\u044F \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u043E\u0432."],"vs/editor/browser/core/keybindingCancellation":['\u0412\u044B\u043F\u043E\u043B\u043D\u044F\u044E\u0442\u0441\u044F \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438, \u0434\u043E\u043F\u0443\u0441\u043A\u0430\u044E\u0449\u0438\u0435 \u043E\u0442\u043C\u0435\u043D\u0443, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, "\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0438"'],"vs/editor/browser/editorExtensions":["&&\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C","\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C","&&\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C","\u0412\u0435\u0440\u043D\u0443\u0442\u044C","&&\u0412\u044B\u0434\u0435\u043B\u0438\u0442\u044C \u0432\u0441\u0435","\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u0441\u0435"],"vs/editor/browser/widget/codeEditorWidget":["\u041A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u043A\u0443\u0440\u0441\u043E\u0440\u043E\u0432 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u043E {0}."],"vs/editor/browser/widget/diffEditorWidget":["\u041E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 \u0434\u043B\u044F \u0432\u0441\u0442\u0430\u0432\u043E\u043A \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.","\u041E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 \u0434\u043B\u044F \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0439 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.","\u041D\u0435\u043B\u044C\u0437\u044F \u0441\u0440\u0430\u0432\u043D\u0438\u0442\u044C \u0444\u0430\u0439\u043B\u044B, \u043F\u043E\u0442\u043E\u043C\u0443 \u0447\u0442\u043E \u043E\u0434\u0438\u043D \u0438\u0437 \u0444\u0430\u0439\u043B\u043E\u0432 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0431\u043E\u043B\u044C\u0448\u043E\u0439."],"vs/editor/browser/widget/diffReview":['\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043A\u043D\u043E\u043F\u043A\u0438 "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C" \u0432 \u043E\u043A\u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.','\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043A\u043D\u043E\u043F\u043A\u0438 "\u0423\u0434\u0430\u043B\u0438\u0442\u044C" \u0432 \u043E\u043A\u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.','\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043A\u043D\u043E\u043F\u043A\u0438 "\u0417\u0430\u043A\u0440\u044B\u0442\u044C" \u0432 \u043E\u043A\u043D\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.',"\u0417\u0430\u043A\u0440\u044B\u0442\u044C","\u043D\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043D\u044B\u0445 \u0441\u0442\u0440\u043E\u043A","1 \u0441\u0442\u0440\u043E\u043A\u0430 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0430","\u0421\u0442\u0440\u043E\u043A \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043E: {0}","\u0420\u0430\u0437\u043B\u0438\u0447\u0438\u0435 {0} \u0438\u0437 {1}: \u0438\u0441\u0445\u043E\u0434\u043D\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 {2}, {3}, \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043D\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 {4}, {5}","\u043F\u0443\u0441\u0442\u043E\u0439","{0} \u043D\u0435\u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043D\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 {1}","{0} \u0438\u0441\u0445\u043E\u0434\u043D\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 {1} \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043D\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 {2}","+ {0} \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043D\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 {1}","- {0} \u0438\u0441\u0445\u043E\u0434\u043D\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 {1}","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C\u0443 \u0440\u0430\u0437\u043B\u0438\u0447\u0438\u044E","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u043C\u0443 \u0440\u0430\u0437\u043B\u0438\u0447\u0438\u044E"],"vs/editor/browser/widget/inlineDiffMargin":["\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u043E\u043A\u0438","\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443","\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443 ({0})","\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u044D\u0442\u043E \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0435","\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443 ({0})"],"vs/editor/common/config/commonEditorConfig":["\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440",'\u0427\u0438\u0441\u043B\u043E \u043F\u0440\u043E\u0431\u0435\u043B\u043E\u0432 \u0432 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438. \u042D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442\u0441\u044F \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430, \u0435\u0441\u043B\u0438 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 "#editor.detectIndentation#".','\u0412\u0441\u0442\u0430\u0432\u043B\u044F\u0442\u044C \u043F\u0440\u043E\u0431\u0435\u043B\u044B \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB. \u042D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442\u0441\u044F \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430, \u0435\u0441\u043B\u0438 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 "#editor.detectIndentation#". ','\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u0431\u0443\u0434\u0443\u0442 \u043B\u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B "#editor.tabSize#" \u0438 "#editor.insertSpaces#" \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0442\u044C\u0441\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043F\u0440\u0438 \u043E\u0442\u043A\u0440\u044B\u0442\u0438\u0438 \u0444\u0430\u0439\u043B\u0430 \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430.',"\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u043C\u044B\u0439 \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u0439 \u043F\u0440\u043E\u0431\u0435\u043B.","\u0421\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u0430\u044F \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0430 \u0434\u043B\u044F \u0431\u043E\u043B\u044C\u0448\u0438\u0445 \u0444\u0430\u0439\u043B\u043E\u0432 \u0441 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435\u043C \u043D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0444\u0443\u043D\u043A\u0446\u0438\u0439, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0438\u043D\u0442\u0435\u043D\u0441\u0438\u0432\u043D\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u044E\u0442 \u043F\u0430\u043C\u044F\u0442\u044C.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043B\u0438 \u043E\u0446\u0435\u043D\u0438\u0432\u0430\u0442\u044C \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0441\u043B\u043E\u0432 \u0432 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0435.","\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0441\u043B\u043E\u0432 \u0442\u043E\u043B\u044C\u043A\u043E \u0438\u0437 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430.","\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0441\u043B\u043E\u0432 \u0438\u0437 \u0432\u0441\u0435\u0445 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u043D\u0430 \u043E\u0434\u043D\u043E\u043C \u044F\u0437\u044B\u043A\u0435.","\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0441\u043B\u043E\u0432 \u0438\u0437 \u0432\u0441\u0435\u0445 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0438\u0437 \u043A\u0430\u043A\u0438\u0445 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432 \u0431\u0443\u0434\u0443\u0442 \u0432\u044B\u0447\u0438\u0441\u043B\u044F\u0442\u044C\u0441\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u0441\u043B\u043E\u0432.","\u0421\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0446\u0432\u0435\u0442\u043E\u0432\u044B\u0445 \u0442\u0435\u043C.","\u0421\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0446\u0432\u0435\u0442\u043E\u0432\u044B\u0445 \u0442\u0435\u043C.",'\u0421\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430 "semanticHighlighting" \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0446\u0432\u0435\u0442\u043E\u0432\u043E\u0439 \u0442\u0435\u043C\u044B.',"\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u043F\u043E\u043A\u0430\u0437 \u0441\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0439 \u043F\u043E\u0434\u0441\u0432\u0435\u0442\u043A\u0438 \u0434\u043B\u044F \u044F\u0437\u044B\u043A\u043E\u0432, \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044E\u0449\u0438\u0445 \u0435\u0435.","\u041E\u0441\u0442\u0430\u0432\u043B\u044F\u0442\u044C \u0431\u044B\u0441\u0442\u0440\u044B\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C \u0434\u0430\u0436\u0435 \u043F\u0440\u0438 \u0434\u0432\u043E\u0439\u043D\u043E\u043C \u0449\u0435\u043B\u0447\u043A\u0435 \u043F\u043E \u0435\u0433\u043E \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u043C\u0443 \u0438 \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 ESC.","\u0421\u0442\u0440\u043E\u043A\u0438, \u0434\u043B\u0438\u043D\u0430 \u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0435\u0442 \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435, \u043D\u0435 \u0431\u0443\u0434\u0443\u0442 \u0440\u0430\u0437\u043C\u0435\u0447\u0435\u043D\u044B \u0438\u0437 \u0441\u043E\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0439 \u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u0438","\u0412\u0440\u0435\u043C\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u0432 \u043C\u0438\u043B\u043B\u0438\u0441\u0435\u043A\u0443\u043D\u0434\u0430\u0445, \u043F\u043E \u0438\u0441\u0442\u0435\u0447\u0435\u043D\u0438\u0438 \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u0432\u044B\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u0435 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u043E\u0442\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F. \u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 0, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u0432\u0440\u0435\u043C\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043A\u0430\u043A \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u043E\u0442\u043B\u0438\u0447\u0438\u044F: \u0440\u044F\u0434\u043E\u043C \u0438\u043B\u0438 \u0432 \u0442\u0435\u043A\u0441\u0442\u0435.","\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u0443\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u0438\u043B\u0438 \u043A\u043E\u043D\u0435\u0447\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0431\u0435\u043B\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u044B +/- \u0434\u043B\u044F \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0438\u043B\u0438 \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0445 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043B\u0438 CodeLens \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0421\u0442\u0440\u043E\u043A\u0438 \u043D\u0435 \u0431\u0443\u0434\u0443\u0442 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0438\u0442\u044C\u0441\u044F \u043D\u0438\u043A\u043E\u0433\u0434\u0430.","\u0421\u0442\u0440\u043E\u043A\u0438 \u0431\u0443\u0434\u0443\u0442 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0438\u0442\u044C\u0441\u044F \u043F\u043E \u0448\u0438\u0440\u0438\u043D\u0435 \u043E\u043A\u043D\u0430 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430.",'\u0421\u0442\u0440\u043E\u043A\u0438 \u0431\u0443\u0434\u0443\u0442 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0438\u0442\u044C\u0441\u044F \u0432 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u043C "#editor.wordWrap#".'],"vs/editor/common/config/editorOptions":["\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0431\u0443\u0434\u0435\u0442 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0442\u044C, \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u043B\u0438 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043E \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430, \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E API-\u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u043E\u0432 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u044B.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0431\u0443\u0434\u0435\u0442 \u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u043D \u0434\u043B\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u0441\u043E \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043E\u043C \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430 \u0432 \u043F\u043E\u0441\u0442\u043E\u044F\u043D\u043D\u043E\u043C \u0440\u0435\u0436\u0438\u043C\u0435. \u041F\u0435\u0440\u0435\u043D\u043E\u0441 \u0442\u0435\u043A\u0441\u0442\u0430 \u0431\u0443\u0434\u0435\u0442 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043D\u0438\u043A\u043E\u0433\u0434\u0430 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0434\u043B\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u0441\u043E \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043E\u043C \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043B\u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0432 \u0440\u0435\u0436\u0438\u043C\u0435 \u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0430\u0446\u0438\u0438 \u0434\u043B\u044F \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430. \u0415\u0441\u043B\u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u043F\u0435\u0440\u0435\u043D\u043E\u0441 \u0441\u0442\u0440\u043E\u043A \u0431\u0443\u0434\u0435\u0442 \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043B\u0438 \u043F\u0440\u043E\u0431\u0435\u043B \u043F\u0440\u0438 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0438.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u043F\u0443\u0441\u0442\u044B\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F, \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0438\u043B\u0438 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u044F \u0434\u043B\u044F \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432 \u043A \u0441\u0442\u0440\u043E\u043A\u0430\u043C.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u043A\u043E\u043F\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u043B\u0438 \u0442\u0435\u043A\u0443\u0449\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u043F\u0440\u0438 \u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0438 \u0431\u0435\u0437 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u043A\u0443\u0440\u0441\u043E\u0440 \u043F\u0435\u0440\u0435\u043C\u0435\u0449\u0430\u0442\u044C\u0441\u044F \u0434\u043B\u044F \u043F\u043E\u0438\u0441\u043A\u0430 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043C\u043E\u0436\u043D\u043E \u043B\u0438 \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u043E\u0438\u0441\u043A\u0430 \u0438\u0437 \u0442\u0435\u043A\u0441\u0442\u0430, \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.",'\u041D\u0438\u043A\u043E\u0433\u0434\u0430 \u043D\u0435 \u0432\u043A\u043B\u044E\u0447\u0430\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u044E "\u041D\u0430\u0439\u0442\u0438 \u0432 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0438" \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E)','\u0412\u0441\u0435\u0433\u0434\u0430 \u0432\u043A\u043B\u044E\u0447\u0430\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u044E "\u041D\u0430\u0439\u0442\u0438 \u0432 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0438" \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438','\u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u0438 "\u041D\u0430\u0439\u0442\u0438 \u0432 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0438" \u043F\u0440\u0438 \u0432\u044B\u0431\u043E\u0440\u0435 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u0441\u0442\u0440\u043E\u043A \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0433\u043E.',"\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0443\u0441\u043B\u043E\u0432\u0438\u0435\u043C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0433\u043E \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u044F \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u043E\u043C \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u0435.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u043E \u043B\u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u043E\u0438\u0441\u043A\u0430 \u0441\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u0438\u043B\u0438 \u0438\u0437\u043C\u0435\u043D\u044F\u0442\u044C \u043E\u0431\u0449\u0438\u0439 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 macOS.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u043E \u043B\u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u043E\u0438\u0441\u043A\u0430 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0442\u044C \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 \u0432 \u043D\u0430\u0447\u0430\u043B\u0435 \u043E\u043A\u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430. \u0415\u0441\u043B\u0438 \u0437\u0430\u0434\u0430\u043D\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 true, \u0432\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u0438\u0442\u044C \u043F\u0435\u0440\u0432\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443 \u043F\u0440\u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u043C\u043E\u043C \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u043E\u0438\u0441\u043A\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0435\u0442 \u043B\u0438 \u043F\u043E\u0438\u0441\u043A \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0442\u044C\u0441\u044F \u0441 \u043D\u0430\u0447\u0430\u043B\u0430 (\u0438\u043B\u0438 \u0441 \u043A\u043E\u043D\u0446\u0430), \u0435\u0441\u043B\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043D\u0438\u043A\u0430\u043A\u0438\u0445 \u0434\u0440\u0443\u0433\u0438\u0445 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0439.",'\u0412\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0438\u043B\u0438 \u043E\u0442\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u043B\u0438\u0433\u0430\u0442\u0443\u0440\u044B \u0448\u0440\u0438\u0444\u0442\u043E\u0432 (\u0445\u0430\u0440\u0430\u043A\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043A\u0438 \u0448\u0440\u0438\u0444\u0442\u0430 "calt" \u0438 "liga"). \u0418\u0437\u043C\u0435\u043D\u0438\u0442\u0435 \u044D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u043D\u0430 \u0441\u0442\u0440\u043E\u043A\u0443 \u0434\u043B\u044F \u0434\u0435\u0442\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u043E\u043C CSS "font-feature-settings".','\u042F\u0432\u043D\u043E\u0435 \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u043E CSS "font-feature-settings". \u0415\u0441\u043B\u0438 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0438\u043B\u0438 \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043B\u0438\u0433\u0430\u0442\u0443\u0440\u044B, \u0432\u043C\u0435\u0441\u0442\u043E \u043D\u0435\u0433\u043E \u043C\u043E\u0436\u043D\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0442\u044C \u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435.','\u041D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442 \u043B\u0438\u0433\u0430\u0442\u0443\u0440\u044B \u0438\u043B\u0438 \u0445\u0430\u0440\u0430\u043A\u0442\u0435\u0440\u0438\u0441\u0442\u0438\u043A\u0438 \u0448\u0440\u0438\u0444\u0442\u0430. \u041C\u043E\u0436\u043D\u043E \u0443\u043A\u0430\u0437\u0430\u0442\u044C \u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435, \u0447\u0442\u043E\u0431\u044B \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0438\u043B\u0438 \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043B\u0438\u0433\u0430\u0442\u0443\u0440\u044B, \u0438\u043B\u0438 \u0441\u0442\u0440\u043E\u043A\u0443 \u0434\u043B\u044F \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u0430 CSS "font-feature-settings".',"\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u0440\u0430\u0437\u043C\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430 \u0432 \u043F\u0438\u043A\u0441\u0435\u043B\u044F\u0445.",'\u0414\u043E\u043F\u0443\u0441\u043A\u0430\u044E\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u043B\u044E\u0447\u0435\u0432\u044B\u0435 \u0441\u043B\u043E\u0432\u0430 "normal" \u0438\u043B\u0438 "bold" \u0438 \u0447\u0438\u0441\u043B\u0430 \u0432 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0435 \u043E\u0442 1 \u0434\u043E 1000.','\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043D\u0430\u0441\u044B\u0449\u0435\u043D\u043D\u043E\u0441\u0442\u044C\u044E \u0448\u0440\u0438\u0444\u0442\u0430. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F: \u043A\u043B\u044E\u0447\u0435\u0432\u044B\u0435 \u0441\u043B\u043E\u0432\u0430 "normal" \u0438\u043B\u0438 "bold", \u0430 \u0442\u0430\u043A\u0436\u0435 \u0447\u0438\u0441\u043B\u0430 \u0432 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0435 \u043E\u0442 1 \u0434\u043E 1000.',"\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B (\u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E)","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u043C\u0443 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0443 \u0438 \u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0431\u044B\u0441\u0442\u0440\u044B\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440","\u041F\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043A \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u043C\u0443 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0443 \u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0431\u044B\u0441\u0442\u0440\u0443\u044E \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044E \u0434\u043B\u044F \u043E\u0441\u0442\u0430\u043B\u044C\u043D\u044B\u0445","\u042D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0443\u0441\u0442\u0430\u0440\u0435\u043B. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0432\u043C\u0435\u0441\u0442\u043E \u043D\u0435\u0433\u043E \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, 'editor.editor.gotoLocation.multipleDefinitions' \u0438\u043B\u0438 'editor.editor.gotoLocation.multipleImplementations'.",'\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435\u043C \u043A\u043E\u043C\u0430\u043D\u0434\u044B "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E" \u043F\u0440\u0438 \u043D\u0430\u043B\u0438\u0447\u0438\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u0446\u0435\u043B\u0435\u0432\u044B\u0445 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0439.','\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435\u043C \u043A\u043E\u043C\u0430\u043D\u0434\u044B "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E \u0442\u0438\u043F\u0430" \u043F\u0440\u0438 \u043D\u0430\u043B\u0438\u0447\u0438\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u0446\u0435\u043B\u0435\u0432\u044B\u0445 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0439.','\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435\u043C \u043A\u043E\u043C\u0430\u043D\u0434\u044B "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u044E" \u043F\u0440\u0438 \u043D\u0430\u043B\u0438\u0447\u0438\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u0446\u0435\u043B\u0435\u0432\u044B\u0445 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0439.','\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435\u043C \u043A\u043E\u043C\u0430\u043D\u0434\u044B "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F\u043C" \u043F\u0440\u0438 \u043D\u0430\u043B\u0438\u0447\u0438\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u0446\u0435\u043B\u0435\u0432\u044B\u0445 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0439.','\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435\u043C \u043A\u043E\u043C\u0430\u043D\u0434\u044B "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0441\u044B\u043B\u043A\u0430\u043C" \u043F\u0440\u0438 \u043D\u0430\u043B\u0438\u0447\u0438\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u0446\u0435\u043B\u0435\u0432\u044B\u0445 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0439.','\u0418\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u043C\u043E\u0439 \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u043A\u043E\u0433\u0434\u0430 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u043C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E" \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0435 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435.','\u0418\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u0435\u0441\u043B\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u043C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E \u0442\u0438\u043F\u0430" \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0435 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435.','\u0418\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u043C\u043E\u0439 \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u043A\u043E\u0433\u0434\u0430 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u043C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u044E" \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0435 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435.','\u0418\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u044B\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u043C\u043E\u0439, \u043A\u043E\u0433\u0434\u0430 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u043C \u043A\u043E\u043C\u0430\u043D\u0434\u044B "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438" \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0435 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435.','\u0418\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0430\u043B\u044C\u0442\u0435\u0440\u043D\u0430\u0442\u0438\u0432\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u044B, \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u043C\u043E\u0439 \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u043A\u043E\u0433\u0434\u0430 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u043C \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0441\u044B\u043B\u043A\u0435" \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0435 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435.',"\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043B\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0435.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u0432\u0440\u0435\u043C\u044F \u0437\u0430\u0434\u0435\u0440\u0436\u043A\u0438 \u0432 \u043C\u0438\u043B\u043B\u0438\u0441\u0435\u043A\u0443\u043D\u0434\u0430\u0445 \u043F\u0435\u0440\u0435\u0434 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u043C \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u044F.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u0434\u043E\u043B\u0436\u043D\u043E \u043B\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0435 \u043E\u0441\u0442\u0430\u0432\u0430\u0442\u044C\u0441\u044F \u0432\u0438\u0434\u0438\u043C\u044B\u043C \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u043D\u0430 \u043D\u0435\u0433\u043E \u043A\u0443\u0440\u0441\u043E\u0440\u0430 \u043C\u044B\u0448\u0438.","\u0412\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u043E\u0434\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0412\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u0438\u044F \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.",'\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u0440\u0430\u0437\u043C\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0445 \u0443\u043A\u0430\u0437\u0430\u043D\u0438\u0439 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u0415\u0441\u043B\u0438 \u0437\u0430\u0434\u0430\u043D\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "0", \u0442\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F 90\xA0% \u043E\u0442 "#editor.fontSize#".',"\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0435\u043C\u0435\u0439\u0441\u0442\u0432\u043E\u043C \u0448\u0440\u0438\u0444\u0442\u043E\u0432 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0445 \u0443\u043A\u0430\u0437\u0430\u043D\u0438\u0439 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0432\u044B\u0441\u043E\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A. \u0423\u043A\u0430\u0436\u0438\u0442\u0435 0 \u0434\u043B\u044F \u0432\u044B\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F \u0432\u044B\u0441\u043E\u0442\u044B \u0441\u0442\u0440\u043E\u043A\u0438 \u043F\u043E \u0440\u0430\u0437\u043C\u0435\u0440\u0443 \u0448\u0440\u0438\u0444\u0442\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043B\u0438 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u0430.","\u041C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u0430 \u0438\u043C\u0435\u0435\u0442 \u0442\u0430\u043A\u043E\u0439 \u0436\u0435 \u0440\u0430\u0437\u043C\u0435\u0440, \u0447\u0442\u043E \u0438 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 (\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u0430 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0430).","\u041C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u0430 \u0431\u0443\u0434\u0435\u0442 \u0440\u0430\u0441\u0442\u044F\u0433\u0438\u0432\u0430\u0442\u044C\u0441\u044F \u0438\u043B\u0438 \u0441\u0436\u0438\u043C\u0430\u0442\u044C\u0441\u044F \u043F\u043E \u043C\u0435\u0440\u0435 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E\u0441\u0442\u0438, \u0447\u0442\u043E\u0431\u044B \u0437\u0430\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043F\u043E \u0432\u044B\u0441\u043E\u0442\u0435 (\u0431\u0435\u0437 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0438).","\u041C\u0438\u043D\u0438\u043A\u0430\u0440\u0442\u0430 \u0431\u0443\u0434\u0435\u0442 \u0443\u043C\u0435\u043D\u044C\u0448\u0430\u0442\u044C\u0441\u044F \u043F\u043E \u043C\u0435\u0440\u0435 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E\u0441\u0442\u0438, \u0447\u0442\u043E\u0431\u044B \u043D\u0438\u043A\u043E\u0433\u0434\u0430 \u043D\u0435 \u0431\u044B\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435, \u0447\u0435\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 (\u0431\u0435\u0437 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0438).","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0440\u0430\u0437\u043C\u0435\u0440\u043E\u043C \u043C\u0438\u043D\u0438\u043A\u0430\u0440\u0442\u044B.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0441 \u043A\u0430\u043A\u043E\u0439 \u0441\u0442\u043E\u0440\u043E\u043D\u044B \u0431\u0443\u0434\u0435\u0442 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043A\u043E\u0433\u0434\u0430 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043F\u043E\u043B\u0437\u0443\u043D\u043E\u043A \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u044B.","\u041C\u0430\u0441\u0448\u0442\u0430\u0431 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0433\u043E, \u043D\u0430\u0440\u0438\u0441\u043E\u0432\u0430\u043D\u043D\u043E\u0433\u043E \u043D\u0430 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u0435: 1, 2 \u0438\u043B\u0438 3.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u0444\u0430\u043A\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0432 \u0441\u0442\u0440\u043E\u043A\u0435 \u0432\u043C\u0435\u0441\u0442\u043E \u0446\u0432\u0435\u0442\u043D\u044B\u0445 \u0431\u043B\u043E\u043A\u043E\u0432.","\u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0432\u0430\u0435\u0442 \u0448\u0438\u0440\u0438\u043D\u0443 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u044B, \u0447\u0442\u043E\u0431\u044B \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u043C\u044B\u0445 \u0441\u0442\u043E\u043B\u0431\u0446\u043E\u0432 \u043D\u0435 \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u043B\u043E \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u043D\u043E\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E.","\u0417\u0430\u0434\u0430\u0435\u0442 \u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0441\u0442\u0432\u043E \u043C\u0435\u0436\u0434\u0443 \u0432\u0435\u0440\u0445\u043D\u0438\u043C \u043A\u0440\u0430\u0435\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u0438 \u043F\u0435\u0440\u0432\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439.","\u0417\u0430\u0434\u0430\u0435\u0442 \u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0441\u0442\u0432\u043E \u043C\u0435\u0436\u0434\u0443 \u043D\u0438\u0436\u043D\u0438\u043C \u043A\u0440\u0430\u0435\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u0438 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439.","\u0412\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0432\u0441\u043F\u043B\u044B\u0432\u0430\u044E\u0449\u0435\u0435 \u043E\u043A\u043D\u043E \u0441 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u0435\u0439 \u043F\u043E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0443 \u0438 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u044F\u043C\u0438 \u043E \u0442\u0438\u043F\u0435, \u043A\u043E\u0442\u043E\u0440\u043E\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u043D\u0430\u0431\u043E\u0440\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043C\u0435\u043D\u044E \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043E\u043A \u043E\u0441\u0442\u0430\u0435\u0442\u0441\u044F \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u043C \u0438\u043B\u0438 \u0437\u0430\u043A\u0440\u043E\u0435\u0442\u0441\u044F \u043F\u0440\u0438 \u0434\u043E\u0441\u0442\u0438\u0436\u0435\u043D\u0438\u0438 \u043A\u043E\u043D\u0446\u0430 \u0441\u043F\u0438\u0441\u043A\u0430.","\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u043A\u0440\u0430\u0442\u043A\u0438\u0445 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0432 \u0441\u0442\u0440\u043E\u043A\u0430\u0445.","\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u043A\u0440\u0430\u0442\u043A\u0438\u0445 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0432 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u044F\u0445.","\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u043A\u0440\u0430\u0442\u043A\u0438\u0445 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0432\u043D\u0435 \u0441\u0442\u0440\u043E\u043A \u0438 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435 \u0442\u0435\u043A\u0441\u0442\u0430 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F.","\u041D\u043E\u043C\u0435\u0440\u0430 \u0441\u0442\u0440\u043E\u043A \u043D\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0430\u0431\u0441\u043E\u043B\u044E\u0442\u043D\u044B\u0435 \u043D\u043E\u043C\u0435\u0440\u0430 \u0441\u0442\u0440\u043E\u043A.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u043C\u044B\u0435 \u043D\u043E\u043C\u0435\u0440\u0430 \u0441\u0442\u0440\u043E\u043A \u0432\u044B\u0447\u0438\u0441\u043B\u044F\u044E\u0442\u0441\u044F \u043A\u0430\u043A \u0440\u0430\u0441\u0441\u0442\u043E\u044F\u043D\u0438\u0435 \u0432 \u0441\u0442\u0440\u043E\u043A\u0430\u0445 \u0434\u043E \u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043A\u0443\u0440\u0441\u043E\u0440\u0430.","\u041D\u043E\u043C\u0435\u0440\u0430 \u0441\u0442\u0440\u043E\u043A \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043A\u0430\u0436\u0434\u044B\u0435 10 \u0441\u0442\u0440\u043E\u043A.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u043C \u043D\u043E\u043C\u0435\u0440\u043E\u0432 \u0441\u0442\u0440\u043E\u043A.","\u0427\u0438\u0441\u043B\u043E \u043C\u043E\u043D\u043E\u0448\u0438\u0440\u0438\u043D\u043D\u044B\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432, \u043F\u0440\u0438 \u043A\u043E\u0442\u043E\u0440\u043E\u043C \u0431\u0443\u0434\u0435\u0442 \u043E\u0442\u0440\u0438\u0441\u043E\u0432\u044B\u0432\u0430\u0442\u044C\u0441\u044F \u043B\u0438\u043D\u0435\u0439\u043A\u0430 \u044D\u0442\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u044D\u0442\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u043F\u043E\u0441\u043B\u0435 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u0447\u0438\u0441\u043B\u0430 \u043C\u043E\u043D\u043E\u0448\u0438\u0440\u0438\u043D\u043D\u044B\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432. \u0414\u043B\u044F \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u043B\u0438\u043D\u0435\u0435\u043A \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0439. \u0415\u0441\u043B\u0438 \u043D\u0435 \u0443\u043A\u0430\u0437\u0430\u043D\u043E \u043D\u0438 \u043E\u0434\u043D\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F, \u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B\u044C\u043D\u044B\u0435 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u043D\u0435 \u0431\u0443\u0434\u0443\u0442.","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0431\u0435\u0437 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0438\u0441\u0438 \u0442\u0435\u043A\u0441\u0442\u0430 \u0441\u043F\u0440\u0430\u0432\u0430 \u043E\u0442 \u043A\u0443\u0440\u0441\u043E\u0440\u0430.","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0438\u0441\u0430\u0442\u044C \u0442\u0435\u043A\u0441\u0442 \u0441\u043F\u0440\u0430\u0432\u0430 \u043E\u0442 \u043A\u0443\u0440\u0441\u043E\u0440\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0443\u0442 \u043B\u0438 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0438\u0441\u044B\u0432\u0430\u0442\u044C\u0441\u044F \u0441\u043B\u043E\u0432\u0430 \u043F\u0440\u0438 \u043F\u0440\u0438\u043D\u044F\u0442\u0438\u0438 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u043E\u0432 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F. \u041E\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043D\u0438\u043C\u0430\u043D\u0438\u0435, \u0447\u0442\u043E \u044D\u0442\u043E \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043E\u0442 \u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043D\u0438\u0439, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u044E\u0449\u0438\u0445 \u044D\u0442\u0443 \u0444\u0443\u043D\u043A\u0446\u0438\u044E.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u0434\u043E\u043F\u0443\u0441\u043A\u0430\u044E\u0442\u0441\u044F \u043B\u0438 \u043D\u0435\u0431\u043E\u043B\u044C\u0448\u0438\u0435 \u043E\u043F\u0435\u0447\u0430\u0442\u043A\u0438 \u0432 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u0445 \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u0438 \u0438 \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0438.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043B\u0438 \u0443\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043F\u0440\u0438 \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0435 \u0441\u043B\u043E\u0432\u0430, \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u0440\u044F\u0434\u043E\u043C \u0441 \u043A\u0443\u0440\u0441\u043E\u0440\u043E\u043C.",'\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u044E\u0442\u0441\u044F \u043B\u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043D\u044B\u0435 \u0432\u0430\u0440\u0438\u0430\u043D\u0442\u044B \u0432\u044B\u0431\u043E\u0440\u0430 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0441\u043E\u0432\u043C\u0435\u0441\u0442\u043D\u043E \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u043C\u0438 \u0440\u0430\u0431\u043E\u0447\u0438\u043C\u0438 \u043E\u0431\u043B\u0430\u0441\u0442\u044F\u043C\u0438 \u0438 \u043E\u043A\u043D\u0430\u043C\u0438 (\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044F "#editor.suggestSelection#").',"\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0437\u0430\u043F\u0440\u0435\u0449\u0430\u0435\u0442 \u043B\u0438 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0439 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442 \u043A\u043E\u0434\u0430 \u044D\u043A\u0441\u043F\u0440\u0435\u0441\u0441-\u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F.","\u0423\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442, \u043D\u0443\u0436\u043D\u043E \u043B\u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u0437\u043D\u0430\u0447\u043A\u0438 \u0432 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u0445.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u0432\u0438\u0434\u0438\u043C\u043E\u0441\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0438 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F \u0432 \u043D\u0438\u0436\u043D\u0435\u0439 \u0447\u0430\u0441\u0442\u0438 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043B\u0438 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u043E \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u043C \u043E\u0431\u0440\u0430\u0437\u043E\u043C \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u043C\u0435\u0442\u043A\u043E\u0439 \u0438\u043B\u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u0439.","\u042D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043D\u0435\u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u043C\u044B\u043C. \u0422\u0435\u043F\u0435\u0440\u044C \u0440\u0430\u0437\u043C\u0435\u0440 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u043C\u043E\u0436\u043D\u043E \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C.","\u042D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0443\u0441\u0442\u0430\u0440\u0435\u043B. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0432\u043C\u0435\u0441\u0442\u043E \u043D\u0435\u0433\u043E \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0435 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, 'editor.suggest.showKeywords' \u0438\u043B\u0438 'editor.suggest.showSnippets'.",'\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "method".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "function".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "constructor".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "field".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "variable".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "class".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "struct".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "interface".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "module".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "property".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "event".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "operator".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "unit".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "value".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "constant".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "enum".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "enumMember".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "keyword".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "text".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "color".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "file".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "reference".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "customcolor".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "folder".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "typeParameter".','\u041A\u043E\u0433\u0434\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0432\u043A\u043B\u044E\u0447\u0435\u043D, \u0432 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F "snippet".','\u0412\u043E \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043D\u043E\u043C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0438 IntelliSense \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0442\u0438\u043F\u0430 "\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438".','\u0412\u043E \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043D\u043E\u043C \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0438 IntelliSense \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0442\u0438\u043F\u0430 "\u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B".',"\u0414\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u0432\u0441\u0435\u0433\u0434\u0430 \u0431\u044B\u0442\u044C \u0432\u044B\u0431\u0440\u0430\u043D\u044B \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u044B\u0439 \u0438 \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u0439 \u043F\u0440\u043E\u0431\u0435\u043B\u044B.",'\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0443\u0442 \u043B\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0442\u044C\u0441\u044F \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0444\u0438\u043A\u0441\u0430\u0446\u0438\u0438. \u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0432 JavaScript \u0442\u043E\u0447\u043A\u0430 \u0441 \u0437\u0430\u043F\u044F\u0442\u043E\u0439 (";") \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u043C \u0444\u0438\u043A\u0441\u0430\u0446\u0438\u0438, \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435 \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0435\u0442\u0441\u044F.',"\u041F\u0440\u0438\u043D\u0438\u043C\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 \u0412\u0412\u041E\u0414 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u0435\u0441\u043B\u0438 \u043E\u043D\u043E \u0438\u0437\u043C\u0435\u043D\u044F\u0435\u0442 \u0442\u0435\u043A\u0441\u0442.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0443\u0442 \u043B\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u0440\u0438\u043D\u0438\u043C\u0430\u0442\u044C\u0441\u044F \u043A\u043B\u0430\u0432\u0438\u0448\u0435\u0439 \u0412\u0412\u041E\u0414 \u0432 \u0434\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A \u043A\u043B\u0430\u0432\u0438\u0448\u0435 TAB. \u042D\u0442\u043E \u043F\u043E\u043C\u043E\u0433\u0430\u0435\u0442 \u0438\u0437\u0431\u0435\u0436\u0430\u0442\u044C \u043D\u0435\u043E\u0434\u043D\u043E\u0437\u043D\u0430\u0447\u043D\u043E\u0441\u0442\u0438 \u043C\u0435\u0436\u0434\u0443 \u0432\u0441\u0442\u0430\u0432\u043A\u043E\u0439 \u043D\u043E\u0432\u044B\u0445 \u0441\u0442\u0440\u043E\u043A \u0438 \u043F\u0440\u0438\u043D\u044F\u0442\u0438\u0435\u043C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0417\u0430\u0434\u0430\u0435\u0442 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u0441\u0442\u0440\u043E\u043A \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043C\u043E\u0433\u0443\u0442 \u0431\u044B\u0442\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u044B \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043E\u043C \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430. \u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435: \u0438\u0437-\u0437\u0430 \u0442\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0438\u0445 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0439 \u044D\u0442\u043E \u0447\u0438\u0441\u043B\u043E \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u043F\u0440\u0435\u0432\u044B\u0448\u0430\u0442\u044C \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E.","\u0421\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u044F\u0437\u044B\u043A\u0430 \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0433\u043E \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u044F \u0441\u043A\u043E\u0431\u043E\u043A.","\u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0442\u044C \u0441\u043A\u043E\u0431\u043A\u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u0435\u0441\u043B\u0438 \u043A\u0443\u0440\u0441\u043E\u0440 \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0441\u043B\u0435\u0432\u0430 \u043E\u0442 \u043F\u0440\u043E\u0431\u0435\u043B\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0442\u044C \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E\u0449\u0443\u044E \u0441\u043A\u043E\u0431\u043A\u0443 \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0435\u043C \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u044E\u0449\u0435\u0439 \u0441\u043A\u043E\u0431\u043A\u0438.","\u0417\u0430\u043C\u0435\u043D\u044F\u0442\u044C \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E\u0449\u0438\u0435 \u043A\u0430\u0432\u044B\u0447\u043A\u0438 \u0438 \u0441\u043A\u043E\u0431\u043A\u0438 \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u0435\u0441\u043B\u0438 \u043A\u0430\u0432\u044B\u0447\u043A\u0438 \u0438\u043B\u0438 \u0441\u043A\u043E\u0431\u043A\u0438 \u0431\u044B\u043B\u0438 \u0432\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0437\u0430\u043C\u0435\u043D\u044F\u0442\u044C\u0441\u044F \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u044E\u0449\u0438\u0435 \u043A\u0430\u0432\u044B\u0447\u043A\u0438 \u0438\u043B\u0438 \u0441\u043A\u043E\u0431\u043A\u0438 \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435.","\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u044F\u0437\u044B\u043A\u0430 \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0433\u043E \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u044F \u043A\u0430\u0432\u044B\u0447\u0435\u043A.","\u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0442\u044C \u043A\u0430\u0432\u044B\u0447\u043A\u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u0435\u0441\u043B\u0438 \u043A\u0443\u0440\u0441\u043E\u0440 \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0441\u043B\u0435\u0432\u0430 \u043E\u0442 \u043F\u0440\u043E\u0431\u0435\u043B\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0437\u0430\u043A\u0440\u044B\u0432\u0430\u0442\u044C \u043A\u0430\u0432\u044B\u0447\u043A\u0438, \u0435\u0441\u043B\u0438 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C \u0434\u043E\u0431\u0430\u0432\u0438\u043B \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u044E\u0449\u0443\u044E \u043A\u0430\u0432\u044B\u0447\u043A\u0443.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F\u044B \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0431\u0443\u0434\u0435\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0442\u0440\u043E\u043A\u0438.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0431\u0443\u0434\u0435\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F\u044B \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0442\u0440\u043E\u043A\u0438 \u0438 \u0443\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u0441\u043A\u043E\u0431\u043A\u0438 \u0432 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0438 \u0441 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441\u043E\u043C \u044F\u0437\u044B\u043A\u0430.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0431\u0443\u0434\u0435\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0442\u0440\u043E\u043A\u0438, \u0443\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435 \u044F\u0437\u044B\u043A\u043E\u043C \u0441\u043A\u043E\u0431\u043A\u0438 \u0438 \u0432\u044B\u0437\u044B\u0432\u0430\u0442\u044C \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 onEnterRules, \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u043C\u044B\u0435 \u044F\u0437\u044B\u043A\u0430\u043C\u0438.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0431\u0443\u0434\u0435\u0442 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0442\u0440\u043E\u043A\u0438, \u0443\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0435 \u044F\u0437\u044B\u043A\u043E\u043C \u0441\u043A\u043E\u0431\u043A\u0438, \u0432\u044B\u0437\u044B\u0432\u0430\u0442\u044C \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u0438\u043B\u0430 onEnterRules, \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u043C\u044B\u0435 \u044F\u0437\u044B\u043A\u0430\u043C\u0438 \u0438 \u0443\u0447\u0438\u0442\u044B\u0432\u0430\u0442\u044C \u043F\u0440\u0430\u0432\u0438\u043B\u0430 \u043E\u0442\u0441\u0442\u0443\u043F\u0430 indentationRules, \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u043C\u044B\u0435 \u044F\u0437\u044B\u043A\u0430\u043C\u0438.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0438\u0437\u043C\u0435\u043D\u044F\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F\u044B, \u043A\u043E\u0433\u0434\u0430 \u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u0438 \u0432\u0432\u043E\u0434\u044F\u0442, \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u044E\u0442 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u043C\u0435\u0449\u0430\u044E\u0442 \u0442\u0435\u043A\u0441\u0442 \u0438\u043B\u0438 \u0438\u0437\u043C\u0435\u043D\u044F\u044E\u0442 \u043E\u0442\u0441\u0442\u0443\u043F\u044B \u0441\u0442\u0440\u043E\u043A.","\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u044F\u0437\u044B\u043A\u0430 \u0434\u043B\u044F \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0433\u043E \u043E\u0431\u0440\u0430\u043C\u043B\u0435\u043D\u0438\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0439.","\u041E\u0431\u0440\u0430\u043C\u043B\u044F\u0442\u044C \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043A\u0430\u0432\u044B\u0447\u0435\u043A, \u0430 \u043D\u0435 \u0441\u043A\u043E\u0431\u043E\u043A.","\u041E\u0431\u0440\u0430\u043C\u043B\u044F\u0442\u044C \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u0441\u043A\u043E\u0431\u043E\u043A, \u0430 \u043D\u0435 \u043A\u0430\u0432\u044B\u0447\u0435\u043A.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043E\u0431\u0440\u0430\u043C\u043B\u044F\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435 \u043A\u0430\u0432\u044B\u0447\u0435\u043A \u0438\u043B\u0438 \u043A\u0432\u0430\u0434\u0440\u0430\u0442\u043D\u044B\u0445 \u0441\u043A\u043E\u0431\u043E\u043A.","\u042D\u043C\u0443\u043B\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438 \u043F\u0440\u0438 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0438 \u043F\u0440\u043E\u0431\u0435\u043B\u043E\u0432 \u0434\u043B\u044F \u043E\u0442\u0441\u0442\u0443\u043F\u0430. \u0412\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u043F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u043E \u043A \u043F\u043E\u0437\u0438\u0446\u0438\u044F\u043C \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043B\u0438 CodeLens \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0435\u043C\u0435\u0439\u0441\u0442\u0432\u043E\u043C \u0448\u0440\u0438\u0444\u0442\u043E\u0432 \u0434\u043B\u044F CodeLens.",'\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u0440\u0430\u0437\u043C\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430 \u0432 \u043F\u0438\u043A\u0441\u0435\u043B\u044F\u0445 \u0434\u043B\u044F CodeLens. \u0415\u0441\u043B\u0438 \u0437\u0430\u0434\u0430\u043D\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "0", \u0442\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F 90% \u043E\u0442 "#editor.fontSize#".',"\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0438\u0435 \u0434\u0435\u043A\u043E\u0440\u0430\u0442\u043E\u0440\u044B \u0446\u0432\u0435\u0442\u0430 \u0438 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043E \u0432\u044B\u0431\u043E\u0440\u0430 \u0446\u0432\u0435\u0442\u0430.","\u0412\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0442\u043E\u0433\u043E, \u0447\u0442\u043E \u0432\u044B\u0431\u043E\u0440 \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0438 \u043C\u044B\u0448\u0438 \u043F\u0440\u0438\u0432\u043E\u0434\u0438\u0442 \u043A \u0432\u044B\u0431\u043E\u0440\u0443 \u0441\u0442\u043E\u043B\u0431\u0446\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0435\u0442 \u043B\u0438 \u0442\u0435\u043A\u0441\u0442 \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D \u0432 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430 \u0441 \u043F\u043E\u0434\u0441\u0432\u0435\u0442\u043A\u043E\u0439 \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441\u0430.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0442\u0438\u043B\u0435\u043C \u0430\u043D\u0438\u043C\u0430\u0446\u0438\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043B\u0438 \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u043B\u0430\u0432\u043D\u0443\u044E \u0430\u043D\u0438\u043C\u0430\u0446\u0438\u044E \u043A\u0443\u0440\u0441\u043E\u0440\u0430.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0442\u0438\u043B\u0435\u043C \u043A\u0443\u0440\u0441\u043E\u0440\u0430.",'\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u043C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u0447\u0438\u0441\u043B\u043E \u0432\u0438\u0434\u0438\u043C\u044B\u0445 \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u044B\u0445 \u0438 \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u0445 \u043B\u0438\u043D\u0438\u0439, \u043E\u043A\u0440\u0443\u0436\u0430\u044E\u0449\u0438\u0445 \u043A\u0443\u0440\u0441\u043E\u0440. \u042D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u0438\u043C\u0435\u0435\u0442 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 "scrollOff" \u0438\u043B\u0438 "scrollOffset" \u0432 \u043D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0434\u0440\u0443\u0433\u0438\u0445 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430\u0445.','"cursorSurroundingLines" \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u0438 \u0437\u0430\u043F\u0443\u0441\u043A\u0435 \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0438\u043B\u0438 API.','"cursorSurroundingLines" \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u0432\u043E \u0432\u0441\u0435\u0445 \u0441\u043B\u0443\u0447\u0430\u044F\u0445.','\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043A\u043E\u0433\u0434\u0430 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0442\u044C "cursorSurroundingLines".',`\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0448\u0438\u0440\u0438\u043D\u043E\u0439 \u043A\u0443\u0440\u0441\u043E\u0440\u0430, \u043A\u043E\u0433\u0434\u0430 \u0434\u043B\u044F \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430 "#editor.cursorStyle#" \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 'line'`,"\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0443 \u0440\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044C \u043F\u0435\u0440\u0435\u043C\u0435\u0449\u0435\u043D\u0438\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0445 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043F\u0435\u0440\u0435\u0442\u0430\u0441\u043A\u0438\u0432\u0430\u043D\u0438\u044F.","\u041A\u043E\u044D\u0444\u0444\u0438\u0446\u0438\u0435\u043D\u0442 \u0443\u0432\u0435\u043B\u0438\u0447\u0435\u043D\u0438\u044F \u0441\u043A\u043E\u0440\u043E\u0441\u0442\u0438 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0438 \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 ALT.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u043B\u0438 \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u043D\u0438\u0435 \u043A\u043E\u0434\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044E \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u043D\u0438\u044F \u0434\u043B\u044F \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u0433\u043E \u044F\u0437\u044B\u043A\u0430, \u0435\u0441\u043B\u0438 \u043E\u043D\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430, \u0432 \u043F\u0440\u043E\u0442\u0438\u0432\u043D\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044E \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u043E\u0442\u0441\u0442\u0443\u043F\u043E\u0432.","\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u044E \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u043D\u0438\u044F \u043D\u0430 \u043E\u0441\u043D\u043E\u0432\u0435 \u043E\u0442\u0441\u0442\u0443\u043F\u043E\u0432.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0442\u0440\u0430\u0442\u0435\u0433\u0438\u0435\u0439 \u0434\u043B\u044F \u0432\u044B\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u0435\u043C\u044B\u0445 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u043E\u0432.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0432\u044B\u0434\u0435\u043B\u044F\u0442\u044C \u0441\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u044B.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0435\u0442 \u043B\u0438 \u0449\u0435\u043B\u0447\u043E\u043A \u043F\u0443\u0441\u0442\u043E\u0433\u043E \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0433\u043E \u043F\u043E\u0441\u043B\u0435 \u0441\u0432\u0435\u0440\u043D\u0443\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0438 \u0440\u0430\u0437\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u0442\u044C \u0435\u0435.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u0441\u0435\u043C\u0435\u0439\u0441\u0442\u0432\u043E \u0448\u0440\u0438\u0444\u0442\u043E\u0432.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0435\u0442 \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u043E\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435. \u041C\u043E\u0434\u0443\u043B\u044C \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0438 \u0438\u043C\u0435\u0442\u044C \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D \u0432 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0435.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u043C, \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u044E\u0449\u0438\u043C, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u043F\u043E\u0441\u043B\u0435 \u0432\u0432\u043E\u0434\u0430.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u043C \u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B\u044C\u043D\u044B\u0445 \u043F\u043E\u043B\u0435\u0439 \u0433\u043B\u0438\u0444\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u041F\u043E\u043B\u044F \u0433\u043B\u0438\u0444\u0430 \u0432 \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u043C \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u044E\u0442\u0441\u044F \u0434\u043B\u044F \u043E\u0442\u043B\u0430\u0434\u043A\u0438.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u043A\u0440\u044B\u0442\u0438\u0435\u043C \u043A\u0443\u0440\u0441\u043E\u0440\u0430 \u0432 \u043E\u0431\u0437\u043E\u0440\u043D\u043E\u0439 \u043B\u0438\u043D\u0435\u0439\u043A\u0435.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u0434\u043E\u043B\u0436\u043D\u0430 \u043B\u0438 \u0432\u044B\u0434\u0435\u043B\u044F\u0442\u044C\u0441\u044F \u0430\u043A\u0442\u0438\u0432\u043D\u0430\u044F \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0430\u044F \u043E\u0442\u0441\u0442\u0443\u043F\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0438\u043D\u0442\u0435\u0440\u0432\u0430\u043B\u043E\u043C \u043C\u0435\u0436\u0434\u0443 \u0431\u0443\u043A\u0432\u0430\u043C\u0438 \u0432 \u043F\u0438\u043A\u0441\u0435\u043B\u044F\u0445.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0430 \u043B\u0438 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u043A\u0430 \u0441\u0432\u044F\u0437\u0430\u043D\u043D\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u0412 \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0438 \u043E\u0442 \u044F\u0437\u044B\u043A\u0430, \u0441\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0442\u0435\u0433\u0438 HTML, \u043E\u0431\u043D\u043E\u0432\u043B\u044F\u044E\u0442\u0441\u044F \u043F\u0440\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0438.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0438 \u0438 \u0434\u0435\u043B\u0430\u0442\u044C \u0438\u0445 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u043C\u0438 \u0434\u043B\u044F \u0449\u0435\u043B\u0447\u043A\u0430.","\u0412\u044B\u0434\u0435\u043B\u044F\u0442\u044C \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0435 \u0441\u043A\u043E\u0431\u043A\u0438.","\u041C\u043D\u043E\u0436\u0438\u0442\u0435\u043B\u044C, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u044B\u0439 \u0434\u043B\u044F \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 deltaX \u0438 deltaY \u0441\u043E\u0431\u044B\u0442\u0438\u0439 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0438 \u043A\u043E\u043B\u0435\u0441\u0438\u043A\u0430 \u043C\u044B\u0448\u0438.","\u0418\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0435 \u0440\u0430\u0437\u043C\u0435\u0440\u0430 \u0448\u0440\u0438\u0444\u0442\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u043E\u0439 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 CTRL \u0438 \u0434\u0432\u0438\u0436\u0435\u043D\u0438\u0438 \u043A\u043E\u043B\u0435\u0441\u0438\u043A\u0430 \u043C\u044B\u0448\u0438.","\u041E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043A\u0443\u0440\u0441\u043E\u0440\u043E\u0432, \u043A\u043E\u0433\u0434\u0430 \u043E\u043D\u0438 \u043F\u0435\u0440\u0435\u043A\u0440\u044B\u0432\u0430\u044E\u0442\u0441\u044F.","\u0421\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 CTRL \u0432 Windows \u0438 Linux \u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 COMMAND \u0432 macOS.","\u0421\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 ALT \u0432 Windows \u0438 Linux \u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 OPTION \u0432 macOS.",'\u041C\u043E\u0434\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0434\u043B\u044F \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u043A\u0443\u0440\u0441\u043E\u0440\u043E\u0432 \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043C\u044B\u0448\u0438. \u0416\u0435\u0441\u0442\u044B \u043C\u044B\u0448\u0438 "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E" \u0438 "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443" \u0431\u0443\u0434\u0443\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u044B \u0442\u0430\u043A, \u0447\u0442\u043E\u0431\u044B \u043E\u043D\u0438 \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u043E\u0432\u0430\u043B\u0438 \u0441 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u043C\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430\u043C\u0438. [\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u044F](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier). ',"\u041A\u0430\u0436\u0434\u044B\u0439 \u043A\u0443\u0440\u0441\u043E\u0440 \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u043E\u0434\u043D\u0443 \u0441\u0442\u0440\u043E\u043A\u0443 \u0442\u0435\u043A\u0441\u0442\u0430.","\u041A\u0430\u0436\u0434\u044B\u0439 \u043A\u0443\u0440\u0441\u043E\u0440 \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u043E\u043B\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0432\u0441\u0442\u0430\u0432\u043A\u043E\u0439, \u043A\u043E\u0433\u0434\u0430 \u0447\u0438\u0441\u043B\u043E \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u043C\u044B\u0445 \u0441\u0442\u0440\u043E\u043A \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0447\u0438\u0441\u043B\u0443 \u043A\u0443\u0440\u0441\u043E\u0440\u043E\u0432.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0432\u044B\u0434\u0435\u043B\u044F\u0442\u044C \u044D\u043A\u0437\u0435\u043C\u043F\u043B\u044F\u0440\u044B \u0441\u0435\u043C\u0430\u043D\u0442\u0438\u0447\u0435\u0441\u043A\u0438\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u0430 \u043B\u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u0433\u0440\u0430\u043D\u0438\u0446\u0430 \u043D\u0430 \u043E\u0431\u0437\u043E\u0440\u043D\u043E\u0439 \u043B\u0438\u043D\u0435\u0439\u043A\u0435.","\u0424\u043E\u043A\u0443\u0441\u0438\u0440\u043E\u0432\u043A\u0430 \u043D\u0430 \u0434\u0435\u0440\u0435\u0432\u0435 \u043F\u0440\u0438 \u043E\u0442\u043A\u0440\u044B\u0442\u0438\u0438 \u043E\u0431\u0437\u043E\u0440\u0430","\u0424\u043E\u043A\u0443\u0441\u0438\u0440\u043E\u0432\u043A\u0430 \u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u0440\u0438 \u043E\u0442\u043A\u0440\u044B\u0442\u0438\u0438 \u043E\u0431\u0437\u043E\u0440\u0430","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043B\u0438 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043D\u0430 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0438\u043B\u0438 \u0434\u0435\u0440\u0435\u0432\u043E \u0432 \u0432\u0438\u0434\u0436\u0435\u0442\u0435 \u043E\u0431\u0437\u043E\u0440\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0432\u0441\u0435\u0433\u0434\u0430 \u043B\u0438 \u0436\u0435\u0441\u0442 \u043C\u044B\u0448\u044C\u044E \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0435\u0442 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0434\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C\u044E \u0437\u0430\u0434\u0435\u0440\u0436\u043A\u0438 (\u0432 \u043C\u0441) \u043F\u0435\u0440\u0435\u0434 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u043C \u043A\u0440\u0430\u0442\u043A\u0438\u0445 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442 \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0435 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435 \u043F\u043E \u0442\u0438\u043F\u0443.",'\u041D\u0435 \u0440\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F; \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0432\u043C\u0435\u0441\u0442\u043E \u044D\u0442\u043E\u0433\u043E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 "editor.linkedEditing".',"\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0438\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0438\u0435 \u043E\u0442\u0441\u0442\u0443\u043F\u0430.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u043D\u043E\u043C\u0435\u0440\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0439 \u0441\u0442\u0440\u043E\u043A\u0438, \u043A\u043E\u0433\u0434\u0430 \u0444\u0430\u0439\u043B \u0437\u0430\u043A\u0430\u043D\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u043D\u043E\u0432\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439.","\u0412\u044B\u0434\u0435\u043B\u044F\u0435\u0442 \u043F\u043E\u043B\u0435 \u0438 \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0432\u044B\u0434\u0435\u043B\u044F\u0442\u044C \u0442\u0435\u043A\u0443\u0449\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043E\u0442\u0440\u0438\u0441\u043E\u0432\u044B\u0432\u0430\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0442\u0440\u043E\u043A\u0438, \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u043E\u0433\u0434\u0430 \u043E\u043D \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0432 \u0444\u043E\u043A\u0443\u0441\u0435","\u041E\u0442\u0440\u0438\u0441\u043E\u0432\u043A\u0430 \u043F\u0440\u043E\u0431\u0435\u043B\u043E\u0432, \u043A\u0440\u043E\u043C\u0435 \u043E\u0434\u0438\u043D\u043E\u0447\u043D\u044B\u0445 \u043F\u0440\u043E\u0431\u0435\u043B\u043E\u0432 \u043C\u0435\u0436\u0434\u0443 \u0441\u043B\u043E\u0432\u0430\u043C\u0438.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u0435\u043B\u044B \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u043E\u043C \u0442\u0435\u043A\u0441\u0442\u0435.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u0435 \u043F\u0440\u043E\u0431\u0435\u043B\u044B","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u043F\u0440\u043E\u0431\u0435\u043B\u044B.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u043B\u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u0441\u043A\u0440\u0443\u0433\u043B\u0435\u043D\u043D\u044B\u0435 \u0443\u0433\u043B\u044B \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E\u043C \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432, \u043D\u0430 \u043A\u043E\u0442\u043E\u0440\u043E\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u0431\u0443\u0434\u0435\u0442 \u043F\u0440\u043E\u043A\u0440\u0443\u0447\u0438\u0432\u0430\u0442\u044C\u0441\u044F \u043F\u043E \u0433\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B\u0438.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0435\u0442 \u043B\u0438 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u043F\u0440\u043E\u043A\u0440\u0443\u0447\u0438\u0432\u0430\u0442\u044C\u0441\u044F \u0437\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044E\u044E \u0441\u0442\u0440\u043E\u043A\u0443.","\u041F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0430 \u0442\u043E\u043B\u044C\u043A\u043E \u0432\u0434\u043E\u043B\u044C \u043E\u0441\u043D\u043E\u0432\u043D\u043E\u0439 \u043E\u0441\u0438 \u043F\u0440\u0438 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0435 \u043F\u043E \u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B\u0438 \u0438 \u0433\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B\u0438 \u043E\u0434\u043D\u043E\u0432\u0440\u0435\u043C\u0435\u043D\u043D\u043E. \u041F\u0440\u0435\u0434\u043E\u0442\u0432\u0440\u0430\u0449\u0430\u0435\u0442 \u0441\u043C\u0435\u0449\u0435\u043D\u0438\u0435 \u043F\u043E \u0433\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B\u0438 \u043F\u0440\u0438 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0435 \u043F\u043E \u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B\u0438 \u043D\u0430 \u0442\u0440\u0435\u043A\u043F\u0430\u0434\u0435.","\u041A\u043E\u043D\u0442\u0440\u043E\u043B\u0438\u0440\u0443\u0435\u0442, \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043B\u0438 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0442\u044C \u043F\u0435\u0440\u0432\u0438\u0447\u043D\u044B\u0439 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430 Linux.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0432\u044B\u0434\u0435\u043B\u044F\u0442\u044C \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u044F, \u0430\u043D\u0430\u043B\u043E\u0433\u0438\u0447\u043D\u044B\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u043C\u0443 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u0443.","\u0412\u0441\u0435\u0433\u0434\u0430 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u0435\u043C\u044B\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F.","\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u043D\u0438\u044F, \u043A\u043E\u0433\u0434\u0430 \u0443\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044C \u043C\u044B\u0448\u0438 \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u043D\u0430\u0434 \u043F\u0435\u0440\u0435\u043F\u043B\u0435\u0442\u043E\u043C.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u043D\u0438\u044F \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u043D\u0430 \u043F\u0435\u0440\u0435\u043F\u043B\u0435\u0442\u0435.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u043A\u0440\u044B\u0442\u0438\u0435\u043C \u043D\u0435\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u043E\u0433\u043E \u043A\u043E\u0434\u0430.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u0435\u0440\u0435\u0447\u0435\u0440\u043A\u0438\u0432\u0430\u043D\u0438\u0435\u043C \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u0445 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0445.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u043E\u0432 \u043F\u043E\u0432\u0435\u0440\u0445 \u0434\u0440\u0443\u0433\u0438\u0445 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u043E\u0432 \u043F\u043E\u0434 \u0434\u0440\u0443\u0433\u0438\u043C\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u043C\u0438.","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u043E\u0432 \u0440\u044F\u0434\u043E\u043C \u0441 \u0434\u0440\u0443\u0433\u0438\u043C\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u043C\u0438.","\u041D\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u043E\u0432.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u043C \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u043E\u0432 \u0432\u043C\u0435\u0441\u0442\u0435 \u0441 \u0434\u0440\u0443\u0433\u0438\u043C\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u043C\u0438 \u0438 \u0438\u0445 \u0441\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u043E\u0439.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0431\u0443\u0434\u0435\u0442 \u043B\u0438 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0430\u043D\u0438\u043C\u0430\u0446\u0438\u044F \u043F\u0440\u0438 \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430",'\u0420\u0430\u0437\u043C\u0435\u0440 \u0448\u0440\u0438\u0444\u0442\u0430 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0441 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u043C\u0438. \u0415\u0441\u043B\u0438 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "0", \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "#editor.fontSize#".','\u0412\u044B\u0441\u043E\u0442\u0430 \u0441\u0442\u0440\u043E\u043A\u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0441 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u043C\u0438. \u0415\u0441\u043B\u0438 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "0", \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "#editor.lineHeight#". \u041C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435\xA0\u2014 8.',"\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0434\u043E\u043B\u0436\u043D\u044B \u043B\u0438 \u043F\u0440\u0438 \u0432\u0432\u043E\u0434\u0435 \u0442\u0440\u0438\u0433\u0433\u0435\u0440\u043D\u044B\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F.","\u0412\u0441\u0435\u0433\u0434\u0430 \u0432\u044B\u0431\u0438\u0440\u0430\u0442\u044C \u043F\u0435\u0440\u0432\u043E\u0435 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435.",'\u0412\u044B\u0431\u043E\u0440 \u043D\u0435\u0434\u0430\u0432\u043D\u0438\u0445 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439, \u0435\u0441\u043B\u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u0430\u043B\u044C\u043D\u0435\u0439\u0448\u0438\u0439 \u0432\u0432\u043E\u0434 \u043D\u0435 \u043F\u0440\u0438\u0432\u043E\u0434\u0438\u0442 \u043A \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044E \u043E\u0434\u043D\u043E\u0433\u043E \u0438\u0437 \u043D\u0438\u0445, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "console.| -> console.log", \u0442\u0430\u043A \u043A\u0430\u043A "log" \u043D\u0435\u0434\u0430\u0432\u043D\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043B\u0441\u044F \u0434\u043B\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F.','\u0412\u044B\u0431\u043E\u0440 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0441 \u0443\u0447\u0435\u0442\u043E\u043C \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u0445 \u043F\u0440\u0435\u0444\u0438\u043A\u0441\u043E\u0432, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u0434\u043B\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u044D\u0442\u0438\u0445 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "co -> console" \u0438 "con -> const".',"\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043F\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0432\u044B\u0431\u043E\u0440\u043E\u043C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u043F\u0440\u0438 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0438 \u0441\u043F\u0438\u0441\u043A\u0430 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u041F\u0440\u0438 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0438 \u0434\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F \u043F\u043E TAB \u0431\u0443\u0434\u0435\u0442 \u0434\u043E\u0431\u0430\u0432\u043B\u044F\u0442\u044C\u0441\u044F \u043D\u0430\u0438\u043B\u0443\u0447\u0448\u0435\u0435 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB.","\u041E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0434\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043F\u043E TAB.",'\u0412\u0441\u0442\u0430\u0432\u043A\u0430 \u0434\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0439 \u043F\u043E TAB \u043F\u0440\u0438 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0438 \u0438\u0445 \u043F\u0440\u0435\u0444\u0438\u043A\u0441\u043E\u0432. \u0424\u0443\u043D\u043A\u0446\u0438\u044F \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043E\u043F\u0442\u0438\u043C\u0430\u043B\u044C\u043D\u043E, \u0435\u0441\u043B\u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 "quickSuggestions" \u043E\u0442\u043A\u043B\u044E\u0447\u0435\u043D.',"\u0412\u043A\u043B\u044E\u0447\u0430\u0435\u0442 \u0434\u043E\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F \u043F\u043E TAB.","\u041D\u0435\u043E\u0431\u044B\u0447\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A\u0438 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0443\u0434\u0430\u043B\u044F\u044E\u0442\u0441\u044F.","\u041D\u0435\u043E\u0431\u044B\u0447\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A\u0438 \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u0443\u044E\u0442\u0441\u044F.","\u0414\u043B\u044F \u043D\u0435\u043E\u0431\u044B\u0447\u043D\u044B\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A\u0438 \u0437\u0430\u043F\u0440\u0430\u0448\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435.","\u0423\u0434\u0430\u043B\u0438\u0442\u0435 \u043D\u0435\u043E\u0431\u044B\u0447\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A\u0438, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u043C\u043E\u0433\u0443\u0442 \u0432\u044B\u0437\u0432\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u044B.","\u0412\u0441\u0442\u0430\u0432\u043A\u0430 \u0438 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u043F\u0440\u043E\u0431\u0435\u043B\u043E\u0432 \u043F\u043E\u0441\u043B\u0435 \u043F\u043E\u0437\u0438\u0446\u0438\u0438 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438","\u0421\u0438\u043C\u0432\u043E\u043B\u044B, \u043A\u043E\u0442\u043E\u0440\u044B\u0435 \u0431\u0443\u0434\u0443\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u043A\u0430\u043A \u0440\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u0435\u043B\u0438 \u0441\u043B\u043E\u0432 \u043F\u0440\u0438 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0438 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438\u043B\u0438 \u0434\u0440\u0443\u0433\u0438\u0445 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0439, \u0441\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0445 \u0441\u043E \u0441\u043B\u043E\u0432\u0430\u043C\u0438.","\u0421\u0442\u0440\u043E\u043A\u0438 \u043D\u0435 \u0431\u0443\u0434\u0443\u0442 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0438\u0442\u044C\u0441\u044F \u043D\u0438\u043A\u043E\u0433\u0434\u0430.","\u0421\u0442\u0440\u043E\u043A\u0438 \u0431\u0443\u0434\u0443\u0442 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0438\u0442\u044C\u0441\u044F \u043F\u043E \u0448\u0438\u0440\u0438\u043D\u0435 \u043E\u043A\u043D\u0430 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430.",'\u0421\u0442\u0440\u043E\u043A\u0438 \u0431\u0443\u0434\u0443\u0442 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0438\u0442\u044C\u0441\u044F \u043F\u043E "#editor.wordWrapColumn#".','\u0421\u0442\u0440\u043E\u043A\u0438 \u0431\u0443\u0434\u0443\u0442 \u043F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u044B \u043F\u043E \u043C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u043C\u0443 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044E \u0438\u0437 \u0434\u0432\u0443\u0445: \u0448\u0438\u0440\u0438\u043D\u0430 \u043E\u043A\u043D\u0430 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u0438 "#editor.wordWrapColumn#".',"\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u043A\u0430\u043A \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0438.",'\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u0441\u0442\u043E\u043B\u0431\u0435\u0446 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430, \u0435\u0441\u043B\u0438 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "#editor.wordWrap#" \u2014 "wordWrapColumn" \u0438\u043B\u0438 "bounded".',"\u0411\u0435\u0437 \u043E\u0442\u0441\u0442\u0443\u043F\u0430. \u041F\u0435\u0440\u0435\u043D\u043E\u0441 \u0441\u0442\u0440\u043E\u043A \u043D\u0430\u0447\u0438\u043D\u0430\u0435\u0442\u0441\u044F \u0441\u043E \u0441\u0442\u043E\u043B\u0431\u0446\u0430 1.","\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 \u043F\u043E\u043B\u0443\u0447\u0430\u0442 \u0442\u043E\u0442 \u0436\u0435 \u043E\u0442\u0441\u0442\u0443\u043F, \u0447\u0442\u043E \u0438 \u0440\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u0441\u043A\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430.","\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 \u043F\u043E\u043B\u0443\u0447\u0430\u0442 \u043E\u0442\u0441\u0442\u0443\u043F, \u0443\u0432\u0435\u043B\u0438\u0447\u0435\u043D\u043D\u044B\u0439 \u043D\u0430 \u0435\u0434\u0438\u043D\u0438\u0446\u0443 \u043F\u043E \u0441\u0440\u0430\u0432\u043D\u0435\u043D\u0438\u044E \u0441 \u0440\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u0441\u043A\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439. ","\u041F\u0435\u0440\u0435\u043D\u0435\u0441\u0435\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 \u043F\u043E\u043B\u0443\u0447\u0430\u0442 \u043E\u0442\u0441\u0442\u0443\u043F, \u0443\u0432\u0435\u043B\u0438\u0447\u0435\u043D\u043D\u044B\u0439 \u043D\u0430 \u0434\u0432\u0430 \u043F\u043E \u0441\u0440\u0430\u0432\u043D\u0435\u043D\u0438\u044E \u0441 \u0440\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u0441\u043A\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u043E\u0442\u0441\u0442\u0443\u043F\u043E\u043C \u0441\u0442\u0440\u043E\u043A \u0441 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u043E\u043C \u043F\u043E \u0441\u043B\u043E\u0432\u0430\u043C.","\u041F\u0440\u0435\u0434\u043F\u043E\u043B\u0430\u0433\u0430\u0435\u0442, \u0447\u0442\u043E \u0432\u0441\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0438\u043C\u0435\u044E\u0442 \u043E\u0434\u0438\u043D\u0430\u043A\u043E\u0432\u0443\u044E \u0448\u0438\u0440\u0438\u043D\u0443. \u042D\u0442\u043E \u0431\u044B\u0441\u0442\u0440\u044B\u0439 \u0430\u043B\u0433\u043E\u0440\u0438\u0442\u043C, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E \u0434\u043B\u044F \u043C\u043E\u043D\u043E\u0448\u0438\u0440\u0438\u043D\u043D\u044B\u0445 \u0448\u0440\u0438\u0444\u0442\u043E\u0432 \u0438 \u043D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0441\u043A\u0440\u0438\u043F\u0442\u043E\u0432 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u043B\u0430\u0442\u0438\u043D\u0441\u043A\u0438\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432), \u0433\u0434\u0435 \u0433\u043B\u0438\u0444\u044B \u0438\u043C\u0435\u044E\u0442 \u043E\u0434\u0438\u043D\u0430\u043A\u043E\u0432\u0443\u044E \u0448\u0438\u0440\u0438\u043D\u0443.","\u0414\u0435\u043B\u0435\u0433\u0438\u0440\u0443\u0435\u0442 \u0432\u044B\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u0435 \u0442\u043E\u0447\u0435\u043A \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0430 \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0443. \u042D\u0442\u043E \u043C\u0435\u0434\u043B\u0435\u043D\u043D\u044B\u0439 \u0430\u043B\u0433\u043E\u0440\u0438\u0442\u043C, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u043C\u043E\u0436\u0435\u0442 \u043F\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u043A \u0437\u0430\u0432\u0438\u0441\u0430\u043D\u0438\u044F\u043C \u043F\u0440\u0438 \u043E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0435 \u0431\u043E\u043B\u044C\u0448\u0438\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u043D\u043E \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u043E \u0432\u043E \u0432\u0441\u0435\u0445 \u0441\u043B\u0443\u0447\u0430\u044F\u0445.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0430\u043B\u0433\u043E\u0440\u0438\u0442\u043C\u043E\u043C, \u0432\u044B\u0447\u0438\u0441\u043B\u044F\u044E\u0449\u0438\u043C \u0442\u043E\u0447\u043A\u0438 \u043F\u0435\u0440\u0435\u043D\u043E\u0441\u0430."],"vs/editor/common/editorContextKeys":["\u041D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u043B\u0438 \u0444\u043E\u043A\u0443\u0441 \u043D\u0430 \u0442\u0435\u043A\u0441\u0442\u0435 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 (\u043A\u0443\u0440\u0441\u043E\u0440 \u043C\u0438\u0433\u0430\u0435\u0442)","\u041D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u043B\u0438 \u0444\u043E\u043A\u0443\u0441 \u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0438\u043B\u0438 \u043D\u0430 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0444\u043E\u043A\u0443\u0441 \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u043D\u0430 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u043E\u0438\u0441\u043A\u0430)","\u041D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u043B\u0438 \u0444\u043E\u043A\u0443\u0441 \u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0438\u043B\u0438 \u043D\u0430 \u043F\u043E\u043B\u0435 \u0432\u0432\u043E\u0434\u0430 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u043E\u0433\u043E \u0442\u0435\u043A\u0441\u0442\u0430 (\u043A\u0443\u0440\u0441\u043E\u0440 \u043C\u0438\u0433\u0430\u0435\u0442)","\u0414\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0447\u0442\u0435\u043D\u0438\u044F","\u042F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043B\u0438 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u043E\u043C \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439",'\u0412\u043A\u043B\u044E\u0447\u0435\u043D \u043B\u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 "editor.columnSelection"',"\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043C\u043D\u043E\u0436\u0435\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u0439 \u0432\u044B\u0431\u043E\u0440","\u041F\u0435\u0440\u0435\u043C\u0435\u0449\u0430\u0435\u0442\u0441\u044F \u043B\u0438 \u0444\u043E\u043A\u0443\u0441 \u0441 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB","\u042F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043B\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0435 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0432\u0438\u0434\u0438\u043C\u044B\u043C","\u042F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0447\u0430\u0441\u0442\u044C\u044E \u0431\u043E\u043B\u044C\u0448\u0435\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0437\u0430\u043F\u0438\u0441\u043D\u044B\u0445 \u043A\u043D\u0438\u0436\u0435\u043A)","\u0418\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u044F\u0437\u044B\u043A\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0441 \u043A\u043E\u0434\u043E\u043C","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A CodeLens","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0439","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u0439","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0439 \u0442\u0438\u043F\u043E\u0432","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u044F","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0441\u0441\u044B\u043B\u043E\u043A","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u044F","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0441\u043F\u0440\u0430\u0432\u043A\u0438 \u043F\u043E \u0441\u0438\u0433\u043D\u0430\u0442\u0443\u0440\u0430\u043C","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0445 \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043E\u043A","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A\u043E\u0432 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432","\u0415\u0441\u0442\u044C \u043B\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A\u043E\u0432 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u043E\u0432"],"vs/editor/common/model/editStack":["\u0412\u0432\u043E\u0434"],"vs/editor/common/modes/modesRegistry":["\u041F\u0440\u043E\u0441\u0442\u043E\u0439 \u0442\u0435\u043A\u0441\u0442"],"vs/editor/common/standaloneStrings":["\u041D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u043E","\u0421\u0442\u0440\u043E\u043A\u0430 {0}, \u0441\u0442\u043E\u043B\u0431\u0435\u0446 {1} (\u0432\u044B\u0431\u0440\u0430\u043D\u043E: {2})","\u0421\u0442\u0440\u043E\u043A\u0430 {0}, \u0441\u0442\u043E\u043B\u0431\u0435\u0446 {1}","\u0412\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0439: {0} (\u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043E \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432: {1})","\u0412\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0439: {0}",'\u0422\u0435\u043F\u0435\u0440\u044C \u0434\u043B\u044F \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430 "accessibilitySupport" \u0443\u0441\u0442\u0430\u043D\u0430\u0432\u043B\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "\u0432\u043A\u043B".',"\u041E\u0442\u043A\u0440\u044B\u0432\u0430\u0435\u0442\u0441\u044F \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u0430\u0446\u0438\u0438 \u043E \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u044B\u0445 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044F\u0445 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0432 \u043F\u0430\u043D\u0435\u043B\u0438 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0447\u0442\u0435\u043D\u0438\u044F \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.","\u043D\u0430 \u043F\u0430\u043D\u0435\u043B\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439."," \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043A\u043E\u0434\u0430 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0447\u0442\u0435\u043D\u0438\u044F"," \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u043A\u043E\u0434\u0430","\u0427\u0442\u043E\u0431\u044B \u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0434\u043B\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u0441\u043E \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043E\u043C \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430, \u043D\u0430\u0436\u043C\u0438\u0442\u0435 COMMAND+E.","\u0427\u0442\u043E\u0431\u044B \u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0434\u043B\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u0441\u043E \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043E\u043C \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430, \u043D\u0430\u0436\u043C\u0438\u0442\u0435 CTRL+E.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u0434\u043B\u044F \u043E\u043F\u0442\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0439 \u0440\u0430\u0431\u043E\u0442\u044B \u0441\u043E \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043E\u043C \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430.","\u0420\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043D \u0431\u0435\u0437 \u043E\u043F\u0442\u0438\u043C\u0438\u0437\u0430\u0446\u0438\u0438 \u0434\u043B\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u044F \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0447\u0442\u0435\u043D\u0438\u044F \u0441 \u044D\u043A\u0440\u0430\u043D\u0430, \u0447\u0442\u043E \u043D\u0435 \u043F\u043E\u0434\u0445\u043E\u0434\u0438\u0442 \u0432 \u0434\u0430\u043D\u043D\u043E\u0439 \u0441\u0438\u0442\u0443\u0430\u0446\u0438\u0438.","\u041F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB \u0432 \u0442\u0435\u043A\u0443\u0449\u0435\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0444\u043E\u043A\u0443\u0441 \u0432\u0432\u043E\u0434\u0430 \u043F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u044D\u043B\u0435\u043C\u0435\u043D\u0442, \u0441\u043F\u043E\u0441\u043E\u0431\u043D\u044B\u0439 \u0435\u0433\u043E \u043F\u0440\u0438\u043D\u044F\u0442\u044C. \u0427\u0442\u043E\u0431\u044B \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u044D\u0442\u043E \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435, \u043D\u0430\u0436\u043C\u0438\u0442\u0435 \u043A\u043B\u0430\u0432\u0438\u0448\u0443 {0}.","\u041F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB \u0432 \u0442\u0435\u043A\u0443\u0449\u0435\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0444\u043E\u043A\u0443\u0441 \u0432\u0432\u043E\u0434\u0430 \u043F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u0441\u044F \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u044D\u043B\u0435\u043C\u0435\u043D\u0442, \u0441\u043F\u043E\u0441\u043E\u0431\u043D\u044B\u0439 \u0435\u0433\u043E \u043F\u0440\u0438\u043D\u044F\u0442\u044C. \u041A\u043E\u043C\u0430\u043D\u0434\u0443 {0} \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043C\u043E\u0433\u043E \u0441\u043E\u0447\u0435\u0442\u0430\u043D\u0438\u044F \u043A\u043B\u0430\u0432\u0438\u0448.","\u041F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB \u0432 \u0442\u0435\u043A\u0443\u0449\u0435\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u0441\u0438\u043C\u0432\u043E\u043B \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438. \u0427\u0442\u043E\u0431\u044B \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u044D\u0442\u043E \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435, \u043D\u0430\u0436\u043C\u0438\u0442\u0435 \u043A\u043B\u0430\u0432\u0438\u0448\u0443 {0}.","\u041F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB \u0432 \u0442\u0435\u043A\u0443\u0449\u0435\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0431\u0443\u0434\u0435\u0442 \u0432\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u0441\u0438\u043C\u0432\u043E\u043B \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438. \u041A\u043E\u043C\u0430\u043D\u0434\u0443 {0} \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u043C\u043E\u0433\u043E \u0441\u043E\u0447\u0435\u0442\u0430\u043D\u0438\u044F \u043A\u043B\u0430\u0432\u0438\u0448.","\u041D\u0430\u0436\u043C\u0438\u0442\u0435 COMMAND+H, \u0447\u0442\u043E\u0431\u044B \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u043E\u043A\u043D\u043E \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u0441 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0439 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u0435\u0439 \u043E \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u044B\u0445 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044F\u0445 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u041D\u0430\u0436\u043C\u0438\u0442\u0435 CTRL+H, \u0447\u0442\u043E\u0431\u044B \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u043E\u043A\u043D\u043E \u0431\u0440\u0430\u0443\u0437\u0435\u0440\u0430 \u0441 \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0439 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u0435\u0439 \u043E \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u044B\u0445 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044F\u0445 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0437\u0430\u043A\u0440\u044B\u0442\u044C \u044D\u0442\u0443 \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0443 \u0438 \u0432\u0435\u0440\u043D\u0443\u0442\u044C\u0441\u044F \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440, \u043D\u0430\u0436\u0430\u0432 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 ESCAPE \u0438\u043B\u0438 SHIFT+ESCAPE.","\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0441\u043F\u0440\u0430\u0432\u043A\u0443 \u043F\u043E \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u044B\u043C \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044F\u043C","\u0420\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A: \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u0442\u043E\u043A\u0435\u043D\u044B","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0442\u0440\u043E\u043A\u0435/\u0441\u0442\u043E\u043B\u0431\u0446\u0443...","\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0432\u0441\u0435\u0445 \u043F\u043E\u0441\u0442\u0430\u0432\u0449\u0438\u043A\u043E\u0432 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0434\u043E\u0441\u0442\u0443\u043F\u0430","\u041F\u0430\u043B\u0438\u0442\u0440\u0430 \u043A\u043E\u043C\u0430\u043D\u0434","\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0438 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u044B","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0438\u043C\u0432\u043E\u043B\u0443...","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0438\u043C\u0432\u043E\u043B\u0443 \u043F\u043E \u043A\u0430\u0442\u0435\u0433\u043E\u0440\u0438\u044F\u043C...","\u0421\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u041D\u0430\u0436\u043C\u0438\u0442\u0435 ALT+F1 \u0434\u043B\u044F \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430\u043C \u0441\u043F\u0435\u0446\u0438\u0430\u043B\u044C\u043D\u044B\u0445 \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u0435\u0439.","\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0432\u044B\u0441\u043E\u043A\u043E\u043A\u043E\u043D\u0442\u0440\u0430\u0441\u0442\u043D\u0443\u044E \u0442\u0435\u043C\u0443","\u0412\u043D\u0435\u0441\u0435\u043D\u043E \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u0432 \u0444\u0430\u0439\u043B\u0430\u0445 ({1}): {0}."],"vs/editor/common/view/editorColorRegistry":["\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A\u0438 \u0432 \u043F\u043E\u0437\u0438\u0446\u0438\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0433\u0440\u0430\u043D\u0438\u0446 \u0432\u043E\u043A\u0440\u0443\u0433 \u0441\u0442\u0440\u043E\u043A\u0438 \u0432 \u043F\u043E\u0437\u0438\u0446\u0438\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0445 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u043E\u0432, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \u043F\u0440\u0438 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0438 \u0444\u0443\u043D\u043A\u0446\u0438\u0439 Quick Open \u0438\u043B\u0438 \u043F\u043E\u0438\u0441\u043A\u0430. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043E\u0431\u0432\u043E\u0434\u043A\u0438 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F.",'\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u0441\u0438\u043C\u0432\u043E\u043B\u0430, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0432 \u0444\u0443\u043D\u043A\u0446\u0438\u044F\u0445 "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E" \u0438\u043B\u0438 "\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C\u0443/\u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u043C\u0443 \u0441\u0438\u043C\u0432\u043E\u043B\u0443". \u0426\u0432\u0435\u0442 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0432\u0430\u0442\u044C \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u0435 \u0442\u0435\u043A\u0441\u0442\u0430 \u043F\u043E\u0434 \u043D\u0438\u043C.',"\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0432\u043E\u043A\u0440\u0443\u0433 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432.","\u0426\u0432\u0435\u0442 \u043A\u0443\u0440\u0441\u043E\u0440\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043A\u0443\u0440\u0441\u043E\u0440\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430. \u041F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u043D\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0442\u044C \u0446\u0432\u0435\u0442 \u0441\u0438\u043C\u0432\u043E\u043B\u0430, \u043F\u0435\u0440\u0435\u043A\u0440\u044B\u0432\u0430\u0435\u043C\u043E\u0433\u043E \u043F\u0440\u044F\u043C\u043E\u0443\u0433\u043E\u043B\u044C\u043D\u044B\u043C \u043A\u0443\u0440\u0441\u043E\u0440\u043E\u043C.","\u0426\u0432\u0435\u0442 \u043F\u0440\u043E\u0431\u0435\u043B\u043E\u0432 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0438\u0445 \u0434\u043B\u044F \u043E\u0442\u0441\u0442\u0443\u043F\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0445 \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0438\u0445 \u0434\u043B\u044F \u043E\u0442\u0441\u0442\u0443\u043F\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043D\u043E\u043C\u0435\u0440\u043E\u0432 \u0441\u0442\u0440\u043E\u043A \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043D\u043E\u043C\u0435\u0440\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 'Id' \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0443\u0441\u0442\u0430\u0440\u0435\u0432\u0448\u0438\u043C. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0432\u043C\u0435\u0441\u0442\u043E \u043D\u0435\u0433\u043E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 'editorLineNumber.activeForeground'.","\u0426\u0432\u0435\u0442 \u043D\u043E\u043C\u0435\u0440\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u0426\u0432\u0435\u0442 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 CodeLens \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u0430\u0440\u043D\u044B\u0445 \u0441\u043A\u043E\u0431\u043E\u043A","\u0426\u0432\u0435\u0442 \u043F\u0440\u044F\u043C\u043E\u0443\u0433\u043E\u043B\u044C\u043D\u0438\u043A\u043E\u0432 \u043F\u0430\u0440\u043D\u044B\u0445 \u0441\u043A\u043E\u0431\u043E\u043A","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043B\u044F \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0432 \u043E\u043A\u043D\u0435 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043E\u0431\u0437\u043E\u0440\u043D\u043E\u0439 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F, \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u0430 \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u0430 \u0438 \u0440\u0430\u0437\u043C\u0435\u0449\u0435\u043D\u0430 \u0432 \u043F\u0440\u0430\u0432\u043E\u0439 \u0447\u0430\u0441\u0442\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u043E\u043B\u044F \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u0412 \u043F\u043E\u043B\u0435 \u0440\u0430\u0437\u043C\u0435\u0449\u0430\u044E\u0442\u0441\u044F \u043E\u0442\u0441\u0442\u0443\u043F\u044B \u0433\u043B\u0438\u0444\u043E\u0432 \u0438 \u043D\u043E\u043C\u0435\u0440\u0430 \u0441\u0442\u0440\u043E\u043A.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043B\u044F \u043D\u0435\u043D\u0443\u0436\u043D\u043E\u0433\u043E (\u043D\u0435\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u043E\u0433\u043E) \u0438\u0441\u0445\u043E\u0434\u043D\u043E\u0433\u043E \u043A\u043E\u0434\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.",'\u041D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u043E\u0441\u0442\u044C \u043D\u0435\u043D\u0443\u0436\u043D\u043E\u0433\u043E (\u043D\u0435\u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u043E\u0433\u043E) \u0438\u0441\u0445\u043E\u0434\u043D\u043E\u0433\u043E \u043A\u043E\u0434\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, "#000000c0" \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442 \u043A\u043E\u0434 \u0441 \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u043E\u0441\u0442\u044C\u044E 75 %. \u0412 \u0432\u044B\u0441\u043E\u043A\u043E\u043A\u043E\u043D\u0442\u0440\u0430\u0441\u0442\u043D\u044B\u0445 \u0442\u0435\u043C\u0430\u0445 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043D\u0435\u043D\u0443\u0436\u043D\u043E\u0433\u043E \u043A\u043E\u0434\u0430 \u0432\u043C\u0435\u0441\u0442\u043E \u0437\u0430\u0442\u0435\u043D\u0435\u043D\u0438\u044F \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0446\u0432\u0435\u0442 \u0442\u0435\u043C\u044B "editorUnnecessaryCode.border".',"\u0426\u0432\u0435\u0442 \u043C\u0430\u0440\u043A\u0435\u0440\u0430 \u043E\u0431\u0437\u043E\u0440\u043D\u043E\u0439 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u043E\u0432. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u043C\u0435\u0442\u043A\u0438 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0432 \u043E\u043A\u043D\u0435 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u0434\u043B\u044F \u043E\u0448\u0438\u0431\u043E\u043A.","\u0426\u0432\u0435\u0442 \u043C\u0435\u0442\u043A\u0438 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0432 \u043E\u043A\u043D\u0435 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043C\u0435\u0442\u043A\u0438 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0432 \u043E\u043A\u043D\u0435 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u0434\u043B\u044F \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0445 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439."],"vs/editor/contrib/anchorSelect/anchorSelect":["\u041D\u0430\u0447\u0430\u043B\u044C\u043D\u0430\u044F \u0442\u043E\u0447\u043A\u0430 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F","\u041D\u0430\u0447\u0430\u043B\u044C\u043D\u0430\u044F \u0442\u043E\u0447\u043A\u0430 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u0430 \u0432 {0}:{1}","\u0423\u0441\u0442\u0430\u043D\u043E\u0432\u0438\u0442\u044C \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u0443\u044E \u0442\u043E\u0447\u043A\u0443 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F","\u0412\u044B\u0434\u0435\u043B\u0438\u0442\u044C \u0442\u0435\u043A\u0441\u0442 \u043E\u0442 \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u043E\u0439 \u0442\u043E\u0447\u043A\u0438 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0434\u043E \u043A\u0443\u0440\u0441\u043E\u0440\u0430","\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u043D\u0430\u0447\u0430\u043B\u044C\u043D\u0443\u044E \u0442\u043E\u0447\u043A\u0443 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F"],"vs/editor/contrib/bracketMatching/bracketMatching":["\u0426\u0432\u0435\u0442 \u043C\u0435\u0442\u043A\u0438 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0432 \u043E\u043A\u043D\u0435 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u0434\u043B\u044F \u043F\u0430\u0440 \u0441\u043A\u043E\u0431\u043E\u043A.","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u043A\u043E\u0431\u043A\u0435","\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0441\u043A\u043E\u0431\u043A\u0443","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A &&\u0441\u043A\u043E\u0431\u043A\u0435"],"vs/editor/contrib/caretOperations/caretOperations":["\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u0432\u043B\u0435\u0432\u043E","\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u0432\u043F\u0440\u0430\u0432\u043E"],"vs/editor/contrib/caretOperations/transpose":["\u0422\u0440\u0430\u043D\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0431\u0443\u043A\u0432\u044B"],"vs/editor/contrib/clipboard/clipboard":["&&\u0412\u044B\u0440\u0435\u0437\u0430\u0442\u044C","\u0412\u044B\u0440\u0435\u0437\u0430\u0442\u044C","\u0412\u044B\u0440\u0435\u0437\u0430\u0442\u044C","&&\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C","\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435","\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435","&&\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C","\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435\u043C \u0441\u0438\u043D\u0442\u0430\u043A\u0441\u0438\u0441\u0430"],"vs/editor/contrib/codeAction/codeActionCommands":["\u0422\u0438\u043F \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0435\u043C\u043E\u0433\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u043E\u0434\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043A\u043E\u0433\u0434\u0430 \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u044E\u0442\u0441\u044F \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u043D\u044B\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F.","\u0412\u0441\u0435\u0433\u0434\u0430 \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0442\u044C \u043F\u0435\u0440\u0432\u043E\u0435 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u043D\u043E\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u043A\u043E\u0434\u0430.","\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0435\u0440\u0432\u043E\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u043D\u043E\u0433\u043E \u043A\u043E\u0434\u0430, \u0435\u0441\u043B\u0438 \u043E\u043D\u043E \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0435\u0434\u0438\u043D\u0441\u0442\u0432\u0435\u043D\u043D\u044B\u043C.","\u041D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0435\u043D\u043D\u043E\u0433\u043E \u043A\u043E\u0434\u0430.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u0441\u043B\u0435\u0434\u0443\u0435\u0442 \u043B\u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u043E\u0434\u0430.","\u041F\u0440\u0438 \u043F\u0440\u0438\u043C\u0435\u043D\u0435\u043D\u0438\u0438 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u043E\u0434\u0430 \u043F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430","\u0411\u044B\u0441\u0442\u0440\u043E\u0435 \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435...","\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u043E\u0434\u0430 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442",'\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043A\u043E\u0434\u0430 \u0434\u043B\u044F "{0}".','\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u043E\u0434\u0430 \u0434\u043B\u044F "{0}" \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B',"\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u043A\u043E\u0434\u0430","\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u043E\u0434\u0430 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442","\u0420\u0435\u0444\u0430\u043A\u0442\u043E\u0440\u0438\u043D\u0433...",'\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0440\u0435\u0444\u0430\u043A\u0442\u043E\u0440\u0438\u043D\u0433\u043E\u0432 \u0434\u043B\u044F "{0}"','\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0433\u043E \u0440\u0435\u0444\u0430\u043A\u0442\u043E\u0440\u0438\u043D\u0433\u0430 \u0434\u043B\u044F "{0}"',"\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0440\u0435\u0444\u0430\u043A\u0442\u043E\u0440\u0438\u043D\u0433\u043E\u0432","\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 \u0440\u0435\u0444\u0430\u043A\u0442\u043E\u0440\u0438\u043D\u0433\u0430 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442","\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0441 \u0438\u0441\u0445\u043E\u0434\u043D\u044B\u043C \u043A\u043E\u0434\u043E\u043C...","\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430 \u0434\u043B\u044F '{0}'",'\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u0438\u0441\u0445\u043E\u0434\u043D\u044B\u0445 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0434\u043B\u044F "{0}"',"\u041F\u0440\u0435\u0434\u043F\u043E\u0447\u0442\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A\u0430 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B","\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0435 \u0438\u0441\u0445\u043E\u0434\u043D\u044B\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442","\u041E\u0440\u0433\u0430\u043D\u0438\u0437\u0430\u0446\u0438\u044F \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432","\u0414\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0434\u043B\u044F \u0443\u043F\u043E\u0440\u044F\u0434\u043E\u0447\u0435\u043D\u0438\u044F \u0438\u043C\u043F\u043E\u0440\u0442\u043E\u0432 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442","\u0418\u0441\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u0432\u0441\u0435","\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u043E\u0433\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043F\u043E \u043E\u0431\u0449\u0435\u043C\u0443 \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044E","\u0410\u0432\u0442\u043E\u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435...","\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B\u0445 \u0430\u0432\u0442\u043E\u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439"],"vs/editor/contrib/codeAction/lightBulbWidget":["\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439. \u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u043F\u0440\u0435\u0434\u043F\u043E\u0447\u0438\u0442\u0430\u0435\u043C\u043E\u0435 \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435 ({0})","\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F ({0})","\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F"],"vs/editor/contrib/codelens/codelensController":["\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u044B CodeLens \u0434\u043B\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0442\u0440\u043E\u043A\u0438"],"vs/editor/contrib/comment/comment":["\u0417\u0430\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438\u043B\u0438 \u0440\u0430\u0441\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443","\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 &&\u0441\u0442\u0440\u043E\u043A\u0438","\u0417\u0430\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443","\u0420\u0430\u0441\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443","\u0417\u0430\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0438\u043B\u0438 \u0440\u0430\u0441\u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0431\u043B\u043E\u043A","\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0439 &&\u0431\u043B\u043E\u043A\u0430"],"vs/editor/contrib/contextmenu/contextmenu":["\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u043D\u043E\u0435 \u043C\u0435\u043D\u044E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430"],"vs/editor/contrib/cursorUndo/cursorUndo":["\u041E\u0442\u043C\u0435\u043D\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u0443\u0440\u0441\u043E\u0440\u0430","\u041F\u043E\u0432\u0442\u043E\u0440 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u043A\u0443\u0440\u0441\u043E\u0440\u0430"],"vs/editor/contrib/find/findController":["\u041D\u0430\u0439\u0442\u0438","&&\u041D\u0430\u0439\u0442\u0438","\u041D\u0430\u0439\u0442\u0438 \u0432 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u043C","\u041D\u0430\u0439\u0442\u0438 \u0434\u0430\u043B\u0435\u0435","\u041D\u0430\u0439\u0442\u0438 \u0434\u0430\u043B\u0435\u0435","\u041D\u0430\u0439\u0442\u0438 \u0440\u0430\u043D\u0435\u0435","\u041D\u0430\u0439\u0442\u0438 \u0440\u0430\u043D\u0435\u0435","\u041D\u0430\u0439\u0442\u0438 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435","\u041D\u0430\u0439\u0442\u0438 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435","\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C","&&\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C"],"vs/editor/contrib/find/findWidget":['\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043A\u043D\u043E\u043F\u043A\u0438 "\u041D\u0430\u0439\u0442\u0438 \u0432 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u043C" \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.',"\u0417\u043D\u0430\u0447\u043E\u043A, \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u044E\u0449\u0438\u0439, \u0447\u0442\u043E \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0441\u0432\u0435\u0440\u043D\u0443\u0442\u043E.","\u0417\u043D\u0430\u0447\u043E\u043A, \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u044E\u0449\u0438\u0439, \u0447\u0442\u043E \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u043E.",'\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043A\u043D\u043E\u043F\u043A\u0438 "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C" \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.','\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043A\u043D\u043E\u043F\u043A\u0438 "\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0435" \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.','\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043A\u043D\u043E\u043F\u043A\u0438 "\u041D\u0430\u0439\u0442\u0438 \u0440\u0430\u043D\u0435\u0435" \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.','\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043A\u043D\u043E\u043F\u043A\u0438 "\u041D\u0430\u0439\u0442\u0438 \u0434\u0430\u043B\u0435\u0435" \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.',"\u041D\u0430\u0439\u0442\u0438","\u041D\u0430\u0439\u0442\u0438","\u041F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0435 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435","\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0435 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u0435","\u041D\u0430\u0439\u0442\u0438 \u0432 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0438","\u0417\u0430\u043A\u0440\u044B\u0442\u044C","\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C","\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C","\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C","\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0435",'\u0420\u0435\u0436\u0438\u043C "\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u043C\u0435\u043D\u044B"',"\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0435\u0440\u0432\u044B\u0435 {0} \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432, \u043D\u043E \u0432\u0441\u0435 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 \u043F\u043E\u0438\u0441\u043A\u0430 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u044E\u0442\u0441\u044F \u0441\u043E \u0432\u0441\u0435\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C.","{0} \u0438\u0437 {1}","\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442","{0} \u043E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D\u043E",'{0} \u043D\u0430\u0439\u0434\u0435\u043D \u0434\u043B\u044F "{1}"','{0} \u043D\u0430\u0439\u0434\u0435\u043D \u0434\u043B\u044F "{1}", \u0432 {2}','{0} \u043D\u0430\u0439\u0434\u0435\u043D \u0434\u043B\u044F "{1}"',"\u0422\u0435\u043F\u0435\u0440\u044C \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448 CTRL+\u0412\u0412\u041E\u0414 \u0432\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0441\u0438\u043C\u0432\u043E\u043B \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430 \u043D\u0430 \u043D\u043E\u0432\u0443\u044E \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u043C\u0435\u0441\u0442\u043E \u0437\u0430\u043C\u0435\u043D\u044B \u0432\u0441\u0435\u0433\u043E \u0442\u0435\u043A\u0441\u0442\u0430. \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u043E\u0447\u0435\u0442\u0430\u043D\u0438\u0435 \u043A\u043B\u0430\u0432\u0438\u0448 editor.action.replaceAll, \u0447\u0442\u043E\u0431\u044B \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u044C \u044D\u0442\u043E \u043F\u043E\u0432\u0435\u0434\u0435\u043D\u0438\u0435."],"vs/editor/contrib/folding/folding":["\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C","\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0432\u043D\u043E","\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C","\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u043D\u0438\u0435","\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0440\u0435\u043A\u0443\u0440\u0441\u0438\u0432\u043D\u043E","\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0432\u0441\u0435 \u0431\u043B\u043E\u043A\u0438 \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0435\u0432","\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0432\u0441\u0435 \u0440\u0435\u0433\u0438\u043E\u043D\u044B","\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0432\u0441\u0435 \u0440\u0435\u0433\u0438\u043E\u043D\u044B","\u0421\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0432\u0441\u0435","\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0432\u0441\u0435","\u0423\u0440\u043E\u0432\u0435\u043D\u044C \u043F\u0430\u043F\u043A\u0438 {0}","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0437\u0430 \u0441\u0432\u0435\u0440\u043D\u0443\u0442\u044B\u043C\u0438 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0430\u043C\u0438. \u042D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0432\u0430\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u0434\u0435\u043A\u043E\u0440\u0430\u0442\u0438\u0432\u043D\u044B\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B.","\u0426\u0432\u0435\u0442 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 \u0443\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0441\u0432\u0435\u0440\u0442\u044B\u0432\u0430\u043D\u0438\u0435\u043C \u0432\u043E \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0435\u043C \u043F\u043E\u043B\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430."],"vs/editor/contrib/folding/foldingDecorations":["\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u0440\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044B\u0445 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u043E\u0432 \u043D\u0430 \u043F\u043E\u043B\u0435 \u0433\u043B\u0438\u0444\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u0441\u0432\u0435\u0440\u043D\u0443\u0442\u044B\u0445 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u043E\u0432 \u043D\u0430 \u043F\u043E\u043B\u0435 \u0433\u043B\u0438\u0444\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430."],"vs/editor/contrib/fontZoom/fontZoom":["\u0423\u0432\u0435\u043B\u0438\u0447\u0438\u0442\u044C \u0448\u0440\u0438\u0444\u0442 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u0423\u043C\u0435\u043D\u044C\u0448\u0438\u0442\u044C \u0448\u0440\u0438\u0444\u0442 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C \u043C\u0430\u0441\u0448\u0442\u0430\u0431 \u0448\u0440\u0438\u0444\u0442\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430"],"vs/editor/contrib/format/format":["\u0412\u043D\u0435\u0441\u0435\u043D\u0430 \u043E\u0434\u043D\u0430 \u043F\u0440\u0430\u0432\u043A\u0430 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u0432 \u0441\u0442\u0440\u043E\u043A\u0435 {0}.","\u0412\u043D\u0435\u0441\u0435\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F ({0}) \u0432 \u0441\u0442\u0440\u043E\u043A\u0435 {1}.","\u0412\u043D\u0435\u0441\u0435\u043D\u0430 \u043E\u0434\u043D\u0430 \u043F\u0440\u0430\u0432\u043A\u0430 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F \u043C\u0435\u0436\u0434\u0443 \u0441\u0442\u0440\u043E\u043A\u0430\u043C\u0438 {0} \u0438 {1}.","\u0412\u043D\u0435\u0441\u0435\u043D\u044B \u043F\u0440\u0430\u0432\u043A\u0438 \u0444\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F ({0}) \u043C\u0435\u0436\u0434\u0443 \u0441\u0442\u0440\u043E\u043A\u0430\u043C\u0438 {1} \u0438 {2}."],"vs/editor/contrib/format/formatActions":["\u0424\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442","\u0424\u043E\u0440\u043C\u0430\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0439 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442"],"vs/editor/contrib/gotoError/gotoError":["\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0439 \u041F\u0440\u043E\u0431\u043B\u0435\u043C\u0435 (\u041E\u0448\u0438\u0431\u043A\u0435, \u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044E, \u0418\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u0438)","\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430 \u043A \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C\u0443 \u043C\u0430\u0440\u043A\u0435\u0440\u0443.","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u041F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0439 \u041F\u0440\u043E\u0431\u043B\u0435\u043C\u0435 (\u041E\u0448\u0438\u0431\u043A\u0435, \u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044E, \u0418\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u0438)","\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430 \u043A \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u043C\u0443 \u043C\u0430\u0440\u043A\u0435\u0440\u0443.","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0439 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0435 \u0432 \u0444\u0430\u0439\u043B\u0430\u0445 (\u043E\u0448\u0438\u0431\u043A\u0438, \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F, \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F)","\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0430\u044F &&\u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0430","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0439 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0435 \u0432 \u0444\u0430\u0439\u043B\u0430\u0445 (\u043E\u0448\u0438\u0431\u043A\u0438, \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F, \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F)","\u041F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0430\u044F &&\u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0430"],"vs/editor/contrib/gotoError/gotoErrorWidget":["\u041E\u0448\u0438\u0431\u043A\u0430","\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435","\u0418\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F","\u0423\u043A\u0430\u0437\u0430\u043D\u0438\u0435","{0} \u0432 {1}. ","\u041F\u0440\u043E\u0431\u043B\u0435\u043C\u044B: {0} \u0438\u0437 {1}","\u041F\u0440\u043E\u0431\u043B\u0435\u043C\u044B: {0} \u0438\u0437 {1}","\u0426\u0432\u0435\u0442 \u043E\u0448\u0438\u0431\u043A\u0438 \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u043F\u043E \u043C\u0435\u0442\u043A\u0430\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u043F\u043E \u043C\u0435\u0442\u043A\u0430\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u043E\u0433\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u043F\u043E \u043C\u0435\u0442\u043A\u0430\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0424\u043E\u043D \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u043F\u043E \u043C\u0435\u0442\u043A\u0430\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430."],"vs/editor/contrib/gotoSymbol/goToCommands":["\u041E\u0431\u0437\u043E\u0440","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F",'\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0434\u043B\u044F "{0}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E.',"\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B.","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A &&\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E","\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0441\u0431\u043E\u043A\u0443","\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435","\u041E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u044F",'\u041E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043B\u044F "{0}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E.',"\u041E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u044E","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A &&\u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u044E",'\u041E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u0435 \u0434\u043B\u044F "{0}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E.',"\u041E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u0435 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E","\u041F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u043E\u0431\u044A\u044F\u0432\u043B\u0435\u043D\u0438\u0435","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0442\u0438\u043F\u043E\u0432",'\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0442\u0438\u043F\u0430 \u0434\u043B\u044F "{0}".',"\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043E \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0442\u0438\u043F\u0430.","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E \u0442\u0438\u043F\u0430","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A &&\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044E \u0442\u0438\u043F\u0430","\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0442\u0438\u043F\u0430","\u0420\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438",'\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F \u0434\u043B\u044F "{0}".',"\u041D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F.","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F\u043C","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A &&\u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044F\u043C","\u041F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u0438",'\u0421\u0441\u044B\u043B\u043A\u0438 \u0434\u043B\u044F "{0}" \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B',"\u0421\u0441\u044B\u043B\u043A\u0438 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0441\u044B\u043B\u043A\u0430\u043C","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A &&\u0441\u0441\u044B\u043B\u043A\u0430\u043C","\u0421\u0441\u044B\u043B\u043A\u0438","\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0438","\u0421\u0441\u044B\u043B\u043A\u0438","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043B\u044E\u0431\u043E\u043C\u0443 \u0441\u0438\u043C\u0432\u043E\u043B\u0443","\u0420\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u044F",'\u041D\u0435\u0442 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u0434\u043B\u044F "{0}"',"\u0421\u0441\u044B\u043B\u043A\u0438"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["\u0429\u0435\u043B\u043A\u043D\u0438\u0442\u0435, \u0447\u0442\u043E\u0431\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0437\u0438\u0442\u044C \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F ({0})."],"vs/editor/contrib/gotoSymbol/peek/referencesController":["\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["\u0421\u0441\u044B\u043B\u043E\u043A: {0}","{0} \u0441\u0441\u044B\u043B\u043A\u0430","\u0421\u0441\u044B\u043B\u043A\u0438"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["\u043F\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D","\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442","\u0421\u0441\u044B\u043B\u043A\u0438"],"vs/editor/contrib/gotoSymbol/referencesModel":["\u0441\u0441\u044B\u043B\u043A\u0430 \u0432 {0} \u0432 \u0441\u0442\u0440\u043E\u043A\u0435 {1} \u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u0435 {2}","\u0441\u0438\u043C\u0432\u043E\u043B \u0432 {0} \u0432 \u0441\u0442\u0440\u043E\u043A\u0435 {1} \u0438 \u0441\u0442\u043E\u043B\u0431\u0446\u0435 {2}, {3}","1 \u0441\u0438\u043C\u0432\u043E\u043B \u0432 {0}, \u043F\u043E\u043B\u043D\u044B\u0439 \u043F\u0443\u0442\u044C: {1}","{0} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0432 {1}, \u043F\u043E\u043B\u043D\u044B\u0439 \u043F\u0443\u0442\u044C: {2} ","\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u044B","\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D 1 \u0441\u0438\u043C\u0432\u043E\u043B \u0432 {0}","\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D\u043E {0} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0432 {1}","\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D\u043E {0} \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0432 {1} \u0444\u0430\u0439\u043B\u0430\u0445"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["\u0421\u0438\u043C\u0432\u043E\u043B {0} \u0438\u0437 {1}, {2} \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E","\u0421\u0438\u043C\u0432\u043E\u043B {0} \u0438\u0437 {1}"],"vs/editor/contrib/hover/hover":["\u041F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438","\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u043F\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430 \u043C\u044B\u0448\u0438"],"vs/editor/contrib/hover/markdownHoverParticipant":["\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430..."],"vs/editor/contrib/hover/markerHoverParticipant":["\u041F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0435\u0442\u044C \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0443","\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B","\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0430\u043B\u0438\u0447\u0438\u044F \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0439...","\u0418\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B","\u0411\u044B\u0441\u0442\u0440\u043E\u0435 \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u0435..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0438\u043C \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435\u043C","\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u043C \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435\u043C"],"vs/editor/contrib/indentation/indentation":["\u041F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F \u0432 \u043F\u0440\u043E\u0431\u0435\u043B\u044B","\u041F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F \u0432 \u0448\u0430\u0433\u0438 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438","\u041D\u0430\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0448\u0430\u0433\u0430 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438","\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0440\u0430\u0437\u043C\u0435\u0440 \u0448\u0430\u0433\u0430 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438 \u0434\u043B\u044F \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u0444\u0430\u0439\u043B\u0430","\u041E\u0442\u0441\u0442\u0443\u043F \u0441 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435\u043C \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438","\u041E\u0442\u0441\u0442\u0443\u043F \u0441 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u0438\u0435\u043C \u043F\u0440\u043E\u0431\u0435\u043B\u043E\u0432","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u043E\u0442\u0441\u0442\u0443\u043F\u0430 \u043E\u0442 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0433\u043E","\u041F\u043E\u0432\u0442\u043E\u0440\u043D\u043E \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F\u044B \u0441\u0442\u0440\u043E\u043A","\u041F\u043E\u0432\u0442\u043E\u0440\u043D\u043E \u0440\u0430\u0441\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F\u044B \u0434\u043B\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0445 \u0441\u0442\u0440\u043E\u043A"],"vs/editor/contrib/linesOperations/linesOperations":["\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u0441\u0432\u0435\u0440\u0445\u0443","&&\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043D\u0430 \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u044B\u0448\u0435","\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u0441\u043D\u0438\u0437\u0443","\u041A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043D\u0430 \u0441\u0442\u0440\u043E\u043A\u0443 &&\u043D\u0438\u0436\u0435","\u0414\u0443\u0431\u043B\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0435","&&\u0414\u0443\u0431\u043B\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0435","\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u0432\u0435\u0440\u0445","\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u044C \u043D\u0430 \u0441&&\u0442\u0440\u043E\u043A\u0443 \u0432\u044B\u0448\u0435","\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u043D\u0438\u0437","&&\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u044C \u043D\u0430 \u0441\u0442\u0440\u043E\u043A\u0443 \u043D\u0438\u0436\u0435","\u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u0441\u0442\u0440\u043E\u043A \u043F\u043E \u0432\u043E\u0437\u0440\u0430\u0441\u0442\u0430\u043D\u0438\u044E","\u0421\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u043A\u0430 \u0441\u0442\u0440\u043E\u043A \u043F\u043E \u0443\u0431\u044B\u0432\u0430\u043D\u0438\u044E","\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B-\u0440\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u0435\u043B\u0438","\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443","\u0423\u0432\u0435\u043B\u0438\u0447\u0438\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F","\u0423\u043C\u0435\u043D\u044C\u0448\u0438\u0442\u044C \u043E\u0442\u0441\u0442\u0443\u043F","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u0432\u044B\u0448\u0435","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 \u043D\u0438\u0436\u0435","\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u0441\u0435 \u0441\u043B\u0435\u0432\u0430","\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u0441\u0435 \u0441\u043F\u0440\u0430\u0432\u0430","_\u041E\u0431\u044A\u0435\u0434\u0438\u043D\u0438\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0438","\u0422\u0440\u0430\u043D\u0441\u043F\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0432\u043E\u043A\u0440\u0443\u0433 \u043A\u0443\u0440\u0441\u043E\u0440\u0430","\u041F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u0442\u044C \u0432 \u0432\u0435\u0440\u0445\u043D\u0438\u0439 \u0440\u0435\u0433\u0438\u0441\u0442\u0440","\u041F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u0442\u044C \u0432 \u043D\u0438\u0436\u043D\u0438\u0439 \u0440\u0435\u0433\u0438\u0441\u0442\u0440","\u041F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u0442\u044C \u0432 \u0437\u0430\u0433\u043B\u0430\u0432\u043D\u044B\u0435 \u0431\u0443\u043A\u0432\u044B","\u041F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u043E\u0432\u0430\u0442\u044C \u0432 \u043D\u0430\u043F\u0438\u0441\u0430\u043D\u0438\u0435 \u0441 \u043F\u043E\u0434\u0447\u0435\u0440\u043A\u0438\u0432\u0430\u043D\u0438\u044F\u043C\u0438"],"vs/editor/contrib/linkedEditing/linkedEditing":["\u0417\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0441\u0432\u044F\u0437\u0430\u043D\u043D\u043E\u0435 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u0435","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u0440\u0438 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u043C \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0438 \u0442\u0438\u043F\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u043E\u043C."],"vs/editor/contrib/links/links":["\u0412\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u0443","\u043F\u0435\u0440\u0435\u0439\u0442\u0438 \u043F\u043E \u0441\u0441\u044B\u043B\u043A\u0435","\u041A\u043D\u043E\u043F\u043A\u0430 OPTION \u0438 \u0449\u0435\u043B\u0447\u043E\u043A \u043B\u0435\u0432\u043E\u0439 \u043A\u043D\u043E\u043F\u043A\u043E\u0439 \u043C\u044B\u0448\u0438","\u041A\u043D\u043E\u043F\u043A\u0430 CTRL \u0438 \u0449\u0435\u043B\u0447\u043E\u043A \u043B\u0435\u0432\u043E\u0439 \u043A\u043D\u043E\u043F\u043A\u043E\u0439 \u043C\u044B\u0448\u0438","\u041A\u043D\u043E\u043F\u043A\u0430 OPTION \u0438 \u0449\u0435\u043B\u0447\u043E\u043A \u043B\u0435\u0432\u043E\u0439 \u043A\u043D\u043E\u043F\u043A\u043E\u0439 \u043C\u044B\u0448\u0438","\u041A\u043D\u043E\u043F\u043A\u0430 ALT \u0438 \u0449\u0435\u043B\u0447\u043E\u043A \u043B\u0435\u0432\u043E\u0439 \u043A\u043D\u043E\u043F\u043A\u043E\u0439 \u043C\u044B\u0448\u0438","\u0412\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B {0}","\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443, \u0442\u0430\u043A \u043A\u0430\u043A \u043E\u043D\u0430 \u0438\u043C\u0435\u0435\u0442 \u043D\u0435\u043F\u0440\u0430\u0432\u0438\u043B\u044C\u043D\u044B\u0439 \u0444\u043E\u0440\u043C\u0430\u0442: {0}","\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443, \u0443 \u043D\u0435\u0435 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0446\u0435\u043B\u0435\u0432\u043E\u0439 \u043E\u0431\u044A\u0435\u043A\u0442.","\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443"],"vs/editor/contrib/message/messageController":["\u041E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043B\u0438 \u0441\u0435\u0439\u0447\u0430\u0441 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0435\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435","\u041D\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044F \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u044C \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0435 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u0447\u0442\u0435\u043D\u0438\u044F"],"vs/editor/contrib/multicursor/multicursor":["\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u0443\u0440\u0441\u043E\u0440 \u0432\u044B\u0448\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u0443\u0440\u0441\u043E\u0440 &&\u0432\u044B\u0448\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u0443\u0440\u0441\u043E\u0440 \u043D\u0438\u0436\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u0443\u0440\u0441\u043E\u0440 &&\u043D\u0438\u0436\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u0443\u0440\u0441\u043E\u0440\u044B \u043A \u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F\u043C \u0441\u0442\u0440\u043E\u043A","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u0443\u0440\u0441\u043E\u0440\u044B \u0432 &&\u043E\u043A\u043E\u043D\u0447\u0430\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u0443\u0440\u0441\u043E\u0440\u044B \u043D\u0438\u0436\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043A\u0443\u0440\u0441\u043E\u0440\u044B \u0432\u044B\u0448\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0432 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C &&\u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0435 \u0432\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0439 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442 \u0432 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0435","\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C &&\u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0435 \u0432\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u0435","\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u044C \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0435 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0432 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0435","\u041F\u0435\u0440\u0435\u043C\u0435\u0441\u0442\u0438\u0442\u044C \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0439 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0439 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442 \u0432 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u043E\u0435 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0435","\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u0441\u0435 \u0432\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u044F \u043D\u0430\u0439\u0434\u0435\u043D\u043D\u044B\u0445 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439","\u0412\u044B\u0431\u0440\u0430\u0442\u044C \u0432\u0441\u0435 &&\u0432\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u044F","\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u0432\u0441\u0435 \u0432\u0445\u043E\u0436\u0434\u0435\u043D\u0438\u044F"],"vs/editor/contrib/parameterHints/parameterHints":["\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0438 \u043A \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430\u043C"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0438 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430.","\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043A\u0438 \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u0433\u043E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430.","{0}, \u0443\u043A\u0430\u0437\u0430\u043D\u0438\u0435"],"vs/editor/contrib/peekView/peekView":["\u0417\u0430\u043A\u0440\u044B\u0442\u044C","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u0439 \u043E \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0435 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u0438 \u043C\u0430\u0441\u0441\u0438\u0432\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0443\u0437\u043B\u043E\u0432 \u0441\u0442\u0440\u043E\u043A\u0438 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0443\u0437\u043B\u043E\u0432 \u0444\u0430\u0439\u043B\u0430 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u043E\u043B\u044F \u0432 \u043E\u043A\u043D\u0435 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u0432 \u0431\u044B\u0441\u0442\u0440\u043E\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0413\u0440\u0430\u043D\u0438\u0446\u0430 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u0432 \u0431\u044B\u0441\u0442\u0440\u043E\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435."],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["\u0427\u0442\u043E\u0431\u044B \u043F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0442\u0440\u043E\u043A\u0435, \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440.","\u041F\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043A \u0441\u0442\u0440\u043E\u043A\u0435 {0} \u0438 \u0441\u0442\u043E\u043B\u0431\u0446\u0443 {1}.","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0442\u0440\u043E\u043A\u0435 {0}.","\u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430: {0}, \u0441\u0438\u043C\u0432\u043E\u043B: {1}. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u043E\u043C\u0435\u0440 \u0441\u0442\u0440\u043E\u043A\u0438 \u043C\u0435\u0436\u0434\u0443 1 \u0438 {2} \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430.","\u0422\u0435\u043A\u0443\u0449\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430: {0}, \u0441\u0438\u043C\u0432\u043E\u043B: {1}. \u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u043E\u043C\u0435\u0440 \u0441\u0442\u0440\u043E\u043A\u0438 \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430."],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["\u0427\u0442\u043E\u0431\u044B \u043F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u0438\u043C\u0432\u043E\u043B\u0443, \u0441\u043D\u0430\u0447\u0430\u043B\u0430 \u043E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u0441 \u0441\u0438\u043C\u0432\u043E\u043B\u044C\u043D\u043E\u0439 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u0435\u0439.","\u0410\u043A\u0442\u0438\u0432\u043D\u044B\u0439 \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440 \u043D\u0435 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0438\u043C\u0432\u043E\u043B\u044C\u043D\u0443\u044E \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044E.","\u041D\u0435\u0442 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u044E\u0449\u0438\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u041D\u0435\u0442 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u0431\u043E\u043A\u0443","\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0432\u043D\u0438\u0437\u0443","\u0441\u0438\u043C\u0432\u043E\u043B\u044B ({0})","\u0441\u0432\u043E\u0439\u0441\u0442\u0432\u0430 ({0})","\u043C\u0435\u0442\u043E\u0434\u044B ({0})","\u0444\u0443\u043D\u043A\u0446\u0438\u0438 ({0})","\u043A\u043E\u043D\u0441\u0442\u0440\u0443\u043A\u0442\u043E\u0440\u044B ({0})","\u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u044B\u0435 ({0})","\u043A\u043B\u0430\u0441\u0441\u044B ({0})","\u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B ({0})","\u0441\u043E\u0431\u044B\u0442\u0438\u044F ({0})","\u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u044B ({0})","\u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u044B ({0})","\u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0441\u0442\u0432\u0430 \u0438\u043C\u0435\u043D ({0})","\u043F\u0430\u043A\u0435\u0442\u044B ({0})","\u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0442\u0438\u043F\u0430 ({0})","\u043C\u043E\u0434\u0443\u043B\u0438 ({0})","\u0441\u0432\u043E\u0439\u0441\u0442\u0432\u0430 ({0})","\u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F ({0})","\u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u0435\u043D\u0438\u044F ({0})","\u0441\u0442\u0440\u043E\u043A\u0438 ({0})","\u0444\u0430\u0439\u043B\u044B ({0})","\u043C\u0430\u0441\u0441\u0438\u0432\u044B ({0})","\u0447\u0438\u0441\u043B\u0430 ({0})","\u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u044F ({0})","\u043E\u0431\u044A\u0435\u043A\u0442\u044B ({0})","\u043A\u043B\u044E\u0447\u0438 ({0})","\u043F\u043E\u043B\u044F ({0})","\u043A\u043E\u043D\u0441\u0442\u0430\u043D\u0442\u044B ({0})"],"vs/editor/contrib/rename/rename":["\u0420\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u044B \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442.","\u041F\u0440\u043E\u0438\u0437\u043E\u0448\u043B\u0430 \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0438 \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u043E\u0441\u043B\u0435 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u044F",'\u041F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435 "{0}"',"\u041F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435 {0}","\xAB{0}\xBB \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D \u0432 \xAB{1}\xBB. \u0421\u0432\u043E\u0434\u043A\u0430: {2}","\u041E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u044F \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u043F\u0440\u0430\u0432\u043A\u0438","\u041E\u043F\u0435\u0440\u0430\u0446\u0438\u0438 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u044F \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u044B\u0447\u0438\u0441\u043B\u0438\u0442\u044C \u043F\u0440\u0430\u0432\u043A\u0438","\u041F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u0442\u044C \u0441\u0438\u043C\u0432\u043E\u043B","\u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C/\u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E\u0441\u0442\u044C \u043F\u0440\u0435\u0434\u0432\u0430\u0440\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0433\u043E \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0439 \u043F\u0435\u0440\u0435\u0434 \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u0435\u043C"],"vs/editor/contrib/rename/renameInputField":["\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u043E\u0432\u043E\u0435 \u0438\u043C\u044F \u0434\u043B\u044F \u0432\u0445\u043E\u0434\u043D\u044B\u0445 \u0434\u0430\u043D\u043D\u044B\u0445 \u0438 \u043D\u0430\u0436\u043C\u0438\u0442\u0435 \u043A\u043B\u0430\u0432\u0438\u0448\u0443 \u0412\u0412\u041E\u0414 \u0434\u043B\u044F \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F.","\u041D\u0430\u0436\u043C\u0438\u0442\u0435 {0} \u0434\u043B\u044F \u043F\u0435\u0440\u0435\u0438\u043C\u0435\u043D\u043E\u0432\u0430\u043D\u0438\u044F, {1} \u0434\u043B\u044F \u043F\u0440\u043E\u0441\u043C\u043E\u0442\u0440\u0430."],"vs/editor/contrib/smartSelect/smartSelect":["\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u0439 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442","&&\u0420\u0430\u0437\u0432\u0435\u0440\u043D\u0443\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435","\u0423\u043C\u0435\u043D\u044C\u0448\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0439 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442","&&\u0421\u0436\u0430\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435"],"vs/editor/contrib/snippet/snippetVariables":["\u0432\u043E\u0441\u043A\u0440\u0435\u0441\u0435\u043D\u044C\u0435","\u043F\u043E\u043D\u0435\u0434\u0435\u043B\u044C\u043D\u0438\u043A","\u0432\u0442\u043E\u0440\u043D\u0438\u043A","\u0441\u0440\u0435\u0434\u0430","\u0447\u0435\u0442\u0432\u0435\u0440\u0433","\u043F\u044F\u0442\u043D\u0438\u0446\u0430","\u0441\u0443\u0431\u0431\u043E\u0442\u0430","\u0412\u0441","\u041F\u043D","\u0412\u0442","\u0421\u0440","\u0427\u0442","\u041F\u0442","\u0421\u0431","\u042F\u043D\u0432\u0430\u0440\u044C","\u0424\u0435\u0432\u0440\u0430\u043B\u044C","\u041C\u0430\u0440\u0442","\u0410\u043F\u0440\u0435\u043B\u044C","\u041C\u0430\u0439","\u0418\u044E\u043D\u044C","\u0418\u044E\u043B\u044C","\u0410\u0432\u0433\u0443\u0441\u0442","\u0421\u0435\u043D\u0442\u044F\u0431\u0440\u044C","\u041E\u043A\u0442\u044F\u0431\u0440\u044C","\u041D\u043E\u044F\u0431\u0440\u044C","\u0414\u0435\u043A\u0430\u0431\u0440\u044C","\u042F\u043D\u0432","\u0424\u0435\u0432","\u041C\u0430\u0440","\u0410\u043F\u0440","\u041C\u0430\u0439","\u0418\u044E\u043D","\u0418\u044E\u043B","\u0410\u0432\u0433","\u0421\u0435\u043D","\u041E\u043A\u0442","\u041D\u043E\u044F","\u0414\u0435\u043A"],"vs/editor/contrib/suggest/suggestController":['\u041F\u0440\u0438\u043D\u044F\u0442\u0438\u0435 "{0}" \u043F\u0440\u0438\u0432\u0435\u043B\u043E \u043A \u0432\u043D\u0435\u0441\u0435\u043D\u0438\u044E \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u043F\u0440\u0430\u0432\u043E\u043A ({1})',"\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0435","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C","\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C","\u0417\u0430\u043C\u0435\u043D\u0438\u0442\u044C","\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C","\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u043C\u0435\u043D\u044C\u0448\u0435","\u043F\u043E\u043A\u0430\u0437\u0430\u0442\u044C \u0431\u043E\u043B\u044C\u0448\u0435","\u0421\u0431\u0440\u043E\u0441 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0440\u0430\u0437\u043C\u0435\u0440\u0430 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F"],"vs/editor/contrib/suggest/suggestWidget":["\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043E\u043A.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043E\u043A.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0439 \u0437\u0430\u043F\u0438\u0441\u0438 \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u044F \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430...","\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0443\u044E\u0442.","{0}, \u0434\u043E\u043A\u0443\u043C\u0435\u043D\u0442\u044B: {1}","\u041F\u0440\u0435\u0434\u043B\u043E\u0436\u0438\u0442\u044C"],"vs/editor/contrib/suggest/suggestWidgetDetails":["\u0417\u0430\u043A\u0440\u044B\u0442\u044C","\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u0438\u044F \u0434\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u0439 \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u041F\u043E\u0434\u0440\u043E\u0431\u043D\u0435\u0435"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043C\u0430\u0441\u0441\u0438\u0432\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u043B\u043E\u0433\u0438\u0447\u0435\u0441\u043A\u0438\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043A\u043B\u0430\u0441\u0441\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0446\u0432\u0435\u0442\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043A\u043E\u043D\u0441\u0442\u0430\u043D\u0442\u044B. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043A\u043E\u043D\u0441\u0442\u0440\u0443\u043A\u0442\u043E\u0440\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u0438\u0442\u0435\u043B\u044F. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0447\u043B\u0435\u043D\u0430 \u043F\u0435\u0440\u0435\u0447\u0438\u0441\u043B\u0438\u0442\u0435\u043B\u044F. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0441\u043E\u0431\u044B\u0442\u0438\u044F. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043F\u043E\u043B\u044F. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0444\u0430\u0439\u043B\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043F\u0430\u043F\u043A\u0438. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0444\u0443\u043D\u043A\u0446\u0438\u0438. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043A\u043B\u044E\u0447\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043A\u043B\u044E\u0447\u0435\u0432\u043E\u0433\u043E \u0441\u043B\u043E\u0432\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043C\u0435\u0442\u043E\u0434\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043C\u043E\u0434\u0443\u043B\u044F. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0441\u0442\u0432\u0430 \u0438\u043C\u0435\u043D. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 NULL. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0447\u0438\u0441\u043B\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043E\u0431\u044A\u0435\u043A\u0442\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043E\u043F\u0435\u0440\u0430\u0442\u043E\u0440\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043F\u0430\u043A\u0435\u0442\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0441\u0441\u044B\u043B\u043A\u0438. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u0430 \u043A\u043E\u0434\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0441\u0442\u0440\u043E\u043A\u0438. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u044B. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0442\u0435\u043A\u0441\u0442\u0430. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0442\u0438\u043F\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0435\u0434\u0438\u043D\u0438\u0446. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439. \u042D\u0442\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0442\u0441\u044F \u0432 \u0441\u0442\u0440\u0443\u043A\u0442\u0443\u0440\u0435, \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u0439."],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["\u041F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB \u043F\u0435\u0440\u0435\u043C\u0435\u0449\u0430\u0435\u0442 \u0444\u043E\u043A\u0443\u0441.","\u041F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB \u0444\u043E\u043A\u0443\u0441 \u043F\u0435\u0440\u0435\u0439\u0434\u0435\u0442 \u043D\u0430 \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0439 \u044D\u043B\u0435\u043C\u0435\u043D\u0442, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u043C\u043E\u0436\u0435\u0442 \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0444\u043E\u043A\u0443\u0441","\u0422\u0435\u043F\u0435\u0440\u044C \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 TAB \u0431\u0443\u0434\u0435\u0442 \u0432\u0441\u0442\u0430\u0432\u043B\u0435\u043D \u0441\u0438\u043C\u0432\u043E\u043B \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438"],"vs/editor/contrib/tokenization/tokenization":["\u0420\u0430\u0437\u0440\u0430\u0431\u043E\u0442\u0447\u0438\u043A: \u043F\u0440\u0438\u043D\u0443\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u0430\u044F \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u0430\u044F \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u0442\u043E\u043A\u0435\u043D\u043E\u0432"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["\u041D\u0435\u043E\u0431\u044B\u0447\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A\u0438","\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D\u044B \u043D\u0435\u043E\u0431\u044B\u0447\u043D\u044B\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u044B \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A\u0438",`\u042D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u043E\u0434\u0438\u043D \u0438\u043B\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043D\u0435\u043E\u0431\u044B\u0447\u043D\u044B\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0438\u044F \u0441\u0442\u0440\u043E\u043A\u0438, \u0442\u0430\u043A\u0438\u0445 \u043A\u0430\u043A \u0440\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u0435\u043B\u044C \u0441\u0442\u0440\u043E\u043A (LS) \u0438\u043B\u0438 \u0440\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u0435\u043B\u044C \u0430\u0431\u0437\u0430\u0446\u0435\u0432 (PS).\r +\r +\u0420\u0435\u043A\u043E\u043C\u0435\u043D\u0434\u0443\u0435\u0442\u0441\u044F \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0438\u0445 \u0438\u0437 \u0444\u0430\u0439\u043B\u0430. \u0423\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u044D\u0442\u0438\u0445 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u043C\u043E\u0436\u043D\u043E \u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430 "editor.unusualLineTerminators".`,"\u0418\u0441\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B","\u0418\u0433\u043D\u043E\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0443 \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0441\u0438\u043C\u0432\u043E\u043B\u0430 \u043F\u0440\u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u0435 \u043D\u0430 \u0447\u0442\u0435\u043D\u0438\u0435, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u043F\u0440\u0438 \u0447\u0442\u0435\u043D\u0438\u0438 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u0430 \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0430 \u0437\u0430\u043F\u0438\u0441\u044C, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \u043F\u0440\u0438 \u0437\u0430\u043F\u0438\u0441\u0438 \u0432 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u0443\u044E. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0441\u0438\u043C\u0432\u043E\u043B\u0430 \u043F\u0440\u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u0435 \u043D\u0430 \u0447\u0442\u0435\u043D\u0438\u0435, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u043F\u0440\u0438 \u0441\u0447\u0438\u0442\u044B\u0432\u0430\u043D\u0438\u0438 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0441\u0438\u043C\u0432\u043E\u043B\u0430 \u043F\u0440\u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u0435 \u043D\u0430 \u0437\u0430\u043F\u0438\u0441\u044C, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u043F\u0440\u0438 \u0437\u0430\u043F\u0438\u0441\u0438 \u043F\u0435\u0440\u0435\u043C\u0435\u043D\u043D\u043E\u0439. ","\u0426\u0432\u0435\u0442 \u043C\u0430\u0440\u043A\u0435\u0440\u0430 \u043E\u0431\u0437\u043E\u0440\u043D\u043E\u0439 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432. \u042D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0432\u0430\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u043C\u0430\u0440\u043A\u0435\u0440\u0430 \u043E\u0431\u0437\u043E\u0440\u043D\u043E\u0439 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043D\u0430 \u0437\u0430\u043F\u0438\u0441\u044C. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u043C\u0443 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044E \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432","\u041F\u0435\u0440\u0435\u0439\u0442\u0438 \u043A \u043F\u0440\u0435\u0434\u044B\u0434\u0443\u0449\u0435\u043C\u0443 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044E \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432","\u0412\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0438\u043B\u0438 \u043E\u0442\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0441\u0438\u043C\u0432\u043E\u043B\u043E\u0432"],"vs/editor/contrib/wordOperations/wordOperations":["\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043B\u043E\u0432\u043E"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["\u041F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u044F\u0437\u044B\u043A\u0430 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E","\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0430 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430, \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u043C\u044B\u0445 \u0434\u043B\u044F \u044F\u0437\u044B\u043A\u0430.","\u042D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0443 \u0434\u043B\u044F \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0445 \u044F\u0437\u044B\u043A\u043E\u0432.","\u041D\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044F \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u0443\u0441\u0442\u043E\u0435 \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u043E",`\u041D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C "{0}". \u041E\u043D\u043E \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0448\u0430\u0431\u043B\u043E\u043D\u0443 \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u0430 '\\\\[.*\\\\]$' \u0434\u043B\u044F \u043E\u043F\u0438\u0441\u0430\u043D\u0438\u044F \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430, \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u043C\u044B\u0445 \u044F\u0437\u044B\u043A\u043E\u043C. \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0439\u0442\u0435 \u0443\u0447\u0430\u0441\u0442\u0438\u0435 configurationDefaults.`,'\u041D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C "{0}". \u042D\u0442\u043E \u0441\u0432\u043E\u0439\u0441\u0442\u0432\u043E \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D\u043E.'],"vs/platform/contextkey/browser/contextKeyService":["\u041A\u043E\u043C\u0430\u043D\u0434\u0430, \u0432\u043E\u0437\u0432\u0440\u0430\u0449\u0430\u044E\u0449\u0430\u044F \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u043E \u043A\u043B\u044E\u0447\u0430\u0445 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430"],"vs/platform/contextkey/common/contextkeys":["\u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u043B\u0438 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u043E\u043D\u043D\u0430\u044F \u0441\u0438\u0441\u0442\u0435\u043C\u0430 Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["\u0411\u044B\u043B\u0430 \u043D\u0430\u0436\u0430\u0442\u0430 \u043A\u043B\u0430\u0432\u0438\u0448\u0430 {0}. \u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435 \u043D\u0430\u0436\u0430\u0442\u0438\u044F \u0432\u0442\u043E\u0440\u043E\u0439 \u043A\u043B\u0430\u0432\u0438\u0448\u0438 \u0441\u043E\u0447\u0435\u0442\u0430\u043D\u0438\u044F...","\u0421\u043E\u0447\u0435\u0442\u0430\u043D\u0438\u0435 \u043A\u043B\u0430\u0432\u0438\u0448 ({0} \u0438 {1}) \u043D\u0435 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439."],"vs/platform/list/browser/listService":["\u0420\u0430\u0431\u043E\u0447\u0435\u0435 \u043C\u0435\u0441\u0442\u043E","\u0421\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 CTRL \u0432 Windows \u0438 Linux \u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 COMMAND \u0432 macOS.","\u0421\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 ALT \u0432 Windows \u0438 Linux \u0438 \u043A\u043B\u0430\u0432\u0438\u0448\u0435 OPTION \u0432 macOS.",'\u041C\u043E\u0434\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0434\u043B\u044F \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 \u0432 \u0434\u0435\u0440\u0435\u0432\u044C\u044F\u0445 \u0438 \u0441\u043F\u0438\u0441\u043A\u0430\u0445 \u0432 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 \u043C\u043D\u043E\u0436\u0435\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430 \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043C\u044B\u0448\u0438 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0432 \u043F\u0440\u043E\u0432\u043E\u0434\u043D\u0438\u043A\u0435, \u0432 \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0445 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430\u0445 \u0438 \u0432 \u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0438 scm). \u0416\u0435\u0441\u0442\u044B \u043C\u044B\u0448\u0438 "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0441\u0431\u043E\u043A\u0443" (\u0435\u0441\u043B\u0438 \u043E\u043D\u0438 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044E\u0442\u0441\u044F) \u0431\u0443\u0434\u0443\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u044B \u0442\u0430\u043A\u0438\u043C \u043E\u0431\u0440\u0430\u0437\u043E\u043C, \u0447\u0442\u043E\u0431\u044B \u043E\u043D\u0438 \u043D\u0435 \u043A\u043E\u043D\u0444\u043B\u0438\u043A\u0442\u043E\u0432\u0430\u043B\u0438 \u0441 \u043C\u043E\u0434\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 \u043C\u043D\u043E\u0436\u0435\u0441\u0442\u0432\u0435\u043D\u043D\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430.',"\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u043A\u0430\u043A \u043E\u0442\u043A\u0440\u044B\u0432\u0430\u0442\u044C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u0432 \u0434\u0435\u0440\u0435\u0432\u044C\u044F\u0445 \u0438 \u0441\u043F\u0438\u0441\u043A\u0430\u0445 \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043C\u044B\u0448\u0438 (\u0435\u0441\u043B\u0438 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F). \u041E\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043D\u0438\u043C\u0430\u043D\u0438\u0435, \u0447\u0442\u043E \u044D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u043C\u043E\u0436\u0435\u0442 \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0432 \u043D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0434\u0435\u0440\u0435\u0432\u044C\u044F\u0445 \u0438 \u0441\u043F\u0438\u0441\u043A\u0430\u0445, \u0435\u0441\u043B\u0438 \u043E\u043D \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u043A \u043D\u0438\u043C.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044E\u0442 \u043B\u0438 \u0433\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B\u044C\u043D\u0443\u044E \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0443 \u0441\u043F\u0438\u0441\u043A\u0438 \u0438 \u0434\u0435\u0440\u0435\u0432\u044C\u044F \u043D\u0430 \u0440\u0430\u0431\u043E\u0447\u0435\u043C \u043C\u0435\u0441\u0442\u0435. \u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435! \u0412\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u044D\u0442\u043E\u0433\u043E \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u0430 \u043C\u043E\u0436\u0435\u0442 \u043F\u043E\u0432\u043B\u0438\u044F\u0442\u044C \u043D\u0430 \u043F\u0440\u043E\u0438\u0437\u0432\u043E\u0434\u0438\u0442\u0435\u043B\u044C\u043D\u043E\u0441\u0442\u044C.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u043E\u0442\u0441\u0442\u0443\u043F \u0434\u043B\u044F \u0434\u0435\u0440\u0435\u0432\u0430 \u0432 \u043F\u0438\u043A\u0441\u0435\u043B\u044F\u0445.","\u041E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442, \u043D\u0443\u0436\u043D\u043E \u043B\u0438 \u0432 \u0434\u0435\u0440\u0435\u0432\u0435 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0438\u0435 \u043E\u0442\u0441\u0442\u0443\u043F\u0430.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u043B\u0438 \u043F\u043B\u0430\u0432\u043D\u0430\u044F \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0430 \u0434\u043B\u044F \u0441\u043F\u0438\u0441\u043A\u043E\u0432 \u0438 \u0434\u0435\u0440\u0435\u0432\u044C\u0435\u0432.","\u041F\u0440\u043E \u043F\u0440\u043E\u0441\u0442\u043E\u0439 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0432\u044B\u0431\u0438\u0440\u0430\u044E\u0442\u0441\u044F \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B, \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0435 \u0432\u0432\u043E\u0434\u0438\u043C\u044B\u043C \u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0434\u0430\u043D\u043D\u044B\u043C. \u0421\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0435 \u043E\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u043E \u043F\u0440\u0435\u0444\u0438\u043A\u0441\u0430\u043C.","\u0424\u0443\u043D\u043A\u0446\u0438\u044F \u043F\u043E\u0434\u0441\u0432\u0435\u0442\u043A\u0438 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0432\u044B\u0434\u0435\u043B\u044F\u0435\u0442 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B, \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0435 \u0432\u0432\u043E\u0434\u0438\u043C\u044B\u043C \u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0434\u0430\u043D\u043D\u044B\u043C. \u041F\u0440\u0438 \u0434\u0430\u043B\u044C\u043D\u0435\u0439\u0448\u0435\u0439 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0432\u0432\u0435\u0440\u0445 \u0438 \u0432\u043D\u0438\u0437 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F \u043E\u0431\u0445\u043E\u0434 \u0442\u043E\u043B\u044C\u043A\u043E \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0445 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432.","\u0424\u0438\u043B\u044C\u0442\u0440 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u043F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u043E\u0442\u0444\u0438\u043B\u044C\u0442\u0440\u043E\u0432\u0430\u0442\u044C \u0438 \u0441\u043A\u0440\u044B\u0442\u044C \u0432\u0441\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B, \u043D\u0435 \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044E\u0449\u0438\u0435 \u0432\u0432\u043E\u0434\u0438\u043C\u044B\u043C \u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0434\u0430\u043D\u043D\u044B\u043C.","\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0441\u0442\u0438\u043B\u0435\u043C \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u0438 \u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0434\u043B\u044F \u0441\u043F\u0438\u0441\u043A\u043E\u0432 \u0438 \u0434\u0435\u0440\u0435\u0432\u044C\u0435\u0432 \u0432 Workbench. \u0414\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u043F\u0440\u043E\u0441\u0442\u043E\u0439 \u0440\u0435\u0436\u0438\u043C, \u0440\u0435\u0436\u0438\u043C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0438 \u0440\u0435\u0436\u0438\u043C \u0444\u0438\u043B\u044C\u0442\u0440\u0430\u0446\u0438\u0438.",'\u0423\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u0442, \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u043B\u0438 \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0432 \u0441\u043F\u0438\u0441\u043A\u0430\u0445 \u0438 \u0434\u0435\u0440\u0435\u0432\u044C\u044F\u0445 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u043F\u0440\u043E\u0441\u0442\u044B\u043C \u0432\u0432\u043E\u0434\u043E\u043C. \u0415\u0441\u043B\u0438 \u0437\u0430\u0434\u0430\u043D\u043E \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 "false", \u043D\u0430\u0432\u0438\u0433\u0430\u0446\u0438\u044F \u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u0438 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u0438 \u043A\u043E\u043C\u0430\u043D\u0434\u044B "list.toggleKeyboardNavigation", \u0434\u043B\u044F \u043A\u043E\u0442\u043E\u0440\u043E\u0439 \u043C\u043E\u0436\u043D\u043E \u043D\u0430\u0437\u043D\u0430\u0447\u0438\u0442\u044C \u0441\u043E\u0447\u0435\u0442\u0430\u043D\u0438\u0435 \u043A\u043B\u0430\u0432\u0438\u0448.',"\u0423\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0442\u0435\u043C, \u043A\u0430\u043A \u043F\u0430\u043F\u043A\u0438 \u0434\u0435\u0440\u0435\u0432\u0430 \u0440\u0430\u0437\u0432\u043E\u0440\u0430\u0447\u0438\u0432\u0430\u044E\u0442\u0441\u044F \u043F\u0440\u0438 \u043D\u0430\u0436\u0430\u0442\u0438\u0438 \u043D\u0430 \u0438\u043C\u0435\u043D\u0430 \u043F\u0430\u043F\u043E\u043A. \u041E\u0431\u0440\u0430\u0442\u0438\u0442\u0435 \u0432\u043D\u0438\u043C\u0430\u043D\u0438\u0435, \u0447\u0442\u043E \u044D\u0442\u043E\u0442 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u043C\u043E\u0436\u0435\u0442 \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0432 \u043D\u0435\u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0434\u0435\u0440\u0435\u0432\u044C\u044F\u0445 \u0438 \u0441\u043F\u0438\u0441\u043A\u0430\u0445, \u0435\u0441\u043B\u0438 \u043E\u043D \u043D\u0435 \u043F\u0440\u0438\u043C\u0435\u043D\u044F\u0435\u0442\u0441\u044F \u043A \u043D\u0438\u043C."],"vs/platform/markers/common/markers":["\u041E\u0448\u0438\u0431\u043A\u0430","\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435","\u0418\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","\u043D\u0435\u0434\u0430\u0432\u043D\u043E \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D\u043D\u044B\u0435","\u0434\u0440\u0443\u0433\u0438\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B",'\u041A\u043E\u043C\u0430\u043D\u0434\u0430 "{0}" \u043F\u0440\u0438\u0432\u0435\u043B\u0430 \u043A \u043E\u0448\u0438\u0431\u043A\u0435 ({1})'],"vs/platform/quickinput/browser/helpQuickAccess":["\u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u044B\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u044B","\u043A\u043E\u043C\u0430\u043D\u0434\u044B \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["\u041E\u0431\u0449\u0438\u0439 \u0446\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430. \u042D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F, \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0435\u0433\u043E \u043D\u0435 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442.","\u041E\u0431\u0449\u0438\u0439 \u0446\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439 \u043E\u0431 \u043E\u0448\u0438\u0431\u043A\u0430\u0445. \u042D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 \u0435\u0433\u043E \u043D\u0435 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0435\u0442 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442.","\u0426\u0432\u0435\u0442 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E \u0434\u043B\u044F \u0437\u043D\u0430\u0447\u043A\u043E\u0432 \u043D\u0430 \u0440\u0430\u0431\u043E\u0447\u0435\u043C \u043C\u0435\u0441\u0442\u0435.","\u041E\u0431\u0449\u0438\u0439 \u0446\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446 \u0434\u043B\u044F \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 \u0441 \u0444\u043E\u043A\u0443\u0441\u043E\u043C. \u042D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u0435\u0441\u043B\u0438 \u043D\u0435 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D \u0432 \u043A\u043E\u043C\u043F\u043E\u043D\u0435\u043D\u0442\u0435.","\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u0430\u044F \u0433\u0440\u0430\u043D\u0438\u0446\u0430 \u0432\u043E\u043A\u0440\u0443\u0433 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u043E\u0442\u0434\u0435\u043B\u044F\u0435\u0442 \u0438\u0445 \u043E\u0442 \u0434\u0440\u0443\u0433\u0438\u0445 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 \u0434\u043B\u044F \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u0438\u044F \u043A\u043E\u043D\u0442\u0440\u0430\u0441\u0442\u0430.","\u0414\u043E\u043F\u043E\u043B\u043D\u0438\u0442\u0435\u043B\u044C\u043D\u0430\u044F \u0433\u0440\u0430\u043D\u0438\u0446\u0430 \u0432\u043E\u043A\u0440\u0443\u0433 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0445 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u043E\u0442\u0434\u0435\u043B\u044F\u0435\u0442 \u0438\u0445 \u043E\u0442 \u0434\u0440\u0443\u0433\u0438\u0445 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 \u0434\u043B\u044F \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u0438\u044F \u043A\u043E\u043D\u0442\u0440\u0430\u0441\u0442\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0441\u044B\u043B\u043E\u043A \u0432 \u0442\u0435\u043A\u0441\u0442\u0435.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u043E\u0433\u043E \u043A\u043E\u0434\u0430 \u0432 \u0442\u0435\u043A\u0441\u0442\u0435.",'\u0426\u0432\u0435\u0442 \u0442\u0435\u043D\u0438 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430, \u0442\u0430\u043A\u0438\u0445 \u043A\u0430\u043A "\u041D\u0430\u0439\u0442\u0438/\u0437\u0430\u043C\u0435\u043D\u0438\u0442\u044C".',"\u0424\u043E\u043D \u043F\u043E\u043B\u044F \u0432\u0432\u043E\u0434\u0430.","\u041F\u0435\u0440\u0435\u0434\u043D\u0438\u0439 \u043F\u043B\u0430\u043D \u043F\u043E\u043B\u044F \u0432\u0432\u043E\u0434\u0430.","\u0413\u0440\u0430\u043D\u0438\u0446\u0430 \u043F\u043E\u043B\u044F \u0432\u0432\u043E\u0434\u0430.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446 \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 \u0432 \u043F\u043E\u043B\u044F\u0445 \u0432\u0432\u043E\u0434\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 \u0432 \u043F\u043E\u043B\u044F\u0445 \u0432\u0432\u043E\u0434\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0445 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u043E\u0432 \u0432 \u043F\u043E\u043B\u044F\u0445 \u0432\u0432\u043E\u0434\u0430.",'\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u0421\u0432\u0435\u0434\u0435\u043D\u0438\u044F".','\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u0421\u0432\u0435\u0434\u0435\u043D\u0438\u044F".','\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u0421\u0432\u0435\u0434\u0435\u043D\u0438\u044F".','\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435".','\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435".','\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u041F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435".','\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u041E\u0448\u0438\u0431\u043A\u0430".','\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u043E\u0431\u043B\u0430\u0441\u0442\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u041E\u0448\u0438\u0431\u043A\u0430".','\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0432\u0432\u043E\u0434\u0430 \u0434\u043B\u044F \u0443\u0440\u043E\u0432\u043D\u044F \u0441\u0435\u0440\u044C\u0435\u0437\u043D\u043E\u0441\u0442\u0438 "\u041E\u0448\u0438\u0431\u043A\u0430".',"\u0424\u043E\u043D \u0440\u0430\u0441\u043A\u0440\u044B\u0432\u0430\u044E\u0449\u0435\u0433\u043E\u0441\u044F \u0441\u043F\u0438\u0441\u043A\u0430.","\u041F\u0435\u0440\u0435\u0434\u043D\u0438\u0439 \u043F\u043B\u0430\u043D \u0440\u0430\u0441\u043A\u0440\u044B\u0432\u0430\u044E\u0449\u0435\u0433\u043E\u0441\u044F \u0441\u043F\u0438\u0441\u043A\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u043A\u043D\u043E\u043F\u043A\u0438.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043A\u043D\u043E\u043F\u043A\u0438.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043A\u043D\u043E\u043F\u043A\u0438 \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0431\u044D\u0434\u0436\u0430. \u0411\u044D\u0434\u0436\u0438 - \u043D\u0435\u0431\u043E\u043B\u044C\u0448\u0438\u0435 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B, \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0449\u0438\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u043F\u043E\u0438\u0441\u043A\u0430.","\u0426\u0432\u0435\u0442 \u0442\u0435\u043A\u0441\u0442\u0430 \u0431\u044D\u0434\u0436\u0430. \u0411\u044D\u0434\u0436\u0438 - \u043D\u0435\u0431\u043E\u043B\u044C\u0448\u0438\u0435 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B, \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u044E\u0449\u0438\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u043F\u043E\u0438\u0441\u043A\u0430.","\u0426\u0432\u0435\u0442 \u0442\u0435\u043D\u0438 \u043F\u043E\u043B\u043E\u0441\u044B \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0438, \u043A\u043E\u0442\u043E\u0440\u0430\u044F \u0441\u0432\u0438\u0434\u0435\u0442\u0435\u043B\u044C\u0441\u0442\u0432\u0443\u0435\u0442 \u043E \u0442\u043E\u043C, \u0447\u0442\u043E \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043F\u0440\u043E\u043A\u0440\u0443\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u043F\u043E\u043B\u0437\u0443\u043D\u043A\u0430 \u043F\u043E\u043B\u043E\u0441\u044B \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0438.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u043E\u043B\u0437\u0443\u043D\u043A\u0430 \u043F\u043E\u043B\u043E\u0441\u044B \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0438 \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u043E\u043B\u0437\u0443\u043D\u043A\u0430 \u043F\u043E\u043B\u043E\u0441\u044B \u043F\u0440\u043E\u043A\u0440\u0443\u0442\u043A\u0438 \u043F\u0440\u0438 \u0449\u0435\u043B\u0447\u043A\u0435 \u043F\u043E \u043D\u0435\u043C\u0443.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0438\u043D\u0434\u0438\u043A\u0430\u0442\u043E\u0440\u0430 \u0432\u044B\u043F\u043E\u043B\u043D\u0435\u043D\u0438\u044F, \u043A\u043E\u0442\u043E\u0440\u044B\u0439 \u043C\u043E\u0436\u0435\u0442 \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0442\u044C\u0441\u044F \u0434\u043B\u044F \u0434\u043B\u0438\u0442\u0435\u043B\u044C\u043D\u044B\u0445 \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u0439.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0442\u0435\u043A\u0441\u0442\u0430 \u043E\u0448\u0438\u0431\u043A\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0432\u043E\u043B\u043D\u0438\u0441\u0442\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043E\u0448\u0438\u0431\u043E\u043A \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043B\u044F \u043E\u043A\u043E\u043D \u043E\u0448\u0438\u0431\u043E\u043A \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0442\u0435\u043A\u0441\u0442\u0430 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0432\u043E\u043B\u043D\u0438\u0441\u0442\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0439 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043B\u044F \u043E\u043A\u043E\u043D \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0439 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0442\u0435\u043A\u0441\u0442\u0430 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u043E\u0433\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u044F \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0432\u043E\u043B\u043D\u0438\u0441\u0442\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u044B\u0445 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043B\u044F \u043E\u043A\u043E\u043D \u0441\u0432\u0435\u0434\u0435\u043D\u0438\u0439 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0432\u043E\u043B\u043D\u0438\u0441\u0442\u043E\u0439 \u043B\u0438\u043D\u0438\u0438 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u043F\u043E\u0434\u0441\u043A\u0430\u0437\u043E\u043A \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043B\u044F \u043E\u043A\u043E\u043D \u0443\u043A\u0430\u0437\u0430\u043D\u0438\u0439 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0432\u0438\u0434\u0436\u0435\u0442\u043E\u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430, \u0442\u0430\u043A\u0438\u0445 \u043A\u0430\u043A \u043D\u0430\u0439\u0442\u0438/\u0437\u0430\u043C\u0435\u043D\u0438\u0442\u044C.",'\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430, \u0442\u0430\u043A\u0438\u0445 \u043A\u0430\u043A "\u041F\u043E\u0438\u0441\u043A/\u0437\u0430\u043C\u0435\u043D\u0430".',"\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430. \u042D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u0435\u0441\u043B\u0438 \u0443 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0435\u0441\u0442\u044C \u0433\u0440\u0430\u043D\u0438\u0446\u0430 \u0438 \u0435\u0441\u043B\u0438 \u044D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u043D\u0435 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435\u043C.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u043F\u0430\u043D\u0435\u043B\u0438 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0440\u0430\u0437\u043C\u0435\u0440\u0430 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0439 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430. \u042D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0442\u043E\u043C \u0441\u043B\u0443\u0447\u0430\u0435, \u0435\u0441\u043B\u0438 \u0443 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0435\u0441\u0442\u044C \u0433\u0440\u0430\u043D\u0438\u0446\u0430 \u0434\u043B\u044F \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0440\u0430\u0437\u043C\u0435\u0440\u0430 \u0438 \u0435\u0441\u043B\u0438 \u044D\u0442\u043E\u0442 \u0446\u0432\u0435\u0442 \u043D\u0435 \u043F\u0435\u0440\u0435\u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435\u043C.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430. \u041C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u043C \u0434\u043B\u044F \u0442\u0430\u043A\u0438\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432 \u0432\u044B\u0431\u043E\u0440\u0430, \u043A\u0430\u043A \u043F\u0430\u043B\u0438\u0442\u0440\u0430 \u043A\u043E\u043C\u0430\u043D\u0434.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430. \u041C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u043C \u0434\u043B\u044F \u0442\u0430\u043A\u0438\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432 \u0432\u044B\u0431\u043E\u0440\u0430, \u043A\u0430\u043A \u043F\u0430\u043B\u0438\u0442\u0440\u0430 \u043A\u043E\u043C\u0430\u043D\u0434.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0437\u0430\u0433\u043E\u043B\u043E\u0432\u043A\u0430 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430. \u041C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u043E\u043C \u0434\u043B\u044F \u0442\u0430\u043A\u0438\u0445 \u0441\u0440\u0435\u0434\u0441\u0442\u0432 \u0432\u044B\u0431\u043E\u0440\u0430, \u043A\u0430\u043A \u043F\u0430\u043B\u0438\u0442\u0440\u0430 \u043A\u043E\u043C\u0430\u043D\u0434.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430 \u0434\u043B\u044F \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430, \u043D\u0430 \u043A\u043E\u0442\u043E\u0440\u043E\u043C \u043D\u0430\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0444\u043E\u043A\u0443\u0441.","\u0426\u0432\u0435\u0442 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430 \u0434\u043B\u044F \u0433\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0438 \u043C\u0435\u0442\u043E\u043A.","\u0426\u0432\u0435\u0442 \u0441\u0440\u0435\u0434\u0441\u0442\u0432\u0430 \u0431\u044B\u0441\u0442\u0440\u043E\u0433\u043E \u0432\u044B\u0431\u043E\u0440\u0430 \u0434\u043B\u044F \u0433\u0440\u0443\u043F\u043F\u0438\u0440\u043E\u0432\u043A\u0438 \u0433\u0440\u0430\u043D\u0438\u0446.","\u0426\u0432\u0435\u0442 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u0442\u0435\u043A\u0441\u0442\u0430 \u0432 \u0440\u0435\u0436\u0438\u043C\u0435 \u0432\u044B\u0441\u043E\u043A\u043E\u0433\u043E \u043A\u043E\u043D\u0442\u0440\u0430\u0441\u0442\u0430.","\u0426\u0432\u0435\u0442 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0432 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0434\u043B\u044F \u043E\u0431\u043B\u0430\u0441\u0442\u0435\u0439, \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u043E\u0435 \u043A\u043E\u0442\u043E\u0440\u044B\u0445 \u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0435\u0442 \u0441 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u044B\u043C \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u043E\u043C. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0440\u0435\u0433\u0438\u043E\u043D\u043E\u0432 \u0441 \u0442\u0435\u043C \u0436\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u043C\u044B\u043C, \u0447\u0442\u043E \u0438 \u0432 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0438.","\u0426\u0432\u0435\u0442 \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u043F\u043E\u0438\u0441\u043A\u0430 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u0434\u0440\u0443\u0433\u0438\u0445 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u043F\u0440\u0438 \u043F\u043E\u0438\u0441\u043A\u0435. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0430, \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0432\u0430\u044E\u0449\u0435\u0433\u043E \u043F\u043E\u0438\u0441\u043A. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0442\u0435\u043A\u0443\u0449\u0435\u0433\u043E \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u0430 \u043F\u043E\u0438\u0441\u043A\u0430.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u0440\u0443\u0433\u0438\u0445 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442\u043E\u0432 \u043F\u043E\u0438\u0441\u043A\u0430.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043B\u044F \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0430, \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0438\u0432\u0430\u044E\u0449\u0435\u0433\u043E \u043F\u043E\u0438\u0441\u043A. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0412\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u043F\u043E\u0434 \u0441\u043B\u043E\u0432\u043E\u043C, \u0434\u043B\u044F \u043A\u043E\u0442\u043E\u0440\u043E\u0433\u043E \u043E\u0442\u043E\u0431\u0440\u0430\u0436\u0430\u0435\u0442\u0441\u044F \u043C\u0435\u043D\u044E \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u0443\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044F \u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u044F \u0443\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044F \u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446 \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u0443\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044F \u043D\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0441\u0442\u0440\u043E\u043A\u0438 \u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u0432 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0435.","\u0426\u0432\u0435\u0442 \u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0445 \u0441\u0441\u044B\u043B\u043E\u043A.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0445 \u0443\u043A\u0430\u0437\u0430\u043D\u0438\u0439","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043D\u044B\u0445 \u0443\u043A\u0430\u0437\u0430\u043D\u0438\u0439","\u0426\u0432\u0435\u0442, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u044B\u0439 \u0434\u043B\u044F \u0437\u043D\u0430\u0447\u043A\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0432 \u043C\u0435\u043D\u044E \u043B\u0430\u043C\u043F\u043E\u0447\u043A\u0438.","\u0426\u0432\u0435\u0442, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u044B\u0439 \u0434\u043B\u044F \u0437\u043D\u0430\u0447\u043A\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0439 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0433\u043E \u0438\u0441\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u0438\u044F \u0432 \u043C\u0435\u043D\u044E \u043B\u0430\u043C\u043F\u043E\u0447\u043A\u0438.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0432\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u0442\u0435\u043A\u0441\u0442\u0430. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u043E\u0433\u043E \u0442\u0435\u043A\u0441\u0442\u0430. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u043A\u043E\u043D\u0442\u0443\u0440\u0430 \u0434\u043B\u044F \u0434\u043E\u0431\u0430\u0432\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u0442\u0440\u043E\u043A.","\u0426\u0432\u0435\u0442 \u043A\u043E\u043D\u0442\u0443\u0440\u0430 \u0434\u043B\u044F \u0443\u0434\u0430\u043B\u0435\u043D\u043D\u044B\u0445 \u0441\u0442\u0440\u043E\u043A.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u043C\u0435\u0436\u0434\u0443 \u0434\u0432\u0443\u043C\u044F \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u044B\u043C\u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430\u043C\u0438.","\u0426\u0432\u0435\u0442 \u0434\u0438\u0430\u0433\u043E\u043D\u0430\u043B\u044C\u043D\u043E\u0439 \u0437\u0430\u043B\u0438\u0432\u043A\u0438 \u0434\u043B\u044F \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439. \u0414\u0438\u0430\u0433\u043E\u043D\u0430\u043B\u044C\u043D\u0430\u044F \u0437\u0430\u043B\u0438\u0432\u043A\u0430 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u0432 \u0440\u0430\u0437\u043C\u0435\u0449\u0430\u0435\u043C\u044B\u0445 \u0440\u044F\u0434\u043E\u043C \u043F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u044F\u0445 \u043D\u0435\u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.","\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u043D\u0430\u0445\u043E\u0434\u044F\u0449\u0435\u0433\u043E\u0441\u044F \u0432 \u0444\u043E\u043A\u0443\u0441\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u2014 \u043D\u0435\u0442.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u043D\u0430\u0445\u043E\u0434\u044F\u0449\u0435\u0433\u043E\u0441\u044F \u0432 \u0444\u043E\u043A\u0443\u0441\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u2014 \u043D\u0435\u0442.","\u0426\u0432\u0435\u0442 \u043A\u043E\u043D\u0442\u0443\u0440\u0430 \u043D\u0430\u0445\u043E\u0434\u044F\u0449\u0435\u0433\u043E\u0441\u044F \u0432 \u0444\u043E\u043A\u0443\u0441\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C\xA0\u2014 \u043D\u0435\u0442.","\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u2014 \u043D\u0435\u0442.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u2014 \u043D\u0435\u0442.","\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u2014 \u043D\u0435\u0442.","\u0426\u0432\u0435\u0442 \u0442\u0435\u043A\u0441\u0442\u0430 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u2014 \u043D\u0435\u0442.","\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u043D\u0430\u0445\u043E\u0434\u044F\u0449\u0435\u0433\u043E\u0441\u044F \u0432 \u0444\u043E\u043A\u0443\u0441\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u043D\u0435 \u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u2014 \u043D\u0435\u0442.","\u0426\u0432\u0435\u0442 \u043A\u043E\u043D\u0442\u0443\u0440\u0430 \u043D\u0430\u0445\u043E\u0434\u044F\u0449\u0435\u0433\u043E\u0441\u044F \u0432 \u0444\u043E\u043A\u0443\u0441\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0430 List/Tree, \u043A\u043E\u0433\u0434\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442 List/Tree \u043D\u0435 \u0430\u043A\u0442\u0438\u0432\u0435\u043D. \u041D\u0430 \u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0435 List/Tree \u0435\u0441\u0442\u044C \u0444\u043E\u043A\u0443\u0441 \u043A\u043B\u0430\u0432\u0438\u0430\u0442\u0443\u0440\u044B, \u043D\u0430 \u043D\u0435\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u043C\xA0\u2014 \u043D\u0435\u0442.","\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 List/Tree \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430 \u043C\u044B\u0448\u0438.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 List/Tree \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u043A\u0443\u0440\u0441\u043E\u0440\u0430 \u043C\u044B\u0448\u0438.","\u0424\u043E\u043D\u043E\u0432\u044B\u0439 \u0446\u0432\u0435\u0442 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u043E\u0432 List/Tree \u043F\u0440\u0438 \u043F\u0435\u0440\u0435\u043C\u0435\u0449\u0435\u043D\u0438\u0438 \u0441 \u043F\u043E\u043C\u043E\u0449\u044C\u044E \u043C\u044B\u0448\u0438.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0441\u043E\u043E\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u044F \u043F\u0440\u0438 \u043F\u043E\u0438\u0441\u043A\u0435 \u043F\u043E \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u0443 List/Tree.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0444\u0438\u043B\u044C\u0442\u0440\u0430 \u0442\u0438\u043F\u043E\u0432 \u0432 \u0441\u043F\u0438\u0441\u043A\u0430\u0445 \u0438 \u0434\u0435\u0440\u0435\u0432\u044C\u044F\u0445.","\u0426\u0432\u0435\u0442 \u043A\u043E\u043D\u0442\u0443\u0440\u0430 \u0434\u043B\u044F \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0444\u0438\u043B\u044C\u0442\u0440\u0430 \u0442\u0438\u043F\u043E\u0432 \u0432 \u0441\u043F\u0438\u0441\u043A\u0430\u0445 \u0438 \u0434\u0435\u0440\u0435\u0432\u044C\u044F\u0445.","\u0426\u0432\u0435\u0442 \u043A\u043E\u043D\u0442\u0443\u0440\u0430 \u0434\u043B\u044F \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0444\u0438\u043B\u044C\u0442\u0440\u0430 \u0442\u0438\u043F\u043E\u0432 \u0432 \u0441\u043F\u0438\u0441\u043A\u0430\u0445 \u0438 \u0434\u0435\u0440\u0435\u0432\u044C\u044F\u0445 \u043F\u0440\u0438 \u043E\u0442\u0441\u0443\u0442\u0441\u0442\u0432\u0438\u0438 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u0448\u0442\u0440\u0438\u0445\u0430 \u0434\u0435\u0440\u0435\u0432\u0430 \u0434\u043B\u044F \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0438\u0445 \u043E\u0442\u0441\u0442\u0443\u043F\u0430.","\u0426\u0432\u0435\u0442 \u0448\u0442\u0440\u0438\u0445\u0430 \u0434\u0435\u0440\u0435\u0432\u0430 \u0434\u043B\u044F \u043D\u0430\u043F\u0440\u0430\u0432\u043B\u044F\u044E\u0449\u0438\u0445 \u043E\u0442\u0441\u0442\u0443\u043F\u0430.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446 \u043C\u0435\u043D\u044E.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u043F\u0443\u043D\u043A\u0442\u043E\u0432 \u043C\u0435\u043D\u044E.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u0443\u043D\u043A\u0442\u043E\u0432 \u043C\u0435\u043D\u044E.","\u0426\u0432\u0435\u0442 \u043F\u0435\u0440\u0435\u0434\u043D\u0435\u0433\u043E \u043F\u043B\u0430\u043D\u0430 \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u043F\u0443\u043D\u043A\u0442\u0430 \u043C\u0435\u043D\u044E \u0432 \u043C\u0435\u043D\u044E.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0434\u043B\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u043F\u0443\u043D\u043A\u0442\u0430 \u0432 \u043C\u0435\u043D\u044E.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043B\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u043F\u0443\u043D\u043A\u0442\u0430 \u0432 \u043C\u0435\u043D\u044E.","\u0426\u0432\u0435\u0442 \u0440\u0430\u0437\u0434\u0435\u043B\u0438\u0442\u0435\u043B\u044F \u043C\u0435\u043D\u044E \u0432 \u043C\u0435\u043D\u044E.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0432 \u043F\u043E\u0437\u0438\u0446\u0438\u0438 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u0430.","\u0426\u0432\u0435\u0442 \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0432 \u043F\u043E\u0437\u0438\u0446\u0438\u0438 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u0430.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0432 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0439 \u043F\u043E\u0437\u0438\u0446\u0438\u0438 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u0430.","\u0412\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u0435 \u0446\u0432\u0435\u0442\u043E\u043C \u0433\u0440\u0430\u043D\u0438\u0446\u044B \u0432 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0439 \u043F\u043E\u0437\u0438\u0446\u0438\u0438 \u0442\u0430\u0431\u0443\u043B\u044F\u0446\u0438\u0438 \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u0430.","\u0426\u0432\u0435\u0442 \u043C\u0430\u0440\u043A\u0435\u0440\u0430 \u043E\u0431\u0437\u043E\u0440\u043D\u043E\u0439 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0434\u043B\u044F \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439 \u043F\u0440\u0438 \u043F\u043E\u0438\u0441\u043A\u0435. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u041C\u0430\u0440\u043A\u0435\u0440 \u043E\u0431\u0437\u043E\u0440\u043D\u043E\u0439 \u043B\u0438\u043D\u0435\u0439\u043A\u0438 \u0434\u043B\u044F \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u0438\u044F \u0432\u044B\u0431\u0440\u0430\u043D\u043D\u043E\u0433\u043E \u0444\u0440\u0430\u0433\u043C\u0435\u043D\u0442\u0430. \u0426\u0432\u0435\u0442 \u043D\u0435 \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u044B\u043C, \u0447\u0442\u043E\u0431\u044B \u043D\u0435 \u0441\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044B\u0435 \u043D\u0438\u0436\u0435 \u044D\u043B\u0435\u043C\u0435\u043D\u0442\u044B \u043E\u0444\u043E\u0440\u043C\u043B\u0435\u043D\u0438\u044F.","\u0426\u0432\u0435\u0442 \u043C\u0430\u0440\u043A\u0435\u0440\u0430 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u044B \u0434\u043B\u044F \u043F\u043E\u0438\u0441\u043A\u0430 \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u043C\u0430\u0440\u043A\u0435\u0440\u0430 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u044B \u0434\u043B\u044F \u0432\u044B\u0431\u043E\u0440\u0430 \u0440\u0435\u0434\u0430\u043A\u0442\u043E\u0440\u0430.","\u0426\u0432\u0435\u0442 \u043C\u0430\u0440\u043A\u0435\u0440\u0430 \u043C\u0438\u043D\u0438\u043A\u0430\u0440\u0442\u044B \u0434\u043B\u044F \u043E\u0448\u0438\u0431\u043E\u043A.","\u0426\u0432\u0435\u0442 \u043C\u0430\u0440\u043A\u0435\u0440\u0430 \u043C\u0438\u043D\u0438\u043A\u0430\u0440\u0442\u044B \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0439.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u044B.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u043E\u043B\u0437\u0443\u043D\u043A\u0430 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u044B.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u043E\u043B\u0437\u0443\u043D\u043A\u0430 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u044B \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438 \u043D\u0430 \u043D\u0435\u0433\u043E \u0443\u043A\u0430\u0437\u0430\u0442\u0435\u043B\u044F.","\u0426\u0432\u0435\u0442 \u0444\u043E\u043D\u0430 \u043F\u043E\u043B\u0437\u0443\u043D\u043A\u0430 \u043C\u0438\u043D\u0438-\u043A\u0430\u0440\u0442\u044B \u043F\u0440\u0438 \u0435\u0433\u043E \u0449\u0435\u043B\u0447\u043A\u0435.","\u0426\u0432\u0435\u0442, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u044B\u0439 \u0434\u043B\u044F \u0437\u043D\u0430\u0447\u043A\u0430 \u043E\u0448\u0438\u0431\u043A\u0438, \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u044E\u0449\u0435\u0433\u043E \u043D\u0430 \u043D\u0430\u043B\u0438\u0447\u0438\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C.","\u0426\u0432\u0435\u0442, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u044B\u0439 \u0434\u043B\u044F \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0430\u044E\u0449\u0435\u0433\u043E \u0437\u043D\u0430\u0447\u043A\u0430, \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u044E\u0449\u0435\u0433\u043E \u043D\u0430 \u043D\u0430\u043B\u0438\u0447\u0438\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C.","\u0426\u0432\u0435\u0442, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u044B\u0439 \u0434\u043B\u044F \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u043E\u043D\u043D\u043E\u0433\u043E \u0437\u043D\u0430\u0447\u043A\u0430, \u0443\u043A\u0430\u0437\u044B\u0432\u0430\u044E\u0449\u0435\u0433\u043E \u043D\u0430 \u043D\u0430\u043B\u0438\u0447\u0438\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C."],"vs/platform/theme/common/iconRegistry":["\u0418\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C\u043E\u0433\u043E \u0448\u0440\u0438\u0444\u0442\u0430. \u0415\u0441\u043B\u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D, \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F \u0448\u0440\u0438\u0444\u0442, \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u043D\u044B\u0439 \u043F\u0435\u0440\u0432\u044B\u043C.","\u0421\u0438\u043C\u0432\u043E\u043B \u0448\u0440\u0438\u0444\u0442\u0430, \u0441\u0432\u044F\u0437\u0430\u043D\u043D\u044B\u0439 \u0441 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0435\u043D\u0438\u0435\u043C \u0437\u043D\u0430\u0447\u043A\u0430.","\u0417\u043D\u0430\u0447\u043E\u043A \u0434\u043B\u044F \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F \u0437\u0430\u043A\u0440\u044B\u0442\u0438\u044F \u0432 \u043C\u0438\u043D\u0438-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u0445."],"vs/platform/undoRedo/common/undoRedoService":["\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0435 \u0444\u0430\u0439\u043B\u044B \u0431\u044B\u043B\u0438 \u0437\u0430\u043A\u0440\u044B\u0442\u044B \u0438 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u044B \u043D\u0430 \u0434\u0438\u0441\u043A\u0435: {0}.","\u0421\u043B\u0435\u0434\u0443\u044E\u0449\u0438\u0435 \u0444\u0430\u0439\u043B\u044B \u0431\u044B\u043B\u0438 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u044B \u043D\u0435\u0441\u043E\u0432\u043C\u0435\u0441\u0442\u0438\u043C\u044B\u043C \u043E\u0431\u0440\u0430\u0437\u043E\u043C: {0}.",'\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432. {1}','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432. {1}','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044E "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0442\u0430\u043A \u043A\u0430\u043A \u0431\u044B\u043B\u0438 \u0432\u043D\u0435\u0441\u0435\u043D\u044B \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0432 {1}','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0442\u0430\u043A \u043A\u0430\u043A \u0432 {1} \u0443\u0436\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043E\u0442\u043C\u0435\u043D\u044B \u0438\u043B\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0442\u0430\u043A \u043A\u0430\u043A \u0443\u0436\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u043B\u0430\u0441\u044C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043E\u0442\u043C\u0435\u043D\u044B \u0438\u043B\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F','\u0412\u044B \u0445\u043E\u0442\u0438\u0442\u0435 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432?',"\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 \u0432 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u0438\u0445 \u0444\u0430\u0439\u043B\u0430\u0445 ({0})","\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u044D\u0442\u043E\u0442 \u0444\u0430\u0439\u043B","\u041E\u0442\u043C\u0435\u043D\u0430",'\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 "{0}", \u0442\u0430\u043A \u043A\u0430\u043A \u0443\u0436\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043E\u0442\u043C\u0435\u043D\u044B \u0438\u043B\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F','\u0412\u044B \u0445\u043E\u0442\u0438\u0442\u0435 \u043E\u0442\u043C\u0435\u043D\u0438\u0442\u044C "{0}"?',"\u041E\u0442\u043C\u0435\u043D\u0438\u0442\u044C","\u041E\u0442\u043C\u0435\u043D\u0430",'\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044E "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432. {1}','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044E "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432. {1}','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044E "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0442\u0430\u043A \u043A\u0430\u043A \u0431\u044B\u043B\u0438 \u0432\u043D\u0435\u0441\u0435\u043D\u044B \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u044F \u0432 {1}','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0442\u0430\u043A \u043A\u0430\u043A \u0434\u043B\u044F {1} \u0443\u0436\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043E\u0442\u043C\u0435\u043D\u044B \u0438\u043B\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F.','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 "{0}" \u0434\u043B\u044F \u0432\u0441\u0435\u0445 \u0444\u0430\u0439\u043B\u043E\u0432, \u0442\u0430\u043A \u043A\u0430\u043A \u0443\u0436\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u043B\u0430\u0441\u044C \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043E\u0442\u043C\u0435\u043D\u044B \u0438\u043B\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F','\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435 "{0}", \u0442\u0430\u043A \u043A\u0430\u043A \u0443\u0436\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u044F\u0435\u0442\u0441\u044F \u043E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043E\u0442\u043C\u0435\u043D\u044B \u0438\u043B\u0438 \u043F\u043E\u0432\u0442\u043E\u0440\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F']}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.zh-cn.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.zh-cn.js new file mode 100644 index 0000000..036ba6b --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.zh-cn.js @@ -0,0 +1,8 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/define("vs/editor/editor.main.nls.zh-cn",{"vs/base/browser/ui/actionbar/actionViewItems":["{0} ({1})"],"vs/base/browser/ui/findinput/findInput":["\u8F93\u5165"],"vs/base/browser/ui/findinput/findInputCheckboxes":["\u533A\u5206\u5927\u5C0F\u5199","\u5168\u5B57\u5339\u914D","\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F"],"vs/base/browser/ui/findinput/replaceInput":["\u8F93\u5165","\u4FDD\u7559\u5927\u5C0F\u5199"],"vs/base/browser/ui/iconLabel/iconLabel":["\u6B63\u5728\u52A0\u8F7D\u2026"],"vs/base/browser/ui/inputbox/inputBox":["\u9519\u8BEF: {0}","\u8B66\u544A: {0}","\u4FE1\u606F: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["\u672A\u7ED1\u5B9A"],"vs/base/browser/ui/menu/menu":["{0} ({1})"],"vs/base/browser/ui/tree/abstractTree":["\u6E05\u9664","\u7981\u7528\u8F93\u5165\u65F6\u7B5B\u9009","\u542F\u7528\u8F93\u5165\u65F6\u7B5B\u9009","\u672A\u627E\u5230\u5143\u7D20","\u5DF2\u5339\u914D {0} \u4E2A\u5143\u7D20(\u5171 {1} \u4E2A)"],"vs/base/common/actions":["(\u7A7A)"],"vs/base/common/errorMessage":["{0}: {1}","\u53D1\u751F\u4E86\u7CFB\u7EDF\u9519\u8BEF ({0})","\u51FA\u73B0\u672A\u77E5\u9519\u8BEF\u3002\u6709\u5173\u8BE6\u7EC6\u4FE1\u606F\uFF0C\u8BF7\u53C2\u9605\u65E5\u5FD7\u3002","\u51FA\u73B0\u672A\u77E5\u9519\u8BEF\u3002\u6709\u5173\u8BE6\u7EC6\u4FE1\u606F\uFF0C\u8BF7\u53C2\u9605\u65E5\u5FD7\u3002","{0} \u4E2A(\u5171 {1} \u4E2A\u9519\u8BEF)","\u51FA\u73B0\u672A\u77E5\u9519\u8BEF\u3002\u6709\u5173\u8BE6\u7EC6\u4FE1\u606F\uFF0C\u8BF7\u53C2\u9605\u65E5\u5FD7\u3002"],"vs/base/common/keybindingLabels":["Ctrl","Shift","Alt","Windows","Ctrl","Shift","Alt","\u8D85\u952E","Control","Shift","Alt","Command","Control","Shift","Alt","Windows","Control","Shift","Alt","\u8D85\u952E"],"vs/base/parts/quickinput/browser/quickInput":["\u4E0A\u4E00\u6B65","{0}/{1}","\u5728\u6B64\u8F93\u5165\u53EF\u7F29\u5C0F\u7ED3\u679C\u8303\u56F4\u3002","{0} \u4E2A\u7ED3\u679C","\u5DF2\u9009 {0} \u9879","\u786E\u5B9A","\u81EA\u5B9A\u4E49","\u540E\u9000 ({0})","\u4E0A\u4E00\u6B65"],"vs/base/parts/quickinput/browser/quickInputList":["\u5FEB\u901F\u8F93\u5165"],"vs/editor/browser/controller/coreCommands":["\u5373\u4F7F\u8F6C\u5230\u8F83\u957F\u7684\u884C\uFF0C\u4E5F\u4E00\u76F4\u5230\u672B\u5C3E","\u5373\u4F7F\u8F6C\u5230\u8F83\u957F\u7684\u884C\uFF0C\u4E5F\u4E00\u76F4\u5230\u672B\u5C3E"],"vs/editor/browser/controller/textAreaHandler":["\u7F16\u8F91\u5668","\u73B0\u5728\u65E0\u6CD5\u8BBF\u95EE\u7F16\u8F91\u5668\u3002\u6309 {0} \u83B7\u53D6\u9009\u9879\u3002"],"vs/editor/browser/core/keybindingCancellation":["Whether the editor runs a cancellable operation, e.g. like 'Peek References'"],"vs/editor/browser/editorExtensions":["\u64A4\u6D88(&&U)","\u64A4\u6D88","\u6062\u590D(&&R)","\u6062\u590D","\u5168\u9009(&&S)","\u9009\u62E9\u5168\u90E8"],"vs/editor/browser/widget/codeEditorWidget":["\u5149\u6807\u6570\u91CF\u88AB\u9650\u5236\u4E3A {0}\u3002"],"vs/editor/browser/widget/diffEditorWidget":["\u5DEE\u5F02\u7F16\u8F91\u5668\u4E2D\u63D2\u5165\u9879\u7684\u7EBF\u6761\u4FEE\u9970\u3002","\u5DEE\u5F02\u7F16\u8F91\u5668\u4E2D\u5220\u9664\u9879\u7684\u7EBF\u6761\u4FEE\u9970\u3002","\u6587\u4EF6\u8FC7\u5927\uFF0C\u65E0\u6CD5\u6BD4\u8F83\u3002"],"vs/editor/browser/widget/diffReview":["\u5DEE\u5F02\u8BC4\u5BA1\u4E2D\u7684\u201C\u63D2\u5165\u201D\u56FE\u6807\u3002","\u5DEE\u5F02\u8BC4\u5BA1\u4E2D\u7684\u201C\u5220\u9664\u201D\u56FE\u6807\u3002","\u5DEE\u5F02\u8BC4\u5BA1\u4E2D\u7684\u201C\u5173\u95ED\u201D\u56FE\u6807\u3002","\u5173\u95ED","\u672A\u66F4\u6539\u884C","\u66F4\u6539\u4E86 1 \u884C","\u66F4\u6539\u4E86 {0} \u884C","\u5DEE\u5F02 {0}/ {1}: \u539F\u59CB\u884C {2}\uFF0C{3}\uFF0C\u4FEE\u6539\u540E\u7684\u884C {4}\uFF0C{5}","\u7A7A\u767D","{0} \u672A\u66F4\u6539\u7684\u884C {1}","{0}\u539F\u59CB\u884C{1}\u4FEE\u6539\u7684\u884C{2}","+ {0}\u4FEE\u6539\u7684\u884C{1}","- {0}\u539F\u59CB\u884C{1}","\u8F6C\u81F3\u4E0B\u4E00\u4E2A\u5DEE\u5F02","\u8F6C\u81F3\u4E0A\u4E00\u4E2A\u5DEE\u5F02"],"vs/editor/browser/widget/inlineDiffMargin":["\u590D\u5236\u5DF2\u5220\u9664\u7684\u884C","\u590D\u5236\u5DF2\u5220\u9664\u7684\u884C","\u590D\u5236\u5DF2\u5220\u9664\u7684\u884C({0})","\u8FD8\u539F\u6B64\u66F4\u6539","\u590D\u5236\u5DF2\u5220\u9664\u7684\u884C({0})"],"vs/editor/common/config/commonEditorConfig":["\u7F16\u8F91\u5668","\u4E00\u4E2A\u5236\u8868\u7B26\u7B49\u4E8E\u7684\u7A7A\u683C\u6570\u3002\u5728 `#editor.detectIndentation#` \u542F\u7528\u65F6\uFF0C\u6839\u636E\u6587\u4EF6\u5185\u5BB9\uFF0C\u8BE5\u8BBE\u7F6E\u53EF\u80FD\u4F1A\u88AB\u8986\u76D6\u3002","\u6309 `Tab` \u952E\u65F6\u63D2\u5165\u7A7A\u683C\u3002\u8BE5\u8BBE\u7F6E\u5728 `#editor.detectIndentation#` \u542F\u7528\u65F6\u6839\u636E\u6587\u4EF6\u5185\u5BB9\u53EF\u80FD\u4F1A\u88AB\u8986\u76D6\u3002","\u63A7\u5236\u662F\u5426\u5728\u6253\u5F00\u6587\u4EF6\u65F6\uFF0C\u57FA\u4E8E\u6587\u4EF6\u5185\u5BB9\u81EA\u52A8\u68C0\u6D4B `#editor.tabSize#` \u548C `#editor.insertSpaces#`\u3002","\u5220\u9664\u81EA\u52A8\u63D2\u5165\u7684\u5C3E\u968F\u7A7A\u767D\u7B26\u53F7\u3002","\u5BF9\u5927\u578B\u6587\u4EF6\u8FDB\u884C\u7279\u6B8A\u5904\u7406\uFF0C\u7981\u7528\u67D0\u4E9B\u5185\u5B58\u5BC6\u96C6\u578B\u529F\u80FD\u3002","\u63A7\u5236\u662F\u5426\u6839\u636E\u6587\u6863\u4E2D\u7684\u6587\u5B57\u8BA1\u7B97\u81EA\u52A8\u5B8C\u6210\u5217\u8868\u3002","\u4EC5\u5EFA\u8BAE\u6D3B\u52A8\u6587\u6863\u4E2D\u7684\u5B57\u8BCD\u3002","\u5EFA\u8BAE\u4F7F\u7528\u540C\u4E00\u8BED\u8A00\u7684\u6240\u6709\u6253\u5F00\u7684\u6587\u6863\u4E2D\u7684\u5B57\u8BCD\u3002","\u5EFA\u8BAE\u6240\u6709\u6253\u5F00\u7684\u6587\u6863\u4E2D\u7684\u5B57\u8BCD\u3002","\u63A7\u5236\u901A\u8FC7\u4EC0\u4E48\u6587\u6863\u8BA1\u7B97\u57FA\u4E8E\u5B57\u8BCD\u7684\u5B8C\u6210\u6570\u3002","\u5BF9\u6240\u6709\u989C\u8272\u4E3B\u9898\u542F\u7528\u8BED\u4E49\u7A81\u51FA\u663E\u793A\u3002","\u5BF9\u6240\u6709\u989C\u8272\u4E3B\u9898\u7981\u7528\u8BED\u4E49\u7A81\u51FA\u663E\u793A\u3002",'\u8BED\u4E49\u7A81\u51FA\u663E\u793A\u662F\u7531\u5F53\u524D\u989C\u8272\u4E3B\u9898\u7684 "semanticHighlighting" \u8BBE\u7F6E\u914D\u7F6E\u7684\u3002',"\u63A7\u5236\u662F\u5426\u4E3A\u652F\u6301\u5B83\u7684\u8BED\u8A00\u663E\u793A\u8BED\u4E49\u7A81\u51FA\u663E\u793A\u3002","\u5728\u901F\u89C8\u7F16\u8F91\u5668\u4E2D\uFF0C\u5373\u4F7F\u53CC\u51FB\u5176\u4E2D\u7684\u5185\u5BB9\u6216\u8005\u6309 `Esc` \u952E\uFF0C\u4E5F\u4FDD\u6301\u5176\u6253\u5F00\u72B6\u6001\u3002","\u7531\u4E8E\u6027\u80FD\u539F\u56E0\uFF0C\u8D85\u8FC7\u8FD9\u4E2A\u957F\u5EA6\u7684\u884C\u5C06\u4E0D\u4F1A\u88AB\u6807\u8BB0","\u8D85\u65F6(\u4EE5\u6BEB\u79D2\u4E3A\u5355\u4F4D)\uFF0C\u4E4B\u540E\u5C06\u53D6\u6D88\u5DEE\u5F02\u8BA1\u7B97\u3002\u4F7F\u75280\u8868\u793A\u6CA1\u6709\u8D85\u65F6\u3002","\u63A7\u5236\u5DEE\u5F02\u7F16\u8F91\u5668\u7684\u663E\u793A\u65B9\u5F0F\u662F\u5E76\u6392\u8FD8\u662F\u5185\u8054\u3002","\u542F\u7528\u540E\uFF0C\u5DEE\u5F02\u7F16\u8F91\u5668\u5C06\u5FFD\u7565\u524D\u5BFC\u7A7A\u683C\u6216\u5C3E\u968F\u7A7A\u683C\u4E2D\u7684\u66F4\u6539\u3002","\u63A7\u5236\u5DEE\u5F02\u7F16\u8F91\u5668\u662F\u5426\u4E3A\u6DFB\u52A0/\u5220\u9664\u7684\u66F4\u6539\u663E\u793A +/- \u6307\u793A\u7B26\u53F7\u3002","\u63A7\u5236\u662F\u5426\u5728\u7F16\u8F91\u5668\u4E2D\u663E\u793A CodeLens\u3002","\u6C38\u4E0D\u6362\u884C\u3002","\u5C06\u5728\u89C6\u533A\u5BBD\u5EA6\u5904\u6362\u884C\u3002","\u5C06\u6839\u636E `#editor.wordWrap#` \u8BBE\u7F6E\u6362\u884C\u3002"],"vs/editor/common/config/editorOptions":["\u7F16\u8F91\u5668\u5C06\u4F7F\u7528\u5E73\u53F0 API \u4EE5\u68C0\u6D4B\u662F\u5426\u9644\u52A0\u4E86\u5C4F\u5E55\u9605\u8BFB\u5668\u3002","\u7F16\u8F91\u5668\u5C06\u9488\u5BF9\u4E0E\u5C4F\u5E55\u9605\u8BFB\u5668\u642D\u914D\u4F7F\u7528\u8FDB\u884C\u6C38\u4E45\u4F18\u5316\u3002\u5C06\u7981\u7528\u81EA\u52A8\u6362\u884C\u3002","\u7F16\u8F91\u5668\u5C06\u4E0D\u518D\u5BF9\u5C4F\u5E55\u9605\u8BFB\u5668\u7684\u4F7F\u7528\u8FDB\u884C\u4F18\u5316\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5E94\u5728\u5BF9\u5C4F\u5E55\u9605\u8BFB\u5668\u8FDB\u884C\u4E86\u4F18\u5316\u7684\u6A21\u5F0F\u4E0B\u8FD0\u884C\u3002\u8BBE\u7F6E\u4E3A\u201C\u5F00\u201D\u5C06\u7981\u7528\u81EA\u52A8\u6362\u884C\u3002","\u63A7\u5236\u5728\u6CE8\u91CA\u65F6\u662F\u5426\u63D2\u5165\u7A7A\u683C\u5B57\u7B26\u3002","\u63A7\u5236\u5728\u5BF9\u884C\u6CE8\u91CA\u6267\u884C\u5207\u6362\u3001\u6DFB\u52A0\u6216\u5220\u9664\u64CD\u4F5C\u65F6\uFF0C\u662F\u5426\u5E94\u5FFD\u7565\u7A7A\u884C\u3002","\u63A7\u5236\u5728\u6CA1\u6709\u9009\u62E9\u5185\u5BB9\u65F6\u8FDB\u884C\u590D\u5236\u662F\u5426\u590D\u5236\u5F53\u524D\u884C\u3002","\u63A7\u5236\u5728\u952E\u5165\u65F6\u5149\u6807\u662F\u5426\u5E94\u8DF3\u8F6C\u4EE5\u67E5\u627E\u5339\u914D\u9879\u3002","\u63A7\u5236\u662F\u5426\u5C06\u7F16\u8F91\u5668\u9009\u4E2D\u5185\u5BB9\u4F5C\u4E3A\u641C\u7D22\u8BCD\u586B\u5165\u5230\u67E5\u627E\u5C0F\u7EC4\u4EF6\u4E2D\u3002","\u5207\u52FF\u81EA\u52A8\u6253\u5F00\u201C\u9009\u62E9\u4E2D\u67E5\u627E\u201D(\u9ED8\u8BA4)","\u59CB\u7EC8\u81EA\u52A8\u6253\u5F00\u201C\u5728\u9009\u62E9\u4E2D\u67E5\u627E\u201D","\u9009\u62E9\u591A\u884C\u5185\u5BB9\u65F6\uFF0C\u81EA\u52A8\u6253\u5F00\u201C\u5728\u9009\u62E9\u4E2D\u67E5\u627E\u201D\u3002","\u63A7\u5236\u5728\u6240\u9009\u5185\u5BB9\u4E2D\u81EA\u52A8\u5F00\u542F\u67E5\u627E\u7684\u6761\u4EF6\u3002","\u63A7\u5236\u201C\u67E5\u627E\u201D\u5C0F\u7EC4\u4EF6\u662F\u5426\u8BFB\u53D6\u6216\u4FEE\u6539 macOS \u7684\u5171\u4EAB\u67E5\u627E\u526A\u8D34\u677F\u3002",'\u63A7\u5236 "\u67E5\u627E\u5C0F\u90E8\u4EF6" \u662F\u5426\u5E94\u5728\u7F16\u8F91\u5668\u9876\u90E8\u6DFB\u52A0\u989D\u5916\u7684\u884C\u3002\u5982\u679C\u4E3A true, \u5219\u53EF\u4EE5\u5728 "\u67E5\u627E\u5C0F\u5DE5\u5177" \u53EF\u89C1\u65F6\u6EDA\u52A8\u5230\u7B2C\u4E00\u884C\u4E4B\u5916\u3002',"\u63A7\u5236\u5728\u627E\u4E0D\u5230\u5176\u4ED6\u5339\u914D\u9879\u65F6\uFF0C\u662F\u5426\u81EA\u52A8\u4ECE\u5F00\u5934(\u6216\u7ED3\u5C3E)\u91CD\u65B0\u5F00\u59CB\u641C\u7D22\u3002",'\u542F\u7528/\u7981\u7528\u5B57\u4F53\u8FDE\u5B57("calt" \u548C "liga" \u5B57\u4F53\u7279\u6027)\u3002\u5C06\u6B64\u66F4\u6539\u4E3A\u5B57\u7B26\u4E32\uFF0C\u53EF\u5BF9 "font-feature-settings" CSS \u5C5E\u6027\u8FDB\u884C\u7CBE\u7EC6\u63A7\u5236\u3002','\u663E\u5F0F "font-feature-settings" CSS \u5C5E\u6027\u3002\u5982\u679C\u53EA\u9700\u6253\u5F00/\u5173\u95ED\u8FDE\u5B57\uFF0C\u53EF\u4EE5\u6539\u4E3A\u4F20\u9012\u5E03\u5C14\u503C\u3002','\u914D\u7F6E\u5B57\u4F53\u8FDE\u5B57\u6216\u5B57\u4F53\u7279\u6027\u3002\u53EF\u4EE5\u662F\u7528\u4E8E\u542F\u7528/\u7981\u7528\u8FDE\u5B57\u7684\u5E03\u5C14\u503C\uFF0C\u6216\u7528\u4E8E\u8BBE\u7F6E CSS "font-feature-settings" \u5C5E\u6027\u503C\u7684\u5B57\u7B26\u4E32\u3002',"\u63A7\u5236\u5B57\u4F53\u5927\u5C0F(\u50CF\u7D20)\u3002","\u4EC5\u5141\u8BB8\u4F7F\u7528\u5173\u952E\u5B57\u201C\u6B63\u5E38\u201D\u548C\u201C\u52A0\u7C97\u201D\uFF0C\u6216\u4F7F\u7528\u4ECB\u4E8E 1 \u81F3 1000 \u4E4B\u95F4\u7684\u6570\u5B57\u3002","\u63A7\u5236\u5B57\u4F53\u7C97\u7EC6\u3002\u63A5\u53D7\u5173\u952E\u5B57\u201C\u6B63\u5E38\u201D\u548C\u201C\u52A0\u7C97\u201D\uFF0C\u6216\u8005\u63A5\u53D7\u4ECB\u4E8E 1 \u81F3 1000 \u4E4B\u95F4\u7684\u6570\u5B57\u3002","\u663E\u793A\u7ED3\u679C\u7684\u9884\u89C8\u89C6\u56FE (\u9ED8\u8BA4\u503C)","\u8F6C\u5230\u4E3B\u7ED3\u679C\u5E76\u663E\u793A\u9884\u89C8\u89C6\u56FE","\u8F6C\u5230\u4E3B\u7ED3\u679C\uFF0C\u5E76\u5BF9\u5176\u4ED6\u4EBA\u542F\u7528\u9632\u5077\u7AA5\u5BFC\u822A",'\u6B64\u8BBE\u7F6E\u5DF2\u5F03\u7528\uFF0C\u8BF7\u6539\u7528\u5355\u72EC\u7684\u8BBE\u7F6E\uFF0C\u5982"editor.editor.gotoLocation.multipleDefinitions"\u6216"editor.editor.gotoLocation.multipleImplementations"\u3002','\u63A7\u5236\u5B58\u5728\u591A\u4E2A\u76EE\u6807\u4F4D\u7F6E\u65F6"\u8F6C\u5230\u5B9A\u4E49"\u547D\u4EE4\u7684\u884C\u4E3A\u3002','\u63A7\u5236\u5B58\u5728\u591A\u4E2A\u76EE\u6807\u4F4D\u7F6E\u65F6"\u8F6C\u5230\u7C7B\u578B\u5B9A\u4E49"\u547D\u4EE4\u7684\u884C\u4E3A\u3002','\u63A7\u5236\u5B58\u5728\u591A\u4E2A\u76EE\u6807\u4F4D\u7F6E\u65F6"\u8F6C\u5230\u58F0\u660E"\u547D\u4EE4\u7684\u884C\u4E3A\u3002','\u63A7\u5236\u5B58\u5728\u591A\u4E2A\u76EE\u6807\u4F4D\u7F6E\u65F6"\u8F6C\u5230\u5B9E\u73B0"\u547D\u4EE4\u7684\u884C\u4E3A\u3002','\u63A7\u5236\u5B58\u5728\u591A\u4E2A\u76EE\u6807\u4F4D\u7F6E\u65F6"\u8F6C\u5230\u5F15\u7528"\u547D\u4EE4\u7684\u884C\u4E3A\u3002','\u5F53"\u8F6C\u5230\u5B9A\u4E49"\u7684\u7ED3\u679C\u4E3A\u5F53\u524D\u4F4D\u7F6E\u65F6\u5C06\u8981\u6267\u884C\u7684\u66FF\u4EE3\u547D\u4EE4\u7684 ID\u3002','\u5F53"\u8F6C\u5230\u7C7B\u578B\u5B9A\u4E49"\u7684\u7ED3\u679C\u662F\u5F53\u524D\u4F4D\u7F6E\u65F6\u6B63\u5728\u6267\u884C\u7684\u5907\u7528\u547D\u4EE4 ID\u3002','\u5F53"\u8F6C\u5230\u58F0\u660E"\u7684\u7ED3\u679C\u4E3A\u5F53\u524D\u4F4D\u7F6E\u65F6\u5C06\u8981\u6267\u884C\u7684\u66FF\u4EE3\u547D\u4EE4\u7684 ID\u3002','\u5F53"\u8F6C\u5230\u5B9E\u73B0"\u7684\u7ED3\u679C\u4E3A\u5F53\u524D\u4F4D\u7F6E\u65F6\u5C06\u8981\u6267\u884C\u7684\u66FF\u4EE3\u547D\u4EE4\u7684 ID\u3002','\u5F53"\u8F6C\u5230\u5F15\u7528"\u7684\u7ED3\u679C\u662F\u5F53\u524D\u4F4D\u7F6E\u65F6\u6B63\u5728\u6267\u884C\u7684\u66FF\u4EE3\u547D\u4EE4 ID\u3002',"\u63A7\u5236\u662F\u5426\u663E\u793A\u60AC\u505C\u63D0\u793A\u3002","\u63A7\u5236\u663E\u793A\u60AC\u505C\u63D0\u793A\u524D\u7684\u7B49\u5F85\u65F6\u95F4 (\u6BEB\u79D2)\u3002","\u63A7\u5236\u5F53\u9F20\u6807\u79FB\u52A8\u5230\u60AC\u505C\u63D0\u793A\u4E0A\u65F6\uFF0C\u5176\u662F\u5426\u4FDD\u6301\u53EF\u89C1\u3002","\u5728\u7F16\u8F91\u5668\u4E2D\u542F\u7528\u4EE3\u7801\u64CD\u4F5C\u5C0F\u706F\u6CE1\u63D0\u793A\u3002","\u5728\u7F16\u8F91\u5668\u4E2D\u542F\u7528\u5185\u8054\u63D0\u793A\u3002","\u5728\u7F16\u8F91\u5668\u4E2D\u63A7\u5236\u5185\u8054\u63D0\u793A\u7684\u5B57\u53F7\u3002\u8BBE\u7F6E\u4E3A `0` \u65F6\uFF0C\u5C06\u4F7F\u7528 `#editor.fontSize#` \u7684 90%\u3002","\u5728\u7F16\u8F91\u5668\u4E2D\u63A7\u5236\u5185\u8054\u63D0\u793A\u7684\u5B57\u4F53\u7CFB\u5217\u3002","\u63A7\u5236\u884C\u9AD8\u3002\u4E3A 0 \u65F6\u5219\u901A\u8FC7\u5B57\u4F53\u5927\u5C0F\u81EA\u52A8\u8BA1\u7B97\u3002","\u63A7\u5236\u662F\u5426\u663E\u793A\u7F29\u7565\u56FE\u3002","\u8FF7\u4F60\u5730\u56FE\u7684\u5927\u5C0F\u4E0E\u7F16\u8F91\u5668\u5185\u5BB9\u76F8\u540C(\u5E76\u4E14\u53EF\u80FD\u6EDA\u52A8)\u3002","\u8FF7\u4F60\u5730\u56FE\u5C06\u6839\u636E\u9700\u8981\u62C9\u4F38\u6216\u7F29\u5C0F\u4EE5\u586B\u5145\u7F16\u8F91\u5668\u7684\u9AD8\u5EA6(\u4E0D\u6EDA\u52A8)\u3002","\u8FF7\u4F60\u5730\u56FE\u5C06\u6839\u636E\u9700\u8981\u7F29\u5C0F\uFF0C\u6C38\u8FDC\u4E0D\u4F1A\u5927\u4E8E\u7F16\u8F91\u5668(\u4E0D\u6EDA\u52A8)\u3002","\u63A7\u5236\u8FF7\u4F60\u5730\u56FE\u7684\u5927\u5C0F\u3002","\u63A7\u5236\u5728\u54EA\u4E00\u4FA7\u663E\u793A\u7F29\u7565\u56FE\u3002","\u63A7\u5236\u4F55\u65F6\u663E\u793A\u8FF7\u4F60\u5730\u56FE\u6ED1\u5757\u3002","\u5728\u8FF7\u4F60\u5730\u56FE\u4E2D\u7ED8\u5236\u7684\u5185\u5BB9\u6BD4\u4F8B: 1\u30012 \u6216 3\u3002","\u6E32\u67D3\u6BCF\u884C\u7684\u5B9E\u9645\u5B57\u7B26\uFF0C\u800C\u4E0D\u662F\u8272\u5757\u3002","\u9650\u5236\u7F29\u7565\u56FE\u7684\u5BBD\u5EA6\uFF0C\u63A7\u5236\u5176\u6700\u591A\u663E\u793A\u7684\u5217\u6570\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u7684\u9876\u8FB9\u548C\u7B2C\u4E00\u884C\u4E4B\u95F4\u7684\u95F4\u8DDD\u91CF\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u7684\u5E95\u8FB9\u548C\u6700\u540E\u4E00\u884C\u4E4B\u95F4\u7684\u95F4\u8DDD\u91CF\u3002","\u5728\u8F93\u5165\u65F6\u663E\u793A\u542B\u6709\u53C2\u6570\u6587\u6863\u548C\u7C7B\u578B\u4FE1\u606F\u7684\u5C0F\u9762\u677F\u3002","\u63A7\u5236\u53C2\u6570\u63D0\u793A\u83DC\u5355\u5728\u5230\u8FBE\u5217\u8868\u672B\u5C3E\u65F6\u8FDB\u884C\u5FAA\u73AF\u8FD8\u662F\u5173\u95ED\u3002","\u5728\u5B57\u7B26\u4E32\u5185\u542F\u7528\u5FEB\u901F\u5EFA\u8BAE\u3002","\u5728\u6CE8\u91CA\u5185\u542F\u7528\u5FEB\u901F\u5EFA\u8BAE\u3002","\u5728\u5B57\u7B26\u4E32\u548C\u6CE8\u91CA\u5916\u542F\u7528\u5FEB\u901F\u5EFA\u8BAE\u3002","\u63A7\u5236\u662F\u5426\u5728\u952E\u5165\u65F6\u81EA\u52A8\u663E\u793A\u5EFA\u8BAE\u3002","\u4E0D\u663E\u793A\u884C\u53F7\u3002","\u5C06\u884C\u53F7\u663E\u793A\u4E3A\u7EDD\u5BF9\u884C\u6570\u3002","\u5C06\u884C\u53F7\u663E\u793A\u4E3A\u4E0E\u5149\u6807\u76F8\u9694\u7684\u884C\u6570\u3002","\u6BCF 10 \u884C\u663E\u793A\u4E00\u6B21\u884C\u53F7\u3002","\u63A7\u5236\u884C\u53F7\u7684\u663E\u793A\u3002","\u6B64\u7F16\u8F91\u5668\u6807\u5C3A\u5C06\u6E32\u67D3\u7684\u7B49\u5BBD\u5B57\u7B26\u6570\u3002","\u6B64\u7F16\u8F91\u5668\u6807\u5C3A\u7684\u989C\u8272\u3002","\u5728\u4E00\u5B9A\u6570\u91CF\u7684\u7B49\u5BBD\u5B57\u7B26\u540E\u663E\u793A\u5782\u76F4\u6807\u5C3A\u3002\u8F93\u5165\u591A\u4E2A\u503C\uFF0C\u663E\u793A\u591A\u4E2A\u6807\u5C3A\u3002\u82E5\u6570\u7EC4\u4E3A\u7A7A\uFF0C\u5219\u4E0D\u7ED8\u5236\u6807\u5C3A\u3002","\u63D2\u5165\u5EFA\u8BAE\u800C\u4E0D\u8986\u76D6\u5149\u6807\u53F3\u4FA7\u7684\u6587\u672C\u3002","\u63D2\u5165\u5EFA\u8BAE\u5E76\u8986\u76D6\u5149\u6807\u53F3\u4FA7\u7684\u6587\u672C\u3002","\u63A7\u5236\u63A5\u53D7\u8865\u5168\u65F6\u662F\u5426\u8986\u76D6\u5355\u8BCD\u3002\u8BF7\u6CE8\u610F\uFF0C\u8FD9\u53D6\u51B3\u4E8E\u6269\u5C55\u9009\u62E9\u4F7F\u7528\u6B64\u529F\u80FD\u3002","\u63A7\u5236\u5BF9\u5EFA\u8BAE\u7684\u7B5B\u9009\u548C\u6392\u5E8F\u662F\u5426\u8003\u8651\u5C0F\u7684\u62FC\u5199\u9519\u8BEF\u3002","\u63A7\u5236\u6392\u5E8F\u65F6\u662F\u5426\u63D0\u9AD8\u9760\u8FD1\u5149\u6807\u7684\u8BCD\u8BED\u7684\u4F18\u5148\u7EA7\u3002","\u63A7\u5236\u662F\u5426\u5728\u591A\u4E2A\u5DE5\u4F5C\u533A\u548C\u7A97\u53E3\u95F4\u5171\u4EAB\u8BB0\u5FC6\u7684\u5EFA\u8BAE\u9009\u9879(\u9700\u8981 `#editor.suggestSelection#`)\u3002","\u63A7\u5236\u6D3B\u52A8\u4EE3\u7801\u6BB5\u662F\u5426\u963B\u6B62\u5FEB\u901F\u5EFA\u8BAE\u3002","\u63A7\u5236\u662F\u5426\u5728\u5EFA\u8BAE\u4E2D\u663E\u793A\u6216\u9690\u85CF\u56FE\u6807\u3002","\u63A7\u5236\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u5E95\u90E8\u7684\u72B6\u6001\u680F\u7684\u53EF\u89C1\u6027\u3002","\u63A7\u5236\u5EFA\u8BAE\u8BE6\u7EC6\u4FE1\u606F\u662F\u968F\u6807\u7B7E\u4E00\u8D77\u663E\u793A\u8FD8\u662F\u4EC5\u663E\u793A\u5728\u8BE6\u7EC6\u4FE1\u606F\u5C0F\u7EC4\u4EF6\u4E2D","\u6B64\u8BBE\u7F6E\u5DF2\u5F03\u7528\u3002\u73B0\u5728\u53EF\u4EE5\u8C03\u6574\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u7684\u5927\u5C0F\u3002",'\u6B64\u8BBE\u7F6E\u5DF2\u5F03\u7528\uFF0C\u8BF7\u6539\u7528\u5355\u72EC\u7684\u8BBE\u7F6E\uFF0C\u5982"editor.suggest.showKeywords"\u6216"editor.suggest.showSnippets"\u3002',"\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u65B9\u6CD5\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u51FD\u6570\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u6784\u9020\u51FD\u6570\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u5B57\u6BB5\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u53D8\u91CF\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u7C7B\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u7ED3\u6784\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u63A5\u53E3\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u6A21\u5757\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u5C5E\u6027\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u4E8B\u4EF6\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u64CD\u4F5C\u7B26\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u5355\u4F4D\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u503C\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u5E38\u91CF\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u679A\u4E3E\u201D\u5EFA\u8BAE\u3002",'\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A "enumMember" \u5EFA\u8BAE\u3002',"\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u5173\u952E\u5B57\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u6587\u672C\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u989C\u8272\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u6587\u4EF6\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u53C2\u8003\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u81EA\u5B9A\u4E49\u989C\u8272\u201D\u5EFA\u8BAE\u3002","\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u6587\u4EF6\u5939\u201D\u5EFA\u8BAE\u3002",'\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A "typeParameter" \u5EFA\u8BAE\u3002',"\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A\u201C\u7247\u6BB5\u201D\u5EFA\u8BAE\u3002",'\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A"\u7528\u6237"\u5EFA\u8BAE\u3002','\u542F\u7528\u540E\uFF0CIntelliSense \u5C06\u663E\u793A"\u95EE\u9898"\u5EFA\u8BAE\u3002',"\u662F\u5426\u5E94\u59CB\u7EC8\u9009\u62E9\u524D\u5BFC\u548C\u5C3E\u968F\u7A7A\u683C\u3002","\u63A7\u5236\u662F\u5426\u5E94\u5728\u9047\u5230\u63D0\u4EA4\u5B57\u7B26\u65F6\u63A5\u53D7\u5EFA\u8BAE\u3002\u4F8B\u5982\uFF0C\u5728 JavaScript \u4E2D\uFF0C\u534A\u89D2\u5206\u53F7 (`;`) \u53EF\u4EE5\u4E3A\u63D0\u4EA4\u5B57\u7B26\uFF0C\u80FD\u591F\u5728\u63A5\u53D7\u5EFA\u8BAE\u7684\u540C\u65F6\u952E\u5165\u8BE5\u5B57\u7B26\u3002","\u4EC5\u5F53\u5EFA\u8BAE\u5305\u542B\u6587\u672C\u6539\u52A8\u65F6\u624D\u53EF\u4F7F\u7528 `Enter` \u952E\u8FDB\u884C\u63A5\u53D7\u3002","\u63A7\u5236\u9664\u4E86 `Tab` \u952E\u4EE5\u5916\uFF0C `Enter` \u952E\u662F\u5426\u540C\u6837\u53EF\u4EE5\u63A5\u53D7\u5EFA\u8BAE\u3002\u8FD9\u80FD\u51CF\u5C11\u201C\u63D2\u5165\u65B0\u884C\u201D\u548C\u201C\u63A5\u53D7\u5EFA\u8BAE\u201D\u547D\u4EE4\u4E4B\u95F4\u7684\u6B67\u4E49\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u4E2D\u53EF\u7531\u5C4F\u5E55\u9605\u8BFB\u5668\u8BFB\u53D6\u7684\u884C\u6570\u3002\u8B66\u544A: \u5BF9\u4E8E\u5927\u4E8E\u9ED8\u8BA4\u503C\u7684\u6570\u5B57\uFF0C\u8FD9\u4F1A\u5F71\u54CD\u6027\u80FD\u3002","\u7F16\u8F91\u5668\u5185\u5BB9","\u4F7F\u7528\u8BED\u8A00\u914D\u7F6E\u786E\u5B9A\u4F55\u65F6\u81EA\u52A8\u95ED\u5408\u62EC\u53F7\u3002","\u4EC5\u5F53\u5149\u6807\u4F4D\u4E8E\u7A7A\u767D\u5B57\u7B26\u5DE6\u4FA7\u65F6\uFF0C\u624D\u81EA\u52A8\u95ED\u5408\u62EC\u53F7\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5728\u5DE6\u62EC\u53F7\u540E\u81EA\u52A8\u63D2\u5165\u53F3\u62EC\u53F7\u3002","\u4EC5\u5728\u81EA\u52A8\u63D2\u5165\u65F6\u624D\u6539\u5199\u53F3\u5F15\u53F7\u6216\u53F3\u62EC\u53F7\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5E94\u6539\u5199\u53F3\u5F15\u53F7\u6216\u53F3\u62EC\u53F7\u3002","\u4F7F\u7528\u8BED\u8A00\u914D\u7F6E\u786E\u5B9A\u4F55\u65F6\u81EA\u52A8\u95ED\u5408\u5F15\u53F7\u3002","\u4EC5\u5F53\u5149\u6807\u4F4D\u4E8E\u7A7A\u767D\u5B57\u7B26\u5DE6\u4FA7\u65F6\uFF0C\u624D\u81EA\u52A8\u95ED\u5408\u5F15\u53F7\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5728\u5DE6\u5F15\u53F7\u540E\u81EA\u52A8\u63D2\u5165\u53F3\u5F15\u53F7\u3002","\u7F16\u8F91\u5668\u4E0D\u4F1A\u81EA\u52A8\u63D2\u5165\u7F29\u8FDB\u3002","\u7F16\u8F91\u5668\u5C06\u4FDD\u7559\u5F53\u524D\u884C\u7684\u7F29\u8FDB\u3002","\u7F16\u8F91\u5668\u5C06\u4FDD\u7559\u5F53\u524D\u884C\u7684\u7F29\u8FDB\u5E76\u9075\u5FAA\u8BED\u8A00\u5B9A\u4E49\u7684\u62EC\u53F7\u3002","\u7F16\u8F91\u5668\u5C06\u4FDD\u7559\u5F53\u524D\u884C\u7684\u7F29\u8FDB\u3001\u4F7F\u7528\u8BED\u8A00\u5B9A\u4E49\u7684\u62EC\u53F7\u5E76\u8C03\u7528\u8BED\u8A00\u5B9A\u4E49\u7684\u7279\u5B9A onEnterRules\u3002","\u7F16\u8F91\u5668\u5C06\u4FDD\u7559\u5F53\u524D\u884C\u7684\u7F29\u8FDB\uFF0C\u4F7F\u7528\u8BED\u8A00\u5B9A\u4E49\u7684\u62EC\u53F7\uFF0C\u8C03\u7528\u7531\u8BED\u8A00\u5B9A\u4E49\u7684\u7279\u6B8A\u8F93\u5165\u89C4\u5219\uFF0C\u5E76\u9075\u5FAA\u7531\u8BED\u8A00\u5B9A\u4E49\u7684\u7F29\u8FDB\u89C4\u5219\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5E94\u5728\u7528\u6237\u952E\u5165\u3001\u7C98\u8D34\u3001\u79FB\u52A8\u6216\u7F29\u8FDB\u884C\u65F6\u81EA\u52A8\u8C03\u6574\u7F29\u8FDB\u3002","\u4F7F\u7528\u8BED\u8A00\u914D\u7F6E\u786E\u5B9A\u4F55\u65F6\u81EA\u52A8\u5305\u4F4F\u6240\u9009\u5185\u5BB9\u3002","\u4F7F\u7528\u5F15\u53F7\u800C\u975E\u62EC\u53F7\u6765\u5305\u4F4F\u6240\u9009\u5185\u5BB9\u3002","\u4F7F\u7528\u62EC\u53F7\u800C\u975E\u5F15\u53F7\u6765\u5305\u4F4F\u6240\u9009\u5185\u5BB9\u3002","\u63A7\u5236\u5728\u952E\u5165\u5F15\u53F7\u6216\u65B9\u62EC\u53F7\u65F6\uFF0C\u7F16\u8F91\u5668\u662F\u5426\u5E94\u81EA\u52A8\u5C06\u6240\u9009\u5185\u5BB9\u62EC\u8D77\u6765\u3002","\u5728\u4F7F\u7528\u7A7A\u683C\u8FDB\u884C\u7F29\u8FDB\u65F6\u6A21\u62DF\u5236\u8868\u7B26\u7684\u9009\u62E9\u884C\u4E3A\u3002\u6240\u9009\u5185\u5BB9\u5C06\u59CB\u7EC8\u4F7F\u7528\u5236\u8868\u7B26\u505C\u6B62\u4F4D\u3002","\u63A7\u5236\u662F\u5426\u5728\u7F16\u8F91\u5668\u4E2D\u663E\u793A CodeLens\u3002","\u63A7\u5236 CodeLens \u7684\u5B57\u4F53\u7CFB\u5217\u3002","\u63A7\u5236 CodeLens \u7684\u5B57\u4F53\u5927\u5C0F(\u50CF\u7D20)\u3002\u8BBE\u7F6E\u4E3A `0` \u65F6\uFF0C\u5C06\u4F7F\u7528 `#editor.fontSize#` \u7684 90%\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u663E\u793A\u5185\u8054\u989C\u8272\u4FEE\u9970\u5668\u548C\u989C\u8272\u9009\u53D6\u5668\u3002","\u542F\u7528\u4F7F\u7528\u9F20\u6807\u548C\u952E\u8FDB\u884C\u5217\u9009\u62E9\u3002","\u63A7\u5236\u5728\u590D\u5236\u65F6\u662F\u5426\u540C\u65F6\u590D\u5236\u8BED\u6CD5\u9AD8\u4EAE\u3002","\u63A7\u5236\u5149\u6807\u7684\u52A8\u753B\u6837\u5F0F\u3002","\u63A7\u5236\u662F\u5426\u542F\u7528\u5E73\u6ED1\u63D2\u5165\u52A8\u753B\u3002","\u63A7\u5236\u5149\u6807\u6837\u5F0F\u3002",'\u63A7\u5236\u5149\u6807\u5468\u56F4\u53EF\u89C1\u7684\u524D\u7F6E\u884C\u548C\u5C3E\u968F\u884C\u7684\u6700\u5C0F\u6570\u76EE\u3002\u5728\u5176\u4ED6\u4E00\u4E9B\u7F16\u8F91\u5668\u4E2D\u79F0\u4E3A "scrollOff" \u6216 "scrollOffset"\u3002','\u4EC5\u5F53\u901A\u8FC7\u952E\u76D8\u6216 API \u89E6\u53D1\u65F6\uFF0C\u624D\u4F1A\u5F3A\u5236\u6267\u884C"\u5149\u6807\u73AF\u7ED5\u884C"\u3002','\u59CB\u7EC8\u5F3A\u5236\u6267\u884C "cursorSurroundingLines"','\u63A7\u5236\u4F55\u65F6\u5E94\u5F3A\u5236\u6267\u884C"\u5149\u6807\u73AF\u7ED5\u884C"\u3002',"\u5F53 `#editor.cursorStyle#` \u8BBE\u7F6E\u4E3A `line` \u65F6\uFF0C\u63A7\u5236\u5149\u6807\u7684\u5BBD\u5EA6\u3002","\u63A7\u5236\u5728\u7F16\u8F91\u5668\u4E2D\u662F\u5426\u5141\u8BB8\u901A\u8FC7\u62D6\u653E\u6765\u79FB\u52A8\u9009\u4E2D\u5185\u5BB9\u3002",'\u6309\u4E0B"Alt"\u65F6\u6EDA\u52A8\u901F\u5EA6\u500D\u589E\u3002',"\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u542F\u7528\u4E86\u4EE3\u7801\u6298\u53E0\u3002","\u4F7F\u7528\u7279\u5B9A\u4E8E\u8BED\u8A00\u7684\u6298\u53E0\u7B56\u7565(\u5982\u679C\u53EF\u7528)\uFF0C\u5426\u5219\u4F7F\u7528\u57FA\u4E8E\u7F29\u8FDB\u7684\u7B56\u7565\u3002","\u4F7F\u7528\u57FA\u4E8E\u7F29\u8FDB\u7684\u6298\u53E0\u7B56\u7565\u3002","\u63A7\u5236\u8BA1\u7B97\u6298\u53E0\u8303\u56F4\u7684\u7B56\u7565\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5E94\u7A81\u51FA\u663E\u793A\u6298\u53E0\u8303\u56F4\u3002","\u63A7\u5236\u5355\u51FB\u5DF2\u6298\u53E0\u7684\u884C\u540E\u9762\u7684\u7A7A\u5185\u5BB9\u662F\u5426\u4F1A\u5C55\u5F00\u8BE5\u884C\u3002","\u63A7\u5236\u5B57\u4F53\u7CFB\u5217\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u81EA\u52A8\u683C\u5F0F\u5316\u7C98\u8D34\u7684\u5185\u5BB9\u3002\u683C\u5F0F\u5316\u7A0B\u5E8F\u5FC5\u987B\u53EF\u7528\uFF0C\u5E76\u4E14\u80FD\u9488\u5BF9\u6587\u6863\u4E2D\u7684\u67D0\u4E00\u8303\u56F4\u8FDB\u884C\u683C\u5F0F\u5316\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u5728\u952E\u5165\u4E00\u884C\u540E\u662F\u5426\u81EA\u52A8\u683C\u5F0F\u5316\u8BE5\u884C\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5E94\u5448\u73B0\u5782\u76F4\u5B57\u5F62\u8FB9\u8DDD\u3002\u5B57\u5F62\u8FB9\u8DDD\u6700\u5E38\u7528\u4E8E\u8C03\u8BD5\u3002","\u63A7\u5236\u662F\u5426\u5728\u6982\u89C8\u6807\u5C3A\u4E2D\u9690\u85CF\u5149\u6807\u3002","\u63A7\u5236\u662F\u5426\u7A81\u51FA\u663E\u793A\u7F16\u8F91\u5668\u4E2D\u6D3B\u52A8\u7684\u7F29\u8FDB\u53C2\u8003\u7EBF\u3002","\u63A7\u5236\u5B57\u6BCD\u95F4\u8DDD(\u50CF\u7D20)\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5DF2\u542F\u7528\u94FE\u63A5\u7F16\u8F91\u3002\u76F8\u5173\u7B26\u53F7(\u5982 HTML \u6807\u8BB0)\u5728\u7F16\u8F91\u65F6\u8FDB\u884C\u66F4\u65B0\uFF0C\u5177\u4F53\u7531\u8BED\u8A00\u800C\u5B9A\u3002","\u63A7\u5236\u662F\u5426\u5728\u7F16\u8F91\u5668\u4E2D\u68C0\u6D4B\u94FE\u63A5\u5E76\u4F7F\u5176\u53EF\u88AB\u70B9\u51FB\u3002","\u7A81\u51FA\u663E\u793A\u5339\u914D\u7684\u62EC\u53F7\u3002","\u5BF9\u9F20\u6807\u6EDA\u8F6E\u6EDA\u52A8\u4E8B\u4EF6\u7684 `deltaX` \u548C `deltaY` \u4E58\u4E0A\u7684\u7CFB\u6570\u3002","\u6309\u4F4F `Ctrl` \u952E\u5E76\u6EDA\u52A8\u9F20\u6807\u6EDA\u8F6E\u65F6\u5BF9\u7F16\u8F91\u5668\u5B57\u4F53\u5927\u5C0F\u8FDB\u884C\u7F29\u653E\u3002","\u5F53\u591A\u4E2A\u5149\u6807\u91CD\u53E0\u65F6\u8FDB\u884C\u5408\u5E76\u3002","\u6620\u5C04\u4E3A `Ctrl` (Windows \u548C Linux) \u6216 `Command` (macOS)\u3002","\u6620\u5C04\u4E3A `Alt` (Windows \u548C Linux) \u6216 `Option` (macOS)\u3002","\u5728\u901A\u8FC7\u9F20\u6807\u6DFB\u52A0\u591A\u4E2A\u5149\u6807\u65F6\u4F7F\u7528\u7684\u4FEE\u6539\u952E\u3002\u201C\u8F6C\u5230\u5B9A\u4E49\u201D\u548C\u201C\u6253\u5F00\u94FE\u63A5\u201D\u529F\u80FD\u6240\u9700\u7684\u9F20\u6807\u52A8\u4F5C\u5C06\u4F1A\u76F8\u5E94\u8C03\u6574\uFF0C\u4E0D\u4E0E\u591A\u5149\u6807\u4FEE\u6539\u952E\u51B2\u7A81\u3002[\u9605\u8BFB\u8BE6\u7EC6\u4FE1\u606F](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier)\u3002","\u6BCF\u4E2A\u5149\u6807\u7C98\u8D34\u4E00\u884C\u6587\u672C\u3002","\u6BCF\u4E2A\u5149\u6807\u7C98\u8D34\u5168\u6587\u3002","\u63A7\u5236\u7C98\u8D34\u65F6\u7C98\u8D34\u6587\u672C\u7684\u884C\u8BA1\u6570\u4E0E\u5149\u6807\u8BA1\u6570\u76F8\u5339\u914D\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u7A81\u51FA\u663E\u793A\u8BED\u4E49\u7B26\u53F7\u7684\u5339\u914D\u9879\u3002","\u63A7\u5236\u662F\u5426\u5728\u6982\u89C8\u6807\u5C3A\u5468\u56F4\u7ED8\u5236\u8FB9\u6846\u3002","\u6253\u5F00\u901F\u89C8\u65F6\u805A\u7126\u6811","\u6253\u5F00\u9884\u89C8\u65F6\u5C06\u7126\u70B9\u653E\u5728\u7F16\u8F91\u5668\u4E0A","\u63A7\u5236\u662F\u5C06\u7126\u70B9\u653E\u5728\u5185\u8054\u7F16\u8F91\u5668\u4E0A\u8FD8\u662F\u653E\u5728\u9884\u89C8\u5C0F\u90E8\u4EF6\u4E2D\u7684\u6811\u4E0A\u3002",'\u63A7\u5236"\u8F6C\u5230\u5B9A\u4E49"\u9F20\u6807\u624B\u52BF\u662F\u5426\u59CB\u7EC8\u6253\u5F00\u9884\u89C8\u5C0F\u90E8\u4EF6\u3002',"\u63A7\u5236\u663E\u793A\u5FEB\u901F\u5EFA\u8BAE\u524D\u7684\u7B49\u5F85\u65F6\u95F4 (\u6BEB\u79D2)\u3002","\u63A7\u5236\u662F\u5426\u5728\u7F16\u8F91\u5668\u4E2D\u8F93\u5165\u65F6\u81EA\u52A8\u91CD\u547D\u540D\u3002",'\u5DF2\u5F03\u7528\uFF0C\u8BF7\u6539\u7528 "editor.linkedEditing"\u3002',"\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u663E\u793A\u63A7\u5236\u5B57\u7B26\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u663E\u793A\u7F29\u8FDB\u53C2\u8003\u7EBF\u3002","\u5F53\u6587\u4EF6\u4EE5\u6362\u884C\u7B26\u7ED3\u675F\u65F6, \u5448\u73B0\u6700\u540E\u4E00\u884C\u7684\u884C\u53F7\u3002","\u540C\u65F6\u7A81\u51FA\u663E\u793A\u5BFC\u822A\u7EBF\u548C\u5F53\u524D\u884C\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u7684\u5F53\u524D\u884C\u8FDB\u884C\u9AD8\u4EAE\u663E\u793A\u7684\u65B9\u5F0F\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u4EC5\u5728\u7126\u70B9\u5728\u7F16\u8F91\u5668\u65F6\u7A81\u51FA\u663E\u793A\u5F53\u524D\u884C","\u5448\u73B0\u7A7A\u683C\u5B57\u7B26(\u5B57\u8BCD\u4E4B\u95F4\u7684\u5355\u4E2A\u7A7A\u683C\u9664\u5916)\u3002","\u4EC5\u5728\u9009\u5B9A\u6587\u672C\u4E0A\u5448\u73B0\u7A7A\u767D\u5B57\u7B26\u3002","\u4EC5\u5448\u73B0\u5C3E\u968F\u7A7A\u683C\u5B57\u7B26","\u63A7\u5236\u7F16\u8F91\u5668\u5728\u7A7A\u767D\u5B57\u7B26\u4E0A\u663E\u793A\u7B26\u53F7\u7684\u65B9\u5F0F\u3002","\u63A7\u5236\u9009\u533A\u662F\u5426\u6709\u5706\u89D2\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u6C34\u5E73\u6EDA\u52A8\u65F6\u53EF\u4EE5\u8D85\u8FC7\u8303\u56F4\u7684\u5B57\u7B26\u6570\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u53EF\u4EE5\u6EDA\u52A8\u5230\u6700\u540E\u4E00\u884C\u4E4B\u540E\u3002","\u540C\u65F6\u5782\u76F4\u548C\u6C34\u5E73\u6EDA\u52A8\u65F6\uFF0C\u4EC5\u6CBF\u4E3B\u8F74\u6EDA\u52A8\u3002\u5728\u89E6\u63A7\u677F\u4E0A\u5782\u76F4\u6EDA\u52A8\u65F6\uFF0C\u53EF\u9632\u6B62\u6C34\u5E73\u6F02\u79FB\u3002","\u63A7\u5236\u662F\u5426\u652F\u6301 Linux \u4E3B\u526A\u8D34\u677F\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5E94\u7A81\u51FA\u663E\u793A\u4E0E\u6240\u9009\u5185\u5BB9\u7C7B\u4F3C\u7684\u5339\u914D\u9879\u3002","\u59CB\u7EC8\u663E\u793A\u6298\u53E0\u63A7\u4EF6\u3002","\u4EC5\u5728\u9F20\u6807\u4F4D\u4E8E\u88C5\u8BA2\u7EBF\u4E0A\u65B9\u65F6\u663E\u793A\u6298\u53E0\u63A7\u4EF6\u3002","\u63A7\u5236\u4F55\u65F6\u663E\u793A\u884C\u53F7\u69FD\u4E0A\u7684\u6298\u53E0\u63A7\u4EF6\u3002","\u63A7\u5236\u662F\u5426\u6DE1\u5316\u672A\u4F7F\u7528\u7684\u4EE3\u7801\u3002","\u63A7\u5236\u52A0\u5220\u9664\u7EBF\u88AB\u5F03\u7528\u7684\u53D8\u91CF\u3002","\u5728\u5176\u4ED6\u5EFA\u8BAE\u4E0A\u65B9\u663E\u793A\u4EE3\u7801\u7247\u6BB5\u5EFA\u8BAE\u3002","\u5728\u5176\u4ED6\u5EFA\u8BAE\u4E0B\u65B9\u663E\u793A\u4EE3\u7801\u7247\u6BB5\u5EFA\u8BAE\u3002","\u5728\u5176\u4ED6\u5EFA\u8BAE\u4E2D\u7A7F\u63D2\u663E\u793A\u4EE3\u7801\u7247\u6BB5\u5EFA\u8BAE\u3002","\u4E0D\u663E\u793A\u4EE3\u7801\u7247\u6BB5\u5EFA\u8BAE\u3002","\u63A7\u5236\u4EE3\u7801\u7247\u6BB5\u662F\u5426\u4E0E\u5176\u4ED6\u5EFA\u8BAE\u4E00\u8D77\u663E\u793A\u53CA\u5176\u6392\u5217\u7684\u4F4D\u7F6E\u3002","\u63A7\u5236\u7F16\u8F91\u5668\u662F\u5426\u5728\u6EDA\u52A8\u65F6\u4F7F\u7528\u52A8\u753B\u3002","\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u7684\u5B57\u53F7\u3002\u5982\u679C\u8BBE\u7F6E\u4E3A `0`\uFF0C\u5219\u4F7F\u7528 `#editor.fontSize#` \u7684\u503C\u3002","\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u7684\u884C\u9AD8\u3002\u5982\u679C\u8BBE\u7F6E\u4E3A `0`\uFF0C\u5219\u4F7F\u7528 `#editor.lineHeight#` \u7684\u503C\u3002\u6700\u5C0F\u503C\u4E3A 8\u3002","\u63A7\u5236\u5728\u952E\u5165\u89E6\u53D1\u5B57\u7B26\u540E\u662F\u5426\u81EA\u52A8\u663E\u793A\u5EFA\u8BAE\u3002","\u59CB\u7EC8\u9009\u62E9\u7B2C\u4E00\u4E2A\u5EFA\u8BAE\u3002","\u9009\u62E9\u6700\u8FD1\u7684\u5EFA\u8BAE\uFF0C\u9664\u975E\u8FDB\u4E00\u6B65\u952E\u5165\u9009\u62E9\u5176\u4ED6\u9879\u3002\u4F8B\u5982 `console. -> console.log`\uFF0C\u56E0\u4E3A\u6700\u8FD1\u8865\u5168\u8FC7 `log`\u3002","\u6839\u636E\u4E4B\u524D\u8865\u5168\u8FC7\u7684\u5EFA\u8BAE\u7684\u524D\u7F00\u6765\u8FDB\u884C\u9009\u62E9\u3002\u4F8B\u5982\uFF0C`co -> console`\u3001`con -> const`\u3002","\u63A7\u5236\u5728\u5EFA\u8BAE\u5217\u8868\u4E2D\u5982\u4F55\u9884\u5148\u9009\u62E9\u5EFA\u8BAE\u3002","\u5728\u6309\u4E0B Tab \u952E\u65F6\u8FDB\u884C Tab \u8865\u5168\uFF0C\u5C06\u63D2\u5165\u6700\u4F73\u5339\u914D\u5EFA\u8BAE\u3002","\u7981\u7528 Tab \u8865\u5168\u3002",'\u5728\u524D\u7F00\u5339\u914D\u65F6\u8FDB\u884C Tab \u8865\u5168\u3002\u5728 "quickSuggestions" \u672A\u542F\u7528\u65F6\u4F53\u9A8C\u6700\u597D\u3002',"\u542F\u7528 Tab \u8865\u5168\u3002","\u81EA\u52A8\u5220\u9664\u5F02\u5E38\u7684\u884C\u7EC8\u6B62\u7B26\u3002","\u5FFD\u7565\u5F02\u5E38\u7684\u884C\u7EC8\u6B62\u7B26\u3002","\u63D0\u793A\u5220\u9664\u5F02\u5E38\u7684\u884C\u7EC8\u6B62\u7B26\u3002","\u5220\u9664\u53EF\u80FD\u5BFC\u81F4\u95EE\u9898\u7684\u5F02\u5E38\u884C\u7EC8\u6B62\u7B26\u3002","\u6839\u636E\u5236\u8868\u4F4D\u63D2\u5165\u548C\u5220\u9664\u7A7A\u683C\u3002","\u6267\u884C\u5355\u8BCD\u76F8\u5173\u7684\u5BFC\u822A\u6216\u64CD\u4F5C\u65F6\u4F5C\u4E3A\u5355\u8BCD\u5206\u9694\u7B26\u7684\u5B57\u7B26\u3002","\u6C38\u4E0D\u6362\u884C\u3002","\u5C06\u5728\u89C6\u533A\u5BBD\u5EA6\u5904\u6362\u884C\u3002","\u5728 `#editor.wordWrapColumn#` \u5904\u6298\u884C\u3002","\u5728\u89C6\u533A\u5BBD\u5EA6\u548C `#editor.wordWrapColumn#` \u4E2D\u7684\u8F83\u5C0F\u503C\u5904\u6298\u884C\u3002","\u63A7\u5236\u6298\u884C\u7684\u65B9\u5F0F\u3002","\u5728 `#editor.wordWrap#` \u4E3A `wordWrapColumn` \u6216 `bounded` \u65F6\uFF0C\u63A7\u5236\u7F16\u8F91\u5668\u7684\u6298\u884C\u5217\u3002","\u6CA1\u6709\u7F29\u8FDB\u3002\u6298\u884C\u4ECE\u7B2C 1 \u5217\u5F00\u59CB\u3002","\u6298\u884C\u7684\u7F29\u8FDB\u91CF\u4E0E\u5176\u7236\u7EA7\u76F8\u540C\u3002","\u6298\u884C\u7684\u7F29\u8FDB\u91CF\u6BD4\u5176\u7236\u7EA7\u591A 1\u3002","\u6298\u884C\u7684\u7F29\u8FDB\u91CF\u6BD4\u5176\u7236\u7EA7\u591A 2\u3002","\u63A7\u5236\u6298\u884C\u7684\u7F29\u8FDB\u3002","\u5047\u5B9A\u6240\u6709\u5B57\u7B26\u7684\u5BBD\u5EA6\u76F8\u540C\u3002\u8FD9\u662F\u4E00\u79CD\u5FEB\u901F\u7B97\u6CD5\uFF0C\u9002\u7528\u4E8E\u7B49\u5BBD\u5B57\u4F53\u548C\u67D0\u4E9B\u5B57\u5F62\u5BBD\u5EA6\u76F8\u7B49\u7684\u6587\u5B57(\u5982\u62C9\u4E01\u5B57\u7B26)\u3002","\u5C06\u5305\u88C5\u70B9\u8BA1\u7B97\u59D4\u6258\u7ED9\u6D4F\u89C8\u5668\u3002\u8FD9\u662F\u4E00\u4E2A\u7F13\u6162\u7B97\u6CD5\uFF0C\u53EF\u80FD\u4F1A\u5BFC\u81F4\u5927\u578B\u6587\u4EF6\u88AB\u51BB\u7ED3\uFF0C\u4F46\u5B83\u5728\u6240\u6709\u60C5\u51B5\u4E0B\u90FD\u6B63\u5E38\u5DE5\u4F5C\u3002","\u63A7\u5236\u8BA1\u7B97\u5305\u88F9\u70B9\u7684\u7B97\u6CD5\u3002"],"vs/editor/common/editorContextKeys":["Whether the editor text has focus (cursor is blinking)","Whether the editor or an editor widget has focus (e.g. focus is in the find widget)","Whether an editor or a rich text input has focus (cursor is blinking)","Whether the editor is read only","Whether the context is a diff editor","Whether `editor.columnSelection` is enabled","Whether the editor has text selected","Whether the editor has multiple selections","Whether `Tab` will move focus out of the editor","Whether the editor hover is visible","Whether the editor is part of a larger editor (e.g. notebooks)","The language identifier of the editor","Whether the editor has a completion item provider","Whether the editor has a code actions provider","Whether the editor has a code lens provider","Whether the editor has a definition provider","Whether the editor has a declaration provider","Whether the editor has an implementation provider","Whether the editor has a type definition provider","Whether the editor has a hover provider","Whether the editor has a document highlight provider","Whether the editor has a document symbol provider","Whether the editor has a reference provider","Whether the editor has a rename provider","Whether the editor has a signature help provider","Whether the editor has an inline hints provider","Whether the editor has a document formatting provider","Whether the editor has a document selection formatting provider","Whether the editor has multiple document formatting providers","Whether the editor has multiple document selection formatting providers"],"vs/editor/common/model/editStack":["\u8F93\u5165"],"vs/editor/common/modes/modesRegistry":["\u7EAF\u6587\u672C"],"vs/editor/common/standaloneStrings":["\u65E0\u9009\u62E9","\u884C {0}, \u5217 {1} (\u9009\u4E2D {2})","\u884C {0}, \u5217 {1}","{0} \u9009\u62E9(\u5DF2\u9009\u62E9 {1} \u4E2A\u5B57\u7B26)","{0} \u9009\u62E9",'\u73B0\u5728\u5C06 "\u8F85\u52A9\u529F\u80FD\u652F\u6301" \u8BBE\u7F6E\u66F4\u6539\u4E3A "\u6253\u5F00"\u3002',"\u73B0\u5728\u6B63\u5728\u6253\u5F00\u201C\u7F16\u8F91\u5668\u8F85\u52A9\u529F\u80FD\u201D\u6587\u6863\u9875\u3002","\u5728\u5DEE\u5F02\u7F16\u8F91\u5668\u7684\u53EA\u8BFB\u7A97\u683C\u4E2D\u3002","\u5728\u4E00\u4E2A\u5DEE\u5F02\u7F16\u8F91\u5668\u7684\u7A97\u683C\u4E2D\u3002","\u5728\u53EA\u8BFB\u4EE3\u7801\u7F16\u8F91\u5668\u4E2D","\u5728\u4EE3\u7801\u7F16\u8F91\u5668\u4E2D","\u82E5\u8981\u914D\u7F6E\u7F16\u8F91\u5668\uFF0C\u5C06\u5176\u8FDB\u884C\u4F18\u5316\u4EE5\u6700\u597D\u5730\u914D\u5408\u5C4F\u5E55\u9605\u8BFB\u5668\u7684\u4F7F\u7528\uFF0C\u8BF7\u7ACB\u5373\u6309 Command+E\u3002","\u82E5\u8981\u914D\u7F6E\u7F16\u8F91\u5668\uFF0C\u5C06\u5176\u8FDB\u884C\u4F18\u5316\u4EE5\u6700\u9AD8\u6548\u5730\u914D\u5408\u5C4F\u5E55\u9605\u8BFB\u5668\u7684\u4F7F\u7528\uFF0C\u6309\u4E0B Ctrl+E\u3002","\u914D\u7F6E\u7F16\u8F91\u5668\uFF0C\u5C06\u5176\u8FDB\u884C\u4F18\u5316\u4EE5\u6700\u597D\u5730\u914D\u5408\u5C4F\u5E55\u8BFB\u53D6\u5668\u7684\u4F7F\u7528\u3002","\u7F16\u8F91\u5668\u88AB\u914D\u7F6E\u4E3A\u6C38\u8FDC\u4E0D\u8FDB\u884C\u4F18\u5316\u4EE5\u914D\u5408\u5C4F\u5E55\u8BFB\u53D6\u5668\u7684\u4F7F\u7528, \u800C\u5F53\u524D\u4E0D\u662F\u8FD9\u79CD\u60C5\u51B5\u3002","\u5728\u5F53\u524D\u7F16\u8F91\u5668\u4E2D\u6309 Tab \u4F1A\u5C06\u7126\u70B9\u79FB\u52A8\u5230\u4E0B\u4E00\u4E2A\u53EF\u805A\u7126\u7684\u5143\u7D20\u3002\u901A\u8FC7\u6309 {0} \u5207\u6362\u6B64\u884C\u4E3A\u3002","\u5728\u5F53\u524D\u7F16\u8F91\u5668\u4E2D\u6309 Tab \u4F1A\u5C06\u7126\u70B9\u79FB\u52A8\u5230\u4E0B\u4E00\u4E2A\u53EF\u805A\u7126\u7684\u5143\u7D20\u3002\u5F53\u524D\u65E0\u6CD5\u901A\u8FC7\u6309\u952E\u7ED1\u5B9A\u89E6\u53D1\u547D\u4EE4 {0}\u3002","\u5728\u5F53\u524D\u7F16\u8F91\u5668\u4E2D\u6309 Tab \u5C06\u63D2\u5165\u5236\u8868\u7B26\u3002\u901A\u8FC7\u6309 {0} \u5207\u6362\u6B64\u884C\u4E3A\u3002","\u5728\u5F53\u524D\u7F16\u8F91\u5668\u4E2D\u6309 Tab \u4F1A\u63D2\u5165\u5236\u8868\u7B26\u3002\u5F53\u524D\u65E0\u6CD5\u901A\u8FC7\u952E\u7ED1\u5B9A\u89E6\u53D1\u547D\u4EE4 {0}\u3002","\u73B0\u5728\u6309 Command+H \u6253\u5F00\u4E00\u4E2A\u6D4F\u89C8\u5668\u7A97\u53E3, \u5176\u4E2D\u5305\u542B\u6709\u5173\u7F16\u8F91\u5668\u8F85\u52A9\u529F\u80FD\u7684\u8BE6\u7EC6\u4FE1\u606F\u3002","\u73B0\u5728\u6309 Ctrl+H \u6253\u5F00\u4E00\u4E2A\u6D4F\u89C8\u5668\u7A97\u53E3, \u5176\u4E2D\u5305\u542B\u6709\u5173\u7F16\u8F91\u5668\u8F85\u52A9\u529F\u80FD\u7684\u66F4\u591A\u4FE1\u606F\u3002","\u4F60\u53EF\u4EE5\u6309 Esc \u6216 Shift+Esc \u6D88\u9664\u6B64\u5DE5\u5177\u63D0\u793A\u5E76\u8FD4\u56DE\u5230\u7F16\u8F91\u5668\u3002","\u663E\u793A\u8F85\u52A9\u529F\u80FD\u5E2E\u52A9","\u5F00\u53D1\u4EBA\u5458: \u68C0\u67E5\u4EE4\u724C","\u8F6C\u5230\u884C/\u5217...","\u663E\u793A\u6240\u6709\u5FEB\u901F\u8BBF\u95EE\u63D0\u4F9B\u7A0B\u5E8F","\u547D\u4EE4\u9762\u677F","\u663E\u793A\u5E76\u8FD0\u884C\u547D\u4EE4","\u8F6C\u5230\u7B26\u53F7...","\u6309\u7C7B\u522B\u8F6C\u5230\u7B26\u53F7...","\u7F16\u8F91\u5668\u5185\u5BB9","\u6309 Alt+F1 \u53EF\u6253\u5F00\u8F85\u52A9\u529F\u80FD\u9009\u9879\u3002","\u5207\u6362\u9AD8\u5BF9\u6BD4\u5EA6\u4E3B\u9898","\u5728 {1} \u4E2A\u6587\u4EF6\u4E2D\u8FDB\u884C\u4E86 {0} \u6B21\u7F16\u8F91"],"vs/editor/common/view/editorColorRegistry":["\u5149\u6807\u6240\u5728\u884C\u9AD8\u4EAE\u5185\u5BB9\u7684\u80CC\u666F\u989C\u8272\u3002","\u5149\u6807\u6240\u5728\u884C\u56DB\u5468\u8FB9\u6846\u7684\u80CC\u666F\u989C\u8272\u3002","\u80CC\u666F\u989C\u8272\u7684\u9AD8\u4EAE\u8303\u56F4\uFF0C\u559C\u6B22\u901A\u8FC7\u5FEB\u901F\u6253\u5F00\u548C\u67E5\u627E\u529F\u80FD\u3002\u989C\u8272\u4E0D\u80FD\u4E0D\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u5E95\u5C42\u88C5\u9970\u3002","\u9AD8\u4EAE\u533A\u57DF\u8FB9\u6846\u7684\u80CC\u666F\u989C\u8272\u3002","\u9AD8\u4EAE\u663E\u793A\u7B26\u53F7\u7684\u80CC\u666F\u989C\u8272\uFF0C\u4F8B\u5982\u8F6C\u5230\u5B9A\u4E49\u6216\u8F6C\u5230\u4E0B\u4E00\u4E2A/\u4E0A\u4E00\u4E2A\u7B26\u53F7\u3002\u989C\u8272\u4E0D\u80FD\u662F\u4E0D\u900F\u660E\u7684\uFF0C\u4EE5\u514D\u9690\u85CF\u5E95\u5C42\u88C5\u9970\u3002","\u9AD8\u4EAE\u663E\u793A\u7B26\u53F7\u5468\u56F4\u7684\u8FB9\u6846\u7684\u80CC\u666F\u989C\u8272\u3002","\u7F16\u8F91\u5668\u5149\u6807\u989C\u8272\u3002","\u7F16\u8F91\u5668\u5149\u6807\u7684\u80CC\u666F\u8272\u3002\u53EF\u4EE5\u81EA\u5B9A\u4E49\u5757\u578B\u5149\u6807\u8986\u76D6\u5B57\u7B26\u7684\u989C\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u7A7A\u767D\u5B57\u7B26\u7684\u989C\u8272\u3002","\u7F16\u8F91\u5668\u7F29\u8FDB\u53C2\u8003\u7EBF\u7684\u989C\u8272\u3002","\u7F16\u8F91\u5668\u6D3B\u52A8\u7F29\u8FDB\u53C2\u8003\u7EBF\u7684\u989C\u8272\u3002","\u7F16\u8F91\u5668\u884C\u53F7\u7684\u989C\u8272\u3002","\u7F16\u8F91\u5668\u6D3B\u52A8\u884C\u53F7\u7684\u989C\u8272",'"Id" \u5DF2\u88AB\u5F03\u7528\uFF0C\u8BF7\u6539\u7528 "editorLineNumber.activeForeground"\u3002',"\u7F16\u8F91\u5668\u6D3B\u52A8\u884C\u53F7\u7684\u989C\u8272","\u7F16\u8F91\u5668\u6807\u5C3A\u7684\u989C\u8272\u3002","\u7F16\u8F91\u5668 CodeLens \u7684\u524D\u666F\u8272","\u5339\u914D\u62EC\u53F7\u7684\u80CC\u666F\u8272","\u5339\u914D\u62EC\u53F7\u5916\u6846\u7684\u989C\u8272","\u6982\u89C8\u6807\u5C3A\u8FB9\u6846\u7684\u989C\u8272\u3002","\u7F16\u8F91\u5668\u6982\u8FF0\u6807\u5C3A\u7684\u80CC\u666F\u8272\u3002\u4EC5\u5F53\u7F29\u7565\u56FE\u5DF2\u542F\u7528\u4E14\u7F6E\u4E8E\u7F16\u8F91\u5668\u53F3\u4FA7\u65F6\u624D\u4F7F\u7528\u3002","\u7F16\u8F91\u5668\u5BFC\u822A\u7EBF\u7684\u80CC\u666F\u8272\u3002\u5BFC\u822A\u7EBF\u5305\u62EC\u8FB9\u7F18\u7B26\u53F7\u548C\u884C\u53F7\u3002","\u7F16\u8F91\u5668\u4E2D\u4E0D\u5FC5\u8981(\u672A\u4F7F\u7528)\u7684\u6E90\u4EE3\u7801\u7684\u8FB9\u6846\u989C\u8272\u3002",'\u975E\u5FC5\u987B(\u672A\u4F7F\u7528)\u4EE3\u7801\u7684\u5728\u7F16\u8F91\u5668\u4E2D\u663E\u793A\u7684\u4E0D\u900F\u660E\u5EA6\u3002\u4F8B\u5982\uFF0C"#000000c0" \u5C06\u4EE5 75% \u7684\u4E0D\u900F\u660E\u5EA6\u663E\u793A\u4EE3\u7801\u3002\u5BF9\u4E8E\u9AD8\u5BF9\u6BD4\u5EA6\u4E3B\u9898\uFF0C\u8BF7\u4F7F\u7528 \u201DeditorUnnecessaryCode.border\u201C \u4E3B\u9898\u6765\u4E3A\u975E\u5FC5\u987B\u4EE3\u7801\u6DFB\u52A0\u4E0B\u5212\u7EBF\uFF0C\u4EE5\u907F\u514D\u989C\u8272\u6DE1\u5316\u3002',"\u7528\u4E8E\u7A81\u51FA\u663E\u793A\u8303\u56F4\u7684\u6982\u8FF0\u6807\u5C3A\u6807\u8BB0\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u6982\u89C8\u6807\u5C3A\u4E2D\u9519\u8BEF\u6807\u8BB0\u7684\u989C\u8272\u3002","\u6982\u89C8\u6807\u5C3A\u4E2D\u8B66\u544A\u6807\u8BB0\u7684\u989C\u8272\u3002","\u6982\u89C8\u6807\u5C3A\u4E2D\u4FE1\u606F\u6807\u8BB0\u7684\u989C\u8272\u3002"],"vs/editor/contrib/anchorSelect/anchorSelect":["\u9009\u62E9\u5B9A\u4F4D\u70B9","\u5B9A\u4F4D\u70B9\u8BBE\u7F6E\u4E3A {0}:{1}","\u8BBE\u7F6E\u9009\u62E9\u5B9A\u4F4D\u70B9","\u8F6C\u5230\u9009\u62E9\u5B9A\u4F4D\u70B9","\u9009\u62E9\u4ECE\u5B9A\u4F4D\u70B9\u5230\u5149\u6807","\u53D6\u6D88\u9009\u62E9\u5B9A\u4F4D\u70B9"],"vs/editor/contrib/bracketMatching/bracketMatching":["\u6982\u89C8\u6807\u5C3A\u4E0A\u8868\u793A\u5339\u914D\u62EC\u53F7\u7684\u6807\u8BB0\u989C\u8272\u3002","\u8F6C\u5230\u62EC\u53F7","\u9009\u62E9\u62EC\u53F7\u6240\u6709\u5185\u5BB9","\u8F6C\u5230\u62EC\u53F7(&&B)"],"vs/editor/contrib/caretOperations/caretOperations":["\u5411\u5DE6\u79FB\u52A8\u6240\u9009\u6587\u672C","\u5411\u53F3\u79FB\u52A8\u6240\u9009\u6587\u672C"],"vs/editor/contrib/caretOperations/transpose":["\u8F6C\u7F6E\u5B57\u6BCD"],"vs/editor/contrib/clipboard/clipboard":["\u526A\u5207(&&T)","\u526A\u5207","\u526A\u5207","\u590D\u5236(&&C)","\u590D\u5236","\u590D\u5236","\u7C98\u8D34(&&P)","\u7C98\u8D34","\u7C98\u8D34","\u590D\u5236\u5E76\u7A81\u51FA\u663E\u793A\u8BED\u6CD5"],"vs/editor/contrib/codeAction/codeActionCommands":["\u8981\u8FD0\u884C\u7684\u4EE3\u7801\u64CD\u4F5C\u7684\u79CD\u7C7B\u3002","\u63A7\u5236\u4F55\u65F6\u5E94\u7528\u8FD4\u56DE\u7684\u64CD\u4F5C\u3002","\u59CB\u7EC8\u5E94\u7528\u7B2C\u4E00\u4E2A\u8FD4\u56DE\u7684\u4EE3\u7801\u64CD\u4F5C\u3002","\u5982\u679C\u4EC5\u8FD4\u56DE\u7684\u7B2C\u4E00\u4E2A\u4EE3\u7801\u64CD\u4F5C\uFF0C\u5219\u5E94\u7528\u8BE5\u64CD\u4F5C\u3002","\u4E0D\u8981\u5E94\u7528\u8FD4\u56DE\u7684\u4EE3\u7801\u64CD\u4F5C\u3002","\u5982\u679C\u53EA\u5E94\u8FD4\u56DE\u9996\u9009\u4EE3\u7801\u64CD\u4F5C\uFF0C\u5219\u5E94\u8FD4\u56DE\u63A7\u4EF6\u3002","\u5E94\u7528\u4EE3\u7801\u64CD\u4F5C\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF","\u5FEB\u901F\u4FEE\u590D...","\u6CA1\u6709\u53EF\u7528\u7684\u4EE3\u7801\u64CD\u4F5C",'\u6CA1\u6709\u9002\u7528\u4E8E"{0}"\u7684\u9996\u9009\u4EE3\u7801\u64CD\u4F5C','\u6CA1\u6709\u9002\u7528\u4E8E"{0}"\u7684\u4EE3\u7801\u64CD\u4F5C',"\u6CA1\u6709\u53EF\u7528\u7684\u9996\u9009\u4EE3\u7801\u64CD\u4F5C","\u6CA1\u6709\u53EF\u7528\u7684\u4EE3\u7801\u64CD\u4F5C","\u91CD\u6784...",'\u6CA1\u6709\u9002\u7528\u4E8E"{0}"\u7684\u9996\u9009\u91CD\u6784','\u6CA1\u6709\u53EF\u7528\u7684"{0}"\u91CD\u6784',"\u6CA1\u6709\u53EF\u7528\u7684\u9996\u9009\u91CD\u6784","\u6CA1\u6709\u53EF\u7528\u7684\u91CD\u6784\u64CD\u4F5C","\u6E90\u4EE3\u7801\u64CD\u4F5C...",'\u6CA1\u6709\u9002\u7528\u4E8E"{0}"\u7684\u9996\u9009\u6E90\u64CD\u4F5C',"\u6CA1\u6709\u9002\u7528\u4E8E\u201C {0}\u201D\u7684\u6E90\u64CD\u4F5C","\u6CA1\u6709\u53EF\u7528\u7684\u9996\u9009\u6E90\u64CD\u4F5C","\u6CA1\u6709\u53EF\u7528\u7684\u6E90\u4EE3\u7801\u64CD\u4F5C","\u6574\u7406 import \u8BED\u53E5","\u6CA1\u6709\u53EF\u7528\u7684\u6574\u7406 import \u8BED\u53E5\u64CD\u4F5C","\u5168\u90E8\u4FEE\u590D","\u6CA1\u6709\u53EF\u7528\u7684\u201C\u5168\u90E8\u4FEE\u590D\u201D\u64CD\u4F5C","\u81EA\u52A8\u4FEE\u590D...","\u6CA1\u6709\u53EF\u7528\u7684\u81EA\u52A8\u4FEE\u590D\u7A0B\u5E8F"],"vs/editor/contrib/codeAction/lightBulbWidget":["\u663E\u793A\u4FEE\u590D\u7A0B\u5E8F\u3002\u9996\u9009\u53EF\u7528\u4FEE\u590D\u7A0B\u5E8F ({0})","\u663E\u793A\u4FEE\u8865\u7A0B\u5E8F({0})","\u663E\u793A\u4FEE\u8865\u7A0B\u5E8F"],"vs/editor/contrib/codelens/codelensController":["\u663E\u793A\u5F53\u524D\u884C\u7684 Code Lens \u547D\u4EE4"],"vs/editor/contrib/comment/comment":["\u5207\u6362\u884C\u6CE8\u91CA","\u5207\u6362\u884C\u6CE8\u91CA(&&T)","\u6DFB\u52A0\u884C\u6CE8\u91CA","\u5220\u9664\u884C\u6CE8\u91CA","\u5207\u6362\u5757\u6CE8\u91CA","\u5207\u6362\u5757\u6CE8\u91CA(&&B)"],"vs/editor/contrib/contextmenu/contextmenu":["\u663E\u793A\u7F16\u8F91\u5668\u4E0A\u4E0B\u6587\u83DC\u5355"],"vs/editor/contrib/cursorUndo/cursorUndo":["\u5149\u6807\u64A4\u6D88","\u5149\u6807\u91CD\u505A"],"vs/editor/contrib/find/findController":["\u67E5\u627E","\u67E5\u627E(&&F)","\u67E5\u627E\u9009\u5B9A\u5185\u5BB9","\u67E5\u627E\u4E0B\u4E00\u4E2A","\u67E5\u627E\u4E0B\u4E00\u4E2A","\u67E5\u627E\u4E0A\u4E00\u4E2A","\u67E5\u627E\u4E0A\u4E00\u4E2A","\u67E5\u627E\u4E0B\u4E00\u4E2A\u9009\u62E9","\u67E5\u627E\u4E0A\u4E00\u4E2A\u9009\u62E9","\u66FF\u6362","\u66FF\u6362(&&R)"],"vs/editor/contrib/find/findWidget":["\u7F16\u8F91\u5668\u67E5\u627E\u5C0F\u7EC4\u4EF6\u4E2D\u7684\u201C\u5728\u9009\u5B9A\u5185\u5BB9\u4E2D\u67E5\u627E\u201D\u56FE\u6807\u3002","\u7528\u4E8E\u6307\u793A\u7F16\u8F91\u5668\u67E5\u627E\u5C0F\u7EC4\u4EF6\u5DF2\u6298\u53E0\u7684\u56FE\u6807\u3002","\u7528\u4E8E\u6307\u793A\u7F16\u8F91\u5668\u67E5\u627E\u5C0F\u7EC4\u4EF6\u5DF2\u5C55\u5F00\u7684\u56FE\u6807\u3002","\u7F16\u8F91\u5668\u67E5\u627E\u5C0F\u7EC4\u4EF6\u4E2D\u7684\u201C\u66FF\u6362\u201D\u56FE\u6807\u3002","\u7F16\u8F91\u5668\u67E5\u627E\u5C0F\u7EC4\u4EF6\u4E2D\u7684\u201C\u5168\u90E8\u66FF\u6362\u201D\u56FE\u6807\u3002","\u7F16\u8F91\u5668\u67E5\u627E\u5C0F\u7EC4\u4EF6\u4E2D\u7684\u201C\u67E5\u627E\u4E0A\u4E00\u4E2A\u201D\u56FE\u6807\u3002","\u7F16\u8F91\u5668\u67E5\u627E\u5C0F\u7EC4\u4EF6\u4E2D\u7684\u201C\u67E5\u627E\u4E0B\u4E00\u4E2A\u201D\u56FE\u6807\u3002","\u67E5\u627E","\u67E5\u627E","\u4E0A\u4E00\u4E2A\u5339\u914D\u9879","\u4E0B\u4E00\u4E2A\u5339\u914D\u9879","\u5728\u9009\u5B9A\u5185\u5BB9\u4E2D\u67E5\u627E","\u5173\u95ED","\u66FF\u6362","\u66FF\u6362","\u66FF\u6362","\u5168\u90E8\u66FF\u6362","\u5207\u6362\u66FF\u6362\u6A21\u5F0F","\u4EC5\u9AD8\u4EAE\u4E86\u524D {0} \u4E2A\u7ED3\u679C\uFF0C\u4F46\u6240\u6709\u67E5\u627E\u64CD\u4F5C\u5747\u9488\u5BF9\u5168\u6587\u3002","{1} \u4E2D\u7684 {0}","\u65E0\u7ED3\u679C","\u627E\u5230 {0}","\u4E3A\u201C{1}\u201D\u627E\u5230 {0}","\u5728 {2} \u5904\u627E\u5230\u201C{1}\u201D\u7684 {0}","\u4E3A\u201C{1}\u201D\u627E\u5230 {0}","Ctrl+Enter \u73B0\u5728\u7531\u5168\u90E8\u66FF\u6362\u6539\u4E3A\u63D2\u5165\u6362\u884C\u3002\u4F60\u53EF\u4EE5\u4FEE\u6539editor.action.replaceAll \u7684\u6309\u952E\u7ED1\u5B9A\u4EE5\u8986\u76D6\u6B64\u884C\u4E3A\u3002"],"vs/editor/contrib/folding/folding":["\u5C55\u5F00","\u4EE5\u9012\u5F52\u65B9\u5F0F\u5C55\u5F00","\u6298\u53E0","\u5207\u6362\u6298\u53E0","\u4EE5\u9012\u5F52\u65B9\u5F0F\u6298\u53E0","\u6298\u53E0\u6240\u6709\u5757\u6CE8\u91CA","\u6298\u53E0\u6240\u6709\u533A\u57DF","\u5C55\u5F00\u6240\u6709\u533A\u57DF","\u5168\u90E8\u6298\u53E0","\u5168\u90E8\u5C55\u5F00","\u6298\u53E0\u7EA7\u522B {0}","\u6298\u53E0\u8303\u56F4\u540E\u9762\u7684\u80CC\u666F\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u8BBE\u4E3A\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u5E95\u5C42\u88C5\u9970\u3002","\u7F16\u8F91\u5668\u88C5\u8BA2\u7EBF\u4E2D\u6298\u53E0\u63A7\u4EF6\u7684\u989C\u8272\u3002"],"vs/editor/contrib/folding/foldingDecorations":["\u7F16\u8F91\u5668\u5B57\u5F62\u8FB9\u8DDD\u4E2D\u5DF2\u5C55\u5F00\u7684\u8303\u56F4\u7684\u56FE\u6807\u3002","\u7F16\u8F91\u5668\u5B57\u5F62\u8FB9\u8DDD\u4E2D\u5DF2\u6298\u53E0\u7684\u8303\u56F4\u7684\u56FE\u6807\u3002"],"vs/editor/contrib/fontZoom/fontZoom":["\u653E\u5927\u7F16\u8F91\u5668\u5B57\u4F53","\u7F29\u5C0F\u7F16\u8F91\u5668\u5B57\u4F53","\u91CD\u7F6E\u7F16\u8F91\u5668\u5B57\u4F53\u5927\u5C0F"],"vs/editor/contrib/format/format":["\u5728\u7B2C {0} \u884C\u8FDB\u884C\u4E86 1 \u6B21\u683C\u5F0F\u7F16\u8F91","\u5728\u7B2C {1} \u884C\u8FDB\u884C\u4E86 {0} \u6B21\u683C\u5F0F\u7F16\u8F91","\u7B2C {0} \u884C\u5230\u7B2C {1} \u884C\u95F4\u8FDB\u884C\u4E86 1 \u6B21\u683C\u5F0F\u7F16\u8F91","\u7B2C {1} \u884C\u5230\u7B2C {2} \u884C\u95F4\u8FDB\u884C\u4E86 {0} \u6B21\u683C\u5F0F\u7F16\u8F91"],"vs/editor/contrib/format/formatActions":["\u683C\u5F0F\u5316\u6587\u6863","\u683C\u5F0F\u5316\u9009\u5B9A\u5185\u5BB9"],"vs/editor/contrib/gotoError/gotoError":["\u8F6C\u5230\u4E0B\u4E00\u4E2A\u95EE\u9898 (\u9519\u8BEF\u3001\u8B66\u544A\u3001\u4FE1\u606F)","\u201C\u8F6C\u5230\u4E0B\u4E00\u4E2A\u201D\u6807\u8BB0\u7684\u56FE\u6807\u3002","\u8F6C\u5230\u4E0A\u4E00\u4E2A\u95EE\u9898 (\u9519\u8BEF\u3001\u8B66\u544A\u3001\u4FE1\u606F)","\u201C\u8F6C\u5230\u4E0A\u4E00\u4E2A\u201D\u6807\u8BB0\u7684\u56FE\u6807\u3002","\u8F6C\u5230\u6587\u4EF6\u4E2D\u7684\u4E0B\u4E00\u4E2A\u95EE\u9898 (\u9519\u8BEF\u3001\u8B66\u544A\u3001\u4FE1\u606F)","\u4E0B\u4E00\u4E2A\u95EE\u9898(&&P)","\u8F6C\u5230\u6587\u4EF6\u4E2D\u7684\u4E0A\u4E00\u4E2A\u95EE\u9898 (\u9519\u8BEF\u3001\u8B66\u544A\u3001\u4FE1\u606F)","\u4E0A\u4E00\u4E2A\u95EE\u9898(&&P)"],"vs/editor/contrib/gotoError/gotoErrorWidget":["\u9519\u8BEF","\u8B66\u544A","\u4FE1\u606F","\u63D0\u793A","{1} \u4E2D\u7684 {0}","{0} \u4E2A\u95EE\u9898(\u5171 {1} \u4E2A)","{0} \u4E2A\u95EE\u9898(\u5171 {1} \u4E2A)","\u7F16\u8F91\u5668\u6807\u8BB0\u5BFC\u822A\u5C0F\u7EC4\u4EF6\u9519\u8BEF\u989C\u8272\u3002","\u7F16\u8F91\u5668\u6807\u8BB0\u5BFC\u822A\u5C0F\u7EC4\u4EF6\u8B66\u544A\u989C\u8272\u3002","\u7F16\u8F91\u5668\u6807\u8BB0\u5BFC\u822A\u5C0F\u7EC4\u4EF6\u4FE1\u606F\u989C\u8272\u3002","\u7F16\u8F91\u5668\u6807\u8BB0\u5BFC\u822A\u5C0F\u7EC4\u4EF6\u80CC\u666F\u8272\u3002"],"vs/editor/contrib/gotoSymbol/goToCommands":["\u5FEB\u901F\u67E5\u770B","\u5B9A\u4E49","\u672A\u627E\u5230\u201C{0}\u201D\u7684\u4EFB\u4F55\u5B9A\u4E49","\u627E\u4E0D\u5230\u5B9A\u4E49","\u8F6C\u5230\u5B9A\u4E49","\u8F6C\u5230\u5B9A\u4E49(&&D)","\u6253\u5F00\u4FA7\u8FB9\u7684\u5B9A\u4E49","\u901F\u89C8\u5B9A\u4E49","\u58F0\u660E","\u672A\u627E\u5230\u201C{0}\u201D\u7684\u58F0\u660E","\u672A\u627E\u5230\u58F0\u660E","\u8F6C\u5230\u58F0\u660E",'\u8F6C\u5230"\u58F0\u660E"(&&D)',"\u672A\u627E\u5230\u201C{0}\u201D\u7684\u58F0\u660E","\u672A\u627E\u5230\u58F0\u660E","\u67E5\u770B\u58F0\u660E","\u7C7B\u578B\u5B9A\u4E49","\u672A\u627E\u5230\u201C{0}\u201D\u7684\u7C7B\u578B\u5B9A\u4E49","\u672A\u627E\u5230\u7C7B\u578B\u5B9A\u4E49","\u8F6C\u5230\u7C7B\u578B\u5B9A\u4E49","\u8F6C\u5230\u7C7B\u578B\u5B9A\u4E49(&&T)","\u5FEB\u901F\u67E5\u770B\u7C7B\u578B\u5B9A\u4E49","\u5B9E\u73B0","\u672A\u627E\u5230\u201C{0}\u201D\u7684\u5B9E\u73B0","\u672A\u627E\u5230\u5B9E\u73B0","\u8F6C\u5230\u5B9E\u73B0","\u8DF3\u8F6C\u5230\u5B9E\u73B0(&&I)","\u67E5\u770B\u5B9E\u73B0",'\u672A\u627E\u5230"{0}"\u7684\u5F15\u7528',"\u672A\u627E\u5230\u5F15\u7528","\u8F6C\u5230\u5F15\u7528","\u8F6C\u5230\u5F15\u7528(&&R)","\u5F15\u7528","\u67E5\u770B\u5F15\u7528","\u5F15\u7528","\u8F6C\u5230\u4EFB\u4F55\u7B26\u53F7","\u4F4D\u7F6E","\u65E0\u201C{0}\u201D\u7684\u7ED3\u679C","\u5F15\u7528"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["\u5355\u51FB\u663E\u793A {0} \u4E2A\u5B9A\u4E49\u3002"],"vs/editor/contrib/gotoSymbol/peek/referencesController":["\u6B63\u5728\u52A0\u8F7D...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["{0} \u4E2A\u5F15\u7528","{0} \u4E2A\u5F15\u7528","\u5F15\u7528"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["\u65E0\u53EF\u7528\u9884\u89C8","\u65E0\u7ED3\u679C","\u5F15\u7528"],"vs/editor/contrib/gotoSymbol/referencesModel":["\u5728\u6587\u4EF6 {0} \u7684 {1} \u884C {2} \u5217\u7684\u7B26\u53F7","{0} \u4E2D {1} \u884C {2} \u5217\u7684\u7B26\u53F7\uFF0C{3}","{0} \u4E2D\u6709 1 \u4E2A\u7B26\u53F7\uFF0C\u5B8C\u6574\u8DEF\u5F84: {1}","{1} \u4E2D\u6709 {0} \u4E2A\u7B26\u53F7\uFF0C\u5B8C\u6574\u8DEF\u5F84: {2}","\u672A\u627E\u5230\u7ED3\u679C","\u5728 {0} \u4E2D\u627E\u5230 1 \u4E2A\u7B26\u53F7","\u5728 {1} \u4E2D\u627E\u5230 {0} \u4E2A\u7B26\u53F7","\u5728 {1} \u4E2A\u6587\u4EF6\u4E2D\u627E\u5230 {0} \u4E2A\u7B26\u53F7"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["{1} \u7684\u7B26\u53F7 {0}\uFF0C\u4E0B\u4E00\u4E2A\u4F7F\u7528 {2}","{1} \u7684\u7B26\u53F7 {0}"],"vs/editor/contrib/hover/hover":["\u663E\u793A\u60AC\u505C","\u663E\u793A\u5B9A\u4E49\u9884\u89C8\u60AC\u505C"],"vs/editor/contrib/hover/markdownHoverParticipant":["\u6B63\u5728\u52A0\u8F7D..."],"vs/editor/contrib/hover/markerHoverParticipant":["View Problem","\u6CA1\u6709\u53EF\u7528\u7684\u5FEB\u901F\u4FEE\u590D","\u6B63\u5728\u68C0\u67E5\u5FEB\u901F\u4FEE\u590D...","\u6CA1\u6709\u53EF\u7528\u7684\u5FEB\u901F\u4FEE\u590D","\u5FEB\u901F\u4FEE\u590D..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["\u66FF\u6362\u4E3A\u4E0A\u4E00\u4E2A\u503C","\u66FF\u6362\u4E3A\u4E0B\u4E00\u4E2A\u503C"],"vs/editor/contrib/indentation/indentation":["\u5C06\u7F29\u8FDB\u8F6C\u6362\u4E3A\u7A7A\u683C","\u5C06\u7F29\u8FDB\u8F6C\u6362\u4E3A\u5236\u8868\u7B26","\u5DF2\u914D\u7F6E\u5236\u8868\u7B26\u5927\u5C0F","\u9009\u62E9\u5F53\u524D\u6587\u4EF6\u7684\u5236\u8868\u7B26\u5927\u5C0F",'\u4F7F\u7528 "Tab" \u7F29\u8FDB',"\u4F7F\u7528\u7A7A\u683C\u7F29\u8FDB","\u4ECE\u5185\u5BB9\u4E2D\u68C0\u6D4B\u7F29\u8FDB\u65B9\u5F0F","\u91CD\u65B0\u7F29\u8FDB\u884C","\u91CD\u65B0\u7F29\u8FDB\u6240\u9009\u884C"],"vs/editor/contrib/linesOperations/linesOperations":["\u5411\u4E0A\u590D\u5236\u884C","\u5411\u4E0A\u590D\u5236\u884C(&&C)","\u5411\u4E0B\u590D\u5236\u884C","\u5411\u4E0B\u590D\u5236\u4E00\u884C(&&P)","\u91CD\u590D\u9009\u62E9","\u91CD\u590D\u9009\u62E9(&&D)","\u5411\u4E0A\u79FB\u52A8\u884C","\u5411\u4E0A\u79FB\u52A8\u4E00\u884C(&&V)","\u5411\u4E0B\u79FB\u52A8\u884C","\u5411\u4E0B\u79FB\u52A8\u4E00\u884C(&&L)","\u6309\u5347\u5E8F\u6392\u5217\u884C","\u6309\u964D\u5E8F\u6392\u5217\u884C","\u88C1\u526A\u5C3E\u968F\u7A7A\u683C","\u5220\u9664\u884C","\u884C\u7F29\u8FDB","\u884C\u51CF\u5C11\u7F29\u8FDB","\u5728\u4E0A\u9762\u63D2\u5165\u884C","\u5728\u4E0B\u9762\u63D2\u5165\u884C","\u5220\u9664\u5DE6\u4FA7\u6240\u6709\u5185\u5BB9","\u5220\u9664\u53F3\u4FA7\u6240\u6709\u5185\u5BB9","\u5408\u5E76\u884C","\u8F6C\u7F6E\u5149\u6807\u5904\u7684\u5B57\u7B26","\u8F6C\u6362\u4E3A\u5927\u5199","\u8F6C\u6362\u4E3A\u5C0F\u5199","\u8F6C\u6362\u4E3A\u8BCD\u9996\u5B57\u6BCD\u5927\u5199","\u8F6C\u6362\u4E3A\u86C7\u5F62\u547D\u540D\u6CD5"],"vs/editor/contrib/linkedEditing/linkedEditing":["\u542F\u52A8\u94FE\u63A5\u7F16\u8F91","\u7F16\u8F91\u5668\u6839\u636E\u7C7B\u578B\u81EA\u52A8\u91CD\u547D\u540D\u65F6\u7684\u80CC\u666F\u8272\u3002"],"vs/editor/contrib/links/links":["\u6267\u884C\u547D\u4EE4","\u5173\u6CE8\u94FE\u63A5","cmd + \u5355\u51FB","ctrl + \u5355\u51FB","option + \u5355\u51FB","alt + \u5355\u51FB","\u6267\u884C\u547D\u4EE4 {0}","\u6B64\u94FE\u63A5\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u65E0\u6CD5\u6253\u5F00: {0}","\u6B64\u94FE\u63A5\u76EE\u6807\u5DF2\u4E22\u5931\uFF0C\u65E0\u6CD5\u6253\u5F00\u3002","\u6253\u5F00\u94FE\u63A5"],"vs/editor/contrib/message/messageController":["Whether the editor is currently showing an inline message","\u65E0\u6CD5\u5728\u53EA\u8BFB\u7F16\u8F91\u5668\u4E2D\u7F16\u8F91"],"vs/editor/contrib/multicursor/multicursor":["\u5728\u4E0A\u9762\u6DFB\u52A0\u5149\u6807","\u5728\u4E0A\u9762\u6DFB\u52A0\u5149\u6807(&&A)","\u5728\u4E0B\u9762\u6DFB\u52A0\u5149\u6807","\u5728\u4E0B\u9762\u6DFB\u52A0\u5149\u6807(&&D)","\u5728\u884C\u5C3E\u6DFB\u52A0\u5149\u6807","\u5728\u884C\u5C3E\u6DFB\u52A0\u5149\u6807(&&U)","\u5728\u5E95\u90E8\u6DFB\u52A0\u5149\u6807","\u5728\u9876\u90E8\u6DFB\u52A0\u5149\u6807","\u5C06\u4E0B\u4E00\u4E2A\u67E5\u627E\u5339\u914D\u9879\u6DFB\u52A0\u5230\u9009\u62E9","\u6DFB\u52A0\u4E0B\u4E00\u4E2A\u5339\u914D\u9879(&&N)","\u5C06\u9009\u62E9\u5185\u5BB9\u6DFB\u52A0\u5230\u4E0A\u4E00\u67E5\u627E\u5339\u914D\u9879","\u6DFB\u52A0\u4E0A\u4E00\u4E2A\u5339\u914D\u9879(&&R)","\u5C06\u4E0A\u6B21\u9009\u62E9\u79FB\u52A8\u5230\u4E0B\u4E00\u4E2A\u67E5\u627E\u5339\u914D\u9879","\u5C06\u4E0A\u4E2A\u9009\u62E9\u5185\u5BB9\u79FB\u52A8\u5230\u4E0A\u4E00\u67E5\u627E\u5339\u914D\u9879","\u9009\u62E9\u6240\u6709\u627E\u5230\u7684\u67E5\u627E\u5339\u914D\u9879","\u9009\u62E9\u6240\u6709\u5339\u914D\u9879(&&O)","\u66F4\u6539\u6240\u6709\u5339\u914D\u9879"],"vs/editor/contrib/parameterHints/parameterHints":["\u89E6\u53D1\u53C2\u6570\u63D0\u793A"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["\u201C\u663E\u793A\u4E0B\u4E00\u4E2A\u53C2\u6570\u201D\u63D0\u793A\u7684\u56FE\u6807\u3002","\u201C\u663E\u793A\u4E0A\u4E00\u4E2A\u53C2\u6570\u201D\u63D0\u793A\u7684\u56FE\u6807\u3002","{0}\uFF0C\u63D0\u793A"],"vs/editor/contrib/peekView/peekView":["\u5173\u95ED","\u901F\u89C8\u89C6\u56FE\u6807\u9898\u533A\u57DF\u80CC\u666F\u989C\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u6807\u9898\u989C\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u6807\u9898\u4FE1\u606F\u989C\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u8FB9\u6846\u548C\u7BAD\u5934\u989C\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u7ED3\u679C\u5217\u8868\u80CC\u666F\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u7ED3\u679C\u5217\u8868\u4E2D\u884C\u8282\u70B9\u7684\u524D\u666F\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u7ED3\u679C\u5217\u8868\u4E2D\u6587\u4EF6\u8282\u70B9\u7684\u524D\u666F\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u7ED3\u679C\u5217\u8868\u4E2D\u6240\u9009\u6761\u76EE\u7684\u80CC\u666F\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u7ED3\u679C\u5217\u8868\u4E2D\u6240\u9009\u6761\u76EE\u7684\u524D\u666F\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u7F16\u8F91\u5668\u80CC\u666F\u8272\u3002","\u901F\u89C8\u89C6\u56FE\u7F16\u8F91\u5668\u4E2D\u88C5\u8BA2\u7EBF\u7684\u80CC\u666F\u8272\u3002","\u5728\u901F\u89C8\u89C6\u56FE\u7ED3\u679C\u5217\u8868\u4E2D\u5339\u914D\u7A81\u51FA\u663E\u793A\u989C\u8272\u3002","\u5728\u901F\u89C8\u89C6\u56FE\u7F16\u8F91\u5668\u4E2D\u5339\u914D\u7A81\u51FA\u663E\u793A\u989C\u8272\u3002","\u5728\u901F\u89C8\u89C6\u56FE\u7F16\u8F91\u5668\u4E2D\u5339\u914D\u9879\u7684\u7A81\u51FA\u663E\u793A\u8FB9\u6846\u3002"],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["\u5148\u6253\u5F00\u6587\u672C\u7F16\u8F91\u5668\u7136\u540E\u8DF3\u8F6C\u5230\u884C\u3002","\u8F6C\u5230\u7B2C {0} \u884C\u3001\u7B2C {1} \u5217\u3002","\u8F6C\u5230\u884C {0}\u3002","\u5F53\u524D\u884C: {0}\uFF0C\u5B57\u7B26: {1}\u3002\u952E\u5165\u8981\u5BFC\u822A\u5230\u7684\u884C\u53F7(\u4ECB\u4E8E 1 \u81F3 {2} \u4E4B\u95F4)\u3002","\u5F53\u524D\u884C: {0}\uFF0C\u5B57\u7B26: {1}\u3002 \u952E\u5165\u8981\u5BFC\u822A\u5230\u7684\u884C\u53F7\u3002"],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["\u8981\u8F6C\u5230\u7B26\u53F7\uFF0C\u9996\u5148\u6253\u5F00\u5177\u6709\u7B26\u53F7\u4FE1\u606F\u7684\u6587\u672C\u7F16\u8F91\u5668\u3002","\u6D3B\u52A8\u6587\u672C\u7F16\u8F91\u5668\u4E0D\u63D0\u4F9B\u7B26\u53F7\u4FE1\u606F\u3002","\u6CA1\u6709\u5339\u914D\u7684\u7F16\u8F91\u5668\u7B26\u53F7","\u6CA1\u6709\u7F16\u8F91\u5668\u7B26\u53F7","\u5728\u4FA7\u8FB9\u6253\u5F00","\u5728\u5E95\u90E8\u6253\u5F00","\u7B26\u53F7({0})","\u5C5E\u6027({0})","\u65B9\u6CD5({0})","\u51FD\u6570({0})","\u6784\u9020\u51FD\u6570 ({0})","\u53D8\u91CF({0})","\u7C7B({0})","\u7ED3\u6784({0})","\u4E8B\u4EF6({0})","\u8FD0\u7B97\u7B26({0})","\u63A5\u53E3({0})","\u547D\u540D\u7A7A\u95F4({0})","\u5305({0})","\u7C7B\u578B\u53C2\u6570({0})","\u6A21\u5757({0})","\u5C5E\u6027({0})","\u679A\u4E3E({0})","\u679A\u4E3E\u6210\u5458({0})","\u5B57\u7B26\u4E32({0})","\u6587\u4EF6({0})","\u6570\u7EC4({0})","\u6570\u5B57({0})","\u5E03\u5C14\u503C({0})","\u5BF9\u8C61({0})","\u952E({0})","\u5B57\u6BB5({0})","\u5E38\u91CF({0})"],"vs/editor/contrib/rename/rename":["\u65E0\u7ED3\u679C\u3002","\u89E3\u6790\u91CD\u547D\u540D\u4F4D\u7F6E\u65F6\u53D1\u751F\u672A\u77E5\u9519\u8BEF","\u6B63\u5728\u91CD\u547D\u540D\u201C{0}\u201D","\u91CD\u547D\u540D {0}","\u6210\u529F\u5C06\u201C{0}\u201D\u91CD\u547D\u540D\u4E3A\u201C{1}\u201D\u3002\u6458\u8981: {2}","\u91CD\u547D\u540D\u65E0\u6CD5\u5E94\u7528\u4FEE\u6539","\u91CD\u547D\u540D\u65E0\u6CD5\u8BA1\u7B97\u4FEE\u6539","\u91CD\u547D\u540D\u7B26\u53F7","\u542F\u7528/\u7981\u7528\u91CD\u547D\u540D\u4E4B\u524D\u9884\u89C8\u66F4\u6539\u7684\u529F\u80FD"],"vs/editor/contrib/rename/renameInputField":['\u91CD\u547D\u540D\u8F93\u5165\u3002\u952E\u5165\u65B0\u540D\u79F0\u5E76\u6309 "Enter" \u63D0\u4EA4\u3002',"\u6309 {0} \u8FDB\u884C\u91CD\u547D\u540D\uFF0C\u6309 {1} \u8FDB\u884C\u9884\u89C8"],"vs/editor/contrib/smartSelect/smartSelect":["\u5C55\u5F00\u9009\u62E9","\u5C55\u5F00\u9009\u5B9A\u5185\u5BB9(&&E)","\u6536\u8D77\u9009\u62E9","\u7F29\u5C0F\u9009\u5B9A\u8303\u56F4(&&S)"],"vs/editor/contrib/snippet/snippetVariables":["\u661F\u671F\u5929","\u661F\u671F\u4E00","\u661F\u671F\u4E8C","\u661F\u671F\u4E09","\u661F\u671F\u56DB","\u661F\u671F\u4E94","\u661F\u671F\u516D","\u5468\u65E5","\u5468\u4E00","\u5468\u4E8C","\u5468\u4E09","\u5468\u56DB","\u5468\u4E94","\u5468\u516D","\u4E00\u6708","\u4E8C\u6708","\u4E09\u6708","\u56DB\u6708","5\u6708","\u516D\u6708","\u4E03\u6708","\u516B\u6708","\u4E5D\u6708","\u5341\u6708","\u5341\u4E00\u6708","\u5341\u4E8C\u6708","1\u6708","2\u6708","3\u6708","4\u6708","5\u6708","6\u6708","7\u6708","8\u6708","9\u6708","10\u6708","11 \u6708","12\u6708"],"vs/editor/contrib/suggest/suggestController":["\u9009\u62E9\u201C{0}\u201D\u540E\u8FDB\u884C\u4E86\u5176\u4ED6 {1} \u6B21\u7F16\u8F91","\u89E6\u53D1\u5EFA\u8BAE","\u63D2\u5165","\u63D2\u5165","\u66FF\u6362","\u66FF\u6362","\u63D2\u5165","\u663E\u793A\u66F4\u5C11","\u663E\u793A\u66F4\u591A","\u91CD\u7F6E\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u5927\u5C0F"],"vs/editor/contrib/suggest/suggestWidget":["\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u7684\u80CC\u666F\u8272\u3002","\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u7684\u8FB9\u6846\u989C\u8272\u3002","\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u7684\u524D\u666F\u8272\u3002","\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u4E2D\u6240\u9009\u6761\u76EE\u7684\u80CC\u666F\u8272\u3002","\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u4E2D\u5339\u914D\u5185\u5BB9\u7684\u9AD8\u4EAE\u989C\u8272\u3002","\u6B63\u5728\u52A0\u8F7D...","\u65E0\u5EFA\u8BAE\u3002","{0}\uFF0C\u6587\u6863: {1}","\u5EFA\u8BAE"],"vs/editor/contrib/suggest/suggestWidgetDetails":["\u5173\u95ED","\u6B63\u5728\u52A0\u8F7D\u2026"],"vs/editor/contrib/suggest/suggestWidgetRenderer":["\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u4E2D\u7684\u8BE6\u7EC6\u4FE1\u606F\u7684\u56FE\u6807\u3002","\u4E86\u89E3\u8BE6\u7EC6\u4FE1\u606F"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["\u6570\u7EC4\u7B26\u53F7\u7684\u524D\u666F\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u5C06\u663E\u793A\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u4E2D\u3002","\u5E03\u5C14\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u7C7B\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u989C\u8272\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u5E38\u91CF\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u6784\u9020\u51FD\u6570\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u679A\u4E3E\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u679A\u4E3E\u5668\u6210\u5458\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u4E8B\u4EF6\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u5B57\u6BB5\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u6587\u4EF6\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u6587\u4EF6\u5939\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u51FD\u6570\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u63A5\u53E3\u7B26\u53F7\u7684\u524D\u666F\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u5C06\u663E\u793A\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u4E2D\u3002","\u952E\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u5173\u952E\u5B57\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u65B9\u6CD5\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u6A21\u5757\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u547D\u540D\u7A7A\u95F4\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u8F6E\u5ED3\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u7A7A\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u6570\u5B57\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u5BF9\u8C61\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u8FD0\u7B97\u7B26\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u5305\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u5C5E\u6027\u7B26\u53F7\u7684\u524D\u666F\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u7EC4\u4EF6\u4E2D\u3002","\u53C2\u8003\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u7247\u6BB5\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u5B57\u7B26\u4E32\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u8F6E\u5ED3\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u7ED3\u6784\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u6587\u672C\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u7C7B\u578B\u53C2\u6570\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u5355\u4F4D\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002","\u53D8\u91CF\u7B26\u53F7\u7684\u524D\u666F\u989C\u8272\u3002\u8FD9\u4E9B\u7B26\u53F7\u51FA\u73B0\u5728\u5927\u7EB2\u3001\u75D5\u8FF9\u5BFC\u822A\u680F\u548C\u5EFA\u8BAE\u5C0F\u90E8\u4EF6\u4E2D\u3002"],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["\u5207\u6362 Tab \u952E\u79FB\u52A8\u7126\u70B9","Tab \u952E\u5C06\u79FB\u52A8\u5230\u4E0B\u4E00\u53EF\u805A\u7126\u7684\u5143\u7D20","Tab \u952E\u5C06\u63D2\u5165\u5236\u8868\u7B26"],"vs/editor/contrib/tokenization/tokenization":["\u5F00\u53D1\u4EBA\u5458: \u5F3A\u5236\u91CD\u65B0\u8FDB\u884C\u6807\u8BB0"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["\u5F02\u5E38\u884C\u7EC8\u6B62\u7B26","\u68C0\u6D4B\u5230\u5F02\u5E38\u884C\u7EC8\u6B62\u7B26",`\u6B64\u6587\u4EF6\u5305\u542B\u4E00\u4E2A\u6216\u591A\u4E2A\u5F02\u5E38\u7684\u884C\u7EC8\u6B62\u7B26\uFF0C\u4F8B\u5982\u884C\u5206\u9694\u7B26(LS)\u6216\u6BB5\u843D\u5206\u9694\u7B26(PS)\u3002\r +\r +\u5EFA\u8BAE\u4ECE\u6587\u4EF6\u4E2D\u5220\u9664\u5B83\u4EEC\u3002\u53EF\u901A\u8FC7 "editor.unusualLineTerminators" \u8FDB\u884C\u914D\u7F6E\u3002`,"\u4FEE\u590D\u6B64\u6587\u4EF6","\u5FFD\u7565\u6B64\u6587\u4EF6\u7684\u95EE\u9898"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["\u8BFB\u53D6\u8BBF\u95EE\u671F\u95F4\u7B26\u53F7\u7684\u80CC\u666F\u8272\uFF0C\u4F8B\u5982\u8BFB\u53D6\u53D8\u91CF\u65F6\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u5199\u5165\u8BBF\u95EE\u8FC7\u7A0B\u4E2D\u7B26\u53F7\u7684\u80CC\u666F\u8272\uFF0C\u4F8B\u5982\u5199\u5165\u53D8\u91CF\u65F6\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u7B26\u53F7\u5728\u8FDB\u884C\u8BFB\u53D6\u8BBF\u95EE\u64CD\u4F5C\u65F6\u7684\u8FB9\u6846\u989C\u8272\uFF0C\u4F8B\u5982\u8BFB\u53D6\u53D8\u91CF\u3002","\u7B26\u53F7\u5728\u8FDB\u884C\u5199\u5165\u8BBF\u95EE\u64CD\u4F5C\u65F6\u7684\u8FB9\u6846\u989C\u8272\uFF0C\u4F8B\u5982\u5199\u5165\u53D8\u91CF\u3002","\u7528\u4E8E\u7A81\u51FA\u663E\u793A\u7B26\u53F7\u7684\u6982\u8FF0\u6807\u5C3A\u6807\u8BB0\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u7528\u4E8E\u7A81\u51FA\u663E\u793A\u5199\u6743\u9650\u7B26\u53F7\u7684\u6982\u8FF0\u6807\u5C3A\u6807\u8BB0\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u8F6C\u5230\u4E0B\u4E00\u4E2A\u7A81\u51FA\u663E\u793A\u7684\u7B26\u53F7","\u8F6C\u5230\u4E0A\u4E00\u4E2A\u7A81\u51FA\u663E\u793A\u7684\u7B26\u53F7","\u89E6\u53D1\u7B26\u53F7\u9AD8\u4EAE"],"vs/editor/contrib/wordOperations/wordOperations":["\u5220\u9664 Word"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["\u9ED8\u8BA4\u8BED\u8A00\u914D\u7F6E\u66FF\u4EE3","\u9488\u5BF9\u67D0\u79CD\u8BED\u8A00\uFF0C\u914D\u7F6E\u66FF\u4EE3\u7F16\u8F91\u5668\u8BBE\u7F6E\u3002","\u6B64\u8BBE\u7F6E\u4E0D\u652F\u6301\u6309\u8BED\u8A00\u914D\u7F6E\u3002","\u65E0\u6CD5\u6CE8\u518C\u7A7A\u5C5E\u6027",'\u65E0\u6CD5\u6CE8\u518C\u201C{0}\u201D\u3002\u5176\u7B26\u5408\u63CF\u8FF0\u7279\u5B9A\u8BED\u8A00\u7F16\u8F91\u5668\u8BBE\u7F6E\u7684\u8868\u8FBE\u5F0F "\\\\[.*\\\\]$"\u3002\u8BF7\u4F7F\u7528 "configurationDefaults"\u3002',"\u65E0\u6CD5\u6CE8\u518C\u201C{0}\u201D\u3002\u6B64\u5C5E\u6027\u5DF2\u6CE8\u518C\u3002"],"vs/platform/contextkey/browser/contextKeyService":["A command that returns information about context keys"],"vs/platform/contextkey/common/contextkeys":["Whether the operating system is Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["({0})\u5DF2\u6309\u4E0B\u3002\u6B63\u5728\u7B49\u5F85\u6309\u4E0B\u7B2C\u4E8C\u4E2A\u952E...","\u7EC4\u5408\u952E({0}\uFF0C{1})\u4E0D\u662F\u547D\u4EE4\u3002"],"vs/platform/list/browser/listService":["\u5DE5\u4F5C\u53F0","\u6620\u5C04\u4E3A `Ctrl` (Windows \u548C Linux) \u6216 `Command` (macOS)\u3002","\u6620\u5C04\u4E3A `Alt` (Windows \u548C Linux) \u6216 `Option` (macOS)\u3002","\u5728\u901A\u8FC7\u9F20\u6807\u591A\u9009\u6811\u548C\u5217\u8868\u6761\u76EE\u65F6\u4F7F\u7528\u7684\u4FEE\u6539\u952E (\u4F8B\u5982\u201C\u8D44\u6E90\u7BA1\u7406\u5668\u201D\u3001\u201C\u6253\u5F00\u7684\u7F16\u8F91\u5668\u201D\u548C\u201C\u6E90\u4EE3\u7801\u7BA1\u7406\u201D\u89C6\u56FE)\u3002\u201C\u5728\u4FA7\u8FB9\u6253\u5F00\u201D\u529F\u80FD\u6240\u9700\u7684\u9F20\u6807\u52A8\u4F5C (\u82E5\u53EF\u7528) \u5C06\u4F1A\u76F8\u5E94\u8C03\u6574\uFF0C\u4E0D\u4E0E\u591A\u9009\u4FEE\u6539\u952E\u51B2\u7A81\u3002","\u63A7\u5236\u5728\u6811\u548C\u5217\u8868\u4E2D\u600E\u6837\u4F7F\u7528\u9F20\u6807\u6765\u5C55\u5F00\u5B50\u9879(\u82E5\u652F\u6301)\u3002\u5BF9\u4E8E\u6811\u4E2D\u7684\u7236\u8282\u70B9\uFF0C\u6B64\u8BBE\u7F6E\u5C06\u63A7\u5236\u662F\u4F7F\u7528\u5355\u51FB\u8FD8\u662F\u53CC\u51FB\u6765\u5C55\u5F00\u3002\u6CE8\u610F\uFF0C\u67D0\u4E9B\u4E0D\u9002\u7528\u4E8E\u6B64\u8BBE\u7F6E\u7684\u6811\u6216\u5217\u8868\u53EF\u80FD\u4F1A\u5FFD\u7565\u6B64\u9879\u3002 ","\u63A7\u5236\u5217\u8868\u548C\u6811\u662F\u5426\u652F\u6301\u5DE5\u4F5C\u53F0\u4E2D\u7684\u6C34\u5E73\u6EDA\u52A8\u3002\u8B66\u544A: \u6253\u5F00\u6B64\u8BBE\u7F6E\u5F71\u54CD\u4F1A\u5F71\u54CD\u6027\u80FD\u3002","\u63A7\u5236\u6811\u7F29\u8FDB(\u4EE5\u50CF\u7D20\u4E3A\u5355\u4F4D)\u3002","\u63A7\u5236\u6811\u662F\u5426\u5E94\u5448\u73B0\u7F29\u8FDB\u53C2\u8003\u7EBF\u3002","\u63A7\u5236\u5217\u8868\u548C\u6811\u662F\u5426\u5177\u6709\u5E73\u6ED1\u6EDA\u52A8\u3002","\u7B80\u5355\u952E\u76D8\u5BFC\u822A\u805A\u7126\u4E0E\u952E\u76D8\u8F93\u5165\u76F8\u5339\u914D\u7684\u5143\u7D20\u3002\u4EC5\u5BF9\u524D\u7F00\u8FDB\u884C\u5339\u914D\u3002","\u9AD8\u4EAE\u952E\u76D8\u5BFC\u822A\u4F1A\u7A81\u51FA\u663E\u793A\u4E0E\u952E\u76D8\u8F93\u5165\u76F8\u5339\u914D\u7684\u5143\u7D20\u3002\u8FDB\u4E00\u6B65\u5411\u4E0A\u548C\u5411\u4E0B\u5BFC\u822A\u5C06\u4EC5\u904D\u5386\u7A81\u51FA\u663E\u793A\u7684\u5143\u7D20\u3002","\u7B5B\u9009\u5668\u952E\u76D8\u5BFC\u822A\u5C06\u7B5B\u9009\u51FA\u5E76\u9690\u85CF\u4E0E\u952E\u76D8\u8F93\u5165\u4E0D\u5339\u914D\u7684\u6240\u6709\u5143\u7D20\u3002","\u63A7\u5236\u5DE5\u4F5C\u53F0\u4E2D\u7684\u5217\u8868\u548C\u6811\u7684\u952E\u76D8\u5BFC\u822A\u6837\u5F0F\u3002\u5B83\u53EF\u4E3A\u201C\u7B80\u5355\u201D\u3001\u201C\u7A81\u51FA\u663E\u793A\u201D\u6216\u201C\u7B5B\u9009\u201D\u3002","\u63A7\u5236\u5217\u8868\u548C\u6811\u4E2D\u7684\u952E\u76D8\u5BFC\u822A\u662F\u5426\u4EC5\u901A\u8FC7\u952E\u5165\u81EA\u52A8\u89E6\u53D1\u3002\u5982\u679C\u8BBE\u7F6E\u4E3A `false` \uFF0C\u952E\u76D8\u5BFC\u822A\u53EA\u5728\u6267\u884C `list.toggleKeyboardNavigation` \u547D\u4EE4\u65F6\u89E6\u53D1\uFF0C\u60A8\u53EF\u4EE5\u4E3A\u8BE5\u547D\u4EE4\u6307\u5B9A\u952E\u76D8\u5FEB\u6377\u65B9\u5F0F\u3002","\u63A7\u5236\u5355\u51FB\u6587\u4EF6\u5939\u540D\u79F0\u65F6\u5982\u4F55\u5C55\u5F00\u6811\u6587\u4EF6\u5939\u3002"],"vs/platform/markers/common/markers":["\u9519\u8BEF","\u8B66\u544A","\u4FE1\u606F"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","\u6700\u8FD1\u4F7F\u7528","\u5176\u4ED6\u547D\u4EE4",'\u547D\u4EE4"{0}"\u5BFC\u81F4\u9519\u8BEF ({1})'],"vs/platform/quickinput/browser/helpQuickAccess":["\u5168\u5C40\u547D\u4EE4","\u7F16\u8F91\u5668\u547D\u4EE4","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["\u6574\u4F53\u524D\u666F\u8272\u3002\u6B64\u989C\u8272\u4EC5\u5728\u4E0D\u88AB\u7EC4\u4EF6\u8986\u76D6\u65F6\u9002\u7528\u3002","\u9519\u8BEF\u4FE1\u606F\u7684\u6574\u4F53\u524D\u666F\u8272\u3002\u6B64\u989C\u8272\u4EC5\u5728\u4E0D\u88AB\u7EC4\u4EF6\u8986\u76D6\u65F6\u9002\u7528\u3002","\u5DE5\u4F5C\u53F0\u4E2D\u56FE\u6807\u7684\u9ED8\u8BA4\u989C\u8272\u3002","\u7126\u70B9\u5143\u7D20\u7684\u6574\u4F53\u8FB9\u6846\u989C\u8272\u3002\u6B64\u989C\u8272\u4EC5\u5728\u4E0D\u88AB\u5176\u4ED6\u7EC4\u4EF6\u8986\u76D6\u65F6\u9002\u7528\u3002","\u5728\u5143\u7D20\u5468\u56F4\u989D\u5916\u7684\u4E00\u5C42\u8FB9\u6846\uFF0C\u7528\u6765\u63D0\u9AD8\u5BF9\u6BD4\u5EA6\u4ECE\u800C\u533A\u522B\u5176\u4ED6\u5143\u7D20\u3002","\u5728\u6D3B\u52A8\u5143\u7D20\u5468\u56F4\u989D\u5916\u7684\u4E00\u5C42\u8FB9\u6846\uFF0C\u7528\u6765\u63D0\u9AD8\u5BF9\u6BD4\u5EA6\u4ECE\u800C\u533A\u522B\u5176\u4ED6\u5143\u7D20\u3002","\u6587\u672C\u4E2D\u94FE\u63A5\u7684\u524D\u666F\u8272\u3002","\u6587\u672C\u4E2D\u4EE3\u7801\u5757\u7684\u80CC\u666F\u989C\u8272\u3002","\u7F16\u8F91\u5668\u5185\u5C0F\u7EC4\u4EF6(\u5982\u67E5\u627E/\u66FF\u6362)\u7684\u9634\u5F71\u989C\u8272\u3002","\u8F93\u5165\u6846\u80CC\u666F\u8272\u3002","\u8F93\u5165\u6846\u524D\u666F\u8272\u3002","\u8F93\u5165\u6846\u8FB9\u6846\u3002","\u8F93\u5165\u5B57\u6BB5\u4E2D\u5DF2\u6FC0\u6D3B\u9009\u9879\u7684\u8FB9\u6846\u989C\u8272\u3002","\u8F93\u5165\u5B57\u6BB5\u4E2D\u6FC0\u6D3B\u9009\u9879\u7684\u80CC\u666F\u989C\u8272\u3002","\u8F93\u5165\u5B57\u6BB5\u4E2D\u5DF2\u6FC0\u6D3B\u7684\u9009\u9879\u7684\u524D\u666F\u8272\u3002","\u8F93\u5165\u9A8C\u8BC1\u7ED3\u679C\u4E3A\u4FE1\u606F\u7EA7\u522B\u65F6\u7684\u80CC\u666F\u8272\u3002","\u8F93\u5165\u9A8C\u8BC1\u7ED3\u679C\u4E3A\u4FE1\u606F\u7EA7\u522B\u65F6\u7684\u524D\u666F\u8272\u3002","\u4E25\u91CD\u6027\u4E3A\u4FE1\u606F\u65F6\u8F93\u5165\u9A8C\u8BC1\u7684\u8FB9\u6846\u989C\u8272\u3002","\u4E25\u91CD\u6027\u4E3A\u8B66\u544A\u65F6\u8F93\u5165\u9A8C\u8BC1\u7684\u80CC\u666F\u8272\u3002","\u8F93\u5165\u9A8C\u8BC1\u7ED3\u679C\u4E3A\u8B66\u544A\u7EA7\u522B\u65F6\u7684\u524D\u666F\u8272\u3002","\u4E25\u91CD\u6027\u4E3A\u8B66\u544A\u65F6\u8F93\u5165\u9A8C\u8BC1\u7684\u8FB9\u6846\u989C\u8272\u3002","\u8F93\u5165\u9A8C\u8BC1\u7ED3\u679C\u4E3A\u9519\u8BEF\u7EA7\u522B\u65F6\u7684\u80CC\u666F\u8272\u3002","\u8F93\u5165\u9A8C\u8BC1\u7ED3\u679C\u4E3A\u9519\u8BEF\u7EA7\u522B\u65F6\u7684\u524D\u666F\u8272\u3002","\u4E25\u91CD\u6027\u4E3A\u9519\u8BEF\u65F6\u8F93\u5165\u9A8C\u8BC1\u7684\u8FB9\u6846\u989C\u8272\u3002","\u4E0B\u62C9\u5217\u8868\u80CC\u666F\u8272\u3002","\u4E0B\u62C9\u5217\u8868\u524D\u666F\u8272\u3002","\u6309\u94AE\u524D\u666F\u8272\u3002","\u6309\u94AE\u80CC\u666F\u8272\u3002","\u6309\u94AE\u5728\u60AC\u505C\u65F6\u7684\u80CC\u666F\u989C\u8272\u3002","Badge \u80CC\u666F\u8272\u3002Badge \u662F\u5C0F\u578B\u7684\u4FE1\u606F\u6807\u7B7E\uFF0C\u5982\u8868\u793A\u641C\u7D22\u7ED3\u679C\u6570\u91CF\u7684\u6807\u7B7E\u3002","Badge \u524D\u666F\u8272\u3002Badge \u662F\u5C0F\u578B\u7684\u4FE1\u606F\u6807\u7B7E\uFF0C\u5982\u8868\u793A\u641C\u7D22\u7ED3\u679C\u6570\u91CF\u7684\u6807\u7B7E\u3002","\u8868\u793A\u89C6\u56FE\u88AB\u6EDA\u52A8\u7684\u6EDA\u52A8\u6761\u9634\u5F71\u3002","\u6EDA\u52A8\u6761\u6ED1\u5757\u80CC\u666F\u8272","\u6EDA\u52A8\u6761\u6ED1\u5757\u5728\u60AC\u505C\u65F6\u7684\u80CC\u666F\u8272","\u6EDA\u52A8\u6761\u6ED1\u5757\u5728\u88AB\u70B9\u51FB\u65F6\u7684\u80CC\u666F\u8272\u3002","\u8868\u793A\u957F\u65F6\u95F4\u64CD\u4F5C\u7684\u8FDB\u5EA6\u6761\u7684\u80CC\u666F\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u9519\u8BEF\u6587\u672C\u7684\u80CC\u666F\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u7F16\u8F91\u5668\u4E2D\u9519\u8BEF\u6CE2\u6D6A\u7EBF\u7684\u524D\u666F\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u9519\u8BEF\u6846\u7684\u8FB9\u6846\u989C\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u8B66\u544A\u6587\u672C\u7684\u80CC\u666F\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u7F16\u8F91\u5668\u4E2D\u8B66\u544A\u6CE2\u6D6A\u7EBF\u7684\u524D\u666F\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u8B66\u544A\u6846\u7684\u8FB9\u6846\u989C\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u4FE1\u606F\u6587\u672C\u7684\u80CC\u666F\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u7F16\u8F91\u5668\u4E2D\u4FE1\u606F\u6CE2\u6D6A\u7EBF\u7684\u524D\u666F\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u4FE1\u606F\u6846\u7684\u8FB9\u6846\u989C\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u63D0\u793A\u6CE2\u6D6A\u7EBF\u7684\u524D\u666F\u8272\u3002","\u7F16\u8F91\u5668\u4E2D\u63D0\u793A\u6846\u7684\u8FB9\u6846\u989C\u8272\u3002","\u7F16\u8F91\u5668\u80CC\u666F\u8272\u3002","\u7F16\u8F91\u5668\u9ED8\u8BA4\u524D\u666F\u8272\u3002","\u7F16\u8F91\u5668\u7EC4\u4EF6(\u5982\u67E5\u627E/\u66FF\u6362)\u80CC\u666F\u989C\u8272\u3002","\u7F16\u8F91\u5668\u5C0F\u90E8\u4EF6\u7684\u524D\u666F\u8272\uFF0C\u5982\u67E5\u627E/\u66FF\u6362\u3002","\u7F16\u8F91\u5668\u5C0F\u90E8\u4EF6\u7684\u8FB9\u6846\u989C\u8272\u3002\u6B64\u989C\u8272\u4EC5\u5728\u5C0F\u90E8\u4EF6\u6709\u8FB9\u6846\u4E14\u4E0D\u88AB\u5C0F\u90E8\u4EF6\u91CD\u5199\u65F6\u9002\u7528\u3002","\u7F16\u8F91\u5668\u5C0F\u90E8\u4EF6\u5927\u5C0F\u8C03\u6574\u6761\u7684\u8FB9\u6846\u989C\u8272\u3002\u6B64\u989C\u8272\u4EC5\u5728\u5C0F\u90E8\u4EF6\u6709\u8C03\u6574\u8FB9\u6846\u4E14\u4E0D\u88AB\u5C0F\u90E8\u4EF6\u989C\u8272\u8986\u76D6\u65F6\u4F7F\u7528\u3002","\u80CC\u666F\u989C\u8272\u5FEB\u901F\u9009\u53D6\u5668\u3002\u5FEB\u901F\u9009\u53D6\u5668\u5C0F\u90E8\u4EF6\u662F\u9009\u53D6\u5668(\u5982\u547D\u4EE4\u8C03\u8272\u677F)\u7684\u5BB9\u5668\u3002","\u524D\u666F\u989C\u8272\u5FEB\u901F\u9009\u53D6\u5668\u3002\u5FEB\u901F\u9009\u53D6\u5668\u5C0F\u90E8\u4EF6\u662F\u547D\u4EE4\u8C03\u8272\u677F\u7B49\u9009\u53D6\u5668\u7684\u5BB9\u5668\u3002","\u6807\u9898\u80CC\u666F\u989C\u8272\u5FEB\u901F\u9009\u53D6\u5668\u3002\u5FEB\u901F\u9009\u53D6\u5668\u5C0F\u90E8\u4EF6\u662F\u547D\u4EE4\u8C03\u8272\u677F\u7B49\u9009\u53D6\u5668\u7684\u5BB9\u5668\u3002","Quick picker background color for the focused item.","\u5FEB\u901F\u9009\u53D6\u5668\u5206\u7EC4\u6807\u7B7E\u7684\u989C\u8272\u3002","\u5FEB\u901F\u9009\u53D6\u5668\u5206\u7EC4\u8FB9\u6846\u7684\u989C\u8272\u3002","\u7F16\u8F91\u5668\u6240\u9009\u5185\u5BB9\u7684\u989C\u8272\u3002","\u7528\u4EE5\u5F70\u663E\u9AD8\u5BF9\u6BD4\u5EA6\u7684\u6240\u9009\u6587\u672C\u7684\u989C\u8272\u3002","\u975E\u6D3B\u52A8\u7F16\u8F91\u5668\u4E2D\u6240\u9009\u5185\u5BB9\u7684\u989C\u8272\uFF0C\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u88C5\u9970\u6548\u679C\u3002","\u5177\u6709\u4E0E\u6240\u9009\u9879\u76F8\u5173\u5185\u5BB9\u7684\u533A\u57DF\u7684\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u4E0E\u6240\u9009\u9879\u5185\u5BB9\u76F8\u540C\u7684\u533A\u57DF\u7684\u8FB9\u6846\u989C\u8272\u3002","\u5F53\u524D\u641C\u7D22\u5339\u914D\u9879\u7684\u989C\u8272\u3002","\u5176\u4ED6\u641C\u7D22\u5339\u914D\u9879\u7684\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u9650\u5236\u641C\u7D22\u8303\u56F4\u7684\u989C\u8272\u3002\u989C\u8272\u4E0D\u80FD\u4E0D\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u5E95\u5C42\u88C5\u9970\u3002","\u5F53\u524D\u641C\u7D22\u5339\u914D\u9879\u7684\u8FB9\u6846\u989C\u8272\u3002","\u5176\u4ED6\u641C\u7D22\u5339\u914D\u9879\u7684\u8FB9\u6846\u989C\u8272\u3002","\u9650\u5236\u641C\u7D22\u7684\u8303\u56F4\u7684\u8FB9\u6846\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u5728\u4E0B\u9762\u7A81\u51FA\u663E\u793A\u60AC\u505C\u7684\u5B57\u8BCD\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u7F16\u8F91\u5668\u60AC\u505C\u63D0\u793A\u7684\u80CC\u666F\u989C\u8272\u3002","\u7F16\u8F91\u5668\u60AC\u505C\u7684\u524D\u666F\u989C\u8272\u3002","\u5149\u6807\u60AC\u505C\u65F6\u7F16\u8F91\u5668\u7684\u8FB9\u6846\u989C\u8272\u3002","\u7F16\u8F91\u5668\u60AC\u505C\u72B6\u6001\u680F\u7684\u80CC\u666F\u8272\u3002","\u6D3B\u52A8\u94FE\u63A5\u989C\u8272\u3002","\u5185\u8054\u63D0\u793A\u7684\u524D\u666F\u8272","\u5185\u8054\u63D0\u793A\u7684\u80CC\u666F\u8272","\u7528\u4E8E\u706F\u6CE1\u64CD\u4F5C\u56FE\u6807\u7684\u989C\u8272\u3002","\u7528\u4E8E\u706F\u6CE1\u81EA\u52A8\u4FEE\u590D\u64CD\u4F5C\u56FE\u6807\u7684\u989C\u8272\u3002","\u5DF2\u63D2\u5165\u7684\u6587\u672C\u7684\u80CC\u666F\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u5DF2\u5220\u9664\u7684\u6587\u672C\u7684\u80CC\u666F\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u63D2\u5165\u7684\u6587\u672C\u7684\u8F6E\u5ED3\u989C\u8272\u3002","\u88AB\u5220\u9664\u6587\u672C\u7684\u8F6E\u5ED3\u989C\u8272\u3002","\u4E24\u4E2A\u6587\u672C\u7F16\u8F91\u5668\u4E4B\u95F4\u7684\u8FB9\u6846\u989C\u8272\u3002","\u5DEE\u5F02\u7F16\u8F91\u5668\u7684\u5BF9\u89D2\u7EBF\u586B\u5145\u989C\u8272\u3002\u5BF9\u89D2\u7EBF\u586B\u5145\u7528\u4E8E\u5E76\u6392\u5DEE\u5F02\u89C6\u56FE\u3002","\u7126\u70B9\u9879\u5728\u5217\u8868\u6216\u6811\u6D3B\u52A8\u65F6\u7684\u80CC\u666F\u989C\u8272\u3002\u6D3B\u52A8\u7684\u5217\u8868\u6216\u6811\u5177\u6709\u952E\u76D8\u7126\u70B9\uFF0C\u975E\u6D3B\u52A8\u7684\u6CA1\u6709\u3002","\u7126\u70B9\u9879\u5728\u5217\u8868\u6216\u6811\u6D3B\u52A8\u65F6\u7684\u524D\u666F\u989C\u8272\u3002\u6D3B\u52A8\u7684\u5217\u8868\u6216\u6811\u5177\u6709\u952E\u76D8\u7126\u70B9\uFF0C\u975E\u6D3B\u52A8\u7684\u6CA1\u6709\u3002","List/Tree outline color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not.","\u5DF2\u9009\u9879\u5728\u5217\u8868\u6216\u6811\u6D3B\u52A8\u65F6\u7684\u80CC\u666F\u989C\u8272\u3002\u6D3B\u52A8\u7684\u5217\u8868\u6216\u6811\u5177\u6709\u952E\u76D8\u7126\u70B9\uFF0C\u975E\u6D3B\u52A8\u7684\u6CA1\u6709\u3002","\u5DF2\u9009\u9879\u5728\u5217\u8868\u6216\u6811\u6D3B\u52A8\u65F6\u7684\u524D\u666F\u989C\u8272\u3002\u6D3B\u52A8\u7684\u5217\u8868\u6216\u6811\u5177\u6709\u952E\u76D8\u7126\u70B9\uFF0C\u975E\u6D3B\u52A8\u7684\u6CA1\u6709\u3002","\u5DF2\u9009\u9879\u5728\u5217\u8868\u6216\u6811\u975E\u6D3B\u52A8\u65F6\u7684\u80CC\u666F\u989C\u8272\u3002\u6D3B\u52A8\u7684\u5217\u8868\u6216\u6811\u5177\u6709\u952E\u76D8\u7126\u70B9\uFF0C\u975E\u6D3B\u52A8\u7684\u6CA1\u6709\u3002","\u5DF2\u9009\u9879\u5728\u5217\u8868\u6216\u6811\u975E\u6D3B\u52A8\u65F6\u7684\u524D\u666F\u989C\u8272\u3002\u6D3B\u52A8\u7684\u5217\u8868\u6216\u6811\u5177\u6709\u952E\u76D8\u7126\u70B9\uFF0C\u975E\u6D3B\u52A8\u7684\u6CA1\u6709\u3002","\u975E\u6D3B\u52A8\u7684\u5217\u8868\u6216\u6811\u63A7\u4EF6\u4E2D\u7126\u70B9\u9879\u7684\u80CC\u666F\u989C\u8272\u3002\u6D3B\u52A8\u7684\u5217\u8868\u6216\u6811\u5177\u6709\u952E\u76D8\u7126\u70B9\uFF0C\u975E\u6D3B\u52A8\u7684\u6CA1\u6709\u3002","List/Tree outline color for the focused item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not.","\u4F7F\u7528\u9F20\u6807\u79FB\u52A8\u9879\u76EE\u65F6\uFF0C\u5217\u8868\u6216\u6811\u7684\u80CC\u666F\u989C\u8272\u3002","\u9F20\u6807\u5728\u9879\u76EE\u4E0A\u60AC\u505C\u65F6\uFF0C\u5217\u8868\u6216\u6811\u7684\u524D\u666F\u989C\u8272\u3002","\u4F7F\u7528\u9F20\u6807\u79FB\u52A8\u9879\u76EE\u65F6\uFF0C\u5217\u8868\u6216\u6811\u8FDB\u884C\u62D6\u653E\u7684\u80CC\u666F\u989C\u8272\u3002","\u5728\u5217\u8868\u6216\u6811\u4E2D\u641C\u7D22\u65F6\uFF0C\u5176\u4E2D\u5339\u914D\u5185\u5BB9\u7684\u9AD8\u4EAE\u989C\u8272\u3002","\u5217\u8868\u548C\u6811\u4E2D\u7C7B\u578B\u7B5B\u9009\u5668\u5C0F\u7EC4\u4EF6\u7684\u80CC\u666F\u8272\u3002","\u5217\u8868\u548C\u6811\u4E2D\u7C7B\u578B\u7B5B\u9009\u5668\u5C0F\u7EC4\u4EF6\u7684\u8F6E\u5ED3\u989C\u8272\u3002","\u5F53\u6CA1\u6709\u5339\u914D\u9879\u65F6\uFF0C\u5217\u8868\u548C\u6811\u4E2D\u7C7B\u578B\u7B5B\u9009\u5668\u5C0F\u7EC4\u4EF6\u7684\u8F6E\u5ED3\u989C\u8272\u3002","\u7F29\u8FDB\u53C2\u8003\u7EBF\u7684\u6811\u63CF\u8FB9\u989C\u8272\u3002","\u7F29\u8FDB\u53C2\u8003\u7EBF\u7684\u6811\u63CF\u8FB9\u989C\u8272\u3002","\u83DC\u5355\u7684\u8FB9\u6846\u989C\u8272\u3002","\u83DC\u5355\u9879\u7684\u524D\u666F\u989C\u8272\u3002","\u83DC\u5355\u9879\u7684\u80CC\u666F\u989C\u8272\u3002","\u83DC\u5355\u4E2D\u9009\u5B9A\u83DC\u5355\u9879\u7684\u524D\u666F\u8272\u3002","\u83DC\u5355\u4E2D\u6240\u9009\u83DC\u5355\u9879\u7684\u80CC\u666F\u8272\u3002","\u83DC\u5355\u4E2D\u6240\u9009\u83DC\u5355\u9879\u7684\u8FB9\u6846\u989C\u8272\u3002","\u83DC\u5355\u4E2D\u5206\u9694\u7EBF\u7684\u989C\u8272\u3002","\u4EE3\u7801\u7247\u6BB5 Tab \u4F4D\u7684\u9AD8\u4EAE\u80CC\u666F\u8272\u3002","\u4EE3\u7801\u7247\u6BB5 Tab \u4F4D\u7684\u9AD8\u4EAE\u8FB9\u6846\u989C\u8272\u3002","\u4EE3\u7801\u7247\u6BB5\u4E2D\u6700\u540E\u7684 Tab \u4F4D\u7684\u9AD8\u4EAE\u80CC\u666F\u8272\u3002","\u4EE3\u7801\u7247\u6BB5\u4E2D\u6700\u540E\u7684\u5236\u8868\u4F4D\u7684\u9AD8\u4EAE\u8FB9\u6846\u989C\u8272\u3002","\u7528\u4E8E\u67E5\u627E\u5339\u914D\u9879\u7684\u6982\u8FF0\u6807\u5C3A\u6807\u8BB0\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u7528\u4E8E\u7A81\u51FA\u663E\u793A\u6240\u9009\u5185\u5BB9\u7684\u6982\u8FF0\u6807\u5C3A\u6807\u8BB0\u989C\u8272\u3002\u989C\u8272\u5FC5\u987B\u900F\u660E\uFF0C\u4EE5\u514D\u9690\u85CF\u4E0B\u9762\u7684\u4FEE\u9970\u6548\u679C\u3002","\u7528\u4E8E\u67E5\u627E\u5339\u914D\u9879\u7684\u8FF7\u4F60\u5730\u56FE\u6807\u8BB0\u989C\u8272\u3002","\u7F16\u8F91\u5668\u9009\u533A\u5728\u8FF7\u4F60\u5730\u56FE\u4E2D\u5BF9\u5E94\u7684\u6807\u8BB0\u989C\u8272\u3002","\u7528\u4E8E\u9519\u8BEF\u7684\u8FF7\u4F60\u5730\u56FE\u6807\u8BB0\u989C\u8272\u3002","\u7528\u4E8E\u8B66\u544A\u7684\u8FF7\u4F60\u5730\u56FE\u6807\u8BB0\u989C\u8272\u3002","\u8FF7\u4F60\u5730\u56FE\u80CC\u666F\u989C\u8272\u3002","\u8FF7\u4F60\u5730\u56FE\u6ED1\u5757\u80CC\u666F\u989C\u8272\u3002","\u60AC\u505C\u65F6\uFF0C\u8FF7\u4F60\u5730\u56FE\u6ED1\u5757\u7684\u80CC\u666F\u989C\u8272\u3002","\u5355\u51FB\u65F6\uFF0C\u8FF7\u4F60\u5730\u56FE\u6ED1\u5757\u7684\u80CC\u666F\u989C\u8272\u3002","\u7528\u4E8E\u95EE\u9898\u9519\u8BEF\u56FE\u6807\u7684\u989C\u8272\u3002","\u7528\u4E8E\u95EE\u9898\u8B66\u544A\u56FE\u6807\u7684\u989C\u8272\u3002","\u7528\u4E8E\u95EE\u9898\u4FE1\u606F\u56FE\u6807\u7684\u989C\u8272\u3002"],"vs/platform/theme/common/iconRegistry":["\u8981\u4F7F\u7528\u7684\u5B57\u4F53\u7684 ID\u3002\u5982\u679C\u672A\u8BBE\u7F6E\uFF0C\u5219\u4F7F\u7528\u6700\u5148\u5B9A\u4E49\u7684\u5B57\u4F53\u3002","\u4E0E\u56FE\u6807\u5B9A\u4E49\u5173\u8054\u7684\u5B57\u4F53\u5B57\u7B26\u3002","\u5C0F\u7EC4\u4EF6\u4E2D\u201C\u5173\u95ED\u201D\u64CD\u4F5C\u7684\u56FE\u6807\u3002"],"vs/platform/undoRedo/common/undoRedoService":["\u4EE5\u4E0B\u6587\u4EF6\u5DF2\u5173\u95ED\u5E76\u4E14\u5DF2\u5728\u78C1\u76D8\u4E0A\u4FEE\u6539: {0}\u3002","\u4EE5\u4E0B\u6587\u4EF6\u5DF2\u4EE5\u4E0D\u517C\u5BB9\u7684\u65B9\u5F0F\u4FEE\u6539: {0}\u3002","\u65E0\u6CD5\u5728\u6240\u6709\u6587\u4EF6\u4E2D\u64A4\u6D88\u201C{0}\u201D\u3002{1}","\u65E0\u6CD5\u5728\u6240\u6709\u6587\u4EF6\u4E2D\u64A4\u6D88\u201C{0}\u201D\u3002{1}","\u65E0\u6CD5\u64A4\u6D88\u6240\u6709\u6587\u4EF6\u7684\u201C{0}\u201D\uFF0C\u56E0\u4E3A\u5DF2\u66F4\u6539 {1}","\u65E0\u6CD5\u8DE8\u6240\u6709\u6587\u4EF6\u64A4\u9500\u201C{0}\u201D\uFF0C\u56E0\u4E3A {1} \u4E0A\u5DF2\u6709\u4E00\u9879\u64A4\u6D88\u6216\u91CD\u505A\u64CD\u4F5C\u6B63\u5728\u8FD0\u884C","\u65E0\u6CD5\u8DE8\u6240\u6709\u6587\u4EF6\u64A4\u9500\u201C{0}\u201D\uFF0C\u56E0\u4E3A\u540C\u65F6\u53D1\u751F\u4E86\u4E00\u9879\u64A4\u6D88\u6216\u91CD\u505A\u64CD\u4F5C","\u662F\u5426\u8981\u5728\u6240\u6709\u6587\u4EF6\u4E2D\u64A4\u6D88\u201C{0}\u201D?","\u5728 {0} \u4E2A\u6587\u4EF6\u4E2D\u64A4\u6D88","\u64A4\u6D88\u6B64\u6587\u4EF6","\u53D6\u6D88","\u65E0\u6CD5\u64A4\u9500\u201C{0}\u201D\uFF0C\u56E0\u4E3A\u5DF2\u6709\u4E00\u9879\u64A4\u6D88\u6216\u91CD\u505A\u64CD\u4F5C\u6B63\u5728\u8FD0\u884C\u3002","\u662F\u5426\u8981\u64A4\u6D88\u201C{0}\u201D?","\u64A4\u6D88","\u53D6\u6D88","\u65E0\u6CD5\u5728\u6240\u6709\u6587\u4EF6\u4E2D\u91CD\u505A\u201C{0}\u201D\u3002{1}","\u65E0\u6CD5\u5728\u6240\u6709\u6587\u4EF6\u4E2D\u91CD\u505A\u201C{0}\u201D\u3002{1}","\u65E0\u6CD5\u5BF9\u6240\u6709\u6587\u4EF6\u91CD\u505A\u201C{0}\u201D\uFF0C\u56E0\u4E3A\u5DF2\u66F4\u6539 {1}","\u65E0\u6CD5\u8DE8\u6240\u6709\u6587\u4EF6\u91CD\u505A\u201C{0}\u201D\uFF0C\u56E0\u4E3A {1} \u4E0A\u5DF2\u6709\u4E00\u9879\u64A4\u6D88\u6216\u91CD\u505A\u64CD\u4F5C\u6B63\u5728\u8FD0\u884C","\u65E0\u6CD5\u8DE8\u6240\u6709\u6587\u4EF6\u91CD\u505A\u201C{0}\u201D\uFF0C\u56E0\u4E3A\u540C\u65F6\u53D1\u751F\u4E86\u4E00\u9879\u64A4\u6D88\u6216\u91CD\u505A\u64CD\u4F5C","\u65E0\u6CD5\u91CD\u505A\u201C{0}\u201D\uFF0C\u56E0\u4E3A\u5DF2\u6709\u4E00\u9879\u64A4\u6D88\u6216\u91CD\u505A\u64CD\u4F5C\u6B63\u5728\u8FD0\u884C\u3002"]}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.zh-tw.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.zh-tw.js new file mode 100644 index 0000000..e3349d6 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/editor/editor.main.nls.zh-tw.js @@ -0,0 +1,6 @@ +/*!----------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Version: 0.23.0(82e8ea39fc101d639262435542c7d43bc20d8aa2) + * Released under the MIT license + * https://github.com/microsoft/vscode/blob/main/LICENSE.txt + *-----------------------------------------------------------*/define("vs/editor/editor.main.nls.zh-tw",{"vs/base/browser/ui/actionbar/actionViewItems":["{0} ({1})"],"vs/base/browser/ui/findinput/findInput":["\u8F38\u5165"],"vs/base/browser/ui/findinput/findInputCheckboxes":["\u5927\u5C0F\u5BEB\u9808\u76F8\u7B26","\u5168\u5B57\u62FC\u5BEB\u9808\u76F8\u7B26","\u4F7F\u7528\u898F\u5247\u904B\u7B97\u5F0F"],"vs/base/browser/ui/findinput/replaceInput":["\u8F38\u5165","\u4FDD\u7559\u6848\u4F8B"],"vs/base/browser/ui/iconLabel/iconLabel":["\u6B63\u5728\u8F09\u5165..."],"vs/base/browser/ui/inputbox/inputBox":["\u932F\u8AA4: {0}","\u8B66\u544A: {0}","\u8CC7\u8A0A: {0}"],"vs/base/browser/ui/keybindingLabel/keybindingLabel":["\u672A\u7E6B\u7D50"],"vs/base/browser/ui/menu/menu":["{0} ({1})"],"vs/base/browser/ui/tree/abstractTree":["\u6E05\u9664","\u5728\u985E\u578B\u4E0A\u505C\u7528\u7BE9\u9078","\u5728\u985E\u578B\u4E0A\u555F\u7528\u7BE9\u9078","\u627E\u4E0D\u5230\u4EFB\u4F55\u5143\u7D20","{1} \u9805\u5143\u7D20\u4E2D\u6709 {0} \u9805\u76F8\u7B26"],"vs/base/common/actions":["(\u7A7A\u7684)"],"vs/base/common/errorMessage":["{0}: {1}","\u767C\u751F\u7CFB\u7D71\u932F\u8AA4 ({0})","\u767C\u751F\u672A\u77E5\u7684\u932F\u8AA4\u3002\u5982\u9700\u8A73\u7D30\u8CC7\u8A0A\uFF0C\u8ACB\u53C3\u95B1\u8A18\u9304\u6A94\u3002","\u767C\u751F\u672A\u77E5\u7684\u932F\u8AA4\u3002\u5982\u9700\u8A73\u7D30\u8CC7\u8A0A\uFF0C\u8ACB\u53C3\u95B1\u8A18\u9304\u6A94\u3002","{0} (\u7E3D\u8A08 {1} \u500B\u932F\u8AA4)","\u767C\u751F\u672A\u77E5\u7684\u932F\u8AA4\u3002\u5982\u9700\u8A73\u7D30\u8CC7\u8A0A\uFF0C\u8ACB\u53C3\u95B1\u8A18\u9304\u6A94\u3002"],"vs/base/common/keybindingLabels":["Ctrl","Shift","Alt","Windows","Ctrl","Shift","Alt","\u8D85\u7D1A\u9375","Control","Shift","Alt","\u547D\u4EE4","Control","Shift","Alt","Windows","Control","Shift","Alt","\u8D85\u7D1A\u9375"],"vs/base/parts/quickinput/browser/quickInput":["\u4E0A\u4E00\u9801","{0}/{1}","\u8F38\u5165\u4EE5\u7E2E\u5C0F\u7D50\u679C\u7BC4\u570D\u3002","{0} \u500B\u7D50\u679C","\u5DF2\u9078\u64C7 {0}","\u78BA\u5B9A","\u81EA\u8A02","\u80CC\u9762 ({0})","\u4E0A\u4E00\u9801"],"vs/base/parts/quickinput/browser/quickInputList":["\u5FEB\u901F\u8F38\u5165"],"vs/editor/browser/controller/coreCommands":["\u5373\u4F7F\u884C\u7684\u9577\u5EA6\u904E\u9577\uFF0C\u4ECD\u8981\u5805\u6301\u81F3\u7D50\u5C3E","\u5373\u4F7F\u884C\u7684\u9577\u5EA6\u904E\u9577\uFF0C\u4ECD\u8981\u5805\u6301\u81F3\u7D50\u5C3E"],"vs/editor/browser/controller/textAreaHandler":["\u7DE8\u8F2F\u5668","\u76EE\u524D\u7121\u6CD5\u5B58\u53D6\u6B64\u7DE8\u8F2F\u5668\u3002\u8ACB\u6309 {0} \u53D6\u5F97\u9078\u9805\u3002"],"vs/editor/browser/core/keybindingCancellation":["\u7DE8\u8F2F\u5668\u662F\u5426\u57F7\u884C\u53EF\u53D6\u6D88\u7684\u4F5C\u696D\uFF0C\u4F8B\u5982\u300C\u9810\u89BD\u53C3\u8003\u300D"],"vs/editor/browser/editorExtensions":["\u5FA9\u539F(&&U)","\u5FA9\u539F","\u53D6\u6D88\u5FA9\u539F(&&R)","\u91CD\u505A","\u5168\u9078(&&S)","\u5168\u9078"],"vs/editor/browser/widget/codeEditorWidget":["\u6E38\u6A19\u6578\u5DF2\u9650\u5236\u70BA {0} \u500B\u3002"],"vs/editor/browser/widget/diffEditorWidget":["Diff \u7DE8\u8F2F\u5668\u4E2D\u7528\u65BC\u63D2\u5165\u7684\u7DDA\u689D\u88DD\u98FE\u3002","Diff \u7DE8\u8F2F\u5668\u4E2D\u7528\u65BC\u79FB\u9664\u7684\u7DDA\u689D\u88DD\u98FE\u3002","\u56E0\u5176\u4E2D\u4E00\u500B\u6A94\u6848\u904E\u5927\u800C\u7121\u6CD5\u6BD4\u8F03\u3002"],"vs/editor/browser/widget/diffReview":["Diff \u6AA2\u95B1\u4E2D [\u63D2\u5165] \u7684\u5716\u793A\u3002","Diff \u6AA2\u95B1\u4E2D [\u79FB\u9664] \u7684\u5716\u793A\u3002","Diff \u6AA2\u95B1\u4E2D [\u95DC\u9589] \u7684\u5716\u793A\u3002","\u95DC\u9589","\u672A\u8B8A\u66F4\u4EFB\u4E00\u884C","\u5DF2\u8B8A\u66F4 1 \u884C","\u5DF2\u8B8A\u66F4 {0} \u884C","{1} \u9805\u5DEE\u7570\u4E2D\u7684\u7B2C {0} \u9805: \u539F\u59CB\u884C {2}\u3001{3}\uFF0C\u4FEE\u6539\u884C {4}\u3001{5}","\u7A7A\u767D","{0} \u672A\u8B8A\u66F4\u884C {1}","{0} \u539F\u59CB\u884C {1} \u4FEE\u6539\u7684\u884C {2}","+ {0} \u4FEE\u6539\u884C {1}","- {0} \u539F\u59CB\u884C {1}","\u79FB\u81F3\u4E0B\u4E00\u500B\u5DEE\u7570","\u79FB\u81F3\u4E0A\u4E00\u500B\u5DEE\u7570"],"vs/editor/browser/widget/inlineDiffMargin":["\u8907\u88FD\u5DF2\u522A\u9664\u7684\u884C","\u8907\u88FD\u5DF2\u522A\u9664\u7684\u884C","\u8907\u88FD\u5DF2\u522A\u9664\u7684\u884C \uFF08{0}\uFF09","\u9084\u539F\u6B64\u8B8A\u66F4","\u8907\u88FD\u5DF2\u522A\u9664\u7684\u884C \uFF08{0}\uFF09"],"vs/editor/common/config/commonEditorConfig":["\u7DE8\u8F2F\u5668","\u8207 Tab \u76F8\u7B49\u7684\u7A7A\u683C\u6578\u91CF\u3002\u7576 `#editor.detectIndentation#` \u5DF2\u958B\u555F\u6642\uFF0C\u6703\u6839\u64DA\u6A94\u6848\u5167\u5BB9\u8986\u5BEB\u6B64\u8A2D\u5B9A\u3002","\u5728\u6309 `Tab` \u6642\u63D2\u5165\u7A7A\u683C\u3002\u7576 `#editor.detectIndentation#` \u958B\u555F\u6642\uFF0C\u6703\u6839\u64DA\u6A94\u6848\u5167\u5BB9\u8986\u5BEB\u6B64\u8A2D\u5B9A\u3002","\u6839\u64DA\u6A94\u6848\u5167\u5BB9\uFF0C\u63A7\u5236\u7576\u6A94\u6848\u958B\u555F\u6642\uFF0C\u662F\u5426\u81EA\u52D5\u5075\u6E2C `#editor.tabSize#` \u548C `#editor.insertSpaces#`\u3002","\u79FB\u9664\u5C3E\u7AEF\u81EA\u52D5\u63D2\u5165\u7684\u7A7A\u767D\u5B57\u5143\u3002","\u91DD\u5C0D\u5927\u578B\u6A94\u6848\u505C\u7528\u90E8\u5206\u9AD8\u8A18\u61B6\u9AD4\u9700\u6C42\u529F\u80FD\u7684\u7279\u6B8A\u8655\u7406\u65B9\u5F0F\u3002","\u63A7\u5236\u662F\u5426\u61C9\u6839\u64DA\u6587\u4EF6\u4E2D\u7684\u55AE\u5B57\u8A08\u7B97\u81EA\u52D5\u5B8C\u6210\u3002","\u50C5\u5EFA\u8B70\u4F86\u81EA\u4F7F\u7528\u4E2D\u6587\u4EF6\u4E2D\u7684\u5B57\u7D44\u3002","\u5EFA\u8B70\u4F86\u81EA\u6240\u6709\u5DF2\u958B\u555F\u6587\u4EF6\u4E2D\uFF0C\u8A9E\u8A00\u76F8\u540C\u7684\u5B57\u7D44\u3002","\u5EFA\u8B70\u4F86\u81EA\u6240\u6709\u5DF2\u958B\u555F\u6587\u4EF6\u4E2D\u7684\u5B57\u7D44\u3002","\u63A7\u5236\u8981\u5F9E\u54EA\u4E9B\u6587\u4EF6\u8A08\u7B97\u4EE5\u5B57\u7D44\u70BA\u57FA\u790E\u7684\u5B8C\u6210\u4F5C\u696D\u3002","\u6240\u6709\u5F69\u8272\u4E3B\u984C\u7686\u5DF2\u555F\u7528\u8A9E\u610F\u9192\u76EE\u63D0\u793A\u3002","\u6240\u6709\u5F69\u8272\u4E3B\u984C\u7686\u5DF2\u505C\u7528\u8A9E\u610F\u9192\u76EE\u63D0\u793A\u3002","\u8A9E\u610F\u9192\u76EE\u63D0\u793A\u7531\u76EE\u524D\u4E4B\u5F69\u8272\u4F48\u666F\u4E3B\u984C\u7684 'semanticHighlighting' \u8A2D\u5B9A\u6240\u8A2D\u5B9A\u3002","\u63A7\u5236 semanticHighlighting \u662F\u5426\u6703\u70BA\u652F\u63F4\u7684\u8A9E\u8A00\u986F\u793A\u3002","\u5373\u4F7F\u6309\u5169\u4E0B\u5167\u5BB9\u6216\u6309 `Escape`\uFF0C\u4ECD\u4FDD\u6301\u7784\u5B54\u7DE8\u8F2F\u5668\u958B\u555F\u3002","\u56E0\u6548\u80FD\u7684\u7DE3\u6545\uFF0C\u4E0D\u6703\u5C07\u8D85\u904E\u6B64\u9AD8\u5EA6\u7684\u884C Token \u5316","\u53D6\u6D88 Diff \u8A08\u7B97\u524D\u7684\u903E\u6642\u9650\u5236 (\u6BEB\u79D2)\u3002\u82E5\u7121\u903E\u6642\uFF0C\u8ACB\u4F7F\u7528 0\u3002","\u63A7\u5236 Diff \u7DE8\u8F2F\u5668\u8981\u4E26\u6392\u6216\u5167\u5D4C\u986F\u793A Diff\u3002","\u555F\u7528\u6642\uFF0CDiff \u7DE8\u8F2F\u5668\u6703\u5FFD\u7565\u524D\u7F6E\u6216\u5F8C\u7F6E\u7A7A\u683C\u7684\u8B8A\u66F4\u3002","\u63A7\u5236 Diff \u7DE8\u8F2F\u5668\u662F\u5426\u8981\u70BA\u65B0\u589E/\u79FB\u9664\u7684\u8B8A\u66F4\u986F\u793A +/- \u6A19\u8A18\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u986F\u793A codelens\u3002","\u4E00\u5F8B\u4E0D\u63DB\u884C\u3002","\u4F9D\u6AA2\u8996\u5340\u5BEC\u5EA6\u63DB\u884C\u3002","\u5C07\u4F9D\u64DA `#editor.wordWrap#` \u8A2D\u5B9A\u81EA\u52D5\u63DB\u884C\u3002"],"vs/editor/common/config/editorOptions":["\u7DE8\u8F2F\u5668\u5C07\u4F7F\u7528\u5E73\u53F0 API \u4EE5\u5075\u6E2C\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u9644\u52A0\u3002","\u7DE8\u8F2F\u5668\u5C07\u4E00\u5F8B\u6700\u4F73\u5316\u4EE5\u7528\u65BC\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u3002\u81EA\u52D5\u63DB\u884C\u5C07\u6703\u505C\u7528\u3002","\u7DE8\u8F2F\u5668\u4E0D\u6703\u70BA\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u7684\u4F7F\u7528\u65B9\u5F0F\u9032\u884C\u6700\u4F73\u5316\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u65BC\u5DF2\u70BA\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u6700\u4F73\u5316\u7684\u6A21\u5F0F\u4E2D\u57F7\u884C\u3002\u8A2D\u5B9A\u70BA\u958B\u555F\u6703\u505C\u7528\u81EA\u52D5\u63DB\u884C\u3002","\u63A7\u5236\u662F\u5426\u8981\u5728\u8A3B\u89E3\u6642\u63D2\u5165\u7A7A\u767D\u5B57\u5143\u3002","\u63A7\u5236\u662F\u5426\u61C9\u4EE5\u884C\u8A3B\u89E3\u7684\u5207\u63DB\u3001\u65B0\u589E\u6216\u79FB\u9664\u52D5\u4F5C\uFF0C\u5FFD\u7565\u7A7A\u767D\u7684\u884C\u3002","\u63A7\u5236\u8907\u88FD\u6642\u4E0D\u9078\u53D6\u4EFB\u4F55\u9805\u76EE\u662F\u5426\u6703\u8907\u88FD\u76EE\u524D\u7A0B\u5F0F\u884C\u3002","\u63A7\u5236\u5728\u8F38\u5165\u671F\u9593\u662F\u5426\u8981\u8DF3\u904E\u6E38\u6A19\u4F86\u5C0B\u627E\u76F8\u7B26\u7684\u9805\u76EE\u3002","\u63A7\u5236 [\u5C0B\u627E\u5C0F\u5DE5\u5177] \u4E2D\u7684\u641C\u5C0B\u5B57\u4E32\u662F\u5426\u4F86\u81EA\u7DE8\u8F2F\u5668\u9078\u53D6\u9805\u76EE\u3002","\u6C38\u4E0D\u81EA\u52D5\u958B\u555F [\u5728\u9078\u53D6\u7BC4\u570D\u4E2D\u5C0B\u627E] (\u9810\u8A2D)","\u4E00\u5F8B\u81EA\u52D5\u958B\u555F [\u5728\u9078\u53D6\u7BC4\u570D\u4E2D\u5C0B\u627E]","\u9078\u53D6\u591A\u884C\u5167\u5BB9\u6642\uFF0C\u81EA\u52D5\u958B\u555F [\u5728\u9078\u53D6\u7BC4\u570D\u4E2D\u5C0B\u627E]\u3002","\u63A7\u5236\u81EA\u52D5\u958B\u555F\u5728\u9078\u53D6\u7BC4\u570D\u4E2D\u5C0B\u627E\u7684\u689D\u4EF6\u3002","\u63A7\u5236\u5C0B\u627E\u5C0F\u5DE5\u5177\u662F\u5426\u5728 macOS \u4E0A\u8B80\u53D6\u6216\u4FEE\u6539\u5171\u7528\u5C0B\u627E\u526A\u8CBC\u7C3F\u3002","\u63A7\u5236\u5C0B\u627E\u5C0F\u5DE5\u5177\u662F\u5426\u61C9\u5728\u7DE8\u8F2F\u5668\u9802\u7AEF\u984D\u5916\u65B0\u589E\u884C\u3002\u82E5\u70BA true\uFF0C\u7576\u60A8\u53EF\u770B\u5230\u5C0B\u627E\u5C0F\u5DE5\u5177\u6642\uFF0C\u60A8\u7684\u6372\u52D5\u7BC4\u570D\u6703\u8D85\u904E\u7B2C\u4E00\u884C\u3002","\u7576\u518D\u4E5F\u627E\u4E0D\u5230\u5176\u4ED6\u76F8\u7B26\u9805\u76EE\u6642\uFF0C\u63A7\u5236\u662F\u5426\u81EA\u52D5\u5F9E\u958B\u982D (\u6216\u7D50\u5C3E) \u91CD\u65B0\u958B\u59CB\u641C\u5C0B\u3002","\u555F\u7528/\u505C\u7528\u9023\u5B57\u5B57\u578B ('calt' \u548C 'liga' \u5B57\u578B\u529F\u80FD)\u3002\u5C07\u6B64\u9805\u8B8A\u66F4\u70BA\u5B57\u4E32\uFF0C\u4EE5\u7CBE\u78BA\u63A7\u5236 'font-feature-settings' CSS \u5C6C\u6027\u3002","\u660E\u78BA\u7684 'font-feature-settings' CSS \u5C6C\u6027\u3002\u5982\u679C\u53EA\u9700\u8981\u958B\u555F/\u95DC\u9589\u9023\u5B57\uFF0C\u53EF\u4EE5\u6539\u70BA\u50B3\u905E\u5E03\u6797\u503C\u3002","\u8A2D\u5B9A\u9023\u5B57\u5B57\u578B\u6216\u5B57\u578B\u529F\u80FD\u3002\u53EF\u4EE5\u662F\u5E03\u6797\u503C\u4EE5\u555F\u7528/\u505C\u7528\u9023\u5B57\uFF0C\u6216\u4EE3\u8868 CSS 'font-feature-settings' \u5C6C\u6027\u7684\u5B57\u4E32\u3002","\u63A7\u5236\u5B57\u578B\u5927\u5C0F (\u50CF\u7D20)\u3002","\u53EA\u5141\u8A31\u300C\u4E00\u822C\u300D\u53CA\u300C\u7C97\u9AD4\u300D\u95DC\u9375\u5B57\uFF0C\u6216\u4ECB\u65BC 1 \u5230 1000 \u4E4B\u9593\u7684\u6578\u503C\u3002","\u63A7\u5236\u5B57\u578B\u7C97\u7D30\u3002\u63A5\u53D7\u300C\u4E00\u822C\u300D\u53CA\u300C\u7C97\u9AD4\u300D\u95DC\u9375\u5B57\uFF0C\u6216\u4ECB\u65BC 1 \u5230 1000 \u4E4B\u9593\u7684\u6578\u503C\u3002","\u986F\u793A\u7D50\u679C\u7684\u9810\u89BD\u6AA2\u8996 (\u9810\u8A2D)","\u79FB\u81F3\u4E3B\u8981\u7D50\u679C\u4E26\u986F\u793A\u9810\u89BD\u6AA2\u8996","\u524D\u5F80\u4E3B\u8981\u7D50\u679C\uFF0C\u4E26\u5C0D\u5176\u4ED6\u4EBA\u555F\u7528\u7121\u9810\u89BD\u700F\u89BD","\u6B64\u8A2D\u5B9A\u5DF2\u6DD8\u6C70\uFF0C\u8ACB\u6539\u7528 'editor.editor.gotoLocation.multipleDefinitions' \u6216 'editor.editor.gotoLocation.multipleImplementations' \u7B49\u55AE\u7368\u8A2D\u5B9A\u3002","\u63A7\u5236 'Go to Definition' \u547D\u4EE4\u5728\u6709\u591A\u500B\u76EE\u6A19\u4F4D\u7F6E\u5B58\u5728\u6642\u7684\u884C\u70BA\u3002","\u63A7\u5236 'Go to Type Definition' \u547D\u4EE4\u5728\u6709\u591A\u500B\u76EE\u6A19\u4F4D\u7F6E\u5B58\u5728\u6642\u7684\u884C\u70BA\u3002","\u63A7\u5236 'Go to Declaration' \u547D\u4EE4\u5728\u6709\u591A\u500B\u76EE\u6A19\u4F4D\u7F6E\u5B58\u5728\u6642\u7684\u884C\u70BA\u3002","\u63A7\u5236 'Go to Implementations' \u547D\u4EE4\u5728\u6709\u591A\u500B\u76EE\u6A19\u4F4D\u7F6E\u5B58\u5728\u6642\u7684\u884C\u70BA\u3002","\u63A7\u5236 'Go to References' \u547D\u4EE4\u5728\u6709\u591A\u500B\u76EE\u6A19\u4F4D\u7F6E\u5B58\u5728\u6642\u7684\u884C\u70BA\u3002","\u7576 'Go to Definition' \u7684\u7D50\u679C\u70BA\u76EE\u524D\u4F4D\u7F6E\u6642\uFF0C\u6B63\u5728\u57F7\u884C\u7684\u66FF\u4EE3\u547D\u4EE4\u8B58\u5225\u78BC\u3002","\u7576 'Go to Type Definition' \u7684\u7D50\u679C\u70BA\u76EE\u524D\u4F4D\u7F6E\u6642\uFF0C\u6B63\u5728\u57F7\u884C\u7684\u66FF\u4EE3\u547D\u4EE4\u8B58\u5225\u78BC\u3002","\u7576 'Go to Declaration' \u7684\u7D50\u679C\u70BA\u76EE\u524D\u4F4D\u7F6E\u6642\uFF0C\u6B63\u5728\u57F7\u884C\u7684\u66FF\u4EE3\u547D\u4EE4\u8B58\u5225\u78BC\u3002","\u7576 'Go to Implementation' \u7684\u7D50\u679C\u70BA\u76EE\u524D\u4F4D\u7F6E\u6642\uFF0C\u6B63\u5728\u57F7\u884C\u7684\u66FF\u4EE3\u547D\u4EE4\u8B58\u5225\u78BC\u3002","\u7576 'Go to Reference' \u7684\u7D50\u679C\u70BA\u76EE\u524D\u4F4D\u7F6E\u6642\uFF0C\u6B63\u5728\u57F7\u884C\u7684\u66FF\u4EE3\u547D\u4EE4\u8B58\u5225\u78BC\u3002","\u63A7\u5236\u662F\u5426\u986F\u793A\u66AB\u7559\u3002","\u63A7\u5236\u66AB\u7559\u986F\u793A\u7684\u5EF6\u9072\u6642\u9593 (\u4EE5\u6BEB\u79D2\u70BA\u55AE\u4F4D)\u3002","\u63A7\u5236\u7576\u6ED1\u9F20\u79FB\u904E\u6642\uFF0C\u662F\u5426\u61C9\u4FDD\u6301\u986F\u793A\u66AB\u7559\u3002","\u5728\u7DE8\u8F2F\u5668\u4E2D\u555F\u7528\u7A0B\u5F0F\u78BC\u52D5\u4F5C\u71C8\u6CE1\u3002","\u555F\u7528\u7DE8\u8F2F\u5668\u4E2D\u7684\u5167\u5D4C\u63D0\u793A\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u4E2D\u7684\u5167\u5D4C\u63D0\u793A\u5B57\u578B\u5927\u5C0F\u3002\u8A2D\u5B9A\u70BA `0` \u6642\uFF0C\u6703\u4F7F\u7528 90% \u7684 `#editor.fontSize#`\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u4E2D\u7684\u5167\u5D4C\u63D0\u793A\u5B57\u578B\u5BB6\u65CF\u3002","\u63A7\u5236\u884C\u9AD8\u3002\u4F7F\u7528 0 \u6703\u5F9E\u5B57\u578B\u5927\u5C0F\u8A08\u7B97\u884C\u9AD8\u3002","\u63A7\u5236\u662F\u5426\u6703\u986F\u793A\u7E2E\u5716","\u7E2E\u5716\u5927\u5C0F\u8207\u7DE8\u8F2F\u5668\u5167\u5BB9\u76F8\u540C (\u4E14\u53EF\u80FD\u6703\u6372\u52D5)\u3002","\u7E2E\u5716\u6703\u8996\u9700\u8981\u4F38\u7E2E\uFF0C\u4EE5\u586B\u6EFF\u8A72\u7DE8\u8F2F\u5668\u7684\u9AD8\u5EA6 (\u7121\u6372\u52D5)\u3002","\u7E2E\u5716\u5C07\u8996\u9700\u8981\u7E2E\u5C0F\uFF0C\u4E00\u5F8B\u4E0D\u6703\u5927\u65BC\u8A72\u7DE8\u8F2F\u5668 (\u7121\u6372\u52D5)\u3002","\u63A7\u5236\u7E2E\u5716\u7684\u5927\u5C0F\u3002","\u63A7\u5236\u8981\u5728\u54EA\u7AEF\u5448\u73FE\u7E2E\u5716\u3002","\u63A7\u5236\u4F55\u6642\u986F\u793A\u8FF7\u4F60\u5730\u5716\u6ED1\u687F\u3002","\u7E2E\u5716\u5167\u6240\u7E6A\u88FD\u7684\u5167\u5BB9\u5927\u5C0F: 1\u30012 \u6216 3\u3002","\u986F\u793A\u884C\u4E2D\u7684\u5BE6\u969B\u5B57\u5143\uFF0C\u800C\u4E0D\u662F\u8272\u5F69\u5340\u584A\u3002","\u9650\u5236\u7E2E\u5716\u7684\u5BEC\u5EA6\uFF0C\u6700\u591A\u986F\u793A\u67D0\u500B\u6578\u76EE\u7684\u5217\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u4E0A\u908A\u7DE3\u8207\u7B2C\u4E00\u884C\u4E4B\u9593\u7684\u7A7A\u683C\u6578\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u4E0B\u908A\u7DE3\u8207\u6700\u5F8C\u4E00\u884C\u4E4B\u9593\u7684\u7A7A\u683C\u6578\u3002","\u555F\u7528\u5FEB\u986F\uFF0C\u5728\u60A8\u9375\u5165\u7684\u540C\u6642\u986F\u793A\u53C3\u6578\u6587\u4EF6\u548C\u985E\u578B\u8CC7\u8A0A\u3002","\u63A7\u5236\u63D0\u793A\u529F\u80FD\u8868\u662F\u5426\u5728\u6E05\u55AE\u7D50\u5C3E\u6642\u5FAA\u74B0\u6216\u95DC\u9589\u3002","\u5141\u8A31\u5728\u5B57\u4E32\u5167\u986F\u793A\u5373\u6642\u5EFA\u8B70\u3002","\u5141\u8A31\u5728\u8A3B\u89E3\u4E2D\u986F\u793A\u5373\u6642\u5EFA\u8B70\u3002","\u5141\u8A31\u5728\u5B57\u4E32\u8207\u8A3B\u89E3\u4EE5\u5916\u4E4B\u8655\u986F\u793A\u5373\u6642\u5EFA\u8B70\u3002","\u63A7\u5236\u662F\u5426\u61C9\u5728\u9375\u5165\u6642\u81EA\u52D5\u986F\u793A\u5EFA\u8B70\u3002","\u4E0D\u986F\u793A\u884C\u865F\u3002","\u884C\u865F\u4EE5\u7D55\u5C0D\u503C\u986F\u793A\u3002","\u884C\u865F\u4EE5\u76EE\u524D\u6E38\u6A19\u7684\u76F8\u5C0D\u503C\u986F\u793A\u3002","\u6BCF 10 \u884C\u986F\u793A\u884C\u865F\u3002","\u63A7\u5236\u884C\u865F\u7684\u986F\u793A\u3002","\u9019\u500B\u7DE8\u8F2F\u5668\u5C3A\u898F\u6703\u8F49\u8B6F\u7684\u7B49\u5BEC\u5B57\u5143\u6578\u3002","\u6B64\u7DE8\u8F2F\u5668\u5C3A\u898F\u7684\u8272\u5F69\u3002","\u5728\u67D0\u500B\u6578\u76EE\u7684\u7B49\u5BEC\u5B57\u5143\u4E4B\u5F8C\u986F\u793A\u5782\u76F4\u5C3A\u898F\u3002\u5982\u6709\u591A\u500B\u5C3A\u898F\uFF0C\u5C31\u6703\u4F7F\u7528\u591A\u500B\u503C\u3002\u82E5\u9663\u5217\u7A7A\u767D\uFF0C\u5C31\u4E0D\u6703\u7E6A\u88FD\u4EFB\u4F55\u5C3A\u898F\u3002","\u63D2\u5165\u5EFA\u8B70\u800C\u4E0D\u8986\u5BEB\u6E38\u6A19\u65C1\u7684\u6587\u5B57\u3002","\u63D2\u5165\u5EFA\u8B70\u4E26\u8986\u5BEB\u6E38\u6A19\u65C1\u7684\u6587\u5B57\u3002","\u63A7\u5236\u662F\u5426\u8981\u5728\u63A5\u53D7\u5B8C\u6210\u6642\u8986\u5BEB\u5B57\u7D44\u3002\u8ACB\u6CE8\u610F\uFF0C\u9019\u53D6\u6C7A\u65BC\u52A0\u5165\u6B64\u529F\u80FD\u7684\u5EF6\u4F38\u6A21\u7D44\u3002","\u63A7\u5236\u5C0D\u65BC\u62DA\u932F\u5B57\u662F\u5426\u9032\u884C\u7BE9\u9078\u548C\u6392\u5E8F\u5176\u5EFA\u8B70","\u63A7\u5236\u6392\u5E8F\u662F\u5426\u6703\u504F\u597D\u6E38\u6A19\u9644\u8FD1\u51FA\u73FE\u7684\u5B57\u7D44\u3002","\u63A7\u5236\u8A18\u9304\u7684\u5EFA\u8B70\u9078\u53D6\u9805\u76EE\u662F\u5426\u5728\u591A\u500B\u5DE5\u4F5C\u5340\u548C\u8996\u7A97\u9593\u5171\u7528 (\u9700\u8981 `#editor.suggestSelection#`)\u3002","\u63A7\u5236\u6B63\u5728\u4F7F\u7528\u7684\u7A0B\u5F0F\u78BC\u7247\u6BB5\u662F\u5426\u6703\u907F\u514D\u5FEB\u901F\u5EFA\u8B70\u3002","\u63A7\u5236\u8981\u5728\u5EFA\u8B70\u4E2D\u986F\u793A\u6216\u96B1\u85CF\u5716\u793A\u3002","\u63A7\u5236\u5EFA\u8B70\u5C0F\u5DE5\u5177\u5E95\u4E0B\u7684\u72C0\u614B\u5217\u53EF\u898B\u5EA6\u3002","\u63A7\u5236\u5EFA\u8B70\u8A73\u7D30\u8CC7\u6599\u662F\u4EE5\u5167\u5D4C\u65BC\u6A19\u7C64\u7684\u65B9\u5F0F\u986F\u793A\uFF0C\u9084\u662F\u53EA\u5728\u8A73\u7D30\u8CC7\u6599\u5C0F\u5DE5\u5177\u4E2D\u986F\u793A","\u6B64\u8A2D\u5B9A\u5DF2\u6DD8\u6C70\u3002\u5EFA\u8B70\u5C0F\u5DE5\u5177\u73FE\u53EF\u8ABF\u6574\u5927\u5C0F\u3002","\u6B64\u8A2D\u5B9A\u5DF2\u6DD8\u6C70\uFF0C\u8ACB\u6539\u7528 'editor.suggest.showKeywords' \u6216 'editor.suggest.showSnippets' \u7B49\u55AE\u7368\u8A2D\u5B9A\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u65B9\u6CD5\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u51FD\u5F0F\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u5EFA\u69CB\u51FD\u5F0F\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u6B04\u4F4D\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u8B8A\u6578\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u985E\u5225\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u7D50\u69CB\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u4ECB\u9762\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u6A21\u7D44\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u5C6C\u6027\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u4E8B\u4EF6\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u904B\u7B97\u5B50\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u55AE\u4F4D\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u503C\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u5E38\u6578\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u5217\u8209\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300CenumMember\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u95DC\u9375\u5B57\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u6587\u5B57\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u8272\u5F69\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u6A94\u6848\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u53C3\u8003\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300Ccustomcolor\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u8CC7\u6599\u593E\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300CtypeParameter\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u986F\u793A\u300C\u7A0B\u5F0F\u78BC\u7247\u6BB5\u300D\u5EFA\u8B70\u3002","\u555F\u7528\u4E4B\u5F8C\uFF0CIntelliSense \u6703\u986F\u793A `user`-suggestions\u3002","\u555F\u7528\u6642\uFF0CIntelliSense \u6703\u986F\u793A `issues`-suggestions\u3002","\u662F\u5426\u61C9\u4E00\u5F8B\u9078\u53D6\u524D\u7F6E\u548C\u5F8C\u7F6E\u7684\u7A7A\u767D\u5B57\u5143\u3002","\u63A7\u5236\u662F\u5426\u900F\u904E\u8A8D\u53EF\u5B57\u5143\u63A5\u53D7\u5EFA\u8B70\u3002\u4F8B\u5982\u5728 JavaScript \u4E2D\uFF0C\u5206\u865F (';') \u53EF\u4EE5\u662F\u63A5\u53D7\u5EFA\u8B70\u4E26\u9375\u5165\u8A72\u5B57\u5143\u7684\u8A8D\u53EF\u5B57\u5143\u3002","\u5728\u5EFA\u8B70\u9032\u884C\u6587\u5B57\u8B8A\u66F4\u6642\uFF0C\u50C5\u900F\u904E `Enter` \u63A5\u53D7\u5EFA\u8B70\u3002","\u63A7\u5236\u9664\u4E86 'Tab' \u5916\uFF0C\u662F\u5426\u4E5F\u900F\u904E 'Enter' \u63A5\u53D7\u5EFA\u8B70\u3002\u9019\u6709\u52A9\u65BC\u907F\u514D\u6DF7\u6DC6\u8981\u63D2\u5165\u65B0\u884C\u6216\u63A5\u53D7\u5EFA\u8B70\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u4E2D\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u53EF\u8B80\u51FA\u7684\u884C\u6578\u3002\u8B66\u544A: \u5927\u65BC\u9810\u8A2D\u7684\u6578\u76EE\u6703\u5C0D\u6548\u80FD\u7522\u751F\u5F71\u97FF\u3002","\u7DE8\u8F2F\u5668\u5167\u5BB9","\u4F7F\u7528\u8A9E\u8A00\u914D\u7F6E\u78BA\u5B9A\u4F55\u6642\u81EA\u52D5\u95DC\u9589\u62EC\u865F\u3002","\u50C5\u7576\u6E38\u6A19\u4F4D\u65BC\u7A7A\u767D\u7684\u5DE6\u5074\u6642\u81EA\u52D5\u95DC\u9589\u62EC\u865F\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u5728\u4F7F\u7528\u8005\u65B0\u589E\u5DE6\u62EC\u5F27\u5F8C\uFF0C\u81EA\u52D5\u52A0\u4E0A\u53F3\u62EC\u5F27\u3002","\u50C5\u5728\u81EA\u52D5\u63D2\u5165\u53F3\u5F15\u865F\u6216\u62EC\u865F\u6642\uFF0C\u624D\u5728\u5176\u4E0A\u65B9\u9375\u5165\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u5728\u53F3\u5F15\u865F\u6216\u62EC\u865F\u4E0A\u9375\u5165\u3002","\u4F7F\u7528\u8A9E\u8A00\u914D\u7F6E\u78BA\u5B9A\u4F55\u6642\u81EA\u52D5\u95DC\u9589\u5F15\u865F\u3002","\u50C5\u7576\u6E38\u6A19\u4F4D\u65BC\u7A7A\u767D\u7684\u5DE6\u5074\u6642\u81EA\u52D5\u95DC\u9589\u5F15\u865F\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u5728\u4F7F\u7528\u8005\u65B0\u589E\u958B\u59CB\u5F15\u865F\u5F8C\uFF0C\u81EA\u52D5\u52A0\u4E0A\u95DC\u9589\u5F15\u865F\u3002","\u7DE8\u8F2F\u5668\u4E0D\u6703\u81EA\u52D5\u63D2\u5165\u7E2E\u6392\u3002","\u7DE8\u8F2F\u5668\u6703\u4FDD\u7559\u76EE\u524D\u884C\u7684\u7E2E\u6392\u3002","\u7DE8\u8F2F\u5668\u6703\u4FDD\u7559\u76EE\u524D\u884C\u7684\u7E2E\u6392\u4E26\u63A5\u53D7\u8A9E\u8A00\u5B9A\u7FA9\u7684\u62EC\u865F\u3002","\u7DE8\u8F2F\u5668\u6703\u76EE\u524D\u884C\u7684\u7E2E\u6392\u3001\u63A5\u53D7\u8A9E\u8A00\u5B9A\u7FA9\u7684\u62EC\u865F\u4E26\u53EB\u7528\u8A9E\u8A00\u5B9A\u7FA9\u7684\u7279\u6B8A onEnterRules\u3002","\u7DE8\u8F2F\u5668\u6703\u4FDD\u7559\u76EE\u524D\u884C\u7684\u7E2E\u6392\u3001\u63A5\u53D7\u8A9E\u8A00\u5B9A\u7FA9\u7684\u62EC\u865F\u4E26\u53EB\u7528\u8A9E\u8A00\u5B9A\u7FA9\u7684\u7279\u6B8A onEnterRules \u4E26\u63A5\u53D7\u8A9E\u8A00\u5B9A\u7FA9\u7684 indentationRules\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u5728\u4F7F\u7528\u8005\u9375\u5165\u3001\u8CBC\u4E0A\u3001\u79FB\u52D5\u6216\u7E2E\u6392\u884C\u6642\u81EA\u52D5\u8ABF\u6574\u7E2E\u6392\u3002","\u4F7F\u7528\u8A9E\u8A00\u7D44\u614B\u4F86\u6C7A\u5B9A\u4F55\u6642\u81EA\u52D5\u74B0\u7E5E\u9078\u53D6\u9805\u76EE\u3002","\u7528\u5F15\u865F\u62EC\u4F4F\uFF0C\u800C\u975E\u4F7F\u7528\u62EC\u5F27\u3002","\u7528\u62EC\u5F27\u62EC\u4F4F\uFF0C\u800C\u975E\u4F7F\u7528\u5F15\u865F\u3002 ","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u5728\u9375\u5165\u5F15\u865F\u6216\u62EC\u5F27\u6642\u81EA\u52D5\u5305\u570D\u9078\u53D6\u7BC4\u570D\u3002","\u7576\u4F7F\u7528\u7A7A\u683C\u9032\u884C\u7E2E\u6392\u6642\uFF0C\u6703\u6A21\u64EC\u5B9A\u4F4D\u5B57\u5143\u7684\u9078\u53D6\u884C\u70BA\u3002\u9078\u53D6\u7BC4\u570D\u6703\u4F9D\u5FAA\u5B9A\u4F4D\u505C\u99D0\u9EDE\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u986F\u793A codelens\u3002","\u63A7\u5236 CodeLens \u7684\u5B57\u578B\u5BB6\u65CF\u3002","\u63A7\u5236 CodeLens \u7684\u5B57\u578B\u5927\u5C0F (\u50CF\u7D20)\u3002\u8A2D\u5B9A\u70BA `0` \u6642\uFF0C\u6703\u4F7F\u7528 90% \u7684 `#editor.fontSize#`\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u8F49\u8B6F\u5167\u5D4C\u8272\u5F69\u88DD\u98FE\u9805\u76EE\u8207\u8272\u5F69\u9078\u64C7\u5668\u3002","\u555F\u7528\u5373\u53EF\u4EE5\u6ED1\u9F20\u8207\u6309\u9375\u9078\u53D6\u9032\u884C\u8CC7\u6599\u884C\u9078\u53D6\u3002","\u63A7\u5236\u8A9E\u6CD5\u9192\u76EE\u63D0\u793A\u662F\u5426\u61C9\u8907\u88FD\u5230\u526A\u8CBC\u7C3F\u3002","\u63A7\u5236\u8CC7\u6599\u6307\u6A19\u52D5\u756B\u6A23\u5F0F\u3002","\u63A7\u5236\u662F\u5426\u61C9\u555F\u7528\u5E73\u6ED1\u63D2\u5165\u9EDE\u52D5\u756B\u3002 ","\u63A7\u5236\u8CC7\u6599\u6307\u6A19\u6A23\u5F0F\u3002","\u63A7\u5236\u6E38\u6A19\u4E0A\u4E0B\u5468\u570D\u53EF\u986F\u793A\u7684\u6700\u5C11\u884C\u6578\u3002\u5728\u67D0\u4E9B\u7DE8\u8F2F\u5668\u4E2D\u7A31\u70BA 'scrollOff' \u6216 'scrollOffset'\u3002","\u53EA\u6709\u901A\u904E\u9375\u76E4\u6216 API \u89F8\u767C\u6642\uFF0C\u624D\u6703\u65BD\u884C `cursorSurroundingLines`\u3002","\u4E00\u5F8B\u5F37\u5236\u57F7\u884C `cursorSurroundingLines`","\u63A7\u5236\u61C9\u65BD\u884C `cursorSurroundingLines` \u7684\u6642\u6A5F\u3002","\u63A7\u5236\u6E38\u6A19\u5BEC\u5EA6\uFF0C\u7576 `#editor.cursorStyle#` \u8A2D\u5B9A\u70BA `line` \u6642\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u5141\u8A31\u900F\u904E\u62D6\u653E\u4F86\u79FB\u52D5\u9078\u53D6\u9805\u76EE\u3002","\u6309\u4E0B `Alt` \u6642\u7684\u6372\u52D5\u901F\u5EA6\u4E58\u6578\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u555F\u7528\u7A0B\u5F0F\u78BC\u647A\u758A\u529F\u80FD\u3002","\u4F7F\u7528\u8A9E\u8A00\u7279\u5B9A\u647A\u758A\u7B56\u7565 (\u5982\u679C\u53EF\u7528)\uFF0C\u5426\u5247\u4F7F\u7528\u7E2E\u6392\u5F0F\u7B56\u7565\u3002","\u4F7F\u7528\u7E2E\u6392\u5F0F\u647A\u758A\u7B56\u7565\u3002","\u63A7\u5236\u8A08\u7B97\u8CC7\u6599\u593E\u7BC4\u570D\u7684\u7B56\u7565\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u5C07\u6298\u758A\u7684\u7BC4\u570D\u9192\u76EE\u63D0\u793A\u3002","\u63A7\u5236\u6309\u4E00\u4E0B\u5DF2\u6298\u758A\u884C\u5F8C\u65B9\u7684\u7A7A\u767D\u5167\u5BB9\u662F\u5426\u6703\u5C55\u958B\u884C\u3002","\u63A7\u5236\u5B57\u578B\u5BB6\u65CF\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u81EA\u52D5\u70BA\u8CBC\u4E0A\u7684\u5167\u5BB9\u8A2D\u5B9A\u683C\u5F0F\u3002\u5FC5\u9808\u6709\u53EF\u7528\u7684\u683C\u5F0F\u5668\uFF0C\u800C\u4E14\u683C\u5F0F\u5668\u61C9\u80FD\u5920\u70BA\u6587\u4EF6\u4E2D\u7684\u4E00\u500B\u7BC4\u570D\u8A2D\u5B9A\u683C\u5F0F\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u81EA\u52D5\u5728\u9375\u5165\u5F8C\u8A2D\u5B9A\u884C\u7684\u683C\u5F0F\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u8F49\u8B6F\u5782\u76F4\u5B57\u7B26\u908A\u754C\u3002\u5B57\u7B26\u908A\u754C\u6700\u5E38\u7528\u4F86\u9032\u884C\u5075\u932F\u3002","\u63A7\u5236\u6E38\u6A19\u662F\u5426\u61C9\u96B1\u85CF\u5728\u6982\u89C0\u5C3A\u898F\u4E2D\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u9192\u76EE\u63D0\u793A\u4F7F\u7528\u4E2D\u7684\u7E2E\u6392\u8F14\u52A9\u7DDA\u3002","\u63A7\u5236\u5B57\u6BCD\u9593\u8DDD (\u50CF\u7D20)\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u5DF2\u555F\u7528\u9023\u7D50\u7DE8\u8F2F\u3002\u76F8\u95DC\u7B26\u865F (\u4F8B\u5982 HTML \u6A19\u7C64) \u6703\u6839\u64DA\u8A9E\u8A00\u5728\u7DE8\u8F2F\u6642\u66F4\u65B0\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u5075\u6E2C\u9023\u7D50\u4E26\u4F7F\u5176\u53EF\u4F9B\u9EDE\u9078\u3002","\u5C07\u7B26\u5408\u7684\u62EC\u865F\u9192\u76EE\u63D0\u793A\u3002","\u8981\u7528\u65BC\u6ED1\u9F20\u6EFE\u8F2A\u6372\u52D5\u4E8B\u4EF6 `deltaX` \u548C `deltaY` \u7684\u4E58\u6578\u3002","\u4F7F\u7528\u6ED1\u9F20\u6EFE\u8F2A\u4E26\u6309\u4F4F `Ctrl` \u6642\uFF0C\u7E2E\u653E\u7DE8\u8F2F\u5668\u7684\u5B57\u578B","\u5728\u591A\u500B\u6E38\u6A19\u91CD\u758A\u6642\u5C07\u5176\u5408\u4F75\u3002","\u5C0D\u61C9Windows\u548CLinux\u7684'Control'\u8207\u5C0D\u61C9 macOS \u7684'Command'\u3002","\u5C0D\u61C9Windows\u548CLinux\u7684'Alt'\u8207\u5C0D\u61C9macOS\u7684'Option'\u3002","\u7528\u65BC\u5728\u6ED1\u9F20\u65B0\u589E\u591A\u500B\u6E38\u6A19\u7684\u4E58\u6578\u3002\u300C\u79FB\u81F3\u5B9A\u7FA9\u300D\u548C\u300C\u958B\u555F\u9023\u7D50\u300D\u6ED1\u9F20\u624B\u52E2\u6703\u52A0\u4EE5\u9069\u61C9\uFF0C\u4EE5\u907F\u514D\u8207\u591A\u500B\u6E38\u6A19\u7684\u4E58\u6578\u76F8\u885D\u7A81\u3002[\u6DF1\u5165\u4E86\u89E3](https://code.visualstudio.com/docs/editor/codebasics#_multicursor-modifier)\u3002","\u6BCF\u500B\u6E38\u6A19\u90FD\u6703\u8CBC\u4E0A\u4E00\u884C\u6587\u5B57\u3002","\u6BCF\u500B\u6E38\u6A19\u90FD\u6703\u8CBC\u4E0A\u5168\u6587\u3002","\u7576\u5DF2\u8CBC\u4E0A\u6587\u5B57\u7684\u884C\u6578\u8207\u6E38\u6A19\u6578\u76F8\u7B26\u6642\u63A7\u5236\u8CBC\u4E0A\u529F\u80FD\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u9192\u76EE\u986F\u793A\u51FA\u73FE\u7684\u8A9E\u610F\u7B26\u865F\u3002","\u63A7\u5236\u662F\u5426\u61C9\u5728\u6982\u89C0\u5C3A\u898F\u5468\u570D\u7E6A\u88FD\u6846\u7DDA\u3002","\u958B\u555F\u9810\u89BD\u6642\u7126\u9EDE\u6A39\u72C0","\u958B\u555F\u6642\u805A\u7126\u7DE8\u8F2F\u5668","\u63A7\u5236\u8981\u805A\u7126\u5167\u5D4C\u7DE8\u8F2F\u5668\u6216\u9810\u89BD\u5C0F\u5DE5\u5177\u4E2D\u7684\u6A39\u7CFB\u3002","\u63A7\u5236\u300C\u524D\u5F80\u5B9A\u7FA9\u300D\u6ED1\u9F20\u624B\u52E2\uFF0C\u662F\u5426\u4E00\u5F8B\u958B\u555F\u7784\u6838\u5C0F\u5DE5\u5177\u3002","\u63A7\u5236\u5728\u5FEB\u901F\u5EFA\u8B70\u986F\u793A\u5F8C\u7684\u5EF6\u9072 (\u4EE5\u6BEB\u79D2\u70BA\u55AE\u4F4D)\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u6703\u81EA\u52D5\u4F9D\u985E\u578B\u91CD\u65B0\u547D\u540D\u3002","\u5DF2\u6DD8\u6C70\uFF0C\u8ACB\u6539\u7528 `editor.linkedEditing`\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u986F\u793A\u63A7\u5236\u5B57\u5143\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u986F\u793A\u7E2E\u6392\u8F14\u52A9\u7DDA\u3002","\u5728\u6A94\u6848\u7D50\u5C3E\u70BA\u65B0\u884C\u6642\uFF0C\u5448\u73FE\u6700\u5F8C\u4E00\u884C\u7684\u865F\u78BC\u3002","\u9192\u76EE\u63D0\u793A\u88DD\u8A02\u908A\u548C\u76EE\u524D\u7684\u884C\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u5982\u4F55\u986F\u793A\u76EE\u524D\u884C\u7684\u9192\u76EE\u63D0\u793A\u3002","\u7576\u7126\u9EDE\u70BA\u8A72\u7DE8\u8F2F\u5668\u6642\uFF0C\u63A7\u5236\u8A72\u7DE8\u8F2F\u5668\u662F\u5426\u50C5\u61C9\u8F49\u8B6F\u76EE\u524D\u884C\u7684\u9192\u76EE\u63D0\u793A","\u8F49\u8B6F\u7A7A\u767D\u5B57\u5143\uFF0C\u4F46\u6587\u5B57\u4E4B\u9593\u7684\u55AE\u4E00\u7A7A\u683C\u9664\u5916\u3002","\u53EA\u8F49\u8B6F\u6240\u9078\u6587\u5B57\u7684\u7A7A\u767D\u5B57\u5143\u3002","\u53EA\u8F49\u8B6F\u7D50\u5C3E\u7A7A\u767D\u5B57\u5143","\u63A7\u5236\u7DE8\u8F2F\u5668\u61C9\u5982\u4F55\u8F49\u8B6F\u7A7A\u767D\u5B57\u5143\u3002","\u63A7\u5236\u9078\u53D6\u7BC4\u570D\u662F\u5426\u6709\u5713\u89D2","\u63A7\u5236\u7DE8\u8F2F\u5668\u6C34\u5E73\u6372\u52D5\u7684\u984D\u5916\u5B57\u5143\u6578\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u6372\u52D5\u5230\u6700\u5F8C\u4E00\u884C\u4E4B\u5916\u3002","\u540C\u6642\u9032\u884C\u5782\u76F4\u8207\u6C34\u5E73\u6372\u52D5\u6642\uFF0C\u50C5\u6CBF\u4E3B\u8EF8\u6372\u52D5\u3002\u907F\u514D\u5728\u8ECC\u8DE1\u677F\u4E0A\u9032\u884C\u5782\u76F4\u6372\u52D5\u6642\u767C\u751F\u6C34\u5E73\u6F02\u79FB\u3002","\u63A7\u5236\u662F\u5426\u652F\u63F4 Linux \u4E3B\u8981\u526A\u8CBC\u7C3F\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u61C9\u9192\u76EE\u63D0\u793A\u8207\u9078\u53D6\u9805\u76EE\u985E\u4F3C\u7684\u76F8\u7B26\u9805\u76EE\u3002","\u4E00\u5F8B\u986F\u793A\u647A\u758A\u63A7\u5236\u9805\u3002","\u50C5\u7576\u6ED1\u9F20\u61F8\u505C\u5728\u6D3B\u52D5\u5217\u4E0A\u6642\uFF0C\u624D\u986F\u793A\u6298\u758A\u529F\u80FD\u3002","\u63A7\u5236\u647A\u758A\u63A7\u5236\u9805\u5728\u88DD\u8A02\u908A\u4E0A\u7684\u986F\u793A\u6642\u6A5F\u3002","\u63A7\u5236\u672A\u4F7F\u7528\u7A0B\u5F0F\u78BC\u7684\u6DE1\u51FA\u3002","\u63A7\u5236\u5DF2\u522A\u9664\u7684\u6DD8\u6C70\u8B8A\u6578\u3002","\u5C07\u7A0B\u5F0F\u78BC\u7247\u6BB5\u5EFA\u8B70\u986F\u793A\u65BC\u5176\u4ED6\u5EFA\u8B70\u7684\u9802\u7AEF\u3002","\u5C07\u7A0B\u5F0F\u78BC\u7247\u6BB5\u5EFA\u8B70\u986F\u793A\u65BC\u5176\u4ED6\u5EFA\u8B70\u7684\u4E0B\u65B9\u3002","\u5C07\u7A0B\u5F0F\u78BC\u7247\u6BB5\u5EFA\u8B70\u8207\u5176\u4ED6\u5EFA\u8B70\u4E00\u540C\u986F\u793A\u3002","\u4E0D\u986F\u793A\u7A0B\u5F0F\u78BC\u7247\u6BB5\u5EFA\u8B70\u3002","\u63A7\u5236\u7A0B\u5F0F\u78BC\u7247\u6BB5\u662F\u5426\u96A8\u5176\u4ED6\u5EFA\u8B70\u986F\u793A\uFF0C\u4EE5\u53CA\u5176\u6392\u5E8F\u65B9\u5F0F\u3002","\u63A7\u5236\u7DE8\u8F2F\u5668\u662F\u5426\u6703\u4F7F\u7528\u52D5\u756B\u6372\u52D5","\u5EFA\u8B70\u5C0F\u5DE5\u5177\u7684\u5B57\u578B\u5927\u5C0F\u3002\u7576\u8A2D\u5B9A\u70BA `0` \u6642\uFF0C\u5247\u4F7F\u7528 `#editor.fontSize#` \u503C.","\u5EFA\u8B70\u5C0F\u5DE5\u5177\u7684\u884C\u9AD8\u3002\u7576\u8A2D\u5B9A\u70BA `0` \u6642\uFF0C\u5247\u4F7F\u7528 `#editor.lineHeight#` \u7684\u503C\u3002\u6700\u5C0F\u503C\u70BA 8\u3002","\u63A7\u5236\u5EFA\u8B70\u662F\u5426\u61C9\u5728\u9375\u5165\u89F8\u767C\u5B57\u5143\u6642\u81EA\u52D5\u986F\u793A\u3002","\u4E00\u5F8B\u9078\u53D6\u7B2C\u4E00\u500B\u5EFA\u8B70\u3002","\u9664\u975E\u9032\u4E00\u6B65\u9375\u5165\u9078\u53D6\u4E86\u5EFA\u8B70\uFF0C\u5426\u5247\u9078\u53D6\u6700\u8FD1\u7684\u5EFA\u8B70\uFF0C\u4F8B\u5982 `console.| -> console.log`\uFF0C\u539F\u56E0\u662F\u6700\u8FD1\u5B8C\u6210\u4E86 `log`\u3002","\u6839\u64DA\u5148\u524D\u5DF2\u5B8C\u6210\u8A72\u5EFA\u8B70\u7684\u524D\u7F6E\u8A5E\u9078\u53D6\u5EFA\u8B70\uFF0C\u4F8B\u5982 `co -> console` \u548C `con -> const`\u3002","\u63A7\u5236\u5728\u986F\u793A\u5EFA\u8B70\u6E05\u55AE\u6642\u5982\u4F55\u9810\u5148\u9078\u53D6\u5EFA\u8B70\u3002","\u6309 Tab \u6642\uFF0CTab \u5B8C\u6210\u6703\u63D2\u5165\u6700\u7B26\u5408\u7684\u5EFA\u8B70\u3002","\u505C\u7528 tab \u9375\u81EA\u52D5\u5B8C\u6210\u3002","\u5728\u7A0B\u5F0F\u78BC\u7247\u6BB5\u7684\u9996\u78BC\u76F8\u7B26\u6642\u4F7F\u7528 Tab \u5B8C\u6210\u3002\u672A\u555F\u7528 'quickSuggestions' \u6642\u6548\u679C\u6700\u4F73\u3002","\u555F\u7528 tab \u9375\u81EA\u52D5\u5B8C\u6210\u3002","\u81EA\u52D5\u79FB\u9664\u7570\u5E38\u7684\u884C\u7D50\u675F\u5B57\u5143\u3002","\u5FFD\u7565\u7570\u5E38\u7684\u884C\u7D50\u675F\u5B57\u5143\u3002","\u8981\u79FB\u9664\u4E4B\u7570\u5E38\u7684\u884C\u7D50\u675F\u5B57\u5143\u63D0\u793A\u3002","\u79FB\u9664\u53EF\u80FD\u5C0E\u81F4\u554F\u984C\u7684\u7570\u5E38\u884C\u7D50\u675F\u5B57\u5143\u3002","\u63D2\u5165\u548C\u522A\u9664\u63A5\u5728\u5B9A\u4F4D\u505C\u99D0\u9EDE\u5F8C\u7684\u7A7A\u767D\u5B57\u5143\u3002","\u5728\u57F7\u884C\u6587\u5B57\u76F8\u95DC\u5C0E\u89BD\u6216\u4F5C\u696D\u6642\u8981\u7528\u4F5C\u6587\u5B57\u5206\u9694\u7B26\u865F\u7684\u5B57\u5143","\u4E00\u5F8B\u4E0D\u63DB\u884C\u3002","\u4F9D\u6AA2\u8996\u5340\u5BEC\u5EA6\u63DB\u884C\u3002","\u65BC '#editor.wordWrapColumn#' \u63DB\u884C\u3002","\u7576\u6AA2\u8996\u5340\u7E2E\u81F3\u6700\u5C0F\u4E26\u8A2D\u5B9A '#editor.wordWrapColumn#' \u6642\u63DB\u884C\u3002","\u63A7\u5236\u5982\u4F55\u63DB\u884C\u3002","\u7576 `#editor.wordWrap#` \u70BA `wordWrapColumn` \u6216 `bounded` \u6642\uFF0C\u63A7\u5236\u7DE8\u8F2F\u5668\u4E2D\u7684\u8CC7\u6599\u884C\u63DB\u884C\u3002","\u7121\u7E2E\u6392\u3002\u63DB\u884C\u5F9E\u7B2C 1 \u5217\u958B\u59CB\u3002","\u63DB\u884C\u7684\u7E2E\u6392\u6703\u8207\u7236\u884C\u76F8\u540C\u3002","\u63DB\u884C\u7684\u7E2E\u6392\u70BA\u7236\u884C +1\u3002","\u63DB\u884C\u7E2E\u6392\u70BA\u7236\u884C +2\u3002","\u63A7\u5236\u63DB\u884C\u7684\u7E2E\u6392\u3002","\u5047\u8A2D\u6240\u6709\u5B57\u5143\u7684\u5BEC\u5EA6\u5747\u76F8\u540C\u3002\u9019\u662F\u4E00\u7A2E\u5FEB\u901F\u7684\u6F14\u7B97\u6CD5\uFF0C\u9069\u7528\u65BC\u7B49\u5BEC\u5B57\u578B\uFF0C\u4EE5\u53CA\u5B57\u7B26\u5BEC\u5EA6\u76F8\u540C\u7684\u90E8\u5206\u6307\u4EE4\u78BC (\u4F8B\u5982\u62C9\u4E01\u6587\u5B57\u5143)\u3002","\u5C07\u5916\u570D\u9EDE\u8A08\u7B97\u59D4\u6D3E\u7D66\u700F\u89BD\u5668\u3002\u9019\u662F\u7DE9\u6162\u7684\u6F14\u7B97\u6CD5\uFF0C\u5982\u679C\u6A94\u6848\u8F03\u5927\u53EF\u80FD\u6703\u5C0E\u81F4\u51CD\u7D50\uFF0C\u4F46\u5728\u6240\u6709\u60C5\u6CC1\u4E0B\u90FD\u6B63\u5E38\u904B\u4F5C\u3002","\u63A7\u5236\u8A08\u7B97\u5916\u570D\u9EDE\u7684\u6F14\u7B97\u6CD5\u3002"],"vs/editor/common/editorContextKeys":["Whether the editor text has focus (cursor is blinking)","Whether the editor or an editor widget has focus (e.g. focus is in the find widget)","Whether an editor or a rich text input has focus (cursor is blinking)","Whether the editor is read only","Whether the context is a diff editor","Whether `editor.columnSelection` is enabled","Whether the editor has text selected","Whether the editor has multiple selections","Whether `Tab` will move focus out of the editor","Whether the editor hover is visible","Whether the editor is part of a larger editor (e.g. notebooks)","The language identifier of the editor","Whether the editor has a completion item provider","Whether the editor has a code actions provider","Whether the editor has a code lens provider","Whether the editor has a definition provider","Whether the editor has a declaration provider","Whether the editor has an implementation provider","Whether the editor has a type definition provider","Whether the editor has a hover provider","Whether the editor has a document highlight provider","Whether the editor has a document symbol provider","Whether the editor has a reference provider","Whether the editor has a rename provider","Whether the editor has a signature help provider","Whether the editor has an inline hints provider","Whether the editor has a document formatting provider","Whether the editor has a document selection formatting provider","Whether the editor has multiple document formatting providers","Whether the editor has multiple document selection formatting providers"],"vs/editor/common/model/editStack":["\u6B63\u5728\u9375\u5165"],"vs/editor/common/modes/modesRegistry":["\u7D14\u6587\u5B57"],"vs/editor/common/standaloneStrings":["\u7121\u9078\u53D6\u9805\u76EE","\u7B2C {0} \u884C\uFF0C\u7B2C {1} \u6B04 (\u5DF2\u9078\u53D6 {2})","\u7B2C {0} \u884C\uFF0C\u7B2C {1} \u6B04","{0} \u500B\u9078\u53D6\u9805\u76EE (\u5DF2\u9078\u53D6 {1} \u500B\u5B57\u5143)","{0} \u500B\u9078\u53D6\u9805\u76EE","\u7ACB\u5373\u5C07\u8A2D\u5B9A `accessibilitySupport` \u8B8A\u66F4\u70BA 'on\u2019\u3002","\u7ACB\u5373\u958B\u555F\u7DE8\u8F2F\u5668\u5354\u52A9\u5DE5\u5177\u6587\u4EF6\u9801\u9762\u3002","\u5728 Diff \u7DE8\u8F2F\u5668\u7684\u552F\u8B80\u7A97\u683C\u4E2D\u3002","\u5728 Diff \u7DE8\u8F2F\u5668\u7684\u7A97\u683C\u4E2D\u3002","\u5728\u552F\u8B80\u7A0B\u5F0F\u78BC\u7DE8\u8F2F\u5668\u4E2D","\u5728\u7A0B\u5F0F\u78BC\u7DE8\u8F2F\u5668\u4E2D","\u82E5\u8981\u70BA\u7DE8\u8F2F\u5668\u9032\u884C\u6700\u80FD\u642D\u914D\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u4F7F\u7528\u7684\u8A2D\u5B9A\uFF0C\u8ACB\u7ACB\u5373\u6309 Command+E\u3002","\u82E5\u8981\u5C07\u7DE8\u8F2F\u5668\u8A2D\u5B9A\u70BA\u91DD\u5C0D\u642D\u914D\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u4F7F\u7528\u6700\u4F73\u5316\uFF0C\u8ACB\u7ACB\u5373\u6309 Control+E\u3002","\u7DE8\u8F2F\u5668\u5DF2\u8A2D\u5B9A\u70BA\u91DD\u5C0D\u642D\u914D\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u4F7F\u7528\u6700\u4F73\u5316\u3002","\u5DF2\u5C07\u6B64\u7DE8\u8F2F\u5668\u8A2D\u5B9A\u70BA\u6C38\u9060\u4E0D\u91DD\u5C0D\u642D\u914D\u87A2\u5E55\u52A9\u8B80\u7A0B\u5F0F\u4F7F\u7528\u6700\u4F73\u5316\uFF0C\u4F46\u76EE\u524D\u4E0D\u662F\u6B64\u60C5\u6CC1\u3002","\u5728\u76EE\u524D\u7684\u7DE8\u8F2F\u5668\u4E2D\u6309 Tab \u9375\u6703\u5C07\u7126\u9EDE\u79FB\u81F3\u4E0B\u4E00\u500B\u53EF\u8A2D\u5B9A\u7126\u9EDE\u7684\u5143\u7D20\u3002\u6309 {0} \u53EF\u5207\u63DB\u6B64\u884C\u70BA\u3002","\u5728\u76EE\u524D\u7684\u7DE8\u8F2F\u5668\u4E2D\u6309 Tab \u9375\u6703\u5C07\u7126\u9EDE\u79FB\u81F3\u4E0B\u4E00\u500B\u53EF\u8A2D\u5B9A\u7126\u9EDE\u7684\u5143\u7D20\u3002\u547D\u4EE4 {0} \u76EE\u524D\u7121\u6CD5\u7531\u6309\u9375\u7E6B\u7D50\u95DC\u4FC2\u89F8\u767C\u3002","\u5728\u76EE\u524D\u7684\u7DE8\u8F2F\u5668\u4E2D\u6309 Tab \u9375\u6703\u63D2\u5165\u5B9A\u4F4D\u5B57\u5143\u3002\u6309 {0} \u53EF\u5207\u63DB\u6B64\u884C\u70BA\u3002","\u5728\u76EE\u524D\u7684\u7DE8\u8F2F\u5668\u4E2D\u6309 Tab \u9375\u6703\u63D2\u5165\u5B9A\u4F4D\u5B57\u5143\u3002\u547D\u4EE4 {0} \u76EE\u524D\u7121\u6CD5\u7531\u6309\u9375\u7E6B\u7D50\u95DC\u4FC2\u89F8\u767C\u3002","\u7ACB\u5373\u6309 Command+H\uFF0C\u4EE5\u958B\u555F\u63D0\u4F9B\u7DE8\u8F2F\u5668\u5354\u52A9\u5DE5\u5177\u76F8\u95DC\u8A73\u7D30\u8CC7\u8A0A\u7684\u700F\u89BD\u5668\u8996\u7A97\u3002","\u7ACB\u5373\u6309 Control+H\uFF0C\u4EE5\u958B\u555F\u63D0\u4F9B\u7DE8\u8F2F\u5668\u5354\u52A9\u5DE5\u5177\u76F8\u95DC\u8A73\u7D30\u8CC7\u8A0A\u7684\u700F\u89BD\u5668\u8996\u7A97\u3002","\u60A8\u53EF\u4EE5\u6309 Esc \u9375\u6216 Shift+Esc \u9375\u4F86\u89E3\u9664\u6B64\u5DE5\u5177\u63D0\u793A\u4E26\u8FD4\u56DE\u7DE8\u8F2F\u5668\u3002","\u986F\u793A\u5354\u52A9\u5DE5\u5177\u8AAA\u660E","\u958B\u767C\u4EBA\u54E1: \u6AA2\u67E5\u6B0A\u6756","\u524D\u5F80\u884C/\u6B04...","\u986F\u793A\u6240\u6709\u5FEB\u901F\u5B58\u53D6\u63D0\u4F9B\u8005","\u547D\u4EE4\u9078\u64C7\u5340","\u986F\u793A\u4E26\u57F7\u884C\u547D\u4EE4","\u79FB\u81F3\u7B26\u865F...","\u524D\u5F80\u7B26\u865F (\u4F9D\u985E\u5225)...","\u7DE8\u8F2F\u5668\u5167\u5BB9","\u6309 Alt+F1 \u53EF\u53D6\u5F97\u5354\u52A9\u5DE5\u5177\u9078\u9805\u3002","\u5207\u63DB\u9AD8\u5C0D\u6BD4\u4F48\u666F\u4E3B\u984C","\u5DF2\u5728 {1} \u6A94\u6848\u4E2D\u9032\u884C {0} \u9805\u7DE8\u8F2F"],"vs/editor/common/view/editorColorRegistry":["\u76EE\u524D\u6E38\u6A19\u4F4D\u7F6E\u884C\u7684\u53CD\u767D\u986F\u793A\u80CC\u666F\u8272\u5F69\u3002","\u76EE\u524D\u6E38\u6A19\u4F4D\u7F6E\u884C\u4E4B\u5468\u570D\u6846\u7DDA\u7684\u80CC\u666F\u8272\u5F69\u3002","\u9192\u76EE\u63D0\u793A\u7BC4\u570D\u7684\u80CC\u666F\u8272\u5F69\uFF0C\u4F8B\u5982\u5FEB\u901F\u958B\u555F\u4E26\u5C0B\u627E\u529F\u80FD\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u53CD\u767D\u986F\u793A\u7BC4\u570D\u5468\u570D\u908A\u6846\u7684\u80CC\u666F\u984F\u8272\u3002","\u9192\u76EE\u63D0\u793A\u7B26\u865F\u7684\u80CC\u666F\u8272\u5F69\uFF0C\u76F8\u4F3C\u65BC\u524D\u5F80\u4E0B\u4E00\u500B\u5B9A\u7FA9\u6216\u524D\u5F80\u4E0B\u4E00\u500B/\u4E0A\u4E00\u500B\u7B26\u865F\u3002\u8272\u5F69\u5FC5\u9808\u900F\u660E\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u9192\u76EE\u63D0\u793A\u5468\u570D\u7684\u908A\u754C\u80CC\u666F\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u6E38\u6A19\u7684\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u6E38\u6A19\u7684\u80CC\u666F\u8272\u5F69\u3002\u5141\u8A31\u81EA\u8A02\u5340\u584A\u6E38\u6A19\u91CD\u758A\u7684\u5B57\u5143\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u4E2D\u7A7A\u767D\u5B57\u5143\u7684\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u7E2E\u6392\u8F14\u52A9\u7DDA\u7684\u8272\u5F69\u3002","\u4F7F\u7528\u4E2D\u7DE8\u8F2F\u5668\u7E2E\u6392\u8F14\u52A9\u7DDA\u7684\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u884C\u865F\u7684\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u4F7F\u7528\u4E2D\u884C\u865F\u7684\u8272\u5F69","Id \u5DF2\u53D6\u4EE3\u3002\u8ACB\u6539\u7528 'editorLineNumber.activeForeground' \u3002","\u7DE8\u8F2F\u5668\u4F7F\u7528\u4E2D\u884C\u865F\u7684\u8272\u5F69","\u7DE8\u8F2F\u5668\u5C3A\u898F\u7684\u8272\u5F69","\u7DE8\u8F2F\u5668\u7A0B\u5F0F\u78BC\u6FFE\u93E1\u7684\u524D\u666F\u8272\u5F69","\u6210\u5C0D\u62EC\u865F\u80CC\u666F\u8272\u5F69","\u6210\u5C0D\u62EC\u865F\u908A\u6846\u8272\u5F69","\u9810\u89BD\u6AA2\u8996\u7DE8\u8F2F\u5668\u5C3A\u898F\u7684\u908A\u6846\u8272\u5F69.","\u7DE8\u8F2F\u5668\u6982\u89C0\u5C3A\u898F\u7684\u80CC\u666F\u8272\u5F69\u3002\u50C5\u5728\u555F\u7528\u7E2E\u5716\u4E26\u5C07\u5176\u7F6E\u65BC\u7DE8\u8F2F\u5668\u53F3\u5074\u6642\u4F7F\u7528\u3002","\u7DE8\u8F2F\u5668\u908A\u6846\u7684\u80CC\u666F\u984F\u8272,\u5305\u542B\u884C\u865F\u8207\u5B57\u5F62\u5716\u793A\u7684\u908A\u6846.","\u7DE8\u8F2F\u5668\u4E2D\u4E0D\u5FC5\u8981 (\u672A\u4F7F\u7528) \u539F\u59CB\u7A0B\u5F0F\u78BC\u7684\u6846\u7DDA\u8272\u5F69\u3002",`\u7DE8\u8F2F\u5668\u4E2D\u4E0D\u5FC5\u8981 (\u672A\u4F7F\u7528) \u539F\u59CB\u7A0B\u5F0F\u78BC\u7684\u4E0D\u900F\u660E\u5EA6\u3002\u4F8B\u5982 "#000000c0\u201D \u6703\u4EE5 75% \u7684\u4E0D\u900F\u660E\u5EA6\u8F49\u8B6F\u7A0B\u5F0F\u78BC\u3002\u91DD\u5C0D\u9AD8\u5C0D\u6BD4\u4E3B\u984C\uFF0C\u4F7F\u7528 'editorUnnecessaryCode.border' \u4E3B\u984C\u8272\u5F69\u53EF\u70BA\u4E0D\u5FC5\u8981\u7684\u7A0B\u5F0F\u78BC\u52A0\u4E0A\u5E95\u7DDA\uFF0C\u800C\u4E0D\u662F\u5C07\u5176\u8B8A\u6DE1\u3002`,"\u7BC4\u570D\u9192\u76EE\u63D0\u793A\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u932F\u8AA4\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u8272\u5F69\u3002","\u8B66\u793A\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u8272\u5F69\u3002","\u8CC7\u8A0A\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u8272\u5F69\u3002"],"vs/editor/contrib/anchorSelect/anchorSelect":["\u9078\u53D6\u7BC4\u570D\u9328\u9EDE","\u8A2D\u5B9A\u9328\u9EDE\u70BA {0}:{1}","\u8A2D\u5B9A\u9078\u53D6\u7BC4\u570D\u9328\u9EDE","\u524D\u5F80\u9078\u53D6\u7BC4\u570D\u9328\u9EDE","\u9078\u53D6\u5F9E\u9328\u9EDE\u5230\u6E38\u6A19\u4E4B\u9593\u7684\u7BC4\u570D","\u53D6\u6D88\u9078\u53D6\u7BC4\u570D\u9328\u9EDE"],"vs/editor/contrib/bracketMatching/bracketMatching":["\u6210\u5C0D\u62EC\u5F27\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u8272\u5F69\u3002","\u79FB\u81F3\u65B9\u62EC\u5F27","\u9078\u53D6\u81F3\u62EC\u5F27","\u524D\u5F80\u62EC\u5F27(&&B)"],"vs/editor/contrib/caretOperations/caretOperations":["\u5C07\u6240\u9078\u6587\u5B57\u5411\u5DE6\u79FB\u52D5","\u5C07\u6240\u9078\u6587\u5B57\u5411\u53F3\u79FB\u52D5"],"vs/editor/contrib/caretOperations/transpose":["\u8ABF\u63DB\u5B57\u6BCD"],"vs/editor/contrib/clipboard/clipboard":["\u526A\u4E0B(&&T)","\u526A\u4E0B","\u526A\u4E0B","\u8907\u88FD(&&C)","\u8907\u88FD","\u8907\u88FD","\u8CBC\u4E0A(&&P)","\u8CBC\u4E0A","\u8CBC\u4E0A","\u96A8\u8A9E\u6CD5\u9192\u76EE\u63D0\u793A\u8907\u88FD"],"vs/editor/contrib/codeAction/codeActionCommands":["\u8981\u57F7\u884C\u7A0B\u5F0F\u78BC\u52D5\u4F5C\u7684\u7A2E\u985E\u3002","\u63A7\u5236\u8981\u5957\u7528\u50B3\u56DE\u52D5\u4F5C\u7684\u6642\u6A5F\u3002","\u4E00\u5F8B\u5957\u7528\u7B2C\u4E00\u500B\u50B3\u56DE\u7684\u7A0B\u5F0F\u78BC\u52D5\u4F5C\u3002","\u5982\u679C\u50B3\u56DE\u7684\u7A0B\u5F0F\u78BC\u52D5\u4F5C\u662F\u552F\u4E00\u52D5\u4F5C\uFF0C\u5247\u52A0\u4EE5\u5957\u7528\u3002","\u4E0D\u8981\u5957\u7528\u50B3\u56DE\u7684\u7A0B\u5F0F\u78BC\u52D5\u4F5C\u3002","\u63A7\u5236\u662F\u5426\u50C5\u61C9\u50B3\u56DE\u504F\u597D\u7684\u7A0B\u5F0F\u78BC\u52D5\u4F5C\u3002","\u5957\u7528\u7A0B\u5F0F\u78BC\u52D5\u4F5C\u6642\u767C\u751F\u672A\u77E5\u7684\u932F\u8AA4","\u5FEB\u901F\u4FEE\u5FA9...","\u6C92\u6709\u53EF\u7528\u7684\u7A0B\u5F0F\u78BC\u64CD\u4F5C",'\u6C92\u6709 "{0}" \u7684\u504F\u597D\u7A0B\u5F0F\u78BC\u52D5\u4F5C','\u6C92\u6709 "{0}" \u53EF\u7528\u7684\u7A0B\u5F0F\u78BC\u52D5\u4F5C',"\u6C92\u6709\u53EF\u7528\u7684\u504F\u597D\u7A0B\u5F0F\u78BC\u52D5\u4F5C","\u6C92\u6709\u53EF\u7528\u7684\u7A0B\u5F0F\u78BC\u64CD\u4F5C","\u91CD\u69CB...","\u6C92\u6709\u9069\u7528\u65BC '{0}' \u7684\u504F\u597D\u91CD\u69CB\u3002",'\u6C92\u6709\u53EF\u7528\u7684 "{0}" \u91CD\u69CB',"\u6C92\u6709\u53EF\u7528\u7684\u504F\u597D\u91CD\u69CB","\u6C92\u6709\u53EF\u7528\u7684\u91CD\u69CB","\u4F86\u6E90\u52D5\u4F5C...","\u6C92\u6709\u9069\u7528\u65BC '{0}' \u7684\u504F\u597D\u4F86\u6E90\u52D5\u4F5C",'\u6C92\u6709 "{0}" \u53EF\u7528\u7684\u4F86\u6E90\u52D5\u4F5C',"\u6C92\u6709\u53EF\u7528\u7684\u504F\u597D\u4F86\u6E90\u52D5\u4F5C","\u6C92\u6709\u53EF\u7528\u7684\u4F86\u6E90\u52D5\u4F5C","\u7D44\u7E54\u532F\u5165","\u6C92\u6709\u4EFB\u4F55\u53EF\u7528\u7684\u7D44\u7E54\u532F\u5165\u52D5\u4F5C","\u5168\u90E8\u4FEE\u6B63","\u6C92\u6709\u5168\u90E8\u4FEE\u6B63\u52D5\u4F5C\u53EF\u7528","\u81EA\u52D5\u4FEE\u6B63...","\u6C92\u6709\u53EF\u7528\u7684\u81EA\u52D5\u4FEE\u6B63"],"vs/editor/contrib/codeAction/lightBulbWidget":["\u986F\u793A\u4FEE\u6B63\u7A0B\u5F0F\u3002\u504F\u597D\u7684\u4FEE\u6B63\u7A0B\u5F0F\u53EF\u7528 ({0})","\u986F\u793A\u4FEE\u6B63 ({0})","\u986F\u793A\u4FEE\u6B63"],"vs/editor/contrib/codelens/codelensController":["\u986F\u793A\u76EE\u524D\u884C\u7684 Code Lens \u547D\u4EE4"],"vs/editor/contrib/comment/comment":["\u5207\u63DB\u884C\u8A3B\u89E3","\u5207\u63DB\u884C\u8A3B\u89E3(&&T)","\u52A0\u5165\u884C\u8A3B\u89E3","\u79FB\u9664\u884C\u8A3B\u89E3","\u5207\u63DB\u5340\u584A\u8A3B\u89E3","\u5207\u63DB\u5340\u584A\u8A3B\u89E3(&&B)"],"vs/editor/contrib/contextmenu/contextmenu":["\u986F\u793A\u7DE8\u8F2F\u5668\u5167\u5BB9\u529F\u80FD\u8868"],"vs/editor/contrib/cursorUndo/cursorUndo":["\u6E38\u6A19\u5FA9\u539F","\u6E38\u6A19\u91CD\u505A"],"vs/editor/contrib/find/findController":["\u5C0B\u627E","\u5C0B\u627E(&&F)","\u5C0B\u627E\u9078\u53D6\u9805\u76EE","\u5C0B\u627E\u4E0B\u4E00\u500B","\u5C0B\u627E\u4E0B\u4E00\u500B","\u5C0B\u627E\u4E0A\u4E00\u500B","\u5C0B\u627E\u4E0A\u4E00\u500B","\u5C0B\u627E\u4E0B\u4E00\u500B\u9078\u53D6\u9805\u76EE","\u5C0B\u627E\u4E0A\u4E00\u500B\u9078\u53D6\u9805\u76EE","\u53D6\u4EE3","\u53D6\u4EE3(&&R)"],"vs/editor/contrib/find/findWidget":["\u7DE8\u8F2F\u5668\u5C0B\u627E\u5C0F\u5DE5\u5177\u4E2D [\u5728\u9078\u53D6\u7BC4\u570D\u4E2D\u5C0B\u627E] \u7684\u5716\u793A\u3002","\u8868\u793A\u7DE8\u8F2F\u5668\u5C0B\u627E\u5C0F\u5DE5\u5177\u5DF2\u647A\u758A\u7684\u5716\u793A\u3002","\u8868\u793A\u7DE8\u8F2F\u5668\u5C0B\u627E\u5C0F\u5DE5\u5177\u5DF2\u5C55\u958B\u7684\u5716\u793A\u3002","\u7DE8\u8F2F\u5668\u5C0B\u627E\u5C0F\u5DE5\u5177\u4E2D [\u53D6\u4EE3] \u7684\u5716\u793A\u3002","\u7DE8\u8F2F\u5668\u5C0B\u627E\u5C0F\u5DE5\u5177\u4E2D [\u5168\u90E8\u53D6\u4EE3] \u7684\u5716\u793A\u3002","\u7DE8\u8F2F\u5668\u5C0B\u627E\u5C0F\u5DE5\u5177\u4E2D [\u5C0B\u627E\u4E0A\u4E00\u500B] \u7684\u5716\u793A\u3002","\u7DE8\u8F2F\u5668\u5C0B\u627E\u5C0F\u5DE5\u5177\u4E2D [\u5C0B\u627E\u4E0B\u4E00\u500B] \u7684\u5716\u793A\u3002","\u5C0B\u627E","\u5C0B\u627E","\u4E0A\u4E00\u500B\u7B26\u5408\u9805\u76EE","\u4E0B\u4E00\u500B\u7B26\u5408\u9805\u76EE","\u5728\u9078\u53D6\u7BC4\u570D\u4E2D\u5C0B\u627E","\u95DC\u9589","\u53D6\u4EE3","\u53D6\u4EE3","\u53D6\u4EE3","\u5168\u90E8\u53D6\u4EE3","\u5207\u63DB\u53D6\u4EE3\u6A21\u5F0F","\u50C5\u53CD\u767D\u986F\u793A\u524D {0} \u7B46\u7D50\u679C\uFF0C\u4F46\u6240\u6709\u5C0B\u627E\u4F5C\u696D\u6703\u5728\u5B8C\u6574\u6587\u5B57\u4E0A\u57F7\u884C\u3002","{1} \u7684 {0}","\u67E5\u7121\u7D50\u679C","\u627E\u5230 {0}","\u4EE5 '{1}' \u627E\u5230 {0}","\u4EE5 '{1}' \u627E\u5230 {0}\uFF0C\u4F4D\u65BC {2}","\u5DF2\u4EE5 '{1}' \u627E\u5230 {0}","Ctrl+Enter \u73FE\u5728\u6703\u63D2\u5165\u5206\u884C\u7B26\u865F\uFF0C\u800C\u4E0D\u6703\u5168\u90E8\u53D6\u4EE3\u3002\u60A8\u53EF\u4EE5\u4FEE\u6539 editor.action.replaceAll \u7684\u6309\u9375\u7E6B\u7D50\u95DC\u4FC2\uFF0C\u4EE5\u8986\u5BEB\u6B64\u884C\u70BA\u3002"],"vs/editor/contrib/folding/folding":["\u5C55\u958B","\u4EE5\u905E\u8FF4\u65B9\u5F0F\u5C55\u958B","\u647A\u758A","\u5207\u63DB\u647A\u758A","\u4EE5\u905E\u8FF4\u65B9\u5F0F\u647A\u758A","\u647A\u758A\u5168\u90E8\u5340\u584A\u8A3B\u89E3","\u647A\u758A\u6240\u6709\u5340\u57DF","\u5C55\u958B\u6240\u6709\u5340\u57DF","\u5168\u90E8\u647A\u758A","\u5168\u90E8\u5C55\u958B","\u647A\u758A\u5C64\u7D1A {0}","\u5DF2\u647A\u758A\u7BC4\u570D\u5F8C\u7684\u80CC\u666F\u8272\u5F69\u3002\u8272\u5F69\u4E0D\u5F97\u8655\u65BC\u4E0D\u900F\u660E\u72C0\u614B\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u7DE8\u8F2F\u5668\u88DD\u8A02\u908A\u7684\u647A\u758A\u63A7\u5236\u9805\u8272\u5F69\u3002"],"vs/editor/contrib/folding/foldingDecorations":["\u7DE8\u8F2F\u5668\u5B57\u7B26\u908A\u754C\u4E2D [\u5C55\u958B\u7684\u7BC4\u570D] \u7684\u5716\u793A\u3002","\u7DE8\u8F2F\u5668\u5B57\u7B26\u908A\u754C\u4E2D [\u647A\u758A\u7684\u7BC4\u570D] \u7684\u5716\u793A\u3002"],"vs/editor/contrib/fontZoom/fontZoom":["\u7DE8\u8F2F\u5668\u5B57\u9AD4\u653E\u5927","\u7DE8\u8F2F\u5668\u5B57\u578B\u7E2E\u5C0F","\u7DE8\u8F2F\u5668\u5B57\u9AD4\u91CD\u8A2D\u7E2E\u653E"],"vs/editor/contrib/format/format":["\u5728\u884C {0} \u7DE8\u8F2F\u4E86 1 \u9805\u683C\u5F0F","\u5728\u884C {1} \u7DE8\u8F2F\u4E86 {0} \u9805\u683C\u5F0F","\u5728\u884C {0} \u8207\u884C {1} \u4E4B\u9593\u7DE8\u8F2F\u4E86 1 \u9805\u683C\u5F0F","\u5728\u884C {1} \u8207\u884C {2} \u4E4B\u9593\u7DE8\u8F2F\u4E86 {0} \u9805\u683C\u5F0F"],"vs/editor/contrib/format/formatActions":["\u683C\u5F0F\u5316\u6587\u4EF6","\u683C\u5F0F\u5316\u9078\u53D6\u7BC4\u570D"],"vs/editor/contrib/gotoError/gotoError":["\u79FB\u81F3\u4E0B\u4E00\u500B\u554F\u984C (\u932F\u8AA4, \u8B66\u544A, \u8CC7\u8A0A)","[\u524D\u5F80\u4E0B\u4E00\u500B\u6A19\u8A18] \u7684\u5716\u793A\u3002","\u79FB\u81F3\u4E0A\u4E00\u500B\u554F\u984C (\u932F\u8AA4, \u8B66\u544A, \u8CC7\u8A0A)","[\u524D\u5F80\u4E0A\u4E00\u500B\u6A19\u8A18] \u7684\u5716\u793A\u3002","\u79FB\u81F3\u6A94\u6848\u88E1\u9762\u7684\u4E0B\u4E00\u500B\u554F\u984C (\u932F\u8AA4, \u8B66\u544A, \u8CC7\u8A0A)","\u4E0B\u4E00\u500B\u554F\u984C(&&P)","\u79FB\u81F3\u6A94\u6848\u88E1\u9762\u7684\u4E0A\u4E00\u500B\u554F\u984C (\u932F\u8AA4, \u8B66\u544A, \u8CC7\u8A0A)","\u524D\u4E00\u500B\u554F\u984C(&&P)"],"vs/editor/contrib/gotoError/gotoErrorWidget":["\u932F\u8AA4","\u8B66\u544A","\u8CC7\u8A0A","\u63D0\u793A","{0} \u65BC {1}\u3002","{0} \u500B\u554F\u984C (\u5171 {1} \u500B)","{0} \u500B\u554F\u984C (\u5171 {1} \u500B)","\u7DE8\u8F2F\u5668\u6A19\u8A18\u5C0E\u89BD\u5C0F\u5DE5\u5177\u932F\u8AA4\u7684\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u6A19\u8A18\u5C0E\u89BD\u5C0F\u5DE5\u5177\u8B66\u544A\u7684\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u6A19\u8A18\u5C0E\u89BD\u5C0F\u5DE5\u5177\u8CC7\u8A0A\u7684\u8272\u5F69","\u7DE8\u8F2F\u5668\u6A19\u8A18\u5C0E\u89BD\u5C0F\u5DE5\u5177\u7684\u80CC\u666F\u3002"],"vs/editor/contrib/gotoSymbol/goToCommands":["\u67E5\u770B","\u5B9A\u7FA9","\u627E\u4E0D\u5230 '{0}' \u7684\u5B9A\u7FA9","\u627E\u4E0D\u5230\u4EFB\u4F55\u5B9A\u7FA9","\u79FB\u81F3\u5B9A\u7FA9","\u79FB\u81F3\u5B9A\u7FA9(&&D)","\u5728\u4E00\u5074\u958B\u555F\u5B9A\u7FA9","\u7784\u6838\u5B9A\u7FA9","\u5BA3\u544A","\u627E\u4E0D\u5230 '{0}' \u7684\u5BA3\u544A ","\u627E\u4E0D\u5230\u4EFB\u4F55\u5BA3\u544A","\u79FB\u81F3\u5BA3\u544A","\u524D\u5F80\u5BA3\u544A(&&D)","\u627E\u4E0D\u5230 '{0}' \u7684\u5BA3\u544A ","\u627E\u4E0D\u5230\u4EFB\u4F55\u5BA3\u544A","\u9810\u89BD\u5BA3\u544A","\u985E\u578B\u5B9A\u7FA9","\u627E\u4E0D\u5230 '{0}' \u7684\u4EFB\u4F55\u985E\u578B\u5B9A\u7FA9","\u627E\u4E0D\u5230\u4EFB\u4F55\u985E\u578B\u5B9A\u7FA9","\u79FB\u81F3\u985E\u578B\u5B9A\u7FA9","\u524D\u5F80\u985E\u578B\u5B9A\u7FA9(&&T)","\u9810\u89BD\u985E\u578B\u5B9A\u7FA9","\u5BE6\u4F5C","\u627E\u4E0D\u5230 '{0}' \u7684\u4EFB\u4F55\u5BE6\u4F5C","\u627E\u4E0D\u5230\u4EFB\u4F55\u5BE6\u4F5C","\u524D\u5F80\u5BE6\u4F5C","\u524D\u5F80\u5BE6\u4F5C(&&I)","\u67E5\u770B\u5BE6\u4F5C",'\u672A\u627E\u5230 "{0}" \u7684\u53C3\u8003',"\u672A\u627E\u5230\u53C3\u8003","\u524D\u5F80\u53C3\u8003","\u524D\u5F80\u53C3\u8003(&&R)","\u53C3\u8003","\u9810\u89BD\u53C3\u8003","\u53C3\u8003","\u79FB\u81F3\u4EFB\u4F55\u7B26\u865F","\u4F4D\u7F6E","'{0}' \u6C92\u6709\u7D50\u679C","\u53C3\u8003"],"vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition":["\u6309\u4E00\u4E0B\u4EE5\u986F\u793A {0} \u9805\u5B9A\u7FA9\u3002"],"vs/editor/contrib/gotoSymbol/peek/referencesController":["\u6B63\u5728\u8F09\u5165...","{0} ({1})"],"vs/editor/contrib/gotoSymbol/peek/referencesTree":["{0} \u500B\u53C3\u8003","{0} \u500B\u53C3\u8003","\u53C3\u8003"],"vs/editor/contrib/gotoSymbol/peek/referencesWidget":["\u7121\u6CD5\u9810\u89BD","\u67E5\u7121\u7D50\u679C","\u53C3\u8003"],"vs/editor/contrib/gotoSymbol/referencesModel":["\u500B\u7B26\u865F\u4F4D\u65BC {0} \u4E2D\u7684\u7B2C {1} \u884C\u7B2C {2} \u6B04","\u7B26\u865F\u4F4D\u65BC {0} \u4E2D\u7684\u7B2C {1} \u884C\u7B2C {2}\u3001{3} \u6B04","1 \u500B\u7B26\u865F\u4F4D\u65BC {0}, \u5B8C\u6574\u8DEF\u5F91 {1}","{0} \u500B\u7B26\u865F\u4F4D\u65BC {1}, \u5B8C\u6574\u8DEF\u5F91 {2}","\u627E\u4E0D\u5230\u7D50\u679C","\u5728 {0} \u4E2D\u627E\u5230 1 \u500B\u7B26\u865F","\u5728 {1} \u4E2D\u627E\u5230 {0} \u500B\u7B26\u865F","\u5728 {1} \u500B\u6A94\u6848\u4E2D\u627E\u5230 {0} \u500B\u7B26\u865F"],"vs/editor/contrib/gotoSymbol/symbolNavigation":["{1} \u7684\u7B26\u865F {0}\uFF0C{2} \u70BA\u4E0B\u4E00\u500B","{1} \u7684\u7B26\u865F {0}"],"vs/editor/contrib/hover/hover":["\u52D5\u614B\u986F\u793A","\u986F\u793A\u5B9A\u7FA9\u9810\u89BD\u61F8\u505C"],"vs/editor/contrib/hover/markdownHoverParticipant":["\u6B63\u5728\u8F09\u5165..."],"vs/editor/contrib/hover/markerHoverParticipant":["View Problem","\u6C92\u6709\u53EF\u7528\u7684\u5FEB\u901F\u4FEE\u6B63","\u6B63\u5728\u6AA2\u67E5\u5FEB\u901F\u4FEE\u6B63...","\u6C92\u6709\u53EF\u7528\u7684\u5FEB\u901F\u4FEE\u6B63","\u5FEB\u901F\u4FEE\u5FA9..."],"vs/editor/contrib/inPlaceReplace/inPlaceReplace":["\u4EE5\u4E0A\u4E00\u500B\u503C\u53D6\u4EE3","\u4EE5\u4E0B\u4E00\u500B\u503C\u53D6\u4EE3"],"vs/editor/contrib/indentation/indentation":["\u5C07\u7E2E\u6392\u8F49\u63DB\u6210\u7A7A\u683C","\u5C07\u7E2E\u6392\u8F49\u63DB\u6210\u5B9A\u4F4D\u9EDE","\u5DF2\u8A2D\u5B9A\u7684\u5B9A\u4F4D\u9EDE\u5927\u5C0F","\u9078\u53D6\u76EE\u524D\u6A94\u6848\u7684\u5B9A\u4F4D\u9EDE\u5927\u5C0F","\u4F7F\u7528 Tab \u9032\u884C\u7E2E\u6392","\u4F7F\u7528\u7A7A\u683C\u9375\u9032\u884C\u7E2E\u6392","\u5075\u6E2C\u5167\u5BB9\u4E2D\u7684\u7E2E\u6392","\u91CD\u65B0\u5C07\u884C\u7E2E\u6392","\u91CD\u65B0\u5C07\u9078\u53D6\u7684\u884C\u7E2E\u6392"],"vs/editor/contrib/linesOperations/linesOperations":["\u5C07\u884C\u5411\u4E0A\u8907\u88FD","\u5C07\u884C\u5411\u4E0A\u8907\u88FD(&&C)","\u5C07\u884C\u5411\u4E0B\u8907\u88FD","\u5C07\u884C\u5411\u4E0B\u8907\u88FD(&&P)","\u91CD\u8907\u9078\u53D6\u9805\u76EE","\u91CD\u8907\u9078\u53D6\u9805\u76EE(&&D)","\u4E0A\u79FB\u4E00\u884C","\u4E0A\u79FB\u4E00\u884C(&&V)","\u4E0B\u79FB\u4E00\u884C","\u4E0B\u79FB\u4E00\u884C(&&L)","\u905E\u589E\u6392\u5E8F\u884C","\u905E\u6E1B\u6392\u5E8F\u884C","\u4FEE\u526A\u5C3E\u7AEF\u7A7A\u767D","\u522A\u9664\u884C","\u7E2E\u6392\u884C","\u51F8\u6392\u884C","\u5728\u4E0A\u65B9\u63D2\u5165\u884C","\u5728\u4E0B\u65B9\u63D2\u5165\u884C","\u5DE6\u908A\u5168\u90E8\u522A\u9664","\u522A\u9664\u6240\u6709\u53F3\u65B9\u9805\u76EE","\u9023\u63A5\u7DDA","\u8F49\u7F6E\u6E38\u6A19\u5468\u570D\u7684\u5B57\u5143\u6578","\u8F49\u63DB\u5230\u5927\u5BEB","\u8F49\u63DB\u5230\u5C0F\u5BEB","\u8F49\u63DB\u70BA\u5B57\u9996\u5927\u5BEB","\u8F49\u63DB\u70BA\u5E95\u7DDA\u9023\u63A5\u5B57"],"vs/editor/contrib/linkedEditing/linkedEditing":["\u958B\u59CB\u9023\u7D50\u7684\u7DE8\u8F2F","\u7576\u7DE8\u8F2F\u5668\u81EA\u52D5\u91CD\u65B0\u547D\u540D\u985E\u578B\u6642\u7684\u80CC\u666F\u8272\u5F69\u3002"],"vs/editor/contrib/links/links":["\u57F7\u884C\u547D\u4EE4","\u8FFD\u8E64\u9023\u7D50","cmd + \u6309\u4E00\u4E0B","ctrl + \u6309\u4E00\u4E0B","\u9078\u9805 + \u6309\u4E00\u4E0B","alt + \u6309\u4E00\u4E0B","\u57F7\u884C\u547D\u4EE4 {0}","\u56E0\u70BA\u6B64\u9023\u7D50\u7684\u683C\u5F0F\u4E0D\u6B63\u78BA\uFF0C\u6240\u4EE5\u7121\u6CD5\u958B\u555F: {0}","\u56E0\u70BA\u6B64\u9023\u7D50\u76EE\u6A19\u907A\u5931\uFF0C\u6240\u4EE5\u7121\u6CD5\u958B\u555F\u3002","\u958B\u555F\u9023\u7D50"],"vs/editor/contrib/message/messageController":["\u7DE8\u8F2F\u5668\u76EE\u524D\u662F\u5426\u6B63\u5728\u986F\u793A\u5167\u5D4C\u8A0A\u606F","\u7121\u6CD5\u5728\u552F\u8B80\u7DE8\u8F2F\u5668\u4E2D\u7DE8\u8F2F"],"vs/editor/contrib/multicursor/multicursor":["\u5728\u4E0A\u65B9\u52A0\u5165\u6E38\u6A19","\u5728\u4E0A\u65B9\u65B0\u589E\u6E38\u6A19(&&A)","\u5728\u4E0B\u65B9\u52A0\u5165\u6E38\u6A19","\u5728\u4E0B\u65B9\u65B0\u589E\u6E38\u6A19(&&D)","\u5728\u884C\u5C3E\u65B0\u589E\u6E38\u6A19","\u5728\u884C\u5C3E\u65B0\u589E\u6E38\u6A19(&&U)","\u5C07\u6E38\u6A19\u65B0\u589E\u5230\u5E95\u90E8 ","\u5C07\u6E38\u6A19\u65B0\u589E\u5230\u9802\u90E8","\u5C07\u9078\u53D6\u9805\u76EE\u52A0\u5165\u4E0B\u4E00\u500B\u627E\u5230\u7684\u76F8\u7B26\u9805","\u65B0\u589E\u4E0B\u4E00\u500B\u9805\u76EE(&&N)","\u5C07\u9078\u53D6\u9805\u76EE\u52A0\u5165\u524D\u4E00\u500B\u627E\u5230\u7684\u76F8\u7B26\u9805\u4E2D","\u65B0\u589E\u4E0A\u4E00\u500B\u9805\u76EE(&&R)","\u5C07\u6700\u5F8C\u4E00\u500B\u9078\u64C7\u9805\u76EE\u79FB\u81F3\u4E0B\u4E00\u500B\u627E\u5230\u7684\u76F8\u7B26\u9805","\u5C07\u6700\u5F8C\u4E00\u500B\u9078\u64C7\u9805\u76EE\u79FB\u81F3\u524D\u4E00\u500B\u627E\u5230\u7684\u76F8\u7B26\u9805","\u9078\u53D6\u6240\u6709\u627E\u5230\u7684\u76F8\u7B26\u9805\u76EE","\u9078\u53D6\u6240\u6709\u9805\u76EE(&&O)","\u8B8A\u66F4\u6240\u6709\u767C\u751F\u6B21\u6578"],"vs/editor/contrib/parameterHints/parameterHints":["\u89F8\u767C\u53C3\u6578\u63D0\u793A"],"vs/editor/contrib/parameterHints/parameterHintsWidget":["[\u986F\u793A\u4E0B\u4E00\u500B\u53C3\u6578\u63D0\u793A] \u7684\u5716\u793A\u3002","[\u986F\u793A\u4E0A\u4E00\u500B\u53C3\u6578\u63D0\u793A] \u7684\u5716\u793A\u3002","{0}\uFF0C\u63D0\u793A"],"vs/editor/contrib/peekView/peekView":["\u95DC\u9589","\u9810\u89BD\u6AA2\u8996\u6A19\u984C\u5340\u57DF\u7684\u80CC\u666F\u8272\u5F69\u3002","\u9810\u89BD\u6AA2\u8996\u6A19\u984C\u7684\u8272\u5F69\u3002","\u9810\u89BD\u6AA2\u8996\u6A19\u984C\u8CC7\u8A0A\u7684\u8272\u5F69\u3002","\u9810\u89BD\u6AA2\u8996\u4E4B\u6846\u7DDA\u8207\u7BAD\u982D\u7684\u8272\u5F69\u3002","\u9810\u89BD\u6AA2\u8996\u4E2D\u7D50\u679C\u6E05\u55AE\u7684\u80CC\u666F\u8272\u5F69\u3002","\u9810\u89BD\u6AA2\u8996\u7D50\u679C\u5217\u8868\u4E2D\u884C\u7BC0\u9EDE\u7684\u524D\u666F\u8272\u5F69","\u9810\u89BD\u6AA2\u8996\u7D50\u679C\u5217\u8868\u4E2D\u6A94\u6848\u7BC0\u9EDE\u7684\u524D\u666F\u8272\u5F69","\u5728\u9810\u89BD\u6AA2\u8996\u4E4B\u7D50\u679C\u6E05\u55AE\u4E2D\u9078\u53D6\u9805\u76EE\u6642\u7684\u80CC\u666F\u8272\u5F69\u3002","\u5728\u9810\u89BD\u6AA2\u8996\u4E4B\u7D50\u679C\u6E05\u55AE\u4E2D\u9078\u53D6\u9805\u76EE\u6642\u7684\u524D\u666F\u8272\u5F69\u3002","\u9810\u89BD\u6AA2\u8996\u7DE8\u8F2F\u5668\u7684\u80CC\u666F\u8272\u5F69\u3002","\u9810\u89BD\u6AA2\u8996\u7DE8\u8F2F\u5668\u908A\u6846(\u542B\u884C\u865F\u6216\u5B57\u5F62\u5716\u793A)\u7684\u80CC\u666F\u8272\u5F69\u3002","\u5728\u9810\u89BD\u6AA2\u8996\u7DE8\u8F2F\u5668\u4E2D\u6BD4\u5C0D\u6642\u7684\u53CD\u767D\u986F\u793A\u8272\u5F69\u3002","\u9810\u89BD\u6AA2\u8996\u7DE8\u8F2F\u5668\u4E2D\u6BD4\u5C0D\u6642\u7684\u53CD\u767D\u986F\u793A\u8272\u5F69\u3002","\u5728\u9810\u89BD\u6AA2\u8996\u7DE8\u8F2F\u5668\u4E2D\u6BD4\u5C0D\u6642\u7684\u53CD\u767D\u986F\u793A\u908A\u754C\u3002"],"vs/editor/contrib/quickAccess/gotoLineQuickAccess":["\u5148\u958B\u555F\u6587\u5B57\u7DE8\u8F2F\u5668\uFF0C\u524D\u5F80\u67D0\u4E00\u884C\u3002","\u524D\u5F80\u7B2C {0} \u884C\u548C\u7B2C {1} \u6B04\u3002","\u524D\u5F80\u7B2C {0} \u884C\u3002","\u76EE\u524D\u884C: {0}\uFF0C\u5B57\u5143: {1}\u3002\u8ACB\u9375\u5165\u4ECB\u65BC 1 \u5230 {2} \u4E4B\u9593\u884C\u865F\uFF0C\u5C0E\u89BD\u81F3\u8A72\u884C\u3002","\u76EE\u524D\u884C: {0}\uFF0C\u5B57\u5143: {1}\u3002\u8ACB\u9375\u5165\u8981\u5C0E\u89BD\u81F3\u7684\u884C\u865F\u3002"],"vs/editor/contrib/quickAccess/gotoSymbolQuickAccess":["\u82E5\u8981\u524D\u5F80\u7B26\u865F\uFF0C\u8ACB\u5148\u958B\u555F\u5305\u542B\u7B26\u865F\u8CC7\u8A0A\u7684\u6587\u5B57\u7DE8\u8F2F\u5668\u3002","\u4F7F\u7528\u4E2D\u7684\u6587\u5B57\u7DE8\u8F2F\u5668\u4E0D\u63D0\u4F9B\u7B26\u865F\u8CC7\u8A0A\u3002","\u6C92\u6709\u76F8\u7B26\u7684\u7DE8\u8F2F\u5668\u7B26\u865F","\u6C92\u6709\u7DE8\u8F2F\u5668\u7B26\u865F","\u958B\u81F3\u5074\u908A","\u958B\u555F\u5230\u5E95\u90E8","\u7B26\u865F ({0})","\u5C6C\u6027 ({0})","\u65B9\u6CD5 ({0})","\u51FD\u5F0F ({0})","\u5EFA\u69CB\u51FD\u5F0F ({0})","\u8B8A\u6578 ({0})","\u985E\u5225 ({0})","\u7D50\u69CB ({0})","\u4E8B\u4EF6 ({0})","\u904B\u7B97\u5B50 ({0})","\u4ECB\u9762 ({0})","\u547D\u540D\u7A7A\u9593 ({0})","\u5957\u4EF6 ({0})","\u578B\u5225\u53C3\u6578 ({0})","\u6A21\u7D44 ({0})","\u5C6C\u6027 ({0})","\u5217\u8209 ({0})","\u5217\u8209\u6210\u54E1 ({0})","\u5B57\u4E32 ({0})","\u6A94\u6848 ({0})","\u9663\u5217 ({0})","\u6578\u5B57 ({0})","\u5E03\u6797\u503C ({0})","\u7269\u4EF6 ({0})","\u7D22\u5F15\u9375 ({0})","\u6B04\u4F4D ({0})","\u5E38\u6578 ({0})"],"vs/editor/contrib/rename/rename":["\u6C92\u6709\u7D50\u679C\u3002","\u89E3\u6790\u91CD\u65B0\u547D\u540D\u4F4D\u7F6E\u6642\u767C\u751F\u672A\u77E5\u7684\u932F\u8AA4","\u6B63\u5728\u70BA '{0}' \u91CD\u65B0\u547D\u540D","\u6B63\u5728\u91CD\u65B0\u547D\u540D {0}","\u5DF2\u6210\u529F\u5C07 '{0}' \u91CD\u65B0\u547D\u540D\u70BA '{1}'\u3002\u6458\u8981: {2}","\u91CD\u547D\u540D\u7121\u6CD5\u5957\u7528\u7DE8\u8F2F","\u91CD\u65B0\u547D\u540D\u7121\u6CD5\u8A08\u7B97\u7DE8\u8F2F","\u91CD\u65B0\u547D\u540D\u7B26\u865F","\u555F\u7528/\u505C\u7528\u91CD\u65B0\u547D\u540D\u524D\u5148\u9810\u89BD\u8B8A\u66F4\u7684\u529F\u80FD"],"vs/editor/contrib/rename/renameInputField":["\u70BA\u8F38\u5165\u91CD\u65B0\u547D\u540D\u3002\u8ACB\u9375\u5165\u65B0\u540D\u7A31\uFF0C\u7136\u5F8C\u6309 Enter \u4EE5\u8A8D\u53EF\u3002","\u6309 {0} \u9032\u884C\u91CD\u65B0\u547D\u540D\uFF0C\u6309 {1} \u9032\u884C\u9810\u89BD"],"vs/editor/contrib/smartSelect/smartSelect":["\u5C55\u958B\u9078\u53D6\u9805\u76EE","\u5C55\u958B\u9078\u53D6\u7BC4\u570D(&&E)","\u7E2E\u5C0F\u9078\u53D6\u9805\u76EE","\u58D3\u7E2E\u9078\u53D6\u7BC4\u570D(&&S)"],"vs/editor/contrib/snippet/snippetVariables":["\u661F\u671F\u5929","\u661F\u671F\u4E00","\u661F\u671F\u4E8C","\u661F\u671F\u4E09","\u661F\u671F\u56DB","\u661F\u671F\u4E94","\u661F\u671F\u516D","\u9031\u65E5","\u9031\u4E00","\u9031\u4E8C","\u9031\u4E09","\u9031\u56DB","\u9031\u4E94","\u9031\u516D","\u4E00\u6708","\u4E8C\u6708","\u4E09\u6708","\u56DB\u6708","\u4E94\u6708","\u516D\u6708","\u4E03\u6708","\u516B\u6708","\u4E5D\u6708","\u5341\u6708","\u5341\u4E00\u6708","\u5341\u4E8C\u6708","1\u6708","2\u6708","3 \u6708","4\u6708","\u4E94\u6708","6\u6708","7 \u6708","8 \u6708","9 \u6708","10 \u6708","11 \u6708","12 \u6708"],"vs/editor/contrib/suggest/suggestController":["\u63A5\u53D7 \u2018{0}\u2019 \u9032\u884C\u4E86\u5176\u4ED6 {1} \u9805\u7DE8\u8F2F","\u89F8\u767C\u5EFA\u8B70","\u63D2\u5165","\u63D2\u5165","\u53D6\u4EE3","\u53D6\u4EE3","\u63D2\u5165","\u986F\u793A\u66F4\u5C11","\u986F\u793A\u66F4\u591A","\u91CD\u8A2D\u5EFA\u8B70\u5C0F\u5DE5\u5177\u5927\u5C0F"],"vs/editor/contrib/suggest/suggestWidget":["\u5EFA\u8B70\u5C0F\u5DE5\u5177\u7684\u80CC\u666F\u8272\u5F69\u3002","\u5EFA\u8B70\u5C0F\u5DE5\u5177\u7684\u908A\u754C\u8272\u5F69\u3002","\u5EFA\u8B70\u5C0F\u5DE5\u5177\u7684\u524D\u666F\u8272\u5F69\u3002","\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u6240\u9078\u9805\u76EE\u7684\u80CC\u666F\u8272\u5F69\u3002","\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u76F8\u7B26\u9192\u76EE\u63D0\u793A\u7684\u8272\u5F69\u3002","\u6B63\u5728\u8F09\u5165...","\u7121\u5EFA\u8B70\u3002","{0}\uFF0C\u6587\u4EF6: {1}","\u5EFA\u8B70"],"vs/editor/contrib/suggest/suggestWidgetDetails":["\u95DC\u9589","\u6B63\u5728\u8F09\u5165..."],"vs/editor/contrib/suggest/suggestWidgetRenderer":["\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D [\u66F4\u591A\u8A73\u7D30\u8CC7\u8A0A] \u7684\u5716\u793A\u3002","\u95B1\u8B80\u66F4\u591A"],"vs/editor/contrib/suggest/suggestWidgetStatus":["{0} ({1})"],"vs/editor/contrib/symbolIcons/symbolIcons":["\u9663\u5217\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u5E03\u6797\u503C\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u985E\u5225\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u8272\u5F69\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u5E38\u6578\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u5EFA\u69CB\u51FD\u5F0F\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u5217\u8209\u503C\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u5217\u8209\u503C\u6210\u54E1\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u4E8B\u4EF6\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u6B04\u4F4D\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u6A94\u6848\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u8CC7\u6599\u593E\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u51FD\u5F0F\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u4ECB\u9762\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u7D22\u5F15\u9375\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u95DC\u9375\u5B57\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u65B9\u6CD5\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u6A21\u7D44\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u547D\u540D\u7A7A\u9593\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","Null \u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u6578\u5B57\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u7269\u4EF6\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u904B\u7B97\u5B50\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u5957\u4EF6\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u5C6C\u6027\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u53C3\u8003\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u7A0B\u5F0F\u78BC\u7247\u6BB5\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u5B57\u4E32\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u7D50\u69CB\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u6587\u5B57\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u578B\u5225\u53C3\u6578\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u55AE\u4F4D\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002","\u8B8A\u6578\u7B26\u865F\u7684\u524D\u666F\u8272\u5F69\u3002\u9019\u4E9B\u7B26\u865F\u6703\u51FA\u73FE\u5728\u5927\u7DB1\u3001\u968E\u5C64\u9023\u7D50\u548C\u5EFA\u8B70\u5C0F\u5DE5\u5177\u4E2D\u3002"],"vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode":["\u5207\u63DB TAB \u9375\u79FB\u52D5\u7126\u9EDE","\u6309 Tab \u73FE\u5728\u6703\u5C07\u7126\u9EDE\u79FB\u81F3\u4E0B\u4E00\u500B\u53EF\u8A2D\u5B9A\u7126\u9EDE\u7684\u5143\u7D20\u3002","\u6309 Tab \u73FE\u5728\u6703\u63D2\u5165\u5B9A\u4F4D\u5B57\u5143\u3002"],"vs/editor/contrib/tokenization/tokenization":["\u958B\u767C\u4EBA\u54E1: \u5F37\u5236\u91CD\u65B0\u7F6E\u653E"],"vs/editor/contrib/unusualLineTerminators/unusualLineTerminators":["\u7570\u5E38\u7684\u884C\u7D50\u675F\u5B57\u5143","\u5075\u6E2C\u5230\u7570\u5E38\u7684\u884C\u7D50\u675F\u5B57\u5143","\u6B64\u6A94\u6848\u5305\u542B\u4E00\u6216\u591A\u500B\u7570\u5E38\u7684\u884C\u7D50\u675F\u5B57\u5143\uFF0C\u4F8B\u5982\u884C\u5206\u9694\u7B26\u865F (LS) \u6216\u6BB5\u843D\u5206\u9694\u7B26\u865F (PS)\u3002\r\n\r\n\u5EFA\u8B70\u60A8\u5C07\u5176\u5F9E\u6A94\u6848\u4E2D\u79FB\u9664\u3002\u9019\u53EF\u4EE5\u900F\u904E `editor.unusualLineTerminators` \u9032\u884C\u8A2D\u5B9A\u3002","\u4FEE\u6B63\u6B64\u6A94\u6848","\u5FFD\u7565\u6B64\u6A94\u6848\u7684\u554F\u984C"],"vs/editor/contrib/wordHighlighter/wordHighlighter":["\u8B80\u53D6\u6B0A\u9650\u671F\u9593 (\u5982\u8B80\u53D6\u8B8A\u6578) \u7B26\u865F\u7684\u80CC\u666F\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u5BEB\u5165\u6B0A\u9650\u671F\u9593 (\u5982\u5BEB\u5165\u8B8A\u6578) \u7B26\u865F\u7684\u80CC\u666F\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u8B80\u53D6\u5B58\u53D6\u671F\u9593 (\u4F8B\u5982\u8B80\u53D6\u8B8A\u6578\u6642) \u7B26\u865F\u7684\u908A\u6846\u984F\u8272\u3002","\u5BEB\u5165\u5B58\u53D6\u671F\u9593 (\u4F8B\u5982\u5BEB\u5165\u8B8A\u6578\u6642) \u7B26\u865F\u7684\u908A\u6846\u984F\u8272\u3002 ","\u7B26\u865F\u9192\u76EE\u63D0\u793A\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u5BEB\u5165\u6B0A\u9650\u7B26\u865F\u9192\u76EE\u63D0\u793A\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u79FB\u81F3\u4E0B\u4E00\u500B\u53CD\u767D\u7B26\u865F","\u79FB\u81F3\u4E0A\u4E00\u500B\u53CD\u767D\u7B26\u865F","\u89F8\u767C\u7B26\u865F\u53CD\u767D\u986F\u793A"],"vs/editor/contrib/wordOperations/wordOperations":["\u522A\u9664\u5B57\u7D44"],"vs/platform/actions/browser/menuEntryActionViewItem":["{0} ({1})"],"vs/platform/configuration/common/configurationRegistry":["\u9810\u8A2D\u8A9E\u8A00\u7D44\u614B\u8986\u5BEB","\u8A2D\u5B9A\u8981\u91DD\u5C0D\u8A9E\u8A00\u8986\u5BEB\u7684\u7DE8\u8F2F\u5668\u8A2D\u5B9A\u3002","\u9019\u500B\u8A2D\u5B9A\u4E0D\u652F\u63F4\u4EE5\u8A9E\u8A00\u70BA\u6839\u64DA\u7684\u7D44\u614B\u3002","\u7121\u6CD5\u8A3B\u518A\u7A7A\u767D\u5C6C\u6027","\u7121\u6CD5\u8A3B\u518A '{0}'\u3002\u9019\u7B26\u5408\u7528\u65BC\u63CF\u8FF0\u8A9E\u8A00\u5C08\u7528\u7DE8\u8F2F\u5668\u8A2D\u5B9A\u7684\u5C6C\u6027\u6A21\u5F0F '\\\\[.*\\\\]$'\u3002\u8ACB\u4F7F\u7528 'configurationDefaults' \u8CA2\u737B\u3002","\u7121\u6CD5\u8A3B\u518A '{0}'\u3002\u6B64\u5C6C\u6027\u5DF2\u7D93\u8A3B\u518A\u3002"],"vs/platform/contextkey/browser/contextKeyService":["\u50B3\u56DE\u6709\u95DC\u5167\u5BB9\u7D22\u5F15\u9375\u8CC7\u8A0A\u7684\u547D\u4EE4"],"vs/platform/contextkey/common/contextkeys":["Whether the operating system is Windows"],"vs/platform/keybinding/common/abstractKeybindingService":["\u5DF2\u6309\u4E0B ({0})\u3002\u7B49\u5F85\u7B2C\u4E8C\u500B\u5957\u7D22\u9375...","\u6309\u9375\u7D44\u5408 ({0}, {1}) \u4E0D\u662F\u547D\u4EE4\u3002"],"vs/platform/list/browser/listService":["\u5DE5\u4F5C\u53F0","\u5C0D\u61C9Windows\u548CLinux\u7684'Control'\u8207\u5C0D\u61C9 macOS \u7684'Command'\u3002","\u5C0D\u61C9Windows\u548CLinux\u7684'Alt'\u8207\u5C0D\u61C9macOS\u7684'Option'\u3002","\u900F\u904E\u6ED1\u9F20\u591A\u9078\uFF0C\u7528\u65BC\u5728\u6A39\u72C0\u76EE\u9304\u8207\u6E05\u55AE\u4E2D\u65B0\u589E\u9805\u76EE\u7684\u8F14\u52A9\u6309\u9375 (\u4F8B\u5982\u5728\u7E3D\u7BA1\u4E2D\u958B\u555F\u7DE8\u8F2F\u5668 \u53CA SCM \u6AA2\u8996)\u3002'\u5728\u5074\u908A\u958B\u555F' \u6ED1\u9F20\u624B\u52E2 (\u82E5\u652F\u63F4) \u5C07\u6703\u9069\u61C9\u4EE5\u907F\u514D\u548C\u591A\u9078\u8F14\u52A9\u6309\u9375\u885D\u7A81\u3002","\u63A7\u5236\u5982\u4F55\u4F7F\u7528\u6ED1\u9F20 (\u5982\u652F\u63F4\u6B64\u7528\u6CD5) \u958B\u555F\u6A39\u72C0\u76EE\u9304\u8207\u6E05\u55AE\u4E2D\u7684\u9805\u76EE\u3002\u82E5\u4E0D\u9069\u7528\uFF0C\u67D0\u4E9B\u6A39\u72C0\u76EE\u9304\u8207\u6E05\u55AE\u53EF\u80FD\u6703\u9078\u64C7\u5FFD\u7565\u6B64\u8A2D\u5B9A\u3002","\u63A7\u5236\u5728\u5DE5\u4F5C\u53F0\u4E2D\uFF0C\u6E05\u55AE\u8207\u6A39\u72C0\u7D50\u69CB\u662F\u5426\u652F\u63F4\u6C34\u5E73\u6372\u52D5\u3002\u8B66\u544A: \u958B\u555F\u6B64\u8A2D\u5B9A\u5C07\u6703\u5F71\u97FF\u6548\u80FD\u3002","\u63A7\u5236\u6A39\u72C0\u7D50\u69CB\u7E2E\u6392 (\u50CF\u7D20)\u3002","\u63A7\u5236\u6A39\u7CFB\u662F\u5426\u61C9\u8F49\u8B6F\u7E2E\u6392\u8F14\u52A9\u7DDA\u3002","\u63A7\u5236\u6E05\u55AE\u548C\u6A39\u72C0\u7D50\u69CB\u662F\u5426\u5177\u6709\u5E73\u6ED1\u6372\u52D5\u3002","\u6BD4\u5C0D\u6309\u9375\u8F38\u5165\u7684\u7C21\u6613\u6309\u9375\u700F\u89BD\u7126\u9EDE\u5143\u7D20\u3002\u50C5\u6BD4\u5C0D\u524D\u7F6E\u8A5E\u3002","\u9192\u76EE\u63D0\u793A\u9375\u76E4\u700F\u89BD\u6703\u9192\u76EE\u63D0\u793A\u7B26\u5408\u9375\u76E4\u8F38\u5165\u7684\u5143\u7D20\u3002\u9032\u4E00\u6B65\u5411\u4E0A\u6216\u5411\u4E0B\u700F\u89BD\u53EA\u6703\u5468\u904A\u9192\u76EE\u63D0\u793A\u7684\u5143\u7D20\u3002","\u7BE9\u9078\u9375\u76E4\u700F\u89BD\u6703\u7BE9\u6389\u4E26\u96B1\u85CF\u4E0D\u7B26\u5408\u9375\u76E4\u8F38\u5165\u7684\u6240\u6709\u5143\u7D20\u3002","\u63A7\u5236 Workbench \u4E2D\u6E05\u55AE\u548C\u6A39\u72C0\u7D50\u69CB\u7684\u9375\u76E4\u700F\u89BD\u6A23\u5F0F\u3002\u53EF\u4EE5\u662F\u7C21\u6613\u7684\u3001\u9192\u76EE\u63D0\u793A\u548C\u7BE9\u9078\u3002","\u63A7\u5236\u662F\u5426\u53EA\u8981\u9375\u5165\u5373\u53EF\u81EA\u52D5\u89F8\u767C\u6E05\u55AE\u548C\u6A39\u72C0\u7D50\u69CB\u4E2D\u7684\u9375\u76E4\u700F\u89BD\u3002\u82E5\u8A2D\u70BA `false`\uFF0C\u53EA\u6709\u5728\u57F7\u884C `list.toggleKeyboardNavigation` \u547D\u4EE4\u6642\uFF0C\u624D\u6703\u89F8\u767C\u9375\u76E4\u700F\u89BD\uFF0C\u60A8\u53EF\u70BA\u5176\u6307\u5B9A\u9375\u76E4\u5FEB\u901F\u9375\u3002","\u63A7\u5236\u7576\u6309\u4E0B\u8CC7\u6599\u593E\u540D\u7A31\u6642\uFF0C\u6A39\u72C0\u76EE\u9304\u8CC7\u6599\u593E\u7684\u5C55\u958B\u65B9\u5F0F\u3002\u8ACB\u6CE8\u610F\uFF0C\u82E5\u4E0D\u9069\u7528\uFF0C\u67D0\u4E9B\u6A39\u72C0\u76EE\u9304\u548C\u6E05\u55AE\u53EF\u80FD\u6703\u9078\u64C7\u5FFD\u7565\u6B64\u8A2D\u5B9A\u3002"],"vs/platform/markers/common/markers":["\u932F\u8AA4","\u8B66\u544A","\u8CC7\u8A0A"],"vs/platform/quickinput/browser/commandsQuickAccess":["{0}, {1}","\u6700\u8FD1\u4F7F\u7528\u7684","\u5176\u4ED6\u547D\u4EE4","\u547D\u4EE4 '{0}' \u9020\u6210\u932F\u8AA4 ({1})"],"vs/platform/quickinput/browser/helpQuickAccess":["\u5168\u57DF\u547D\u4EE4","\u7DE8\u8F2F\u5668\u547D\u4EE4","{0}, {1}"],"vs/platform/theme/common/colorRegistry":["\u6574\u9AD4\u7684\u524D\u666F\u8272\u5F69\u3002\u50C5\u7576\u672A\u88AB\u4EFB\u4F55\u5143\u4EF6\u8986\u758A\u6642\uFF0C\u624D\u6703\u4F7F\u7528\u6B64\u8272\u5F69\u3002","\u6574\u9AD4\u932F\u8AA4\u8A0A\u606F\u7684\u524D\u666F\u8272\u5F69\u3002\u50C5\u7576\u672A\u88AB\u4EFB\u4F55\u5143\u4EF6\u8986\u84CB\u6642\uFF0C\u624D\u6703\u4F7F\u7528\u6B64\u8272\u5F69\u3002","\u5DE5\u4F5C\u53F0\u4E2D\u5716\u793A\u7684\u9810\u8A2D\u8272\u5F69\u3002","\u7126\u9EDE\u9805\u76EE\u7684\u6574\u9AD4\u6846\u7DDA\u8272\u5F69\u3002\u53EA\u5728\u6C92\u6709\u4EFB\u4F55\u5143\u4EF6\u8986\u5BEB\u6B64\u8272\u5F69\u6642\uFF0C\u624D\u6703\u52A0\u4EE5\u4F7F\u7528\u3002","\u9805\u76EE\u5468\u570D\u7684\u984D\u5916\u6846\u7DDA\uFF0C\u53EF\u5C07\u9805\u76EE\u5F9E\u5176\u4ED6\u9805\u76EE\u4E2D\u5340\u9694\u51FA\u4F86\u4EE5\u63D0\u9AD8\u5C0D\u6BD4\u3002","\u4F7F\u7528\u4E2D\u9805\u76EE\u5468\u570D\u7684\u984D\u5916\u908A\u754C\uFF0C\u53EF\u5C07\u9805\u76EE\u5F9E\u5176\u4ED6\u9805\u76EE\u4E2D\u5340\u9694\u51FA\u4F86\u4EE5\u63D0\u9AD8\u5C0D\u6BD4\u3002","\u5167\u6587\u9023\u7D50\u7684\u524D\u666F\u8272\u5F69","\u6587\u5B57\u5340\u584A\u7684\u80CC\u666F\u984F\u8272\u3002","\u5C0F\u5DE5\u5177\u7684\u9670\u5F71\u8272\u5F69\uFF0C\u4F8B\u5982\u7DE8\u8F2F\u5668\u4E2D\u7684\u5C0B\u627E/\u53D6\u4EE3\u3002","\u8F38\u5165\u65B9\u584A\u7684\u80CC\u666F\u3002","\u8F38\u5165\u65B9\u584A\u7684\u524D\u666F\u3002","\u8F38\u5165\u65B9\u584A\u7684\u6846\u7DDA\u3002","\u8F38\u5165\u6B04\u4F4D\u4E2D\u53EF\u4F7F\u7528\u4E4B\u9805\u76EE\u7684\u6846\u7DDA\u8272\u5F69\u3002","\u5728\u8F38\u5165\u6B04\u4F4D\u4E2D\u6240\u555F\u52D5\u9078\u9805\u7684\u80CC\u666F\u8272\u5F69\u3002","\u5728\u8F38\u5165\u6B04\u4F4D\u4E2D\u6240\u555F\u52D5\u9078\u9805\u7684\u524D\u666F\u8272\u5F69\u3002","\u8CC7\u8A0A\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u80CC\u666F\u8272\u5F69\u3002","\u8CC7\u8A0A\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u524D\u666F\u8272\u5F69\u3002","\u8CC7\u8A0A\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u908A\u754C\u8272\u5F69\u3002","\u8B66\u544A\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u80CC\u666F\u8272\u5F69\u3002","\u8B66\u544A\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u524D\u666F\u8272\u5F69\u3002","\u8B66\u544A\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u908A\u754C\u8272\u5F69\u3002","\u932F\u8AA4\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u80CC\u666F\u8272\u5F69\u3002","\u932F\u8AA4\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u524D\u666F\u8272\u5F69\u3002","\u932F\u8AA4\u56B4\u91CD\u6027\u7684\u8F38\u5165\u9A57\u8B49\u908A\u754C\u8272\u5F69\u3002","\u4E0B\u62C9\u5F0F\u6E05\u55AE\u7684\u80CC\u666F\u3002","\u4E0B\u62C9\u5F0F\u6E05\u55AE\u7684\u524D\u666F\u3002","\u6309\u9215\u524D\u666F\u8272\u5F69\u3002","\u6309\u9215\u80CC\u666F\u8272\u5F69\u3002","\u66AB\u7559\u6642\u7684\u6309\u9215\u80CC\u666F\u8272\u5F69\u3002","\u6A19\u8A18\u7684\u80CC\u666F\u984F\u8272\u3002\u6A19\u8A18\u70BA\u5C0F\u578B\u7684\u8A0A\u606F\u6A19\u7C64,\u4F8B\u5982\u641C\u5C0B\u7D50\u679C\u7684\u6578\u91CF\u3002","\u6A19\u8A18\u7684\u524D\u666F\u984F\u8272\u3002\u6A19\u8A18\u70BA\u5C0F\u578B\u7684\u8A0A\u606F\u6A19\u7C64,\u4F8B\u5982\u641C\u5C0B\u7D50\u679C\u7684\u6578\u91CF\u3002","\u6307\u51FA\u5728\u6372\u52D5\u8A72\u6AA2\u8996\u7684\u6372\u8EF8\u9670\u5F71\u3002","\u6372\u8EF8\u6ED1\u687F\u7684\u80CC\u666F\u984F\u8272\u3002","\u52D5\u614B\u986F\u793A\u6642\u6372\u8EF8\u6ED1\u687F\u7684\u80CC\u666F\u984F\u8272\u3002","\u7576\u9EDE\u64CA\u6642\u6372\u8EF8\u6ED1\u687F\u7684\u80CC\u666F\u984F\u8272\u3002","\u9577\u6642\u9593\u904B\u884C\u9032\u5EA6\u689D\u7684\u80CC\u666F\u8272\u5F69.","\u7DE8\u8F2F\u5668\u4E2D\u932F\u8AA4\u6587\u5B57\u7684\u80CC\u666F\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u7DE8\u8F2F\u5668\u5167\u932F\u8AA4\u63D0\u793A\u7DDA\u7684\u524D\u666F\u8272\u5F69.","\u7DE8\u8F2F\u5668\u4E2D\u932F\u8AA4\u65B9\u584A\u7684\u6846\u7DDA\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u4E2D\u8B66\u544A\u6587\u5B57\u7684\u80CC\u666F\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u7DE8\u8F2F\u5668\u5167\u8B66\u544A\u63D0\u793A\u7DDA\u7684\u524D\u666F\u8272\u5F69.","\u7DE8\u8F2F\u5668\u4E2D\u7684\u8B66\u544A\u65B9\u584A\u6846\u7DDA\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u4E2D\u8CC7\u8A0A\u6587\u5B57\u7684\u80CC\u666F\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u7DE8\u8F2F\u5668\u5167\u8CC7\u8A0A\u63D0\u793A\u7DDA\u7684\u524D\u666F\u8272\u5F69","\u7DE8\u8F2F\u5668\u4E2D\u7684\u8CC7\u8A0A\u65B9\u584A\u6846\u7DDA\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u5167\u63D0\u793A\u8A0A\u606F\u7684\u63D0\u793A\u7DDA\u524D\u666F\u8272\u5F69","\u7DE8\u8F2F\u5668\u4E2D\u7684\u63D0\u793A\u65B9\u584A\u6846\u7DDA\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u7684\u80CC\u666F\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u7684\u9810\u8A2D\u524D\u666F\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u5C0F\u5DE5\u5177\u7684\u80CC\u666F\u8272\u5F69\uFF0C\u4F8B\u5982\u5C0B\u627E/\u53D6\u4EE3\u3002","\u7DE8\u8F2F\u5668\u5C0F\u5DE5\u5177 (\u4F8B\u5982\u5C0B\u627E/\u53D6\u4EE3) \u7684\u524D\u666F\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u5C0F\u5DE5\u5177\u7684\u908A\u754C\u8272\u5F69\u3002\u5C0F\u5DE5\u5177\u9078\u64C7\u64C1\u6709\u908A\u754C\u6216\u8272\u5F69\u672A\u88AB\u5C0F\u5DE5\u5177\u8986\u5BEB\u6642\uFF0C\u624D\u6703\u4F7F\u7528\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u5C0F\u5DE5\u5177\u4E4B\u8ABF\u6574\u5927\u5C0F\u5217\u7684\u908A\u754C\u8272\u5F69\u3002\u53EA\u5728\u5C0F\u5DE5\u5177\u9078\u64C7\u5177\u6709\u8ABF\u6574\u5927\u5C0F\u908A\u754C\u4E14\u672A\u8986\u5BEB\u8A72\u8272\u5F69\u6642\uFF0C\u624D\u4F7F\u7528\u8A72\u8272\u5F69\u3002","\u5FEB\u901F\u9078\u64C7\u5668\u80CC\u666F\u8272\u5F69\u3002\u8A72\u5FEB\u901F\u9078\u64C7\u5668\u5C0F\u5DE5\u5177\u662F\u985E\u4F3C\u547D\u4EE4\u9078\u64C7\u5340\u7684\u9078\u64C7\u5668\u5BB9\u5668\u3002","\u5FEB\u901F\u9078\u64C7\u5668\u524D\u666F\u8272\u5F69\u3002\u5FEB\u901F\u9078\u64C7\u5668\u5C0F\u5DE5\u5177\u662F\u985E\u4F3C\u547D\u4EE4\u9078\u64C7\u5340\u7B49\u9078\u64C7\u5668\u7684\u5BB9\u5668\u3002","\u5FEB\u901F\u9078\u64C7\u5668\u6A19\u984C\u80CC\u666F\u8272\u5F69\u3002\u5FEB\u901F\u9078\u64C7\u5668\u5C0F\u5DE5\u5177\u662F\u985E\u4F3C\u547D\u4EE4\u9078\u64C7\u5340\u7684\u9078\u64C7\u5668\u5BB9\u5668\u3002","\u7126\u9EDE\u9805\u76EE\u7684\u5FEB\u901F\u9078\u64C7\u5668\u80CC\u666F\u8272\u5F69\u3002","\u5206\u7D44\u6A19\u7C64\u7684\u5FEB\u901F\u9078\u64C7\u5668\u8272\u5F69\u3002","\u5206\u7D44\u908A\u754C\u7684\u5FEB\u901F\u9078\u64C7\u5668\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u9078\u53D6\u7BC4\u570D\u7684\u8272\u5F69\u3002","\u70BA\u9078\u53D6\u7684\u6587\u5B57\u984F\u8272\u9AD8\u5C0D\u6BD4\u5316","\u975E\u4F7F\u7528\u4E2D\u7DE8\u8F2F\u5668\u5167\u7684\u9078\u53D6\u9805\u76EE\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u8207\u9078\u53D6\u9805\u76EE\u5167\u5BB9\u76F8\u540C\u4E4B\u5340\u57DF\u7684\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u9078\u53D6\u6642\uFF0C\u5167\u5BB9\u76F8\u540C\u4E4B\u5340\u57DF\u7684\u6846\u7DDA\u8272\u5F69\u3002","\u7B26\u5408\u76EE\u524D\u641C\u5C0B\u7684\u8272\u5F69\u3002","\u5176\u4ED6\u641C\u5C0B\u76F8\u7B26\u9805\u76EE\u7684\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u9650\u5236\u641C\u5C0B\u4E4B\u7BC4\u570D\u7684\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u7B26\u5408\u76EE\u524D\u641C\u5C0B\u7684\u6846\u7DDA\u8272\u5F69\u3002","\u7B26\u5408\u5176\u4ED6\u641C\u5C0B\u7684\u6846\u7DDA\u8272\u5F69\u3002","\u9650\u5236\u641C\u5C0B\u4E4B\u7BC4\u570D\u7684\u6846\u7DDA\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u5728\u986F\u793A\u52D5\u614B\u986F\u793A\u7684\u6587\u5B57\u4E0B\u9192\u76EE\u63D0\u793A\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u7DE8\u8F2F\u5668\u52D5\u614B\u986F\u793A\u7684\u80CC\u666F\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u52D5\u614B\u986F\u793A\u7684\u524D\u666F\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u52D5\u614B\u986F\u793A\u7684\u6846\u7DDA\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u66AB\u7559\u72C0\u614B\u5217\u7684\u80CC\u666F\u8272\u5F69\u3002","\u4F7F\u7528\u4E2D\u4E4B\u9023\u7D50\u7684\u8272\u5F69\u3002","\u5167\u5D4C\u63D0\u793A\u7684\u524D\u666F\u8272\u5F69","\u5167\u5D4C\u63D0\u793A\u7684\u80CC\u666F\u8272\u5F69","\u7528\u65BC\u71C8\u6CE1\u52D5\u4F5C\u5716\u793A\u7684\u8272\u5F69\u3002","\u7528\u65BC\u71C8\u6CE1\u81EA\u52D5\u4FEE\u6B63\u52D5\u4F5C\u5716\u793A\u7684\u8272\u5F69\u3002","\u5DF2\u63D2\u5165\u6587\u5B57\u7684\u80CC\u666F\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u5DF2\u79FB\u9664\u6587\u5B57\u7684\u80CC\u666F\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u63D2\u5165\u7684\u6587\u5B57\u5916\u6846\u8272\u5F69\u3002","\u79FB\u9664\u7684\u6587\u5B57\u5916\u6846\u8272\u5F69\u3002","\u5169\u500B\u6587\u5B57\u7DE8\u8F2F\u5668\u4E4B\u9593\u7684\u6846\u7DDA\u8272\u5F69\u3002","Diff \u7DE8\u8F2F\u5668\u7684\u659C\u7D0B\u586B\u6EFF\u8272\u5F69\u3002\u659C\u7D0B\u586B\u6EFF\u7528\u65BC\u4E26\u6392 Diff \u6AA2\u8996\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u70BA\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u7126\u9EDE\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u80CC\u666F\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u8005\u5247\u6C92\u6709\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u70BA\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u7126\u9EDE\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u524D\u666F\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u8005\u5247\u6C92\u6709\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u76EE\u9304\u70BA\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u7126\u9EDE\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u76EE\u9304\u5916\u6846\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u76EE\u9304\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u8005\u5247\u6C92\u6709\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u70BA\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u6240\u9078\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u80CC\u666F\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u8005\u5247\u6C92\u6709\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u70BA\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u6240\u9078\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u524D\u666F\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u8005\u5247\u6C92\u6709\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u70BA\u975E\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u6240\u9078\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u80CC\u666F\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u8005\u5247\u6C92\u6709\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u70BA\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u6240\u9078\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u524D\u666F\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u5247\u6C92\u6709\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u70BA\u975E\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u7126\u9EDE\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u80CC\u666F\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u8005\u5247\u6C92\u6709\u3002","\u7576\u6E05\u55AE/\u6A39\u72C0\u76EE\u9304\u70BA\u975E\u4F7F\u7528\u4E2D\u72C0\u614B\u6642\uFF0C\u7126\u9EDE\u9805\u76EE\u7684\u6E05\u55AE/\u6A39\u72C0\u76EE\u9304\u5916\u6846\u8272\u5F69\u3002\u4F7F\u7528\u4E2D\u7684\u6E05\u55AE/\u6A39\u72C0\u76EE\u9304\u6709\u9375\u76E4\u7126\u9EDE\uFF0C\u975E\u4F7F\u7528\u4E2D\u8005\u5247\u6C92\u6709\u3002","\u4F7F\u7528\u6ED1\u9F20\u66AB\u7559\u5728\u9805\u76EE\u6642\u7684\u6E05\u55AE/\u6A39\u72C0\u80CC\u666F\u3002","\u6ED1\u9F20\u66AB\u7559\u5728\u9805\u76EE\u6642\u7684\u6E05\u55AE/\u6A39\u72C0\u524D\u666F\u3002","\u4F7F\u7528\u6ED1\u9F20\u56DB\u8655\u79FB\u52D5\u9805\u76EE\u6642\u7684\u6E05\u55AE/\u6A39\u72C0\u62D6\u653E\u80CC\u666F\u3002","\u5728\u6E05\u55AE/\u6A39\u72C0\u5167\u641C\u5C0B\u6642\uFF0C\u76F8\u7B26\u9192\u76EE\u63D0\u793A\u7684\u6E05\u55AE/\u6A39\u72C0\u524D\u666F\u8272\u5F69\u3002","\u6E05\u55AE\u548C\u6A39\u72C0\u7D50\u69CB\u4E2D\u985E\u578B\u7BE9\u9078\u5C0F\u5DE5\u5177\u7684\u80CC\u666F\u8272\u5F69\u3002","\u6E05\u55AE\u548C\u6A39\u72C0\u7D50\u69CB\u4E2D\u985E\u578B\u7BE9\u9078\u5C0F\u5DE5\u5177\u7684\u5927\u7DB1\u8272\u5F69\u3002","\u5728\u6C92\u6709\u76F8\u7B26\u9805\u76EE\u6642\uFF0C\u6E05\u55AE\u548C\u6A39\u72C0\u7D50\u69CB\u4E2D\u985E\u578B\u7BE9\u9078\u5C0F\u5DE5\u5177\u7684\u5927\u7DB1\u8272\u5F69\u3002","\u7E2E\u6392\u8F14\u52A9\u7DDA\u7684\u6A39\u72C0\u7B46\u89F8\u8272\u5F69\u3002","\u7E2E\u6392\u8F14\u52A9\u7DDA\u7684\u6A39\u72C0\u7B46\u89F8\u8272\u5F69\u3002","\u529F\u80FD\u8868\u7684\u908A\u6846\u8272\u5F69\u3002","\u529F\u80FD\u8868\u9805\u76EE\u7684\u524D\u666F\u8272\u5F69\u3002","\u529F\u80FD\u8868\u9805\u76EE\u7684\u80CC\u666F\u8272\u5F69\u3002","\u529F\u80FD\u8868\u4E2D\u6240\u9078\u529F\u80FD\u8868\u9805\u76EE\u7684\u524D\u666F\u8272\u5F69\u3002","\u529F\u80FD\u8868\u4E2D\u6240\u9078\u529F\u80FD\u8868\u9805\u76EE\u7684\u80CC\u666F\u8272\u5F69\u3002","\u529F\u80FD\u8868\u4E2D\u6240\u9078\u529F\u80FD\u8868\u9805\u76EE\u7684\u6846\u7DDA\u8272\u5F69\u3002","\u529F\u80FD\u8868\u4E2D\u5206\u9694\u7DDA\u529F\u80FD\u8868\u9805\u76EE\u7684\u8272\u5F69\u3002","\u7A0B\u5F0F\u78BC\u7247\u6BB5\u5B9A\u4F4D\u505C\u99D0\u9EDE\u7684\u53CD\u767D\u986F\u793A\u80CC\u666F\u8272\u5F69\u3002","\u7A0B\u5F0F\u78BC\u7247\u6BB5\u5B9A\u4F4D\u505C\u99D0\u9EDE\u7684\u53CD\u767D\u986F\u793A\u908A\u754C\u8272\u5F69\u3002","\u7A0B\u5F0F\u78BC\u7247\u6BB5\u6700\u7D42\u5B9A\u4F4D\u505C\u99D0\u9EDE\u7684\u53CD\u767D\u986F\u793A\u80CC\u666F\u8272\u5F69\u3002","\u7A0B\u5F0F\u78BC\u7247\u6BB5\u6700\u7D42\u5B9A\u4F4D\u505C\u99D0\u9EDE\u7684\u9192\u76EE\u63D0\u793A\u6846\u7DDA\u8272\u5F69\u3002","\u5C0B\u627E\u76F8\u7B26\u9805\u76EE\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u8272\u5F69\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u9078\u53D6\u9805\u76EE\u9192\u76EE\u63D0\u793A\u7684\u6982\u89C0\u5C3A\u898F\u6A19\u8A18\u3002\u5176\u4E0D\u5F97\u70BA\u4E0D\u900F\u660E\u8272\u5F69\uFF0C\u4EE5\u514D\u96B1\u85CF\u5E95\u5C64\u88DD\u98FE\u3002","\u7528\u65BC\u5C0B\u627E\u76F8\u7B26\u9805\u76EE\u7684\u7E2E\u5716\u6A19\u8A18\u8272\u5F69\u3002","\u7DE8\u8F2F\u5668\u9078\u53D6\u7BC4\u570D\u7684\u8FF7\u4F60\u5730\u5716\u6A19\u8A18\u8272\u5F69\u3002","\u932F\u8AA4\u7684\u7E2E\u5716\u6A19\u8A18\u8272\u5F69\u3002","\u8B66\u544A\u7684\u7E2E\u5716\u6A19\u8A18\u8272\u5F69\u3002","\u7E2E\u5716\u80CC\u666F\u8272\u5F69\u3002","\u7E2E\u5716\u6ED1\u687F\u80CC\u666F\u8272\u5F69\u3002","\u66AB\u7559\u6642\u7684\u7E2E\u5716\u6ED1\u687F\u80CC\u666F\u8272\u5F69\u3002","\u6309\u4E00\u4E0B\u6642\u7684\u7E2E\u5716\u6ED1\u687F\u80CC\u666F\u8272\u5F69\u3002","\u7528\u65BC\u554F\u984C\u932F\u8AA4\u5716\u793A\u7684\u8272\u5F69\u3002","\u7528\u65BC\u554F\u984C\u8B66\u544A\u5716\u793A\u7684\u8272\u5F69\u3002","\u7528\u65BC\u554F\u984C\u8CC7\u8A0A\u5716\u793A\u7684\u8272\u5F69\u3002"],"vs/platform/theme/common/iconRegistry":["\u8981\u4F7F\u7528\u7684\u5B57\u578B\u8B58\u5225\u78BC\u3002\u5982\u672A\u8A2D\u5B9A\uFF0C\u5C31\u6703\u4F7F\u7528\u6700\u5148\u5B9A\u7FA9\u7684\u5B57\u578B\u3002","\u8207\u5716\u793A\u5B9A\u7FA9\u5EFA\u7ACB\u95DC\u806F\u7684\u5B57\u578B\u5B57\u5143\u3002","\u5C0F\u5DE5\u5177\u4E2D\u95DC\u9589\u52D5\u4F5C\u7684\u5716\u793A\u3002"],"vs/platform/undoRedo/common/undoRedoService":["\u5DF2\u5728\u78C1\u789F\u4E0A\u95DC\u9589\u4E26\u4FEE\u6539\u4EE5\u4E0B\u6A94\u6848: {0}\u3002","\u4E0B\u5217\u6A94\u6848\u5DF2\u4F7F\u7528\u4E0D\u76F8\u5BB9\u7684\u65B9\u5F0F\u4FEE\u6539: {0}\u3002","\u7121\u6CD5\u5FA9\u539F\u6240\u6709\u6A94\u6848\u7684 '{0}'\u3002{1}","\u7121\u6CD5\u5FA9\u539F\u6240\u6709\u6A94\u6848\u7684 '{0}'\u3002{1}","\u56E0\u70BA\u5DF2\u5C0D {1} \u9032\u884C\u8B8A\u66F4\uFF0C\u6240\u4EE5\u7121\u6CD5\u5FA9\u539F\u6240\u6709\u6A94\u6848\u7684 '{0}'","\u56E0\u70BA {1} \u4E2D\u5DF2\u7D93\u6709\u6B63\u5728\u57F7\u884C\u7684\u5FA9\u539F\u6216\u91CD\u505A\u4F5C\u696D\uFF0C\u6240\u4EE5\u7121\u6CD5\u70BA\u6240\u6709\u6A94\u6848\u5FA9\u539F '{0}'","\u56E0\u70BA\u540C\u6642\u767C\u751F\u5176\u4ED6\u5FA9\u539F\u6216\u91CD\u505A\u4F5C\u696D\uFF0C\u6240\u4EE5\u7121\u6CD5\u70BA\u6240\u6709\u6A94\u6848\u5FA9\u539F '{0}'","\u8981\u5FA9\u539F\u6240\u6709\u6A94\u6848\u7684 '{0}' \u55CE?","\u5728 {0} \u500B\u6A94\u6848\u4E2D\u5FA9\u539F","\u5FA9\u539F\u6B64\u6A94\u6848","\u53D6\u6D88","\u56E0\u70BA\u5DF2\u7D93\u6709\u6B63\u5728\u57F7\u884C\u7684\u5FA9\u539F\u6216\u91CD\u505A\u4F5C\u696D\uFF0C\u6240\u4EE5\u7121\u6CD5\u5FA9\u539F '{0}'\u3002","\u8981\u5FA9\u539F '{0}' \u55CE?","\u5FA9\u539F","\u53D6\u6D88","\u7121\u6CD5\u5FA9\u539F\u6240\u6709\u6A94\u6848\u7684 '{0}'\u3002{1}","\u7121\u6CD5\u5FA9\u539F\u6240\u6709\u6A94\u6848\u7684 '{0}'\u3002{1}","\u56E0\u70BA\u5DF2\u5C0D {1} \u9032\u884C\u8B8A\u66F4\uFF0C\u6240\u4EE5\u7121\u6CD5\u5FA9\u539F\u6240\u6709\u6A94\u6848\u7684 '{0}'","\u56E0\u70BA {1} \u4E2D\u5DF2\u7D93\u6709\u6B63\u5728\u57F7\u884C\u7684\u5FA9\u539F\u6216\u91CD\u505A\u4F5C\u696D\uFF0C\u6240\u4EE5\u7121\u6CD5\u70BA\u6240\u6709\u6A94\u6848\u91CD\u505A '{0}'","\u56E0\u70BA\u540C\u6642\u767C\u751F\u5176\u4ED6\u5FA9\u539F\u6216\u91CD\u505A\u4F5C\u696D\uFF0C\u6240\u4EE5\u7121\u6CD5\u70BA\u6240\u6709\u6A94\u6848\u91CD\u505A '{0}'","\u56E0\u70BA\u5DF2\u7D93\u6709\u6B63\u5728\u57F7\u884C\u7684\u5FA9\u539F\u6216\u91CD\u505A\u4F5C\u696D\uFF0C\u6240\u4EE5\u7121\u6CD5\u91CD\u505A '{0}'\u3002"]}); diff --git a/yunxi-ui-admin/public/libs/monaco-editor/vs/language/css/cssMode.js b/yunxi-ui-admin/public/libs/monaco-editor/vs/language/css/cssMode.js new file mode 100644 index 0000000..c4b71c0 --- /dev/null +++ b/yunxi-ui-admin/public/libs/monaco-editor/vs/language/css/cssMode.js @@ -0,0 +1,7 @@ +/*!----------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * monaco-css version: 3.3.0(ed57760e69ec24098de2b4e49fa5be7f48be019a) + * Released under the MIT license + * https://github.com/Microsoft/monaco-css/blob/master/LICENSE.md + *-----------------------------------------------------------------------------*/ +define("vs/language/css/workerManager",["require","exports","./fillers/monaco-editor-core"],(function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.WorkerManager=void 0;var r=function(){function e(e){var t=this;this._defaults=e,this._worker=null,this._idleCheckInterval=window.setInterval((function(){return t._checkIfIdle()}),3e4),this._lastUsedTime=0,this._configChangeListener=this._defaults.onDidChange((function(){return t._stopWorker()}))}return e.prototype._stopWorker=function(){this._worker&&(this._worker.dispose(),this._worker=null),this._client=null},e.prototype.dispose=function(){clearInterval(this._idleCheckInterval),this._configChangeListener.dispose(),this._stopWorker()},e.prototype._checkIfIdle=function(){this._worker&&(Date.now()-this._lastUsedTime>12e4&&this._stopWorker())},e.prototype._getClient=function(){return this._lastUsedTime=Date.now(),this._client||(this._worker=n.editor.createWebWorker({moduleId:"vs/language/css/cssWorker",label:this._defaults.languageId,createData:{languageSettings:this._defaults.diagnosticsOptions,languageId:this._defaults.languageId}}),this._client=this._worker.getProxy()),this._client},e.prototype.getLanguageServiceWorker=function(){for(var e,t=this,n=[],r=0;rthis.source.length)return!1;for(var t=0;t=d&&e<=p&&(this.stream.advance(t+1),this.stream.advanceWhileChar((function(e){return e>=d&&e<=p||0===t&&e===V})),!0)},e.prototype._newline=function(e){var t=this.stream.peekChar();switch(t){case _:case P:case R:return this.stream.advance(1),e.push(String.fromCharCode(t)),t===_&&this.stream.advanceIfChar(R)&&e.push("\n"),!0}return!1},e.prototype._escape=function(e,t){var n=this.stream.peekChar();if(n===F){this.stream.advance(1),n=this.stream.peekChar();for(var r=0;r<6&&(n>=d&&n<=p||n>=i&&n<=o||n>=a&&n<=l);)this.stream.advance(1),n=this.stream.peekChar(),r++;if(r>0){try{var s=parseInt(this.stream.substring(this.stream.pos()-r),16);s&&e.push(String.fromCharCode(s))}catch(e){}return n===N||n===M?this.stream.advance(1):this._newline([]),!0}if(n!==_&&n!==P&&n!==R)return this.stream.advance(1),e.push(String.fromCharCode(n)),!0;if(t)return this._newline(e)}return!1},e.prototype._stringChar=function(e,t){var n=this.stream.peekChar();return 0!==n&&n!==e&&n!==F&&n!==_&&n!==P&&n!==R&&(this.stream.advance(1),t.push(String.fromCharCode(n)),!0)},e.prototype._string=function(e){if(this.stream.peekChar()===I||this.stream.peekChar()===z){var t=this.stream.nextChar();for(e.push(String.fromCharCode(t));this._stringChar(t,e)||this._escape(e,!0););return this.stream.peekChar()===t?(this.stream.nextChar(),e.push(String.fromCharCode(t)),n.String):n.BadString}return null},e.prototype._unquotedChar=function(e){var t=this.stream.peekChar();return 0!==t&&t!==F&&t!==I&&t!==z&&t!==w&&t!==x&&t!==N&&t!==M&&t!==R&&t!==P&&t!==_&&(this.stream.advance(1),e.push(String.fromCharCode(t)),!0)},e.prototype._unquotedString=function(e){for(var t=!1;this._unquotedChar(e)||this._escape(e);)t=!0;return t},e.prototype._whitespace=function(){return this.stream.advanceWhileChar((function(e){return e===N||e===M||e===R||e===P||e===_}))>0},e.prototype._name=function(e){for(var t=!1;this._identChar(e)||this._escape(e);)t=!0;return t},e.prototype.ident=function(e){var t=this.stream.pos();if(this._minus(e)&&this._minus(e)){if(this._identFirstChar(e)||this._escape(e)){for(;this._identChar(e)||this._escape(e););return!0}}else if(this._identFirstChar(e)||this._escape(e)){for(;this._identChar(e)||this._escape(e););return!0}return this.stream.goBackTo(t),!1},e.prototype._identFirstChar=function(e){var t=this.stream.peekChar();return(t===b||t>=i&&t<=s||t>=a&&t<=c||t>=128&&t<=65535)&&(this.stream.advance(1),e.push(String.fromCharCode(t)),!0)},e.prototype._minus=function(e){var t=this.stream.peekChar();return t===g&&(this.stream.advance(1),e.push(String.fromCharCode(t)),!0)},e.prototype._identChar=function(e){var t=this.stream.peekChar();return(t===b||t===g||t>=i&&t<=s||t>=a&&t<=c||t>=d&&t<=p||t>=128&&t<=65535)&&(this.stream.advance(1),e.push(String.fromCharCode(t)),!0)},e}();t.Scanner=G})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/utils/strings",["require","exports"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.trim=t.getLimitedString=t.difference=t.endsWith=t.startsWith=void 0,t.startsWith=function(e,t){if(e.length0?e.lastIndexOf(t)===n:0===n&&e===t},t.difference=function(e,t,n){void 0===n&&(n=4);var r=Math.abs(e.length-t.length);if(r>n)return 0;var i,o,s=[],a=[];for(i=0;ie.end?null:(e.accept((function(e){return-1===e.offset&&-1===e.length||e.offset<=t&&e.end>=t&&(n?e.length<=n.length&&(n=e):n=e,!0)})),n)}!function(e){e[e.Undefined=0]="Undefined",e[e.Identifier=1]="Identifier",e[e.Stylesheet=2]="Stylesheet",e[e.Ruleset=3]="Ruleset",e[e.Selector=4]="Selector",e[e.SimpleSelector=5]="SimpleSelector",e[e.SelectorInterpolation=6]="SelectorInterpolation",e[e.SelectorCombinator=7]="SelectorCombinator",e[e.SelectorCombinatorParent=8]="SelectorCombinatorParent",e[e.SelectorCombinatorSibling=9]="SelectorCombinatorSibling",e[e.SelectorCombinatorAllSiblings=10]="SelectorCombinatorAllSiblings",e[e.SelectorCombinatorShadowPiercingDescendant=11]="SelectorCombinatorShadowPiercingDescendant",e[e.Page=12]="Page",e[e.PageBoxMarginBox=13]="PageBoxMarginBox",e[e.ClassSelector=14]="ClassSelector",e[e.IdentifierSelector=15]="IdentifierSelector",e[e.ElementNameSelector=16]="ElementNameSelector",e[e.PseudoSelector=17]="PseudoSelector",e[e.AttributeSelector=18]="AttributeSelector",e[e.Declaration=19]="Declaration",e[e.Declarations=20]="Declarations",e[e.Property=21]="Property",e[e.Expression=22]="Expression",e[e.BinaryExpression=23]="BinaryExpression",e[e.Term=24]="Term",e[e.Operator=25]="Operator",e[e.Value=26]="Value",e[e.StringLiteral=27]="StringLiteral",e[e.URILiteral=28]="URILiteral",e[e.EscapedValue=29]="EscapedValue",e[e.Function=30]="Function",e[e.NumericValue=31]="NumericValue",e[e.HexColorValue=32]="HexColorValue",e[e.MixinDeclaration=33]="MixinDeclaration",e[e.MixinReference=34]="MixinReference",e[e.VariableName=35]="VariableName",e[e.VariableDeclaration=36]="VariableDeclaration",e[e.Prio=37]="Prio",e[e.Interpolation=38]="Interpolation",e[e.NestedProperties=39]="NestedProperties",e[e.ExtendsReference=40]="ExtendsReference",e[e.SelectorPlaceholder=41]="SelectorPlaceholder",e[e.Debug=42]="Debug",e[e.If=43]="If",e[e.Else=44]="Else",e[e.For=45]="For",e[e.Each=46]="Each",e[e.While=47]="While",e[e.MixinContentReference=48]="MixinContentReference",e[e.MixinContentDeclaration=49]="MixinContentDeclaration",e[e.Media=50]="Media",e[e.Keyframe=51]="Keyframe",e[e.FontFace=52]="FontFace",e[e.Import=53]="Import",e[e.Namespace=54]="Namespace",e[e.Invocation=55]="Invocation",e[e.FunctionDeclaration=56]="FunctionDeclaration",e[e.ReturnStatement=57]="ReturnStatement",e[e.MediaQuery=58]="MediaQuery",e[e.FunctionParameter=59]="FunctionParameter",e[e.FunctionArgument=60]="FunctionArgument",e[e.KeyframeSelector=61]="KeyframeSelector",e[e.ViewPort=62]="ViewPort",e[e.Document=63]="Document",e[e.AtApplyRule=64]="AtApplyRule",e[e.CustomPropertyDeclaration=65]="CustomPropertyDeclaration",e[e.CustomPropertySet=66]="CustomPropertySet",e[e.ListEntry=67]="ListEntry",e[e.Supports=68]="Supports",e[e.SupportsCondition=69]="SupportsCondition",e[e.NamespacePrefix=70]="NamespacePrefix",e[e.GridLine=71]="GridLine",e[e.Plugin=72]="Plugin",e[e.UnknownAtRule=73]="UnknownAtRule",e[e.Use=74]="Use",e[e.ModuleConfiguration=75]="ModuleConfiguration",e[e.Forward=76]="Forward",e[e.ForwardVisibility=77]="ForwardVisibility",e[e.Module=78]="Module"}(n=t.NodeType||(t.NodeType={})),function(e){e[e.Mixin=0]="Mixin",e[e.Rule=1]="Rule",e[e.Variable=2]="Variable",e[e.Function=3]="Function",e[e.Keyframe=4]="Keyframe",e[e.Unknown=5]="Unknown",e[e.Module=6]="Module",e[e.Forward=7]="Forward",e[e.ForwardVisibility=8]="ForwardVisibility"}(t.ReferenceType||(t.ReferenceType={})),t.getNodeAtOffset=i,t.getNodePath=function(e,t){for(var n=i(e,t),r=[];n;)r.unshift(n),n=n.parent;return r},t.getParentDeclaration=function(e){var t=e.findParent(n.Declaration),r=t&&t.getValue();return r&&r.encloses(e)?t:null};var o=function(){function e(e,t,n){void 0===e&&(e=-1),void 0===t&&(t=-1),this.parent=null,this.offset=e,this.length=t,n&&(this.nodeType=n)}return Object.defineProperty(e.prototype,"end",{get:function(){return this.offset+this.length},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"type",{get:function(){return this.nodeType||n.Undefined},set:function(e){this.nodeType=e},enumerable:!1,configurable:!0}),e.prototype.getTextProvider=function(){for(var e=this;e&&!e.textProvider;)e=e.parent;return e?e.textProvider:function(){return"unknown"}},e.prototype.getText=function(){return this.getTextProvider()(this.offset,this.length)},e.prototype.matches=function(e){return this.length===e.length&&this.getTextProvider()(this.offset,this.length)===e},e.prototype.startsWith=function(e){return this.length>=e.length&&this.getTextProvider()(this.offset,e.length)===e},e.prototype.endsWith=function(e){return this.length>=e.length&&this.getTextProvider()(this.end-e.length,e.length)===e},e.prototype.accept=function(e){if(e(this)&&this.children)for(var t=0,n=this.children;t=0&&e.parent.children.splice(n,1)}e.parent=this;var r=this.children;return r||(r=this.children=[]),-1!==t?r.splice(t,0,e):r.push(e),e},e.prototype.attachTo=function(e,t){return void 0===t&&(t=-1),e&&e.adoptChild(this,t),this},e.prototype.collectIssues=function(e){this.issues&&e.push.apply(e,this.issues)},e.prototype.addIssue=function(e){this.issues||(this.issues=[]),this.issues.push(e)},e.prototype.hasIssue=function(e){return Array.isArray(this.issues)&&this.issues.some((function(t){return t.getRule()===e}))},e.prototype.isErroneous=function(e){return void 0===e&&(e=!1),!!(this.issues&&this.issues.length>0)||e&&Array.isArray(this.children)&&this.children.some((function(e){return e.isErroneous(!0)}))},e.prototype.setNode=function(e,t,n){return void 0===n&&(n=-1),!!t&&(t.attachTo(this,n),this[e]=t,!0)},e.prototype.addChild=function(e){return!!e&&(this.children||(this.children=[]),e.attachTo(this),this.updateOffsetAndLength(e),!0)},e.prototype.updateOffsetAndLength=function(e){(e.offsetthis.end||-1===this.length)&&(this.length=t-this.offset)},e.prototype.hasChildren=function(){return!!this.children&&this.children.length>0},e.prototype.getChildren=function(){return this.children?this.children.slice(0):[]},e.prototype.getChild=function(e){return this.children&&e=0;n--)if((t=this.children[n]).offset<=e)return t;return null},e.prototype.findChildAtOffset=function(e,t){var n=this.findFirstChildBeforeOffset(e);return n&&n.end>=e?t&&n.findChildAtOffset(e,!0)||n:null},e.prototype.encloses=function(e){return this.offset<=e.offset&&this.offset+this.length>=e.offset+e.length},e.prototype.getParent=function(){for(var e=this.parent;e instanceof s;)e=e.parent;return e},e.prototype.findParent=function(e){for(var t=this;t&&t.type!==e;)t=t.parent;return t},e.prototype.findAParent=function(){for(var e=[],t=0;t/g,">")}function i(e,t){if(!e.description||""===e.description)return"";if("string"!=typeof e.description)return e.description.value;var r="";if(!1!==(null==t?void 0:t.documentation)){e.status&&(r+=n(e.status)),r+=e.description;var i=s(e.browsers);i&&(r+="\n("+i+")"),"syntax"in e&&(r+="\n\nSyntax: "+e.syntax)}return e.references&&e.references.length>0&&!1!==(null==t?void 0:t.references)&&(r.length>0&&(r+="\n\n"),r+=e.references.map((function(e){return e.name+": "+e.url})).join(" | ")),r}function o(e,t){if(!e.description||""===e.description)return"";var i="";if(!1!==(null==t?void 0:t.documentation)){e.status&&(i+=n(e.status)),i+=r("string"==typeof e.description?e.description:e.description.value);var o=s(e.browsers);o&&(i+="\n\n("+r(o)+")"),"syntax"in e&&e.syntax&&(i+="\n\nSyntax: "+r(e.syntax))}return e.references&&e.references.length>0&&!1!==(null==t?void 0:t.references)&&(i.length>0&&(i+="\n\n"),i+=e.references.map((function(e){return"["+e.name+"]("+e.url+")"})).join(" | ")),i}function s(e){return void 0===e&&(e=[]),0===e.length?null:e.map((function(e){var n="",r=e.match(/([A-Z]+)(\d+)?/),i=r[1],o=r[2];return i in t.browserNames&&(n+=t.browserNames[i]),o&&(n+=" "+o),n})).join(", ")}Object.defineProperty(t,"__esModule",{value:!0}),t.getBrowserLabel=t.textToMarkedString=t.getEntryDescription=t.browserNames=void 0,t.browserNames={E:"Edge",FF:"Firefox",S:"Safari",C:"Chrome",IE:"IE",O:"Opera"},t.getEntryDescription=function(e,t,n){var r;if(""!==(r=t?{kind:"markdown",value:o(e,n)}:{kind:"plaintext",value:i(e,n)}).value)return r},t.textToMarkedString=r,t.getBrowserLabel=s})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/languageFacts/colors",["require","exports","../parser/cssNodes","vscode-nls"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getColorValue=t.hslFromColor=t.colorFromHSL=t.colorFrom256RGB=t.colorFromHex=t.hexDigit=t.isColorValue=t.isColorConstructor=t.colorKeywords=t.colors=t.colorFunctions=void 0;var n=e("../parser/cssNodes"),r=e("vscode-nls").loadMessageBundle();function i(e,t){var n=e.getText().match(/^([-+]?[0-9]*\.?[0-9]+)(%?)$/);if(n){n[2]&&(t=100);var r=parseFloat(n[1])/t;if(r>=0&&r<=1)return r}throw new Error}function o(e){var t=e.getName();return!!t&&/^(rgb|rgba|hsl|hsla)$/gi.test(t)}t.colorFunctions=[{func:"rgb($red, $green, $blue)",desc:r("css.builtin.rgb","Creates a Color from red, green, and blue values.")},{func:"rgba($red, $green, $blue, $alpha)",desc:r("css.builtin.rgba","Creates a Color from red, green, blue, and alpha values.")},{func:"hsl($hue, $saturation, $lightness)",desc:r("css.builtin.hsl","Creates a Color from hue, saturation, and lightness values.")},{func:"hsla($hue, $saturation, $lightness, $alpha)",desc:r("css.builtin.hsla","Creates a Color from hue, saturation, lightness, and alpha values.")}],t.colors={aliceblue:"#f0f8ff",antiquewhite:"#faebd7",aqua:"#00ffff",aquamarine:"#7fffd4",azure:"#f0ffff",beige:"#f5f5dc",bisque:"#ffe4c4",black:"#000000",blanchedalmond:"#ffebcd",blue:"#0000ff",blueviolet:"#8a2be2",brown:"#a52a2a",burlywood:"#deb887",cadetblue:"#5f9ea0",chartreuse:"#7fff00",chocolate:"#d2691e",coral:"#ff7f50",cornflowerblue:"#6495ed",cornsilk:"#fff8dc",crimson:"#dc143c",cyan:"#00ffff",darkblue:"#00008b",darkcyan:"#008b8b",darkgoldenrod:"#b8860b",darkgray:"#a9a9a9",darkgrey:"#a9a9a9",darkgreen:"#006400",darkkhaki:"#bdb76b",darkmagenta:"#8b008b",darkolivegreen:"#556b2f",darkorange:"#ff8c00",darkorchid:"#9932cc",darkred:"#8b0000",darksalmon:"#e9967a",darkseagreen:"#8fbc8f",darkslateblue:"#483d8b",darkslategray:"#2f4f4f",darkslategrey:"#2f4f4f",darkturquoise:"#00ced1",darkviolet:"#9400d3",deeppink:"#ff1493",deepskyblue:"#00bfff",dimgray:"#696969",dimgrey:"#696969",dodgerblue:"#1e90ff",firebrick:"#b22222",floralwhite:"#fffaf0",forestgreen:"#228b22",fuchsia:"#ff00ff",gainsboro:"#dcdcdc",ghostwhite:"#f8f8ff",gold:"#ffd700",goldenrod:"#daa520",gray:"#808080",grey:"#808080",green:"#008000",greenyellow:"#adff2f",honeydew:"#f0fff0",hotpink:"#ff69b4",indianred:"#cd5c5c",indigo:"#4b0082",ivory:"#fffff0",khaki:"#f0e68c",lavender:"#e6e6fa",lavenderblush:"#fff0f5",lawngreen:"#7cfc00",lemonchiffon:"#fffacd",lightblue:"#add8e6",lightcoral:"#f08080",lightcyan:"#e0ffff",lightgoldenrodyellow:"#fafad2",lightgray:"#d3d3d3",lightgrey:"#d3d3d3",lightgreen:"#90ee90",lightpink:"#ffb6c1",lightsalmon:"#ffa07a",lightseagreen:"#20b2aa",lightskyblue:"#87cefa",lightslategray:"#778899",lightslategrey:"#778899",lightsteelblue:"#b0c4de",lightyellow:"#ffffe0",lime:"#00ff00",limegreen:"#32cd32",linen:"#faf0e6",magenta:"#ff00ff",maroon:"#800000",mediumaquamarine:"#66cdaa",mediumblue:"#0000cd",mediumorchid:"#ba55d3",mediumpurple:"#9370d8",mediumseagreen:"#3cb371",mediumslateblue:"#7b68ee",mediumspringgreen:"#00fa9a",mediumturquoise:"#48d1cc",mediumvioletred:"#c71585",midnightblue:"#191970",mintcream:"#f5fffa",mistyrose:"#ffe4e1",moccasin:"#ffe4b5",navajowhite:"#ffdead",navy:"#000080",oldlace:"#fdf5e6",olive:"#808000",olivedrab:"#6b8e23",orange:"#ffa500",orangered:"#ff4500",orchid:"#da70d6",palegoldenrod:"#eee8aa",palegreen:"#98fb98",paleturquoise:"#afeeee",palevioletred:"#d87093",papayawhip:"#ffefd5",peachpuff:"#ffdab9",peru:"#cd853f",pink:"#ffc0cb",plum:"#dda0dd",powderblue:"#b0e0e6",purple:"#800080",red:"#ff0000",rebeccapurple:"#663399",rosybrown:"#bc8f8f",royalblue:"#4169e1",saddlebrown:"#8b4513",salmon:"#fa8072",sandybrown:"#f4a460",seagreen:"#2e8b57",seashell:"#fff5ee",sienna:"#a0522d",silver:"#c0c0c0",skyblue:"#87ceeb",slateblue:"#6a5acd",slategray:"#708090",slategrey:"#708090",snow:"#fffafa",springgreen:"#00ff7f",steelblue:"#4682b4",tan:"#d2b48c",teal:"#008080",thistle:"#d8bfd8",tomato:"#ff6347",turquoise:"#40e0d0",violet:"#ee82ee",wheat:"#f5deb3",white:"#ffffff",whitesmoke:"#f5f5f5",yellow:"#ffff00",yellowgreen:"#9acd32"},t.colorKeywords={currentColor:"The value of the 'color' property. The computed value of the 'currentColor' keyword is the computed value of the 'color' property. If the 'currentColor' keyword is set on the 'color' property itself, it is treated as 'color:inherit' at parse time.",transparent:"Fully transparent. This keyword can be considered a shorthand for rgba(0,0,0,0) which is its computed value."},t.isColorConstructor=o,t.isColorValue=function(e){if(e.type===n.NodeType.HexColorValue)return!0;if(e.type===n.NodeType.Function)return o(e);if(e.type===n.NodeType.Identifier){if(e.parent&&e.parent.type!==n.NodeType.Term)return!1;var r=e.getText().toLowerCase();if("none"===r)return!1;if(t.colors[r])return!0}return!1};function s(e){return e<48?0:e<=57?e-48:(e<97&&(e+=32),e>=97&&e<=102?e-97+10:0)}function a(e){if("#"!==e[0])return null;switch(e.length){case 4:return{red:17*s(e.charCodeAt(1))/255,green:17*s(e.charCodeAt(2))/255,blue:17*s(e.charCodeAt(3))/255,alpha:1};case 5:return{red:17*s(e.charCodeAt(1))/255,green:17*s(e.charCodeAt(2))/255,blue:17*s(e.charCodeAt(3))/255,alpha:17*s(e.charCodeAt(4))/255};case 7:return{red:(16*s(e.charCodeAt(1))+s(e.charCodeAt(2)))/255,green:(16*s(e.charCodeAt(3))+s(e.charCodeAt(4)))/255,blue:(16*s(e.charCodeAt(5))+s(e.charCodeAt(6)))/255,alpha:1};case 9:return{red:(16*s(e.charCodeAt(1))+s(e.charCodeAt(2)))/255,green:(16*s(e.charCodeAt(3))+s(e.charCodeAt(4)))/255,blue:(16*s(e.charCodeAt(5))+s(e.charCodeAt(6)))/255,alpha:(16*s(e.charCodeAt(7))+s(e.charCodeAt(8)))/255}}return null}function l(e,t,n,r){if(void 0===r&&(r=1),0===t)return{red:n,green:n,blue:n,alpha:r};var i=function(e,t,n){for(;n<0;)n+=6;for(;n>=6;)n-=6;return n<1?(t-e)*n+e:n<3?t:n<4?(t-e)*(4-n)+e:e},o=n<=.5?n*(t+1):n+t-n*t,s=2*n-o;return{red:i(s,o,(e/=60)+2),green:i(s,o,e),blue:i(s,o,e-2),alpha:r}}t.hexDigit=s,t.colorFromHex=a,t.colorFrom256RGB=function(e,t,n,r){return void 0===r&&(r=1),{red:e/255,green:t/255,blue:n/255,alpha:r}},t.colorFromHSL=l,t.hslFromColor=function(e){var t=e.red,n=e.green,r=e.blue,i=e.alpha,o=Math.max(t,n,r),s=Math.min(t,n,r),a=0,l=0,c=(s+o)/2,d=o-s;if(d>0){switch(l=Math.min(c<=.5?d/(2*c):d/(2-2*c),1),o){case t:a=(n-r)/d+(n4)return null;try{var c=4===s.length?i(s[3],1):1;if("rgb"===o||"rgba"===o)return{red:i(s[0],255),green:i(s[1],255),blue:i(s[2],255),alpha:c};if("hsl"===o||"hsla"===o)return l(function(e){var t=e.getText();if(t.match(/^([-+]?[0-9]*\.?[0-9]+)(deg)?$/))return parseFloat(t)%360;throw new Error}(s[0]),i(s[1],100),i(s[2],100),c)}catch(e){return null}}else if(e.type===n.NodeType.Identifier){if(e.parent&&e.parent.type!==n.NodeType.Term)return null;var d=e.parent;if(d&&d.parent&&d.parent.type===n.NodeType.BinaryExpression){var p=d.parent;if(p.parent&&p.parent.type===n.NodeType.ListEntry&&p.parent.key===p)return null}var h=e.getText().toLowerCase();if("none"===h)return null;var u=t.colors[h];if(u)return a(u)}return null}})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/languageFacts/builtinData",["require","exports"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.pageBoxDirectives=t.svgElements=t.html5Tags=t.units=t.basicShapeFunctions=t.transitionTimingFunctions=t.imageFunctions=t.cssWideKeywords=t.geometryBoxKeywords=t.boxKeywords=t.lineWidthKeywords=t.lineStyleKeywords=t.repeatStyleKeywords=t.positionKeywords=void 0,t.positionKeywords={bottom:"Computes to ‘100%’ for the vertical position if one or two values are given, otherwise specifies the bottom edge as the origin for the next offset.",center:"Computes to ‘50%’ (‘left 50%’) for the horizontal position if the horizontal position is not otherwise specified, or ‘50%’ (‘top 50%’) for the vertical position if it is.",left:"Computes to ‘0%’ for the horizontal position if one or two values are given, otherwise specifies the left edge as the origin for the next offset.",right:"Computes to ‘100%’ for the horizontal position if one or two values are given, otherwise specifies the right edge as the origin for the next offset.",top:"Computes to ‘0%’ for the vertical position if one or two values are given, otherwise specifies the top edge as the origin for the next offset."},t.repeatStyleKeywords={"no-repeat":"Placed once and not repeated in this direction.",repeat:"Repeated in this direction as often as needed to cover the background painting area.","repeat-x":"Computes to ‘repeat no-repeat’.","repeat-y":"Computes to ‘no-repeat repeat’.",round:"Repeated as often as will fit within the background positioning area. If it doesn’t fit a whole number of times, it is rescaled so that it does.",space:"Repeated as often as will fit within the background positioning area without being clipped and then the images are spaced out to fill the area."},t.lineStyleKeywords={dashed:"A series of square-ended dashes.",dotted:"A series of round dots.",double:"Two parallel solid lines with some space between them.",groove:"Looks as if it were carved in the canvas.",hidden:"Same as ‘none’, but has different behavior in the border conflict resolution rules for border-collapsed tables.",inset:"Looks as if the content on the inside of the border is sunken into the canvas.",none:"No border. Color and width are ignored.",outset:"Looks as if the content on the inside of the border is coming out of the canvas.",ridge:"Looks as if it were coming out of the canvas.",solid:"A single line segment."},t.lineWidthKeywords=["medium","thick","thin"],t.boxKeywords={"border-box":"The background is painted within (clipped to) the border box.","content-box":"The background is painted within (clipped to) the content box.","padding-box":"The background is painted within (clipped to) the padding box."},t.geometryBoxKeywords={"margin-box":"Uses the margin box as reference box.","fill-box":"Uses the object bounding box as reference box.","stroke-box":"Uses the stroke bounding box as reference box.","view-box":"Uses the nearest SVG viewport as reference box."},t.cssWideKeywords={initial:"Represents the value specified as the property’s initial value.",inherit:"Represents the computed value of the property on the element’s parent.",unset:"Acts as either `inherit` or `initial`, depending on whether the property is inherited or not."},t.imageFunctions={"url()":"Reference an image file by URL","image()":"Provide image fallbacks and annotations.","-webkit-image-set()":"Provide multiple resolutions. Remember to use unprefixed image-set() in addition.","image-set()":"Provide multiple resolutions of an image and const the UA decide which is most appropriate in a given situation.","-moz-element()":"Use an element in the document as an image. Remember to use unprefixed element() in addition.","element()":"Use an element in the document as an image.","cross-fade()":"Indicates the two images to be combined and how far along in the transition the combination is.","-webkit-gradient()":"Deprecated. Use modern linear-gradient() or radial-gradient() instead.","-webkit-linear-gradient()":"Linear gradient. Remember to use unprefixed version in addition.","-moz-linear-gradient()":"Linear gradient. Remember to use unprefixed version in addition.","-o-linear-gradient()":"Linear gradient. Remember to use unprefixed version in addition.","linear-gradient()":"A linear gradient is created by specifying a straight gradient line, and then several colors placed along that line.","-webkit-repeating-linear-gradient()":"Repeating Linear gradient. Remember to use unprefixed version in addition.","-moz-repeating-linear-gradient()":"Repeating Linear gradient. Remember to use unprefixed version in addition.","-o-repeating-linear-gradient()":"Repeating Linear gradient. Remember to use unprefixed version in addition.","repeating-linear-gradient()":"Same as linear-gradient, except the color-stops are repeated infinitely in both directions, with their positions shifted by multiples of the difference between the last specified color-stop’s position and the first specified color-stop’s position.","-webkit-radial-gradient()":"Radial gradient. Remember to use unprefixed version in addition.","-moz-radial-gradient()":"Radial gradient. Remember to use unprefixed version in addition.","radial-gradient()":"Colors emerge from a single point and smoothly spread outward in a circular or elliptical shape.","-webkit-repeating-radial-gradient()":"Repeating radial gradient. Remember to use unprefixed version in addition.","-moz-repeating-radial-gradient()":"Repeating radial gradient. Remember to use unprefixed version in addition.","repeating-radial-gradient()":"Same as radial-gradient, except the color-stops are repeated infinitely in both directions, with their positions shifted by multiples of the difference between the last specified color-stop’s position and the first specified color-stop’s position."},t.transitionTimingFunctions={ease:"Equivalent to cubic-bezier(0.25, 0.1, 0.25, 1.0).","ease-in":"Equivalent to cubic-bezier(0.42, 0, 1.0, 1.0).","ease-in-out":"Equivalent to cubic-bezier(0.42, 0, 0.58, 1.0).","ease-out":"Equivalent to cubic-bezier(0, 0, 0.58, 1.0).",linear:"Equivalent to cubic-bezier(0.0, 0.0, 1.0, 1.0).","step-end":"Equivalent to steps(1, end).","step-start":"Equivalent to steps(1, start).","steps()":"The first parameter specifies the number of intervals in the function. The second parameter, which is optional, is either the value “start” or “end”.","cubic-bezier()":"Specifies a cubic-bezier curve. The four values specify points P1 and P2 of the curve as (x1, y1, x2, y2).","cubic-bezier(0.6, -0.28, 0.735, 0.045)":"Ease-in Back. Overshoots.","cubic-bezier(0.68, -0.55, 0.265, 1.55)":"Ease-in-out Back. Overshoots.","cubic-bezier(0.175, 0.885, 0.32, 1.275)":"Ease-out Back. Overshoots.","cubic-bezier(0.6, 0.04, 0.98, 0.335)":"Ease-in Circular. Based on half circle.","cubic-bezier(0.785, 0.135, 0.15, 0.86)":"Ease-in-out Circular. Based on half circle.","cubic-bezier(0.075, 0.82, 0.165, 1)":"Ease-out Circular. Based on half circle.","cubic-bezier(0.55, 0.055, 0.675, 0.19)":"Ease-in Cubic. Based on power of three.","cubic-bezier(0.645, 0.045, 0.355, 1)":"Ease-in-out Cubic. Based on power of three.","cubic-bezier(0.215, 0.610, 0.355, 1)":"Ease-out Cubic. Based on power of three.","cubic-bezier(0.95, 0.05, 0.795, 0.035)":"Ease-in Exponential. Based on two to the power ten.","cubic-bezier(1, 0, 0, 1)":"Ease-in-out Exponential. Based on two to the power ten.","cubic-bezier(0.19, 1, 0.22, 1)":"Ease-out Exponential. Based on two to the power ten.","cubic-bezier(0.47, 0, 0.745, 0.715)":"Ease-in Sine.","cubic-bezier(0.445, 0.05, 0.55, 0.95)":"Ease-in-out Sine.","cubic-bezier(0.39, 0.575, 0.565, 1)":"Ease-out Sine.","cubic-bezier(0.55, 0.085, 0.68, 0.53)":"Ease-in Quadratic. Based on power of two.","cubic-bezier(0.455, 0.03, 0.515, 0.955)":"Ease-in-out Quadratic. Based on power of two.","cubic-bezier(0.25, 0.46, 0.45, 0.94)":"Ease-out Quadratic. Based on power of two.","cubic-bezier(0.895, 0.03, 0.685, 0.22)":"Ease-in Quartic. Based on power of four.","cubic-bezier(0.77, 0, 0.175, 1)":"Ease-in-out Quartic. Based on power of four.","cubic-bezier(0.165, 0.84, 0.44, 1)":"Ease-out Quartic. Based on power of four.","cubic-bezier(0.755, 0.05, 0.855, 0.06)":"Ease-in Quintic. Based on power of five.","cubic-bezier(0.86, 0, 0.07, 1)":"Ease-in-out Quintic. Based on power of five.","cubic-bezier(0.23, 1, 0.320, 1)":"Ease-out Quintic. Based on power of five."},t.basicShapeFunctions={"circle()":"Defines a circle.","ellipse()":"Defines an ellipse.","inset()":"Defines an inset rectangle.","polygon()":"Defines a polygon."},t.units={length:["em","rem","ex","px","cm","mm","in","pt","pc","ch","vw","vh","vmin","vmax"],angle:["deg","rad","grad","turn"],time:["ms","s"],frequency:["Hz","kHz"],resolution:["dpi","dpcm","dppx"],percentage:["%","fr"]},t.html5Tags=["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","dialog","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","menu","menuitem","meta","meter","nav","noscript","object","ol","optgroup","option","output","p","param","picture","pre","progress","q","rb","rp","rt","rtc","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","title","tr","track","u","ul","const","video","wbr"],t.svgElements=["circle","clipPath","cursor","defs","desc","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","hatch","hatchpath","image","line","linearGradient","marker","mask","mesh","meshpatch","meshrow","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","set","solidcolor","stop","svg","switch","symbol","text","textPath","tspan","use","view"],t.pageBoxDirectives=["@bottom-center","@bottom-left","@bottom-left-corner","@bottom-right","@bottom-right-corner","@left-bottom","@left-middle","@left-top","@right-bottom","@right-middle","@right-top","@top-center","@top-left","@top-left-corner","@top-right","@top-right-corner"]}));var __createBinding=this&&this.__createBinding||(Object.create?function(e,t,n,r){void 0===r&&(r=n),Object.defineProperty(e,r,{enumerable:!0,get:function(){return t[n]}})}:function(e,t,n,r){void 0===r&&(r=n),e[r]=t[n]}),__exportStar=this&&this.__exportStar||function(e,t){for(var n in e)"default"===n||Object.prototype.hasOwnProperty.call(t,n)||__createBinding(t,e,n)};!function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/languageFacts/facts",["require","exports","./entry","./colors","./builtinData"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),__exportStar(e("./entry"),t),__exportStar(e("./colors"),t),__exportStar(e("./builtinData"),t)})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/utils/objects",["require","exports"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.isDefined=t.values=void 0,t.values=function(e){return Object.keys(e).map((function(t){return e[t]}))},t.isDefined=function(e){return void 0!==e}})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/parser/cssParser",["require","exports","./cssScanner","./cssNodes","./cssErrors","../languageFacts/facts","../utils/objects"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Parser=void 0;var n=e("./cssScanner"),r=e("./cssNodes"),i=e("./cssErrors"),o=e("../languageFacts/facts"),s=e("../utils/objects"),a=function(){function e(e){void 0===e&&(e=new n.Scanner),this.keyframeRegex=/^@(\-(webkit|ms|moz|o)\-)?keyframes$/i,this.scanner=e,this.token={type:n.TokenType.EOF,offset:-1,len:0,text:""},this.prevToken=void 0}return e.prototype.peekIdent=function(e){return n.TokenType.Ident===this.token.type&&e.length===this.token.text.length&&e===this.token.text.toLowerCase()},e.prototype.peekKeyword=function(e){return n.TokenType.AtKeyword===this.token.type&&e.length===this.token.text.length&&e===this.token.text.toLowerCase()},e.prototype.peekDelim=function(e){return n.TokenType.Delim===this.token.type&&e===this.token.text},e.prototype.peek=function(e){return e===this.token.type},e.prototype.peekOne=function(e){return-1!==e.indexOf(this.token.type)},e.prototype.peekRegExp=function(e,t){return e===this.token.type&&t.test(this.token.text)},e.prototype.hasWhitespace=function(){return!!this.prevToken&&this.prevToken.offset+this.prevToken.len!==this.token.offset},e.prototype.consumeToken=function(){this.prevToken=this.token,this.token=this.scanner.scan()},e.prototype.mark=function(){return{prev:this.prevToken,curr:this.token,pos:this.scanner.pos()}},e.prototype.restoreAtMark=function(e){this.prevToken=e.prev,this.token=e.curr,this.scanner.goBackTo(e.pos)},e.prototype.try=function(e){var t=this.mark(),n=e();return n||(this.restoreAtMark(t),null)},e.prototype.acceptOneKeyword=function(e){if(n.TokenType.AtKeyword===this.token.type)for(var t=0,r=e;te.offset?o-e.offset:0}return e},e.prototype.markError=function(e,t,n,i){this.token!==this.lastErrorToken&&(e.addIssue(new r.Marker(e,t,r.Level.Error,void 0,this.token.offset,this.token.len)),this.lastErrorToken=this.token),(n||i)&&this.resync(n,i)},e.prototype.parseStylesheet=function(e){var t=e.version,n=e.getText();return this.internalParse(n,this._parseStylesheet,(function(r,i){if(e.version!==t)throw new Error("Underlying model has changed, AST is no longer valid");return n.substr(r,i)}))},e.prototype.internalParse=function(e,t,n){this.scanner.setSource(e),this.token=this.scanner.scan();var r=t.bind(this)();return r&&(r.textProvider=n||function(t,n){return e.substr(t,n)}),r},e.prototype._parseStylesheet=function(){for(var e=this.create(r.Stylesheet);e.addChild(this._parseStylesheetStart()););var t=!1;do{var o=!1;do{o=!1;var s=this._parseStylesheetStatement();for(s&&(e.addChild(s),o=!0,t=!1,this.peek(n.TokenType.EOF)||!this._needsSemicolonAfter(s)||this.accept(n.TokenType.SemiColon)||this.markError(e,i.ParseError.SemiColonExpected));this.accept(n.TokenType.SemiColon)||this.accept(n.TokenType.CDO)||this.accept(n.TokenType.CDC);)o=!0,t=!1}while(o);if(this.peek(n.TokenType.EOF))break;t||(this.peek(n.TokenType.AtKeyword)?this.markError(e,i.ParseError.UnknownAtRule):this.markError(e,i.ParseError.RuleOrSelectorExpected),t=!0),this.consumeToken()}while(!this.peek(n.TokenType.EOF));return this.finish(e)},e.prototype._parseStylesheetStart=function(){return this._parseCharset()},e.prototype._parseStylesheetStatement=function(e){return void 0===e&&(e=!1),this.peek(n.TokenType.AtKeyword)?this._parseStylesheetAtStatement(e):this._parseRuleset(e)},e.prototype._parseStylesheetAtStatement=function(e){return void 0===e&&(e=!1),this._parseImport()||this._parseMedia(e)||this._parsePage()||this._parseFontFace()||this._parseKeyframe()||this._parseSupports(e)||this._parseViewPort()||this._parseNamespace()||this._parseDocument()||this._parseUnknownAtRule()},e.prototype._tryParseRuleset=function(e){var t=this.mark();if(this._parseSelector(e)){for(;this.accept(n.TokenType.Comma)&&this._parseSelector(e););if(this.accept(n.TokenType.CurlyL))return this.restoreAtMark(t),this._parseRuleset(e)}return this.restoreAtMark(t),null},e.prototype._parseRuleset=function(e){void 0===e&&(e=!1);var t=this.create(r.RuleSet),o=t.getSelectors();if(!o.addChild(this._parseSelector(e)))return null;for(;this.accept(n.TokenType.Comma);)if(!o.addChild(this._parseSelector(e)))return this.finish(t,i.ParseError.SelectorExpected);return this._parseBody(t,this._parseRuleSetDeclaration.bind(this))},e.prototype._parseRuleSetDeclarationAtStatement=function(){return this._parseAtApply()||this._parseUnknownAtRule()},e.prototype._parseRuleSetDeclaration=function(){return this.peek(n.TokenType.AtKeyword)?this._parseRuleSetDeclarationAtStatement():this._parseDeclaration()},e.prototype._parseAtApply=function(){if(!this.peekKeyword("@apply"))return null;var e=this.create(r.AtApplyRule);return this.consumeToken(),e.setIdentifier(this._parseIdent([r.ReferenceType.Variable]))?this.finish(e):this.finish(e,i.ParseError.IdentifierExpected)},e.prototype._needsSemicolonAfter=function(e){switch(e.type){case r.NodeType.Keyframe:case r.NodeType.ViewPort:case r.NodeType.Media:case r.NodeType.Ruleset:case r.NodeType.Namespace:case r.NodeType.If:case r.NodeType.For:case r.NodeType.Each:case r.NodeType.While:case r.NodeType.MixinDeclaration:case r.NodeType.FunctionDeclaration:case r.NodeType.MixinContentDeclaration:return!1;case r.NodeType.ExtendsReference:case r.NodeType.MixinContentReference:case r.NodeType.ReturnStatement:case r.NodeType.MediaQuery:case r.NodeType.Debug:case r.NodeType.Import:case r.NodeType.AtApplyRule:case r.NodeType.CustomPropertyDeclaration:return!0;case r.NodeType.VariableDeclaration:return e.needsSemicolon;case r.NodeType.MixinReference:return!e.getContent();case r.NodeType.Declaration:return!e.getNestedProperties()}return!1},e.prototype._parseDeclarations=function(e){var t=this.create(r.Declarations);if(!this.accept(n.TokenType.CurlyL))return null;for(var o=e();t.addChild(o)&&!this.peek(n.TokenType.CurlyR);){if(this._needsSemicolonAfter(o)&&!this.accept(n.TokenType.SemiColon))return this.finish(t,i.ParseError.SemiColonExpected,[n.TokenType.SemiColon,n.TokenType.CurlyR]);for(o&&this.prevToken&&this.prevToken.type===n.TokenType.SemiColon&&(o.semicolonPosition=this.prevToken.offset);this.accept(n.TokenType.SemiColon););o=e()}return this.accept(n.TokenType.CurlyR)?this.finish(t):this.finish(t,i.ParseError.RightCurlyExpected,[n.TokenType.CurlyR,n.TokenType.SemiColon])},e.prototype._parseBody=function(e,t){return e.setDeclarations(this._parseDeclarations(t))?this.finish(e):this.finish(e,i.ParseError.LeftCurlyExpected,[n.TokenType.CurlyR,n.TokenType.SemiColon])},e.prototype._parseSelector=function(e){var t=this.create(r.Selector),n=!1;for(e&&(n=t.addChild(this._parseCombinator()));t.addChild(this._parseSimpleSelector());)n=!0,t.addChild(this._parseCombinator());return n?this.finish(t):null},e.prototype._parseDeclaration=function(e){var t=this._tryParseCustomPropertyDeclaration(e);if(t)return t;var o=this.create(r.Declaration);return o.setProperty(this._parseProperty())?this.accept(n.TokenType.Colon)?(this.prevToken&&(o.colonPosition=this.prevToken.offset),o.setValue(this._parseExpr())?(o.addChild(this._parsePrio()),this.peek(n.TokenType.SemiColon)&&(o.semicolonPosition=this.token.offset),this.finish(o)):this.finish(o,i.ParseError.PropertyValueExpected)):this.finish(o,i.ParseError.ColonExpected,[n.TokenType.Colon],e||[n.TokenType.SemiColon]):null},e.prototype._tryParseCustomPropertyDeclaration=function(e){if(!this.peekRegExp(n.TokenType.Ident,/^--/))return null;var t=this.create(r.CustomPropertyDeclaration);if(!t.setProperty(this._parseProperty()))return null;if(!this.accept(n.TokenType.Colon))return this.finish(t,i.ParseError.ColonExpected,[n.TokenType.Colon]);this.prevToken&&(t.colonPosition=this.prevToken.offset);var o=this.mark();if(this.peek(n.TokenType.CurlyL)){var a=this.create(r.CustomPropertySet),l=this._parseDeclarations(this._parseRuleSetDeclaration.bind(this));if(a.setDeclarations(l)&&!l.isErroneous(!0)&&(a.addChild(this._parsePrio()),this.peek(n.TokenType.SemiColon)))return this.finish(a),t.setPropertySet(a),t.semicolonPosition=this.token.offset,this.finish(t);this.restoreAtMark(o)}var c=this._parseExpr();return c&&!c.isErroneous(!0)&&(this._parsePrio(),this.peekOne(e||[n.TokenType.SemiColon]))?(t.setValue(c),t.semicolonPosition=this.token.offset,this.finish(t)):(this.restoreAtMark(o),t.addChild(this._parseCustomPropertyValue(e)),t.addChild(this._parsePrio()),s.isDefined(t.colonPosition)&&this.token.offset===t.colonPosition+1?this.finish(t,i.ParseError.PropertyValueExpected):this.finish(t))},e.prototype._parseCustomPropertyValue=function(e){var t=this;void 0===e&&(e=[n.TokenType.CurlyR]);var o=this.create(r.Node),s=function(){return 0===l&&0===c&&0===d},a=function(){return-1!==e.indexOf(t.token.type)},l=0,c=0,d=0;e:for(;;){switch(this.token.type){case n.TokenType.SemiColon:case n.TokenType.Exclamation:if(s())break e;break;case n.TokenType.CurlyL:l++;break;case n.TokenType.CurlyR:if(--l<0){if(a()&&0===c&&0===d)break e;return this.finish(o,i.ParseError.LeftCurlyExpected)}break;case n.TokenType.ParenthesisL:c++;break;case n.TokenType.ParenthesisR:if(--c<0){if(a()&&0===d&&0===l)break e;return this.finish(o,i.ParseError.LeftParenthesisExpected)}break;case n.TokenType.BracketL:d++;break;case n.TokenType.BracketR:if(--d<0)return this.finish(o,i.ParseError.LeftSquareBracketExpected);break;case n.TokenType.BadString:break e;case n.TokenType.EOF:var p=i.ParseError.RightCurlyExpected;return d>0?p=i.ParseError.RightSquareBracketExpected:c>0&&(p=i.ParseError.RightParenthesisExpected),this.finish(o,p)}this.consumeToken()}return this.finish(o)},e.prototype._tryToParseDeclaration=function(e){var t=this.mark();return this._parseProperty()&&this.accept(n.TokenType.Colon)?(this.restoreAtMark(t),this._parseDeclaration(e)):(this.restoreAtMark(t),null)},e.prototype._parseProperty=function(){var e=this.create(r.Property),t=this.mark();return(this.acceptDelim("*")||this.acceptDelim("_"))&&this.hasWhitespace()?(this.restoreAtMark(t),null):e.setIdentifier(this._parsePropertyIdentifier())?this.finish(e):null},e.prototype._parsePropertyIdentifier=function(){return this._parseIdent()},e.prototype._parseCharset=function(){if(!this.peek(n.TokenType.Charset))return null;var e=this.create(r.Node);return this.consumeToken(),this.accept(n.TokenType.String)?this.accept(n.TokenType.SemiColon)?this.finish(e):this.finish(e,i.ParseError.SemiColonExpected):this.finish(e,i.ParseError.IdentifierExpected)},e.prototype._parseImport=function(){if(!this.peekKeyword("@import"))return null;var e=this.create(r.Import);return this.consumeToken(),e.addChild(this._parseURILiteral())||e.addChild(this._parseStringLiteral())?(this.peek(n.TokenType.SemiColon)||this.peek(n.TokenType.EOF)||e.setMedialist(this._parseMediaQueryList()),this.finish(e)):this.finish(e,i.ParseError.URIOrStringExpected)},e.prototype._parseNamespace=function(){if(!this.peekKeyword("@namespace"))return null;var e=this.create(r.Namespace);return this.consumeToken(),e.addChild(this._parseURILiteral())||(e.addChild(this._parseIdent()),e.addChild(this._parseURILiteral())||e.addChild(this._parseStringLiteral()))?this.accept(n.TokenType.SemiColon)?this.finish(e):this.finish(e,i.ParseError.SemiColonExpected):this.finish(e,i.ParseError.URIExpected,[n.TokenType.SemiColon])},e.prototype._parseFontFace=function(){if(!this.peekKeyword("@font-face"))return null;var e=this.create(r.FontFace);return this.consumeToken(),this._parseBody(e,this._parseRuleSetDeclaration.bind(this))},e.prototype._parseViewPort=function(){if(!this.peekKeyword("@-ms-viewport")&&!this.peekKeyword("@-o-viewport")&&!this.peekKeyword("@viewport"))return null;var e=this.create(r.ViewPort);return this.consumeToken(),this._parseBody(e,this._parseRuleSetDeclaration.bind(this))},e.prototype._parseKeyframe=function(){if(!this.peekRegExp(n.TokenType.AtKeyword,this.keyframeRegex))return null;var e=this.create(r.Keyframe),t=this.create(r.Node);return this.consumeToken(),e.setKeyword(this.finish(t)),t.matches("@-ms-keyframes")&&this.markError(t,i.ParseError.UnknownKeyword),e.setIdentifier(this._parseKeyframeIdent())?this._parseBody(e,this._parseKeyframeSelector.bind(this)):this.finish(e,i.ParseError.IdentifierExpected,[n.TokenType.CurlyR])},e.prototype._parseKeyframeIdent=function(){return this._parseIdent([r.ReferenceType.Keyframe])},e.prototype._parseKeyframeSelector=function(){var e=this.create(r.KeyframeSelector);if(!e.addChild(this._parseIdent())&&!this.accept(n.TokenType.Percentage))return null;for(;this.accept(n.TokenType.Comma);)if(!e.addChild(this._parseIdent())&&!this.accept(n.TokenType.Percentage))return this.finish(e,i.ParseError.PercentageExpected);return this._parseBody(e,this._parseRuleSetDeclaration.bind(this))},e.prototype._tryParseKeyframeSelector=function(){var e=this.create(r.KeyframeSelector),t=this.mark();if(!e.addChild(this._parseIdent())&&!this.accept(n.TokenType.Percentage))return null;for(;this.accept(n.TokenType.Comma);)if(!e.addChild(this._parseIdent())&&!this.accept(n.TokenType.Percentage))return this.restoreAtMark(t),null;return this.peek(n.TokenType.CurlyL)?this._parseBody(e,this._parseRuleSetDeclaration.bind(this)):(this.restoreAtMark(t),null)},e.prototype._parseSupports=function(e){if(void 0===e&&(e=!1),!this.peekKeyword("@supports"))return null;var t=this.create(r.Supports);return this.consumeToken(),t.addChild(this._parseSupportsCondition()),this._parseBody(t,this._parseSupportsDeclaration.bind(this,e))},e.prototype._parseSupportsDeclaration=function(e){return void 0===e&&(e=!1),e?this._tryParseRuleset(!0)||this._tryToParseDeclaration()||this._parseStylesheetStatement(!0):this._parseStylesheetStatement(!1)},e.prototype._parseSupportsCondition=function(){var e=this.create(r.SupportsCondition);if(this.acceptIdent("not"))e.addChild(this._parseSupportsConditionInParens());else if(e.addChild(this._parseSupportsConditionInParens()),this.peekRegExp(n.TokenType.Ident,/^(and|or)$/i))for(var t=this.token.text.toLowerCase();this.acceptIdent(t);)e.addChild(this._parseSupportsConditionInParens());return this.finish(e)},e.prototype._parseSupportsConditionInParens=function(){var e=this.create(r.SupportsCondition);if(this.accept(n.TokenType.ParenthesisL))return this.prevToken&&(e.lParent=this.prevToken.offset),e.addChild(this._tryToParseDeclaration([n.TokenType.ParenthesisR]))||this._parseSupportsCondition()?this.accept(n.TokenType.ParenthesisR)?(this.prevToken&&(e.rParent=this.prevToken.offset),this.finish(e)):this.finish(e,i.ParseError.RightParenthesisExpected,[n.TokenType.ParenthesisR],[]):this.finish(e,i.ParseError.ConditionExpected);if(this.peek(n.TokenType.Ident)){var t=this.mark();if(this.consumeToken(),!this.hasWhitespace()&&this.accept(n.TokenType.ParenthesisL)){for(var o=1;this.token.type!==n.TokenType.EOF&&0!==o;)this.token.type===n.TokenType.ParenthesisL?o++:this.token.type===n.TokenType.ParenthesisR&&o--,this.consumeToken();return this.finish(e)}this.restoreAtMark(t)}return this.finish(e,i.ParseError.LeftParenthesisExpected,[],[n.TokenType.ParenthesisL])},e.prototype._parseMediaDeclaration=function(e){return void 0===e&&(e=!1),e?this._tryParseRuleset(!0)||this._tryToParseDeclaration()||this._parseStylesheetStatement(!0):this._parseStylesheetStatement(!1)},e.prototype._parseMedia=function(e){if(void 0===e&&(e=!1),!this.peekKeyword("@media"))return null;var t=this.create(r.Media);return this.consumeToken(),t.addChild(this._parseMediaQueryList())?this._parseBody(t,this._parseMediaDeclaration.bind(this,e)):this.finish(t,i.ParseError.MediaQueryExpected)},e.prototype._parseMediaQueryList=function(){var e=this.create(r.Medialist);if(!e.addChild(this._parseMediaQuery([n.TokenType.CurlyL])))return this.finish(e,i.ParseError.MediaQueryExpected);for(;this.accept(n.TokenType.Comma);)if(!e.addChild(this._parseMediaQuery([n.TokenType.CurlyL])))return this.finish(e,i.ParseError.MediaQueryExpected);return this.finish(e)},e.prototype._parseMediaQuery=function(e){var t=this.create(r.MediaQuery),o=!0,s=!1;if(!this.peek(n.TokenType.ParenthesisL)){if(this.acceptIdent("only")||this.acceptIdent("not"),!t.addChild(this._parseIdent()))return null;s=!0,o=this.acceptIdent("and")}for(;o;)if(t.addChild(this._parseMediaContentStart()))o=this.acceptIdent("and");else{if(!this.accept(n.TokenType.ParenthesisL))return s?this.finish(t,i.ParseError.LeftParenthesisExpected,[],e):null;if(!t.addChild(this._parseMediaFeatureName()))return this.finish(t,i.ParseError.IdentifierExpected,[],e);if(this.accept(n.TokenType.Colon)&&!t.addChild(this._parseExpr()))return this.finish(t,i.ParseError.TermExpected,[],e);if(!this.accept(n.TokenType.ParenthesisR))return this.finish(t,i.ParseError.RightParenthesisExpected,[],e);o=this.acceptIdent("and")}return this.finish(t)},e.prototype._parseMediaContentStart=function(){return null},e.prototype._parseMediaFeatureName=function(){return this._parseIdent()},e.prototype._parseMedium=function(){var e=this.create(r.Node);return e.addChild(this._parseIdent())?this.finish(e):null},e.prototype._parsePageDeclaration=function(){return this._parsePageMarginBox()||this._parseRuleSetDeclaration()},e.prototype._parsePage=function(){if(!this.peekKeyword("@page"))return null;var e=this.create(r.Page);if(this.consumeToken(),e.addChild(this._parsePageSelector()))for(;this.accept(n.TokenType.Comma);)if(!e.addChild(this._parsePageSelector()))return this.finish(e,i.ParseError.IdentifierExpected);return this._parseBody(e,this._parsePageDeclaration.bind(this))},e.prototype._parsePageMarginBox=function(){if(!this.peek(n.TokenType.AtKeyword))return null;var e=this.create(r.PageBoxMarginBox);return this.acceptOneKeyword(o.pageBoxDirectives)||this.markError(e,i.ParseError.UnknownAtRule,[],[n.TokenType.CurlyL]),this._parseBody(e,this._parseRuleSetDeclaration.bind(this))},e.prototype._parsePageSelector=function(){if(!this.peek(n.TokenType.Ident)&&!this.peek(n.TokenType.Colon))return null;var e=this.create(r.Node);return e.addChild(this._parseIdent()),this.accept(n.TokenType.Colon)&&!e.addChild(this._parseIdent())?this.finish(e,i.ParseError.IdentifierExpected):this.finish(e)},e.prototype._parseDocument=function(){if(!this.peekKeyword("@-moz-document"))return null;var e=this.create(r.Document);return this.consumeToken(),this.resync([],[n.TokenType.CurlyL]),this._parseBody(e,this._parseStylesheetStatement.bind(this))},e.prototype._parseUnknownAtRule=function(){if(!this.peek(n.TokenType.AtKeyword))return null;var e=this.create(r.UnknownAtRule);e.addChild(this._parseUnknownAtRuleName());var t=0,o=0,s=0,a=0;e:for(;;){switch(this.token.type){case n.TokenType.SemiColon:if(0===o&&0===s&&0===a)break e;break;case n.TokenType.EOF:return o>0?this.finish(e,i.ParseError.RightCurlyExpected):a>0?this.finish(e,i.ParseError.RightSquareBracketExpected):s>0?this.finish(e,i.ParseError.RightParenthesisExpected):this.finish(e);case n.TokenType.CurlyL:t++,o++;break;case n.TokenType.CurlyR:if(o--,t>0&&0===o){if(this.consumeToken(),a>0)return this.finish(e,i.ParseError.RightSquareBracketExpected);if(s>0)return this.finish(e,i.ParseError.RightParenthesisExpected);break e}if(o<0){if(0===s&&0===a)break e;return this.finish(e,i.ParseError.LeftCurlyExpected)}break;case n.TokenType.ParenthesisL:s++;break;case n.TokenType.ParenthesisR:if(--s<0)return this.finish(e,i.ParseError.LeftParenthesisExpected);break;case n.TokenType.BracketL:a++;break;case n.TokenType.BracketR:if(--a<0)return this.finish(e,i.ParseError.LeftSquareBracketExpected)}this.consumeToken()}return e},e.prototype._parseUnknownAtRuleName=function(){var e=this.create(r.Node);return this.accept(n.TokenType.AtKeyword)?this.finish(e):e},e.prototype._parseOperator=function(){if(this.peekDelim("/")||this.peekDelim("*")||this.peekDelim("+")||this.peekDelim("-")||this.peek(n.TokenType.Dashmatch)||this.peek(n.TokenType.Includes)||this.peek(n.TokenType.SubstringOperator)||this.peek(n.TokenType.PrefixOperator)||this.peek(n.TokenType.SuffixOperator)||this.peekDelim("=")){var e=this.createNode(r.NodeType.Operator);return this.consumeToken(),this.finish(e)}return null},e.prototype._parseUnaryOperator=function(){if(!this.peekDelim("+")&&!this.peekDelim("-"))return null;var e=this.create(r.Node);return this.consumeToken(),this.finish(e)},e.prototype._parseCombinator=function(){if(this.peekDelim(">")){var e=this.create(r.Node);this.consumeToken();var t=this.mark();if(!this.hasWhitespace()&&this.acceptDelim(">")){if(!this.hasWhitespace()&&this.acceptDelim(">"))return e.type=r.NodeType.SelectorCombinatorShadowPiercingDescendant,this.finish(e);this.restoreAtMark(t)}return e.type=r.NodeType.SelectorCombinatorParent,this.finish(e)}if(this.peekDelim("+")){e=this.create(r.Node);return this.consumeToken(),e.type=r.NodeType.SelectorCombinatorSibling,this.finish(e)}if(this.peekDelim("~")){e=this.create(r.Node);return this.consumeToken(),e.type=r.NodeType.SelectorCombinatorAllSiblings,this.finish(e)}if(this.peekDelim("/")){e=this.create(r.Node);this.consumeToken();t=this.mark();if(!this.hasWhitespace()&&this.acceptIdent("deep")&&!this.hasWhitespace()&&this.acceptDelim("/"))return e.type=r.NodeType.SelectorCombinatorShadowPiercingDescendant,this.finish(e);this.restoreAtMark(t)}return null},e.prototype._parseSimpleSelector=function(){var e=this.create(r.SimpleSelector),t=0;for(e.addChild(this._parseElementName())&&t++;(0===t||!this.hasWhitespace())&&e.addChild(this._parseSimpleSelectorBody());)t++;return t>0?this.finish(e):null},e.prototype._parseSimpleSelectorBody=function(){return this._parsePseudo()||this._parseHash()||this._parseClass()||this._parseAttrib()},e.prototype._parseSelectorIdent=function(){return this._parseIdent()},e.prototype._parseHash=function(){if(!this.peek(n.TokenType.Hash)&&!this.peekDelim("#"))return null;var e=this.createNode(r.NodeType.IdentifierSelector);if(this.acceptDelim("#")){if(this.hasWhitespace()||!e.addChild(this._parseSelectorIdent()))return this.finish(e,i.ParseError.IdentifierExpected)}else this.consumeToken();return this.finish(e)},e.prototype._parseClass=function(){if(!this.peekDelim("."))return null;var e=this.createNode(r.NodeType.ClassSelector);return this.consumeToken(),this.hasWhitespace()||!e.addChild(this._parseSelectorIdent())?this.finish(e,i.ParseError.IdentifierExpected):this.finish(e)},e.prototype._parseElementName=function(){var e=this.mark(),t=this.createNode(r.NodeType.ElementNameSelector);return t.addChild(this._parseNamespacePrefix()),t.addChild(this._parseSelectorIdent())||this.acceptDelim("*")?this.finish(t):(this.restoreAtMark(e),null)},e.prototype._parseNamespacePrefix=function(){var e=this.mark(),t=this.createNode(r.NodeType.NamespacePrefix);return!t.addChild(this._parseIdent())&&this.acceptDelim("*"),this.acceptDelim("|")?this.finish(t):(this.restoreAtMark(e),null)},e.prototype._parseAttrib=function(){if(!this.peek(n.TokenType.BracketL))return null;var e=this.create(r.AttributeSelector);return this.consumeToken(),e.setNamespacePrefix(this._parseNamespacePrefix()),e.setIdentifier(this._parseIdent())?(e.setOperator(this._parseOperator())&&(e.setValue(this._parseBinaryExpr()),this.acceptIdent("i")),this.accept(n.TokenType.BracketR)?this.finish(e):this.finish(e,i.ParseError.RightSquareBracketExpected)):this.finish(e,i.ParseError.IdentifierExpected)},e.prototype._parsePseudo=function(){var e=this,t=this._tryParsePseudoIdentifier();if(t){if(!this.hasWhitespace()&&this.accept(n.TokenType.ParenthesisL)){if(t.addChild(this.try((function(){var t=e.create(r.Node);if(!t.addChild(e._parseSelector(!1)))return null;for(;e.accept(n.TokenType.Comma)&&t.addChild(e._parseSelector(!1)););return e.peek(n.TokenType.ParenthesisR)?e.finish(t):null}))||this._parseBinaryExpr()),!this.accept(n.TokenType.ParenthesisR))return this.finish(t,i.ParseError.RightParenthesisExpected)}return this.finish(t)}return null},e.prototype._tryParsePseudoIdentifier=function(){if(!this.peek(n.TokenType.Colon))return null;var e=this.mark(),t=this.createNode(r.NodeType.PseudoSelector);return this.consumeToken(),this.hasWhitespace()?(this.restoreAtMark(e),null):(this.accept(n.TokenType.Colon),this.hasWhitespace()||!t.addChild(this._parseIdent())?this.finish(t,i.ParseError.IdentifierExpected):this.finish(t))},e.prototype._tryParsePrio=function(){var e=this.mark(),t=this._parsePrio();return t||(this.restoreAtMark(e),null)},e.prototype._parsePrio=function(){if(!this.peek(n.TokenType.Exclamation))return null;var e=this.createNode(r.NodeType.Prio);return this.accept(n.TokenType.Exclamation)&&this.acceptIdent("important")?this.finish(e):null},e.prototype._parseExpr=function(e){void 0===e&&(e=!1);var t=this.create(r.Expression);if(!t.addChild(this._parseBinaryExpr()))return null;for(;;){if(this.peek(n.TokenType.Comma)){if(e)return this.finish(t);this.consumeToken()}if(!t.addChild(this._parseBinaryExpr()))break}return this.finish(t)},e.prototype._parseNamedLine=function(){if(!this.peek(n.TokenType.BracketL))return null;var e=this.createNode(r.NodeType.GridLine);for(this.consumeToken();e.addChild(this._parseIdent()););return this.accept(n.TokenType.BracketR)?this.finish(e):this.finish(e,i.ParseError.RightSquareBracketExpected)},e.prototype._parseBinaryExpr=function(e,t){var n=this.create(r.BinaryExpression);if(!n.setLeft(e||this._parseTerm()))return null;if(!n.setOperator(t||this._parseOperator()))return this.finish(n);if(!n.setRight(this._parseTerm()))return this.finish(n,i.ParseError.TermExpected);n=this.finish(n);var o=this._parseOperator();return o&&(n=this._parseBinaryExpr(n,o)),this.finish(n)},e.prototype._parseTerm=function(){var e=this.create(r.Term);return e.setOperator(this._parseUnaryOperator()),e.setExpression(this._parseTermExpression())?this.finish(e):null},e.prototype._parseTermExpression=function(){return this._parseURILiteral()||this._parseFunction()||this._parseIdent()||this._parseStringLiteral()||this._parseNumeric()||this._parseHexColor()||this._parseOperation()||this._parseNamedLine()},e.prototype._parseOperation=function(){if(!this.peek(n.TokenType.ParenthesisL))return null;var e=this.create(r.Node);return this.consumeToken(),e.addChild(this._parseExpr()),this.accept(n.TokenType.ParenthesisR)?this.finish(e):this.finish(e,i.ParseError.RightParenthesisExpected)},e.prototype._parseNumeric=function(){if(this.peek(n.TokenType.Num)||this.peek(n.TokenType.Percentage)||this.peek(n.TokenType.Resolution)||this.peek(n.TokenType.Length)||this.peek(n.TokenType.EMS)||this.peek(n.TokenType.EXS)||this.peek(n.TokenType.Angle)||this.peek(n.TokenType.Time)||this.peek(n.TokenType.Dimension)||this.peek(n.TokenType.Freq)){var e=this.create(r.NumericValue);return this.consumeToken(),this.finish(e)}return null},e.prototype._parseStringLiteral=function(){if(!this.peek(n.TokenType.String)&&!this.peek(n.TokenType.BadString))return null;var e=this.createNode(r.NodeType.StringLiteral);return this.consumeToken(),this.finish(e)},e.prototype._parseURILiteral=function(){if(!this.peekRegExp(n.TokenType.Ident,/^url(-prefix)?$/i))return null;var e=this.mark(),t=this.createNode(r.NodeType.URILiteral);return this.accept(n.TokenType.Ident),this.hasWhitespace()||!this.peek(n.TokenType.ParenthesisL)?(this.restoreAtMark(e),null):(this.scanner.inURL=!0,this.consumeToken(),t.addChild(this._parseURLArgument()),this.scanner.inURL=!1,this.accept(n.TokenType.ParenthesisR)?this.finish(t):this.finish(t,i.ParseError.RightParenthesisExpected))},e.prototype._parseURLArgument=function(){var e=this.create(r.Node);return this.accept(n.TokenType.String)||this.accept(n.TokenType.BadString)||this.acceptUnquotedString()?this.finish(e):null},e.prototype._parseIdent=function(e){if(!this.peek(n.TokenType.Ident))return null;var t=this.create(r.Identifier);return e&&(t.referenceTypes=e),t.isCustomProperty=this.peekRegExp(n.TokenType.Ident,/^--/),this.consumeToken(),this.finish(t)},e.prototype._parseFunction=function(){var e=this.mark(),t=this.create(r.Function);if(!t.setIdentifier(this._parseFunctionIdentifier()))return null;if(this.hasWhitespace()||!this.accept(n.TokenType.ParenthesisL))return this.restoreAtMark(e),null;if(t.getArguments().addChild(this._parseFunctionArgument()))for(;this.accept(n.TokenType.Comma)&&!this.peek(n.TokenType.ParenthesisR);)t.getArguments().addChild(this._parseFunctionArgument())||this.markError(t,i.ParseError.ExpressionExpected);return this.accept(n.TokenType.ParenthesisR)?this.finish(t):this.finish(t,i.ParseError.RightParenthesisExpected)},e.prototype._parseFunctionIdentifier=function(){if(!this.peek(n.TokenType.Ident))return null;var e=this.create(r.Identifier);if(e.referenceTypes=[r.ReferenceType.Function],this.acceptIdent("progid")){if(this.accept(n.TokenType.Colon))for(;this.accept(n.TokenType.Ident)&&this.acceptDelim("."););return this.finish(e)}return this.consumeToken(),this.finish(e)},e.prototype._parseFunctionArgument=function(){var e=this.create(r.FunctionArgument);return e.setValue(this._parseExpr(!0))?this.finish(e):null},e.prototype._parseHexColor=function(){if(this.peekRegExp(n.TokenType.Hash,/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/g)){var e=this.create(r.HexColorValue);return this.consumeToken(),this.finish(e)}return null},e}();t.Parser=a})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/utils/arrays",["require","exports"],e)}((function(e,t){"use strict";function n(e,t){return-1!==e.indexOf(t)}Object.defineProperty(t,"__esModule",{value:!0}),t.union=t.includes=t.findFirst=void 0,t.findFirst=function(e,t){var n=0,r=e.length;if(0===r)return 0;for(;ne+t||this.offset===e&&this.length===t?this.findInScope(e,t):null},e.prototype.findInScope=function(e,t){void 0===t&&(t=0);var n=e+t,i=r.findFirst(this.children,(function(e){return e.offset>n}));if(0===i)return this;var o=this.children[i-1];return o.offset<=e&&o.offset+o.length>=e+t?o.findInScope(e,t):this},e.prototype.addSymbol=function(e){this.symbols.push(e)},e.prototype.getSymbol=function(e,t){for(var n=0;n0&&(i.arguments=n),i},e.is=function(e){var t=e;return E.defined(t)&&E.string(t.title)&&E.string(t.command)}}(c=t.Command||(t.Command={})),function(e){e.replace=function(e,t){return{range:e,newText:t}},e.insert=function(e,t){return{range:{start:e,end:e},newText:t}},e.del=function(e){return{range:e,newText:""}},e.is=function(e){var t=e;return E.objectLiteral(t)&&E.string(t.newText)&&i.is(t.range)}}(d=t.TextEdit||(t.TextEdit={})),function(e){e.create=function(e,t,n){var r={label:e};return void 0!==t&&(r.needsConfirmation=t),void 0!==n&&(r.description=n),r},e.is=function(e){var t=e;return void 0!==t&&E.objectLiteral(t)&&E.string(t.label)&&(E.boolean(t.needsConfirmation)||void 0===t.needsConfirmation)&&(E.string(t.description)||void 0===t.description)}}(p=t.ChangeAnnotation||(t.ChangeAnnotation={})),function(e){e.is=function(e){return"string"==typeof e}}(h=t.ChangeAnnotationIdentifier||(t.ChangeAnnotationIdentifier={})),function(e){e.replace=function(e,t,n){return{range:e,newText:t,annotationId:n}},e.insert=function(e,t,n){return{range:{start:e,end:e},newText:t,annotationId:n}},e.del=function(e,t){return{range:e,newText:"",annotationId:t}},e.is=function(e){var t=e;return d.is(t)&&(p.is(t.annotationId)||h.is(t.annotationId))}}(u=t.AnnotatedTextEdit||(t.AnnotatedTextEdit={})),function(e){e.create=function(e,t){return{textDocument:e,edits:t}},e.is=function(e){var t=e;return E.defined(t)&&v.is(t.textDocument)&&Array.isArray(t.edits)}}(m=t.TextDocumentEdit||(t.TextDocumentEdit={})),function(e){e.create=function(e,t,n){var r={kind:"create",uri:e};return void 0===t||void 0===t.overwrite&&void 0===t.ignoreIfExists||(r.options=t),void 0!==n&&(r.annotationId=n),r},e.is=function(e){var t=e;return t&&"create"===t.kind&&E.string(t.uri)&&(void 0===t.options||(void 0===t.options.overwrite||E.boolean(t.options.overwrite))&&(void 0===t.options.ignoreIfExists||E.boolean(t.options.ignoreIfExists)))&&(void 0===t.annotationId||h.is(t.annotationId))}}(f=t.CreateFile||(t.CreateFile={})),function(e){e.create=function(e,t,n,r){var i={kind:"rename",oldUri:e,newUri:t};return void 0===n||void 0===n.overwrite&&void 0===n.ignoreIfExists||(i.options=n),void 0!==r&&(i.annotationId=r),i},e.is=function(e){var t=e;return t&&"rename"===t.kind&&E.string(t.oldUri)&&E.string(t.newUri)&&(void 0===t.options||(void 0===t.options.overwrite||E.boolean(t.options.overwrite))&&(void 0===t.options.ignoreIfExists||E.boolean(t.options.ignoreIfExists)))&&(void 0===t.annotationId||h.is(t.annotationId))}}(g=t.RenameFile||(t.RenameFile={})),function(e){e.create=function(e,t,n){var r={kind:"delete",uri:e};return void 0===t||void 0===t.recursive&&void 0===t.ignoreIfNotExists||(r.options=t),void 0!==n&&(r.annotationId=n),r},e.is=function(e){var t=e;return t&&"delete"===t.kind&&E.string(t.uri)&&(void 0===t.options||(void 0===t.options.recursive||E.boolean(t.options.recursive))&&(void 0===t.options.ignoreIfNotExists||E.boolean(t.options.ignoreIfNotExists)))&&(void 0===t.annotationId||h.is(t.annotationId))}}(b=t.DeleteFile||(t.DeleteFile={})),function(e){e.is=function(e){var t=e;return t&&(void 0!==t.changes||void 0!==t.documentChanges)&&(void 0===t.documentChanges||t.documentChanges.every((function(e){return E.string(e.kind)?f.is(e)||g.is(e)||b.is(e):m.is(e)})))}}(y=t.WorkspaceEdit||(t.WorkspaceEdit={}));var v,w,x,S,k=function(){function e(e,t){this.edits=e,this.changeAnnotations=t}return e.prototype.insert=function(e,t,n){var r,i;if(void 0===n?r=d.insert(e,t):h.is(n)?(i=n,r=u.insert(e,t,n)):(this.assertChangeAnnotations(this.changeAnnotations),i=this.changeAnnotations.manage(n),r=u.insert(e,t,i)),this.edits.push(r),void 0!==i)return i},e.prototype.replace=function(e,t,n){var r,i;if(void 0===n?r=d.replace(e,t):h.is(n)?(i=n,r=u.replace(e,t,n)):(this.assertChangeAnnotations(this.changeAnnotations),i=this.changeAnnotations.manage(n),r=u.replace(e,t,i)),this.edits.push(r),void 0!==i)return i},e.prototype.delete=function(e,t){var n,r;if(void 0===t?n=d.del(e):h.is(t)?(r=t,n=u.del(e,t)):(this.assertChangeAnnotations(this.changeAnnotations),r=this.changeAnnotations.manage(t),n=u.del(e,r)),this.edits.push(n),void 0!==r)return r},e.prototype.add=function(e){this.edits.push(e)},e.prototype.all=function(){return this.edits},e.prototype.clear=function(){this.edits.splice(0,this.edits.length)},e.prototype.assertChangeAnnotations=function(e){if(void 0===e)throw new Error("Text edit change is not configured to manage change annotations.")},e}(),C=function(){function e(e){this._annotations=void 0===e?Object.create(null):e,this._counter=0,this._size=0}return e.prototype.all=function(){return this._annotations},Object.defineProperty(e.prototype,"size",{get:function(){return this._size},enumerable:!1,configurable:!0}),e.prototype.manage=function(e,t){var n;if(h.is(e)?n=e:(n=this.nextId(),t=e),void 0!==this._annotations[n])throw new Error("Id "+n+" is already in use.");if(void 0===t)throw new Error("No annotation provided for id "+n);return this._annotations[n]=t,this._size++,n},e.prototype.nextId=function(){return this._counter++,this._counter.toString()},e}(),T=function(){function e(e){var t=this;this._textEditChanges=Object.create(null),void 0!==e?(this._workspaceEdit=e,e.documentChanges?(this._changeAnnotations=new C(e.changeAnnotations),e.changeAnnotations=this._changeAnnotations.all(),e.documentChanges.forEach((function(e){if(m.is(e)){var n=new k(e.edits,t._changeAnnotations);t._textEditChanges[e.textDocument.uri]=n}}))):e.changes&&Object.keys(e.changes).forEach((function(n){var r=new k(e.changes[n]);t._textEditChanges[n]=r}))):this._workspaceEdit={}}return Object.defineProperty(e.prototype,"edit",{get:function(){return this.initDocumentChanges(),void 0!==this._changeAnnotations&&(0===this._changeAnnotations.size?this._workspaceEdit.changeAnnotations=void 0:this._workspaceEdit.changeAnnotations=this._changeAnnotations.all()),this._workspaceEdit},enumerable:!1,configurable:!0}),e.prototype.getTextEditChange=function(e){if(v.is(e)){if(this.initDocumentChanges(),void 0===this._workspaceEdit.documentChanges)throw new Error("Workspace edit is not configured for document changes.");var t={uri:e.uri,version:e.version};if(!(r=this._textEditChanges[t.uri])){var n={textDocument:t,edits:i=[]};this._workspaceEdit.documentChanges.push(n),r=new k(i,this._changeAnnotations),this._textEditChanges[t.uri]=r}return r}if(this.initChanges(),void 0===this._workspaceEdit.changes)throw new Error("Workspace edit is not configured for normal text edit changes.");var r;if(!(r=this._textEditChanges[e])){var i=[];this._workspaceEdit.changes[e]=i,r=new k(i),this._textEditChanges[e]=r}return r},e.prototype.initDocumentChanges=function(){void 0===this._workspaceEdit.documentChanges&&void 0===this._workspaceEdit.changes&&(this._changeAnnotations=new C,this._workspaceEdit.documentChanges=[],this._workspaceEdit.changeAnnotations=this._changeAnnotations.all())},e.prototype.initChanges=function(){void 0===this._workspaceEdit.documentChanges&&void 0===this._workspaceEdit.changes&&(this._workspaceEdit.changes=Object.create(null))},e.prototype.createFile=function(e,t,n){if(this.initDocumentChanges(),void 0===this._workspaceEdit.documentChanges)throw new Error("Workspace edit is not configured for document changes.");var r,i,o;if(p.is(t)||h.is(t)?r=t:n=t,void 0===r?i=f.create(e,n):(o=h.is(r)?r:this._changeAnnotations.manage(r),i=f.create(e,n,o)),this._workspaceEdit.documentChanges.push(i),void 0!==o)return o},e.prototype.renameFile=function(e,t,n,r){if(this.initDocumentChanges(),void 0===this._workspaceEdit.documentChanges)throw new Error("Workspace edit is not configured for document changes.");var i,o,s;if(p.is(n)||h.is(n)?i=n:r=n,void 0===i?o=g.create(e,t,r):(s=h.is(i)?i:this._changeAnnotations.manage(i),o=g.create(e,t,r,s)),this._workspaceEdit.documentChanges.push(o),void 0!==s)return s},e.prototype.deleteFile=function(e,t,n){if(this.initDocumentChanges(),void 0===this._workspaceEdit.documentChanges)throw new Error("Workspace edit is not configured for document changes.");var r,i,o;if(p.is(t)||h.is(t)?r=t:n=t,void 0===r?i=b.create(e,n):(o=h.is(r)?r:this._changeAnnotations.manage(r),i=b.create(e,n,o)),this._workspaceEdit.documentChanges.push(i),void 0!==o)return o},e}();t.WorkspaceChange=T,function(e){e.create=function(e){return{uri:e}},e.is=function(e){var t=e;return E.defined(t)&&E.string(t.uri)}}(t.TextDocumentIdentifier||(t.TextDocumentIdentifier={})),function(e){e.create=function(e,t){return{uri:e,version:t}},e.is=function(e){var t=e;return E.defined(t)&&E.string(t.uri)&&E.integer(t.version)}}(t.VersionedTextDocumentIdentifier||(t.VersionedTextDocumentIdentifier={})),function(e){e.create=function(e,t){return{uri:e,version:t}},e.is=function(e){var t=e;return E.defined(t)&&E.string(t.uri)&&(null===t.version||E.integer(t.version))}}(v=t.OptionalVersionedTextDocumentIdentifier||(t.OptionalVersionedTextDocumentIdentifier={})),function(e){e.create=function(e,t,n,r){return{uri:e,languageId:t,version:n,text:r}},e.is=function(e){var t=e;return E.defined(t)&&E.string(t.uri)&&E.string(t.languageId)&&E.integer(t.version)&&E.string(t.text)}}(t.TextDocumentItem||(t.TextDocumentItem={})),function(e){e.PlainText="plaintext",e.Markdown="markdown"}(w=t.MarkupKind||(t.MarkupKind={})),function(e){e.is=function(t){var n=t;return n===e.PlainText||n===e.Markdown}}(w=t.MarkupKind||(t.MarkupKind={})),function(e){e.is=function(e){var t=e;return E.objectLiteral(e)&&w.is(t.kind)&&E.string(t.value)}}(x=t.MarkupContent||(t.MarkupContent={})),function(e){e.Text=1,e.Method=2,e.Function=3,e.Constructor=4,e.Field=5,e.Variable=6,e.Class=7,e.Interface=8,e.Module=9,e.Property=10,e.Unit=11,e.Value=12,e.Enum=13,e.Keyword=14,e.Snippet=15,e.Color=16,e.File=17,e.Reference=18,e.Folder=19,e.EnumMember=20,e.Constant=21,e.Struct=22,e.Event=23,e.Operator=24,e.TypeParameter=25}(t.CompletionItemKind||(t.CompletionItemKind={})),function(e){e.PlainText=1,e.Snippet=2}(t.InsertTextFormat||(t.InsertTextFormat={})),function(e){e.Deprecated=1}(t.CompletionItemTag||(t.CompletionItemTag={})),function(e){e.create=function(e,t,n){return{newText:e,insert:t,replace:n}},e.is=function(e){var t=e;return t&&E.string(t.newText)&&i.is(t.insert)&&i.is(t.replace)}}(t.InsertReplaceEdit||(t.InsertReplaceEdit={})),function(e){e.asIs=1,e.adjustIndentation=2}(t.InsertTextMode||(t.InsertTextMode={})),function(e){e.create=function(e){return{label:e}}}(t.CompletionItem||(t.CompletionItem={})),function(e){e.create=function(e,t){return{items:e||[],isIncomplete:!!t}}}(t.CompletionList||(t.CompletionList={})),function(e){e.fromPlainText=function(e){return e.replace(/[\\`*_{}[\]()#+\-.!]/g,"\\$&")},e.is=function(e){var t=e;return E.string(t)||E.objectLiteral(t)&&E.string(t.language)&&E.string(t.value)}}(S=t.MarkedString||(t.MarkedString={})),function(e){e.is=function(e){var t=e;return!!t&&E.objectLiteral(t)&&(x.is(t.contents)||S.is(t.contents)||E.typedArray(t.contents,S.is))&&(void 0===e.range||i.is(e.range))}}(t.Hover||(t.Hover={})),function(e){e.create=function(e,t){return t?{label:e,documentation:t}:{label:e}}}(t.ParameterInformation||(t.ParameterInformation={})),function(e){e.create=function(e,t){for(var n=[],r=2;r=0;s--){var a=i[s],l=e.offsetAt(a.range.start),c=e.offsetAt(a.range.end);if(!(c<=o))throw new Error("Overlapping edit");r=r.substring(0,l)+a.newText+r.substring(c,r.length),o=l}return r}}(t.TextDocument||(t.TextDocument={}));var E,F=function(){function e(e,t,n,r){this._uri=e,this._languageId=t,this._version=n,this._content=r,this._lineOffsets=void 0}return Object.defineProperty(e.prototype,"uri",{get:function(){return this._uri},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"languageId",{get:function(){return this._languageId},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"version",{get:function(){return this._version},enumerable:!1,configurable:!0}),e.prototype.getText=function(e){if(e){var t=this.offsetAt(e.start),n=this.offsetAt(e.end);return this._content.substring(t,n)}return this._content},e.prototype.update=function(e,t){this._content=e.text,this._version=t,this._lineOffsets=void 0},e.prototype.getLineOffsets=function(){if(void 0===this._lineOffsets){for(var e=[],t=this._content,n=!0,r=0;r0&&e.push(t.length),this._lineOffsets=e}return this._lineOffsets},e.prototype.positionAt=function(e){e=Math.max(Math.min(e,this._content.length),0);var t=this.getLineOffsets(),n=0,i=t.length;if(0===i)return r.create(0,e);for(;ne?i=o:n=o+1}var s=n-1;return r.create(s,e-t[s])},e.prototype.offsetAt=function(e){var t=this.getLineOffsets();if(e.line>=t.length)return this._content.length;if(e.line<0)return 0;var n=t[e.line],r=e.line+1e?r=i:n=i+1}var o=n-1;return{line:o,character:e-t[o]}},e.prototype.offsetAt=function(e){var t=this.getLineOffsets();if(e.line>=t.length)return this._content.length;if(e.line<0)return 0;var n=t[e.line],r=e.line+1n.line||t.line===n.line&&t.character>n.character?{start:n,end:t}:e}function s(e){var t=o(e.range);return t!==e.range?{newText:e.newText,range:t}:e}!function(e){e.create=function(e,t,r,i){return new n(e,t,r,i)},e.update=function(e,t,r){if(e instanceof n)return e.update(t,r),e;throw new Error("TextDocument.update: document must be created by TextDocument.create")},e.applyEdits=function(e,t){for(var n=e.getText(),i=0,o=[],a=0,l=r(t.map(s),(function(e,t){var n=e.range.start.line-t.range.start.line;return 0===n?e.range.start.character-t.range.start.character:n}));ai&&o.push(n.substring(i,d)),c.newText.length&&o.push(c.newText),i=e.offsetAt(c.range.end)}return o.push(n.substr(i)),o.join("")}}(t.TextDocument||(t.TextDocument={}))})),define("vscode-languageserver-textdocument",["vscode-languageserver-textdocument/main"],(function(e){return e})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/cssLanguageTypes",["require","exports","vscode-languageserver-types","vscode-languageserver-textdocument"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.FileType=t.ClientCapabilities=t.DocumentHighlightKind=t.VersionedTextDocumentIdentifier=t.TextDocumentEdit=t.CodeActionKind=t.TextEdit=t.WorkspaceEdit=t.DocumentLink=t.DocumentHighlight=t.CodeAction=t.Command=t.CodeActionContext=t.MarkedString=t.Hover=t.Location=t.DocumentSymbol=t.SymbolKind=t.SymbolInformation=t.InsertTextFormat=t.CompletionItemTag=t.CompletionList=t.CompletionItemKind=t.CompletionItem=t.DiagnosticSeverity=t.Diagnostic=t.SelectionRange=t.FoldingRangeKind=t.FoldingRange=t.ColorPresentation=t.ColorInformation=t.Color=t.MarkupKind=t.MarkupContent=t.Position=t.Range=t.TextDocument=void 0;var n=e("vscode-languageserver-types");Object.defineProperty(t,"Range",{enumerable:!0,get:function(){return n.Range}}),Object.defineProperty(t,"Position",{enumerable:!0,get:function(){return n.Position}}),Object.defineProperty(t,"MarkupContent",{enumerable:!0,get:function(){return n.MarkupContent}}),Object.defineProperty(t,"MarkupKind",{enumerable:!0,get:function(){return n.MarkupKind}}),Object.defineProperty(t,"Color",{enumerable:!0,get:function(){return n.Color}}),Object.defineProperty(t,"ColorInformation",{enumerable:!0,get:function(){return n.ColorInformation}}),Object.defineProperty(t,"ColorPresentation",{enumerable:!0,get:function(){return n.ColorPresentation}}),Object.defineProperty(t,"FoldingRange",{enumerable:!0,get:function(){return n.FoldingRange}}),Object.defineProperty(t,"FoldingRangeKind",{enumerable:!0,get:function(){return n.FoldingRangeKind}}),Object.defineProperty(t,"SelectionRange",{enumerable:!0,get:function(){return n.SelectionRange}}),Object.defineProperty(t,"Diagnostic",{enumerable:!0,get:function(){return n.Diagnostic}}),Object.defineProperty(t,"DiagnosticSeverity",{enumerable:!0,get:function(){return n.DiagnosticSeverity}}),Object.defineProperty(t,"CompletionItem",{enumerable:!0,get:function(){return n.CompletionItem}}),Object.defineProperty(t,"CompletionItemKind",{enumerable:!0,get:function(){return n.CompletionItemKind}}),Object.defineProperty(t,"CompletionList",{enumerable:!0,get:function(){return n.CompletionList}}),Object.defineProperty(t,"CompletionItemTag",{enumerable:!0,get:function(){return n.CompletionItemTag}}),Object.defineProperty(t,"InsertTextFormat",{enumerable:!0,get:function(){return n.InsertTextFormat}}),Object.defineProperty(t,"SymbolInformation",{enumerable:!0,get:function(){return n.SymbolInformation}}),Object.defineProperty(t,"SymbolKind",{enumerable:!0,get:function(){return n.SymbolKind}}),Object.defineProperty(t,"DocumentSymbol",{enumerable:!0,get:function(){return n.DocumentSymbol}}),Object.defineProperty(t,"Location",{enumerable:!0,get:function(){return n.Location}}),Object.defineProperty(t,"Hover",{enumerable:!0,get:function(){return n.Hover}}),Object.defineProperty(t,"MarkedString",{enumerable:!0,get:function(){return n.MarkedString}}),Object.defineProperty(t,"CodeActionContext",{enumerable:!0,get:function(){return n.CodeActionContext}}),Object.defineProperty(t,"Command",{enumerable:!0,get:function(){return n.Command}}),Object.defineProperty(t,"CodeAction",{enumerable:!0,get:function(){return n.CodeAction}}),Object.defineProperty(t,"DocumentHighlight",{enumerable:!0,get:function(){return n.DocumentHighlight}}),Object.defineProperty(t,"DocumentLink",{enumerable:!0,get:function(){return n.DocumentLink}}),Object.defineProperty(t,"WorkspaceEdit",{enumerable:!0,get:function(){return n.WorkspaceEdit}}),Object.defineProperty(t,"TextEdit",{enumerable:!0,get:function(){return n.TextEdit}}),Object.defineProperty(t,"CodeActionKind",{enumerable:!0,get:function(){return n.CodeActionKind}}),Object.defineProperty(t,"TextDocumentEdit",{enumerable:!0,get:function(){return n.TextDocumentEdit}}),Object.defineProperty(t,"VersionedTextDocumentIdentifier",{enumerable:!0,get:function(){return n.VersionedTextDocumentIdentifier}}),Object.defineProperty(t,"DocumentHighlightKind",{enumerable:!0,get:function(){return n.DocumentHighlightKind}});var r=e("vscode-languageserver-textdocument");Object.defineProperty(t,"TextDocument",{enumerable:!0,get:function(){return r.TextDocument}}),function(e){e.LATEST={textDocument:{completion:{completionItem:{documentationFormat:[n.MarkupKind.Markdown,n.MarkupKind.PlainText]}},hover:{contentFormat:[n.MarkupKind.Markdown,n.MarkupKind.PlainText]}}}}(t.ClientCapabilities||(t.ClientCapabilities={})),function(e){e[e.Unknown=0]="Unknown",e[e.File=1]="File",e[e.Directory=2]="Directory",e[e.SymbolicLink=64]="SymbolicLink"}(t.FileType||(t.FileType={}))})),function(e,t){if("object"==typeof exports&&"object"==typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define("vscode-uri/index",[],t);else{var n=t();for(var r in n)("object"==typeof exports?exports:e)[r]=n[r]}}(this,(function(){return(()=>{"use strict";var e={470:e=>{function t(e){if("string"!=typeof e)throw new TypeError("Path must be a string. Received "+JSON.stringify(e))}function n(e,t){for(var n,r="",i=0,o=-1,s=0,a=0;a<=e.length;++a){if(a2){var l=r.lastIndexOf("/");if(l!==r.length-1){-1===l?(r="",i=0):i=(r=r.slice(0,l)).length-1-r.lastIndexOf("/"),o=a,s=0;continue}}else if(2===r.length||1===r.length){r="",i=0,o=a,s=0;continue}t&&(r.length>0?r+="/..":r="..",i=2)}else r.length>0?r+="/"+e.slice(o+1,a):r=e.slice(o+1,a),i=a-o-1;o=a,s=0}else 46===n&&-1!==s?++s:s=-1}return r}var r={resolve:function(){for(var e,r="",i=!1,o=arguments.length-1;o>=-1&&!i;o--){var s;o>=0?s=arguments[o]:(void 0===e&&(e=process.cwd()),s=e),t(s),0!==s.length&&(r=s+"/"+r,i=47===s.charCodeAt(0))}return r=n(r,!i),i?r.length>0?"/"+r:"/":r.length>0?r:"."},normalize:function(e){if(t(e),0===e.length)return".";var r=47===e.charCodeAt(0),i=47===e.charCodeAt(e.length-1);return 0!==(e=n(e,!r)).length||r||(e="."),e.length>0&&i&&(e+="/"),r?"/"+e:e},isAbsolute:function(e){return t(e),e.length>0&&47===e.charCodeAt(0)},join:function(){if(0===arguments.length)return".";for(var e,n=0;n0&&(void 0===e?e=i:e+="/"+i)}return void 0===e?".":r.normalize(e)},relative:function(e,n){if(t(e),t(n),e===n)return"";if((e=r.resolve(e))===(n=r.resolve(n)))return"";for(var i=1;ic){if(47===n.charCodeAt(a+p))return n.slice(a+p+1);if(0===p)return n.slice(a+p)}else s>c&&(47===e.charCodeAt(i+p)?d=p:0===p&&(d=0));break}var h=e.charCodeAt(i+p);if(h!==n.charCodeAt(a+p))break;47===h&&(d=p)}var u="";for(p=i+d+1;p<=o;++p)p!==o&&47!==e.charCodeAt(p)||(0===u.length?u+="..":u+="/..");return u.length>0?u+n.slice(a+d):(a+=d,47===n.charCodeAt(a)&&++a,n.slice(a))},_makeLong:function(e){return e},dirname:function(e){if(t(e),0===e.length)return".";for(var n=e.charCodeAt(0),r=47===n,i=-1,o=!0,s=e.length-1;s>=1;--s)if(47===(n=e.charCodeAt(s))){if(!o){i=s;break}}else o=!1;return-1===i?r?"/":".":r&&1===i?"//":e.slice(0,i)},basename:function(e,n){if(void 0!==n&&"string"!=typeof n)throw new TypeError('"ext" argument must be a string');t(e);var r,i=0,o=-1,s=!0;if(void 0!==n&&n.length>0&&n.length<=e.length){if(n.length===e.length&&n===e)return"";var a=n.length-1,l=-1;for(r=e.length-1;r>=0;--r){var c=e.charCodeAt(r);if(47===c){if(!s){i=r+1;break}}else-1===l&&(s=!1,l=r+1),a>=0&&(c===n.charCodeAt(a)?-1==--a&&(o=r):(a=-1,o=l))}return i===o?o=l:-1===o&&(o=e.length),e.slice(i,o)}for(r=e.length-1;r>=0;--r)if(47===e.charCodeAt(r)){if(!s){i=r+1;break}}else-1===o&&(s=!1,o=r+1);return-1===o?"":e.slice(i,o)},extname:function(e){t(e);for(var n=-1,r=0,i=-1,o=!0,s=0,a=e.length-1;a>=0;--a){var l=e.charCodeAt(a);if(47!==l)-1===i&&(o=!1,i=a+1),46===l?-1===n?n=a:1!==s&&(s=1):-1!==n&&(s=-1);else if(!o){r=a+1;break}}return-1===n||-1===i||0===s||1===s&&n===i-1&&n===r+1?"":e.slice(n,i)},format:function(e){if(null===e||"object"!=typeof e)throw new TypeError('The "pathObject" argument must be of type Object. Received type '+typeof e);return function(e,t){var n=t.dir||t.root,r=t.base||(t.name||"")+(t.ext||"");return n?n===t.root?n+r:n+"/"+r:r}(0,e)},parse:function(e){t(e);var n={root:"",dir:"",base:"",ext:"",name:""};if(0===e.length)return n;var r,i=e.charCodeAt(0),o=47===i;o?(n.root="/",r=1):r=0;for(var s=-1,a=0,l=-1,c=!0,d=e.length-1,p=0;d>=r;--d)if(47!==(i=e.charCodeAt(d)))-1===l&&(c=!1,l=d+1),46===i?-1===s?s=d:1!==p&&(p=1):-1!==s&&(p=-1);else if(!c){a=d+1;break}return-1===s||-1===l||0===p||1===p&&s===l-1&&s===a+1?-1!==l&&(n.base=n.name=0===a&&o?e.slice(1,l):e.slice(a,l)):(0===a&&o?(n.name=e.slice(1,s),n.base=e.slice(1,l)):(n.name=e.slice(a,s),n.base=e.slice(a,l)),n.ext=e.slice(s,l)),a>0?n.dir=e.slice(0,a-1):o&&(n.dir="/"),n},sep:"/",delimiter:":",win32:null,posix:null};r.posix=r,e.exports=r},465:(e,t,n)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.Utils=t.URI=void 0;var r=n(796);Object.defineProperty(t,"URI",{enumerable:!0,get:function(){return r.URI}});var i=n(679);Object.defineProperty(t,"Utils",{enumerable:!0,get:function(){return i.Utils}})},674:(e,t)=>{if(Object.defineProperty(t,"__esModule",{value:!0}),t.isWindows=void 0,"object"==typeof process)t.isWindows="win32"===process.platform;else if("object"==typeof navigator){var n=navigator.userAgent;t.isWindows=n.indexOf("Windows")>=0}},796:function(e,t,n){var r,i,o=this&&this.__extends||(r=function(e,t){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(e,t)},function(e,t){function n(){this.constructor=e}r(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)});Object.defineProperty(t,"__esModule",{value:!0}),t.uriToFsPath=t.URI=void 0;var s=n(674),a=/^\w[\w\d+.-]*$/,l=/^\//,c=/^\/\//,d="",p="/",h=/^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/,u=function(){function e(e,t,n,r,i,o){void 0===o&&(o=!1),"object"==typeof e?(this.scheme=e.scheme||d,this.authority=e.authority||d,this.path=e.path||d,this.query=e.query||d,this.fragment=e.fragment||d):(this.scheme=function(e,t){return e||t?e:"file"}(e,o),this.authority=t||d,this.path=function(e,t){switch(e){case"https":case"http":case"file":t?t[0]!==p&&(t=p+t):t=p}return t}(this.scheme,n||d),this.query=r||d,this.fragment=i||d,function(e,t){if(!e.scheme&&t)throw new Error('[UriError]: Scheme is missing: {scheme: "", authority: "'+e.authority+'", path: "'+e.path+'", query: "'+e.query+'", fragment: "'+e.fragment+'"}');if(e.scheme&&!a.test(e.scheme))throw new Error("[UriError]: Scheme contains illegal characters.");if(e.path)if(e.authority){if(!l.test(e.path))throw new Error('[UriError]: If a URI contains an authority component, then the path component must either be empty or begin with a slash ("/") character')}else if(c.test(e.path))throw new Error('[UriError]: If a URI does not contain an authority component, then the path cannot begin with two slash characters ("//")')}(this,o))}return e.isUri=function(t){return t instanceof e||!!t&&"string"==typeof t.authority&&"string"==typeof t.fragment&&"string"==typeof t.path&&"string"==typeof t.query&&"string"==typeof t.scheme&&"function"==typeof t.fsPath&&"function"==typeof t.with&&"function"==typeof t.toString},Object.defineProperty(e.prototype,"fsPath",{get:function(){return v(this,!1)},enumerable:!1,configurable:!0}),e.prototype.with=function(e){if(!e)return this;var t=e.scheme,n=e.authority,r=e.path,i=e.query,o=e.fragment;return void 0===t?t=this.scheme:null===t&&(t=d),void 0===n?n=this.authority:null===n&&(n=d),void 0===r?r=this.path:null===r&&(r=d),void 0===i?i=this.query:null===i&&(i=d),void 0===o?o=this.fragment:null===o&&(o=d),t===this.scheme&&n===this.authority&&r===this.path&&i===this.query&&o===this.fragment?this:new f(t,n,r,i,o)},e.parse=function(e,t){void 0===t&&(t=!1);var n=h.exec(e);return n?new f(n[2]||d,k(n[4]||d),k(n[5]||d),k(n[7]||d),k(n[9]||d),t):new f(d,d,d,d,d)},e.file=function(e){var t=d;if(s.isWindows&&(e=e.replace(/\\/g,p)),e[0]===p&&e[1]===p){var n=e.indexOf(p,2);-1===n?(t=e.substring(2),e=p):(t=e.substring(2,n),e=e.substring(n)||p)}return new f("file",t,e,d,d)},e.from=function(e){return new f(e.scheme,e.authority,e.path,e.query,e.fragment)},e.prototype.toString=function(e){return void 0===e&&(e=!1),w(this,e)},e.prototype.toJSON=function(){return this},e.revive=function(t){if(t){if(t instanceof e)return t;var n=new f(t);return n._formatted=t.external,n._fsPath=t._sep===m?t.fsPath:null,n}return t},e}();t.URI=u;var m=s.isWindows?1:void 0,f=function(e){function t(){var t=null!==e&&e.apply(this,arguments)||this;return t._formatted=null,t._fsPath=null,t}return o(t,e),Object.defineProperty(t.prototype,"fsPath",{get:function(){return this._fsPath||(this._fsPath=v(this,!1)),this._fsPath},enumerable:!1,configurable:!0}),t.prototype.toString=function(e){return void 0===e&&(e=!1),e?w(this,!0):(this._formatted||(this._formatted=w(this,!1)),this._formatted)},t.prototype.toJSON=function(){var e={$mid:1};return this._fsPath&&(e.fsPath=this._fsPath,e._sep=m),this._formatted&&(e.external=this._formatted),this.path&&(e.path=this.path),this.scheme&&(e.scheme=this.scheme),this.authority&&(e.authority=this.authority),this.query&&(e.query=this.query),this.fragment&&(e.fragment=this.fragment),e},t}(u),g=((i={})[58]="%3A",i[47]="%2F",i[63]="%3F",i[35]="%23",i[91]="%5B",i[93]="%5D",i[64]="%40",i[33]="%21",i[36]="%24",i[38]="%26",i[39]="%27",i[40]="%28",i[41]="%29",i[42]="%2A",i[43]="%2B",i[44]="%2C",i[59]="%3B",i[61]="%3D",i[32]="%20",i);function b(e,t){for(var n=void 0,r=-1,i=0;i=97&&o<=122||o>=65&&o<=90||o>=48&&o<=57||45===o||46===o||95===o||126===o||t&&47===o)-1!==r&&(n+=encodeURIComponent(e.substring(r,i)),r=-1),void 0!==n&&(n+=e.charAt(i));else{void 0===n&&(n=e.substr(0,i));var s=g[o];void 0!==s?(-1!==r&&(n+=encodeURIComponent(e.substring(r,i)),r=-1),n+=s):-1===r&&(r=i)}}return-1!==r&&(n+=encodeURIComponent(e.substring(r))),void 0!==n?n:e}function y(e){for(var t=void 0,n=0;n1&&"file"===e.scheme?"//"+e.authority+e.path:47===e.path.charCodeAt(0)&&(e.path.charCodeAt(1)>=65&&e.path.charCodeAt(1)<=90||e.path.charCodeAt(1)>=97&&e.path.charCodeAt(1)<=122)&&58===e.path.charCodeAt(2)?t?e.path.substr(1):e.path[1].toLowerCase()+e.path.substr(2):e.path,s.isWindows&&(n=n.replace(/\//g,"\\")),n}function w(e,t){var n=t?y:b,r="",i=e.scheme,o=e.authority,s=e.path,a=e.query,l=e.fragment;if(i&&(r+=i,r+=":"),(o||"file"===i)&&(r+=p,r+=p),o){var c=o.indexOf("@");if(-1!==c){var d=o.substr(0,c);o=o.substr(c+1),-1===(c=d.indexOf(":"))?r+=n(d,!1):(r+=n(d.substr(0,c),!1),r+=":",r+=n(d.substr(c+1),!1)),r+="@"}-1===(c=(o=o.toLowerCase()).indexOf(":"))?r+=n(o,!1):(r+=n(o.substr(0,c),!1),r+=o.substr(c))}if(s){if(s.length>=3&&47===s.charCodeAt(0)&&58===s.charCodeAt(2))(h=s.charCodeAt(1))>=65&&h<=90&&(s="/"+String.fromCharCode(h+32)+":"+s.substr(3));else if(s.length>=2&&58===s.charCodeAt(1)){var h;(h=s.charCodeAt(0))>=65&&h<=90&&(s=String.fromCharCode(h+32)+":"+s.substr(2))}r+=n(s,!0)}return a&&(r+="?",r+=n(a,!1)),l&&(r+="#",r+=t?l:b(l,!1)),r}function x(e){try{return decodeURIComponent(e)}catch(t){return e.length>3?e.substr(0,3)+x(e.substr(3)):e}}t.uriToFsPath=v;var S=/(%[0-9A-Za-z][0-9A-Za-z])+/g;function k(e){return e.match(S)?e.replace(S,(function(e){return x(e)})):e}},679:function(e,t,n){var r=this&&this.__spreadArrays||function(){for(var e=0,t=0,n=arguments.length;t0&&i[i.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]0&&i[i.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=0&&-1===' \t\n\r":{[()]},*>+'.indexOf(r.charAt(n));)n--;return r.substring(n+1,t)}(e,this.offset),this.defaultReplaceRange=a.Range.create(a.Position.create(this.position.line,this.position.character-this.currentWord.length),this.position),this.textDocument=e,this.styleSheet=n,this.documentSettings=i;try{var o={isIncomplete:!1,items:[]};this.nodePath=r.getNodePath(this.styleSheet,this.offset);for(var s=this.nodePath.length-1;s>=0;s--){var l=this.nodePath[s];if(l instanceof r.Property)this.getCompletionsForDeclarationProperty(l.getParent(),o);else if(l instanceof r.Expression)l.parent instanceof r.Interpolation?this.getVariableProposals(null,o):this.getCompletionsForExpression(l,o);else if(l instanceof r.SimpleSelector){var c=l.findAParent(r.NodeType.ExtendsReference,r.NodeType.Ruleset);if(c)if(c.type===r.NodeType.ExtendsReference)this.getCompletionsForExtendsReference(c,l,o);else{var d=c;this.getCompletionsForSelector(d,d&&d.isNested(),o)}}else if(l instanceof r.FunctionArgument)this.getCompletionsForFunctionArgument(l,l.getParent(),o);else if(l instanceof r.Declarations)this.getCompletionsForDeclarations(l,o);else if(l instanceof r.VariableDeclaration)this.getCompletionsForVariableDeclaration(l,o);else if(l instanceof r.RuleSet)this.getCompletionsForRuleSet(l,o);else if(l instanceof r.Interpolation)this.getCompletionsForInterpolation(l,o);else if(l instanceof r.FunctionDeclaration)this.getCompletionsForFunctionDeclaration(l,o);else if(l instanceof r.MixinReference)this.getCompletionsForMixinReference(l,o);else if(l instanceof r.Function)this.getCompletionsForFunctionArgument(null,l,o);else if(l instanceof r.Supports)this.getCompletionsForSupports(l,o);else if(l instanceof r.SupportsCondition)this.getCompletionsForSupportsCondition(l,o);else if(l instanceof r.ExtendsReference)this.getCompletionsForExtendsReference(l,null,o);else if(l.type===r.NodeType.URILiteral)this.getCompletionForUriLiteralValue(l,o);else if(null===l.parent)this.getCompletionForTopLevel(o);else{if(l.type!==r.NodeType.StringLiteral||!this.isImportPathParent(l.parent.type))continue;this.getCompletionForImportPath(l,o)}if(o.items.length>0||this.offset>l.offset)return this.finalize(o)}return this.getCompletionsForStylesheet(o),0===o.items.length&&this.variablePrefix&&0===this.currentWord.indexOf(this.variablePrefix)&&this.getVariableProposals(null,o),this.finalize(o)}finally{this.position=null,this.currentWord=null,this.textDocument=null,this.styleSheet=null,this.symbolContext=null,this.defaultReplaceRange=null,this.nodePath=null}},e.prototype.isImportPathParent=function(e){return e===r.NodeType.Import},e.prototype.finalize=function(e){return e},e.prototype.findInNodePath=function(){for(var e=[],t=0;t=0;n--){var r=this.nodePath[n];if(-1!==e.indexOf(r.type))return r}return null},e.prototype.getCompletionsForDeclarationProperty=function(e,t){return this.getPropertyProposals(e,t)},e.prototype.getPropertyProposals=function(e,t){var r=this,i=this.isTriggerPropertyValueCompletionEnabled,l=this.isCompletePropertyWithSemicolonEnabled;return this.cssDataManager.getProperties().forEach((function(d){var p,h,u=!1;e?(p=r.getCompletionRange(e.getProperty()),h=d.name,c.isDefined(e.colonPosition)||(h+=": ",u=!0)):(p=r.getCompletionRange(null),h=d.name+": ",u=!0),!e&&l&&(h+="$0;"),e&&!e.semicolonPosition&&l&&r.offset>=r.textDocument.offsetAt(p.end)&&(h+="$0;");var f={label:d.name,documentation:o.getEntryDescription(d,r.doesSupportMarkdown()),tags:m(d)?[a.CompletionItemTag.Deprecated]:[],textEdit:a.TextEdit.replace(p,h),insertTextFormat:a.InsertTextFormat.Snippet,kind:a.CompletionItemKind.Property};d.restrictions||(u=!1),i&&u&&(f.command={title:"Suggest",command:"editor.action.triggerSuggest"});var g=(255-("number"==typeof d.relevance?Math.min(Math.max(d.relevance,0),99):50)).toString(16),b=s.startsWith(d.name,"-")?n.VendorPrefixed:n.Normal;f.sortText=b+"_"+g,t.items.push(f)})),this.completionParticipants.forEach((function(e){e.onCssProperty&&e.onCssProperty({propertyName:r.currentWord,range:r.defaultReplaceRange})})),t},Object.defineProperty(e.prototype,"isTriggerPropertyValueCompletionEnabled",{get:function(){var e,t;return null===(t=null===(e=this.documentSettings)||void 0===e?void 0:e.triggerPropertyValueCompletion)||void 0===t||t},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isCompletePropertyWithSemicolonEnabled",{get:function(){var e,t;return null===(t=null===(e=this.documentSettings)||void 0===e?void 0:e.completePropertyWithSemicolon)||void 0===t||t},enumerable:!1,configurable:!0}),e.prototype.getCompletionsForDeclarationValue=function(e,t){for(var n=this,i=e.getFullPropertyName(),o=this.cssDataManager.getProperty(i),s=e.getValue()||null;s&&s.hasChildren();)s=s.findChildAtOffset(this.offset,!1);if(this.completionParticipants.forEach((function(e){e.onCssPropertyValue&&e.onCssPropertyValue({propertyName:i,propertyValue:n.currentWord,range:n.getCompletionRange(s)})})),o){if(o.restrictions)for(var l=0,c=o.restrictions;l=e.offset+2&&this.getVariableProposals(null,t),t},e.prototype.getVariableProposals=function(e,t){for(var i=0,o=this.getSymbolContext().findSymbolsAtOffset(this.offset,r.ReferenceType.Variable);i0){var s=this.currentWord.match(/^-?\d[\.\d+]*/);s&&(i=s[0],n.isIncomplete=i.length===this.currentWord.length)}else 0===this.currentWord.length&&(n.isIncomplete=!0);if(t&&t.parent&&t.parent.type===r.NodeType.Term&&(t=t.getParent()),e.restrictions)for(var l=0,c=e.restrictions;l=n.end?this.getCompletionForTopLevel(t):!n||this.offset<=n.offset?this.getCompletionsForSelector(e,e.isNested(),t):this.getCompletionsForDeclarations(e.getDeclarations(),t)},e.prototype.getCompletionsForSelector=function(e,t,i){var l=this,c=this.findInNodePath(r.NodeType.PseudoSelector,r.NodeType.IdentifierSelector,r.NodeType.ClassSelector,r.NodeType.ElementNameSelector);if(!c&&this.hasCharacterAtPosition(this.offset-this.currentWord.length-1,":")&&(this.currentWord=":"+this.currentWord,this.hasCharacterAtPosition(this.offset-this.currentWord.length-1,":")&&(this.currentWord=":"+this.currentWord),this.defaultReplaceRange=a.Range.create(a.Position.create(this.position.line,this.position.character-this.currentWord.length),this.position)),this.cssDataManager.getPseudoClasses().forEach((function(e){var t=g(e.name),r={label:e.name,textEdit:a.TextEdit.replace(l.getCompletionRange(c),t),documentation:o.getEntryDescription(e,l.doesSupportMarkdown()),tags:m(e)?[a.CompletionItemTag.Deprecated]:[],kind:a.CompletionItemKind.Function,insertTextFormat:e.name!==t?h:void 0};s.startsWith(e.name,":-")&&(r.sortText=n.VendorPrefixed),i.items.push(r)})),this.cssDataManager.getPseudoElements().forEach((function(e){var t=g(e.name),r={label:e.name,textEdit:a.TextEdit.replace(l.getCompletionRange(c),t),documentation:o.getEntryDescription(e,l.doesSupportMarkdown()),tags:m(e)?[a.CompletionItemTag.Deprecated]:[],kind:a.CompletionItemKind.Function,insertTextFormat:e.name!==t?h:void 0};s.startsWith(e.name,"::-")&&(r.sortText=n.VendorPrefixed),i.items.push(r)})),!t){for(var d=0,p=o.html5Tags;d0){var t=v.substr(e.offset,e.length);return"."!==t.charAt(0)||y[t]||(y[t]=!0,i.items.push({label:t,textEdit:a.TextEdit.replace(l.getCompletionRange(c),t),kind:a.CompletionItemKind.Keyword})),!1}return!0})),e&&e.isNested()){var w=e.getSelectors().findFirstChildBeforeOffset(this.offset);w&&0===e.getSelectors().getChildren().indexOf(w)&&this.getPropertyProposals(null,i)}return i},e.prototype.getCompletionsForDeclarations=function(e,t){if(!e||this.offset===e.offset)return t;var n=e.findFirstChildBeforeOffset(this.offset);if(!n)return this.getCompletionsForDeclarationProperty(null,t);if(n instanceof r.AbstractDeclaration){var i=n;if(!c.isDefined(i.colonPosition)||this.offset<=i.colonPosition)return this.getCompletionsForDeclarationProperty(i,t);if(c.isDefined(i.semicolonPosition)&&i.semicolonPositione.colonPosition&&this.getVariableProposals(e.getValue(),t),t},e.prototype.getCompletionsForExpression=function(e,t){var n=e.getParent();if(n instanceof r.FunctionArgument)return this.getCompletionsForFunctionArgument(n,n.getParent(),t),t;var i=e.findParent(r.NodeType.Declaration);if(!i)return this.getTermProposals(void 0,null,t),t;var o=e.findChildAtOffset(this.offset,!0);return o?o instanceof r.NumericValue||o instanceof r.Identifier?this.getCompletionsForDeclarationValue(i,t):t:this.getCompletionsForDeclarationValue(i,t)},e.prototype.getCompletionsForFunctionArgument=function(e,t,n){var r=t.getIdentifier();return r&&r.matches("var")&&(t.getArguments().hasChildren()&&t.getArguments().getChild(0)!==e||this.getVariableProposalsForCSSVarFunction(n)),n},e.prototype.getCompletionsForFunctionDeclaration=function(e,t){var n=e.getDeclarations();return n&&this.offset>n.offset&&this.offsete.lParent&&(!c.isDefined(e.rParent)||this.offset<=e.rParent)?this.getCompletionsForDeclarationProperty(null,t):t},e.prototype.getCompletionsForSupports=function(e,t){var n=e.getDeclarations();if(!n||this.offset<=n.offset){var i=e.findFirstChildBeforeOffset(this.offset);return i instanceof r.SupportsCondition?this.getCompletionsForSupportsCondition(i,t):t}return this.getCompletionForTopLevel(t)},e.prototype.getCompletionsForExtendsReference=function(e,t,n){return n},e.prototype.getCompletionForUriLiteralValue=function(e,t){var n,r,i;if(e.hasChildren()){var o=e.getChild(0);n=o.getText(),r=this.position,i=this.getCompletionRange(o)}else{n="",r=this.position;var s=this.textDocument.positionAt(e.offset+"url(".length);i=a.Range.create(s,s)}return this.completionParticipants.forEach((function(e){e.onCssURILiteralValue&&e.onCssURILiteralValue({uriValue:n,position:r,range:i})})),t},e.prototype.getCompletionForImportPath=function(e,t){var n=this;return this.completionParticipants.forEach((function(t){t.onCssImportPath&&t.onCssImportPath({pathValue:e.getText(),position:n.position,range:n.getCompletionRange(e)})})),t},e.prototype.hasCharacterAtPosition=function(e,t){var n=this.textDocument.getText();return e>=0&&e"),this.writeLine(t,r.join(""))}},e}();!function(e){function t(e){var t=e.match(/^['"](.*)["']$/);return t?t[1]:e}e.ensure=function(e,n){return n+t(e)+n},e.remove=t}(l||(l={}));var d=function(){this.id=0,this.attr=0,this.tag=0};function p(e,t){for(var r=new o,i=0,s=e.getChildren();i1){var p=t.cloneWithParent();r.addChild(p.findRoot()),r=p}r.append(c[d])}}break;case n.NodeType.SelectorPlaceholder:if(a.matches("@at-root"))return r;case n.NodeType.ElementNameSelector:var u=a.getText();r.addAttr("name","*"===u?"element":h(u));break;case n.NodeType.ClassSelector:r.addAttr("class",h(a.getText().substring(1)));break;case n.NodeType.IdentifierSelector:r.addAttr("id",h(a.getText().substring(1)));break;case n.NodeType.MixinDeclaration:r.addAttr("class",a.getName());break;case n.NodeType.PseudoSelector:r.addAttr(h(a.getText()),"");break;case n.NodeType.AttributeSelector:var m=a,f=m.getIdentifier();if(f){var g=m.getValue(),b=m.getOperator(),y=void 0;if(g&&b)switch(h(b.getText())){case"|=":y=l.remove(h(g.getText()))+"-…";break;case"^=":y=l.remove(h(g.getText()))+"…";break;case"$=":y="…"+l.remove(h(g.getText()));break;case"~=":y=" … "+l.remove(h(g.getText()))+" … ";break;case"*=":y="…"+l.remove(h(g.getText()))+"…";break;default:y=l.remove(h(g.getText()))}r.addAttr(h(f.getText()),y)}}}return r}function h(e){var t=new r.Scanner;t.setSource(e);var n=t.scanUnquotedString();return n?n.text:e}t.toElement=p;var u=function(){function e(e){this.cssDataManager=e}return e.prototype.selectorToMarkedString=function(e){var t=g(e);if(t){var n=new c('"').print(t);return n.push(this.selectorToSpecificityMarkedString(e)),n}return[]},e.prototype.simpleSelectorToMarkedString=function(e){var t=p(e),n=new c('"').print(t);return n.push(this.selectorToSpecificityMarkedString(e)),n},e.prototype.isPseudoElementIdentifier=function(e){var t=e.match(/^::?([\w-]+)/);return!!t&&!!this.cssDataManager.getPseudoElement("::"+t[1])},e.prototype.selectorToSpecificityMarkedString=function(e){var t=this,r=function(e){for(var i=0,s=e.getChildren();i0&&r(a)}},o=new d;return r(e),i("specificity","[Selector Specificity](https://developer.mozilla.org/en-US/docs/Web/CSS/Specificity): ({0}, {1}, {2})",o.id,o.attr,o.tag)},e}();t.SelectorPrinting=u;var m=function(){function e(e){this.prev=null,this.element=e}return e.prototype.processSelector=function(e){var t=null;if(!(this.element instanceof s)&&e.getChildren().some((function(e){return e.hasChildren()&&e.getChild(0).type===n.NodeType.SelectorCombinator}))){var r=this.element.findRoot();r.parent instanceof s&&(t=this.element,this.element=r.parent,this.element.removeChild(r),this.prev=null)}for(var i=0,o=e.getChildren();i=0;l--){var c=r[l].getSelectors().getChild(0);c&&a.processSelector(c)}return a.processSelector(e),t}t.selectorToElement=g})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/services/cssHover",["require","exports","../parser/cssNodes","../languageFacts/facts","./selectorPrinting","../utils/strings","../cssLanguageTypes","../utils/objects"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CSSHover=void 0;var n=e("../parser/cssNodes"),r=e("../languageFacts/facts"),i=e("./selectorPrinting"),o=e("../utils/strings"),s=e("../cssLanguageTypes"),a=e("../utils/objects"),l=function(){function e(e,t){this.clientCapabilities=e,this.cssDataManager=t,this.selectorPrinting=new i.SelectorPrinting(t)}return e.prototype.configure=function(e){this.defaultSettings=e},e.prototype.doHover=function(e,t,i,a){function l(t){return s.Range.create(e.positionAt(t.offset),e.positionAt(t.end))}void 0===a&&(a=this.defaultSettings);for(var c=e.offsetAt(t),d=n.getNodePath(i,c),p=null,h=0;h0&&i[i.length-1])||6!==o[0]&&2!==o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]=a.length/2&&l.push({property:e.name,score:t})})),l.sort((function(e,t){return t.score-e.score||e.property.localeCompare(t.property)}));for(var c=3,d=0,p=l;d=0;c--){var d=l[c];if(d instanceof n.Declaration){var p=d.getProperty();if(p&&p.offset===s&&p.end===a)return void this.getFixesForUnknownProperty(e,p,r,o)}}},e}();t.CSSCodeActions=a})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/services/lintUtil",["require","exports","../utils/arrays"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.Element=void 0;var n=e("../utils/arrays"),r=function(e){this.fullPropertyName=e.getFullPropertyName().toLowerCase(),this.node=e};function i(e,t,r,i){var o=e[t];o.value=r,r&&(n.includes(o.properties,i)||o.properties.push(i))}function o(e,t,n,r){"top"===t||"right"===t||"bottom"===t||"left"===t?i(e,t,n,r):function(e,t,n){i(e,"top",t,n),i(e,"right",t,n),i(e,"bottom",t,n),i(e,"left",t,n)}(e,n,r)}function s(e,t,n){switch(t.length){case 1:o(e,void 0,t[0],n);break;case 2:o(e,"top",t[0],n),o(e,"bottom",t[0],n),o(e,"right",t[1],n),o(e,"left",t[1],n);break;case 3:o(e,"top",t[0],n),o(e,"right",t[1],n),o(e,"left",t[1],n),o(e,"bottom",t[2],n);break;case 4:o(e,"top",t[0],n),o(e,"right",t[1],n),o(e,"bottom",t[2],n),o(e,"left",t[3],n)}}function a(e,t){for(var n=0,r=t;n0)for(var w=this.fetch(r,"float"),x=0;x0)for(w=this.fetch(r,"vertical-align"),x=0;x1)for(var D=0;D".charCodeAt(0),m=".".charCodeAt(0),f=("@".charCodeAt(0),n.TokenType.CustomToken);t.VariableName=f++,t.InterpolationFunction=f++,t.Default=f++,t.EqualsOperator=f++,t.NotEqualsOperator=f++,t.GreaterEqualsOperator=f++,t.SmallerEqualsOperator=f++,t.Ellipsis=f++,t.Module=f++;var g=function(e){function f(){return null!==e&&e.apply(this,arguments)||this}return __extends(f,e),f.prototype.scanNext=function(r){if(this.stream.advanceIfChar(a)){var i=["$"];if(this.ident(i))return this.finishToken(r,t.VariableName,i.join(""));this.stream.goBackTo(r)}return this.stream.advanceIfChars([l,c])?this.finishToken(r,t.InterpolationFunction):this.stream.advanceIfChars([d,d])?this.finishToken(r,t.EqualsOperator):this.stream.advanceIfChars([p,d])?this.finishToken(r,t.NotEqualsOperator):this.stream.advanceIfChar(h)?this.stream.advanceIfChar(d)?this.finishToken(r,t.SmallerEqualsOperator):this.finishToken(r,n.TokenType.Delim):this.stream.advanceIfChar(u)?this.stream.advanceIfChar(d)?this.finishToken(r,t.GreaterEqualsOperator):this.finishToken(r,n.TokenType.Delim):this.stream.advanceIfChars([m,m,m])?this.finishToken(r,t.Ellipsis):e.prototype.scanNext.call(this,r)},f.prototype.comment=function(){return!!e.prototype.comment.call(this)||!(this.inURL||!this.stream.advanceIfChars([r,r]))&&(this.stream.advanceWhileChar((function(e){switch(e){case i:case o:case s:return!1;default:return!0}})),!0)},f}(n.Scanner);t.SCSSScanner=g})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/parser/scssErrors",["require","exports","vscode-nls"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SCSSParseError=t.SCSSIssueType=void 0;var n=e("vscode-nls").loadMessageBundle(),r=function(e,t){this.id=e,this.message=t};t.SCSSIssueType=r,t.SCSSParseError={FromExpected:new r("scss-fromexpected",n("expected.from","'from' expected")),ThroughOrToExpected:new r("scss-throughexpected",n("expected.through","'through' or 'to' expected")),InExpected:new r("scss-fromexpected",n("expected.in","'in' expected"))}}));__extends=this&&this.__extends||function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();!function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/parser/scssParser",["require","exports","./scssScanner","./cssScanner","./cssParser","./cssNodes","./scssErrors","./cssErrors"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SCSSParser=void 0;var n=e("./scssScanner"),r=e("./cssScanner"),i=e("./cssParser"),o=e("./cssNodes"),s=e("./scssErrors"),a=e("./cssErrors"),l=function(e){function t(){return e.call(this,new n.SCSSScanner)||this}return __extends(t,e),t.prototype._parseStylesheetStatement=function(t){return void 0===t&&(t=!1),this.peek(r.TokenType.AtKeyword)?this._parseWarnAndDebug()||this._parseControlStatement()||this._parseMixinDeclaration()||this._parseMixinContent()||this._parseMixinReference()||this._parseFunctionDeclaration()||this._parseForward()||this._parseUse()||this._parseRuleset(t)||e.prototype._parseStylesheetAtStatement.call(this,t):this._parseRuleset(!0)||this._parseVariableDeclaration()},t.prototype._parseImport=function(){if(!this.peekKeyword("@import"))return null;var e=this.create(o.Import);if(this.consumeToken(),!e.addChild(this._parseURILiteral())&&!e.addChild(this._parseStringLiteral()))return this.finish(e,a.ParseError.URIOrStringExpected);for(;this.accept(r.TokenType.Comma);)if(!e.addChild(this._parseURILiteral())&&!e.addChild(this._parseStringLiteral()))return this.finish(e,a.ParseError.URIOrStringExpected);return this.peek(r.TokenType.SemiColon)||this.peek(r.TokenType.EOF)||e.setMedialist(this._parseMediaQueryList()),this.finish(e)},t.prototype._parseVariableDeclaration=function(e){if(void 0===e&&(e=[]),!this.peek(n.VariableName))return null;var t=this.create(o.VariableDeclaration);if(!t.setVariable(this._parseVariable()))return null;if(!this.accept(r.TokenType.Colon))return this.finish(t,a.ParseError.ColonExpected);if(this.prevToken&&(t.colonPosition=this.prevToken.offset),!t.setValue(this._parseExpr()))return this.finish(t,a.ParseError.VariableValueExpected,[],e);for(;this.peek(r.TokenType.Exclamation);)if(t.addChild(this._tryParsePrio()));else{if(this.consumeToken(),!this.peekRegExp(r.TokenType.Ident,/^(default|global)$/))return this.finish(t,a.ParseError.UnknownKeyword);this.consumeToken()}return this.peek(r.TokenType.SemiColon)&&(t.semicolonPosition=this.token.offset),this.finish(t)},t.prototype._parseMediaContentStart=function(){return this._parseInterpolation()},t.prototype._parseMediaFeatureName=function(){return this._parseModuleMember()||this._parseFunction()||this._parseIdent()||this._parseVariable()},t.prototype._parseKeyframeSelector=function(){return this._tryParseKeyframeSelector()||this._parseControlStatement(this._parseKeyframeSelector.bind(this))||this._parseVariableDeclaration()||this._parseMixinContent()},t.prototype._parseVariable=function(){if(!this.peek(n.VariableName))return null;var e=this.create(o.Variable);return this.consumeToken(),e},t.prototype._parseModuleMember=function(){var e=this.mark(),t=this.create(o.Module);return t.setIdentifier(this._parseIdent([o.ReferenceType.Module]))?this.hasWhitespace()||!this.acceptDelim(".")||this.hasWhitespace()?(this.restoreAtMark(e),null):t.addChild(this._parseVariable()||this._parseFunction())?t:this.finish(t,a.ParseError.IdentifierOrVariableExpected):null},t.prototype._parseIdent=function(e){var t=this;if(!this.peek(r.TokenType.Ident)&&!this.peek(n.InterpolationFunction)&&!this.peekDelim("-"))return null;var i=this.create(o.Identifier);i.referenceTypes=e,i.isCustomProperty=this.peekRegExp(r.TokenType.Ident,/^--/);for(var s,a=!1;(this.accept(r.TokenType.Ident)||i.addChild((s=void 0,s=t.mark(),t.acceptDelim("-")&&(t.hasWhitespace()||t.acceptDelim("-"),t.hasWhitespace())?(t.restoreAtMark(s),null):t._parseInterpolation()))||a&&this.acceptRegexp(/^[\w-]/))&&(a=!0,!this.hasWhitespace()););return a?this.finish(i):null},t.prototype._parseTermExpression=function(){return this._parseModuleMember()||this._parseVariable()||this._parseSelectorCombinator()||e.prototype._parseTermExpression.call(this)},t.prototype._parseInterpolation=function(){if(this.peek(n.InterpolationFunction)){var e=this.create(o.Interpolation);return this.consumeToken(),e.addChild(this._parseExpr())||this._parseSelectorCombinator()?this.accept(r.TokenType.CurlyR)?this.finish(e):this.finish(e,a.ParseError.RightCurlyExpected):this.accept(r.TokenType.CurlyR)?this.finish(e):this.finish(e,a.ParseError.ExpressionExpected)}return null},t.prototype._parseOperator=function(){if(this.peek(n.EqualsOperator)||this.peek(n.NotEqualsOperator)||this.peek(n.GreaterEqualsOperator)||this.peek(n.SmallerEqualsOperator)||this.peekDelim(">")||this.peekDelim("<")||this.peekIdent("and")||this.peekIdent("or")||this.peekDelim("%")){var t=this.createNode(o.NodeType.Operator);return this.consumeToken(),this.finish(t)}return e.prototype._parseOperator.call(this)},t.prototype._parseUnaryOperator=function(){if(this.peekIdent("not")){var t=this.create(o.Node);return this.consumeToken(),this.finish(t)}return e.prototype._parseUnaryOperator.call(this)},t.prototype._parseRuleSetDeclaration=function(){return this.peek(r.TokenType.AtKeyword)?this._parseKeyframe()||this._parseImport()||this._parseMedia(!0)||this._parseFontFace()||this._parseWarnAndDebug()||this._parseControlStatement()||this._parseFunctionDeclaration()||this._parseExtends()||this._parseMixinReference()||this._parseMixinContent()||this._parseMixinDeclaration()||this._parseRuleset(!0)||this._parseSupports(!0)||e.prototype._parseRuleSetDeclarationAtStatement.call(this):this._parseVariableDeclaration()||this._tryParseRuleset(!0)||e.prototype._parseRuleSetDeclaration.call(this)},t.prototype._parseDeclaration=function(e){var t=this._tryParseCustomPropertyDeclaration(e);if(t)return t;var n=this.create(o.Declaration);if(!n.setProperty(this._parseProperty()))return null;if(!this.accept(r.TokenType.Colon))return this.finish(n,a.ParseError.ColonExpected,[r.TokenType.Colon],e||[r.TokenType.SemiColon]);this.prevToken&&(n.colonPosition=this.prevToken.offset);var i=!1;if(n.setValue(this._parseExpr())&&(i=!0,n.addChild(this._parsePrio())),this.peek(r.TokenType.CurlyL))n.setNestedProperties(this._parseNestedProperties());else if(!i)return this.finish(n,a.ParseError.PropertyValueExpected);return this.peek(r.TokenType.SemiColon)&&(n.semicolonPosition=this.token.offset),this.finish(n)},t.prototype._parseNestedProperties=function(){var e=this.create(o.NestedProperties);return this._parseBody(e,this._parseDeclaration.bind(this))},t.prototype._parseExtends=function(){if(this.peekKeyword("@extend")){var e=this.create(o.ExtendsReference);if(this.consumeToken(),!e.getSelectors().addChild(this._parseSimpleSelector()))return this.finish(e,a.ParseError.SelectorExpected);for(;this.accept(r.TokenType.Comma);)e.getSelectors().addChild(this._parseSimpleSelector());return this.accept(r.TokenType.Exclamation)&&!this.acceptIdent("optional")?this.finish(e,a.ParseError.UnknownKeyword):this.finish(e)}return null},t.prototype._parseSimpleSelectorBody=function(){return this._parseSelectorCombinator()||this._parseSelectorPlaceholder()||e.prototype._parseSimpleSelectorBody.call(this)},t.prototype._parseSelectorCombinator=function(){if(this.peekDelim("&")){var e=this.createNode(o.NodeType.SelectorCombinator);for(this.consumeToken();!this.hasWhitespace()&&(this.acceptDelim("-")||this.accept(r.TokenType.Num)||this.accept(r.TokenType.Dimension)||e.addChild(this._parseIdent())||this.acceptDelim("&")););return this.finish(e)}return null},t.prototype._parseSelectorPlaceholder=function(){if(this.peekDelim("%")){var e=this.createNode(o.NodeType.SelectorPlaceholder);return this.consumeToken(),this._parseIdent(),this.finish(e)}if(this.peekKeyword("@at-root")){e=this.createNode(o.NodeType.SelectorPlaceholder);return this.consumeToken(),this.finish(e)}return null},t.prototype._parseElementName=function(){var t=this.mark(),n=e.prototype._parseElementName.call(this);return n&&!this.hasWhitespace()&&this.peek(r.TokenType.ParenthesisL)?(this.restoreAtMark(t),null):n},t.prototype._tryParsePseudoIdentifier=function(){return this._parseInterpolation()||e.prototype._tryParsePseudoIdentifier.call(this)},t.prototype._parseWarnAndDebug=function(){if(!this.peekKeyword("@debug")&&!this.peekKeyword("@warn")&&!this.peekKeyword("@error"))return null;var e=this.createNode(o.NodeType.Debug);return this.consumeToken(),e.addChild(this._parseExpr()),this.finish(e)},t.prototype._parseControlStatement=function(e){return void 0===e&&(e=this._parseRuleSetDeclaration.bind(this)),this.peek(r.TokenType.AtKeyword)?this._parseIfStatement(e)||this._parseForStatement(e)||this._parseEachStatement(e)||this._parseWhileStatement(e):null},t.prototype._parseIfStatement=function(e){return this.peekKeyword("@if")?this._internalParseIfStatement(e):null},t.prototype._internalParseIfStatement=function(e){var t=this.create(o.IfStatement);if(this.consumeToken(),!t.setExpression(this._parseExpr(!0)))return this.finish(t,a.ParseError.ExpressionExpected);if(this._parseBody(t,e),this.acceptKeyword("@else"))if(this.peekIdent("if"))t.setElseClause(this._internalParseIfStatement(e));else if(this.peek(r.TokenType.CurlyL)){var n=this.create(o.ElseStatement);this._parseBody(n,e),t.setElseClause(n)}return this.finish(t)},t.prototype._parseForStatement=function(e){if(!this.peekKeyword("@for"))return null;var t=this.create(o.ForStatement);return this.consumeToken(),t.setVariable(this._parseVariable())?this.acceptIdent("from")?t.addChild(this._parseBinaryExpr())?this.acceptIdent("to")||this.acceptIdent("through")?t.addChild(this._parseBinaryExpr())?this._parseBody(t,e):this.finish(t,a.ParseError.ExpressionExpected,[r.TokenType.CurlyR]):this.finish(t,s.SCSSParseError.ThroughOrToExpected,[r.TokenType.CurlyR]):this.finish(t,a.ParseError.ExpressionExpected,[r.TokenType.CurlyR]):this.finish(t,s.SCSSParseError.FromExpected,[r.TokenType.CurlyR]):this.finish(t,a.ParseError.VariableNameExpected,[r.TokenType.CurlyR])},t.prototype._parseEachStatement=function(e){if(!this.peekKeyword("@each"))return null;var t=this.create(o.EachStatement);this.consumeToken();var n=t.getVariables();if(!n.addChild(this._parseVariable()))return this.finish(t,a.ParseError.VariableNameExpected,[r.TokenType.CurlyR]);for(;this.accept(r.TokenType.Comma);)if(!n.addChild(this._parseVariable()))return this.finish(t,a.ParseError.VariableNameExpected,[r.TokenType.CurlyR]);return this.finish(n),this.acceptIdent("in")?t.addChild(this._parseExpr())?this._parseBody(t,e):this.finish(t,a.ParseError.ExpressionExpected,[r.TokenType.CurlyR]):this.finish(t,s.SCSSParseError.InExpected,[r.TokenType.CurlyR])},t.prototype._parseWhileStatement=function(e){if(!this.peekKeyword("@while"))return null;var t=this.create(o.WhileStatement);return this.consumeToken(),t.addChild(this._parseBinaryExpr())?this._parseBody(t,e):this.finish(t,a.ParseError.ExpressionExpected,[r.TokenType.CurlyR])},t.prototype._parseFunctionBodyDeclaration=function(){return this._parseVariableDeclaration()||this._parseReturnStatement()||this._parseWarnAndDebug()||this._parseControlStatement(this._parseFunctionBodyDeclaration.bind(this))},t.prototype._parseFunctionDeclaration=function(){if(!this.peekKeyword("@function"))return null;var e=this.create(o.FunctionDeclaration);if(this.consumeToken(),!e.setIdentifier(this._parseIdent([o.ReferenceType.Function])))return this.finish(e,a.ParseError.IdentifierExpected,[r.TokenType.CurlyR]);if(!this.accept(r.TokenType.ParenthesisL))return this.finish(e,a.ParseError.LeftParenthesisExpected,[r.TokenType.CurlyR]);if(e.getParameters().addChild(this._parseParameterDeclaration()))for(;this.accept(r.TokenType.Comma)&&!this.peek(r.TokenType.ParenthesisR);)if(!e.getParameters().addChild(this._parseParameterDeclaration()))return this.finish(e,a.ParseError.VariableNameExpected);return this.accept(r.TokenType.ParenthesisR)?this._parseBody(e,this._parseFunctionBodyDeclaration.bind(this)):this.finish(e,a.ParseError.RightParenthesisExpected,[r.TokenType.CurlyR])},t.prototype._parseReturnStatement=function(){if(!this.peekKeyword("@return"))return null;var e=this.createNode(o.NodeType.ReturnStatement);return this.consumeToken(),e.addChild(this._parseExpr())?this.finish(e):this.finish(e,a.ParseError.ExpressionExpected)},t.prototype._parseMixinDeclaration=function(){if(!this.peekKeyword("@mixin"))return null;var e=this.create(o.MixinDeclaration);if(this.consumeToken(),!e.setIdentifier(this._parseIdent([o.ReferenceType.Mixin])))return this.finish(e,a.ParseError.IdentifierExpected,[r.TokenType.CurlyR]);if(this.accept(r.TokenType.ParenthesisL)){if(e.getParameters().addChild(this._parseParameterDeclaration()))for(;this.accept(r.TokenType.Comma)&&!this.peek(r.TokenType.ParenthesisR);)if(!e.getParameters().addChild(this._parseParameterDeclaration()))return this.finish(e,a.ParseError.VariableNameExpected);if(!this.accept(r.TokenType.ParenthesisR))return this.finish(e,a.ParseError.RightParenthesisExpected,[r.TokenType.CurlyR])}return this._parseBody(e,this._parseRuleSetDeclaration.bind(this))},t.prototype._parseParameterDeclaration=function(){var e=this.create(o.FunctionParameter);return e.setIdentifier(this._parseVariable())?(this.accept(n.Ellipsis),this.accept(r.TokenType.Colon)&&!e.setDefaultValue(this._parseExpr(!0))?this.finish(e,a.ParseError.VariableValueExpected,[],[r.TokenType.Comma,r.TokenType.ParenthesisR]):this.finish(e)):null},t.prototype._parseMixinContent=function(){if(!this.peekKeyword("@content"))return null;var e=this.create(o.MixinContentReference);if(this.consumeToken(),this.accept(r.TokenType.ParenthesisL)){if(e.getArguments().addChild(this._parseFunctionArgument()))for(;this.accept(r.TokenType.Comma)&&!this.peek(r.TokenType.ParenthesisR);)if(!e.getArguments().addChild(this._parseFunctionArgument()))return this.finish(e,a.ParseError.ExpressionExpected);if(!this.accept(r.TokenType.ParenthesisR))return this.finish(e,a.ParseError.RightParenthesisExpected)}return this.finish(e)},t.prototype._parseMixinReference=function(){if(!this.peekKeyword("@include"))return null;var e=this.create(o.MixinReference);this.consumeToken();var t=this._parseIdent([o.ReferenceType.Mixin]);if(!e.setIdentifier(t))return this.finish(e,a.ParseError.IdentifierExpected,[r.TokenType.CurlyR]);if(!this.hasWhitespace()&&this.acceptDelim(".")&&!this.hasWhitespace()){var n=this._parseIdent([o.ReferenceType.Mixin]);if(!n)return this.finish(e,a.ParseError.IdentifierExpected,[r.TokenType.CurlyR]);var i=this.create(o.Module);t.referenceTypes=[o.ReferenceType.Module],i.setIdentifier(t),e.setIdentifier(n),e.addChild(i)}if(this.accept(r.TokenType.ParenthesisL)){if(e.getArguments().addChild(this._parseFunctionArgument()))for(;this.accept(r.TokenType.Comma)&&!this.peek(r.TokenType.ParenthesisR);)if(!e.getArguments().addChild(this._parseFunctionArgument()))return this.finish(e,a.ParseError.ExpressionExpected);if(!this.accept(r.TokenType.ParenthesisR))return this.finish(e,a.ParseError.RightParenthesisExpected)}return(this.peekIdent("using")||this.peek(r.TokenType.CurlyL))&&e.setContent(this._parseMixinContentDeclaration()),this.finish(e)},t.prototype._parseMixinContentDeclaration=function(){var e=this.create(o.MixinContentDeclaration);if(this.acceptIdent("using")){if(!this.accept(r.TokenType.ParenthesisL))return this.finish(e,a.ParseError.LeftParenthesisExpected,[r.TokenType.CurlyL]);if(e.getParameters().addChild(this._parseParameterDeclaration()))for(;this.accept(r.TokenType.Comma)&&!this.peek(r.TokenType.ParenthesisR);)if(!e.getParameters().addChild(this._parseParameterDeclaration()))return this.finish(e,a.ParseError.VariableNameExpected);if(!this.accept(r.TokenType.ParenthesisR))return this.finish(e,a.ParseError.RightParenthesisExpected,[r.TokenType.CurlyL])}return this.peek(r.TokenType.CurlyL)&&this._parseBody(e,this._parseMixinReferenceBodyStatement.bind(this)),this.finish(e)},t.prototype._parseMixinReferenceBodyStatement=function(){return this._tryParseKeyframeSelector()||this._parseRuleSetDeclaration()},t.prototype._parseFunctionArgument=function(){var e=this.create(o.FunctionArgument),t=this.mark(),i=this._parseVariable();if(i)if(this.accept(r.TokenType.Colon))e.setIdentifier(i);else{if(this.accept(n.Ellipsis))return e.setValue(i),this.finish(e);this.restoreAtMark(t)}return e.setValue(this._parseExpr(!0))?(this.accept(n.Ellipsis),e.addChild(this._parsePrio()),this.finish(e)):e.setValue(this._tryParsePrio())?this.finish(e):null},t.prototype._parseURLArgument=function(){var t=this.mark(),n=e.prototype._parseURLArgument.call(this);if(!n||!this.peek(r.TokenType.ParenthesisR)){this.restoreAtMark(t);var i=this.create(o.Node);return i.addChild(this._parseBinaryExpr()),this.finish(i)}return n},t.prototype._parseOperation=function(){if(!this.peek(r.TokenType.ParenthesisL))return null;var e=this.create(o.Node);for(this.consumeToken();e.addChild(this._parseListElement());)this.accept(r.TokenType.Comma);return this.accept(r.TokenType.ParenthesisR)?this.finish(e):this.finish(e,a.ParseError.RightParenthesisExpected)},t.prototype._parseListElement=function(){var e=this.create(o.ListEntry),t=this._parseBinaryExpr();if(!t)return null;if(this.accept(r.TokenType.Colon)){if(e.setKey(t),!e.setValue(this._parseBinaryExpr()))return this.finish(e,a.ParseError.ExpressionExpected)}else e.setValue(t);return this.finish(e)},t.prototype._parseUse=function(){if(!this.peekKeyword("@use"))return null;var e=this.create(o.Use);if(this.consumeToken(),!e.addChild(this._parseStringLiteral()))return this.finish(e,a.ParseError.StringLiteralExpected);if(!this.peek(r.TokenType.SemiColon)&&!this.peek(r.TokenType.EOF)){if(!this.peekRegExp(r.TokenType.Ident,/as|with/))return this.finish(e,a.ParseError.UnknownKeyword);if(this.acceptIdent("as")&&!e.setIdentifier(this._parseIdent([o.ReferenceType.Module]))&&!this.acceptDelim("*"))return this.finish(e,a.ParseError.IdentifierOrWildcardExpected);if(this.acceptIdent("with")){if(!this.accept(r.TokenType.ParenthesisL))return this.finish(e,a.ParseError.LeftParenthesisExpected,[r.TokenType.ParenthesisR]);if(!e.getParameters().addChild(this._parseModuleConfigDeclaration()))return this.finish(e,a.ParseError.VariableNameExpected);for(;this.accept(r.TokenType.Comma)&&!this.peek(r.TokenType.ParenthesisR);)if(!e.getParameters().addChild(this._parseModuleConfigDeclaration()))return this.finish(e,a.ParseError.VariableNameExpected);if(!this.accept(r.TokenType.ParenthesisR))return this.finish(e,a.ParseError.RightParenthesisExpected)}}return this.accept(r.TokenType.SemiColon)||this.accept(r.TokenType.EOF)?this.finish(e):this.finish(e,a.ParseError.SemiColonExpected)},t.prototype._parseModuleConfigDeclaration=function(){var e=this.create(o.ModuleConfiguration);return e.setIdentifier(this._parseVariable())?this.accept(r.TokenType.Colon)&&e.setValue(this._parseExpr(!0))?!this.accept(r.TokenType.Exclamation)||!this.hasWhitespace()&&this.acceptIdent("default")?this.finish(e):this.finish(e,a.ParseError.UnknownKeyword):this.finish(e,a.ParseError.VariableValueExpected,[],[r.TokenType.Comma,r.TokenType.ParenthesisR]):null},t.prototype._parseForward=function(){if(!this.peekKeyword("@forward"))return null;var e=this.create(o.Forward);if(this.consumeToken(),!e.addChild(this._parseStringLiteral()))return this.finish(e,a.ParseError.StringLiteralExpected);if(this.acceptIdent("with")){if(!this.accept(r.TokenType.ParenthesisL))return this.finish(e,a.ParseError.LeftParenthesisExpected,[r.TokenType.ParenthesisR]);if(!e.getParameters().addChild(this._parseModuleConfigDeclaration()))return this.finish(e,a.ParseError.VariableNameExpected);for(;this.accept(r.TokenType.Comma)&&!this.peek(r.TokenType.ParenthesisR);)if(!e.getParameters().addChild(this._parseModuleConfigDeclaration()))return this.finish(e,a.ParseError.VariableNameExpected);if(!this.accept(r.TokenType.ParenthesisR))return this.finish(e,a.ParseError.RightParenthesisExpected)}if(!this.peek(r.TokenType.SemiColon)&&!this.peek(r.TokenType.EOF)){if(!this.peekRegExp(r.TokenType.Ident,/as|hide|show/))return this.finish(e,a.ParseError.UnknownKeyword);if(this.acceptIdent("as")){var t=this._parseIdent([o.ReferenceType.Forward]);if(!e.setIdentifier(t))return this.finish(e,a.ParseError.IdentifierExpected);if(this.hasWhitespace()||!this.acceptDelim("*"))return this.finish(e,a.ParseError.WildcardExpected)}if((this.peekIdent("hide")||this.peekIdent("show"))&&!e.addChild(this._parseForwardVisibility()))return this.finish(e,a.ParseError.IdentifierOrVariableExpected)}return this.accept(r.TokenType.SemiColon)||this.accept(r.TokenType.EOF)?this.finish(e):this.finish(e,a.ParseError.SemiColonExpected)},t.prototype._parseForwardVisibility=function(){var e=this.create(o.ForwardVisibility);for(e.setIdentifier(this._parseIdent());e.addChild(this._parseVariable()||this._parseIdent());)this.accept(r.TokenType.Comma);return e.getChildren().length>1?e:null},t.prototype._parseSupportsCondition=function(){return this._parseInterpolation()||e.prototype._parseSupportsCondition.call(this)},t}(i.Parser);t.SCSSParser=l}));__extends=this&&this.__extends||function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();!function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/services/scssCompletion",["require","exports","./cssCompletion","../parser/cssNodes","../cssLanguageTypes","vscode-nls"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.SCSSCompletion=void 0;var n=e("./cssCompletion"),r=e("../parser/cssNodes"),i=e("../cssLanguageTypes"),o=e("vscode-nls").loadMessageBundle(),s=function(e){function t(n,r){var i=e.call(this,"$",n,r)||this;return a(t.scssModuleLoaders),a(t.scssModuleBuiltIns),i}return __extends(t,e),t.prototype.isImportPathParent=function(t){return t===r.NodeType.Forward||t===r.NodeType.Use||e.prototype.isImportPathParent.call(this,t)},t.prototype.getCompletionForImportPath=function(n,o){var s=n.getParent().type;if(s===r.NodeType.Forward||s===r.NodeType.Use)for(var a=0,l=t.scssModuleBuiltIns;a0){var t="string"==typeof e.documentation?{kind:"markdown",value:e.documentation}:{kind:"markdown",value:e.documentation.value};t.value+="\n\n",t.value+=e.references.map((function(e){return"["+e.name+"]("+e.url+")"})).join(" | "),e.documentation=t}}))}t.SCSSCompletion=s}));__extends=this&&this.__extends||function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();!function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/parser/lessScanner",["require","exports","./cssScanner"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.LESSScanner=t.Ellipsis=void 0;var n=e("./cssScanner"),r="/".charCodeAt(0),i="\n".charCodeAt(0),o="\r".charCodeAt(0),s="\f".charCodeAt(0),a="`".charCodeAt(0),l=".".charCodeAt(0),c=n.TokenType.CustomToken;t.Ellipsis=c++;var d=function(e){function c(){return null!==e&&e.apply(this,arguments)||this}return __extends(c,e),c.prototype.scanNext=function(n){var r=this.escapedJavaScript();return null!==r?this.finishToken(n,r):this.stream.advanceIfChars([l,l,l])?this.finishToken(n,t.Ellipsis):e.prototype.scanNext.call(this,n)},c.prototype.comment=function(){return!!e.prototype.comment.call(this)||!(this.inURL||!this.stream.advanceIfChars([r,r]))&&(this.stream.advanceWhileChar((function(e){switch(e){case i:case o:case s:return!1;default:return!0}})),!0)},c.prototype.escapedJavaScript=function(){return this.stream.peekChar()===a?(this.stream.advance(1),this.stream.advanceWhileChar((function(e){return e!==a})),this.stream.advanceIfChar(a)?n.TokenType.EscapedJavaScript:n.TokenType.BadEscapedJavaScript):null},c}(n.Scanner);t.LESSScanner=d}));__extends=this&&this.__extends||function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();!function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/parser/lessParser",["require","exports","./lessScanner","./cssScanner","./cssParser","./cssNodes","./cssErrors"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.LESSParser=void 0;var n=e("./lessScanner"),r=e("./cssScanner"),i=e("./cssParser"),o=e("./cssNodes"),s=e("./cssErrors"),a=function(e){function t(){return e.call(this,new n.LESSScanner)||this}return __extends(t,e),t.prototype._parseStylesheetStatement=function(t){return void 0===t&&(t=!1),this.peek(r.TokenType.AtKeyword)?this._parseVariableDeclaration()||this._parsePlugin()||e.prototype._parseStylesheetAtStatement.call(this,t):this._tryParseMixinDeclaration()||this._tryParseMixinReference()||this._parseFunction()||this._parseRuleset(!0)},t.prototype._parseImport=function(){if(!this.peekKeyword("@import")&&!this.peekKeyword("@import-once"))return null;var e=this.create(o.Import);if(this.consumeToken(),this.accept(r.TokenType.ParenthesisL)){if(!this.accept(r.TokenType.Ident))return this.finish(e,s.ParseError.IdentifierExpected,[r.TokenType.SemiColon]);do{if(!this.accept(r.TokenType.Comma))break}while(this.accept(r.TokenType.Ident));if(!this.accept(r.TokenType.ParenthesisR))return this.finish(e,s.ParseError.RightParenthesisExpected,[r.TokenType.SemiColon])}return e.addChild(this._parseURILiteral())||e.addChild(this._parseStringLiteral())?(this.peek(r.TokenType.SemiColon)||this.peek(r.TokenType.EOF)||e.setMedialist(this._parseMediaQueryList()),this.finish(e)):this.finish(e,s.ParseError.URIOrStringExpected,[r.TokenType.SemiColon])},t.prototype._parsePlugin=function(){if(!this.peekKeyword("@plugin"))return null;var e=this.createNode(o.NodeType.Plugin);return this.consumeToken(),e.addChild(this._parseStringLiteral())?this.accept(r.TokenType.SemiColon)?this.finish(e):this.finish(e,s.ParseError.SemiColonExpected):this.finish(e,s.ParseError.StringLiteralExpected)},t.prototype._parseMediaQuery=function(t){var n=e.prototype._parseMediaQuery.call(this,t);if(!n){var r=this.create(o.MediaQuery);return r.addChild(this._parseVariable())?this.finish(r):null}return n},t.prototype._parseMediaDeclaration=function(e){return void 0===e&&(e=!1),this._tryParseRuleset(e)||this._tryToParseDeclaration()||this._tryParseMixinDeclaration()||this._tryParseMixinReference()||this._parseDetachedRuleSetMixin()||this._parseStylesheetStatement(e)},t.prototype._parseMediaFeatureName=function(){return this._parseIdent()||this._parseVariable()},t.prototype._parseVariableDeclaration=function(e){void 0===e&&(e=[]);var t=this.create(o.VariableDeclaration),n=this.mark();if(!t.setVariable(this._parseVariable(!0)))return null;if(!this.accept(r.TokenType.Colon))return this.restoreAtMark(n),null;if(this.prevToken&&(t.colonPosition=this.prevToken.offset),t.setValue(this._parseDetachedRuleSet()))t.needsSemicolon=!1;else if(!t.setValue(this._parseExpr()))return this.finish(t,s.ParseError.VariableValueExpected,[],e);return t.addChild(this._parsePrio()),this.peek(r.TokenType.SemiColon)&&(t.semicolonPosition=this.token.offset),this.finish(t)},t.prototype._parseDetachedRuleSet=function(){var e=this.mark();if(this.peekDelim("#")||this.peekDelim(".")){if(this.consumeToken(),this.hasWhitespace()||!this.accept(r.TokenType.ParenthesisL))return this.restoreAtMark(e),null;var t=this.create(o.MixinDeclaration);if(t.getParameters().addChild(this._parseMixinParameter()))for(;(this.accept(r.TokenType.Comma)||this.accept(r.TokenType.SemiColon))&&!this.peek(r.TokenType.ParenthesisR);)t.getParameters().addChild(this._parseMixinParameter())||this.markError(t,s.ParseError.IdentifierExpected,[],[r.TokenType.ParenthesisR]);if(!this.accept(r.TokenType.ParenthesisR))return this.restoreAtMark(e),null}if(!this.peek(r.TokenType.CurlyL))return null;var n=this.create(o.BodyDeclaration);return this._parseBody(n,this._parseDetachedRuleSetBody.bind(this)),this.finish(n)},t.prototype._parseDetachedRuleSetBody=function(){return this._tryParseKeyframeSelector()||this._parseRuleSetDeclaration()},t.prototype._addLookupChildren=function(e){if(!e.addChild(this._parseLookupValue()))return!1;for(var t=!1;this.peek(r.TokenType.BracketL)&&(t=!0),e.addChild(this._parseLookupValue());)t=!1;return!t},t.prototype._parseLookupValue=function(){var e=this.create(o.Node),t=this.mark();return this.accept(r.TokenType.BracketL)&&((e.addChild(this._parseVariable(!1,!0))||e.addChild(this._parsePropertyIdentifier()))&&this.accept(r.TokenType.BracketR)||this.accept(r.TokenType.BracketR))?e:(this.restoreAtMark(t),null)},t.prototype._parseVariable=function(e,t){void 0===e&&(e=!1),void 0===t&&(t=!1);var n=!e&&this.peekDelim("$");if(!this.peekDelim("@")&&!n&&!this.peek(r.TokenType.AtKeyword))return null;for(var i=this.create(o.Variable),s=this.mark();this.acceptDelim("@")||!e&&this.acceptDelim("$");)if(this.hasWhitespace())return this.restoreAtMark(s),null;return(this.accept(r.TokenType.AtKeyword)||this.accept(r.TokenType.Ident))&&(t||!this.peek(r.TokenType.BracketL)||this._addLookupChildren(i))?i:(this.restoreAtMark(s),null)},t.prototype._parseTermExpression=function(){return this._parseVariable()||this._parseEscaped()||e.prototype._parseTermExpression.call(this)||this._tryParseMixinReference(!1)},t.prototype._parseEscaped=function(){if(this.peek(r.TokenType.EscapedJavaScript)||this.peek(r.TokenType.BadEscapedJavaScript)){var e=this.createNode(o.NodeType.EscapedValue);return this.consumeToken(),this.finish(e)}if(this.peekDelim("~")){e=this.createNode(o.NodeType.EscapedValue);return this.consumeToken(),this.accept(r.TokenType.String)||this.accept(r.TokenType.EscapedJavaScript)?this.finish(e):this.finish(e,s.ParseError.TermExpected)}return null},t.prototype._parseOperator=function(){var t=this._parseGuardOperator();return t||e.prototype._parseOperator.call(this)},t.prototype._parseGuardOperator=function(){if(this.peekDelim(">")){var e=this.createNode(o.NodeType.Operator);return this.consumeToken(),this.acceptDelim("="),e}if(this.peekDelim("=")){e=this.createNode(o.NodeType.Operator);return this.consumeToken(),this.acceptDelim("<"),e}if(this.peekDelim("<")){e=this.createNode(o.NodeType.Operator);return this.consumeToken(),this.acceptDelim("="),e}return null},t.prototype._parseRuleSetDeclaration=function(){return this.peek(r.TokenType.AtKeyword)?this._parseKeyframe()||this._parseMedia(!0)||this._parseImport()||this._parseSupports(!0)||this._parseDetachedRuleSetMixin()||this._parseVariableDeclaration()||e.prototype._parseRuleSetDeclarationAtStatement.call(this):this._tryParseMixinDeclaration()||this._tryParseRuleset(!0)||this._tryParseMixinReference()||this._parseFunction()||this._parseExtend()||e.prototype._parseRuleSetDeclaration.call(this)},t.prototype._parseKeyframeIdent=function(){return this._parseIdent([o.ReferenceType.Keyframe])||this._parseVariable()},t.prototype._parseKeyframeSelector=function(){return this._parseDetachedRuleSetMixin()||e.prototype._parseKeyframeSelector.call(this)},t.prototype._parseSimpleSelectorBody=function(){return this._parseSelectorCombinator()||e.prototype._parseSimpleSelectorBody.call(this)},t.prototype._parseSelector=function(e){var t=this.create(o.Selector),n=!1;for(e&&(n=t.addChild(this._parseCombinator()));t.addChild(this._parseSimpleSelector());){n=!0;var i=this.mark();if(t.addChild(this._parseGuard())&&this.peek(r.TokenType.CurlyL))break;this.restoreAtMark(i),t.addChild(this._parseCombinator())}return n?this.finish(t):null},t.prototype._parseSelectorCombinator=function(){if(this.peekDelim("&")){var e=this.createNode(o.NodeType.SelectorCombinator);for(this.consumeToken();!this.hasWhitespace()&&(this.acceptDelim("-")||this.accept(r.TokenType.Num)||this.accept(r.TokenType.Dimension)||e.addChild(this._parseIdent())||this.acceptDelim("&")););return this.finish(e)}return null},t.prototype._parseSelectorIdent=function(){if(!this.peekInterpolatedIdent())return null;var e=this.createNode(o.NodeType.SelectorInterpolation);return this._acceptInterpolatedIdent(e)?this.finish(e):null},t.prototype._parsePropertyIdentifier=function(e){void 0===e&&(e=!1);var t=/^[\w-]+/;if(!this.peekInterpolatedIdent()&&!this.peekRegExp(this.token.type,t))return null;var n=this.mark(),r=this.create(o.Identifier);r.isCustomProperty=this.acceptDelim("-")&&this.acceptDelim("-");return(e?r.isCustomProperty?r.addChild(this._parseIdent()):r.addChild(this._parseRegexp(t)):r.isCustomProperty?this._acceptInterpolatedIdent(r):this._acceptInterpolatedIdent(r,t))?(e||this.hasWhitespace()||(this.acceptDelim("+"),this.hasWhitespace()||this.acceptIdent("_")),this.finish(r)):(this.restoreAtMark(n),null)},t.prototype.peekInterpolatedIdent=function(){return this.peek(r.TokenType.Ident)||this.peekDelim("@")||this.peekDelim("$")||this.peekDelim("-")},t.prototype._acceptInterpolatedIdent=function(e,t){for(var n=this,i=!1,o=function(){var e=n.mark();return n.acceptDelim("-")&&(n.hasWhitespace()||n.acceptDelim("-"),n.hasWhitespace())?(n.restoreAtMark(e),null):n._parseInterpolation()},s=t?function(){return n.acceptRegexp(t)}:function(){return n.accept(r.TokenType.Ident)};(s()||e.addChild(this._parseInterpolation()||this.try(o)))&&(i=!0,!this.hasWhitespace()););return i},t.prototype._parseInterpolation=function(){var e=this.mark();if(this.peekDelim("@")||this.peekDelim("$")){var t=this.createNode(o.NodeType.Interpolation);return this.consumeToken(),this.hasWhitespace()||!this.accept(r.TokenType.CurlyL)?(this.restoreAtMark(e),null):t.addChild(this._parseIdent())?this.accept(r.TokenType.CurlyR)?this.finish(t):this.finish(t,s.ParseError.RightCurlyExpected):this.finish(t,s.ParseError.IdentifierExpected)}return null},t.prototype._tryParseMixinDeclaration=function(){var e=this.mark(),t=this.create(o.MixinDeclaration);if(!t.setIdentifier(this._parseMixinDeclarationIdentifier())||!this.accept(r.TokenType.ParenthesisL))return this.restoreAtMark(e),null;if(t.getParameters().addChild(this._parseMixinParameter()))for(;(this.accept(r.TokenType.Comma)||this.accept(r.TokenType.SemiColon))&&!this.peek(r.TokenType.ParenthesisR);)t.getParameters().addChild(this._parseMixinParameter())||this.markError(t,s.ParseError.IdentifierExpected,[],[r.TokenType.ParenthesisR]);return this.accept(r.TokenType.ParenthesisR)?(t.setGuard(this._parseGuard()),this.peek(r.TokenType.CurlyL)?this._parseBody(t,this._parseMixInBodyDeclaration.bind(this)):(this.restoreAtMark(e),null)):(this.restoreAtMark(e),null)},t.prototype._parseMixInBodyDeclaration=function(){return this._parseFontFace()||this._parseRuleSetDeclaration()},t.prototype._parseMixinDeclarationIdentifier=function(){var e;if(this.peekDelim("#")||this.peekDelim(".")){if(e=this.create(o.Identifier),this.consumeToken(),this.hasWhitespace()||!e.addChild(this._parseIdent()))return null}else{if(!this.peek(r.TokenType.Hash))return null;e=this.create(o.Identifier),this.consumeToken()}return e.referenceTypes=[o.ReferenceType.Mixin],this.finish(e)},t.prototype._parsePseudo=function(){if(!this.peek(r.TokenType.Colon))return null;var t=this.mark(),n=this.create(o.ExtendsReference);return this.consumeToken(),this.acceptIdent("extend")?this._completeExtends(n):(this.restoreAtMark(t),e.prototype._parsePseudo.call(this))},t.prototype._parseExtend=function(){if(!this.peekDelim("&"))return null;var e=this.mark(),t=this.create(o.ExtendsReference);return this.consumeToken(),!this.hasWhitespace()&&this.accept(r.TokenType.Colon)&&this.acceptIdent("extend")?this._completeExtends(t):(this.restoreAtMark(e),null)},t.prototype._completeExtends=function(e){if(!this.accept(r.TokenType.ParenthesisL))return this.finish(e,s.ParseError.LeftParenthesisExpected);var t=e.getSelectors();if(!t.addChild(this._parseSelector(!0)))return this.finish(e,s.ParseError.SelectorExpected);for(;this.accept(r.TokenType.Comma);)if(!t.addChild(this._parseSelector(!0)))return this.finish(e,s.ParseError.SelectorExpected);return this.accept(r.TokenType.ParenthesisR)?this.finish(e):this.finish(e,s.ParseError.RightParenthesisExpected)},t.prototype._parseDetachedRuleSetMixin=function(){if(!this.peek(r.TokenType.AtKeyword))return null;var e=this.mark(),t=this.create(o.MixinReference);return!t.addChild(this._parseVariable(!0))||!this.hasWhitespace()&&this.accept(r.TokenType.ParenthesisL)?this.accept(r.TokenType.ParenthesisR)?this.finish(t):this.finish(t,s.ParseError.RightParenthesisExpected):(this.restoreAtMark(e),null)},t.prototype._tryParseMixinReference=function(e){void 0===e&&(e=!0);for(var t=this.mark(),n=this.create(o.MixinReference),i=this._parseMixinDeclarationIdentifier();i;){this.acceptDelim(">");var a=this._parseMixinDeclarationIdentifier();if(!a)break;n.getNamespaces().addChild(i),i=a}if(!n.setIdentifier(i))return this.restoreAtMark(t),null;var l=!1;if(this.accept(r.TokenType.ParenthesisL)){if(l=!0,n.getArguments().addChild(this._parseMixinArgument()))for(;(this.accept(r.TokenType.Comma)||this.accept(r.TokenType.SemiColon))&&!this.peek(r.TokenType.ParenthesisR);)if(!n.getArguments().addChild(this._parseMixinArgument()))return this.finish(n,s.ParseError.ExpressionExpected);if(!this.accept(r.TokenType.ParenthesisR))return this.finish(n,s.ParseError.RightParenthesisExpected);i.referenceTypes=[o.ReferenceType.Mixin]}else i.referenceTypes=[o.ReferenceType.Mixin,o.ReferenceType.Rule];return this.peek(r.TokenType.BracketL)?e||this._addLookupChildren(n):n.addChild(this._parsePrio()),l||this.peek(r.TokenType.SemiColon)||this.peek(r.TokenType.CurlyR)||this.peek(r.TokenType.EOF)?this.finish(n):(this.restoreAtMark(t),null)},t.prototype._parseMixinArgument=function(){var e=this.create(o.FunctionArgument),t=this.mark(),n=this._parseVariable();return n&&(this.accept(r.TokenType.Colon)?e.setIdentifier(n):this.restoreAtMark(t)),e.setValue(this._parseDetachedRuleSet()||this._parseExpr(!0))?this.finish(e):(this.restoreAtMark(t),null)},t.prototype._parseMixinParameter=function(){var e=this.create(o.FunctionParameter);if(this.peekKeyword("@rest")){var t=this.create(o.Node);return this.consumeToken(),this.accept(n.Ellipsis)?(e.setIdentifier(this.finish(t)),this.finish(e)):this.finish(e,s.ParseError.DotExpected,[],[r.TokenType.Comma,r.TokenType.ParenthesisR])}if(this.peek(n.Ellipsis)){var i=this.create(o.Node);return this.consumeToken(),e.setIdentifier(this.finish(i)),this.finish(e)}var a=!1;return e.setIdentifier(this._parseVariable())&&(this.accept(r.TokenType.Colon),a=!0),e.setDefaultValue(this._parseDetachedRuleSet()||this._parseExpr(!0))||a?this.finish(e):null},t.prototype._parseGuard=function(){if(!this.peekIdent("when"))return null;var e=this.create(o.LessGuard);if(this.consumeToken(),e.isNegated=this.acceptIdent("not"),!e.getConditions().addChild(this._parseGuardCondition()))return this.finish(e,s.ParseError.ConditionExpected);for(;this.acceptIdent("and")||this.accept(r.TokenType.Comma);)if(!e.getConditions().addChild(this._parseGuardCondition()))return this.finish(e,s.ParseError.ConditionExpected);return this.finish(e)},t.prototype._parseGuardCondition=function(){if(!this.peek(r.TokenType.ParenthesisL))return null;var e=this.create(o.GuardCondition);return this.consumeToken(),e.addChild(this._parseExpr()),this.accept(r.TokenType.ParenthesisR)?this.finish(e):this.finish(e,s.ParseError.RightParenthesisExpected)},t.prototype._parseFunction=function(){var e=this.mark(),t=this.create(o.Function);if(!t.setIdentifier(this._parseFunctionIdentifier()))return null;if(this.hasWhitespace()||!this.accept(r.TokenType.ParenthesisL))return this.restoreAtMark(e),null;if(t.getArguments().addChild(this._parseMixinArgument()))for(;(this.accept(r.TokenType.Comma)||this.accept(r.TokenType.SemiColon))&&!this.peek(r.TokenType.ParenthesisR);)if(!t.getArguments().addChild(this._parseMixinArgument()))return this.finish(t,s.ParseError.ExpressionExpected);return this.accept(r.TokenType.ParenthesisR)?this.finish(t):this.finish(t,s.ParseError.RightParenthesisExpected)},t.prototype._parseFunctionIdentifier=function(){if(this.peekDelim("%")){var t=this.create(o.Identifier);return t.referenceTypes=[o.ReferenceType.Function],this.consumeToken(),this.finish(t)}return e.prototype._parseFunctionIdentifier.call(this)},t.prototype._parseURLArgument=function(){var t=this.mark(),n=e.prototype._parseURLArgument.call(this);if(!n||!this.peek(r.TokenType.ParenthesisR)){this.restoreAtMark(t);var i=this.create(o.Node);return i.addChild(this._parseBinaryExpr()),this.finish(i)}return n},t}(i.Parser);t.LESSParser=a}));__extends=this&&this.__extends||function(){var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)Object.prototype.hasOwnProperty.call(t,n)&&(e[n]=t[n])})(t,n)};return function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Class extends value "+String(n)+" is not a constructor or null");function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}}();!function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/services/lessCompletion",["require","exports","./cssCompletion","../cssLanguageTypes","vscode-nls"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.LESSCompletion=void 0;var n=e("./cssCompletion"),r=e("../cssLanguageTypes"),i=e("vscode-nls").loadMessageBundle(),o=function(e){function t(t,n){return e.call(this,"@",t,n)||this}return __extends(t,e),t.prototype.createFunctionProposals=function(e,t,n,i){for(var o=0,s=e;o 50%"),example:"percentage(@number);",type:"percentage"},{name:"round",description:i("less.builtin.round","rounds a number to a number of places"),example:"round(number, [places: 0]);"},{name:"sqrt",description:i("less.builtin.sqrt","calculates square root of a number"),example:"sqrt(number);"},{name:"sin",description:i("less.builtin.sin","sine function"),example:"sin(number);"},{name:"tan",description:i("less.builtin.tan","tangent function"),example:"tan(number);"},{name:"atan",description:i("less.builtin.atan","arctangent - inverse of tangent function"),example:"atan(number);"},{name:"pi",description:i("less.builtin.pi","returns pi"),example:"pi();"},{name:"pow",description:i("less.builtin.pow","first argument raised to the power of the second argument"),example:"pow(@base, @exponent);"},{name:"mod",description:i("less.builtin.mod","first argument modulus second argument"),example:"mod(number, number);"},{name:"min",description:i("less.builtin.min","returns the lowest of one or more values"),example:"min(@x, @y);"},{name:"max",description:i("less.builtin.max","returns the lowest of one or more values"),example:"max(@x, @y);"}],t.colorProposals=[{name:"argb",example:"argb(@color);",description:i("less.builtin.argb","creates a #AARRGGBB")},{name:"hsl",example:"hsl(@hue, @saturation, @lightness);",description:i("less.builtin.hsl","creates a color")},{name:"hsla",example:"hsla(@hue, @saturation, @lightness, @alpha);",description:i("less.builtin.hsla","creates a color")},{name:"hsv",example:"hsv(@hue, @saturation, @value);",description:i("less.builtin.hsv","creates a color")},{name:"hsva",example:"hsva(@hue, @saturation, @value, @alpha);",description:i("less.builtin.hsva","creates a color")},{name:"hue",example:"hue(@color);",description:i("less.builtin.hue","returns the `hue` channel of `@color` in the HSL space")},{name:"saturation",example:"saturation(@color);",description:i("less.builtin.saturation","returns the `saturation` channel of `@color` in the HSL space")},{name:"lightness",example:"lightness(@color);",description:i("less.builtin.lightness","returns the `lightness` channel of `@color` in the HSL space")},{name:"hsvhue",example:"hsvhue(@color);",description:i("less.builtin.hsvhue","returns the `hue` channel of `@color` in the HSV space")},{name:"hsvsaturation",example:"hsvsaturation(@color);",description:i("less.builtin.hsvsaturation","returns the `saturation` channel of `@color` in the HSV space")},{name:"hsvvalue",example:"hsvvalue(@color);",description:i("less.builtin.hsvvalue","returns the `value` channel of `@color` in the HSV space")},{name:"red",example:"red(@color);",description:i("less.builtin.red","returns the `red` channel of `@color`")},{name:"green",example:"green(@color);",description:i("less.builtin.green","returns the `green` channel of `@color`")},{name:"blue",example:"blue(@color);",description:i("less.builtin.blue","returns the `blue` channel of `@color`")},{name:"alpha",example:"alpha(@color);",description:i("less.builtin.alpha","returns the `alpha` channel of `@color`")},{name:"luma",example:"luma(@color);",description:i("less.builtin.luma","returns the `luma` value (perceptual brightness) of `@color`")},{name:"saturate",example:"saturate(@color, 10%);",description:i("less.builtin.saturate","return `@color` 10% points more saturated")},{name:"desaturate",example:"desaturate(@color, 10%);",description:i("less.builtin.desaturate","return `@color` 10% points less saturated")},{name:"lighten",example:"lighten(@color, 10%);",description:i("less.builtin.lighten","return `@color` 10% points lighter")},{name:"darken",example:"darken(@color, 10%);",description:i("less.builtin.darken","return `@color` 10% points darker")},{name:"fadein",example:"fadein(@color, 10%);",description:i("less.builtin.fadein","return `@color` 10% points less transparent")},{name:"fadeout",example:"fadeout(@color, 10%);",description:i("less.builtin.fadeout","return `@color` 10% points more transparent")},{name:"fade",example:"fade(@color, 50%);",description:i("less.builtin.fade","return `@color` with 50% transparency")},{name:"spin",example:"spin(@color, 10);",description:i("less.builtin.spin","return `@color` with a 10 degree larger in hue")},{name:"mix",example:"mix(@color1, @color2, [@weight: 50%]);",description:i("less.builtin.mix","return a mix of `@color1` and `@color2`")},{name:"greyscale",example:"greyscale(@color);",description:i("less.builtin.greyscale","returns a grey, 100% desaturated color")},{name:"contrast",example:"contrast(@color1, [@darkcolor: black], [@lightcolor: white], [@threshold: 43%]);",description:i("less.builtin.contrast","return `@darkcolor` if `@color1 is> 43% luma` otherwise return `@lightcolor`, see notes")},{name:"multiply",example:"multiply(@color1, @color2);"},{name:"screen",example:"screen(@color1, @color2);"},{name:"overlay",example:"overlay(@color1, @color2);"},{name:"softlight",example:"softlight(@color1, @color2);"},{name:"hardlight",example:"hardlight(@color1, @color2);"},{name:"difference",example:"difference(@color1, @color2);"},{name:"exclusion",example:"exclusion(@color1, @color2);"},{name:"average",example:"average(@color1, @color2);"},{name:"negation",example:"negation(@color1, @color2);"}],t}(n.CSSCompletion);t.LESSCompletion=o})),function(e){if("object"==typeof module&&"object"==typeof module.exports){var t=e(require,exports);void 0!==t&&(module.exports=t)}else"function"==typeof define&&define.amd&&define("vscode-css-languageservice/services/cssFolding",["require","exports","../parser/cssScanner","../parser/scssScanner","../parser/lessScanner"],e)}((function(e,t){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getFoldingRanges=void 0;var n=e("../parser/cssScanner"),r=e("../parser/scssScanner"),i=e("../parser/lessScanner");function o(e,t){if(0===e.length)return null;for(var n=e.length-1;n>=0;n--)if(e[n].type===t&&e[n].isStart)return e.splice(n,1)[0];return null}t.getFoldingRanges=function(e,t){return function(e,t){var n=t&&t.rangeLimit||Number.MAX_VALUE,r=e.sort((function(e,t){var n=e.startLine-t.startLine;return 0===n&&(n=e.endLine-t.endLine),n})),i=[],o=-1;return r.forEach((function(e){e.startLine && ]#",relevance:50,description:"@counter-style descriptor. Specifies the symbols used by the marker-construction algorithm specified by the system descriptor. Needs to be specified if the counter system is 'additive'.",restrictions:["integer","string","image","identifier"]},{name:"align-content",values:[{name:"center",description:"Lines are packed toward the center of the flex container."},{name:"flex-end",description:"Lines are packed toward the end of the flex container."},{name:"flex-start",description:"Lines are packed toward the start of the flex container."},{name:"space-around",description:"Lines are evenly distributed in the flex container, with half-size spaces on either end."},{name:"space-between",description:"Lines are evenly distributed in the flex container."},{name:"stretch",description:"Lines stretch to take up the remaining space."}],syntax:"normal | | | ? ",relevance:60,description:"Aligns a flex container’s lines within the flex container when there is extra space in the cross-axis, similar to how 'justify-content' aligns individual items within the main-axis.",restrictions:["enum"]},{name:"align-items",values:[{name:"baseline",description:"If the flex item’s inline axis is the same as the cross axis, this value is identical to 'flex-start'. Otherwise, it participates in baseline alignment."},{name:"center",description:"The flex item’s margin box is centered in the cross axis within the line."},{name:"flex-end",description:"The cross-end margin edge of the flex item is placed flush with the cross-end edge of the line."},{name:"flex-start",description:"The cross-start margin edge of the flex item is placed flush with the cross-start edge of the line."},{name:"stretch",description:"If the cross size property of the flex item computes to auto, and neither of the cross-axis margins are auto, the flex item is stretched."}],syntax:"normal | stretch | | [ ? ]",relevance:83,description:"Aligns flex items along the cross axis of the current line of the flex container.",restrictions:["enum"]},{name:"justify-items",values:[{name:"auto"},{name:"normal"},{name:"end"},{name:"start"},{name:"flex-end",description:'"Flex items are packed toward the end of the line."'},{name:"flex-start",description:'"Flex items are packed toward the start of the line."'},{name:"self-end",description:"The item is packed flush to the edge of the alignment container of the end side of the item, in the appropriate axis."},{name:"self-start",description:"The item is packed flush to the edge of the alignment container of the start side of the item, in the appropriate axis.."},{name:"center",description:"The items are packed flush to each other toward the center of the of the alignment container."},{name:"left"},{name:"right"},{name:"baseline"},{name:"first baseline"},{name:"last baseline"},{name:"stretch",description:"If the cross size property of the flex item computes to auto, and neither of the cross-axis margins are auto, the flex item is stretched."},{name:"save"},{name:"unsave"},{name:"legacy"}],syntax:"normal | stretch | | ? [ | left | right ] | legacy | legacy && [ left | right | center ]",relevance:51,description:"Defines the default justify-self for all items of the box, giving them the default way of justifying each box along the appropriate axis",restrictions:["enum"]},{name:"justify-self",values:[{name:"auto"},{name:"normal"},{name:"end"},{name:"start"},{name:"flex-end",description:'"Flex items are packed toward the end of the line."'},{name:"flex-start",description:'"Flex items are packed toward the start of the line."'},{name:"self-end",description:"The item is packed flush to the edge of the alignment container of the end side of the item, in the appropriate axis."},{name:"self-start",description:"The item is packed flush to the edge of the alignment container of the start side of the item, in the appropriate axis.."},{name:"center",description:"The items are packed flush to each other toward the center of the of the alignment container."},{name:"left"},{name:"right"},{name:"baseline"},{name:"first baseline"},{name:"last baseline"},{name:"stretch",description:"If the cross size property of the flex item computes to auto, and neither of the cross-axis margins are auto, the flex item is stretched."},{name:"save"},{name:"unsave"}],syntax:"auto | normal | stretch | | ? [ | left | right ]",relevance:52,description:"Defines the way of justifying a box inside its container along the appropriate axis.",restrictions:["enum"]},{name:"align-self",values:[{name:"auto",description:"Computes to the value of 'align-items' on the element’s parent, or 'stretch' if the element has no parent. On absolutely positioned elements, it computes to itself."},{name:"baseline",description:"If the flex item’s inline axis is the same as the cross axis, this value is identical to 'flex-start'. Otherwise, it participates in baseline alignment."},{name:"center",description:"The flex item’s margin box is centered in the cross axis within the line."},{name:"flex-end",description:"The cross-end margin edge of the flex item is placed flush with the cross-end edge of the line."},{name:"flex-start",description:"The cross-start margin edge of the flex item is placed flush with the cross-start edge of the line."},{name:"stretch",description:"If the cross size property of the flex item computes to auto, and neither of the cross-axis margins are auto, the flex item is stretched."}],syntax:"auto | normal | stretch | | ? ",relevance:70,description:"Allows the default alignment along the cross axis to be overridden for individual flex items.",restrictions:["enum"]},{name:"all",browsers:["E79","FF27","S9.1","C37","O24"],values:[],syntax:"initial | inherit | unset | revert",relevance:52,references:[{name:"MDN Reference",url:"https://developer.mozilla.org/docs/Web/CSS/all"}],description:"Shorthand that resets all properties except 'direction' and 'unicode-bidi'.",restrictions:["enum"]},{name:"alt",browsers:["S9"],values:[],relevance:50,references:[{name:"MDN Reference",url:"https://developer.mozilla.org/docs/Web/CSS/alt"}],description:"Provides alternative text for assistive technology to replace the generated content of a ::before or ::after element.",restrictions:["string","enum"]},{name:"animation",values:[{name:"alternate",description:"The animation cycle iterations that are odd counts are played in the normal direction, and the animation cycle iterations that are even counts are played in a reverse direction."},{name:"alternate-reverse",description:"The animation cycle iterations that are odd counts are played in the reverse direction, and the animation cycle iterations that are even counts are played in a normal direction."},{name:"backwards",description:"The beginning property value (as defined in the first @keyframes at-rule) is applied before the animation is displayed, during the period defined by 'animation-delay'."},{name:"both",description:"Both forwards and backwards fill modes are applied."},{name:"forwards",description:"The final property value (as defined in the last @keyframes at-rule) is maintained after the animation completes."},{name:"infinite",description:"Causes the animation to repeat forever."},{name:"none",description:"No animation is performed"},{name:"normal",description:"Normal playback."},{name:"reverse",description:"All iterations of the animation are played in the reverse direction from the way they were specified."}],syntax:"#",relevance:80,references:[{name:"MDN Reference",url:"https://developer.mozilla.org/docs/Web/CSS/animation"}],description:"Shorthand property combines six of the animation properties into a single property.",restrictions:["time","timing-function","enum","identifier","number"]},{name:"animation-delay",syntax:"